From ed4ce1647a4d10f1418783e268811f55ca4aa35e Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Thu, 12 Jan 2023 13:21:54 -0800 Subject: [PATCH] Quick affordance to open the camera in video mode. Also updates the older, normal camera config: 1. Don't show it if there's no camera app, tests included 2. Use the camera icon and reserve the video camera icon to the new video camera config (done in coordination with UX) Fix: 265316887 Test: unit tests Test: manually made sure that the video camera shortcut works Change-Id: Ie3551f9a79a8df98d30e0f16e931fcea82171a39 --- packages/SystemUI/res/drawable/ic_camera.xml | 10 ++ .../SystemUI/res/drawable/ic_videocam.xml | 10 ++ packages/SystemUI/res/values/strings.xml | 6 + .../android/systemui/camera/CameraIntents.kt | 22 ++-- .../systemui/camera/CameraIntentsWrapper.kt | 9 +- .../BuiltInKeyguardQuickAffordanceKeys.kt | 1 + .../CameraQuickAffordanceConfig.kt | 17 ++- .../KeyguardDataQuickAffordanceModule.kt | 2 + .../VideoCameraQuickAffordanceConfig.kt | 105 ++++++++++++++++ .../CameraQuickAffordanceConfigTest.kt | 26 ++++ .../VideoCameraQuickAffordanceConfigTest.kt | 115 ++++++++++++++++++ 11 files changed, 309 insertions(+), 14 deletions(-) create mode 100644 packages/SystemUI/res/drawable/ic_camera.xml create mode 100644 packages/SystemUI/res/drawable/ic_videocam.xml create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfigTest.kt diff --git a/packages/SystemUI/res/drawable/ic_camera.xml b/packages/SystemUI/res/drawable/ic_camera.xml new file mode 100644 index 0000000000000..ef1406c1c58ad --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_camera.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_videocam.xml b/packages/SystemUI/res/drawable/ic_videocam.xml new file mode 100644 index 0000000000000..de2bc7bccdf13 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_videocam.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 43dea0a06898b..066b185c6d1f1 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2778,6 +2778,12 @@ Connect your stylus to a charger + + Stylus battery low + + + Video camera + diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt index 867faf9843fe5..cc43e7ed25a57 100644 --- a/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt +++ b/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt @@ -20,15 +20,13 @@ import android.content.Context import android.content.Intent import android.provider.MediaStore import android.text.TextUtils - import com.android.systemui.R class CameraIntents { companion object { - val DEFAULT_SECURE_CAMERA_INTENT_ACTION = - MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE - val DEFAULT_INSECURE_CAMERA_INTENT_ACTION = - MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA + val DEFAULT_SECURE_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE + val DEFAULT_INSECURE_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA + private val VIDEO_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_VIDEO_CAMERA const val EXTRA_LAUNCH_SOURCE = "com.android.systemui.camera_launch_source" @JvmStatic @@ -44,18 +42,14 @@ class CameraIntents { @JvmStatic fun getInsecureCameraIntent(context: Context): Intent { val intent = Intent(DEFAULT_INSECURE_CAMERA_INTENT_ACTION) - getOverrideCameraPackage(context)?.let { - intent.setPackage(it) - } + getOverrideCameraPackage(context)?.let { intent.setPackage(it) } return intent } @JvmStatic fun getSecureCameraIntent(context: Context): Intent { val intent = Intent(DEFAULT_SECURE_CAMERA_INTENT_ACTION) - getOverrideCameraPackage(context)?.let { - intent.setPackage(it) - } + getOverrideCameraPackage(context)?.let { intent.setPackage(it) } return intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) } @@ -68,5 +62,11 @@ class CameraIntents { fun isInsecureCameraIntent(intent: Intent?): Boolean { return intent?.getAction()?.equals(DEFAULT_INSECURE_CAMERA_INTENT_ACTION) ?: false } + + /** Returns an [Intent] that can be used to start the camera in video mode. */ + @JvmStatic + fun getVideoCameraIntent(): Intent { + return Intent(VIDEO_CAMERA_INTENT_ACTION) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt index cf02f8fb4a3cb..a434617f2da78 100644 --- a/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt @@ -21,7 +21,9 @@ import android.content.Intent import javax.inject.Inject /** Injectable wrapper around [CameraIntents]. */ -class CameraIntentsWrapper @Inject constructor( +class CameraIntentsWrapper +@Inject +constructor( private val context: Context, ) { @@ -40,4 +42,9 @@ class CameraIntentsWrapper @Inject constructor( fun getInsecureCameraIntent(): Intent { return CameraIntents.getInsecureCameraIntent(context) } + + /** Returns an [Intent] that can be used to start the camera in video mode. */ + fun getVideoCameraIntent(): Intent { + return CameraIntents.getVideoCameraIntent() + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt index ea5b4f43cc75f..cd4dac0d59c51 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt @@ -30,5 +30,6 @@ object BuiltInKeyguardQuickAffordanceKeys { const val HOME_CONTROLS = "home" const val QR_CODE_SCANNER = "qr_code_scanner" const val QUICK_ACCESS_WALLET = "wallet" + const val VIDEO_CAMERA = "video_camera" // Please keep alphabetical order of const names to simplify future maintenance. } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt index dbc376e62950e..f6e6d6b7dc1b0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt @@ -26,6 +26,7 @@ import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.StatusBarState import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -46,7 +47,7 @@ constructor( get() = context.getString(R.string.accessibility_camera_button) override val pickerIconResourceId: Int - get() = com.android.internal.R.drawable.perm_group_camera + get() = R.drawable.ic_camera override val lockScreenState: Flow get() = @@ -54,12 +55,20 @@ constructor( KeyguardQuickAffordanceConfig.LockScreenState.Visible( icon = Icon.Resource( - com.android.internal.R.drawable.perm_group_camera, + R.drawable.ic_camera, ContentDescription.Resource(R.string.accessibility_camera_button) ) ) ) + override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState { + return if (isLaunchable()) { + super.getPickerScreenState() + } else { + KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice + } + } + override fun onTriggered( expandable: Expandable? ): KeyguardQuickAffordanceConfig.OnTriggeredResult { @@ -68,4 +77,8 @@ constructor( .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled } + + private fun isLaunchable(): Boolean { + return cameraGestureHelper.get().canCameraGestureBeLaunched(StatusBarState.KEYGUARD) + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt index 71d01ebc84967..a1cce5c670baa 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt @@ -39,6 +39,7 @@ interface KeyguardDataQuickAffordanceModule { quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig, qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig, camera: CameraQuickAffordanceConfig, + videoCamera: VideoCameraQuickAffordanceConfig, ): Set { return setOf( camera, @@ -47,6 +48,7 @@ interface KeyguardDataQuickAffordanceModule { home, quickAccessWallet, qrCodeScanner, + videoCamera, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt new file mode 100644 index 0000000000000..d9ec3b1c2f872 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2023 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.keyguard.data.quickaffordance + +import android.app.StatusBarManager +import android.content.Context +import android.content.Intent +import com.android.systemui.ActivityIntentHelper +import com.android.systemui.R +import com.android.systemui.animation.Expandable +import com.android.systemui.camera.CameraIntents +import com.android.systemui.camera.CameraIntentsWrapper +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.settings.UserTracker +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +@SysUISingleton +class VideoCameraQuickAffordanceConfig +@Inject +constructor( + @Application private val context: Context, + private val cameraIntents: CameraIntentsWrapper, + private val activityIntentHelper: ActivityIntentHelper, + private val userTracker: UserTracker, +) : KeyguardQuickAffordanceConfig { + + private val intent: Intent by lazy { + cameraIntents.getVideoCameraIntent().apply { + putExtra( + CameraIntents.EXTRA_LAUNCH_SOURCE, + StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE, + ) + } + } + + override val key: String + get() = BuiltInKeyguardQuickAffordanceKeys.VIDEO_CAMERA + + override val pickerName: String + get() = context.getString(R.string.video_camera) + + override val pickerIconResourceId: Int + get() = R.drawable.ic_videocam + + override val lockScreenState: Flow + get() = + flowOf( + if (isLaunchable()) { + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = + Icon.Resource( + R.drawable.ic_videocam, + ContentDescription.Resource(R.string.video_camera) + ) + ) + } else { + KeyguardQuickAffordanceConfig.LockScreenState.Hidden + } + ) + + override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState { + return if (isLaunchable()) { + super.getPickerScreenState() + } else { + KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice + } + } + + override fun onTriggered( + expandable: Expandable? + ): KeyguardQuickAffordanceConfig.OnTriggeredResult { + return KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity( + intent = intent, + canShowWhileLocked = false, + ) + } + + private fun isLaunchable(): Boolean { + return activityIntentHelper.getTargetActivityInfo( + intent, + userTracker.userId, + true, + ) != null + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt index 7205f3068abb5..8da4eae2f64a0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt @@ -22,15 +22,21 @@ import android.content.Context import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.camera.CameraGestureHelper +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals 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.anyInt import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class CameraQuickAffordanceConfigTest : SysuiTestCase() { @@ -62,4 +68,24 @@ class CameraQuickAffordanceConfigTest : SysuiTestCase() { .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) } + + @Test + fun `getPickerScreenState - default when launchable`() = runTest { + setLaunchable(true) + + Truth.assertThat(underTest.getPickerScreenState()) + .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Default::class.java) + } + + @Test + fun `getPickerScreenState - unavailable when not launchable`() = runTest { + setLaunchable(false) + + Truth.assertThat(underTest.getPickerScreenState()) + .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice) + } + + private fun setLaunchable(isLaunchable: Boolean) { + whenever(cameraGestureHelper.canCameraGestureBeLaunched(anyInt())).thenReturn(isLaunchable) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfigTest.kt new file mode 100644 index 0000000000000..805dcec0f5b19 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfigTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2023 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.keyguard.data.quickaffordance + +import androidx.test.filters.SmallTest +import com.android.systemui.ActivityIntentHelper +import com.android.systemui.SysuiTestCase +import com.android.systemui.camera.CameraIntentsWrapper +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class VideoCameraQuickAffordanceConfigTest : SysuiTestCase() { + + @Mock private lateinit var activityIntentHelper: ActivityIntentHelper + + private lateinit var underTest: VideoCameraQuickAffordanceConfig + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + underTest = + VideoCameraQuickAffordanceConfig( + context = context, + cameraIntents = CameraIntentsWrapper(context), + activityIntentHelper = activityIntentHelper, + userTracker = FakeUserTracker(), + ) + } + + @Test + fun `lockScreenState - visible when launchable`() = runTest { + setLaunchable(true) + + val lockScreenState = collectLastValue(underTest.lockScreenState) + + assertThat(lockScreenState()) + .isInstanceOf(KeyguardQuickAffordanceConfig.LockScreenState.Visible::class.java) + } + + @Test + fun `lockScreenState - hidden when not launchable`() = runTest { + setLaunchable(false) + + val lockScreenState = collectLastValue(underTest.lockScreenState) + + assertThat(lockScreenState()) + .isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden) + } + + @Test + fun `getPickerScreenState - default when launchable`() = runTest { + setLaunchable(true) + + assertThat(underTest.getPickerScreenState()) + .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Default::class.java) + } + + @Test + fun `getPickerScreenState - unavailable when not launchable`() = runTest { + setLaunchable(false) + + assertThat(underTest.getPickerScreenState()) + .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice) + } + + private fun setLaunchable(isLaunchable: Boolean) { + whenever( + activityIntentHelper.getTargetActivityInfo( + any(), + anyInt(), + anyBoolean(), + ) + ) + .thenReturn( + if (isLaunchable) { + mock() + } else { + null + } + ) + } +}