Merge "Quick affordance to open the camera in video mode." into tm-qpr-dev am: 4c1411e09b

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20965068

Change-Id: Ic43ec24c34dbddf7c2725bbcf9a8ebe1044e5fa8
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Ale Nijamkin
2023-01-19 00:35:04 +00:00
committed by Automerger Merge Worker
11 changed files with 309 additions and 14 deletions

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M7,42Q5.8,42 4.9,41.1Q4,40.2 4,39V13.35Q4,12.15 4.9,11.25Q5.8,10.35 7,10.35H14.35L18,6H30L33.65,10.35H41Q42.2,10.35 43.1,11.25Q44,12.15 44,13.35V39Q44,40.2 43.1,41.1Q42.2,42 41,42ZM7,39H41Q41,39 41,39Q41,39 41,39V13.35Q41,13.35 41,13.35Q41,13.35 41,13.35H7Q7,13.35 7,13.35Q7,13.35 7,13.35V39Q7,39 7,39Q7,39 7,39ZM7,39Q7,39 7,39Q7,39 7,39V13.35Q7,13.35 7,13.35Q7,13.35 7,13.35Q7,13.35 7,13.35Q7,13.35 7,13.35V39Q7,39 7,39Q7,39 7,39ZM24,34.7Q27.5,34.7 30,32.225Q32.5,29.75 32.5,26.2Q32.5,22.7 30,20.2Q27.5,17.7 24,17.7Q20.45,17.7 17.975,20.2Q15.5,22.7 15.5,26.2Q15.5,29.75 17.975,32.225Q20.45,34.7 24,34.7ZM24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M7,40Q5.8,40 4.9,39.1Q4,38.2 4,37V11Q4,9.8 4.9,8.9Q5.8,8 7,8H33Q34.2,8 35.1,8.9Q36,9.8 36,11V21.75L44,13.75V34.25L36,26.25V37Q36,38.2 35.1,39.1Q34.2,40 33,40ZM7,37H33Q33,37 33,37Q33,37 33,37V11Q33,11 33,11Q33,11 33,11H7Q7,11 7,11Q7,11 7,11V37Q7,37 7,37Q7,37 7,37ZM7,37Q7,37 7,37Q7,37 7,37V11Q7,11 7,11Q7,11 7,11Q7,11 7,11Q7,11 7,11V37Q7,37 7,37Q7,37 7,37Z"/>
</vector>

View File

@@ -2778,6 +2778,12 @@
<!-- Subtitle for the notification sent when a stylus battery is low. [CHAR LIMIT=none]--> <!-- Subtitle for the notification sent when a stylus battery is low. [CHAR LIMIT=none]-->
<string name="stylus_battery_low_subtitle">Connect your stylus to a charger</string> <string name="stylus_battery_low_subtitle">Connect your stylus to a charger</string>
<!-- Title for notification of low stylus battery. [CHAR_LIMIT=NONE] -->
<string name="stylus_battery_low">Stylus battery low</string>
<!-- Label for a lock screen shortcut to start the camera in video mode. [CHAR_LIMIT=16] -->
<string name="video_camera">Video camera</string>
<!-- Switch to work profile dialer app for placing a call dialog. --> <!-- Switch to work profile dialer app for placing a call dialog. -->
<!-- Text for Switch to work profile dialog's Title. Switch to work profile dialog guide users to make call from work <!-- Text for Switch to work profile dialog's Title. Switch to work profile dialog guide users to make call from work
profile dialer app as it's not possible to make call from current profile due to an admin policy. [CHAR LIMIT=60] --> profile dialer app as it's not possible to make call from current profile due to an admin policy. [CHAR LIMIT=60] -->

View File

@@ -20,15 +20,13 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.provider.MediaStore import android.provider.MediaStore
import android.text.TextUtils import android.text.TextUtils
import com.android.systemui.R import com.android.systemui.R
class CameraIntents { class CameraIntents {
companion object { companion object {
val DEFAULT_SECURE_CAMERA_INTENT_ACTION = val DEFAULT_SECURE_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE
MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE val DEFAULT_INSECURE_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA
val DEFAULT_INSECURE_CAMERA_INTENT_ACTION = private val VIDEO_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_VIDEO_CAMERA
MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA
const val EXTRA_LAUNCH_SOURCE = "com.android.systemui.camera_launch_source" const val EXTRA_LAUNCH_SOURCE = "com.android.systemui.camera_launch_source"
@JvmStatic @JvmStatic
@@ -44,18 +42,14 @@ class CameraIntents {
@JvmStatic @JvmStatic
fun getInsecureCameraIntent(context: Context): Intent { fun getInsecureCameraIntent(context: Context): Intent {
val intent = Intent(DEFAULT_INSECURE_CAMERA_INTENT_ACTION) val intent = Intent(DEFAULT_INSECURE_CAMERA_INTENT_ACTION)
getOverrideCameraPackage(context)?.let { getOverrideCameraPackage(context)?.let { intent.setPackage(it) }
intent.setPackage(it)
}
return intent return intent
} }
@JvmStatic @JvmStatic
fun getSecureCameraIntent(context: Context): Intent { fun getSecureCameraIntent(context: Context): Intent {
val intent = Intent(DEFAULT_SECURE_CAMERA_INTENT_ACTION) val intent = Intent(DEFAULT_SECURE_CAMERA_INTENT_ACTION)
getOverrideCameraPackage(context)?.let { getOverrideCameraPackage(context)?.let { intent.setPackage(it) }
intent.setPackage(it)
}
return intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) return intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
} }
@@ -68,5 +62,11 @@ class CameraIntents {
fun isInsecureCameraIntent(intent: Intent?): Boolean { fun isInsecureCameraIntent(intent: Intent?): Boolean {
return intent?.getAction()?.equals(DEFAULT_INSECURE_CAMERA_INTENT_ACTION) ?: false 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)
}
} }
} }

View File

@@ -21,7 +21,9 @@ import android.content.Intent
import javax.inject.Inject import javax.inject.Inject
/** Injectable wrapper around [CameraIntents]. */ /** Injectable wrapper around [CameraIntents]. */
class CameraIntentsWrapper @Inject constructor( class CameraIntentsWrapper
@Inject
constructor(
private val context: Context, private val context: Context,
) { ) {
@@ -40,4 +42,9 @@ class CameraIntentsWrapper @Inject constructor(
fun getInsecureCameraIntent(): Intent { fun getInsecureCameraIntent(): Intent {
return CameraIntents.getInsecureCameraIntent(context) return CameraIntents.getInsecureCameraIntent(context)
} }
/** Returns an [Intent] that can be used to start the camera in video mode. */
fun getVideoCameraIntent(): Intent {
return CameraIntents.getVideoCameraIntent()
}
} }

View File

@@ -30,5 +30,6 @@ object BuiltInKeyguardQuickAffordanceKeys {
const val HOME_CONTROLS = "home" const val HOME_CONTROLS = "home"
const val QR_CODE_SCANNER = "qr_code_scanner" const val QR_CODE_SCANNER = "qr_code_scanner"
const val QUICK_ACCESS_WALLET = "wallet" const val QUICK_ACCESS_WALLET = "wallet"
const val VIDEO_CAMERA = "video_camera"
// Please keep alphabetical order of const names to simplify future maintenance. // Please keep alphabetical order of const names to simplify future maintenance.
} }

View File

@@ -26,6 +26,7 @@ import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.StatusBarState
import dagger.Lazy import dagger.Lazy
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -46,7 +47,7 @@ constructor(
get() = context.getString(R.string.accessibility_camera_button) get() = context.getString(R.string.accessibility_camera_button)
override val pickerIconResourceId: Int override val pickerIconResourceId: Int
get() = com.android.internal.R.drawable.perm_group_camera get() = R.drawable.ic_camera
override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState>
get() = get() =
@@ -54,12 +55,20 @@ constructor(
KeyguardQuickAffordanceConfig.LockScreenState.Visible( KeyguardQuickAffordanceConfig.LockScreenState.Visible(
icon = icon =
Icon.Resource( Icon.Resource(
com.android.internal.R.drawable.perm_group_camera, R.drawable.ic_camera,
ContentDescription.Resource(R.string.accessibility_camera_button) 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( override fun onTriggered(
expandable: Expandable? expandable: Expandable?
): KeyguardQuickAffordanceConfig.OnTriggeredResult { ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
@@ -68,4 +77,8 @@ constructor(
.launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE)
return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
} }
private fun isLaunchable(): Boolean {
return cameraGestureHelper.get().canCameraGestureBeLaunched(StatusBarState.KEYGUARD)
}
} }

View File

@@ -39,6 +39,7 @@ interface KeyguardDataQuickAffordanceModule {
quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig, quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig,
qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig, qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig,
camera: CameraQuickAffordanceConfig, camera: CameraQuickAffordanceConfig,
videoCamera: VideoCameraQuickAffordanceConfig,
): Set<KeyguardQuickAffordanceConfig> { ): Set<KeyguardQuickAffordanceConfig> {
return setOf( return setOf(
camera, camera,
@@ -47,6 +48,7 @@ interface KeyguardDataQuickAffordanceModule {
home, home,
quickAccessWallet, quickAccessWallet,
qrCodeScanner, qrCodeScanner,
videoCamera,
) )
} }
} }

View File

@@ -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<KeyguardQuickAffordanceConfig.LockScreenState>
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
}
}

View File

@@ -22,15 +22,21 @@ import android.content.Context
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.camera.CameraGestureHelper 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.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.JUnit4 import org.junit.runners.JUnit4
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest @SmallTest
@RunWith(JUnit4::class) @RunWith(JUnit4::class)
class CameraQuickAffordanceConfigTest : SysuiTestCase() { class CameraQuickAffordanceConfigTest : SysuiTestCase() {
@@ -62,4 +68,24 @@ class CameraQuickAffordanceConfigTest : SysuiTestCase() {
.launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE)
assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) 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)
}
} }

View File

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