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 + } + ) + } +}