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
This commit is contained in:
committed by
Ale Nijamkin
parent
2f12b73182
commit
ed4ce1647a
10
packages/SystemUI/res/drawable/ic_camera.xml
Normal file
10
packages/SystemUI/res/drawable/ic_camera.xml
Normal 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>
|
||||
10
packages/SystemUI/res/drawable/ic_videocam.xml
Normal file
10
packages/SystemUI/res/drawable/ic_videocam.xml
Normal 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>
|
||||
@@ -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] -->
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user