diff options
| author | 2022-07-08 15:03:26 -0700 | |
|---|---|---|
| committer | 2022-07-09 15:02:28 -0700 | |
| commit | 4af7cc97bcfcd859065a9ea5e40272a9afbe738f (patch) | |
| tree | a656f7fc42a1da0115dd19ed3665d8c2680263a6 | |
| parent | 613af5311b1bb69f7d23029922ecf0f9720ec10d (diff) | |
Unit test for CameraGestureHelper.
Also includes some massaging of `CameraGestureHelper` to be more
testable.
Test: Made sure that double-tapping the power button still brings up the
camera correctly in all the scenarios described in ag/19214502
Bug: b/235403546
Change-Id: I54e3f760b0c2fa5738766f8b97568caf07676a8d
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" + } +} |