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

This commit is contained in:
Ale Nijamkin
2023-01-18 23:55:00 +00:00
committed by Android (Google) Code Review
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]-->
<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. -->
<!-- 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] -->

View File

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

View File

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

View File

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

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

View File

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

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