Merge "Add comments about TODO Bug 347083680" into main
diff --git a/core/java/android/content/pm/CrossProfileApps.java b/core/java/android/content/pm/CrossProfileApps.java
index 57ee622..bf51f00 100644
--- a/core/java/android/content/pm/CrossProfileApps.java
+++ b/core/java/android/content/pm/CrossProfileApps.java
@@ -304,6 +304,7 @@
* <li>It is not equal to the calling user</li>
* <li>It is in the same profile group of calling user profile</li>
* <li>It is enabled</li>
+ * <li>It is not hidden (ex. profile type {@link UserManager#USER_TYPE_PROFILE_PRIVATE})</li>
* </ul>
*
* @see UserManager#getUserProfiles()
@@ -460,8 +461,8 @@
*
* <p>Specifically, returns whether the following are all true:
* <ul>
- * <li>{@code UserManager#getEnabledProfileIds(int)} returns at least one other profile for the
- * calling user.</li>
+ * <li>{@code UserManager#getProfileIdsExcludingHidden(int)} returns at least one other
+ * profile for the calling user.</li>
* <li>The calling app has requested
* {@code android.Manifest.permission.INTERACT_ACROSS_PROFILES} in its manifest.</li>
* <li>The calling app is not a profile owner within the profile group of the calling user.</li>
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 20522fa..7926afe 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -5577,8 +5577,8 @@
}
/**
- * Enables or disables quiet mode for a managed profile. If quiet mode is enabled, apps in a
- * managed profile don't run, generate notifications, or consume data or battery.
+ * Enables or disables quiet mode for a profile. If quiet mode is enabled, apps in the profile
+ * don't run, generate notifications, or consume data or battery.
* <p>
* If a user's credential is needed to turn off quiet mode, a confirm credential screen will be
* shown to the user.
@@ -5586,8 +5586,11 @@
* The change may not happen instantly, however apps can listen for
* {@link Intent#ACTION_MANAGED_PROFILE_AVAILABLE} and
* {@link Intent#ACTION_MANAGED_PROFILE_UNAVAILABLE} broadcasts in order to be notified of
- * the change of the quiet mode. Apps can also check the current state of quiet mode by
- * calling {@link #isQuietModeEnabled(UserHandle)}.
+ * the change of the quiet mode for managed profile.
+ * Apps can listen to generic broadcasts {@link Intent#ACTION_PROFILE_AVAILABLE} and
+ * {@link Intent#ACTION_PROFILE_UNAVAILABLE} to be notified of the change in quiet mode for
+ * any profiles. Apps can also check the current state of quiet mode by calling
+ * {@link #isQuietModeEnabled(UserHandle)}.
* <p>
* The caller must either be the foreground default launcher or have one of these permissions:
* {@code MANAGE_USERS} or {@code MODIFY_QUIET_MODE}.
@@ -5597,7 +5600,7 @@
* @return {@code false} if user's credential is needed in order to turn off quiet mode,
* {@code true} otherwise
* @throws SecurityException if the caller is invalid
- * @throws IllegalArgumentException if {@code userHandle} is not a managed profile
+ * @throws IllegalArgumentException if {@code userHandle} is not a profile
*
* @see #isQuietModeEnabled(UserHandle)
*/
@@ -5662,7 +5665,6 @@
/**
* Returns whether the given profile is in quiet mode or not.
- * Notes: Quiet mode is only supported for managed profiles.
*
* @param userHandle The user handle of the profile to be queried.
* @return true if the profile is in quiet mode, false otherwise.
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index f43351a..4e133de 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -6969,9 +6969,17 @@
Note that, indefinitely repeating vibrations are not allowed as shutdown vibrations. -->
<string name="config_defaultShutdownVibrationFile" />
- <!-- Whether single finger panning is enabled when magnification is on -->
+ <!-- Whether single finger panning is enabled by default when magnification is on -->
<bool name="config_enable_a11y_magnification_single_panning">false</bool>
+ <!-- Whether the overscroll handler is enabled when fullscreen magnification is on. When true,
+ the magnification will change the scale if the user pans the magnifier horizontally past
+ the edge of the screen, or delegate the touch events to the app if the user pans vertically
+ past the edge. When false, the magnification will delegate the touch events to the app only
+ when the users uses single finger to pan the magnifier past the edge of the screen,
+ otherwise there are no extra actions. -->
+ <bool name="config_enable_a11y_fullscreen_magnification_overscroll_handler">false</bool>
+
<!-- The file path in which custom vibrations are provided for haptic feedbacks.
If the device does not specify any such file path here, if the file path specified here
does not exist, or if the contents of the file does not make up a valid customization
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index a0807ca..5fea515 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -620,6 +620,9 @@
<!-- width of the border of the magnification thumbnail -->
<dimen name="accessibility_magnification_thumbnail_container_stroke_width">4dp</dimen>
+ <!-- The distance from the edge within which the gesture is considered to be at the edge -->
+ <dimen name="accessibility_fullscreen_magnification_gesture_edge_slop">12dp</dimen>
+
<!-- The padding ratio of the Accessibility icon foreground drawable -->
<item name="accessibility_icon_foreground_padding_ratio" type="dimen">21.88%</item>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index c16bd24..44a6b26 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5427,6 +5427,8 @@
<java-symbol type="string" name="lockscreen_too_many_failed_attempts_countdown" />
<java-symbol type="bool" name="config_enable_a11y_magnification_single_panning" />
+ <java-symbol type="bool" name="config_enable_a11y_fullscreen_magnification_overscroll_handler" />
+ <java-symbol type="dimen" name="accessibility_fullscreen_magnification_gesture_edge_slop" />
<java-symbol type="string" name="config_hapticFeedbackCustomizationFile" />
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
index c35721c..772e02e 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
@@ -34,6 +34,7 @@
import com.android.credentialmanager.getflow.RequestDisplayInfo
import com.android.credentialmanager.getflow.generateDisplayTitleTextResCode
import com.android.credentialmanager.model.BiometricRequestInfo
+import com.android.credentialmanager.model.CredentialType
import com.android.credentialmanager.model.EntryInfo
import com.android.credentialmanager.model.creation.CreateOptionInfo
import com.android.credentialmanager.model.get.CredentialEntryInfo
@@ -476,7 +477,9 @@
return null
}
val singleEntryType = selectedEntry.credentialType
- val username = selectedEntry.userName
+ val descriptionName = if (singleEntryType == CredentialType.PASSKEY &&
+ !selectedEntry.displayName.isNullOrBlank()) selectedEntry.displayName else
+ selectedEntry.userName
// TODO(b/336362538) : In W, utilize updated localization strings
displayTitleText = context.getString(
@@ -487,7 +490,7 @@
descriptionText = context.getString(
R.string.get_dialog_description_single_tap,
getRequestDisplayInfo.appName,
- username
+ descriptionName
)
return BiometricDisplayInfo(providerIcon = icon, providerName = providerName,
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
index 79f8b5fc..16ec1a9 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
@@ -17,6 +17,7 @@
package com.android.egg.landroid
import android.content.res.Resources
+import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
@@ -119,6 +120,26 @@
}.absoluteValue
}
+fun getDessertCode(): String =
+ when (Build.VERSION.SDK_INT) {
+ Build.VERSION_CODES.LOLLIPOP -> "LMP"
+ Build.VERSION_CODES.LOLLIPOP_MR1 -> "LM1"
+ Build.VERSION_CODES.M -> "MNC"
+ Build.VERSION_CODES.N -> "NYC"
+ Build.VERSION_CODES.N_MR1 -> "NM1"
+ Build.VERSION_CODES.O -> "OC"
+ Build.VERSION_CODES.P -> "PIE"
+ Build.VERSION_CODES.Q -> "QT"
+ Build.VERSION_CODES.R -> "RVC"
+ Build.VERSION_CODES.S -> "SC"
+ Build.VERSION_CODES.S_V2 -> "SC2"
+ Build.VERSION_CODES.TIRAMISU -> "TM"
+ Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> "UDC"
+ Build.VERSION_CODES.VANILLA_ICE_CREAM -> "VIC"
+ else -> Build.VERSION.RELEASE_OR_CODENAME.replace(Regex("[a-z]*"), "")
+ }
+
+
val DEBUG_TEXT = mutableStateOf("Hello Universe")
const val SHOW_DEBUG_TEXT = false
@@ -239,7 +260,8 @@
text =
(with(universe.star) {
listOf(
- " STAR: $name (UDC-${universe.randomSeed % 100_000})",
+ " STAR: $name (${getDessertCode()}-" +
+ "${universe.randomSeed % 100_000})",
" CLASS: ${cls.name}",
"RADIUS: ${radius.toInt()}",
" MASS: %.3g".format(mass),
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 58c39b4..fde7c2c 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -785,6 +785,7 @@
kotlincflags: ["-Xjvm-default=all"],
optimize: {
shrink_resources: false,
+ optimized_shrink_resources: false,
proguard_flags_files: ["proguard.flags"],
},
@@ -921,6 +922,7 @@
optimize: true,
shrink: true,
shrink_resources: true,
+ optimized_shrink_resources: true,
ignore_warnings: false,
proguard_compatibility: false,
},
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt
index 0250c9d..baeb2dd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt
@@ -31,7 +31,6 @@
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -42,13 +41,11 @@
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val communalSceneRepository = kosmos.fakeCommunalSceneRepository
- private lateinit var underTest: CommunalTransitionViewModel
-
- @Before
- fun setup() {
- underTest = kosmos.communalTransitionViewModel
+ private val underTest: CommunalTransitionViewModel by lazy {
+ kosmos.communalTransitionViewModel
}
@Test
@@ -60,11 +57,7 @@
enterCommunal(from = KeyguardState.LOCKSCREEN)
assertThat(isUmoOnCommunal).isTrue()
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.GLANCEABLE_HUB,
- to = KeyguardState.LOCKSCREEN,
- testScope
- )
+ exitCommunal(to = KeyguardState.LOCKSCREEN)
assertThat(isUmoOnCommunal).isFalse()
}
@@ -77,11 +70,7 @@
enterCommunal(from = KeyguardState.DREAMING)
assertThat(isUmoOnCommunal).isTrue()
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.GLANCEABLE_HUB,
- to = KeyguardState.DREAMING,
- testScope
- )
+ exitCommunal(to = KeyguardState.DREAMING)
assertThat(isUmoOnCommunal).isFalse()
}
@@ -94,11 +83,7 @@
enterCommunal(from = KeyguardState.OCCLUDED)
assertThat(isUmoOnCommunal).isTrue()
- keyguardTransitionRepository.sendTransitionSteps(
- from = KeyguardState.GLANCEABLE_HUB,
- to = KeyguardState.OCCLUDED,
- testScope
- )
+ exitCommunal(to = KeyguardState.OCCLUDED)
assertThat(isUmoOnCommunal).isFalse()
}
@@ -112,20 +97,42 @@
assertThat(isUmoOnCommunal).isTrue()
// Communal is no longer visible.
- kosmos.fakeCommunalSceneRepository.changeScene(CommunalScenes.Blank)
- runCurrent()
+ communalSceneRepository.changeScene(CommunalScenes.Blank)
// isUmoOnCommunal returns false, even without any keyguard transition.
assertThat(isUmoOnCommunal).isFalse()
}
+ @Test
+ fun isUmoOnCommunal_idleOnCommunal_returnsTrue() =
+ testScope.runTest {
+ val isUmoOnCommunal by collectLastValue(underTest.isUmoOnCommunal)
+ assertThat(isUmoOnCommunal).isFalse()
+
+ // Communal is fully visible.
+ communalSceneRepository.changeScene(CommunalScenes.Communal)
+
+ // isUmoOnCommunal returns true, even without any keyguard transition.
+ assertThat(isUmoOnCommunal).isTrue()
+ }
+
private suspend fun TestScope.enterCommunal(from: KeyguardState) {
keyguardTransitionRepository.sendTransitionSteps(
from = from,
to = KeyguardState.GLANCEABLE_HUB,
testScope
)
- kosmos.fakeCommunalSceneRepository.changeScene(CommunalScenes.Communal)
+ communalSceneRepository.changeScene(CommunalScenes.Communal)
+ runCurrent()
+ }
+
+ private suspend fun TestScope.exitCommunal(to: KeyguardState) {
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = to,
+ testScope
+ )
+ communalSceneRepository.changeScene(CommunalScenes.Blank)
runCurrent()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index 4c9af66..e055e7c 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -1250,6 +1250,11 @@
if (mOverlays == null) {
return;
}
+ if (mPendingConfigChange) {
+ // Let RestartingPreDrawListener's onPreDraw call updateConfiguration
+ // -> updateOverlayProviderViews to redraw with display change synchronously.
+ return;
+ }
++mProviderRefreshToken;
for (final OverlayWindow overlay: mOverlays) {
if (overlay == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index 65c5b6b..408e2c2 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -457,17 +457,10 @@
// Retry and confirmation when finger on sensor
launch {
- combine(
- viewModel.canTryAgainNow,
- viewModel.hasFingerOnSensor,
- viewModel.isPendingConfirmation,
- ::Triple
- )
- .collect { (canRetry, fingerAcquired, pendingConfirmation) ->
+ combine(viewModel.canTryAgainNow, viewModel.hasFingerOnSensor, ::Pair)
+ .collect { (canRetry, fingerAcquired) ->
if (canRetry && fingerAcquired) {
legacyCallback.onButtonTryAgain()
- } else if (pendingConfirmation && fingerAcquired) {
- viewModel.confirmAuthenticated()
}
}
}
@@ -497,13 +490,21 @@
@Deprecated("TODO(b/330788871): remove after replacing AuthContainerView")
interface Callback {
fun onAuthenticated()
+
fun onUserCanceled()
+
fun onButtonNegative()
+
fun onButtonTryAgain()
+
fun onContentViewMoreOptionsButtonPressed()
+
fun onError()
+
fun onUseDeviceCredential()
+
fun onStartDelayedFingerprintSensor()
+
fun onAuthenticatedAndConfirmed()
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
index 7d494a5..ac8807d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -232,7 +232,6 @@
val fingerprintStartMode: Flow<FingerprintStartMode> = _fingerprintStartMode.asStateFlow()
/** Whether a finger has been acquired by the sensor */
- // TODO(b/331948073): Add support for detecting SFPS finger without authentication running
val hasFingerBeenAcquired: Flow<Boolean> =
combine(biometricStatusInteractor.fingerprintAcquiredStatus, modalities) {
status,
@@ -617,7 +616,8 @@
}
/** If the icon can be used as a confirmation button. */
- val isIconConfirmButton: Flow<Boolean> = size.map { it.isNotSmall }.distinctUntilChanged()
+ val isIconConfirmButton: Flow<Boolean> =
+ combine(modalities, size) { modalities, size -> modalities.hasUdfps && size.isNotSmall }
/** If the negative button should be shown. */
val isNegativeButtonVisible: Flow<Boolean> =
@@ -700,6 +700,9 @@
failedModality: BiometricModality = BiometricModality.None,
) = coroutineScope {
if (_isAuthenticated.value.isAuthenticated) {
+ if (_isAuthenticated.value.needsUserConfirmation && hapticFeedback) {
+ vibrateOnError()
+ }
return@coroutineScope
}
@@ -823,6 +826,14 @@
helpMessage: String = "",
) {
if (_isAuthenticated.value.isAuthenticated) {
+ // Treat second authentication with a different modality as confirmation for the first
+ if (
+ _isAuthenticated.value.needsUserConfirmation &&
+ modality != _isAuthenticated.value.authenticatedModality
+ ) {
+ confirmAuthenticated()
+ return
+ }
// TODO(jbolinger): convert to go/tex-apc?
Log.w(TAG, "Cannot show authenticated after authenticated")
return
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnRepository.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnRepository.kt
index 81fe2a5..7f1cb5d 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnRepository.kt
@@ -97,6 +97,10 @@
// Server could throw TimeoutException, InterruptedException or ExecutionException
Log.e(TAG, "Error calling isAutoOnSupported", e)
false
+ } catch (e: NoSuchMethodError) {
+ // TODO(b/346716614): Remove this when the flag is cleaned up.
+ Log.e(TAG, "Non-existed api isAutoOnSupported", e)
+ false
}
}
@@ -109,6 +113,9 @@
// Server could throw IllegalStateException, TimeoutException, InterruptedException
// or ExecutionException
Log.e(TAG, "Error calling setAutoOnEnabled", e)
+ } catch (e: NoSuchMethodError) {
+ // TODO(b/346716614): Remove this when the flag is cleaned up.
+ Log.e(TAG, "Non-existed api setAutoOn", e)
}
}
}
@@ -122,6 +129,10 @@
// or ExecutionException
Log.e(TAG, "Error calling isAutoOnEnabled", e)
false
+ } catch (e: NoSuchMethodError) {
+ // TODO(b/346716614): Remove this when the flag is cleaned up.
+ Log.e(TAG, "Non-existed api isAutoOnEnabled", e)
+ false
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt
index fce18a26..e1408a0 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt
@@ -21,6 +21,7 @@
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.communal.util.CommunalColors
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.Edge
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -31,13 +32,18 @@
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
+import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
/** View model for transitions related to the communal hub. */
@OptIn(ExperimentalCoroutinesApi::class)
@@ -45,6 +51,7 @@
class CommunalTransitionViewModel
@Inject
constructor(
+ @Application applicationScope: CoroutineScope,
communalColors: CommunalColors,
glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel,
lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel,
@@ -85,21 +92,32 @@
* of UMO should be updated.
*/
val isUmoOnCommunal: Flow<Boolean> =
- allOf(
- // Only show UMO on the hub if the hub is at least partially visible. This prevents
- // the UMO from being missing on the lock screen when going from the hub to lock
- // screen in some way other than through a direct transition, such as unlocking from
- // the hub, then pressing power twice to go back to the lock screen.
- communalSceneInteractor.isCommunalVisible,
- merge(
- lockscreenToGlanceableHubTransitionViewModel.showUmo,
- glanceableHubToLockscreenTransitionViewModel.showUmo,
- dreamToGlanceableHubTransitionViewModel.showUmo,
- glanceableHubToDreamTransitionViewModel.showUmo,
- showUmoFromOccludedToGlanceableHub,
- showUmoFromGlanceableHubToOccluded,
+ anyOf(
+ communalSceneInteractor.isIdleOnCommunal,
+ allOf(
+ // Only show UMO on the hub if the hub is at least partially visible. This
+ // prevents
+ // the UMO from being missing on the lock screen when going from the hub to lock
+ // screen in some way other than through a direct transition, such as unlocking
+ // from
+ // the hub, then pressing power twice to go back to the lock screen.
+ communalSceneInteractor.isCommunalVisible,
+ merge(
+ lockscreenToGlanceableHubTransitionViewModel.showUmo,
+ glanceableHubToLockscreenTransitionViewModel.showUmo,
+ dreamToGlanceableHubTransitionViewModel.showUmo,
+ glanceableHubToDreamTransitionViewModel.showUmo,
+ showUmoFromOccludedToGlanceableHub,
+ showUmoFromGlanceableHubToOccluded,
+ )
+ .onStart { emit(false) }
+ )
)
- )
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = false
+ )
/** Whether to show communal when exiting the occluded state. */
val showCommunalFromOccluded: Flow<Boolean> = communalInteractor.showCommunalFromOccluded
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index f457470..4205dd8 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -135,6 +135,7 @@
import com.android.systemui.util.AlphaTintDrawableWrapper;
import com.android.systemui.util.RoundedCornerProgressDrawable;
import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.volume.domain.interactor.VolumeDialogInteractor;
import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor;
import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag;
import com.android.systemui.volume.ui.binder.VolumeDialogMenuIconBinder;
@@ -315,6 +316,7 @@
private final com.android.systemui.util.time.SystemClock mSystemClock;
private final VolumeDialogMenuIconBinder mVolumeDialogMenuIconBinder;
private final VolumePanelFlag mVolumePanelFlag;
+ private final VolumeDialogInteractor mInteractor;
public VolumeDialogImpl(
Context context,
@@ -335,7 +337,8 @@
Lazy<SecureSettings> secureSettings,
VibratorHelper vibratorHelper,
VolumeDialogMenuIconBinder volumeDialogMenuIconBinder,
- com.android.systemui.util.time.SystemClock systemClock) {
+ com.android.systemui.util.time.SystemClock systemClock,
+ VolumeDialogInteractor interactor) {
mContext =
new ContextThemeWrapper(context, R.style.volume_dialog_theme);
mHandler = new H(looper);
@@ -370,6 +373,7 @@
mVolumeDialogMenuIconBinder = volumeDialogMenuIconBinder;
mDialogTimeoutMillis = DIALOG_TIMEOUT_MILLIS;
mVolumePanelFlag = volumePanelFlag;
+ mInteractor = interactor;
dumpManager.registerDumpable("VolumeDialogImpl", this);
@@ -1519,6 +1523,7 @@
mShowing = true;
mIsAnimatingDismiss = false;
mDialog.show();
+ mInteractor.onDialogShown();
Events.writeEvent(Events.EVENT_SHOW_DIALOG, reason, keyguardLocked);
mController.notifyVisible(true);
mController.getCaptionsComponentState(false);
@@ -1605,6 +1610,7 @@
}
mIsAnimatingDismiss = true;
mDialogView.animate().cancel();
+ mInteractor.onDialogDismissed();
if (mShowing) {
mShowing = false;
// Only logs when the volume dialog visibility is changed.
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index f8ddc42..8003f39 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -41,6 +41,7 @@
import com.android.systemui.volume.VolumeDialogModule;
import com.android.systemui.volume.VolumePanelDialogReceiver;
import com.android.systemui.volume.VolumeUI;
+import com.android.systemui.volume.domain.interactor.VolumeDialogInteractor;
import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor;
import com.android.systemui.volume.panel.dagger.VolumePanelComponent;
import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory;
@@ -118,7 +119,8 @@
Lazy<SecureSettings> secureSettings,
VibratorHelper vibratorHelper,
VolumeDialogMenuIconBinder volumeDialogMenuIconBinder,
- SystemClock systemClock) {
+ SystemClock systemClock,
+ VolumeDialogInteractor interactor) {
VolumeDialogImpl impl = new VolumeDialogImpl(
context,
volumeDialogController,
@@ -138,7 +140,8 @@
secureSettings,
vibratorHelper,
volumeDialogMenuIconBinder,
- systemClock);
+ systemClock,
+ interactor);
impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
impl.setAutomute(true);
impl.setSilentMode(false);
diff --git a/packages/SystemUI/src/com/android/systemui/volume/data/repository/VolumeDialogRepository.kt b/packages/SystemUI/src/com/android/systemui/volume/data/repository/VolumeDialogRepository.kt
new file mode 100644
index 0000000..75e1c5ac
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/data/repository/VolumeDialogRepository.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 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.volume.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** A repository that encapsulates the states for Volume Dialog. */
+@SysUISingleton
+class VolumeDialogRepository @Inject constructor() {
+ private val _isDialogVisible: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ /** Whether the Volume Dialog is visible. */
+ val isDialogVisible = _isDialogVisible.asStateFlow()
+
+ /** Sets whether the Volume Dialog is visible. */
+ fun setDialogVisibility(isVisible: Boolean) {
+ _isDialogVisible.value = isVisible
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/VolumeDialogInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/VolumeDialogInteractor.kt
new file mode 100644
index 0000000..813e707
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/VolumeDialogInteractor.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 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.volume.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.volume.data.repository.VolumeDialogRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
+
+/** An interactor that propagates the UI states of the Volume Dialog. */
+@SysUISingleton
+class VolumeDialogInteractor
+@Inject
+constructor(
+ private val repository: VolumeDialogRepository,
+) {
+ /** Whether the Volume Dialog is visible. */
+ val isDialogVisible: StateFlow<Boolean> = repository.isDialogVisible
+
+ /** Notifies that the Volume Dialog is shown. */
+ fun onDialogShown() {
+ repository.setDialogVisibility(true)
+ }
+
+ /** Notifies that the Volume Dialog has been dismissed. */
+ fun onDialogDismissed() {
+ repository.setDialogVisibility(false)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index d267ad4..54a14a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -39,6 +39,7 @@
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -1182,6 +1183,24 @@
}
@Test
+ public void testUpdateOverlayProviderViews_PendingConfigChange() {
+ final DecorProvider cutout = new CutoutDecorProviderImpl(BOUNDS_POSITION_TOP);
+ spyOn(cutout);
+ doNothing().when(cutout).onReloadResAndMeasure(any(), anyInt(), anyInt(), anyInt(), any());
+ mMockCutoutList.add(cutout);
+ mScreenDecorations.start();
+ doCallRealMethod().when(mScreenDecorations).updateOverlayProviderViews(any());
+
+ mScreenDecorations.mPendingConfigChange = true;
+ mScreenDecorations.updateOverlayProviderViews(null /* filterIds */);
+ verify(cutout, never()).onReloadResAndMeasure(any(), anyInt(), anyInt(), anyInt(), any());
+
+ mScreenDecorations.mPendingConfigChange = false;
+ mScreenDecorations.updateOverlayProviderViews(null /* filterIds */);
+ verify(cutout).onReloadResAndMeasure(any(), anyInt(), anyInt(), anyInt(), any());
+ }
+
+ @Test
public void testSupportHwcLayer_SwitchFrom_NotSupport() {
setupResources(0 /* radius */, 10 /* radiusTop */, 20 /* radiusBottom */,
null /* roundedTopDrawable */, null /* roundedBottomDrawable */,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index cc7dec56..93c6d9e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -847,6 +847,28 @@
assertThat(haptics?.hapticFeedbackConstant).isEqualTo(HapticFeedbackConstants.NO_HAPTICS)
}
+ @Test
+ fun plays_haptic_on_error_after_auth_when_confirmation_needed() = runGenericTest {
+ val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
+ viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+ viewModel.showTemporaryError(
+ "still sad",
+ messageAfterError = "",
+ authenticateAfterError = false,
+ hapticFeedback = true,
+ )
+
+ val haptics by collectLastValue(viewModel.hapticsToPlay)
+ if (expectConfirmation) {
+ assertThat(haptics?.hapticFeedbackConstant).isEqualTo(HapticFeedbackConstants.REJECT)
+ assertThat(haptics?.flag).isEqualTo(HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING)
+ } else {
+ assertThat(haptics?.hapticFeedbackConstant)
+ .isEqualTo(HapticFeedbackConstants.CONFIRM)
+ }
+ }
+
private suspend fun TestScope.showTemporaryErrors(
restart: Boolean,
helpAfterError: String = "",
@@ -994,7 +1016,7 @@
}
@Test
- fun authenticated_at_most_once() = runGenericTest {
+ fun authenticated_at_most_once_same_modality() = runGenericTest {
val authenticating by collectLastValue(viewModel.isAuthenticating)
val authenticated by collectLastValue(viewModel.isAuthenticated)
@@ -1056,6 +1078,38 @@
}
@Test
+ fun second_authentication_acts_as_confirmation() = runGenericTest {
+ val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
+
+ viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+ val message by collectLastValue(viewModel.message)
+ val size by collectLastValue(viewModel.size)
+ val canTryAgain by collectLastValue(viewModel.canTryAgainNow)
+
+ assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
+ if (expectConfirmation) {
+ assertThat(size).isEqualTo(PromptSize.MEDIUM)
+ assertButtonsVisible(
+ cancel = true,
+ confirm = true,
+ )
+
+ if (testCase.modalities.hasSfps) {
+ viewModel.showAuthenticated(BiometricModality.Fingerprint, 0)
+ assertThat(message).isEqualTo(PromptMessage.Empty)
+ assertButtonsVisible()
+ }
+ }
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isTrue()
+ assertThat(canTryAgain).isFalse()
+ }
+
+ @Test
fun auto_confirm_authentication_when_finger_down() = runGenericTest {
val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index fac6a4c..57ddcde 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -86,6 +86,7 @@
import com.android.systemui.util.settings.FakeSettings;
import com.android.systemui.util.settings.SecureSettings;
import com.android.systemui.util.time.FakeSystemClock;
+import com.android.systemui.volume.domain.interactor.VolumeDialogInteractor;
import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor;
import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag;
import com.android.systemui.volume.ui.binder.VolumeDialogMenuIconBinder;
@@ -153,6 +154,8 @@
private VolumeDialogMenuIconBinder mVolumeDialogMenuIconBinder;
@Mock
private VolumePanelFlag mVolumePanelFlag;
+ @Mock
+ private VolumeDialogInteractor mVolumeDialogInteractor;
private final CsdWarningDialog.Factory mCsdWarningDialogFactory =
new CsdWarningDialog.Factory() {
@@ -218,7 +221,8 @@
mLazySecureSettings,
mVibratorHelper,
mVolumeDialogMenuIconBinder,
- new FakeSystemClock());
+ new FakeSystemClock(),
+ mVolumeDialogInteractor);
mDialog.init(0, null);
State state = createShellState();
mDialog.onStateChangedH(state);
@@ -778,6 +782,15 @@
assertFalse(foundDnDIcon);
}
+ @Test
+ public void testInteractor_onShow() {
+ mDialog.show(SHOW_REASON_UNKNOWN);
+ mTestableLooper.processAllMessages();
+
+ verify(mVolumeDialogInteractor).onDialogShown();
+ verify(mVolumeDialogInteractor).onDialogDismissed(); // dismiss by timeout
+ }
+
/**
* @return true if at least one volume row has the DND icon
*/
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/data/repository/VolumeDialogRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/volume/data/repository/VolumeDialogRepositoryTest.kt
new file mode 100644
index 0000000..dcac85e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/data/repository/VolumeDialogRepositoryTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2024 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.volume.data.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class VolumeDialogRepositoryTest : SysuiTestCase() {
+ private lateinit var underTest: VolumeDialogRepository
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+
+ @Before
+ fun setUp() {
+ underTest = kosmos.volumeDialogRepository
+ }
+
+ @Test
+ fun isDialogVisible_initialValueFalse() {
+ testScope.runTest {
+ val isVisible by collectLastValue(underTest.isDialogVisible)
+ runCurrent()
+
+ assertThat(isVisible).isFalse()
+ }
+ }
+
+ @Test
+ fun isDialogVisible_onChange() {
+ testScope.runTest {
+ val isVisible by collectLastValue(underTest.isDialogVisible)
+ runCurrent()
+
+ underTest.setDialogVisibility(true)
+ assertThat(isVisible).isTrue()
+
+ underTest.setDialogVisibility(false)
+ assertThat(isVisible).isFalse()
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/domain/interactor/VolumeDialogInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/volume/domain/interactor/VolumeDialogInteractorTest.kt
new file mode 100644
index 0000000..5c735e3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/domain/interactor/VolumeDialogInteractorTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 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.volume.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class VolumeDialogInteractorTest : SysuiTestCase() {
+ private lateinit var underTest: VolumeDialogInteractor
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+
+ @Before
+ fun setUp() {
+ underTest = kosmos.volumeDialogInteractor
+ }
+
+ @Test
+ fun onDialogDismissed() {
+ testScope.runTest {
+ val isVisible by collectLastValue(underTest.isDialogVisible)
+ underTest.onDialogDismissed()
+ runCurrent()
+
+ assertThat(isVisible).isFalse()
+ }
+ }
+
+ @Test
+ fun onDialogShown() {
+ testScope.runTest {
+ val isVisible by collectLastValue(underTest.isDialogVisible)
+ underTest.onDialogShown()
+ runCurrent()
+
+ assertThat(isVisible).isTrue()
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelKosmos.kt
index 3f38408..1ae8449 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelKosmos.kt
@@ -25,12 +25,14 @@
import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGlanceableHubTransitionViewModel
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@OptIn(ExperimentalCoroutinesApi::class)
val Kosmos.communalTransitionViewModel by
Kosmos.Fixture {
CommunalTransitionViewModel(
+ applicationScope = applicationCoroutineScope,
glanceableHubToLockscreenTransitionViewModel =
glanceableHubToLockscreenTransitionViewModel,
lockscreenToGlanceableHubTransitionViewModel =
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index 67f8443..31cdbc7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -68,6 +68,7 @@
import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository
import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor
import com.android.systemui.util.time.systemClock
+import com.android.systemui.volume.domain.interactor.volumeDialogInteractor
import kotlinx.coroutines.ExperimentalCoroutinesApi
/**
@@ -136,6 +137,7 @@
val shadeInteractor by lazy { kosmos.shadeInteractor }
val wifiInteractor by lazy { kosmos.wifiInteractor }
val fakeWifiRepository by lazy { kosmos.fakeWifiRepository }
+ val volumeDialogInteractor by lazy { kosmos.volumeDialogInteractor }
val ongoingActivityChipsViewModel by lazy { kosmos.ongoingActivityChipsViewModel }
val scrimController by lazy { kosmos.scrimController }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/VolumeDialogRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/VolumeDialogRepositoryKosmos.kt
new file mode 100644
index 0000000..2f4eef5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/VolumeDialogRepositoryKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 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.volume.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.volumeDialogRepository by Kosmos.Fixture { VolumeDialogRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/VolumeDialogInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/VolumeDialogInteractorKosmos.kt
new file mode 100644
index 0000000..2e5d040
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/VolumeDialogInteractorKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 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.volume.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.data.repository.volumeDialogRepository
+
+val Kosmos.volumeDialogInteractor by
+ Kosmos.Fixture { VolumeDialogInteractor(volumeDialogRepository) }
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
index aa0af7e..b5b998f 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
@@ -18,6 +18,7 @@
import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MANAGER_INTERNAL;
import static android.accessibilityservice.MagnificationConfig.MAGNIFICATION_MODE_FULLSCREEN;
+import static android.util.MathUtils.abs;
import static android.util.TypedValue.COMPLEX_UNIT_DIP;
import static android.view.accessibility.MagnificationAnimationCallback.STUB_ANIMATION_CALLBACK;
@@ -263,27 +264,27 @@
@GuardedBy("mLock")
boolean isAtEdge() {
- return isAtLeftEdge() || isAtRightEdge() || isAtTopEdge() || isAtBottomEdge();
+ return isAtLeftEdge(0f) || isAtRightEdge(0f) || isAtTopEdge(0f) || isAtBottomEdge(0f);
}
@GuardedBy("mLock")
- boolean isAtLeftEdge() {
- return getOffsetX() == getMaxOffsetXLocked();
+ boolean isAtLeftEdge(float slop) {
+ return abs(getOffsetX() - getMaxOffsetXLocked()) <= slop;
}
@GuardedBy("mLock")
- boolean isAtRightEdge() {
- return getOffsetX() == getMinOffsetXLocked();
+ boolean isAtRightEdge(float slop) {
+ return abs(getOffsetX() - getMinOffsetXLocked()) <= slop;
}
@GuardedBy("mLock")
- boolean isAtTopEdge() {
- return getOffsetY() == getMaxOffsetYLocked();
+ boolean isAtTopEdge(float slop) {
+ return abs(getOffsetY() - getMaxOffsetYLocked()) <= slop;
}
@GuardedBy("mLock")
- boolean isAtBottomEdge() {
- return getOffsetY() == getMinOffsetYLocked();
+ boolean isAtBottomEdge(float slop) {
+ return abs(getOffsetY() - getMinOffsetYLocked()) <= slop;
}
@GuardedBy("mLock")
@@ -1298,7 +1299,7 @@
* Returns whether the user is at one of the edges (left, right, top, bottom)
* of the magnification viewport
*
- * @param displayId
+ * @param displayId The logical display id.
* @return if user is at the edge of the view
*/
public boolean isAtEdge(int displayId) {
@@ -1314,64 +1315,72 @@
/**
* Returns whether the user is at the left edge of the viewport
*
- * @param displayId
- * @return if user is at left edge of view
+ * @param displayId The logical display id.
+ * @param slop The buffer distance in pixels from the left edge within that will be considered
+ * to be at the edge.
+ * @return if user is considered at left edge of view
*/
- public boolean isAtLeftEdge(int displayId) {
+ public boolean isAtLeftEdge(int displayId, float slop) {
synchronized (mLock) {
final DisplayMagnification display = mDisplays.get(displayId);
if (display == null) {
return false;
}
- return display.isAtLeftEdge();
+ return display.isAtLeftEdge(slop);
}
}
/**
* Returns whether the user is at the right edge of the viewport
*
- * @param displayId
- * @return if user is at right edge of view
+ * @param displayId The logical display id.
+ * @param slop The buffer distance in pixels from the right edge within that will be considered
+ * to be at the edge.
+ * @return if user is considered at right edge of view
*/
- public boolean isAtRightEdge(int displayId) {
+ public boolean isAtRightEdge(int displayId, float slop) {
synchronized (mLock) {
final DisplayMagnification display = mDisplays.get(displayId);
if (display == null) {
return false;
}
- return display.isAtRightEdge();
+ return display.isAtRightEdge(slop);
}
}
/**
* Returns whether the user is at the top edge of the viewport
*
- * @param displayId
- * @return if user is at top edge of view
+ * @param displayId The logical display id.
+ * @param slop The buffer distance in pixels from the top edge within that will be considered
+ * to be at the edge.
+ * @return if user is considered at top edge of view
*/
- public boolean isAtTopEdge(int displayId) {
+ public boolean isAtTopEdge(int displayId, float slop) {
synchronized (mLock) {
final DisplayMagnification display = mDisplays.get(displayId);
if (display == null) {
return false;
}
- return display.isAtTopEdge();
+ return display.isAtTopEdge(slop);
}
}
/**
* Returns whether the user is at the bottom edge of the viewport
*
- * @param displayId
- * @return if user is at bottom edge of view
+ * @param displayId The logical display id.
+ * @param slop The buffer distance in pixels from the bottom edge within that will be considered
+ * to be at the edge.
+ * @return if user is considered at bottom edge of view
*/
- public boolean isAtBottomEdge(int displayId) {
+ public boolean isAtBottomEdge(int displayId, float slop) {
synchronized (mLock) {
final DisplayMagnification display = mDisplays.get(displayId);
if (display == null) {
return false;
}
- return display.isAtBottomEdge();
+ return display.isAtBottomEdge(slop);
}
}
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
index 6d1ab9f..e9c3fbd 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
@@ -171,7 +171,11 @@
private final FullScreenMagnificationVibrationHelper mFullScreenMagnificationVibrationHelper;
- @VisibleForTesting final OverscrollHandler mOverscrollHandler;
+ @VisibleForTesting
+ @Nullable
+ final OverscrollHandler mOverscrollHandler;
+
+ private final float mOverscrollEdgeSlop;
private final boolean mIsWatch;
@@ -308,7 +312,11 @@
mSinglePanningState = new SinglePanningState(context);
mFullScreenMagnificationVibrationHelper = fullScreenMagnificationVibrationHelper;
mOneFingerPanningSettingsProvider = oneFingerPanningSettingsProvider;
- mOverscrollHandler = new OverscrollHandler();
+ boolean overscrollHandlerSupported = context.getResources().getBoolean(
+ R.bool.config_enable_a11y_fullscreen_magnification_overscroll_handler);
+ mOverscrollHandler = overscrollHandlerSupported ? new OverscrollHandler() : null;
+ mOverscrollEdgeSlop = context.getResources().getDimensionPixelSize(
+ R.dimen.accessibility_fullscreen_magnification_gesture_edge_slop);
mIsWatch = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
if (mDetectShortcutTrigger) {
@@ -523,16 +531,14 @@
if (action == ACTION_POINTER_UP
&& event.getPointerCount() == 2 // includes the pointer currently being released
&& mPreviousState == mViewportDraggingState) {
- // if feature flag is enabled, currently only true on watches
- if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()) {
+ if (mOverscrollHandler != null) {
mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
mOverscrollHandler.clearEdgeState();
}
persistScaleAndTransitionTo(mViewportDraggingState);
} else if (action == ACTION_UP || action == ACTION_CANCEL) {
onPanningFinished(event);
- // if feature flag is enabled, currently only true on watches
- if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()) {
+ if (mOverscrollHandler != null) {
mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
mOverscrollHandler.clearEdgeState();
}
@@ -540,7 +546,6 @@
}
}
-
void prepareForState() {
checkShouldDetectPassPersistedScale();
}
@@ -611,7 +616,7 @@
onPan(second);
mFullScreenMagnificationController.offsetMagnifiedRegion(mDisplayId, distanceX,
distanceY, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
- if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()) {
+ if (mOverscrollHandler != null) {
mOverscrollHandler.onScrollStateChanged(first, second);
}
return /* event consumed: */ true;
@@ -1000,7 +1005,7 @@
&& event.getPointerCount() == 2) {
transitionToViewportDraggingStateAndClear(event);
} else if (isActivated() && event.getPointerCount() == 2) {
- if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()
+ if (mOverscrollHandler != null
&& overscrollState(event, mFirstPointerDownLocation)
== OVERSCROLL_VERTICAL_EDGE) {
transitionToDelegatingStateAndClear();
@@ -1011,9 +1016,13 @@
} else if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()
&& isActivated()
&& event.getPointerCount() == 1) {
- if (overscrollState(event, mFirstPointerDownLocation)
+ if (mOverscrollHandler != null
+ && overscrollState(event, mFirstPointerDownLocation)
== OVERSCROLL_VERTICAL_EDGE) {
transitionToDelegatingStateAndClear();
+ } else if (overscrollState(event, mFirstPointerDownLocation)
+ != OVERSCROLL_NONE) {
+ transitionToDelegatingStateAndClear();
} else {
transitToSinglePanningStateAndClear();
}
@@ -1255,7 +1264,7 @@
if (isMultiTapTriggered(2 /* taps */) && event.getPointerCount() == 1) {
transitionToViewportDraggingStateAndClear(event);
} else if (isActivated() && event.getPointerCount() == 2) {
- if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()
+ if (mOverscrollHandler != null
&& overscrollState(event, mFirstPointerDownLocation)
== OVERSCROLL_VERTICAL_EDGE) {
transitionToDelegatingStateAndClear();
@@ -1266,9 +1275,13 @@
} else if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()
&& isActivated()
&& event.getPointerCount() == 1) {
- if (overscrollState(event, mFirstPointerDownLocation)
+ if (mOverscrollHandler != null
+ && overscrollState(event, mFirstPointerDownLocation)
== OVERSCROLL_VERTICAL_EDGE) {
transitionToDelegatingStateAndClear();
+ } else if (overscrollState(event, mFirstPointerDownLocation)
+ != OVERSCROLL_NONE) {
+ transitionToDelegatingStateAndClear();
} else {
transitToSinglePanningStateAndClear();
}
@@ -1645,22 +1658,36 @@
}
float dX = event.getX() - firstPointerDownLocation.x;
float dY = event.getY() - firstPointerDownLocation.y;
- if (mFullScreenMagnificationController.isAtLeftEdge(mDisplayId) && dX > 0) {
+ if (isAtLeftEdge() && dX > 0) {
return OVERSCROLL_LEFT_EDGE;
- } else if (mFullScreenMagnificationController.isAtRightEdge(mDisplayId) && dX < 0) {
+ } else if (isAtRightEdge() && dX < 0) {
return OVERSCROLL_RIGHT_EDGE;
- } else if (mFullScreenMagnificationController.isAtTopEdge(mDisplayId) && dY > 0
- || mFullScreenMagnificationController.isAtBottomEdge(mDisplayId) && dY < 0) {
+ } else if ((isAtTopEdge() && dY > 0) || (isAtBottomEdge() && dY < 0)) {
return OVERSCROLL_VERTICAL_EDGE;
}
return OVERSCROLL_NONE;
}
+ private boolean isAtLeftEdge() {
+ return mFullScreenMagnificationController.isAtLeftEdge(mDisplayId, mOverscrollEdgeSlop);
+ }
+
+ private boolean isAtRightEdge() {
+ return mFullScreenMagnificationController.isAtRightEdge(mDisplayId, mOverscrollEdgeSlop);
+ }
+
+ private boolean isAtTopEdge() {
+ return mFullScreenMagnificationController.isAtTopEdge(mDisplayId, mOverscrollEdgeSlop);
+ }
+
+ private boolean isAtBottomEdge() {
+ return mFullScreenMagnificationController.isAtBottomEdge(mDisplayId, mOverscrollEdgeSlop);
+ }
+
private boolean pointerValid(PointF pointerDownLocation) {
return !(Float.isNaN(pointerDownLocation.x) && Float.isNaN(pointerDownLocation.y));
}
-
private static final class MotionEventInfo {
private static final int MAX_POOL_SIZE = 10;
@@ -1845,7 +1872,6 @@
final class SinglePanningState extends SimpleOnGestureListener implements State {
-
private final GestureDetector mScrollGestureDetector;
private MotionEventInfo mEvent;
SinglePanningState(Context context) {
@@ -1860,8 +1886,10 @@
onPanningFinished(event);
// fall-through!
case ACTION_CANCEL:
- mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
- mOverscrollHandler.clearEdgeState();
+ if (mOverscrollHandler != null) {
+ mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
+ mOverscrollHandler.clearEdgeState();
+ }
transitionTo(mDetectingState);
break;
}
@@ -1889,26 +1917,18 @@
+ " isAtEdge: "
+ mFullScreenMagnificationController.isAtEdge(mDisplayId));
}
- mOverscrollHandler.onScrollStateChanged(first, second);
+ if (mOverscrollHandler != null) {
+ mOverscrollHandler.onScrollStateChanged(first, second);
+ }
return /* event consumed: */ true;
}
- private void vibrateIfNeeded() {
- if ((mFullScreenMagnificationController.isAtLeftEdge(mDisplayId)
- || mFullScreenMagnificationController.isAtRightEdge(mDisplayId))) {
- mFullScreenMagnificationVibrationHelper.vibrateIfSettingEnabled();
- }
- }
-
-
-
@Override
public String toString() {
return "SinglePanningState{"
+ "isEdgeOfView="
+ mFullScreenMagnificationController.isAtEdge(mDisplayId);
}
-
}
/** Overscroll Handler handles the logic when user is at the edge and scrolls past an edge */
@@ -2007,9 +2027,7 @@
if (mOverscrollState != OVERSCROLL_NONE) {
return;
}
- if ((mFullScreenMagnificationController.isAtLeftEdge(mDisplayId)
- || mFullScreenMagnificationController.isAtRightEdge(mDisplayId))
- && !mEdgeCooldown) {
+ if ((isAtLeftEdge() || isAtRightEdge()) && !mEdgeCooldown) {
mFullScreenMagnificationVibrationHelper.vibrateIfSettingEnabled();
}
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 5ffab55..d41de38 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -306,6 +306,7 @@
import android.content.pm.SharedLibraryInfo;
import android.content.pm.TestUtilityService;
import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
import android.content.pm.VersionedPackage;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
@@ -18209,7 +18210,9 @@
/**
* Stops user but allow delayed locking. Delayed locking keeps user unlocked even after
- * stopping only if {@code config_multiuserDelayUserDataLocking} overlay is set true.
+ * stopping only if {@code config_multiuserDelayUserDataLocking} overlay is set true on the
+ * device or if the user has {@link UserProperties#getAllowStoppingUserWithDelayedLocking()}
+ * set to true.
*
* <p>When delayed locking is not enabled through the overlay, this call becomes the same
* with {@link #stopUserWithCallback(int, IStopUserCallback)} call.
@@ -18221,8 +18224,6 @@
* other {@code ActivityManager#USER_OP_*} codes for failure.
*
*/
- // TODO(b/302662311): Add javadoc changes corresponding to the user property that allows
- // delayed locking behavior once the private space flag is finalized.
@Override
public int stopUserWithDelayedLocking(@UserIdInt int userId, IStopUserCallback callback) {
return mUserController.stopUser(userId, /* allowDelayedLocking= */ true,
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index c6c1f98..fa0e2ca 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -384,9 +384,11 @@
* postponed until total number of unlocked users in the system reaches mMaxRunningUsers.
* Once total number of unlocked users reach mMaxRunningUsers, least recently used user
* will be locked.
+ *
+ * <p> Note: Even if this is false for the device as a whole, it is possible some users may
+ * individually allow delayed locking, as specified by
+ * {@link UserProperties#getAllowStoppingUserWithDelayedLocking()}.
*/
- // TODO(b/302662311): Add javadoc changes corresponding to the user property that allows
- // delayed locking behavior once the private space flag is finalized.
@GuardedBy("mLock")
private boolean mDelayUserDataLocking;
diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java
index 469e8b7..276ab03 100644
--- a/services/core/java/com/android/server/biometrics/AuthSession.java
+++ b/services/core/java/com/android/server/biometrics/AuthSession.java
@@ -108,6 +108,7 @@
}
private final Context mContext;
+ private final BiometricManager mBiometricManager;
@NonNull private final BiometricContext mBiometricContext;
private final IStatusBarService mStatusBarService;
@VisibleForTesting final IBiometricSysuiReceiver mSysuiReceiver;
@@ -131,6 +132,7 @@
private final String mOpPackageName;
private final boolean mDebugEnabled;
private final List<FingerprintSensorPropertiesInternal> mFingerprintSensorProperties;
+ private final List<Integer> mSfpsSensorIds;
// The current state, which can be either idle, called, or started
private @SessionState int mState = STATE_AUTH_IDLE;
@@ -220,6 +222,11 @@
mCancelled = false;
mBiometricFrameworkStatsLogger = logger;
mOperationContext = new OperationContextExt(true /* isBP */);
+ mBiometricManager = mContext.getSystemService(BiometricManager.class);
+
+ mSfpsSensorIds = mFingerprintSensorProperties.stream().filter(
+ FingerprintSensorPropertiesInternal::isAnySidefpsType).map(
+ prop -> prop.sensorId).toList();
try {
mClientReceiver.asBinder().linkToDeath(this, 0 /* flags */);
@@ -316,7 +323,7 @@
Slog.w(TAG, "Received cookie but already cancelled (ignoring): " + cookie);
return;
}
- if (hasAuthenticated()) {
+ if (hasAuthenticatedAndConfirmed()) {
Slog.d(TAG, "onCookieReceived after successful auth");
return;
}
@@ -494,6 +501,7 @@
}
case STATE_AUTH_STARTED:
+ case STATE_AUTH_PENDING_CONFIRM:
case STATE_AUTH_STARTED_UI_SHOWING: {
if (isAllowDeviceCredential() && errorLockout) {
// SystemUI handles transition from biometric to device credential.
@@ -539,7 +547,7 @@
}
void onAcquired(int sensorId, int acquiredInfo, int vendorCode) {
- if (hasAuthenticated()) {
+ if (hasAuthenticatedAndConfirmed()) {
Slog.d(TAG, "onAcquired after successful auth");
return;
}
@@ -562,7 +570,7 @@
}
void onSystemEvent(int event) {
- if (hasAuthenticated()) {
+ if (hasAuthenticatedAndConfirmed()) {
Slog.d(TAG, "onSystemEvent after successful auth");
return;
}
@@ -579,12 +587,15 @@
void onDialogAnimatedIn(boolean startFingerprintNow) {
if (mState != STATE_AUTH_STARTED && mState != STATE_ERROR_PENDING_SYSUI
- && mState != STATE_AUTH_PAUSED) {
+ && mState != STATE_AUTH_PAUSED && mState != STATE_AUTH_PENDING_CONFIRM) {
Slog.e(TAG, "onDialogAnimatedIn, unexpected state: " + mState);
return;
}
- mState = STATE_AUTH_STARTED_UI_SHOWING;
+ if (mState != STATE_AUTH_PENDING_CONFIRM) {
+ mState = STATE_AUTH_STARTED_UI_SHOWING;
+ }
+
if (startFingerprintNow) {
startAllPreparedFingerprintSensors();
} else {
@@ -600,6 +611,7 @@
if (mState != STATE_AUTH_STARTED
&& mState != STATE_AUTH_STARTED_UI_SHOWING
&& mState != STATE_AUTH_PAUSED
+ && mState != STATE_AUTH_PENDING_CONFIRM
&& mState != STATE_ERROR_PENDING_SYSUI) {
Slog.w(TAG, "onStartFingerprint, started from unexpected state: " + mState);
}
@@ -608,7 +620,7 @@
}
void onTryAgainPressed() {
- if (hasAuthenticated()) {
+ if (hasAuthenticatedAndConfirmed()) {
Slog.d(TAG, "onTryAgainPressed after successful auth");
return;
}
@@ -625,7 +637,7 @@
}
void onAuthenticationSucceeded(int sensorId, boolean strong, byte[] token) {
- if (hasAuthenticated()) {
+ if (hasAuthenticatedAndConfirmed()) {
Slog.d(TAG, "onAuthenticationSucceeded after successful auth");
return;
}
@@ -656,11 +668,17 @@
Slog.e(TAG, "RemoteException", e);
}
- cancelAllSensors(sensor -> sensor.id != sensorId);
+ if (mState == STATE_AUTH_PENDING_CONFIRM) {
+ // Do not cancel Sfps sensors so auth can continue running
+ cancelAllSensors(
+ sensor -> sensor.id != sensorId && !mSfpsSensorIds.contains(sensor.id));
+ } else {
+ cancelAllSensors(sensor -> sensor.id != sensorId);
+ }
}
void onAuthenticationRejected(int sensorId) {
- if (hasAuthenticated()) {
+ if (hasAuthenticatedAndConfirmed()) {
Slog.d(TAG, "onAuthenticationRejected after successful auth");
return;
}
@@ -678,7 +696,7 @@
}
void onAuthenticationTimedOut(int sensorId, int cookie, int error, int vendorCode) {
- if (hasAuthenticated()) {
+ if (hasAuthenticatedAndConfirmed()) {
Slog.d(TAG, "onAuthenticationTimedOut after successful auth");
return;
}
@@ -703,7 +721,7 @@
}
void onDeviceCredentialPressed() {
- if (hasAuthenticated()) {
+ if (hasAuthenticatedAndConfirmed()) {
Slog.d(TAG, "onDeviceCredentialPressed after successful auth");
return;
}
@@ -739,6 +757,10 @@
return mAuthenticatedSensorId != -1;
}
+ private boolean hasAuthenticatedAndConfirmed() {
+ return mAuthenticatedSensorId != -1 && mState == STATE_AUTHENTICATED_PENDING_SYSUI;
+ }
+
private void logOnDialogDismissed(@BiometricPrompt.DismissedReason int reason) {
if (reason == BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED) {
// Explicit auth, authentication confirmed.
@@ -828,6 +850,7 @@
} else {
Slog.e(TAG, "mTokenEscrow is null");
}
+
mClientReceiver.onAuthenticationSucceeded(
Utils.getAuthenticationTypeForResult(reason));
break;
@@ -861,6 +884,16 @@
} catch (RemoteException e) {
Slog.e(TAG, "Remote exception", e);
} finally {
+ if (mTokenEscrow != null && mBiometricManager != null) {
+ final byte[] byteToken = new byte[mTokenEscrow.length];
+ for (int i = 0; i < mTokenEscrow.length; i++) {
+ byteToken[i] = mTokenEscrow[i];
+ }
+ mBiometricManager.resetLockoutTimeBound(mToken,
+ mContext.getOpPackageName(),
+ mAuthenticatedSensorId, mUserId, byteToken);
+ }
+
// ensure everything is cleaned up when dismissed
cancelAllSensors();
}
@@ -874,7 +907,7 @@
* @return true if this AuthSession is finished, e.g. should be set to null
*/
boolean onCancelAuthSession(boolean force) {
- if (hasAuthenticated()) {
+ if (hasAuthenticatedAndConfirmed()) {
Slog.d(TAG, "onCancelAuthSession after successful auth");
return true;
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
index 1e2451c..daaafcb 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
@@ -242,14 +242,14 @@
byteToken[i] = hardwareAuthToken.get(i);
}
- if (mIsStrongBiometric) {
- mBiometricManager.resetLockoutTimeBound(getToken(),
- getContext().getOpPackageName(),
- getSensorId(), getTargetUserId(), byteToken);
- }
-
// For BP, BiometricService will add the authToken to Keystore.
- if (!isBiometricPrompt() && mIsStrongBiometric) {
+ if (!isBiometricPrompt()) {
+ if (mIsStrongBiometric) {
+ mBiometricManager.resetLockoutTimeBound(getToken(),
+ getContext().getOpPackageName(),
+ getSensorId(), getTargetUserId(), byteToken);
+ }
+
final int result = KeyStoreAuthorization.getInstance().addAuthToken(byteToken);
if (result != 0) {
Slog.d(TAG, "Error adding auth token : " + result);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index 2660932..72d92b9 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -209,7 +209,7 @@
Slog.d(TAG, "Lockout is implemented by the HAL");
return;
}
- if (authenticated) {
+ if (authenticated && !isBiometricPrompt()) {
getLockoutTracker().resetFailedAttemptsForUser(true /* clearAttemptCounter */,
getTargetUserId());
} else {
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 0606563..00e9d8d 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -33,6 +33,7 @@
import static android.content.pm.PackageManager.INSTALL_FAILED_MEDIA_UNAVAILABLE;
import static android.content.pm.PackageManager.INSTALL_FAILED_MISSING_SPLIT;
import static android.content.pm.PackageManager.INSTALL_FAILED_PRE_APPROVAL_NOT_AVAILABLE;
+import static android.content.pm.PackageManager.INSTALL_FAILED_SESSION_INVALID;
import static android.content.pm.PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
import static android.content.pm.PackageManager.INSTALL_STAGED;
@@ -3459,11 +3460,6 @@
}
}
- if (mHasAppMetadataFile && !getStagedAppMetadataFile().exists()) {
- throw new PackageManagerException(INSTALL_FAILED_VERIFICATION_FAILURE,
- "App metadata file expected but not found in " + stageDir.getAbsolutePath());
- }
-
final List<ApkLite> addedFiles = getAddedApkLitesLocked();
if (addedFiles.isEmpty()
&& (removeSplitList.size() == 0 || mHasAppMetadataFile)) {
@@ -3593,6 +3589,13 @@
}
}
+ File stagedAppMetadataFile = isIncrementalInstallation()
+ ? getTmpAppMetadataFile() : getStagedAppMetadataFile();
+ if (mHasAppMetadataFile && !stagedAppMetadataFile.exists()) {
+ throw new PackageManagerException(INSTALL_FAILED_SESSION_INVALID,
+ "App metadata file expected but not found in " + stageDir.getAbsolutePath());
+ }
+
if (isIncrementalInstallation()) {
if (!isIncrementalInstallationAllowed(existingPkgSetting)) {
throw new PackageManagerException(
@@ -3601,8 +3604,8 @@
}
// Since we moved the staged app metadata file so that incfs can be initialized, lets
// now move it back.
- File appMetadataFile = getTmpAppMetadataFile();
- if (appMetadataFile.exists()) {
+ if (mHasAppMetadataFile) {
+ File appMetadataFile = getTmpAppMetadataFile();
final IncrementalFileStorages incrementalFileStorages =
getIncrementalFileStorages();
try {
diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java
index 0052d4f..7f24769 100644
--- a/services/core/java/com/android/server/power/ThermalManagerService.java
+++ b/services/core/java/com/android/server/power/ThermalManagerService.java
@@ -543,7 +543,7 @@
if (!mHalReady.get()) {
FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED, getCallingUid(),
FrameworkStatsLog.THERMAL_HEADROOM_CALLED__API_STATUS__HAL_NOT_READY,
- Float.NaN);
+ Float.NaN, forecastSeconds);
return Float.NaN;
}
@@ -553,7 +553,7 @@
}
FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED, getCallingUid(),
FrameworkStatsLog.THERMAL_HEADROOM_CALLED__API_STATUS__INVALID_ARGUMENT,
- Float.NaN);
+ Float.NaN, forecastSeconds);
return Float.NaN;
}
@@ -1778,7 +1778,7 @@
FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED,
Binder.getCallingUid(),
FrameworkStatsLog.THERMAL_HEADROOM_CALLED__API_STATUS__NO_TEMPERATURE,
- Float.NaN);
+ Float.NaN, forecastSeconds);
return Float.NaN;
}
@@ -1789,7 +1789,7 @@
FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED,
Binder.getCallingUid(),
THERMAL_HEADROOM_CALLED__API_STATUS__NO_TEMPERATURE_THRESHOLD,
- Float.NaN);
+ Float.NaN, forecastSeconds);
return Float.NaN;
}
@@ -1828,12 +1828,12 @@
FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED,
Binder.getCallingUid(),
THERMAL_HEADROOM_CALLED__API_STATUS__NO_TEMPERATURE_THRESHOLD,
- Float.NaN);
+ Float.NaN, forecastSeconds);
} else {
FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED,
Binder.getCallingUid(),
FrameworkStatsLog.THERMAL_HEADROOM_CALLED__API_STATUS__SUCCESS,
- maxNormalized);
+ maxNormalized, forecastSeconds);
}
return maxNormalized;
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 5159fc4..ed867d1 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -8701,9 +8701,11 @@
if (!mOptOutEdgeToEdge && (!mResolveConfigHint.mUseOverrideInsetsForConfig
|| getCompatDisplayInsets() != null
|| (isFloating(parentWindowingMode)
- // Check the windowing mode of activity as well in case it is switching
- // between PiP and fullscreen.
- && isFloating(inOutConfig.windowConfiguration.getWindowingMode()))
+ // Check the requested windowing mode of activity as well in case it is
+ // switching between PiP and fullscreen.
+ && (inOutConfig.windowConfiguration.getWindowingMode()
+ == WINDOWING_MODE_UNDEFINED
+ || isFloating(inOutConfig.windowConfiguration.getWindowingMode())))
|| rotation == ROTATION_UNDEFINED)) {
// If the insets configuration decoupled logic is not enabled for the app, or the app
// already has a compat override, or the context doesn't contain enough info to
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 0febec9..2972190 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -1366,8 +1366,6 @@
? task.getSurfaceControl()
: mAdaptors[0].mTarget.getSurfaceControl());
}
- // remove starting surface.
- mStartingSurface = null;
}
}
@@ -1384,7 +1382,10 @@
.removeWindowlessStartingSurface(mRequestedStartingSurfaceId,
!openTransitionMatch);
mRequestedStartingSurfaceId = INVALID_TASK_ID;
- mStartingSurface = null;
+ if (mStartingSurface != null && mStartingSurface.isValid()) {
+ mStartingSurface.release();
+ mStartingSurface = null;
+ }
}
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
index f482ddc..3ef81fd 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
@@ -28,8 +28,11 @@
import static com.android.server.testutils.TestUtils.strictMock;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import static org.mockito.AdditionalMatchers.gt;
import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -198,6 +201,7 @@
private final Scroller mMockScroller = spy(new Scroller(mContext));
private boolean mMockMagnificationConnectionState;
+ private boolean mMockOneFingerPanningEnabled;
private OffsettableClock mClock;
private FullScreenMagnificationGestureHandler mMgh;
@@ -209,8 +213,6 @@
static final Rect INITIAL_MAGNIFICATION_BOUNDS = new Rect(0, 0, 800, 800);
- static final Region INITIAL_MAGNIFICATION_REGION = new Region(INITIAL_MAGNIFICATION_BOUNDS);
-
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -282,6 +284,8 @@
@NonNull
private FullScreenMagnificationGestureHandler newInstance(boolean detectSingleFingerTripleTap,
boolean detectTwoFingerTripleTap, boolean detectShortcutTrigger) {
+ enableOneFingerPanning(
+ isWatch() || Flags.enableMagnificationOneFingerPanningGesture());
FullScreenMagnificationGestureHandler h =
new FullScreenMagnificationGestureHandler(
mContext,
@@ -297,8 +301,12 @@
mMockMagnificationLogger,
ViewConfiguration.get(mContext),
mMockOneFingerPanningSettingsProvider);
+ // OverscrollHandler is only supported on watches.
+ // @See config_enable_a11y_fullscreen_magnification_overscroll_handler
if (isWatch()) {
- enableOneFingerPanning(true);
+ assertNotNull(h.mOverscrollHandler);
+ } else {
+ assertNull(h.mOverscrollHandler);
}
mHandler = new TestHandler(h.mDetectingState, mClock) {
@Override
@@ -836,7 +844,7 @@
@Test
public void testActionUpNotAtEdge_singlePanningState_detectingState() {
- assumeTrue(isWatch());
+ assumeTrue(isOneFingerPanningEnabled());
goFromStateIdleTo(STATE_SINGLE_PANNING);
send(upEvent());
@@ -846,8 +854,7 @@
}
@Test
- public void testScroll_SinglePanningDisabled_delegatingState() {
- assumeTrue(isWatch());
+ public void testScroll_singlePanningDisabled_delegatingState() {
enableOneFingerPanning(false);
goFromStateIdleTo(STATE_ACTIVATED);
@@ -858,8 +865,54 @@
}
@Test
+ public void testSingleFingerOverscrollAtLeftEdge_isNotWatch_transitionToDelegatingState() {
+ assumeTrue(isOneFingerPanningEnabled());
+ assumeFalse(isWatch());
+ goFromStateIdleTo(STATE_ACTIVATED);
+ float centerY =
+ (INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.bottom) / 2.0f;
+ mFullScreenMagnificationController.setCenter(
+ DISPLAY_0, INITIAL_MAGNIFICATION_BOUNDS.left, centerY, false, 1);
+ final float swipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop() + 1;
+ PointF initCoords =
+ new PointF(INITIAL_MAGNIFICATION_BOUNDS.centerX(),
+ INITIAL_MAGNIFICATION_BOUNDS.centerY());
+ PointF edgeCoords = new PointF(initCoords.x, initCoords.y);
+ edgeCoords.offset(swipeMinDistance, 0);
+
+ allowEventDelegation();
+ swipeAndHold(initCoords, edgeCoords);
+
+ assertTrue(mMgh.mCurrentState == mMgh.mDelegatingState);
+ assertTrue(isZoomed());
+ }
+
+ @Test
+ public void testSingleFingerOverscrollAtBottomEdge_isNotWatch_transitionToDelegatingState() {
+ assumeTrue(isOneFingerPanningEnabled());
+ assumeFalse(isWatch());
+ goFromStateIdleTo(STATE_ACTIVATED);
+ float centerX =
+ (INITIAL_MAGNIFICATION_BOUNDS.right + INITIAL_MAGNIFICATION_BOUNDS.left) / 2.0f;
+ mFullScreenMagnificationController.setCenter(
+ DISPLAY_0, centerX, INITIAL_MAGNIFICATION_BOUNDS.bottom, false, 1);
+ final float swipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop() + 1;
+ PointF initCoords =
+ new PointF(INITIAL_MAGNIFICATION_BOUNDS.centerX(),
+ INITIAL_MAGNIFICATION_BOUNDS.centerY());
+ PointF edgeCoords = new PointF(initCoords.x, initCoords.y);
+ edgeCoords.offset(0, -swipeMinDistance);
+
+ allowEventDelegation();
+ swipeAndHold(initCoords, edgeCoords);
+
+ assertTrue(mMgh.mCurrentState == mMgh.mDelegatingState);
+ assertTrue(isZoomed());
+ }
+
+ @Test
@FlakyTest
- public void testScroll_singleHorizontalPanningAndAtEdge_leftEdgeOverscroll() {
+ public void testSingleFingerOverscrollAtLeftEdge_isWatch_expectedOverscrollState() {
assumeTrue(isWatch());
goFromStateIdleTo(STATE_SINGLE_PANNING);
float centerY =
@@ -883,7 +936,7 @@
@Test
@FlakyTest
- public void testScroll_singleHorizontalPanningAndAtEdge_rightEdgeOverscroll() {
+ public void testSingleFingerOverscrollAtRightEdge_isWatch_expectedOverscrollState() {
assumeTrue(isWatch());
goFromStateIdleTo(STATE_SINGLE_PANNING);
float centerY =
@@ -907,7 +960,7 @@
@Test
@FlakyTest
- public void testScroll_singleVerticalPanningAndAtEdge_verticalOverscroll() {
+ public void testSingleFingerOverscrollAtTopEdge_isWatch_expectedOverscrollState() {
assumeTrue(isWatch());
goFromStateIdleTo(STATE_SINGLE_PANNING);
float centerX =
@@ -929,7 +982,7 @@
}
@Test
- public void testScroll_singlePanningAndAtEdge_noOverscroll() {
+ public void testSingleFingerScrollAtEdge_isWatch_noOverscroll() {
assumeTrue(isWatch());
goFromStateIdleTo(STATE_SINGLE_PANNING);
float centerY =
@@ -951,7 +1004,7 @@
}
@Test
- public void testScroll_singleHorizontalPanningAndAtEdge_vibrate() {
+ public void testSingleFingerHorizontalScrollAtEdge_isWatch_vibrate() {
assumeTrue(isWatch());
goFromStateIdleTo(STATE_SINGLE_PANNING);
mFullScreenMagnificationController.setCenter(
@@ -975,7 +1028,7 @@
}
@Test
- public void testScroll_singleVerticalPanningAndAtEdge_doNotVibrate() {
+ public void testSingleFingerVerticalScrollAtEdge_isWatch_doNotVibrate() {
assumeTrue(isWatch());
goFromStateIdleTo(STATE_SINGLE_PANNING);
mFullScreenMagnificationController.setCenter(
@@ -1000,7 +1053,7 @@
@Test
@RequiresFlagsEnabled(Flags.FLAG_FULLSCREEN_FLING_GESTURE)
- public void singleFinger_testScrollAfterMagnified_startsFling() {
+ public void testSingleFingerScrollAfterMagnified_startsFling() {
assumeTrue(isWatch());
goFromStateIdleTo(STATE_ACTIVATED);
@@ -1282,9 +1335,14 @@
}
private void enableOneFingerPanning(boolean enable) {
+ mMockOneFingerPanningEnabled = enable;
when(mMockOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()).thenReturn(enable);
}
+ private boolean isOneFingerPanningEnabled() {
+ return mMockOneFingerPanningEnabled;
+ }
+
private void assertActionsInOrder(List<MotionEvent> actualEvents,
List<Integer> expectedActions) {
assertTrue(actualEvents.size() == expectedActions.size());
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
index 1eaa170..bc2fd73 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
@@ -22,10 +22,11 @@
import static android.hardware.biometrics.BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED;
import static android.hardware.biometrics.BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED;
import static android.hardware.biometrics.BiometricPrompt.DISMISSED_REASON_NEGATIVE;
+import static android.hardware.biometrics.BiometricPrompt.DISMISSED_REASON_USER_CANCEL;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_CALLED;
-import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_STARTED;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_PAUSED;
+import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_STARTED;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_STARTED_UI_SHOWING;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_ERROR_PENDING_SYSUI;
@@ -50,9 +51,9 @@
import android.annotation.NonNull;
import android.app.admin.DevicePolicyManager;
import android.app.trust.ITrustManager;
-import android.content.Context;
import android.content.res.Resources;
import android.hardware.biometrics.BiometricConstants;
+import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricManager.Authenticators;
import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.biometrics.ComponentInfoInternal;
@@ -70,9 +71,12 @@
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import android.security.KeyStoreAuthorization;
+import android.testing.TestableContext;
import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.internal.R;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.biometrics.log.BiometricContext;
@@ -80,6 +84,7 @@
import com.android.server.biometrics.log.OperationContextExt;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -95,8 +100,12 @@
private static final String TEST_PACKAGE = "test_package";
private static final long TEST_REQUEST_ID = 22;
+ private static final String ACQUIRED_STRING = "test_acquired_info_callback";
+ private static final String ACQUIRED_STRING_VENDOR = "test_acquired_info_callback_vendor";
- @Mock private Context mContext;
+ @Rule
+ public final TestableContext mContext = new TestableContext(
+ InstrumentationRegistry.getInstrumentation().getTargetContext(), null);
@Mock private Resources mResources;
@Mock private BiometricContext mBiometricContext;
@Mock private ITrustManager mTrustManager;
@@ -110,6 +119,7 @@
@Mock private AuthSession.ClientDeathReceiver mClientDeathReceiver;
@Mock private BiometricFrameworkStatsLogger mBiometricFrameworkStatsLogger;
@Mock private BiometricCameraManager mBiometricCameraManager;
+ @Mock private BiometricManager mBiometricManager;
private Random mRandom;
private IBinder mToken;
@@ -121,7 +131,11 @@
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- when(mContext.getResources()).thenReturn(mResources);
+ mContext.addMockSystemService(BiometricManager.class, mBiometricManager);
+ mContext.getOrCreateTestableResources().addOverride(R.string.fingerprint_acquired_partial,
+ ACQUIRED_STRING);
+ mContext.getOrCreateTestableResources().addOverride(R.array.fingerprint_acquired_vendor,
+ new String[]{ACQUIRED_STRING_VENDOR});
when(mClientReceiver.asBinder()).thenReturn(mock(Binder.class));
when(mBiometricContext.updateContext(any(), anyBoolean()))
.thenAnswer(invocation -> invocation.getArgument(0));
@@ -499,8 +513,6 @@
@Test
public void testCallbackOnAcquired() throws RemoteException {
- final String acquiredStr = "test_acquired_info_callback";
- final String acquiredStrVendor = "test_acquired_info_callback_vendor";
setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_REAR);
final AuthSession session = createAuthSession(mSensors,
@@ -510,18 +522,15 @@
0 /* operationId */,
0 /* userId */);
- when(mContext.getString(com.android.internal.R.string.fingerprint_acquired_partial))
- .thenReturn(acquiredStr);
session.onAcquired(0, FingerprintManager.FINGERPRINT_ACQUIRED_PARTIAL, 0);
- verify(mStatusBarService).onBiometricHelp(anyInt(), eq(acquiredStr));
- verify(mClientReceiver).onAcquired(eq(1), eq(acquiredStr));
+ verify(mStatusBarService).onBiometricHelp(anyInt(), eq(ACQUIRED_STRING));
+ verify(mClientReceiver).onAcquired(eq(1), eq(ACQUIRED_STRING));
- when(mResources.getStringArray(com.android.internal.R.array.fingerprint_acquired_vendor))
- .thenReturn(new String[]{acquiredStrVendor});
session.onAcquired(0, FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, 0);
- verify(mStatusBarService).onBiometricHelp(anyInt(), eq(acquiredStrVendor));
+ verify(mStatusBarService).onBiometricHelp(anyInt(), eq(ACQUIRED_STRING_VENDOR));
verify(mClientReceiver).onAcquired(
- eq(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR_BASE), eq(acquiredStrVendor));
+ eq(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR_BASE),
+ eq(ACQUIRED_STRING_VENDOR));
}
@Test
@@ -665,6 +674,87 @@
verify(mStatusBarService, never()).onBiometricError(anyInt(), anyInt(), anyInt());
}
+ @Test
+ public void onAuthReceivedWhileWaitingForConfirmation_SFPS() throws Exception {
+ setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_POWER_BUTTON);
+ setupFace(1 /* id */, false, mock(IBiometricAuthenticator.class));
+ final long operationId = 123;
+ final int userId = 10;
+ final AuthSession session = createAuthSession(mSensors,
+ false /* checkDevicePolicyManager */,
+ Authenticators.BIOMETRIC_STRONG,
+ TEST_REQUEST_ID,
+ operationId,
+ userId);
+ session.goToInitialState();
+ for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) {
+ session.onCookieReceived(
+ session.mPreAuthInfo.eligibleSensors.get(sensor.id).getCookie());
+ }
+ session.onDialogAnimatedIn(true /* startFingerprintNow */);
+
+ // Face succeeds
+ session.onAuthenticationSucceeded(1, true, null);
+ verify(mStatusBarService).onBiometricAuthenticated(TYPE_FACE);
+ for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) {
+ if (sensor.modality == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
+ assertEquals(BiometricSensor.STATE_AUTHENTICATING, sensor.getSensorState());
+ }
+ }
+
+ // SFPS succeeds
+ session.onAuthenticationSucceeded(0, true, null);
+ verify(mStatusBarService).onBiometricAuthenticated(TYPE_FINGERPRINT);
+ }
+
+ @Test
+ public void onDialogDismissedResetLockout_Confirmed() throws Exception {
+ setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_POWER_BUTTON);
+ setupFace(1 /* id */, false, mock(IBiometricAuthenticator.class));
+ final long operationId = 123;
+ final int userId = 10;
+ final AuthSession session = createAuthSession(mSensors,
+ false /* checkDevicePolicyManager */,
+ Authenticators.BIOMETRIC_STRONG,
+ TEST_REQUEST_ID,
+ operationId,
+ userId);
+ session.goToInitialState();
+ session.onDialogAnimatedIn(true /* startFingerprintNow */);
+
+ // Face succeeds
+ session.onAuthenticationSucceeded(1, true, new byte[1]);
+
+ // Dismiss through confirmation
+ session.onDialogDismissed(DISMISSED_REASON_BIOMETRIC_CONFIRMED, null);
+
+ verify(mBiometricManager).resetLockoutTimeBound(any(), any(), anyInt(), anyInt(), any());
+ }
+
+ @Test
+ public void onDialogDismissedResetLockout_Cancelled() throws Exception {
+ setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_POWER_BUTTON);
+ setupFace(1 /* id */, false, mock(IBiometricAuthenticator.class));
+ final long operationId = 123;
+ final int userId = 10;
+ final AuthSession session = createAuthSession(mSensors,
+ false /* checkDevicePolicyManager */,
+ Authenticators.BIOMETRIC_STRONG,
+ TEST_REQUEST_ID,
+ operationId,
+ userId);
+ session.goToInitialState();
+ session.onDialogAnimatedIn(true /* startFingerprintNow */);
+
+ // Face succeeds
+ session.onAuthenticationSucceeded(1, true, new byte[1]);
+
+ // User cancel after success
+ session.onDialogDismissed(DISMISSED_REASON_USER_CANCEL, null);
+
+ verify(mBiometricManager).resetLockoutTimeBound(any(), any(), anyInt(), anyInt(), any());
+ }
+
// TODO (b/208484275) : Enable these tests
// @Test
// public void testPreAuth_canAuthAndPrivacyDisabled() throws Exception {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java
index 9873805..2d4dbb7 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java
@@ -92,6 +92,7 @@
import org.mockito.junit.MockitoRule;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
@@ -445,21 +446,61 @@
);
}
+ @Test
+ public void testResetLockoutOnAuthSuccess_nonBiometricPrompt() throws RemoteException {
+ FaceAuthenticationClient client = createClient(false);
+ client.start(mCallback);
+ client.onAuthenticated(new Face("friendly", 1 /* faceId */,
+ 2 /* deviceId */), true /* authenticated */, createHardwareAuthToken());
+
+ verify(mBiometricManager).resetLockoutTimeBound(eq(mToken), eq(mContext.getOpPackageName()),
+ anyInt(), anyInt(), any());
+ }
+
+ @Test
+ public void testNoResetLockoutOnAuthFailure_nonBiometricPrompt() throws RemoteException {
+ FaceAuthenticationClient client = createClient(false);
+ client.start(mCallback);
+ client.onAuthenticated(new Face("friendly", 1 /* faceId */,
+ 2 /* deviceId */), false /* authenticated */, createHardwareAuthToken());
+
+ verify(mBiometricManager, never()).resetLockoutTimeBound(eq(mToken),
+ eq(mContext.getOpPackageName()), anyInt(), anyInt(), any());
+ }
+
+ @Test
+ public void testNoResetLockoutOnAuthSuccess_BiometricPrompt() throws RemoteException {
+ FaceAuthenticationClient client = createClient(true);
+ client.start(mCallback);
+ client.onAuthenticated(new Face("friendly", 1 /* faceId */,
+ 2 /* deviceId */), true /* authenticated */, createHardwareAuthToken());
+
+ verify(mBiometricManager, never()).resetLockoutTimeBound(eq(mToken),
+ eq(mContext.getOpPackageName()), anyInt(), anyInt(), any());
+ }
+
private FaceAuthenticationClient createClient() throws RemoteException {
return createClient(2 /* version */, mClientMonitorCallbackConverter,
- false /* allowBackgroundAuthentication */,
+ false /* allowBackgroundAuthentication */, true /* isBiometricPrompt */,
+ null /* lockoutTracker */);
+ }
+
+ private FaceAuthenticationClient createClient(boolean isBiometricPrompt)
+ throws RemoteException {
+ return createClient(2 /* version */, mClientMonitorCallbackConverter,
+ true /* allowBackgroundAuthentication */, isBiometricPrompt,
null /* lockoutTracker */);
}
private FaceAuthenticationClient createClientWithNullListener() throws RemoteException {
return createClient(2 /* version */, null /* listener */,
- true /* allowBackgroundAuthentication */,
+ false /* allowBackgroundAuthentication */, true /* isBiometricPrompt */,
null /* lockoutTracker */);
}
private FaceAuthenticationClient createClient(int version) throws RemoteException {
return createClient(version, mClientMonitorCallbackConverter,
- false /* allowBackgroundAuthentication */,
+ false /* allowBackgroundAuthentication */, true /* isBiometricPrompt */,
null /* lockoutTracker */);
}
@@ -468,12 +509,14 @@
return createClient(0 /* version */,
mClientMonitorCallbackConverter,
true /* allowBackgroundAuthentication */,
+ true /* isBiometricPrompt */,
lockoutTracker);
}
private FaceAuthenticationClient createClient(int version,
ClientMonitorCallbackConverter listener,
boolean allowBackgroundAuthentication,
+ boolean isBiometricPrompt,
LockoutTracker lockoutTracker) throws RemoteException {
when(mHal.getInterfaceVersion()).thenReturn(version);
@@ -488,7 +531,7 @@
.build();
return new FaceAuthenticationClient(mContext, () -> aidl, mToken,
2 /* requestId */, listener, OP_ID,
- false /* restricted */, options, 4 /* cookie */,
+ false /* restricted */, options, isBiometricPrompt ? 4 : 0 /* cookie */,
false /* requireConfirmation */,
mBiometricLogger, mBiometricContext, true /* isStrongBiometric */,
mUsageStats, lockoutTracker, allowBackgroundAuthentication,
@@ -500,4 +543,8 @@
}
};
}
+
+ private ArrayList<Byte> createHardwareAuthToken() {
+ return new ArrayList<>(Collections.nCopies(69, Byte.valueOf("0")));
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
index 182d603..ecd799f 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
@@ -98,6 +98,7 @@
import java.time.Clock;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
@@ -608,6 +609,45 @@
}
@Test
+ public void testResetLockoutOnAuthSuccess_nonBiometricPrompt() throws RemoteException {
+ final FingerprintAuthenticationClient client = createClient(1 /* version */,
+ true /* allowBackgroundAuthentication */, false /* isBiometricPrompt */,
+ mClientMonitorCallbackConverter, mLockoutTracker);
+ client.start(mCallback);
+ client.onAuthenticated(new Fingerprint("friendly", 1 /* fingerId */,
+ 2 /* deviceId */), true /* authenticated */, createHardwareAuthToken());
+
+ verify(mBiometricManager).resetLockoutTimeBound(eq(mToken), eq(mContext.getOpPackageName()),
+ anyInt(), anyInt(), any());
+ }
+
+ @Test
+ public void testNoResetLockoutOnAuthFailure_nonBiometricPrompt() throws RemoteException {
+ final FingerprintAuthenticationClient client = createClient(1 /* version */,
+ true /* allowBackgroundAuthentication */, false /* isBiometricPrompt */,
+ mClientMonitorCallbackConverter, mLockoutTracker);
+ client.start(mCallback);
+ client.onAuthenticated(new Fingerprint("friendly", 1 /* fingerId */,
+ 2 /* deviceId */), false /* authenticated */, createHardwareAuthToken());
+
+ verify(mBiometricManager, never()).resetLockoutTimeBound(eq(mToken),
+ eq(mContext.getOpPackageName()), anyInt(), anyInt(), any());
+ }
+
+ @Test
+ public void testNoResetLockoutOnAuthSuccess_BiometricPrompt() throws RemoteException {
+ final FingerprintAuthenticationClient client = createClient(1 /* version */,
+ true /* allowBackgroundAuthentication */, true /* isBiometricPrompt */,
+ mClientMonitorCallbackConverter, mLockoutTracker);
+ client.start(mCallback);
+ client.onAuthenticated(new Fingerprint("friendly", 1 /* fingerId */,
+ 2 /* deviceId */), true /* authenticated */, createHardwareAuthToken());
+
+ verify(mBiometricManager, never()).resetLockoutTimeBound(eq(mToken),
+ eq(mContext.getOpPackageName()), anyInt(), anyInt(), any());
+ }
+
+ @Test
public void testOnAuthenticatedFalseWhenListenerIsNull() throws RemoteException {
final FingerprintAuthenticationClient client = createClientWithNullListener();
client.start(mCallback);
@@ -630,11 +670,11 @@
@Test
public void testLockoutTracker_authSuccess() throws RemoteException {
final FingerprintAuthenticationClient client = createClient(1 /* version */,
- true /* allowBackgroundAuthentication */, mClientMonitorCallbackConverter,
- mLockoutTracker);
+ true /* allowBackgroundAuthentication */, false /* isBiometricPrompt */,
+ mClientMonitorCallbackConverter, mLockoutTracker);
client.start(mCallback);
client.onAuthenticated(new Fingerprint("friendly", 1 /* fingerId */,
- 2 /* deviceId */), true /* authenticated */, new ArrayList<>());
+ 2 /* deviceId */), true /* authenticated */, createHardwareAuthToken());
verify(mLockoutTracker).resetFailedAttemptsForUser(true, USER_ID);
verify(mLockoutTracker, never()).addFailedAttemptForUser(anyInt());
@@ -643,11 +683,11 @@
@Test
public void testLockoutTracker_authFailed() throws RemoteException {
final FingerprintAuthenticationClient client = createClient(1 /* version */,
- true /* allowBackgroundAuthentication */, mClientMonitorCallbackConverter,
- mLockoutTracker);
+ true /* allowBackgroundAuthentication */, false /* isBiometricPrompt */,
+ mClientMonitorCallbackConverter, mLockoutTracker);
client.start(mCallback);
client.onAuthenticated(new Fingerprint("friendly", 1 /* fingerId */,
- 2 /* deviceId */), false /* authenticated */, new ArrayList<>());
+ 2 /* deviceId */), false /* authenticated */, createHardwareAuthToken());
verify(mLockoutTracker, never()).resetFailedAttemptsForUser(anyBoolean(), anyInt());
verify(mLockoutTracker).addFailedAttemptForUser(USER_ID);
@@ -655,27 +695,31 @@
private FingerprintAuthenticationClient createClient() throws RemoteException {
return createClient(100 /* version */, true /* allowBackgroundAuthentication */,
+ true /* isBiometricPrompt */,
mClientMonitorCallbackConverter, null);
}
private FingerprintAuthenticationClient createClientWithoutBackgroundAuth()
throws RemoteException {
return createClient(100 /* version */, false /* allowBackgroundAuthentication */,
- mClientMonitorCallbackConverter, null);
+ true /* isBiometricPrompt */, mClientMonitorCallbackConverter, null);
}
private FingerprintAuthenticationClient createClient(int version) throws RemoteException {
return createClient(version, true /* allowBackgroundAuthentication */,
+ true /* isBiometricPrompt */,
mClientMonitorCallbackConverter, null);
}
private FingerprintAuthenticationClient createClientWithNullListener() throws RemoteException {
return createClient(100 /* version */, true /* allowBackgroundAuthentication */,
- null, /* listener */null);
+ true /* isBiometricPrompt */,
+ /* listener */null, null);
}
private FingerprintAuthenticationClient createClient(int version,
- boolean allowBackgroundAuthentication, ClientMonitorCallbackConverter listener,
+ boolean allowBackgroundAuthentication, boolean isBiometricPrompt,
+ ClientMonitorCallbackConverter listener,
LockoutTracker lockoutTracker)
throws RemoteException {
when(mHal.getInterfaceVersion()).thenReturn(version);
@@ -687,7 +731,8 @@
.setSensorId(SENSOR_ID)
.build();
return new FingerprintAuthenticationClient(mContext, () -> aidl, mToken, REQUEST_ID,
- listener, OP_ID, false /* restricted */, options, 4 /* cookie */,
+ listener, OP_ID, false /* restricted */, options,
+ isBiometricPrompt ? 4 : 0 /* cookie */,
false /* requireConfirmation */, mBiometricLogger, mBiometricContext,
true /* isStrongBiometric */, null /* taskStackListener */, mUdfpsOverlayController,
mAuthenticationStateListeners, allowBackgroundAuthentication, mSensorProps,
@@ -698,4 +743,8 @@
}
};
}
+
+ private ArrayList<Byte> createHardwareAuthToken() {
+ return new ArrayList<>(Collections.nCopies(69, Byte.valueOf("0")));
+ }
}