Loading core/java/android/content/pm/PackageManager.java +14 −0 Original line number Diff line number Diff line Loading @@ -2882,6 +2882,20 @@ public abstract class PackageManager { public static final String FEATURE_CAR_TEMPLATES_HOST = "android.software.car.templates_host"; /** * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}:If this * feature is supported, the device should also declare {@link #FEATURE_AUTOMOTIVE} and show * a UI that can display multiple tasks at the same time on a single display. The user can * perform multiple actions on different tasks simultaneously. Apps open in split screen mode * by default, instead of full screen. Unlike Android's multi-window mode, where users can * choose how to display apps, the device determines how apps are shown. * * @hide */ @SdkConstant(SdkConstantType.FEATURE) public static final String FEATURE_CAR_SPLITSCREEN_MULTITASKING = "android.software.car.splitscreen_multitasking"; /** * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature(String, int)}: If this feature is supported, the device supports Loading libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java +12 −3 Original line number Diff line number Diff line Loading @@ -65,9 +65,18 @@ public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithmInterfac } Rect pipBounds = new Rect(startingBounds); // move PiP towards corner if user hasn't moved it manually or the flag is on if (mKeepClearAreaGravityEnabled || (!pipBoundsState.hasUserMovedPip() && !pipBoundsState.hasUserResizedPip())) { boolean shouldApplyGravity = false; // if PiP is outside of screen insets, reposition using gravity if (!insets.contains(pipBounds)) { shouldApplyGravity = true; } // if user has not interacted with PiP, reposition using gravity if (!pipBoundsState.hasUserMovedPip() && !pipBoundsState.hasUserResizedPip()) { shouldApplyGravity = true; } // apply gravity that will position PiP in bottom left or bottom right corner within insets if (mKeepClearAreaGravityEnabled || shouldApplyGravity) { float snapFraction = pipBoundsAlgorithm.getSnapFraction(startingBounds); int verticalGravity = Gravity.BOTTOM; int horizontalGravity; Loading packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java +13 −1 Original line number Diff line number Diff line Loading @@ -19,9 +19,12 @@ package com.android.systemui.biometrics.ui; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.graphics.Insets; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.WindowInsets; import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; Loading @@ -44,6 +47,8 @@ public class BiometricPromptLayout extends LinearLayout { private static final String TAG = "BiometricPromptLayout"; @NonNull private final WindowManager mWindowManager; @Nullable private AuthController.ScaleFactorProvider mScaleFactorProvider; @Nullable Loading @@ -60,6 +65,8 @@ public class BiometricPromptLayout extends LinearLayout { public BiometricPromptLayout(Context context, AttributeSet attrs) { super(context, attrs); mWindowManager = context.getSystemService(WindowManager.class); mUseCustomBpSize = getResources().getBoolean(R.bool.use_custom_bp_size); mCustomBpWidth = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_width); mCustomBpHeight = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_height); Loading Loading @@ -144,8 +151,13 @@ public class BiometricPromptLayout extends LinearLayout { width = Math.min(width, height); } // add nav bar insets since the parent AuthContainerView // uses LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS final Insets insets = mWindowManager.getMaximumWindowMetrics().getWindowInsets() .getInsets(WindowInsets.Type.navigationBars()); final AuthDialog.LayoutParams params = onMeasureInternal(width, height); setMeasuredDimension(params.mMediumWidth, params.mMediumHeight); setMeasuredDimension(params.mMediumWidth + insets.left + insets.right, params.mMediumHeight + insets.bottom); } @Override Loading packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt +11 −1 Original line number Diff line number Diff line Loading @@ -16,4 +16,14 @@ package com.android.systemui.bouncer.ui.viewmodel sealed interface AuthMethodBouncerViewModel import kotlinx.coroutines.flow.StateFlow sealed interface AuthMethodBouncerViewModel { /** * Whether user input is enabled. * * If `false`, user input should be completely ignored in the UI as the user is "locked out" of * being able to attempt to unlock the device. */ val isInputEnabled: StateFlow<Boolean> } packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt +80 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.bouncer.ui.viewmodel import android.content.Context import com.android.systemui.R import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.dagger.qualifiers.Application Loading @@ -24,10 +25,14 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch /** Holds UI state and handles user input on bouncer UIs. */ class BouncerViewModel Loading @@ -40,16 +45,42 @@ constructor( ) { private val interactor: BouncerInteractor = interactorFactory.create(containerName) /** * Whether updates to the message should be cross-animated from one message to another. * * If `false`, no animation should be applied, the message text should just be replaced * instantly. */ val isMessageUpdateAnimationsEnabled: StateFlow<Boolean> = interactor.throttling .map { it == null } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = interactor.throttling.value == null, ) private val isInputEnabled: StateFlow<Boolean> = interactor.throttling .map { it == null } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = interactor.throttling.value == null, ) private val pin: PinBouncerViewModel by lazy { PinBouncerViewModel( applicationScope = applicationScope, interactor = interactor, isInputEnabled = isInputEnabled, ) } private val password: PasswordBouncerViewModel by lazy { PasswordBouncerViewModel( interactor = interactor, isInputEnabled = isInputEnabled, ) } Loading @@ -58,6 +89,7 @@ constructor( applicationContext = applicationContext, applicationScope = applicationScope, interactor = interactor, isInputEnabled = isInputEnabled, ) } Loading @@ -81,11 +113,59 @@ constructor( initialValue = interactor.message.value ?: "", ) private val _throttlingDialogMessage = MutableStateFlow<String?>(null) /** * A message for a throttling dialog to show when the user has attempted the wrong credential * too many times and now must wait a while before attempting again. * * If `null`, no dialog should be shown. * * Once the dialog is shown, the UI should call [onThrottlingDialogDismissed] when the user * dismisses this dialog. */ val throttlingDialogMessage: StateFlow<String?> = _throttlingDialogMessage.asStateFlow() init { applicationScope.launch { interactor.throttling .map { model -> model?.let { when (interactor.authenticationMethod.value) { is AuthenticationMethodModel.PIN -> R.string.kg_too_many_failed_pin_attempts_dialog_message is AuthenticationMethodModel.Password -> R.string.kg_too_many_failed_password_attempts_dialog_message is AuthenticationMethodModel.Pattern -> R.string.kg_too_many_failed_pattern_attempts_dialog_message else -> null }?.let { stringResourceId -> applicationContext.getString( stringResourceId, model.failedAttemptCount, model.totalDurationSec, ) } } } .distinctUntilChanged() .collect { dialogMessageOrNull -> if (dialogMessageOrNull != null) { _throttlingDialogMessage.value = dialogMessageOrNull } } } } /** Notifies that the emergency services button was clicked. */ fun onEmergencyServicesButtonClicked() { // TODO(b/280877228): implement this } /** Notifies that a throttling dialog has been dismissed by the user. */ fun onThrottlingDialogDismissed() { _throttlingDialogMessage.value = null } private fun toViewModel( authMethod: AuthenticationMethodModel, ): AuthMethodBouncerViewModel? { Loading Loading
core/java/android/content/pm/PackageManager.java +14 −0 Original line number Diff line number Diff line Loading @@ -2882,6 +2882,20 @@ public abstract class PackageManager { public static final String FEATURE_CAR_TEMPLATES_HOST = "android.software.car.templates_host"; /** * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}:If this * feature is supported, the device should also declare {@link #FEATURE_AUTOMOTIVE} and show * a UI that can display multiple tasks at the same time on a single display. The user can * perform multiple actions on different tasks simultaneously. Apps open in split screen mode * by default, instead of full screen. Unlike Android's multi-window mode, where users can * choose how to display apps, the device determines how apps are shown. * * @hide */ @SdkConstant(SdkConstantType.FEATURE) public static final String FEATURE_CAR_SPLITSCREEN_MULTITASKING = "android.software.car.splitscreen_multitasking"; /** * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature(String, int)}: If this feature is supported, the device supports Loading
libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java +12 −3 Original line number Diff line number Diff line Loading @@ -65,9 +65,18 @@ public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithmInterfac } Rect pipBounds = new Rect(startingBounds); // move PiP towards corner if user hasn't moved it manually or the flag is on if (mKeepClearAreaGravityEnabled || (!pipBoundsState.hasUserMovedPip() && !pipBoundsState.hasUserResizedPip())) { boolean shouldApplyGravity = false; // if PiP is outside of screen insets, reposition using gravity if (!insets.contains(pipBounds)) { shouldApplyGravity = true; } // if user has not interacted with PiP, reposition using gravity if (!pipBoundsState.hasUserMovedPip() && !pipBoundsState.hasUserResizedPip()) { shouldApplyGravity = true; } // apply gravity that will position PiP in bottom left or bottom right corner within insets if (mKeepClearAreaGravityEnabled || shouldApplyGravity) { float snapFraction = pipBoundsAlgorithm.getSnapFraction(startingBounds); int verticalGravity = Gravity.BOTTOM; int horizontalGravity; Loading
packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java +13 −1 Original line number Diff line number Diff line Loading @@ -19,9 +19,12 @@ package com.android.systemui.biometrics.ui; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.graphics.Insets; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.WindowInsets; import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; Loading @@ -44,6 +47,8 @@ public class BiometricPromptLayout extends LinearLayout { private static final String TAG = "BiometricPromptLayout"; @NonNull private final WindowManager mWindowManager; @Nullable private AuthController.ScaleFactorProvider mScaleFactorProvider; @Nullable Loading @@ -60,6 +65,8 @@ public class BiometricPromptLayout extends LinearLayout { public BiometricPromptLayout(Context context, AttributeSet attrs) { super(context, attrs); mWindowManager = context.getSystemService(WindowManager.class); mUseCustomBpSize = getResources().getBoolean(R.bool.use_custom_bp_size); mCustomBpWidth = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_width); mCustomBpHeight = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_height); Loading Loading @@ -144,8 +151,13 @@ public class BiometricPromptLayout extends LinearLayout { width = Math.min(width, height); } // add nav bar insets since the parent AuthContainerView // uses LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS final Insets insets = mWindowManager.getMaximumWindowMetrics().getWindowInsets() .getInsets(WindowInsets.Type.navigationBars()); final AuthDialog.LayoutParams params = onMeasureInternal(width, height); setMeasuredDimension(params.mMediumWidth, params.mMediumHeight); setMeasuredDimension(params.mMediumWidth + insets.left + insets.right, params.mMediumHeight + insets.bottom); } @Override Loading
packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt +11 −1 Original line number Diff line number Diff line Loading @@ -16,4 +16,14 @@ package com.android.systemui.bouncer.ui.viewmodel sealed interface AuthMethodBouncerViewModel import kotlinx.coroutines.flow.StateFlow sealed interface AuthMethodBouncerViewModel { /** * Whether user input is enabled. * * If `false`, user input should be completely ignored in the UI as the user is "locked out" of * being able to attempt to unlock the device. */ val isInputEnabled: StateFlow<Boolean> }
packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt +80 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.bouncer.ui.viewmodel import android.content.Context import com.android.systemui.R import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.dagger.qualifiers.Application Loading @@ -24,10 +25,14 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch /** Holds UI state and handles user input on bouncer UIs. */ class BouncerViewModel Loading @@ -40,16 +45,42 @@ constructor( ) { private val interactor: BouncerInteractor = interactorFactory.create(containerName) /** * Whether updates to the message should be cross-animated from one message to another. * * If `false`, no animation should be applied, the message text should just be replaced * instantly. */ val isMessageUpdateAnimationsEnabled: StateFlow<Boolean> = interactor.throttling .map { it == null } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = interactor.throttling.value == null, ) private val isInputEnabled: StateFlow<Boolean> = interactor.throttling .map { it == null } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = interactor.throttling.value == null, ) private val pin: PinBouncerViewModel by lazy { PinBouncerViewModel( applicationScope = applicationScope, interactor = interactor, isInputEnabled = isInputEnabled, ) } private val password: PasswordBouncerViewModel by lazy { PasswordBouncerViewModel( interactor = interactor, isInputEnabled = isInputEnabled, ) } Loading @@ -58,6 +89,7 @@ constructor( applicationContext = applicationContext, applicationScope = applicationScope, interactor = interactor, isInputEnabled = isInputEnabled, ) } Loading @@ -81,11 +113,59 @@ constructor( initialValue = interactor.message.value ?: "", ) private val _throttlingDialogMessage = MutableStateFlow<String?>(null) /** * A message for a throttling dialog to show when the user has attempted the wrong credential * too many times and now must wait a while before attempting again. * * If `null`, no dialog should be shown. * * Once the dialog is shown, the UI should call [onThrottlingDialogDismissed] when the user * dismisses this dialog. */ val throttlingDialogMessage: StateFlow<String?> = _throttlingDialogMessage.asStateFlow() init { applicationScope.launch { interactor.throttling .map { model -> model?.let { when (interactor.authenticationMethod.value) { is AuthenticationMethodModel.PIN -> R.string.kg_too_many_failed_pin_attempts_dialog_message is AuthenticationMethodModel.Password -> R.string.kg_too_many_failed_password_attempts_dialog_message is AuthenticationMethodModel.Pattern -> R.string.kg_too_many_failed_pattern_attempts_dialog_message else -> null }?.let { stringResourceId -> applicationContext.getString( stringResourceId, model.failedAttemptCount, model.totalDurationSec, ) } } } .distinctUntilChanged() .collect { dialogMessageOrNull -> if (dialogMessageOrNull != null) { _throttlingDialogMessage.value = dialogMessageOrNull } } } } /** Notifies that the emergency services button was clicked. */ fun onEmergencyServicesButtonClicked() { // TODO(b/280877228): implement this } /** Notifies that a throttling dialog has been dismissed by the user. */ fun onThrottlingDialogDismissed() { _throttlingDialogMessage.value = null } private fun toViewModel( authMethod: AuthenticationMethodModel, ): AuthMethodBouncerViewModel? { Loading