Merge "Adjust clock switch animation timing" into udc-qpr-dev
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 7f19897..8fafb18 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -2883,6 +2883,20 @@
"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
* {@link android.security.identity.IdentityCredentialStore} implemented in secure hardware
diff --git a/core/java/android/view/contentcapture/ContentCaptureSession.java b/core/java/android/view/contentcapture/ContentCaptureSession.java
index 62044aa..b229106 100644
--- a/core/java/android/view/contentcapture/ContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/ContentCaptureSession.java
@@ -181,6 +181,8 @@
public static final int FLUSH_REASON_VIEW_TREE_APPEARING = 9;
/** @hide */
public static final int FLUSH_REASON_VIEW_TREE_APPEARED = 10;
+ /** @hide */
+ public static final int FLUSH_REASON_LOGIN_DETECTED = 11;
/**
* After {@link UPSIDE_DOWN_CAKE}, {@link #notifyViewsDisappeared(AutofillId, long[])} wraps
@@ -191,20 +193,23 @@
static final long NOTIFY_NODES_DISAPPEAR_NOW_SENDS_TREE_EVENTS = 258825825L;
/** @hide */
- @IntDef(prefix = { "FLUSH_REASON_" }, value = {
- FLUSH_REASON_FULL,
- FLUSH_REASON_VIEW_ROOT_ENTERED,
- FLUSH_REASON_SESSION_STARTED,
- FLUSH_REASON_SESSION_FINISHED,
- FLUSH_REASON_IDLE_TIMEOUT,
- FLUSH_REASON_TEXT_CHANGE_TIMEOUT,
- FLUSH_REASON_SESSION_CONNECTED,
- FLUSH_REASON_FORCE_FLUSH,
- FLUSH_REASON_VIEW_TREE_APPEARING,
- FLUSH_REASON_VIEW_TREE_APPEARED
- })
+ @IntDef(
+ prefix = {"FLUSH_REASON_"},
+ value = {
+ FLUSH_REASON_FULL,
+ FLUSH_REASON_VIEW_ROOT_ENTERED,
+ FLUSH_REASON_SESSION_STARTED,
+ FLUSH_REASON_SESSION_FINISHED,
+ FLUSH_REASON_IDLE_TIMEOUT,
+ FLUSH_REASON_TEXT_CHANGE_TIMEOUT,
+ FLUSH_REASON_SESSION_CONNECTED,
+ FLUSH_REASON_FORCE_FLUSH,
+ FLUSH_REASON_VIEW_TREE_APPEARING,
+ FLUSH_REASON_VIEW_TREE_APPEARED,
+ FLUSH_REASON_LOGIN_DETECTED
+ })
@Retention(RetentionPolicy.SOURCE)
- public @interface FlushReason{}
+ public @interface FlushReason {}
private final Object mLock = new Object();
@@ -685,8 +690,10 @@
return "VIEW_TREE_APPEARING";
case FLUSH_REASON_VIEW_TREE_APPEARED:
return "VIEW_TREE_APPEARED";
+ case FLUSH_REASON_LOGIN_DETECTED:
+ return "LOGIN_DETECTED";
default:
- return "UNKOWN-" + reason;
+ return "UNKNOWN-" + reason;
}
}
diff --git a/core/java/android/view/contentcapture/ViewNode.java b/core/java/android/view/contentcapture/ViewNode.java
index 044a31f..f218995 100644
--- a/core/java/android/view/contentcapture/ViewNode.java
+++ b/core/java/android/view/contentcapture/ViewNode.java
@@ -480,6 +480,11 @@
return mLocaleList;
}
+ /** @hide */
+ public void setTextIdEntry(@NonNull String textIdEntry) {
+ mTextIdEntry = textIdEntry;
+ }
+
private void writeSelfToParcel(@NonNull Parcel parcel, int parcelFlags) {
long nodeFlags = mFlags;
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 79bb97d..a4643d0 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5132,4 +5132,5 @@
<java-symbol type="style" name="ThemeOverlay.DeviceDefault.Dark.ActionBar.Accent" />
<java-symbol type="drawable" name="focus_event_pressed_key_background" />
+ <java-symbol type="string" name="lockscreen_too_many_failed_attempts_countdown" />
</resources>
diff --git a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java
index 27d58b8..f8ebd09 100644
--- a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java
@@ -27,6 +27,8 @@
import android.view.autofill.AutofillId;
import android.view.contentcapture.ViewNode.ViewStructureImpl;
+import com.google.common.collect.ImmutableMap;
+
import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
@@ -37,6 +39,8 @@
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
+import java.util.Map;
+
/**
* Unit tests for {@link ContentCaptureSession}.
*
@@ -145,6 +149,35 @@
assertThat(session.mInternalNotifyViewTreeEventFinishedCount).isEqualTo(1);
}
+ @Test
+ public void testGetFlushReasonAsString() {
+ int invalidFlushReason = ContentCaptureSession.FLUSH_REASON_LOGIN_DETECTED + 1;
+ Map<Integer, String> expectedMap =
+ new ImmutableMap.Builder<Integer, String>()
+ .put(ContentCaptureSession.FLUSH_REASON_FULL, "FULL")
+ .put(ContentCaptureSession.FLUSH_REASON_VIEW_ROOT_ENTERED, "VIEW_ROOT")
+ .put(ContentCaptureSession.FLUSH_REASON_SESSION_STARTED, "STARTED")
+ .put(ContentCaptureSession.FLUSH_REASON_SESSION_FINISHED, "FINISHED")
+ .put(ContentCaptureSession.FLUSH_REASON_IDLE_TIMEOUT, "IDLE")
+ .put(ContentCaptureSession.FLUSH_REASON_TEXT_CHANGE_TIMEOUT, "TEXT_CHANGE")
+ .put(ContentCaptureSession.FLUSH_REASON_SESSION_CONNECTED, "CONNECTED")
+ .put(ContentCaptureSession.FLUSH_REASON_FORCE_FLUSH, "FORCE_FLUSH")
+ .put(
+ ContentCaptureSession.FLUSH_REASON_VIEW_TREE_APPEARING,
+ "VIEW_TREE_APPEARING")
+ .put(
+ ContentCaptureSession.FLUSH_REASON_VIEW_TREE_APPEARED,
+ "VIEW_TREE_APPEARED")
+ .put(ContentCaptureSession.FLUSH_REASON_LOGIN_DETECTED, "LOGIN_DETECTED")
+ .put(invalidFlushReason, "UNKOWN-" + invalidFlushReason)
+ .build();
+
+ expectedMap.forEach(
+ (reason, expected) ->
+ assertThat(ContentCaptureSession.getFlushReasonAsString(reason))
+ .isEqualTo(expected));
+ }
+
// Cannot use @Spy because we need to pass the session id on constructor
private class MyContentCaptureSession extends ContentCaptureSession {
int mInternalNotifyViewTreeEventStartedCount = 0;
diff --git a/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java b/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java
index 93315f1..a4e77f5 100644
--- a/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java
@@ -41,49 +41,60 @@
private final Context mContext = InstrumentationRegistry.getTargetContext();
+ private final View mView = new View(mContext);
+
+ private final ViewStructureImpl mViewStructure = new ViewStructureImpl(mView);
+
+ private final ViewNode mViewNode = mViewStructure.getNode();
+
@Mock
private HtmlInfo mHtmlInfoMock;
@Test
public void testUnsupportedProperties() {
- View view = new View(mContext);
+ mViewStructure.setChildCount(1);
+ assertThat(mViewNode.getChildCount()).isEqualTo(0);
- ViewStructureImpl structure = new ViewStructureImpl(view);
- ViewNode node = structure.getNode();
+ mViewStructure.addChildCount(1);
+ assertThat(mViewNode.getChildCount()).isEqualTo(0);
- structure.setChildCount(1);
- assertThat(node.getChildCount()).isEqualTo(0);
+ assertThat(mViewStructure.newChild(0)).isNull();
+ assertThat(mViewNode.getChildCount()).isEqualTo(0);
- structure.addChildCount(1);
- assertThat(node.getChildCount()).isEqualTo(0);
+ assertThat(mViewStructure.asyncNewChild(0)).isNull();
+ assertThat(mViewNode.getChildCount()).isEqualTo(0);
- assertThat(structure.newChild(0)).isNull();
- assertThat(node.getChildCount()).isEqualTo(0);
+ mViewStructure.asyncCommit();
+ assertThat(mViewNode.getChildCount()).isEqualTo(0);
- assertThat(structure.asyncNewChild(0)).isNull();
- assertThat(node.getChildCount()).isEqualTo(0);
+ mViewStructure.setWebDomain("Y U NO SET?");
+ assertThat(mViewNode.getWebDomain()).isNull();
- structure.asyncCommit();
- assertThat(node.getChildCount()).isEqualTo(0);
+ assertThat(mViewStructure.newHtmlInfoBuilder("WHATEVER")).isNull();
- structure.setWebDomain("Y U NO SET?");
- assertThat(node.getWebDomain()).isNull();
+ mViewStructure.setHtmlInfo(mHtmlInfoMock);
+ assertThat(mViewNode.getHtmlInfo()).isNull();
- assertThat(structure.newHtmlInfoBuilder("WHATEVER")).isNull();
+ mViewStructure.setDataIsSensitive(true);
- structure.setHtmlInfo(mHtmlInfoMock);
- assertThat(node.getHtmlInfo()).isNull();
-
- structure.setDataIsSensitive(true);
-
- assertThat(structure.getTempRect()).isNull();
+ assertThat(mViewStructure.getTempRect()).isNull();
// Graphic properties
- structure.setElevation(6.66f);
- assertThat(node.getElevation()).isWithin(1.0e-10f).of(0f);
- structure.setAlpha(66.6f);
- assertThat(node.getAlpha()).isWithin(1.0e-10f).of(1.0f);
- structure.setTransformation(Matrix.IDENTITY_MATRIX);
- assertThat(node.getTransformation()).isNull();
+ mViewStructure.setElevation(6.66f);
+ assertThat(mViewNode.getElevation()).isEqualTo(0f);
+ mViewStructure.setAlpha(66.6f);
+ assertThat(mViewNode.getAlpha()).isEqualTo(1.0f);
+ mViewStructure.setTransformation(Matrix.IDENTITY_MATRIX);
+ assertThat(mViewNode.getTransformation()).isNull();
+ }
+
+ @Test
+ public void testGetSet_textIdEntry() {
+ assertThat(mViewNode.getTextIdEntry()).isNull();
+
+ String expected = "TEXT_ID_ENTRY";
+ mViewNode.setTextIdEntry(expected);
+
+ assertThat(mViewNode.getTextIdEntry()).isEqualTo(expected);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
index ed8dc7de..fc674a8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
@@ -65,9 +65,18 @@
}
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;
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index 9d9a87d..c684dc5 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -51,6 +51,12 @@
*/
val isBypassEnabled: StateFlow<Boolean>
+ /**
+ * Number of consecutively failed authentication attempts. This resets to `0` when
+ * authentication succeeds.
+ */
+ val failedAuthenticationAttempts: StateFlow<Int>
+
/** See [isUnlocked]. */
fun setUnlocked(isUnlocked: Boolean)
@@ -59,6 +65,9 @@
/** See [isBypassEnabled]. */
fun setBypassEnabled(isBypassEnabled: Boolean)
+
+ /** See [failedAuthenticationAttempts]. */
+ fun setFailedAuthenticationAttempts(failedAuthenticationAttempts: Int)
}
class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationRepository {
@@ -75,6 +84,10 @@
private val _isBypassEnabled = MutableStateFlow(false)
override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled.asStateFlow()
+ private val _failedAuthenticationAttempts = MutableStateFlow(0)
+ override val failedAuthenticationAttempts: StateFlow<Int> =
+ _failedAuthenticationAttempts.asStateFlow()
+
override fun setUnlocked(isUnlocked: Boolean) {
_isUnlocked.value = isUnlocked
}
@@ -86,6 +99,10 @@
override fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) {
_authenticationMethod.value = authenticationMethod
}
+
+ override fun setFailedAuthenticationAttempts(failedAuthenticationAttempts: Int) {
+ _failedAuthenticationAttempts.value = failedAuthenticationAttempts
+ }
}
@Module
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 5aea930..3984627 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -75,6 +75,12 @@
*/
val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled
+ /**
+ * Number of consecutively failed authentication attempts. This resets to `0` when
+ * authentication succeeds.
+ */
+ val failedAuthenticationAttempts: StateFlow<Int> = repository.failedAuthenticationAttempts
+
init {
// UNLOCKS WHEN AUTH METHOD REMOVED.
//
@@ -130,7 +136,12 @@
}
if (isSuccessful) {
+ repository.setFailedAuthenticationAttempts(0)
repository.setUnlocked(true)
+ } else {
+ repository.setFailedAuthenticationAttempts(
+ repository.failedAuthenticationAttempts.value + 1
+ )
}
return isSuccessful
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
index 83250b6..6f008c3 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
@@ -36,8 +36,10 @@
data class Password(val password: String) : AuthenticationMethodModel(isSecure = true)
- data class Pattern(val coordinates: List<PatternCoordinate>) :
- AuthenticationMethodModel(isSecure = true) {
+ data class Pattern(
+ val coordinates: List<PatternCoordinate>,
+ val isPatternVisible: Boolean = true,
+ ) : AuthenticationMethodModel(isSecure = true) {
data class PatternCoordinate(
val x: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
index 3753d10..fb246cd 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
@@ -19,9 +19,12 @@
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;
@@ -44,6 +47,8 @@
private static final String TAG = "BiometricPromptLayout";
+ @NonNull
+ private final WindowManager mWindowManager;
@Nullable
private AuthController.ScaleFactorProvider mScaleFactorProvider;
@Nullable
@@ -60,6 +65,8 @@
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);
@@ -144,8 +151,13 @@
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
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt
index 4c817b2..49a0a3c 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt
@@ -16,6 +16,7 @@
package com.android.systemui.bouncer.data.repo
+import com.android.systemui.bouncer.shared.model.AuthenticationThrottledModel
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
@@ -29,7 +30,15 @@
/** The user-facing message to show in the bouncer. */
val message: StateFlow<String?> = _message.asStateFlow()
+ private val _throttling = MutableStateFlow<AuthenticationThrottledModel?>(null)
+ /** The current authentication throttling state. If `null`, there's no throttling. */
+ val throttling: StateFlow<AuthenticationThrottledModel?> = _throttling.asStateFlow()
+
fun setMessage(message: String?) {
_message.value = message
}
+
+ fun setThrottling(throttling: AuthenticationThrottledModel?) {
+ _throttling.value = throttling
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index 8264fed..e462e2f 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -17,10 +17,12 @@
package com.android.systemui.bouncer.domain.interactor
import android.content.Context
+import androidx.annotation.VisibleForTesting
import com.android.systemui.R
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.data.repo.BouncerRepository
+import com.android.systemui.bouncer.shared.model.AuthenticationThrottledModel
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.SceneKey
@@ -29,8 +31,11 @@
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/** Encapsulates business logic and application state accessing use-cases. */
@@ -46,7 +51,22 @@
) {
/** The user-facing message to show in the bouncer. */
- val message: StateFlow<String?> = repository.message
+ val message: StateFlow<String?> =
+ combine(
+ repository.message,
+ repository.throttling,
+ ) { message, throttling ->
+ messageOrThrottlingMessage(message, throttling)
+ }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue =
+ messageOrThrottlingMessage(
+ repository.message.value,
+ repository.throttling.value,
+ )
+ )
/**
* The currently-configured authentication method. This determines how the authentication
@@ -55,6 +75,9 @@
val authenticationMethod: StateFlow<AuthenticationMethodModel> =
authenticationInteractor.authenticationMethod
+ /** The current authentication throttling state. If `null`, there's no throttling. */
+ val throttling: StateFlow<AuthenticationThrottledModel?> = repository.throttling
+
init {
applicationScope.launch {
combine(
@@ -129,14 +152,39 @@
fun authenticate(
input: List<Any>,
) {
+ if (repository.throttling.value != null) {
+ return
+ }
+
val isAuthenticated = authenticationInteractor.authenticate(input)
- if (isAuthenticated) {
- sceneInteractor.setCurrentScene(
- containerName = containerName,
- scene = SceneModel(SceneKey.Gone),
- )
- } else {
- repository.setMessage(errorMessage(authenticationMethod.value))
+ val failedAttempts = authenticationInteractor.failedAuthenticationAttempts.value
+ when {
+ isAuthenticated -> {
+ repository.setThrottling(null)
+ sceneInteractor.setCurrentScene(
+ containerName = containerName,
+ scene = SceneModel(SceneKey.Gone),
+ )
+ }
+ failedAttempts >= THROTTLE_AGGRESSIVELY_AFTER || failedAttempts % THROTTLE_EVERY == 0 ->
+ applicationScope.launch {
+ var remainingDurationSec = THROTTLE_DURATION_SEC
+ while (remainingDurationSec > 0) {
+ repository.setThrottling(
+ AuthenticationThrottledModel(
+ failedAttemptCount = failedAttempts,
+ totalDurationSec = THROTTLE_DURATION_SEC,
+ remainingDurationSec = remainingDurationSec,
+ )
+ )
+ remainingDurationSec--
+ delay(1000)
+ }
+
+ repository.setThrottling(null)
+ clearMessage()
+ }
+ else -> repository.setMessage(errorMessage(authenticationMethod.value))
}
}
@@ -163,10 +211,31 @@
}
}
+ private fun messageOrThrottlingMessage(
+ message: String?,
+ throttling: AuthenticationThrottledModel?,
+ ): String {
+ return when {
+ throttling != null ->
+ applicationContext.getString(
+ com.android.internal.R.string.lockscreen_too_many_failed_attempts_countdown,
+ throttling.remainingDurationSec,
+ )
+ message != null -> message
+ else -> ""
+ }
+ }
+
@AssistedFactory
interface Factory {
fun create(
containerName: String,
): BouncerInteractor
}
+
+ companion object {
+ @VisibleForTesting const val THROTTLE_DURATION_SEC = 30
+ @VisibleForTesting const val THROTTLE_AGGRESSIVELY_AFTER = 15
+ @VisibleForTesting const val THROTTLE_EVERY = 5
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/AuthenticationThrottledModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/AuthenticationThrottledModel.kt
new file mode 100644
index 0000000..cbea635
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/AuthenticationThrottledModel.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.shared.model
+
+/**
+ * Models application state for when further authentication attempts are being throttled due to too
+ * many consecutive failed authentication attempts.
+ */
+data class AuthenticationThrottledModel(
+ /** Total number of failed attempts so far. */
+ val failedAttemptCount: Int,
+ /** Total amount of time the user has to wait before attempting again. */
+ val totalDurationSec: Int,
+ /** Remaining amount of time the user has to wait before attempting again. */
+ val remainingDurationSec: Int,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
index ebefb78..774a559 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
@@ -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>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
index c6528d0..02991bd 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
@@ -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
@@ -24,10 +25,14 @@
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
@@ -40,16 +45,42 @@
) {
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,
)
}
@@ -58,6 +89,7 @@
applicationContext = applicationContext,
applicationScope = applicationScope,
interactor = interactor,
+ isInputEnabled = isInputEnabled,
)
}
@@ -81,11 +113,59 @@
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? {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
index 730d4e8..c38fcaa 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
@@ -24,6 +24,7 @@
/** Holds UI state and handles user input for the password bouncer UI. */
class PasswordBouncerViewModel(
private val interactor: BouncerInteractor,
+ override val isInputEnabled: StateFlow<Boolean>,
) : AuthMethodBouncerViewModel {
private val _password = MutableStateFlow("")
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
index eb1b457..1b0b38e 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
@@ -37,6 +37,7 @@
private val applicationContext: Context,
applicationScope: CoroutineScope,
private val interactor: BouncerInteractor,
+ override val isInputEnabled: StateFlow<Boolean>,
) : AuthMethodBouncerViewModel {
/** The number of columns in the dot grid. */
@@ -63,6 +64,16 @@
/** All dots on the grid. */
val dots: StateFlow<List<PatternDotViewModel>> = _dots.asStateFlow()
+ /** Whether the pattern itself should be rendered visibly. */
+ val isPatternVisible: StateFlow<Boolean> =
+ interactor.authenticationMethod
+ .map { authMethod -> isPatternVisible(authMethod) }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = isPatternVisible(interactor.authenticationMethod.value),
+ )
+
/** Notifies that the UI has been shown to the user. */
fun onShown() {
interactor.resetMessage()
@@ -146,6 +157,10 @@
_selectedDots.value = linkedSetOf()
}
+ private fun isPatternVisible(authMethodModel: AuthenticationMethodModel): Boolean {
+ return (authMethodModel as? AuthenticationMethodModel.Pattern)?.isPatternVisible ?: false
+ }
+
private fun defaultDots(): List<PatternDotViewModel> {
return buildList {
(0 until columnCount).forEach { x ->
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index f9223cb..2a733d9 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -33,6 +33,7 @@
class PinBouncerViewModel(
private val applicationScope: CoroutineScope,
private val interactor: BouncerInteractor,
+ override val isInputEnabled: StateFlow<Boolean>,
) : AuthMethodBouncerViewModel {
private val entered = MutableStateFlow<List<Int>>(emptyList())
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index ee23c51..23cc182 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -237,7 +237,7 @@
/** Whether to delay showing bouncer UI when face auth or active unlock are enrolled. */
// TODO(b/279794160): Tracking bug.
@JvmField
- val DELAY_BOUNCER = releasedFlag(235, "delay_bouncer")
+ val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer")
/** Migrate the indication area to the new keyguard root view. */
// TODO(b/280067944): Tracking bug.
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
index 5079487..1469d96 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
@@ -786,10 +786,10 @@
// Song name
var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
- if (song == null) {
+ if (song.isNullOrBlank()) {
song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE)
}
- if (song == null) {
+ if (song.isNullOrBlank()) {
song = HybridGroupManager.resolveTitle(notif)
}
if (song.isNullOrBlank()) {
@@ -846,7 +846,7 @@
// Artist name
var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST)
- if (artist == null) {
+ if (artist.isNullOrBlank()) {
artist = HybridGroupManager.resolveText(notif)
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index bbc86c8..cfb581b 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -39,6 +39,7 @@
import android.graphics.Rect;
import android.graphics.Region;
import android.hardware.input.InputManager;
+import android.icu.text.SimpleDateFormat;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
@@ -101,7 +102,9 @@
import java.io.PrintWriter;
import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Date;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Executor;
@@ -287,6 +290,8 @@
private LogArray mPredictionLog = new LogArray(MAX_NUM_LOGGED_PREDICTIONS);
private LogArray mGestureLogInsideInsets = new LogArray(MAX_NUM_LOGGED_GESTURES);
private LogArray mGestureLogOutsideInsets = new LogArray(MAX_NUM_LOGGED_GESTURES);
+ private SimpleDateFormat mLogDateFormat = new SimpleDateFormat("HH:mm:ss.SSS", Locale.US);
+ private Date mTmpLogDate = new Date();
private final GestureNavigationSettingsObserver mGestureNavigationSettingsObserver;
@@ -1036,11 +1041,17 @@
}
// For debugging purposes, only log edge points
+ long curTime = System.currentTimeMillis();
+ mTmpLogDate.setTime(curTime);
+ String curTimeStr = mLogDateFormat.format(mTmpLogDate);
(isWithinInsets ? mGestureLogInsideInsets : mGestureLogOutsideInsets).log(String.format(
- "Gesture [%d,alw=%B,%B,%B,%B,%B,%B,disp=%s,wl=%d,il=%d,wr=%d,ir=%d,excl=%s]",
- System.currentTimeMillis(), isTrackpadMultiFingerSwipe, mAllowGesture,
+ "Gesture [%d [%s],alw=%B, mltf=%B, left=%B, defLeft=%B, backAlw=%B, disbld=%B,"
+ + " qsDisbld=%b, blkdAct=%B, pip=%B,"
+ + " disp=%s, wl=%d, il=%d, wr=%d, ir=%d, excl=%s]",
+ curTime, curTimeStr, mAllowGesture, isTrackpadMultiFingerSwipe,
mIsOnLeftEdge, mDeferSetIsOnLeftEdge, mIsBackGestureAllowed,
- QuickStepContract.isBackGestureDisabled(mSysUiFlags), mDisplaySize,
+ QuickStepContract.isBackGestureDisabled(mSysUiFlags), mDisabledForQuickstep,
+ mGestureBlockingActivityRunning, mIsInPip, mDisplaySize,
mEdgeWidthLeft, mLeftInset, mEdgeWidthRight, mRightInset, mExcludeRegion));
} else if (mAllowGesture || mLogGesture) {
if (!mThresholdCrossed) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
index 44c9905..1990c8f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -145,50 +145,59 @@
@Test
fun authenticate_withCorrectPin_returnsTrueAndUnlocksDevice() =
testScope.runTest {
+ val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
assertThat(isUnlocked).isFalse()
assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue()
assertThat(isUnlocked).isTrue()
+ assertThat(failedAttemptCount).isEqualTo(0)
}
@Test
fun authenticate_withIncorrectPin_returnsFalseAndDoesNotUnlockDevice() =
testScope.runTest {
+ val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
assertThat(isUnlocked).isFalse()
assertThat(underTest.authenticate(listOf(9, 8, 7))).isFalse()
assertThat(isUnlocked).isFalse()
+ assertThat(failedAttemptCount).isEqualTo(1)
}
@Test
fun authenticate_withCorrectPassword_returnsTrueAndUnlocksDevice() =
testScope.runTest {
+ val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
assertThat(isUnlocked).isFalse()
assertThat(underTest.authenticate("password".toList())).isTrue()
assertThat(isUnlocked).isTrue()
+ assertThat(failedAttemptCount).isEqualTo(0)
}
@Test
fun authenticate_withIncorrectPassword_returnsFalseAndDoesNotUnlockDevice() =
testScope.runTest {
+ val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
assertThat(isUnlocked).isFalse()
assertThat(underTest.authenticate("alohomora".toList())).isFalse()
assertThat(isUnlocked).isFalse()
+ assertThat(failedAttemptCount).isEqualTo(1)
}
@Test
fun authenticate_withCorrectPattern_returnsTrueAndUnlocksDevice() =
testScope.runTest {
+ val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(
AuthenticationMethodModel.Pattern(
@@ -230,11 +239,13 @@
)
.isTrue()
assertThat(isUnlocked).isTrue()
+ assertThat(failedAttemptCount).isEqualTo(0)
}
@Test
fun authenticate_withIncorrectPattern_returnsFalseAndDoesNotUnlockDevice() =
testScope.runTest {
+ val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(
AuthenticationMethodModel.Pattern(
@@ -276,6 +287,7 @@
)
.isFalse()
assertThat(isUnlocked).isFalse()
+ assertThat(failedAttemptCount).isEqualTo(1)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 730f89d..9f5c181 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -27,6 +27,7 @@
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -75,7 +76,7 @@
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
underTest.clearMessage()
- assertThat(message).isNull()
+ assertThat(message).isEmpty()
underTest.resetMessage()
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
@@ -107,7 +108,7 @@
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
underTest.clearMessage()
- assertThat(message).isNull()
+ assertThat(message).isEmpty()
underTest.resetMessage()
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
@@ -139,7 +140,7 @@
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
underTest.clearMessage()
- assertThat(message).isNull()
+ assertThat(message).isEmpty()
underTest.resetMessage()
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
@@ -201,6 +202,56 @@
assertThat(message).isEqualTo(customMessage)
}
+ @Test
+ fun throttling() =
+ testScope.runTest {
+ val throttling by collectLastValue(underTest.throttling)
+ val message by collectLastValue(underTest.message)
+ val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
+ authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+ assertThat(throttling).isNull()
+ assertThat(message).isEqualTo("")
+ assertThat(isUnlocked).isFalse()
+ repeat(BouncerInteractor.THROTTLE_EVERY) { times ->
+ // Wrong PIN.
+ underTest.authenticate(listOf(6, 7, 8, 9))
+ if (times < BouncerInteractor.THROTTLE_EVERY - 1) {
+ assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
+ }
+ }
+ assertThat(throttling).isNotNull()
+ assertTryAgainMessage(message, BouncerInteractor.THROTTLE_DURATION_SEC)
+
+ // Correct PIN, but throttled, so doesn't unlock:
+ underTest.authenticate(listOf(1, 2, 3, 4))
+ assertThat(isUnlocked).isFalse()
+ assertTryAgainMessage(message, BouncerInteractor.THROTTLE_DURATION_SEC)
+
+ throttling?.totalDurationSec?.let { seconds ->
+ repeat(seconds) { time ->
+ advanceTimeBy(1000)
+ val remainingTime = seconds - time - 1
+ if (remainingTime > 0) {
+ assertTryAgainMessage(message, remainingTime)
+ }
+ }
+ }
+ assertThat(message).isEqualTo("")
+ assertThat(throttling).isNull()
+ assertThat(isUnlocked).isFalse()
+
+ // Correct PIN and no longer throttled so unlocks:
+ underTest.authenticate(listOf(1, 2, 3, 4))
+ assertThat(isUnlocked).isTrue()
+ }
+
+ private fun assertTryAgainMessage(
+ message: String?,
+ time: Int,
+ ) {
+ assertThat(message).isEqualTo("Try again in $time seconds.")
+ }
+
companion object {
private const val MESSAGE_ENTER_YOUR_PIN = "Enter your PIN"
private const val MESSAGE_ENTER_YOUR_PASSWORD = "Enter your password"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index 954e67d..b942ccb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -19,11 +19,15 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.SceneTestUtils
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@@ -40,13 +44,12 @@
utils.authenticationInteractor(
repository = utils.authenticationRepository(),
)
- private val underTest =
- utils.bouncerViewModel(
- utils.bouncerInteractor(
- authenticationInteractor = authenticationInteractor,
- sceneInteractor = utils.sceneInteractor(),
- )
+ private val bouncerInteractor =
+ utils.bouncerInteractor(
+ authenticationInteractor = authenticationInteractor,
+ sceneInteractor = utils.sceneInteractor(),
)
+ private val underTest = utils.bouncerViewModel(bouncerInteractor)
@Test
fun authMethod_nonNullForSecureMethods_nullForNotSecureMethods() =
@@ -89,6 +92,65 @@
.isEqualTo(AuthenticationMethodModel::class.sealedSubclasses.toSet())
}
+ @Test
+ fun isMessageUpdateAnimationsEnabled() =
+ testScope.runTest {
+ val isMessageUpdateAnimationsEnabled by
+ collectLastValue(underTest.isMessageUpdateAnimationsEnabled)
+ val throttling by collectLastValue(bouncerInteractor.throttling)
+ authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+ assertThat(isMessageUpdateAnimationsEnabled).isTrue()
+
+ repeat(BouncerInteractor.THROTTLE_EVERY) {
+ // Wrong PIN.
+ bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
+ }
+ assertThat(isMessageUpdateAnimationsEnabled).isFalse()
+
+ throttling?.totalDurationSec?.let { seconds -> advanceTimeBy(seconds * 1000L) }
+ assertThat(isMessageUpdateAnimationsEnabled).isTrue()
+ }
+
+ @Test
+ fun isInputEnabled() =
+ testScope.runTest {
+ val isInputEnabled by
+ collectLastValue(
+ underTest.authMethod.flatMapLatest { authViewModel ->
+ authViewModel?.isInputEnabled ?: emptyFlow()
+ }
+ )
+ val throttling by collectLastValue(bouncerInteractor.throttling)
+ authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+ assertThat(isInputEnabled).isTrue()
+
+ repeat(BouncerInteractor.THROTTLE_EVERY) {
+ // Wrong PIN.
+ bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
+ }
+ assertThat(isInputEnabled).isFalse()
+
+ throttling?.totalDurationSec?.let { seconds -> advanceTimeBy(seconds * 1000L) }
+ assertThat(isInputEnabled).isTrue()
+ }
+
+ @Test
+ fun throttlingDialogMessage() =
+ testScope.runTest {
+ val throttlingDialogMessage by collectLastValue(underTest.throttlingDialogMessage)
+ authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+
+ repeat(BouncerInteractor.THROTTLE_EVERY) {
+ // Wrong PIN.
+ assertThat(throttlingDialogMessage).isNull()
+ bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
+ }
+ assertThat(throttlingDialogMessage).isNotEmpty()
+
+ underTest.onThrottlingDialogDismissed()
+ assertThat(throttlingDialogMessage).isNull()
+ }
+
private fun authMethodsToTest(): List<AuthenticationMethodModel> {
return listOf(
AuthenticationMethodModel.None,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index e48b638..b7b90de 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -26,6 +26,8 @@
import com.android.systemui.scene.shared.model.SceneModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -57,6 +59,7 @@
private val underTest =
PasswordBouncerViewModel(
interactor = bouncerInteractor,
+ isInputEnabled = MutableStateFlow(true).asStateFlow(),
)
@Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 6ce29e6..b588ba2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -27,6 +27,8 @@
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -60,6 +62,7 @@
applicationContext = context,
applicationScope = testScope.backgroundScope,
interactor = bouncerInteractor,
+ isInputEnabled = MutableStateFlow(true).asStateFlow(),
)
@Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index bb28520..83f9687 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -27,6 +27,8 @@
import com.android.systemui.scene.shared.model.SceneModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
@@ -68,6 +70,7 @@
PinBouncerViewModel(
applicationScope = testScope.backgroundScope,
interactor = bouncerInteractor,
+ isInputEnabled = MutableStateFlow(true).asStateFlow(),
)
@Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
index fd6e457..3bcefcf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
@@ -716,6 +716,48 @@
}
@Test
+ fun testOnNotificationAdded_emptyMetadata_usesNotificationTitle() {
+ // When the app sets the metadata title fields to empty strings, but does include a
+ // non-blank notification title
+ val mockPackageManager = mock(PackageManager::class.java)
+ context.setMockPackageManager(mockPackageManager)
+ whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME)
+ whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(true)
+ whenever(controller.metadata)
+ .thenReturn(
+ metadataBuilder
+ .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE)
+ .putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, SESSION_EMPTY_TITLE)
+ .build()
+ )
+ mediaNotification =
+ SbnBuilder().run {
+ setPkg(PACKAGE_NAME)
+ modifyNotification(context).also {
+ it.setSmallIcon(android.R.drawable.ic_media_pause)
+ it.setContentTitle(SESSION_TITLE)
+ it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
+ }
+ build()
+ }
+ mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+
+ // Then the media control is added using the notification's title
+ assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+ assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+ verify(listener)
+ .onMediaDataLoaded(
+ eq(KEY),
+ eq(null),
+ capture(mediaDataCaptor),
+ eq(true),
+ eq(0),
+ eq(false)
+ )
+ assertThat(mediaDataCaptor.value.song).isEqualTo(SESSION_TITLE)
+ }
+
+ @Test
fun testOnNotificationRemoved_emptyTitle_notConverted() {
// GIVEN that the manager has a notification with a resume action and empty title.
addNotificationAndLoad()
diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java
index 030d596..8c1fd51 100644
--- a/services/core/java/com/android/server/am/BroadcastConstants.java
+++ b/services/core/java/com/android/server/am/BroadcastConstants.java
@@ -292,6 +292,15 @@
private static final String KEY_CORE_DEFER_UNTIL_ACTIVE = "bcast_core_defer_until_active";
private static final boolean DEFAULT_CORE_DEFER_UNTIL_ACTIVE = true;
+ /**
+ * For {@link BroadcastQueueModernImpl}: How frequently we should check for the pending
+ * cold start validity.
+ */
+ public long PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 30 * 1000;
+ private static final String KEY_PENDING_COLD_START_CHECK_INTERVAL_MILLIS =
+ "pending_cold_start_check_interval_millis";
+ private static final long DEFAULT_PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 30_000;
+
// Settings override tracking for this instance
private String mSettingsKey;
private SettingsObserver mSettingsObserver;
@@ -441,6 +450,9 @@
DEFAULT_MAX_HISTORY_SUMMARY_SIZE);
CORE_DEFER_UNTIL_ACTIVE = getDeviceConfigBoolean(KEY_CORE_DEFER_UNTIL_ACTIVE,
DEFAULT_CORE_DEFER_UNTIL_ACTIVE);
+ PENDING_COLD_START_CHECK_INTERVAL_MILLIS = getDeviceConfigLong(
+ KEY_PENDING_COLD_START_CHECK_INTERVAL_MILLIS,
+ DEFAULT_PENDING_COLD_START_CHECK_INTERVAL_MILLIS);
}
// TODO: migrate BroadcastRecord to accept a BroadcastConstants
@@ -499,6 +511,8 @@
MAX_CONSECUTIVE_NORMAL_DISPATCHES).println();
pw.print(KEY_CORE_DEFER_UNTIL_ACTIVE,
CORE_DEFER_UNTIL_ACTIVE).println();
+ pw.print(KEY_PENDING_COLD_START_CHECK_INTERVAL_MILLIS,
+ PENDING_COLD_START_CHECK_INTERVAL_MILLIS).println();
pw.decreaseIndent();
pw.println();
}
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index d6e692c..f180f02 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -248,6 +248,7 @@
private static final int MSG_DELIVERY_TIMEOUT_HARD = 3;
private static final int MSG_BG_ACTIVITY_START_TIMEOUT = 4;
private static final int MSG_CHECK_HEALTH = 5;
+ private static final int MSG_CHECK_PENDING_COLD_START_VALIDITY = 6;
private void enqueueUpdateRunningList() {
mLocalHandler.removeMessages(MSG_UPDATE_RUNNING_LIST);
@@ -284,6 +285,10 @@
checkHealth();
return true;
}
+ case MSG_CHECK_PENDING_COLD_START_VALIDITY: {
+ checkPendingColdStartValidity();
+ return true;
+ }
}
return false;
};
@@ -450,10 +455,14 @@
// skip to look for another warm process
if (mRunningColdStart == null) {
mRunningColdStart = queue;
- } else {
+ } else if (isPendingColdStartValid()) {
// Move to considering next runnable queue
queue = nextQueue;
continue;
+ } else {
+ // Pending cold start is not valid, so clear it and move on.
+ clearInvalidPendingColdStart();
+ mRunningColdStart = queue;
}
}
@@ -486,11 +495,46 @@
mService.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_START_RECEIVER);
}
+ checkPendingColdStartValidity();
checkAndRemoveWaitingFor();
traceEnd(cookie);
}
+ private boolean isPendingColdStartValid() {
+ if (mRunningColdStart.app.getPid() > 0) {
+ // If the process has already started, check if it wasn't killed.
+ return !mRunningColdStart.app.isKilled();
+ } else {
+ // Otherwise, check if the process start is still pending.
+ return mRunningColdStart.app.isPendingStart();
+ }
+ }
+
+ private void clearInvalidPendingColdStart() {
+ logw("Clearing invalid pending cold start: " + mRunningColdStart);
+ onApplicationCleanupLocked(mRunningColdStart.app);
+ }
+
+ private void checkPendingColdStartValidity() {
+ // There are a few cases where a starting process gets killed but AMS doesn't report
+ // this event. So, once we start waiting for a pending cold start, periodically check
+ // if the pending start is still valid and if not, clear it so that the queue doesn't
+ // keep waiting for the process start forever.
+ synchronized (mService) {
+ // If there is no pending cold start, then nothing to do.
+ if (mRunningColdStart == null) {
+ return;
+ }
+ if (isPendingColdStartValid()) {
+ mLocalHandler.sendEmptyMessageDelayed(MSG_CHECK_PENDING_COLD_START_VALIDITY,
+ mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS);
+ } else {
+ clearInvalidPendingColdStart();
+ }
+ }
+ }
+
@Override
public boolean onApplicationAttachedLocked(@NonNull ProcessRecord app) {
// Process records can be recycled, so always start by looking up the
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index ab4fb46..202d407 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -349,21 +349,22 @@
* use caller's BAL permission.
*/
public static BackgroundStartPrivileges getBackgroundStartPrivilegesAllowedByCaller(
- @Nullable ActivityOptions activityOptions, int callingUid) {
+ @Nullable ActivityOptions activityOptions, int callingUid,
+ @Nullable String callingPackage) {
if (activityOptions == null) {
// since the ActivityOptions were not created by the app itself, determine the default
// for the app
- return getDefaultBackgroundStartPrivileges(callingUid);
+ return getDefaultBackgroundStartPrivileges(callingUid, callingPackage);
}
return getBackgroundStartPrivilegesAllowedByCaller(activityOptions.toBundle(),
- callingUid);
+ callingUid, callingPackage);
}
private static BackgroundStartPrivileges getBackgroundStartPrivilegesAllowedByCaller(
- @Nullable Bundle options, int callingUid) {
+ @Nullable Bundle options, int callingUid, @Nullable String callingPackage) {
if (options == null || !options.containsKey(
ActivityOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED)) {
- return getDefaultBackgroundStartPrivileges(callingUid);
+ return getDefaultBackgroundStartPrivileges(callingUid, callingPackage);
}
return options.getBoolean(ActivityOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED)
? BackgroundStartPrivileges.ALLOW_BAL
@@ -382,7 +383,7 @@
android.Manifest.permission.LOG_COMPAT_CHANGE
})
public static BackgroundStartPrivileges getDefaultBackgroundStartPrivileges(
- int callingUid) {
+ int callingUid, @Nullable String callingPackage) {
if (UserHandle.getAppId(callingUid) == Process.SYSTEM_UID) {
// We temporarily allow BAL for system processes, while we verify that all valid use
// cases are opted in explicitly to grant their BAL permission.
@@ -391,7 +392,9 @@
// as soon as that app is upgraded (or removed) BAL would be blocked. (b/283138430)
return BackgroundStartPrivileges.ALLOW_BAL;
}
- boolean isChangeEnabledForApp = CompatChanges.isChangeEnabled(
+ boolean isChangeEnabledForApp = callingPackage != null ? CompatChanges.isChangeEnabled(
+ DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_SENDER, callingPackage,
+ UserHandle.getUserHandleForUid(callingUid)) : CompatChanges.isChangeEnabled(
DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_SENDER, callingUid);
if (isChangeEnabledForApp) {
return BackgroundStartPrivileges.ALLOW_FGS;
@@ -647,7 +650,7 @@
// temporarily allow receivers and services to open activities from background if the
// PendingIntent.send() caller was foreground at the time of sendInner() call
if (uid != callingUid && controller.mAtmInternal.isUidForeground(callingUid)) {
- return getBackgroundStartPrivilegesAllowedByCaller(options, callingUid);
+ return getBackgroundStartPrivilegesAllowedByCaller(options, callingUid, null);
}
return BackgroundStartPrivileges.NONE;
}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 1360a95..750ed98 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -5342,6 +5342,12 @@
return null;
}
+ /**
+ * Returns the {@link WindowProcessController} for the app process for the given uid and pid.
+ *
+ * If no such {@link WindowProcessController} is found, it does not belong to an app, or the
+ * pid does not match the uid {@code null} is returned.
+ */
WindowProcessController getProcessController(int pid, int uid) {
final WindowProcessController proc = mProcessMap.getProcess(pid);
if (proc == null) return null;
@@ -5351,6 +5357,27 @@
return null;
}
+ /**
+ * Returns the package name if (and only if) the package name can be uniquely determined.
+ * Otherwise returns {@code null}.
+ *
+ * The provided pid must match the provided uid, otherwise this also returns null.
+ */
+ @Nullable String getPackageNameIfUnique(int uid, int pid) {
+ final WindowProcessController proc = mProcessMap.getProcess(pid);
+ if (proc == null || proc.mUid != uid) {
+ Slog.w(TAG, "callingPackage for (uid=" + uid + ", pid=" + pid + ") has no WPC");
+ return null;
+ }
+ List<String> realCallingPackages = proc.getPackageList();
+ if (realCallingPackages.size() != 1) {
+ Slog.w(TAG, "callingPackage for (uid=" + uid + ", pid=" + pid + ") is ambiguous: "
+ + realCallingPackages);
+ return null;
+ }
+ return realCallingPackages.get(0);
+ }
+
/** A uid is considered to be foreground if it has a visible non-toast window. */
@HotPath(caller = HotPath.START_SERVICE)
boolean hasActiveVisibleWindow(int uid) {
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index dc49e8c..b216578 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -180,7 +180,8 @@
Intent intent,
ActivityOptions checkedOptions) {
return checkBackgroundActivityStart(callingUid, callingPid, callingPackage,
- realCallingUid, realCallingPid, callerApp, originatingPendingIntent,
+ realCallingUid, realCallingPid,
+ callerApp, originatingPendingIntent,
backgroundStartPrivileges, intent, checkedOptions) == BAL_BLOCK;
}
@@ -288,11 +289,13 @@
}
}
+ String realCallingPackage = mService.getPackageNameIfUnique(realCallingUid, realCallingPid);
+
// Legacy behavior allows to use caller foreground state to bypass BAL restriction.
// The options here are the options passed by the sender and not those on the intent.
final BackgroundStartPrivileges balAllowedByPiSender =
PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller(
- checkedOptions, realCallingUid);
+ checkedOptions, realCallingUid, realCallingPackage);
final boolean logVerdictChangeByPiDefaultChange = checkedOptions == null
|| checkedOptions.getPendingIntentBackgroundActivityStartMode()
@@ -460,8 +463,11 @@
// If we are here, it means all exemptions not based on PI sender failed, so we'll block
// unless resultIfPiSenderAllowsBal is an allow and the PI sender allows BAL
- String realCallingPackage = callingUid == realCallingUid ? callingPackage :
- mService.mContext.getPackageManager().getNameForUid(realCallingUid);
+ if (realCallingPackage == null) {
+ realCallingPackage = (callingUid == realCallingUid ? callingPackage :
+ mService.mContext.getPackageManager().getNameForUid(realCallingUid))
+ + "[debugOnly]";
+ }
String stateDumpLog = " [callingPackage: " + callingPackage
+ "; callingUid: " + callingUid
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index dbd9e4b..3672820 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -721,6 +721,12 @@
}
}
+ List<String> getPackageList() {
+ synchronized (mPkgList) {
+ return new ArrayList<>(mPkgList);
+ }
+ }
+
void addActivityIfNeeded(ActivityRecord r) {
// even if we already track this activity, note down that it has been launched
setLastActivityLaunchTime(r);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index ad5f0d7..6365764 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -269,7 +269,9 @@
deliverRes = res;
break;
}
+ res.setPendingStart(true);
mHandlerThread.getThreadHandler().post(() -> {
+ res.setPendingStart(false);
synchronized (mAms) {
switch (behavior) {
case SUCCESS:
@@ -281,6 +283,10 @@
mActiveProcesses.remove(deliverRes);
mQueue.onApplicationTimeoutLocked(deliverRes);
break;
+ case KILLED_WITHOUT_NOTIFY:
+ mActiveProcesses.remove(res);
+ res.setKilled(true);
+ break;
default:
throw new UnsupportedOperationException();
}
@@ -310,6 +316,7 @@
mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
mConstants.TIMEOUT = 100;
mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
+ mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 500;
mSkipPolicy = spy(new BroadcastSkipPolicy(mAms));
doReturn(null).when(mSkipPolicy).shouldSkipMessage(any(), any());
@@ -381,6 +388,8 @@
FAIL_TIMEOUT_PREDECESSOR,
/** Process fails by immediately returning null */
FAIL_NULL,
+ /** Process is killed without reporting to BroadcastQueue */
+ KILLED_WITHOUT_NOTIFY,
}
private enum ProcessBehavior {
@@ -522,6 +531,11 @@
return info;
}
+ static BroadcastFilter withPriority(BroadcastFilter filter, int priority) {
+ filter.setPriority(priority);
+ return filter;
+ }
+
static ResolveInfo makeManifestReceiver(String packageName, String name) {
return makeManifestReceiver(packageName, name, UserHandle.USER_SYSTEM);
}
@@ -1261,6 +1275,46 @@
new ComponentName(PACKAGE_GREEN, CLASS_GREEN));
}
+ /**
+ * Verify that when BroadcastQueue doesn't get notified when a process gets killed, it
+ * doesn't get stuck.
+ */
+ @Test
+ public void testKillWithoutNotify() throws Exception {
+ final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+ final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+
+ mNextProcessStartBehavior.set(ProcessStartBehavior.KILLED_WITHOUT_NOTIFY);
+
+ final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, List.of(
+ withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10),
+ withPriority(makeRegisteredReceiver(receiverBlueApp), 5),
+ withPriority(makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW), 0))));
+
+ final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
+ enqueueBroadcast(makeBroadcastRecord(timezone, callerApp,
+ List.of(makeManifestReceiver(PACKAGE_ORANGE, CLASS_ORANGE))));
+
+ waitForIdle();
+ final ProcessRecord receiverGreenApp = mAms.getProcessRecordLocked(PACKAGE_GREEN,
+ getUidForPackage(PACKAGE_GREEN));
+ final ProcessRecord receiverYellowApp = mAms.getProcessRecordLocked(PACKAGE_YELLOW,
+ getUidForPackage(PACKAGE_YELLOW));
+ final ProcessRecord receiverOrangeApp = mAms.getProcessRecordLocked(PACKAGE_ORANGE,
+ getUidForPackage(PACKAGE_ORANGE));
+
+ if (mImpl == Impl.MODERN) {
+ // Modern queue does not retry sending a broadcast once any broadcast delivery fails.
+ assertNull(receiverGreenApp);
+ } else {
+ verifyScheduleReceiver(times(1), receiverGreenApp, airplane);
+ }
+ verifyScheduleRegisteredReceiver(times(1), receiverBlueApp, airplane);
+ verifyScheduleReceiver(times(1), receiverYellowApp, airplane);
+ verifyScheduleReceiver(times(1), receiverOrangeApp, timezone);
+ }
+
@Test
public void testCold_Success() throws Exception {
doCold(ProcessStartBehavior.SUCCESS);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 2671e77..2b589bf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -944,7 +944,7 @@
anyInt(), anyInt()));
doReturn(BackgroundStartPrivileges.allowBackgroundActivityStarts(null)).when(
() -> PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller(
- anyObject(), anyInt()));
+ anyObject(), anyInt(), anyObject()));
runAndVerifyBackgroundActivityStartsSubtest(
"allowed_notAborted", false,
UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,