diff options
3 files changed, 358 insertions, 20 deletions
| diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerKt.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerKt.kt new file mode 100644 index 000000000000..c1429335292f --- /dev/null +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerKt.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.system + +import android.app.ActivityManager + +/** Kotlin extensions for [ActivityManager] */ +object ActivityManagerKt { + +    /** +     * Returns `true` whether the app with the given package name has an activity at the top of the +     * most recent task; `false` otherwise +     */ +    fun ActivityManager.isInForeground(packageName: String): Boolean { +        val tasks: List<ActivityManager.RunningTaskInfo> = getRunningTasks(1) +        return tasks.isNotEmpty() && packageName == tasks[0].topActivity.packageName +    } +} diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt index 99267e8ee1c9..cccd3a482ef0 100644 --- a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt @@ -17,27 +17,28 @@  package com.android.systemui.camera  import android.app.ActivityManager -import android.app.ActivityManager.RunningTaskInfo  import android.app.ActivityOptions -import android.app.ActivityTaskManager +import android.app.IActivityTaskManager  import android.content.ContentResolver  import android.content.Context  import android.content.Intent  import android.content.pm.PackageManager  import android.content.pm.ResolveInfo -import android.os.AsyncTask  import android.os.RemoteException  import android.os.UserHandle  import android.util.Log  import android.view.WindowManager +import androidx.annotation.VisibleForTesting  import com.android.keyguard.KeyguardUpdateMonitor  import com.android.systemui.ActivityIntentHelper -import com.android.systemui.camera.CameraIntents.Companion.isSecureCameraIntent +import com.android.systemui.dagger.qualifiers.Main  import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.shared.system.ActivityManagerKt.isInForeground  import com.android.systemui.statusbar.StatusBarState  import com.android.systemui.statusbar.phone.CentralSurfaces  import com.android.systemui.statusbar.phone.PanelViewController  import com.android.systemui.statusbar.policy.KeyguardStateController +import java.util.concurrent.Executor  import javax.inject.Inject  /** @@ -52,8 +53,10 @@ class CameraGestureHelper @Inject constructor(      private val activityManager: ActivityManager,      private val activityStarter: ActivityStarter,      private val activityIntentHelper: ActivityIntentHelper, +    private val activityTaskManager: IActivityTaskManager,      private val cameraIntents: CameraIntentsWrapper,      private val contentResolver: ContentResolver, +    @Main private val uiExecutor: Executor,  ) {      /**       * Whether the camera application can be launched for the camera launch gesture. @@ -63,15 +66,15 @@ class CameraGestureHelper @Inject constructor(              return false          } -        val resolveInfo: ResolveInfo = packageManager.resolveActivityAsUser( +        val resolveInfo: ResolveInfo? = packageManager.resolveActivityAsUser(              getStartCameraIntent(),              PackageManager.MATCH_DEFAULT_ONLY,              KeyguardUpdateMonitor.getCurrentUser()          ) -        val resolvedPackage = resolveInfo.activityInfo?.packageName +        val resolvedPackage = resolveInfo?.activityInfo?.packageName          return (resolvedPackage != null &&                  (statusBarState != StatusBarState.SHADE || -                !isForegroundApp(resolvedPackage))) +                !activityManager.isInForeground(resolvedPackage)))      }      /** @@ -85,8 +88,8 @@ class CameraGestureHelper @Inject constructor(          val wouldLaunchResolverActivity = activityIntentHelper.wouldLaunchResolverActivity(              intent, KeyguardUpdateMonitor.getCurrentUser()          ) -        if (isSecureCameraIntent(intent) && !wouldLaunchResolverActivity) { -            AsyncTask.execute { +        if (CameraIntents.isSecureCameraIntent(intent) && !wouldLaunchResolverActivity) { +            uiExecutor.execute {                  // Normally an activity will set its requested rotation animation on its window.                  // However when launching an activity causes the orientation to change this is too                  // late. In these cases, the default animation is used. This doesn't look good for @@ -98,7 +101,7 @@ class CameraGestureHelper @Inject constructor(                  activityOptions.rotationAnimationHint =                      WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS                  try { -                    ActivityTaskManager.getService().startActivityAsUser( +                    activityTaskManager.startActivityAsUser(                          null,                          context.basePackageName,                          context.attributionTag, @@ -148,16 +151,8 @@ class CameraGestureHelper @Inject constructor(          }      } -    /** -     * Returns `true` if the application with the given package name is running in the foreground; -     * `false` otherwise -     */ -    private fun isForegroundApp(packageName: String): Boolean { -        val tasks: List<RunningTaskInfo> = activityManager.getRunningTasks(1) -        return tasks.isNotEmpty() && packageName == tasks[0].topActivity.packageName -    } -      companion object { -        private const val EXTRA_CAMERA_LAUNCH_SOURCE = "com.android.systemui.camera_launch_source" +        @VisibleForTesting +        const val EXTRA_CAMERA_LAUNCH_SOURCE = "com.android.systemui.camera_launch_source"      }  } diff --git a/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt new file mode 100644 index 000000000000..ca94ea826782 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.camera + +import android.app.ActivityManager +import android.app.IActivityTaskManager +import android.content.ComponentName +import android.content.ContentResolver +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import androidx.test.filters.SmallTest +import com.android.systemui.ActivityIntentHelper +import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.phone.CentralSurfaces +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.mockito.KotlinArgumentCaptor +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class CameraGestureHelperTest : SysuiTestCase() { + +    @Mock +    lateinit var centralSurfaces: CentralSurfaces +    @Mock +    lateinit var keyguardStateController: KeyguardStateController +    @Mock +    lateinit var packageManager: PackageManager +    @Mock +    lateinit var activityManager: ActivityManager +    @Mock +    lateinit var activityStarter: ActivityStarter +    @Mock +    lateinit var activityIntentHelper: ActivityIntentHelper +    @Mock +    lateinit var activityTaskManager: IActivityTaskManager +    @Mock +    lateinit var cameraIntents: CameraIntentsWrapper +    @Mock +    lateinit var contentResolver: ContentResolver + +    private lateinit var underTest: CameraGestureHelper + +    @Before +    fun setUp() { +        MockitoAnnotations.initMocks(this) +        whenever(cameraIntents.getSecureCameraIntent()).thenReturn( +            Intent(CameraIntents.DEFAULT_SECURE_CAMERA_INTENT_ACTION) +        ) +        whenever(cameraIntents.getInsecureCameraIntent()).thenReturn( +            Intent(CameraIntents.DEFAULT_INSECURE_CAMERA_INTENT_ACTION) +        ) + +        prepare() + +        underTest = CameraGestureHelper( +            context = mock(), +            centralSurfaces = centralSurfaces, +            keyguardStateController = keyguardStateController, +            packageManager = packageManager, +            activityManager = activityManager, +            activityStarter = activityStarter, +            activityIntentHelper = activityIntentHelper, +            activityTaskManager = activityTaskManager, +            cameraIntents = cameraIntents, +            contentResolver = contentResolver, +            uiExecutor = MoreExecutors.directExecutor(), +        ) +    } + +    /** +     * Prepares for tests by setting up the various mocks to emulate a specific device state. +     * +     * <p>Safe to call multiple times in a single test (for example, once in [setUp] and once in the +     * actual test case). +     * +     * @param isCameraAllowedByAdmin Whether the device administrator allows use of the camera app +     * @param installedCameraAppCount The number of installed camera apps on the device +     * @param isUsingSecureScreenLockOption Whether the user-controlled setting for Screen Lock is +     * set with a "secure" option that requires the user to provide some secret/credentials to be +     * able to unlock the device, for example "Face Unlock", "PIN", or "Password". Examples of +     * non-secure options are "None" and "Swipe" +     * @param isCameraActivityRunningOnTop Whether the camera activity is running at the top of the +     * most recent/current task of activities +     * @param isTaskListEmpty Whether there are no active activity tasks at all. Note that this is +     * treated as `false` if [isCameraActivityRunningOnTop] is set to `true` +     */ +    private fun prepare( +        isCameraAllowedByAdmin: Boolean = true, +        installedCameraAppCount: Int = 1, +        isUsingSecureScreenLockOption: Boolean = true, +        isCameraActivityRunningOnTop: Boolean = false, +        isTaskListEmpty: Boolean = false, +    ) { +        whenever(centralSurfaces.isCameraAllowedByAdmin).thenReturn(isCameraAllowedByAdmin) + +        whenever(activityIntentHelper.wouldLaunchResolverActivity(any(), anyInt())) +            .thenReturn(installedCameraAppCount > 1) + +        whenever(keyguardStateController.isMethodSecure).thenReturn(isUsingSecureScreenLockOption) +        whenever(keyguardStateController.canDismissLockScreen()) +            .thenReturn(!isUsingSecureScreenLockOption) + +        if (installedCameraAppCount >= 1) { +            val resolveInfo = ResolveInfo().apply { +                this.activityInfo = ActivityInfo().apply { +                    packageName = CAMERA_APP_PACKAGE_NAME +                } +            } +            whenever(packageManager.resolveActivityAsUser(any(), anyInt(), anyInt())).thenReturn( +                resolveInfo +            ) +        } else { +            whenever(packageManager.resolveActivityAsUser(any(), anyInt(), anyInt())).thenReturn( +                null +            ) +        } + +        when { +            isCameraActivityRunningOnTop -> { +                val runningTaskInfo = ActivityManager.RunningTaskInfo().apply { +                    topActivity = ComponentName(CAMERA_APP_PACKAGE_NAME, "cameraActivity") +                } +                whenever(activityManager.getRunningTasks(anyInt())).thenReturn( +                    listOf( +                        runningTaskInfo +                    ) +                ) +            } +            isTaskListEmpty -> { +                whenever(activityManager.getRunningTasks(anyInt())).thenReturn(emptyList()) +            } +            else -> { +                whenever(activityManager.getRunningTasks(anyInt())).thenReturn(listOf()) +            } +        } +    } + +    @Test +    fun `canCameraGestureBeLaunched - status bar state is keyguard - returns true`() { +        assertThat(underTest.canCameraGestureBeLaunched(StatusBarState.KEYGUARD)).isTrue() +    } + +    @Test +    fun `canCameraGestureBeLaunched - state is shade-locked - returns true`() { +        assertThat(underTest.canCameraGestureBeLaunched(StatusBarState.SHADE_LOCKED)).isTrue() +    } + +    @Test +    fun `canCameraGestureBeLaunched - state is keyguard - camera activity on top - returns true`() { +        prepare(isCameraActivityRunningOnTop = true) + +        assertThat(underTest.canCameraGestureBeLaunched(StatusBarState.KEYGUARD)).isTrue() +    } + +    @Test +    fun `canCameraGestureBeLaunched - state is shade-locked - camera activity on top - true`() { +        prepare(isCameraActivityRunningOnTop = true) + +        assertThat(underTest.canCameraGestureBeLaunched(StatusBarState.SHADE_LOCKED)).isTrue() +    } + +    @Test +    fun `canCameraGestureBeLaunched - not allowed by admin - returns false`() { +        prepare(isCameraAllowedByAdmin = false) + +        assertThat(underTest.canCameraGestureBeLaunched(StatusBarState.KEYGUARD)).isFalse() +    } + +    @Test +    fun `canCameraGestureBeLaunched - intent does not resolve to any app - returns false`() { +        prepare(installedCameraAppCount = 0) + +        assertThat(underTest.canCameraGestureBeLaunched(StatusBarState.KEYGUARD)).isFalse() +    } + +    @Test +    fun `canCameraGestureBeLaunched - state is shade - no running tasks - returns true`() { +        prepare(isCameraActivityRunningOnTop = false, isTaskListEmpty = true) + +        assertThat(underTest.canCameraGestureBeLaunched(StatusBarState.SHADE)).isTrue() +    } + +    @Test +    fun `canCameraGestureBeLaunched - state is shade - camera activity on top - returns false`() { +        prepare(isCameraActivityRunningOnTop = true) + +        assertThat(underTest.canCameraGestureBeLaunched(StatusBarState.SHADE)).isFalse() +    } + +    @Test +    fun `canCameraGestureBeLaunched - state is shade - camera activity not on top - true`() { +        assertThat(underTest.canCameraGestureBeLaunched(StatusBarState.SHADE)).isTrue() +    } + +    @Test +    fun `launchCamera - only one camera app installed - using secure screen lock option`() { +        val source = 1337 + +        underTest.launchCamera(source) + +        assertActivityStarting(isSecure = true, source = source) +    } + +    @Test +    fun `launchCamera - only one camera app installed - using non-secure screen lock option`() { +        prepare(isUsingSecureScreenLockOption = false) +        val source = 1337 + +        underTest.launchCamera(source) + +        assertActivityStarting(isSecure = false, source = source) +    } + +    @Test +    fun `launchCamera - multiple camera apps installed - using secure screen lock option`() { +        prepare(installedCameraAppCount = 2) +        val source = 1337 + +        underTest.launchCamera(source) + +        assertActivityStarting( +            isSecure = true, +            source = source, +            moreThanOneCameraAppInstalled = true +        ) +    } + +    @Test +    fun `launchCamera - multiple camera apps installed - using non-secure screen lock option`() { +        prepare( +            isUsingSecureScreenLockOption = false, +            installedCameraAppCount = 2, +        ) +        val source = 1337 + +        underTest.launchCamera(source) + +        assertActivityStarting( +            isSecure = false, +            moreThanOneCameraAppInstalled = true, +            source = source +        ) +    } + +    private fun assertActivityStarting( +        isSecure: Boolean, +        source: Int, +        moreThanOneCameraAppInstalled: Boolean = false, +    ) { +        val intentCaptor = KotlinArgumentCaptor(Intent::class.java) +        if (isSecure && !moreThanOneCameraAppInstalled) { +            verify(activityTaskManager).startActivityAsUser( +                any(), +                any(), +                any(), +                intentCaptor.capture(), +                any(), +                any(), +                any(), +                anyInt(), +                anyInt(), +                any(), +                any(), +                anyInt() +            ) +        } else { +            verify(activityStarter).startActivity(intentCaptor.capture(), eq(false)) +        } +        val intent = intentCaptor.value + +        assertThat(CameraIntents.isSecureCameraIntent(intent)).isEqualTo(isSecure) +        assertThat(intent.getIntExtra(CameraGestureHelper.EXTRA_CAMERA_LAUNCH_SOURCE, -1)) +            .isEqualTo(source) +    } + +    companion object { +        private const val CAMERA_APP_PACKAGE_NAME = "cameraAppPackageName" +    } +} |