Merge "Fix live timers flickering when pausing" into main
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 857154f..9a178e5 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -4495,8 +4495,9 @@
final int[] userIds =
mUserWakeupStore.getUserIdsToWakeup(nowELAPSED);
for (int i = 0; i < userIds.length; i++) {
- if (!mActivityManagerInternal.startUserInBackground(
- userIds[i])) {
+ if (mActivityManagerInternal.isUserRunning(userIds[i], 0)
+ || !mActivityManagerInternal.startUserInBackground(
+ userIds[i])) {
mUserWakeupStore.removeUserWakeup(userIds[i]);
}
}
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index c3fe031..d92351d 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -1990,7 +1990,7 @@
}
}
if (android.app.admin.flags.Flags.disallowUserControlBgUsageFix()) {
- if (!Flags.avoidIdleCheck()) {
+ if (!Flags.avoidIdleCheck() || mInjector.getBootPhase() >= PHASE_BOOT_COMPLETED) {
postCheckIdleStates(userId);
}
}
diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java
index e95c6a4..8aec7eb 100644
--- a/core/java/android/os/ServiceManager.java
+++ b/core/java/android/os/ServiceManager.java
@@ -425,7 +425,7 @@
private static IBinder rawGetService(String name) throws RemoteException {
final long start = sStatLogger.getTime();
- final IBinder binder = getIServiceManager().getService(name).getBinder();
+ final IBinder binder = getIServiceManager().getService2(name).getBinder();
final int time = (int) sStatLogger.logDurationStat(Stats.GET_SERVICE, start);
diff --git a/core/java/android/os/ServiceManagerNative.java b/core/java/android/os/ServiceManagerNative.java
index 6c9a5c7..5a9c878 100644
--- a/core/java/android/os/ServiceManagerNative.java
+++ b/core/java/android/os/ServiceManagerNative.java
@@ -57,8 +57,14 @@
return mRemote;
}
+ // TODO(b/355394904): This function has been deprecated, please use getService2 instead.
@UnsupportedAppUsage
- public Service getService(String name) throws RemoteException {
+ public IBinder getService(String name) throws RemoteException {
+ // Same as checkService (old versions of servicemanager had both methods).
+ return checkService(name).getBinder();
+ }
+
+ public Service getService2(String name) throws RemoteException {
// Same as checkService (old versions of servicemanager had both methods).
return checkService(name);
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 850b979..c186538 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -12560,6 +12560,14 @@
"contextual_screen_timeout_enabled";
/**
+ * Whether hinge angle lidevent is enabled.
+ *
+ * @hide
+ */
+ public static final String HINGE_ANGLE_LIDEVENT_ENABLED =
+ "hinge_angle_lidevent_enabled";
+
+ /**
* Whether lockscreen weather is enabled.
*
* @hide
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 64c7766..5f8bea1 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -34071,14 +34071,21 @@
}
private float convertVelocityToFrameRate(float velocityPps) {
- // From UXR study, premium experience is:
- // 1500+ dp/s: 120fps
- // 0 - 1500 dp/s: 80fps
- // OEMs are likely to modify this to balance battery and user experience for their
- // specific device.
+ // Internal testing has shown that this gives a premium experience:
+ // above 300dp/s => 120fps
+ // between 300dp/s and 125fps => 80fps
+ // below 125dp/s => 60fps
float density = mAttachInfo.mDensity;
float velocityDps = velocityPps / density;
- return (velocityDps >= 1500f) ? MAX_FRAME_RATE : 80f;
+ float frameRate;
+ if (velocityDps > 300f) {
+ frameRate = MAX_FRAME_RATE; // Use maximum at fast motion
+ } else if (velocityDps > 125f) {
+ frameRate = 80f; // Use medium frame rate when motion is slower
+ } else {
+ frameRate = 60f; // Use minimum frame rate when motion is very slow
+ }
+ return frameRate;
}
/**
diff --git a/core/java/android/view/autofill/AutofillClientController.java b/core/java/android/view/autofill/AutofillClientController.java
index d505c733..95cae226 100644
--- a/core/java/android/view/autofill/AutofillClientController.java
+++ b/core/java/android/view/autofill/AutofillClientController.java
@@ -582,4 +582,9 @@
Log.e(TAG, "authenticate() failed for intent:" + intent, e);
}
}
+
+ @Override
+ public boolean isActivityResumed() {
+ return mActivity.isResumed();
+ }
}
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 02a86c9..79ecfe1e 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -114,6 +114,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -774,6 +775,13 @@
// dataset in responses. Used to avoid request pre-fill request again and again.
private final ArraySet<AutofillId> mAllTrackedViews = new ArraySet<>();
+ // Whether we need to re-attempt fill again. Needed for case of relayout.
+ private boolean mFillReAttemptNeeded = false;
+
+ private Map<Integer, AutofillId> mFingerprintToViewMap = new ArrayMap<>();
+
+ private AutofillStateFingerprint mAutofillStateFingerprint;
+
/** @hide */
public interface AutofillClient {
/**
@@ -909,6 +917,11 @@
* @return An ID that is unique in the activity.
*/
@Nullable AutofillId autofillClientGetNextAutofillId();
+
+ /**
+ * @return Whether the activity is resumed or not.
+ */
+ boolean isActivityResumed();
}
/**
@@ -919,6 +932,7 @@
mService = service;
mOptions = context.getAutofillOptions();
mIsFillRequested = new AtomicBoolean(false);
+ mAutofillStateFingerprint = AutofillStateFingerprint.createInstance();
mIsFillDialogEnabled = AutofillFeatureFlags.isFillDialogEnabled();
mFillDialogEnabledHints = AutofillFeatureFlags.getFillDialogEnabledHints();
@@ -1357,6 +1371,20 @@
mOnInvisibleCalled = true;
if (isExpiredResponse) {
+ if (mRelayoutFix && isAuthenticationPending()) {
+ Log.i(TAG, "onInvisibleForAutofill(): Ignoring expiringResponse due to pending"
+ + " authentication");
+ try {
+ mService.notifyNotExpiringResponseDuringAuth(
+ mSessionId, mContext.getUserId());
+ } catch (RemoteException e) {
+ // The failure could be a consequence of something going wrong on the
+ // server side. Do nothing here since it's just logging, but it's
+ // possible follow-up actions may fail.
+ }
+ return;
+ }
+ Log.i(TAG, "onInvisibleForAutofill(): expiringResponse");
// Notify service the response has expired.
updateSessionLocked(/* id= */ null, /* bounds= */ null, /* value= */ null,
ACTION_RESPONSE_EXPIRED, /* flags= */ 0);
@@ -1513,14 +1541,29 @@
}
/**
+ * Called to log notify view entered was ignored due to pending auth
+ * @hide
+ */
+ public void notifyViewEnteredIgnoredDuringAuthCount() {
+ try {
+ mService.notifyViewEnteredIgnoredDuringAuthCount(mSessionId, mContext.getUserId());
+ } catch (RemoteException e) {
+ // The failure could be a consequence of something going wrong on the
+ // server side. Do nothing here since it's just logging, but it's
+ // possible follow-up actions may fail.
+ }
+ }
+
+ /**
* Called to check if we should retry fill.
* Useful for knowing whether to attempt refill after relayout.
*
* @hide
*/
public boolean shouldRetryFill() {
- // TODO: Implement in follow-up cl
- return false;
+ synchronized (mLock) {
+ return isAuthenticationPending() && mFillReAttemptNeeded;
+ }
}
/**
@@ -1531,8 +1574,13 @@
*/
public boolean attemptRefill() {
Log.i(TAG, "Attempting refill");
- // TODO: Implement in follow-up cl
- return false;
+ // Find active autofillable views. Compute their fingerprints
+ List<View> autofillableViews =
+ getClient().autofillClientFindAutofillableViewsByTraversal();
+ if (sDebug) {
+ Log.d(TAG, "Autofillable views count:" + autofillableViews.size());
+ }
+ return mAutofillStateFingerprint.attemptRefill(autofillableViews, this);
}
/**
@@ -2493,7 +2541,13 @@
/** @hide */
public void onAuthenticationResult(int authenticationId, Intent data, View focusView) {
+ if (sVerbose) {
+ Log.v(TAG, "onAuthenticationResult(): authId= " + authenticationId + ", data=" + data);
+ }
if (!hasAutofillFeature()) {
+ if (sVerbose) {
+ Log.v(TAG, "onAuthenticationResult(): autofill not enabled");
+ }
return;
}
// TODO: the result code is being ignored, so this method is not reliably
@@ -2501,10 +2555,6 @@
// set the EXTRA_AUTHENTICATION_RESULT extra, but it could cause weird results if the
// service set the extra and returned RESULT_CANCELED...
- if (sDebug) {
- Log.d(TAG, "onAuthenticationResult(): id= " + authenticationId + ", data=" + data);
- }
-
synchronized (mLock) {
if (!isActiveLocked()) {
Log.w(TAG, "onAuthenticationResult(): sessionId=" + mSessionId + " not active");
@@ -2661,6 +2711,7 @@
mSessionId = receiver.getIntResult();
if (mSessionId != NO_SESSION) {
mState = STATE_ACTIVE;
+ mAutofillStateFingerprint.setSessionId(mSessionId);
}
final int extraFlags = receiver.getOptionalExtraIntResult(0);
if ((extraFlags & RECEIVER_FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY) != 0) {
@@ -2722,6 +2773,9 @@
if (resetEnteredIds) {
mEnteredIds = null;
}
+ mFillReAttemptNeeded = false;
+ mFingerprintToViewMap.clear();
+ mAutofillStateFingerprint = AutofillStateFingerprint.createInstance();
}
@GuardedBy("mLock")
@@ -2984,8 +3038,12 @@
Intent fillInIntent, boolean authenticateInline) {
synchronized (mLock) {
if (sessionId == mSessionId) {
- if (mRelayoutFixDeprecated) {
+ if (mRelayoutFixDeprecated || mRelayoutFix) {
mState = STATE_PENDING_AUTHENTICATION;
+ if (sVerbose) {
+ Log.v(TAG, "entering STATE_PENDING_AUTHENTICATION : mRelayoutFix:"
+ + mRelayoutFix);
+ }
}
final AutofillClient client = getClient();
if (client != null) {
@@ -3191,17 +3249,56 @@
@GuardedBy("mLock")
private void handleFailedIdsLocked(@NonNull ArrayList<AutofillId> failedIds) {
+ handleFailedIdsLocked(failedIds, null, false, false);
+ }
+
+ @GuardedBy("mLock")
+ private void handleFailedIdsLocked(@NonNull ArrayList<AutofillId> failedIds,
+ ArrayList<AutofillValue> failedAutofillValues, boolean hideHighlight,
+ boolean isRefill) {
if (!failedIds.isEmpty() && sVerbose) {
Log.v(TAG, "autofill(): total failed views: " + failedIds);
}
+
+ if (mRelayoutFix && !failedIds.isEmpty()) {
+ // Activity isn't in resumed state, so it's very possible that relayout could've
+ // occurred, so wait for it to declare proper failure. It's a temporary failure at the
+ // moment. We'll try again later when the activity is resumed.
+
+ // The above doesn't seem to be the correct way. Look for pending auth cases.
+ // TODO(b/238252288): Check whether there was any auth done at all
+ mFillReAttemptNeeded = true;
+ mAutofillStateFingerprint.storeFailedIdsAndValues(
+ failedIds, failedAutofillValues, hideHighlight);
+ }
try {
- mService.setAutofillFailure(mSessionId, failedIds, mContext.getUserId());
+ mService.setAutofillFailure(mSessionId, failedIds, isRefill, mContext.getUserId());
} catch (RemoteException e) {
// In theory, we could ignore this error since it's not a big deal, but
// in reality, we rather crash the app anyways, as the failure could be
// a consequence of something going wrong on the server side...
throw e.rethrowFromSystemServer();
}
+ if (mRelayoutFix && !failedIds.isEmpty()) {
+ if (!getClient().isActivityResumed()) {
+ if (sVerbose) {
+ Log.v(TAG, "handleFailedIdsLocked(): failed id's exist, but activity not"
+ + " resumed");
+ }
+ } else {
+ if (isRefill) {
+ Log.i(TAG, "handleFailedIdsLocked(): Attempted refill, but failed");
+ } else {
+ // activity has been resumed, try to re-fill
+ // getClient().isActivityResumed() && !failedIds.isEmpty() && !isRefill
+ // TODO(b/238252288): Do better state management, and only trigger the following
+ // if there was auth previously.
+ Log.i(TAG, "handleFailedIdsLocked(): Attempting refill");
+ attemptRefill();
+ mFillReAttemptNeeded = false;
+ }
+ }
+ }
}
private void autofill(int sessionId, List<AutofillId> ids, List<AutofillValue> values,
@@ -3216,13 +3313,46 @@
return;
}
- final int itemCount = ids.size();
- int numApplied = 0;
- ArrayMap<View, SparseArray<AutofillValue>> virtualValues = null;
final View[] views = client.autofillClientFindViewsByAutofillIdTraversal(
Helper.toArray(ids));
+ autofill(views, ids, values, hideHighlight, false);
+ }
+ }
+
+ void autofill(View[] views, List<AutofillId> ids, List<AutofillValue> values,
+ boolean hideHighlight, boolean isRefill) {
+ if (sVerbose) {
+ Log.v(TAG, "autofill() ids:" + ids + " isRefill:" + isRefill);
+ }
+ synchronized (mLock) {
+ final AutofillClient client = getClient();
+ if (client == null) {
+ return;
+ }
+
+ if (ids == null) {
+ Log.i(TAG, "autofill(): No id's to fill");
+ return;
+ }
+
+ if (mRelayoutFix && isRefill) {
+ try {
+ mService.setAutofillIdsAttemptedForRefill(
+ mSessionId, ids, mContext.getUserId());
+ } catch (RemoteException e) {
+ // The failure could be a consequence of something going wrong on the
+ // server side. Do nothing here since it's just logging, but it's
+ // possible follow-up actions may fail.
+ }
+ }
+
+ final int itemCount = ids.size();
+ int numApplied = 0;
+ ArrayMap<View, SparseArray<AutofillValue>> virtualValues = null;
+
ArrayList<AutofillId> failedIds = new ArrayList<>();
+ ArrayList<AutofillValue> failedAutofillValues = new ArrayList<>();
if (mLastAutofilledData == null) {
mLastAutofilledData = new ParcelableMap(itemCount);
@@ -3237,7 +3367,9 @@
// the service; this is fine, but we need to update the view status in the
// server side so it can be triggered again.
Log.d(TAG, "autofill(): no View with id " + id);
+ // Possible relayout scenario
failedIds.add(id);
+ failedAutofillValues.add(value);
continue;
}
// Mark the view as to be autofilled with 'value'
@@ -3268,7 +3400,8 @@
}
}
- handleFailedIdsLocked(failedIds);
+ handleFailedIdsLocked(
+ failedIds, failedAutofillValues, hideHighlight, isRefill);
if (virtualValues != null) {
for (int i = 0; i < virtualValues.size(); i++) {
@@ -3322,7 +3455,7 @@
private void reportAutofillContentFailure(AutofillId id) {
try {
mService.setAutofillFailure(mSessionId, Collections.singletonList(id),
- mContext.getUserId());
+ false /* isRefill */, mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -3349,20 +3482,22 @@
}
/**
- * Set the tracked views.
+ * Set the tracked views.
*
- * @param trackedIds The views to be tracked.
+ * @param trackedIds The views to be tracked.
* @param saveOnAllViewsInvisible Finish the session once all tracked views are invisible.
- * @param saveOnFinish Finish the session once the activity is finished.
- * @param fillableIds Views that might anchor FillUI.
- * @param saveTriggerId View that when clicked triggers commit().
+ * @param saveOnFinish Finish the session once the activity is finished.
+ * @param fillableIds Views that might anchor FillUI.
+ * @param saveTriggerId View that when clicked triggers commit().
*/
private void setTrackedViews(int sessionId, @Nullable AutofillId[] trackedIds,
boolean saveOnAllViewsInvisible, boolean saveOnFinish,
- @Nullable AutofillId[] fillableIds, @Nullable AutofillId saveTriggerId) {
+ @Nullable AutofillId[] fillableIds, @Nullable AutofillId saveTriggerId,
+ boolean shouldGrabViewFingerprints) {
if (saveTriggerId != null) {
saveTriggerId.resetSessionId();
}
+ final ArraySet<AutofillId> allFillableIds = new ArraySet<>();
synchronized (mLock) {
if (sVerbose) {
Log.v(TAG, "setTrackedViews(): sessionId=" + sessionId
@@ -3372,6 +3507,7 @@
+ ", fillableIds=" + Arrays.toString(fillableIds)
+ ", saveTrigerId=" + saveTriggerId
+ ", mFillableIds=" + mFillableIds
+ + ", shouldGrabViewFingerprints=" + shouldGrabViewFingerprints
+ ", mEnabled=" + mEnabled
+ ", mSessionId=" + mSessionId);
}
@@ -3405,7 +3541,6 @@
trackedIds = null;
}
- final ArraySet<AutofillId> allFillableIds = new ArraySet<>();
if (mFillableIds != null) {
allFillableIds.addAll(mFillableIds);
}
@@ -3424,6 +3559,12 @@
mTrackedViews = null;
}
}
+ if (mRelayoutFix && shouldGrabViewFingerprints) {
+ // For all the views: tracked and others, calculate fingerprints and store them.
+ mAutofillStateFingerprint.setUseRelativePosition(mRelativePositionForRelayout);
+ mAutofillStateFingerprint.storeStatePriorToAuthentication(
+ getClient(), allFillableIds);
+ }
}
}
@@ -3845,7 +3986,7 @@
@GuardedBy("mLock")
private boolean isPendingAuthenticationLocked() {
- return mRelayoutFixDeprecated && mState == STATE_PENDING_AUTHENTICATION;
+ return (mRelayoutFixDeprecated || mRelayoutFix) && mState == STATE_PENDING_AUTHENTICATION;
}
@GuardedBy("mLock")
@@ -3858,7 +3999,7 @@
return mState == STATE_FINISHED;
}
- private void post(Runnable runnable) {
+ void post(Runnable runnable) {
final AutofillClient client = getClient();
if (client == null) {
if (sVerbose) Log.v(TAG, "ignoring post() because client is null");
@@ -4700,11 +4841,11 @@
@Override
public void setTrackedViews(int sessionId, AutofillId[] ids,
boolean saveOnAllViewsInvisible, boolean saveOnFinish, AutofillId[] fillableIds,
- AutofillId saveTriggerId) {
+ AutofillId saveTriggerId, boolean shouldGrabViewFingerprints) {
final AutofillManager afm = mAfm.get();
if (afm != null) {
afm.post(() -> afm.setTrackedViews(sessionId, ids, saveOnAllViewsInvisible,
- saveOnFinish, fillableIds, saveTriggerId));
+ saveOnFinish, fillableIds, saveTriggerId, shouldGrabViewFingerprints));
}
}
diff --git a/core/java/android/view/autofill/AutofillStateFingerprint.java b/core/java/android/view/autofill/AutofillStateFingerprint.java
new file mode 100644
index 0000000..2db4285
--- /dev/null
+++ b/core/java/android/view/autofill/AutofillStateFingerprint.java
@@ -0,0 +1,352 @@
+/*
+ * 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 android.view.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Slog;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * This class manages and stores the autofillable views fingerprints for use in relayout situations.
+ * @hide
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public final class AutofillStateFingerprint {
+
+ ArrayList<AutofillId> mPriorAutofillIds;
+ ArrayList<Integer> mViewHashCodes; // each entry corresponding to mPriorAutofillIds .
+
+ boolean mHideHighlight = false;
+
+ private int mSessionId;
+
+ Map<Integer, AutofillId> mHashToAutofillIdMap = new ArrayMap<>();
+ Map<AutofillId, AutofillId> mOldIdsToCurrentAutofillIdMap = new ArrayMap<>();
+
+ // These failed id's are attempted to be refilled again after relayout.
+ private ArrayList<AutofillId> mFailedIds = new ArrayList<>();
+ private ArrayList<AutofillValue> mFailedAutofillValues = new ArrayList<>();
+
+ // whether to use relative positions for computing hashes.
+ private boolean mUseRelativePosition;
+
+ private static final String TAG = "AutofillStateFingerprint";
+
+ /**
+ * Returns an instance of this class
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public static AutofillStateFingerprint createInstance() {
+ return new AutofillStateFingerprint();
+ }
+
+ private AutofillStateFingerprint() {
+ }
+
+ /**
+ * Set sessionId for the instance
+ */
+ void setSessionId(int sessionId) {
+ mSessionId = sessionId;
+ }
+
+ /**
+ * Sets whether relative position of the views should be used to calculate fingerprints.
+ */
+ void setUseRelativePosition(boolean useRelativePosition) {
+ mUseRelativePosition = useRelativePosition;
+ }
+
+ /**
+ * Store the state of the views prior to the authentication.
+ */
+ void storeStatePriorToAuthentication(
+ AutofillManager.AutofillClient client, Set<AutofillId> autofillIds) {
+ if (mUseRelativePosition) {
+ List<View> autofillableViews = client.autofillClientFindAutofillableViewsByTraversal();
+ if (sDebug) {
+ Log.d(TAG, "Autofillable views count prior to auth:" + autofillableViews.size());
+ }
+// ArrayList<Integer> hashes = getFingerprintIds(autofillableViews);
+
+ ArrayMap<Integer, View> hashes = getFingerprintIds(autofillableViews);
+ for (Map.Entry<Integer, View> entry : hashes.entrySet()) {
+ View view = entry.getValue();
+ if (view != null) {
+ mHashToAutofillIdMap.put(entry.getKey(), view.getAutofillId());
+ } else {
+ if (sDebug) {
+ Log.d(TAG, "Encountered null view");
+ }
+ }
+ }
+ } else {
+ // Just use the provided autofillIds and get their hashes
+ if (sDebug) {
+ Log.d(TAG, "Size of autofillId's being stored: " + autofillIds.size()
+ + " list:" + autofillIds);
+ }
+ AutofillId[] autofillIdsArr = Helper.toArray(autofillIds);
+ View[] views = client.autofillClientFindViewsByAutofillIdTraversal(autofillIdsArr);
+ for (int i = 0; i < autofillIdsArr.length; i++) {
+ View view = views[i];
+ if (view != null) {
+ int id = getEphemeralFingerprintId(view, 0 /* position irrelevant */);
+ AutofillId autofillId = view.getAutofillId();
+ autofillId.setSessionId(mSessionId);
+ mHashToAutofillIdMap.put(id, autofillId);
+ } else {
+ if (sDebug) {
+ Log.d(TAG, "Encountered null view");
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Store failed ids, so that they can be refilled later
+ */
+ void storeFailedIdsAndValues(
+ @NonNull ArrayList<AutofillId> failedIds,
+ ArrayList<AutofillValue> failedAutofillValues,
+ boolean hideHighlight) {
+ for (AutofillId failedId : failedIds) {
+ if (failedId != null) {
+ failedId.setSessionId(mSessionId);
+ } else {
+ if (sDebug) {
+ Log.d(TAG, "Got null failed ids");
+ }
+ }
+ }
+ mFailedIds = failedIds;
+ mFailedAutofillValues = failedAutofillValues;
+ mHideHighlight = hideHighlight;
+ }
+
+ private void dumpCurrentState() {
+ Log.d(TAG, "FailedId's: " + mFailedIds);
+ Log.d(TAG, "Hashes from map" + mHashToAutofillIdMap);
+ }
+
+ boolean attemptRefill(
+ List<View> currentAutofillableViews, @NonNull AutofillManager autofillManager) {
+ if (sDebug) {
+ dumpCurrentState();
+ }
+ // For the autofillable views, compute their hashes
+ ArrayMap<Integer, View> currentHashes = getFingerprintIds(currentAutofillableViews);
+
+ // For the computed hashes, try to look for the old fingerprints.
+ // If match found, update the new autofill ids of those views
+ Map<AutofillId, View> oldFailedIdsToCurrentViewMap = new HashMap<>();
+ for (Map.Entry<Integer, View> entry : currentHashes.entrySet()) {
+ View view = entry.getValue();
+ int currentHash = entry.getKey();
+ AutofillId currentAutofillId = view.getAutofillId();
+ currentAutofillId.setSessionId(mSessionId);
+ if (mHashToAutofillIdMap.containsKey(currentHash)) {
+ AutofillId oldAutofillId = mHashToAutofillIdMap.get(currentHash);
+ oldAutofillId.setSessionId(mSessionId);
+ mOldIdsToCurrentAutofillIdMap.put(oldAutofillId, currentAutofillId);
+ Log.i(TAG, "Mapping current autofill id: " + view.getAutofillId()
+ + " to existing autofill id " + oldAutofillId);
+
+ oldFailedIdsToCurrentViewMap.put(oldAutofillId, view);
+ } else {
+ Log.i(TAG, "Couldn't map current autofill id: " + view.getAutofillId()
+ + " with currentHash:" + currentHash + " for view:" + view);
+ }
+ }
+
+ int viewsCount = 0;
+ View[] views = new View[mFailedIds.size()];
+ for (int i = 0; i < mFailedIds.size(); i++) {
+ AutofillId oldAutofillId = mFailedIds.get(i);
+ AutofillId currentAutofillId = mOldIdsToCurrentAutofillIdMap.get(oldAutofillId);
+ if (currentAutofillId == null) {
+ if (sDebug) {
+ Log.d(TAG, "currentAutofillId = null");
+ }
+ }
+ mFailedIds.set(i, currentAutofillId);
+ views[i] = oldFailedIdsToCurrentViewMap.get(oldAutofillId);
+ if (views[i] != null) {
+ viewsCount++;
+ }
+ }
+
+ if (sDebug) {
+ dumpCurrentState();
+ }
+
+ // Attempt autofill now
+ Slog.i(TAG, "Attempting refill of views. Found " + viewsCount
+ + " views to refill from previously " + mFailedIds.size()
+ + " failed ids:" + mFailedIds);
+ autofillManager.post(
+ () -> autofillManager.autofill(
+ views, mFailedIds, mFailedAutofillValues, mHideHighlight,
+ true /* isRefill */));
+
+ return false;
+ }
+
+ /**
+ * Retrieves fingerprint hashes for the views
+ */
+ ArrayMap<Integer, View> getFingerprintIds(@NonNull List<View> views) {
+ ArrayMap<Integer, View> map = new ArrayMap<>();
+ if (mUseRelativePosition) {
+ Collections.sort(views, (View v1, View v2) -> {
+ int[] posV1 = v1.getLocationOnScreen();
+ int[] posV2 = v2.getLocationOnScreen();
+
+ int compare = posV1[0] - posV2[0]; // x coordinate
+ if (compare != 0) {
+ return compare;
+ }
+ compare = posV1[1] - posV2[1]; // y coordinate
+ if (compare != 0) {
+ return compare;
+ }
+ // Sort on vertical
+ compare = compareTop(v1, v2);
+ if (compare != 0) {
+ return compare;
+ }
+ compare = compareBottom(v1, v2);
+ if (compare != 0) {
+ return compare;
+ }
+ compare = compareLeft(v1, v2);
+ if (compare != 0) {
+ return compare;
+ }
+ return compareRight(v1, v2);
+ // Note that if compareRight also returned 0, that means both the views have exact
+ // same location, so just treat them as equal
+ });
+ }
+ for (int i = 0; i < views.size(); i++) {
+ View view = views.get(i);
+ map.put(getEphemeralFingerprintId(view, i), view);
+ }
+ return map;
+ }
+
+ /**
+ * Returns fingerprint hash for the view.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ public int getEphemeralFingerprintId(View v, int position) {
+ if (v == null) return -1;
+ int inputType = Integer.MIN_VALUE;
+ int imeOptions = Integer.MIN_VALUE;
+ boolean isSingleLine = false;
+ CharSequence hints = "";
+ if (v instanceof TextView) {
+ TextView tv = (TextView) v;
+ inputType = tv.getInputType();
+ hints = tv.getHint();
+ isSingleLine = tv.isSingleLine();
+ imeOptions = tv.getImeOptions();
+ // TODO(b/238252288): Consider adding more IME related fields.
+ }
+ CharSequence contentDesc = v.getContentDescription();
+ CharSequence tooltip = v.getTooltipText();
+
+ int autofillType = v.getAutofillType();
+ String[] autofillHints = v.getAutofillHints();
+ int visibility = v.getVisibility();
+
+ int paddingLeft = v.getPaddingLeft();
+ int paddingRight = v.getPaddingRight();
+ int paddingTop = v.getPaddingTop();
+ int paddingBottom = v.getPaddingBottom();
+
+ // TODO(b/238252288): Following are making relayout flaky. Do more analysis to figure out
+ // why.
+ int height = v.getHeight();
+ int width = v.getWidth();
+
+ // Order doesn't matter much here. We can change the order, as long as we use the same
+ // order for storing and fetching fingerprints. The order can be changed in platform
+ // versions.
+ int hash = Objects.hash(visibility, inputType, imeOptions, isSingleLine, hints,
+ contentDesc, tooltip, autofillType, Arrays.deepHashCode(autofillHints),
+ paddingBottom, paddingTop, paddingRight, paddingLeft);
+ if (mUseRelativePosition) {
+ hash = Objects.hash(hash, position);
+ }
+ if (sDebug) {
+ Log.d(TAG, "Hash: " + hash + " for AutofillId:" + v.getAutofillId()
+ + " visibility:" + visibility
+ + " inputType:" + inputType
+ + " imeOptions:" + imeOptions
+ + " isSingleLine:" + isSingleLine
+ + " hints:" + hints
+ + " contentDesc:" + contentDesc
+ + " tooltipText:" + tooltip
+ + " autofillType:" + autofillType
+ + " autofillHints:" + Arrays.toString(autofillHints)
+ + " height:" + height
+ + " width:" + width
+ + " paddingLeft:" + paddingLeft
+ + " paddingRight:" + paddingRight
+ + " paddingTop:" + paddingTop
+ + " paddingBottom:" + paddingBottom
+ + " mUseRelativePosition" + mUseRelativePosition
+ + " position:" + position
+ );
+ }
+ return hash;
+ }
+
+ private int compareTop(View v1, View v2) {
+ return v1.getTop() - v2.getTop();
+ }
+
+ private int compareBottom(View v1, View v2) {
+ return v1.getBottom() - v2.getBottom();
+ }
+
+ private int compareLeft(View v1, View v2) {
+ return v1.getLeft() - v2.getLeft();
+ }
+
+ private int compareRight(View v1, View v2) {
+ return v1.getRight() - v2.getRight();
+ }
+}
diff --git a/core/java/android/view/autofill/IAutoFillManager.aidl b/core/java/android/view/autofill/IAutoFillManager.aidl
index 2039b4d..f67405f 100644
--- a/core/java/android/view/autofill/IAutoFillManager.aidl
+++ b/core/java/android/view/autofill/IAutoFillManager.aidl
@@ -48,7 +48,7 @@
in IResultReceiver result);
void updateSession(int sessionId, in AutofillId id, in Rect bounds,
in AutofillValue value, int action, int flags, int userId);
- void setAutofillFailure(int sessionId, in List<AutofillId> ids, int userId);
+ void setAutofillFailure(int sessionId, in List<AutofillId> ids, boolean isRefill, int userId);
void setViewAutofilled(int sessionId, in AutofillId id, int userId);
void finishSession(int sessionId, int userId, int commitReason);
void cancelSession(int sessionId, int userId);
@@ -67,4 +67,7 @@
void getDefaultFieldClassificationAlgorithm(in IResultReceiver result);
void setAugmentedAutofillWhitelist(in List<String> packages, in List<ComponentName> activities,
in IResultReceiver result);
+ void notifyNotExpiringResponseDuringAuth(int sessionId, int userId);
+ void notifyViewEnteredIgnoredDuringAuthCount(int sessionId, int userId);
+ void setAutofillIdsAttemptedForRefill(int sessionId, in List<AutofillId> ids, int userId);
}
diff --git a/core/java/android/view/autofill/IAutoFillManagerClient.aidl b/core/java/android/view/autofill/IAutoFillManagerClient.aidl
index 904a7e0..39d71da 100644
--- a/core/java/android/view/autofill/IAutoFillManagerClient.aidl
+++ b/core/java/android/view/autofill/IAutoFillManagerClient.aidl
@@ -72,7 +72,8 @@
*/
void setTrackedViews(int sessionId, in @nullable AutofillId[] savableIds,
boolean saveOnAllViewsInvisible, boolean saveOnFinish,
- in @nullable AutofillId[] fillableIds, in AutofillId saveTriggerId);
+ in @nullable AutofillId[] fillableIds, in AutofillId saveTriggerId,
+ in boolean shouldGrabViewFingerprints);
/**
* Requests showing the fill UI.
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
index 07a9794..a4ca55e 100644
--- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
@@ -196,16 +196,20 @@
/**
* Invokes {@link IInputMethodManager#removeImeSurface()}
+ *
+ * @param displayId display ID from which this request originates
+ * @param exceptionHandler an optional {@link RemoteException} handler
*/
@AnyThread
@RequiresPermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
- static void removeImeSurface(@Nullable Consumer<RemoteException> exceptionHandler) {
+ static void removeImeSurface(int displayId,
+ @Nullable Consumer<RemoteException> exceptionHandler) {
final IInputMethodManager service = getService();
if (service == null) {
return;
}
try {
- service.removeImeSurface();
+ service.removeImeSurface(displayId);
} catch (RemoteException e) {
handleRemoteExceptionOrRethrow(e, exceptionHandler);
}
diff --git a/core/java/android/view/inputmethod/InputMethodManagerGlobal.java b/core/java/android/view/inputmethod/InputMethodManagerGlobal.java
index 5df9fd1..244b239 100644
--- a/core/java/android/view/inputmethod/InputMethodManagerGlobal.java
+++ b/core/java/android/view/inputmethod/InputMethodManagerGlobal.java
@@ -95,11 +95,13 @@
/**
* Invokes {@link IInputMethodManager#removeImeSurface()}
*
+ * @param displayId display ID from which this request originates.
* @param exceptionHandler an optional {@link RemoteException} handler.
*/
@AnyThread
@RequiresPermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
- public static void removeImeSurface(@Nullable Consumer<RemoteException> exceptionHandler) {
- IInputMethodManagerGlobalInvoker.removeImeSurface(exceptionHandler);
+ public static void removeImeSurface(int displayId,
+ @Nullable Consumer<RemoteException> exceptionHandler) {
+ IInputMethodManagerGlobalInvoker.removeImeSurface(displayId, exceptionHandler);
}
}
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index cba27ce..3f7ba0a 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -171,7 +171,7 @@
@EnforcePermission("INTERNAL_SYSTEM_WINDOW")
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ "android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)")
- void removeImeSurface();
+ void removeImeSurface(int displayId);
/** Remove the IME surface. Requires passing the currently focused window. */
oneway void removeImeSurfaceFromWindowAsync(in IBinder windowToken);
diff --git a/core/tests/coretests/src/android/app/activity/ServiceTest.java b/core/tests/coretests/src/android/app/activity/ServiceTest.java
index 3f3d6a3..4e3b2af 100644
--- a/core/tests/coretests/src/android/app/activity/ServiceTest.java
+++ b/core/tests/coretests/src/android/app/activity/ServiceTest.java
@@ -157,6 +157,21 @@
assertThat(mCurrentConnection.takePid(), is(NOT_STARTED));
}
+ @Test
+ public void testRestart_stickyStartedService_unbindHappenedAfterRestart_restarted() {
+ final int servicePid = startService(Service.START_STICKY);
+ assertThat(servicePid, not(NOT_STARTED));
+ assertThat(bindService(0 /* flags */), is(servicePid));
+
+ final int restartedServicePid = waitForServiceStarted(
+ () -> {
+ Process.killProcess(servicePid);
+ mContext.unbindService(mCurrentConnection);
+ mCurrentConnection = null;
+ });
+ assertThat(restartedServicePid, not(NOT_STARTED));
+ }
+
/** @return The pid of the started service. */
private int startService(int code) {
return waitForServiceStarted(
diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
index 18364ad..b8ff595 100644
--- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java
+++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
@@ -278,7 +278,7 @@
@RequiresFlagsEnabled({FLAG_VIEW_VELOCITY_API,
FLAG_TOOLKIT_FRAME_RATE_VELOCITY_MAPPING_READ_ONLY,
FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY})
- public void lowVelocity80() throws Throwable {
+ public void lowVelocity60() throws Throwable {
if (!ViewProperties.vrr_enabled().orElse(true)) {
return;
}
@@ -292,6 +292,31 @@
mActivityRule.runOnUiThread(() -> {
mMovingView.setFrameContentVelocity(1f);
mMovingView.invalidate();
+ runAfterDraw(() -> assertEquals(60f, mViewRoot.getLastPreferredFrameRate(), 0f));
+ });
+ waitForAfterDraw();
+ }
+
+ @Test
+ @RequiresFlagsEnabled({FLAG_VIEW_VELOCITY_API,
+ FLAG_TOOLKIT_FRAME_RATE_VELOCITY_MAPPING_READ_ONLY,
+ FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY})
+ public void midVelocity80() throws Throwable {
+ if (!ViewProperties.vrr_enabled().orElse(true)) {
+ return;
+ }
+ mActivityRule.runOnUiThread(() -> {
+ ViewGroup.LayoutParams layoutParams = mMovingView.getLayoutParams();
+ layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
+ layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
+ mMovingView.setLayoutParams(layoutParams);
+ });
+ waitForFrameRateCategoryToSettle();
+ mActivityRule.runOnUiThread(() -> {
+ float midSpeed =
+ 200f * mMovingView.getContext().getResources().getDisplayMetrics().density;
+ mMovingView.setFrameContentVelocity(midSpeed);
+ mMovingView.invalidate();
runAfterDraw(() -> assertEquals(80f, mViewRoot.getLastPreferredFrameRate(), 0f));
});
waitForAfterDraw();
@@ -321,7 +346,7 @@
frameLayout.setFrameContentVelocity(1f);
mMovingView.offsetTopAndBottom(100);
frameLayout.invalidate();
- runAfterDraw(() -> assertEquals(80f, mViewRoot.getLastPreferredFrameRate(), 0f));
+ runAfterDraw(() -> assertEquals(60f, mViewRoot.getLastPreferredFrameRate(), 0f));
});
waitForAfterDraw();
}
@@ -590,7 +615,7 @@
runAfterDraw(() -> {
assertEquals(FRAME_RATE_CATEGORY_LOW,
mViewRoot.getLastPreferredFrameRateCategory());
- assertEquals(80f, mViewRoot.getLastPreferredFrameRate());
+ assertEquals(60f, mViewRoot.getLastPreferredFrameRate());
});
});
waitForAfterDraw();
diff --git a/core/tests/coretests/src/android/view/autofill/AutofillStateFingerprintTest.java b/core/tests/coretests/src/android/view/autofill/AutofillStateFingerprintTest.java
new file mode 100644
index 0000000..7cbfc40
--- /dev/null
+++ b/core/tests/coretests/src/android/view/autofill/AutofillStateFingerprintTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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 android.view.autofill;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.content.Context;
+import android.text.InputType;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.widget.TextView;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class AutofillStateFingerprintTest {
+
+ private static final Context sContext = ApplicationProvider.getApplicationContext();
+
+ private static final int MAGIC_AUTOFILL_NUMBER = 1000;
+
+ private AutofillStateFingerprint mAutofillStateFingerprint =
+ AutofillStateFingerprint.createInstance();
+
+ @Test
+ public void testSameFingerprintsForTextView() throws Exception {
+ TextView tv = new TextView(sContext);
+ tv.setHint("Password");
+ tv.setSingleLine(true);
+ tv.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ tv.setImeOptions(EditorInfo.IME_FLAG_NAVIGATE_NEXT);
+ fillViewProperties(tv);
+
+ // Create a copy Text View, and compare both id's
+ View tvCopy = copySelectiveViewAttributes(tv);
+ assertIdsEqual(tv, tvCopy);
+ }
+
+ @Test
+ public void testDifferentFingerprintsForTextViewWithDifferentHint() throws Exception {
+ TextView tv = new TextView(sContext);
+ tv.setHint("Password");
+ tv.setSingleLine(true);
+ tv.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ tv.setImeOptions(EditorInfo.IME_FLAG_NAVIGATE_NEXT);
+ fillViewProperties(tv);
+
+ TextView tvCopy = (TextView) copySelectiveViewAttributes(tv);
+ tvCopy.setHint("what a useless different hint");
+ assertIdsNotEqual(tv, tvCopy);
+ }
+
+ @Test
+ public void testSameFingerprintsForNonTextView() throws Exception {
+ View v = new View(sContext);
+ fillViewProperties(v);
+
+ // Create a copy Text View, and compare both id's
+ View copy = copySelectiveViewAttributes(v);
+ assertIdsEqual(v, copy);
+ }
+
+ @Test
+ public void testDifferentFingerprintsForNonTextViewWithDifferentVisibility() throws Exception {
+ View v = new View(sContext);
+ fillViewProperties(v);
+
+ View copy = copySelectiveViewAttributes(v);
+ copy.setVisibility(View.GONE);
+ assertIdsNotEqual(v, copy);
+ }
+
+ private void assertIdsEqual(View v1, View v2) {
+ assertEquals(mAutofillStateFingerprint.getEphemeralFingerprintId(v1, 0),
+ mAutofillStateFingerprint.getEphemeralFingerprintId(v2, 0));
+ }
+
+ private void assertIdsNotEqual(View v1, View v2) {
+ assertNotEquals(mAutofillStateFingerprint.getEphemeralFingerprintId(v1, 0),
+ mAutofillStateFingerprint.getEphemeralFingerprintId(v2, 0));
+ }
+
+ private void fillViewProperties(View view) {
+ // Fill in relevant view properties
+ view.setContentDescription("ContentDesc");
+ view.setTooltipText("TooltipText");
+ view.setAutofillHints(new String[] {"password"});
+ view.setVisibility(View.VISIBLE);
+ view.setLeft(20);
+ view.setRight(200);
+ view.setTop(20);
+ view.setBottom(200);
+ view.setPadding(0, 1, 2, 3);
+ }
+
+ // Only copy interesting view attributes, particularly the view attributes that are critical
+ // for calculating fingerprint. Keep Autofill Id different.
+ private View copySelectiveViewAttributes(View view) {
+ View copy;
+ if (view instanceof TextView) {
+ copy = new TextView(sContext);
+ copySelectiveTextViewAttributes((TextView) view, (TextView) copy);
+ } else {
+ copy = new View(sContext) {
+ public @AutofillType int getAutofillType() {
+ return view.getAutofillType();
+ }
+ };
+ }
+ // Copy over interested view properties.
+ // Keep the order same as with the tested code for easier clarity.
+ copy.setVisibility(view.getVisibility());
+ copy.setAutofillHints(view.getAutofillHints());
+ copy.setContentDescription(view.getContentDescription());
+ copy.setTooltip(view.getTooltipText());
+
+ copy.setRight(view.getRight());
+ copy.setLeft(view.getLeft());
+ copy.setTop(view.getTop());
+ copy.setBottom(view.getBottom());
+ copy.setPadding(view.getPaddingLeft(), view.getPaddingTop(),
+ view.getPaddingRight(), view.getPaddingBottom());
+
+ // DO not copy over autofill id
+ AutofillId newId = new AutofillId(view.getAutofillId().getViewId() + MAGIC_AUTOFILL_NUMBER);
+ copy.setAutofillId(newId);
+ return copy;
+ }
+
+ private void copySelectiveTextViewAttributes(TextView fromView, TextView toView) {
+ toView.setInputType(fromView.getInputType());
+ toView.setHint(fromView.getHint());
+ toView.setSingleLine(fromView.isSingleLine());
+ toView.setImeOptions(fromView.getImeOptions());
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/AcceptOnceConsumer.java
similarity index 98%
rename from libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java
rename to libs/WindowManager/Jetpack/src/androidx/window/common/AcceptOnceConsumer.java
index 63828ab..c2f827a 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/AcceptOnceConsumer.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.window.util;
+package androidx.window.common;
import android.annotation.NonNull;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/BaseDataProducer.java
similarity index 98%
rename from libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
rename to libs/WindowManager/Jetpack/src/androidx/window/common/BaseDataProducer.java
index cd26efd..e7099dc 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/BaseDataProducer.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.window.util;
+package androidx.window.common;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
@@ -125,4 +125,4 @@
mCallbacksToRemove.add(callback);
}
}
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java b/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java
index e37dea4..b95bca1 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java
@@ -16,7 +16,7 @@
package androidx.window.common;
-import static androidx.window.util.ExtensionHelper.isZero;
+import static androidx.window.common.ExtensionHelper.isZero;
import android.annotation.IntDef;
import android.annotation.Nullable;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
index 98935e9..b2bc3de 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
@@ -31,9 +31,6 @@
import android.util.Log;
import android.util.SparseIntArray;
-import androidx.window.util.AcceptOnceConsumer;
-import androidx.window.util.BaseDataProducer;
-
import com.android.internal.R;
import java.util.ArrayList;
@@ -44,7 +41,7 @@
import java.util.function.Consumer;
/**
- * An implementation of {@link androidx.window.util.BaseDataProducer} that returns
+ * An implementation of {@link BaseDataProducer} that returns
* the device's posture by mapping the state returned from {@link DeviceStateManager} to
* values provided in the resources' config at {@link R.array#config_device_state_postures}.
*/
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/common/ExtensionHelper.java
similarity index 99%
rename from libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java
rename to libs/WindowManager/Jetpack/src/androidx/window/common/ExtensionHelper.java
index a08db79..f466d60 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/ExtensionHelper.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.window.util;
+package androidx.window.common;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
index 88264f3..6d758f1 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
@@ -26,15 +26,13 @@
import android.provider.Settings;
import android.text.TextUtils;
-import androidx.window.util.BaseDataProducer;
-
import com.android.internal.R;
import java.util.Optional;
import java.util.function.Consumer;
/**
- * Implementation of {@link androidx.window.util.BaseDataProducer} that produces a
+ * Implementation of {@link BaseDataProducer} that produces a
* {@link String} that can be parsed to a {@link CommonFoldingFeature}.
* {@link RawFoldingFeatureProducer} searches for the value in two places. The first check is in
* settings where the {@link String} property is saved with the key
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
index 84984a9..a3ef68a 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -21,9 +21,9 @@
import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_FLAT;
import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
-import static androidx.window.util.ExtensionHelper.isZero;
-import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation;
-import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect;
+import static androidx.window.common.ExtensionHelper.isZero;
+import static androidx.window.common.ExtensionHelper.rotateRectToDisplayRotation;
+import static androidx.window.common.ExtensionHelper.transformToWindowSpaceRect;
import android.app.Activity;
import android.app.ActivityThread;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
index 339908a..b63fd08 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
@@ -25,11 +25,11 @@
import android.os.IBinder;
import androidx.annotation.NonNull;
+import androidx.window.common.BaseDataProducer;
import androidx.window.common.CommonFoldingFeature;
import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
import androidx.window.common.EmptyLifecycleCallbacksAdapter;
import androidx.window.common.RawFoldingFeatureProducer;
-import androidx.window.util.BaseDataProducer;
import java.util.ArrayList;
import java.util.List;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
index bb6ab47..4fd03e4 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
@@ -17,8 +17,8 @@
import static android.view.Display.DEFAULT_DISPLAY;
-import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation;
-import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect;
+import static androidx.window.common.ExtensionHelper.rotateRectToDisplayRotation;
+import static androidx.window.common.ExtensionHelper.transformToWindowSpaceRect;
import android.annotation.NonNull;
import android.app.Activity;
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/util/ExtensionHelperTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/ExtensionHelperTest.java
similarity index 99%
rename from libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/util/ExtensionHelperTest.java
rename to libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/ExtensionHelperTest.java
index 3278cdf..b6e9519 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/util/ExtensionHelperTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/ExtensionHelperTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.window.util;
+package androidx.window.common;
import static org.junit.Assert.assertEquals;
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
index b1c9a77..2b01eac 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
@@ -35,6 +35,7 @@
) {
// All desktop mode related flags will be added here
DESKTOP_WINDOWING_MODE(Flags::enableDesktopWindowingMode, true),
+ CASCADING_WINDOWS(Flags::enableCascadingWindows, true),
WALLPAPER_ACTIVITY(Flags::enableDesktopWindowingWallpaperActivity, true),
MODALS_POLICY(Flags::enableDesktopWindowingModalsPolicy, true),
THEMED_APP_HEADERS(Flags::enableThemedAppHeaders, true),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index 972dce5..24c568c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -169,6 +169,8 @@
R.layout.bubble_overflow_container, null /* root */);
mOverflowView.initialize(expandedViewManager, positioner);
addView(mOverflowView);
+ // Don't show handle for overflow
+ mHandleView.setVisibility(View.GONE);
} else {
mTaskView = bubbleTaskView.getTaskView();
mBubbleTaskViewHelper = new BubbleTaskViewHelper(mContext, expandedViewManager,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index 4fbb574..d7d19f7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -315,7 +315,7 @@
}
}
if (!mImeShowing) {
- removeImeSurface();
+ removeImeSurface(mDisplayId);
}
}
} else if (!android.view.inputmethod.Flags.refactorInsetsController()
@@ -617,7 +617,7 @@
|| hasLeash) {
t.hide(mImeSourceControl.getLeash());
}
- removeImeSurface();
+ removeImeSurface(mDisplayId);
ImeTracker.forLogging().onHidden(mStatsToken);
} else if (mAnimationDirection == DIRECTION_SHOW && !mCancelled) {
ImeTracker.forLogging().onShown(mStatsToken);
@@ -671,10 +671,10 @@
}
}
- void removeImeSurface() {
+ void removeImeSurface(int displayId) {
// Remove the IME surface to make the insets invisible for
// non-client controlled insets.
- InputMethodManagerGlobal.removeImeSurface(
+ InputMethodManagerGlobal.removeImeSurface(displayId,
e -> Slog.e(TAG, "Failed to remove IME surface.", e));
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index afe46f5..35d3876 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -569,7 +569,7 @@
ShellTaskOrganizer shellTaskOrganizer) {
int maxTaskLimit = DesktopModeStatus.getMaxTaskLimit(context);
if (!DesktopModeStatus.canEnterDesktopMode(context)
- || DESKTOP_WINDOWING_MODE.isEnabled(context)
+ || !DESKTOP_WINDOWING_MODE.isEnabled(context)
|| maxTaskLimit <= 0) {
return Optional.empty();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
index da212e7..9fcf73d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
@@ -52,8 +52,10 @@
val idealSize = calculateIdealSize(screenBounds, scale)
// If no top activity exists, apps fullscreen bounds and aspect ratio cannot be calculated.
// Instead default to the desired initial bounds.
+ val stableBounds = Rect()
+ displayLayout.getStableBoundsForDesktopMode(stableBounds)
val topActivityInfo =
- taskInfo.topActivityInfo ?: return positionInScreen(idealSize, screenBounds)
+ taskInfo.topActivityInfo ?: return positionInScreen(idealSize, stableBounds)
val initialSize: Size =
when (taskInfo.configuration.orientation) {
@@ -100,7 +102,7 @@
}
}
- return positionInScreen(initialSize, screenBounds)
+ return positionInScreen(initialSize, stableBounds)
}
/**
@@ -163,17 +165,11 @@
}
/** Adjusts bounds to be positioned in the middle of the screen. */
-private fun positionInScreen(desiredSize: Size, screenBounds: Rect): Rect {
- // TODO(b/325240051): Position apps with bottom heavy offset
- val heightOffset = (screenBounds.height() - desiredSize.height) / 2
- val widthOffset = (screenBounds.width() - desiredSize.width) / 2
- return Rect(
- widthOffset,
- heightOffset,
- desiredSize.width + widthOffset,
- desiredSize.height + heightOffset
- )
-}
+private fun positionInScreen(desiredSize: Size, stableBounds: Rect): Rect =
+ Rect(0, 0, desiredSize.width, desiredSize.height).apply {
+ val offset = DesktopTaskPosition.Center.getTopLeftCoordinates(stableBounds, this)
+ offsetTo(offset.x, offset.y)
+ }
/**
* Adjusts bounds to be positioned in the middle of the area provided, not necessarily the
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt
new file mode 100644
index 0000000..97abda8
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt
@@ -0,0 +1,151 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.app.TaskInfo
+import android.content.res.Resources
+import android.graphics.Point
+import android.graphics.Rect
+import android.view.Gravity
+import com.android.internal.annotations.VisibleForTesting
+import com.android.wm.shell.desktopmode.DesktopTaskPosition.BottomLeft
+import com.android.wm.shell.desktopmode.DesktopTaskPosition.BottomRight
+import com.android.wm.shell.desktopmode.DesktopTaskPosition.Center
+import com.android.wm.shell.desktopmode.DesktopTaskPosition.TopLeft
+import com.android.wm.shell.desktopmode.DesktopTaskPosition.TopRight
+import com.android.wm.shell.R
+
+/**
+ * The position of a task window in desktop mode.
+ */
+sealed class DesktopTaskPosition {
+ data object Center : DesktopTaskPosition() {
+ private const val WINDOW_HEIGHT_PROPORTION = 0.375
+
+ override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
+ val x = (frame.width() - window.width()) / 2
+ // Position with more margin at the bottom.
+ val y = (frame.height() - window.height()) * WINDOW_HEIGHT_PROPORTION + frame.top
+ return Point(x, y.toInt())
+ }
+
+ override fun next(): DesktopTaskPosition {
+ return BottomRight
+ }
+ }
+
+ data object BottomRight : DesktopTaskPosition() {
+ override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
+ return Point(frame.right - window.width(), frame.bottom - window.height())
+ }
+
+ override fun next(): DesktopTaskPosition {
+ return TopLeft
+ }
+ }
+
+ data object TopLeft : DesktopTaskPosition() {
+ override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
+ return Point(frame.left, frame.top)
+ }
+
+ override fun next(): DesktopTaskPosition {
+ return BottomLeft
+ }
+ }
+
+ data object BottomLeft : DesktopTaskPosition() {
+ override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
+ return Point(frame.left, frame.bottom - window.height())
+ }
+
+ override fun next(): DesktopTaskPosition {
+ return TopRight
+ }
+ }
+
+ data object TopRight : DesktopTaskPosition() {
+ override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
+ return Point(frame.right - window.width(), frame.top)
+ }
+
+ override fun next(): DesktopTaskPosition {
+ return Center
+ }
+ }
+
+ /**
+ * Returns the top left coordinates for the window to be placed in the given
+ * DesktopTaskPosition in the frame.
+ */
+ abstract fun getTopLeftCoordinates(frame: Rect, window: Rect): Point
+
+ abstract fun next(): DesktopTaskPosition
+}
+
+/**
+ * If the app has specified horizontal or vertical gravity layout, don't change the
+ * task position for cascading effect.
+ */
+fun canChangeTaskPosition(taskInfo: TaskInfo): Boolean {
+ taskInfo.topActivityInfo?.windowLayout?.let {
+ val horizontalGravityApplied = it.gravity.and(Gravity.HORIZONTAL_GRAVITY_MASK)
+ val verticalGravityApplied = it.gravity.and(Gravity.VERTICAL_GRAVITY_MASK)
+ return horizontalGravityApplied == 0 && verticalGravityApplied == 0
+ }
+ return true
+}
+
+/**
+ * Returns the current DesktopTaskPosition for a given window in the frame.
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+fun Rect.getDesktopTaskPosition(bounds: Rect): DesktopTaskPosition {
+ return when {
+ top == bounds.top && left == bounds.left -> TopLeft
+ top == bounds.top && right == bounds.right -> TopRight
+ bottom == bounds.bottom && left == bounds.left -> BottomLeft
+ bottom == bounds.bottom && right == bounds.right -> BottomRight
+ else -> Center
+ }
+}
+
+internal fun cascadeWindow(res: Resources, frame: Rect, prev: Rect, dest: Rect) {
+ val candidateBounds = Rect(dest)
+ val lastPos = frame.getDesktopTaskPosition(prev)
+ var destCoord = Center.getTopLeftCoordinates(frame, candidateBounds)
+ candidateBounds.offsetTo(destCoord.x, destCoord.y)
+ // If the default center position is not free or if last focused window is not at the
+ // center, get the next cascading window position.
+ if (!prevBoundsMovedAboveThreshold(res, prev, candidateBounds) || Center != lastPos) {
+ val nextCascadingPos = lastPos.next()
+ destCoord = nextCascadingPos.getTopLeftCoordinates(frame, dest)
+ }
+ dest.offsetTo(destCoord.x, destCoord.y)
+}
+
+internal fun prevBoundsMovedAboveThreshold(res: Resources, prev: Rect, newBounds: Rect): Boolean {
+ // This is the required minimum dp for a task to be touchable.
+ val moveThresholdPx = res.getDimensionPixelSize(
+ R.dimen.freeform_required_visible_empty_space_in_header)
+ val leftFar = newBounds.left - prev.left > moveThresholdPx
+ val topFar = newBounds.top - prev.top > moveThresholdPx
+ val rightFar = prev.right - newBounds.right > moveThresholdPx
+ val bottomFar = prev.bottom - newBounds.bottom > moveThresholdPx
+
+ return leftFar || topFar || rightFar || bottomFar
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 9d3b2e5..84d2aad 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -403,7 +403,7 @@
* The second part of the animated drag to desktop transition, called after
* [startDragToDesktop].
*/
- private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo, freeformBounds: Rect) {
+ private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo) {
ProtoLog.v(
WM_SHELL_DESKTOP_MODE,
"DesktopTasksController: finalizeDragToDesktop taskId=%d",
@@ -415,7 +415,6 @@
val taskToMinimize =
bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId)
addMoveToDesktopChanges(wct, taskInfo)
- wct.setBounds(taskInfo.token, freeformBounds)
val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct)
transition?.let { addPendingMinimizeTransition(it, taskToMinimize) }
}
@@ -1071,10 +1070,12 @@
return if (wct.isEmpty) null else wct
}
- private fun addMoveToDesktopChanges(
+ @VisibleForTesting
+ fun addMoveToDesktopChanges(
wct: WindowContainerTransaction,
taskInfo: RunningTaskInfo
) {
+ val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!!
val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
val targetWindowingMode =
@@ -1084,6 +1085,28 @@
} else {
WINDOWING_MODE_FREEFORM
}
+ val initialBounds = if (DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(context)) {
+ calculateInitialBounds(displayLayout, taskInfo)
+ } else {
+ getDefaultDesktopTaskBounds(displayLayout)
+ }
+
+ if (DesktopModeFlags.CASCADING_WINDOWS.isEnabled(context)) {
+ val stableBounds = Rect()
+ displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+ val activeTasks = desktopModeTaskRepository
+ .getActiveNonMinimizedOrderedTasks(taskInfo.displayId)
+ activeTasks.firstOrNull()?.let { activeTask ->
+ shellTaskOrganizer.getRunningTaskInfo(activeTask)?.let {
+ cascadeWindow(context.resources, stableBounds,
+ it.configuration.windowConfiguration.bounds, initialBounds)
+ }
+ }
+ }
+ if (canChangeTaskPosition(taskInfo)) {
+ wct.setBounds(taskInfo.token, initialBounds)
+ }
wct.setWindowingMode(taskInfo.token, targetWindowingMode)
wct.reorder(taskInfo.token, true /* onTop */)
if (useDesktopOverrideDensity()) {
@@ -1343,16 +1366,10 @@
val indicatorType = indicator.updateIndicatorType(inputCoordinates, taskInfo.windowingMode)
when (indicatorType) {
IndicatorType.TO_DESKTOP_INDICATOR -> {
- val displayLayout = displayController.getDisplayLayout(taskInfo.displayId)
- ?: return IndicatorType.NO_INDICATOR
// Start a new jank interaction for the drag release to desktop window animation.
interactionJankMonitor.begin(taskSurface, context,
CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE, "to_desktop")
- if (DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(context)) {
- finalizeDragToDesktop(taskInfo, calculateInitialBounds(displayLayout, taskInfo))
- } else {
- finalizeDragToDesktop(taskInfo, getDefaultDesktopTaskBounds(displayLayout))
- }
+ finalizeDragToDesktop(taskInfo)
}
IndicatorType.NO_INDICATOR,
IndicatorType.TO_FULLSCREEN_INDICATOR -> {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
index 2c0aa12..764d5a9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
@@ -83,7 +83,7 @@
}
}, mExecutor) {
@Override
- void removeImeSurface() { }
+ void removeImeSurface(int displayId) { }
}.new PerDisplay(DEFAULT_DISPLAY, ROTATION_0);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index e66018f..2368cef 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -43,6 +43,7 @@
import android.platform.test.flag.junit.SetFlagsRule
import android.testing.AndroidTestingRunner
import android.view.Display.DEFAULT_DISPLAY
+import android.view.Gravity
import android.view.SurfaceControl
import android.view.WindowManager
import android.view.WindowManager.TRANSIT_CHANGE
@@ -70,6 +71,7 @@
import com.android.window.flags.Flags
import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
import com.android.wm.shell.MockToken
+import com.android.wm.shell.R
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
@@ -185,12 +187,12 @@
private val DISPLAY_DIMENSION_SHORT = 1600
private val DISPLAY_DIMENSION_LONG = 2560
- private val DEFAULT_LANDSCAPE_BOUNDS = Rect(320, 200, 2240, 1400)
- private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 320, 1400, 2240)
- private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 680, 1575, 1880)
- private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 200, 1880, 1400)
- private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 699, 1575, 1861)
- private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 200, 1730, 1400)
+ private val DEFAULT_LANDSCAPE_BOUNDS = Rect(320, 75, 2240, 1275)
+ private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 165, 1400, 2085)
+ private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 435, 1575, 1635)
+ private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 75, 1880, 1275)
+ private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 449, 1575, 1611)
+ private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 75, 1730, 1275)
@Before
fun setUp() {
@@ -590,6 +592,161 @@
}
@Test
+ fun addMoveToDesktopChanges_gravityLeft_noBoundsApplied() {
+ setUpLandscapeDisplay()
+ val task = setUpFullscreenTask(gravity = Gravity.LEFT)
+ val wct = WindowContainerTransaction()
+ controller.addMoveToDesktopChanges(wct, task)
+
+ val finalBounds = findBoundsChange(wct, task)
+ assertThat(finalBounds).isEqualTo(Rect())
+ }
+
+ @Test
+ fun addMoveToDesktopChanges_gravityRight_noBoundsApplied() {
+ setUpLandscapeDisplay()
+ val task = setUpFullscreenTask(gravity = Gravity.RIGHT)
+ val wct = WindowContainerTransaction()
+ controller.addMoveToDesktopChanges(wct, task)
+
+ val finalBounds = findBoundsChange(wct, task)
+ assertThat(finalBounds).isEqualTo(Rect())
+ }
+
+ @Test
+ fun addMoveToDesktopChanges_gravityTop_noBoundsApplied() {
+ setUpLandscapeDisplay()
+ val task = setUpFullscreenTask(gravity = Gravity.TOP)
+ val wct = WindowContainerTransaction()
+ controller.addMoveToDesktopChanges(wct, task)
+
+ val finalBounds = findBoundsChange(wct, task)
+ assertThat(finalBounds).isEqualTo(Rect())
+ }
+
+ @Test
+ fun addMoveToDesktopChanges_gravityBottom_noBoundsApplied() {
+ setUpLandscapeDisplay()
+ val task = setUpFullscreenTask(gravity = Gravity.BOTTOM)
+ val wct = WindowContainerTransaction()
+ controller.addMoveToDesktopChanges(wct, task)
+
+ val finalBounds = findBoundsChange(wct, task)
+ assertThat(finalBounds).isEqualTo(Rect())
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+ fun addMoveToDesktopChanges_positionBottomRight() {
+ setUpLandscapeDisplay()
+ val stableBounds = Rect()
+ displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+ setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS)
+
+ val task = setUpFullscreenTask()
+ val wct = WindowContainerTransaction()
+ controller.addMoveToDesktopChanges(wct, task)
+
+ val finalBounds = findBoundsChange(wct, task)
+ assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+ .isEqualTo(DesktopTaskPosition.BottomRight)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+ fun addMoveToDesktopChanges_positionTopLeft() {
+ setUpLandscapeDisplay()
+ val stableBounds = Rect()
+ displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+ addFreeformTaskAtPosition(DesktopTaskPosition.BottomRight, stableBounds)
+
+ val task = setUpFullscreenTask()
+ val wct = WindowContainerTransaction()
+ controller.addMoveToDesktopChanges(wct, task)
+
+ val finalBounds = findBoundsChange(wct, task)
+ assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+ .isEqualTo(DesktopTaskPosition.TopLeft)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+ fun addMoveToDesktopChanges_positionBottomLeft() {
+ setUpLandscapeDisplay()
+ val stableBounds = Rect()
+ displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+ addFreeformTaskAtPosition(DesktopTaskPosition.TopLeft, stableBounds)
+
+ val task = setUpFullscreenTask()
+ val wct = WindowContainerTransaction()
+ controller.addMoveToDesktopChanges(wct, task)
+
+ val finalBounds = findBoundsChange(wct, task)
+ assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+ .isEqualTo(DesktopTaskPosition.BottomLeft)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+ fun addMoveToDesktopChanges_positionTopRight() {
+ setUpLandscapeDisplay()
+ val stableBounds = Rect()
+ displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+ addFreeformTaskAtPosition(DesktopTaskPosition.BottomLeft, stableBounds)
+
+ val task = setUpFullscreenTask()
+ val wct = WindowContainerTransaction()
+ controller.addMoveToDesktopChanges(wct, task)
+
+ val finalBounds = findBoundsChange(wct, task)
+ assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+ .isEqualTo(DesktopTaskPosition.TopRight)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+ fun addMoveToDesktopChanges_positionResetsToCenter() {
+ setUpLandscapeDisplay()
+ val stableBounds = Rect()
+ displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+ addFreeformTaskAtPosition(DesktopTaskPosition.TopRight, stableBounds)
+
+ val task = setUpFullscreenTask()
+ val wct = WindowContainerTransaction()
+ controller.addMoveToDesktopChanges(wct, task)
+
+ val finalBounds = findBoundsChange(wct, task)
+ assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+ .isEqualTo(DesktopTaskPosition.Center)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+ fun addMoveToDesktopChanges_defaultToCenterIfFree() {
+ setUpLandscapeDisplay()
+ val stableBounds = Rect()
+ displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+ val minTouchTarget = context.resources.getDimensionPixelSize(
+ R.dimen.freeform_required_visible_empty_space_in_header)
+ addFreeformTaskAtPosition(DesktopTaskPosition.Center, stableBounds,
+ Rect(0, 0, 1600, 1200), Point(0, minTouchTarget + 1))
+
+ val task = setUpFullscreenTask()
+ val wct = WindowContainerTransaction()
+ controller.addMoveToDesktopChanges(wct, task)
+
+ val finalBounds = findBoundsChange(wct, task)
+ assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+ .isEqualTo(DesktopTaskPosition.Center)
+ }
+
+ @Test
fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() {
val task = setUpFullscreenTask()
val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
@@ -2406,6 +2563,18 @@
private val desktopWallpaperIntent: Intent
get() = Intent(context, DesktopWallpaperActivity::class.java)
+ private fun addFreeformTaskAtPosition(
+ pos: DesktopTaskPosition,
+ stableBounds: Rect,
+ bounds: Rect = DEFAULT_LANDSCAPE_BOUNDS,
+ offsetPos: Point = Point(0, 0)
+ ): RunningTaskInfo {
+ val offset = pos.getTopLeftCoordinates(stableBounds, bounds)
+ val prevTaskBounds = Rect(bounds)
+ prevTaskBounds.offsetTo(offset.x + offsetPos.x, offset.y + offsetPos.y)
+ return setUpFreeformTask(bounds = prevTaskBounds)
+ }
+
private fun setUpFreeformTask(
displayId: Int = DEFAULT_DISPLAY,
bounds: Rect? = null
@@ -2434,11 +2603,13 @@
windowingMode: Int = WINDOWING_MODE_FULLSCREEN,
deviceOrientation: Int = ORIENTATION_LANDSCAPE,
screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED,
- shouldLetterbox: Boolean = false
+ shouldLetterbox: Boolean = false,
+ gravity: Int = Gravity.NO_GRAVITY
): RunningTaskInfo {
val task = createFullscreenTask(displayId)
val activityInfo = ActivityInfo()
activityInfo.screenOrientation = screenOrientation
+ activityInfo.windowLayout = ActivityInfo.WindowLayout(0, 0F, 0, 0F, gravity, 0, 0)
with(task) {
topActivityInfo = activityInfo
isResizeable = isResizable
@@ -2479,11 +2650,23 @@
private fun setUpLandscapeDisplay() {
whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_LONG)
whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_SHORT)
+ val stableBounds = Rect(0, 0, DISPLAY_DIMENSION_LONG,
+ DISPLAY_DIMENSION_SHORT - Companion.TASKBAR_FRAME_HEIGHT
+ )
+ whenever(displayLayout.getStableBoundsForDesktopMode(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
}
private fun setUpPortraitDisplay() {
whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_SHORT)
whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_LONG)
+ val stableBounds = Rect(0, 0, DISPLAY_DIMENSION_SHORT,
+ DISPLAY_DIMENSION_LONG - Companion.TASKBAR_FRAME_HEIGHT
+ )
+ whenever(displayLayout.getStableBoundsForDesktopMode(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
}
private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
@@ -2601,6 +2784,7 @@
const val SECOND_DISPLAY = 2
val STABLE_BOUNDS = Rect(0, 0, 1000, 1000)
const val MAX_TASK_LIMIT = 6
+ private const val TASKBAR_FRAME_HEIGHT = 200
}
}
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt
index b4a9172..ce02404 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt
@@ -129,7 +129,7 @@
}
}
- override fun onChanged(reason: Int) = onKeyChanged(null, reason)
+ override fun onChanged(observable: Observable, reason: Int) = onKeyChanged(null, reason)
override fun onKeyChanged(key: Any?, reason: Int) {
notifyBackupManager(key, reason)
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt
index ede7c63..4ce1d37 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt
@@ -103,7 +103,7 @@
}
/** A thread safe implementation of [KeyedObservable]. */
-class KeyedDataObservable<K> : KeyedObservable<K> {
+open class KeyedDataObservable<K> : KeyedObservable<K> {
// Instead of @GuardedBy("this"), guarded by itself because KeyedDataObservable object could be
// synchronized outside by the holder
@GuardedBy("itself") private val observers = WeakHashMap<KeyedObserver<K?>, Executor>()
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/ObservableBackupRestoreStorage.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/ObservableBackupRestoreStorage.kt
index 0e399c0..300d240 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/ObservableBackupRestoreStorage.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/ObservableBackupRestoreStorage.kt
@@ -21,5 +21,7 @@
*
* This class provides the [Observable] implementations on top of [DataObservable] by delegation.
*/
-abstract class ObservableBackupRestoreStorage :
- BackupRestoreStorage(), Observable by DataObservable()
+abstract class ObservableBackupRestoreStorage : BackupRestoreStorage(), ObservableDelegation {
+
+ final override val observableDelegate: Observable = DataObservable(this)
+}
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/Observer.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/Observer.kt
index 98d0f6e..6af3d1c 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/Observer.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/Observer.kt
@@ -32,10 +32,11 @@
*
* This callback will run in the given [Executor] when observer is added.
*
+ * @param observable observable of the change
* @param reason the reason of change
* @see [Observable.addObserver] for the notices.
*/
- fun onChanged(reason: Int)
+ fun onChanged(observable: Observable, reason: Int)
}
/** An observable object allows to observe change with [Observer]. */
@@ -68,8 +69,21 @@
fun notifyChange(reason: Int)
}
+/** Delegation of [Observable]. */
+interface ObservableDelegation : Observable {
+ /** [Observable] to delegate. */
+ val observableDelegate: Observable
+
+ override fun addObserver(observer: Observer, executor: Executor) =
+ observableDelegate.addObserver(observer, executor)
+
+ override fun removeObserver(observer: Observer) = observableDelegate.removeObserver(observer)
+
+ override fun notifyChange(reason: Int) = observableDelegate.notifyChange(reason)
+}
+
/** A thread safe implementation of [Observable]. */
-class DataObservable : Observable {
+class DataObservable(private val observable: Observable) : Observable {
// Instead of @GuardedBy("this"), guarded by itself because DataObservable object could be
// synchronized outside by the holder
@GuardedBy("itself") private val observers = WeakHashMap<Observer, Executor>()
@@ -90,7 +104,7 @@
val entries = synchronized(observers) { observers.entries.toTypedArray() }
for (entry in entries) {
val observer = entry.key // avoid reference "entry"
- entry.value.execute { observer.onChanged(reason) }
+ entry.value.execute { observer.onChanged(observable, reason) }
}
}
}
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesObservable.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesObservable.kt
new file mode 100644
index 0000000..e70ec5b
--- /dev/null
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesObservable.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.settingslib.datastore
+
+import android.content.SharedPreferences
+
+/** [SharedPreferences] based [KeyedDataObservable]. */
+class SharedPreferencesObservable(private val sharedPreferences: SharedPreferences) :
+ KeyedDataObservable<String>(), AutoCloseable {
+
+ private val listener = createSharedPreferenceListener()
+
+ init {
+ sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
+ }
+
+ override fun close() {
+ sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
+ }
+}
+
+/** Creates [SharedPreferences.OnSharedPreferenceChangeListener] for [KeyedObservable]. */
+internal fun KeyedObservable<String>.createSharedPreferenceListener() =
+ SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
+ if (key != null) {
+ notifyChange(key, DataChangeReason.UPDATE)
+ } else {
+ // On Android >= R, SharedPreferences.Editor.clear() will trigger this case
+ notifyChange(DataChangeReason.DELETE)
+ }
+ }
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesStorage.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesStorage.kt
index 20a95d7..0ca91cd 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesStorage.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesStorage.kt
@@ -80,15 +80,7 @@
return context.getSharedPreferences(intermediateName, Context.MODE_MULTI_PROCESS)
}
- private val sharedPreferencesListener =
- SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
- if (key != null) {
- notifyChange(key, DataChangeReason.UPDATE)
- } else {
- // On Android >= R, SharedPreferences.Editor.clear() will trigger this case
- notifyChange(DataChangeReason.DELETE)
- }
- }
+ private val sharedPreferencesListener = createSharedPreferenceListener()
init {
// listener is weakly referenced, so unregister is optional
@@ -191,8 +183,7 @@
else -> {
Log.e(
LOG_TAG,
- "[$name] $operation $key=$value, unknown type: ${value?.javaClass}"
- )
+ "[$name] $operation $key=$value, unknown type: ${value?.javaClass}")
}
}
}
diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/BackupRestoreStorageManagerTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/BackupRestoreStorageManagerTest.kt
index 19c574a..97b473c 100644
--- a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/BackupRestoreStorageManagerTest.kt
+++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/BackupRestoreStorageManagerTest.kt
@@ -159,7 +159,7 @@
verify(keyedObserver).onKeyChanged("key", DataChangeReason.RESTORE)
verify(anyKeyObserver).onKeyChanged(null, DataChangeReason.RESTORE)
- verify(observer).onChanged(DataChangeReason.RESTORE)
+ verify(observer).onChanged(fileStorage, DataChangeReason.RESTORE)
if (isRobolectric()) {
Shadows.shadowOf(BackupManager(application)).apply {
assertThat(isDataChanged).isFalse()
@@ -187,7 +187,7 @@
}
fileStorage.notifyChange(DataChangeReason.UPDATE)
- verify(observer).onChanged(DataChangeReason.UPDATE)
+ verify(observer).onChanged(fileStorage, DataChangeReason.UPDATE)
verify(keyedObserver, never()).onKeyChanged(any(), any())
verify(anyKeyObserver, never()).onKeyChanged(any(), any())
reset(observer)
@@ -197,7 +197,7 @@
}
keyedStorage.notifyChange("key", DataChangeReason.DELETE)
- verify(observer, never()).onChanged(any())
+ verify(observer, never()).onChanged(any(), any())
verify(keyedObserver).onKeyChanged("key", DataChangeReason.DELETE)
verify(anyKeyObserver).onKeyChanged("key", DataChangeReason.DELETE)
backupManager?.apply {
@@ -209,7 +209,7 @@
// backup manager is not notified for restore event
fileStorage.notifyChange(DataChangeReason.RESTORE)
keyedStorage.notifyChange("key", DataChangeReason.RESTORE)
- verify(observer).onChanged(DataChangeReason.RESTORE)
+ verify(observer).onChanged(fileStorage, DataChangeReason.RESTORE)
verify(keyedObserver).onKeyChanged("key", DataChangeReason.RESTORE)
verify(anyKeyObserver).onKeyChanged("key", DataChangeReason.RESTORE)
backupManager?.apply {
@@ -225,7 +225,10 @@
}
private class FileStorage(override val name: String) :
- BackupRestoreFileStorage(getApplicationContext(), "file"), Observable by DataObservable()
+ BackupRestoreFileStorage(getApplicationContext(), "file"), ObservableDelegation {
+
+ override val observableDelegate: Observable = DataObservable(this)
+ }
private class DummyBackupAgentHelper : BackupAgentHelper() {
val backupHelpers = mutableMapOf<String, BackupHelper>()
diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
index 5d0303c..bd114d1 100644
--- a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
+++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
@@ -24,6 +24,7 @@
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.reset
@@ -33,10 +34,11 @@
class ObserverTest {
private val observer1 = mock<Observer>()
private val observer2 = mock<Observer>()
+ private val originalObservable = mock<Observable>()
private val executor1: Executor = MoreExecutors.directExecutor()
private val executor2: Executor = MoreExecutors.newDirectExecutorService()
- private val observable = DataObservable()
+ private val observable = DataObservable(originalObservable)
@Test
fun addObserver_sameExecutor() {
@@ -55,7 +57,7 @@
@Test
fun addObserver_weaklyReferenced() {
val counter = AtomicInteger()
- var observer: Observer? = Observer { counter.incrementAndGet() }
+ var observer: Observer? = Observer { _, _ -> counter.incrementAndGet() }
observable.addObserver(observer!!, executor1)
observable.notifyChange(DataChangeReason.UPDATE)
@@ -77,21 +79,21 @@
observable.notifyChange(DataChangeReason.DELETE)
- verify(observer1).onChanged(DataChangeReason.DELETE)
- verify(observer2).onChanged(DataChangeReason.DELETE)
+ verify(observer1).onChanged(originalObservable, DataChangeReason.DELETE)
+ verify(observer2).onChanged(originalObservable, DataChangeReason.DELETE)
reset(observer1, observer2)
observable.removeObserver(observer2)
observable.notifyChange(DataChangeReason.UPDATE)
- verify(observer1).onChanged(DataChangeReason.UPDATE)
- verify(observer2, never()).onChanged(DataChangeReason.UPDATE)
+ verify(observer1).onChanged(originalObservable, DataChangeReason.UPDATE)
+ verify(observer2, never()).onChanged(any(), any())
}
@Test
fun notifyChange_addObserverWithinCallback() {
// ConcurrentModificationException is raised if it is not implemented correctly
- val observer = Observer { observable.addObserver(observer1, executor1) }
+ val observer = Observer { _, _ -> observable.addObserver(observer1, executor1) }
observable.addObserver(observer, executor1)
observable.notifyChange(DataChangeReason.UPDATE)
observable.removeObserver(observer)
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
index 990a2d4..732b358 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
@@ -16,6 +16,8 @@
package com.android.settingslib.notification.modes;
+import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR;
+import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME;
import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleEvent;
@@ -298,6 +300,17 @@
return mIsManualDnd;
}
+ /**
+ * A <em>custom manual</em> mode is a mode created by the user, and not yet assigned an
+ * automatic trigger condition (neither time schedule nor a calendar).
+ */
+ public boolean isCustomManual() {
+ return isSystemOwned()
+ && getType() != TYPE_SCHEDULE_TIME
+ && getType() != TYPE_SCHEDULE_CALENDAR
+ && !isManualDnd();
+ }
+
public boolean isEnabled() {
return mRule.isEnabled();
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
index e705f97..651e57c 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
@@ -27,6 +27,7 @@
import android.net.Uri;
import android.os.Parcel;
import android.service.notification.Condition;
+import android.service.notification.SystemZenRules;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenPolicy;
@@ -109,6 +110,61 @@
}
@Test
+ public void isCustomManual_customManualMode() {
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Mode", Uri.parse("x"))
+ .setPackage(SystemZenRules.PACKAGE_ANDROID)
+ .setType(AutomaticZenRule.TYPE_OTHER)
+ .build();
+ ZenMode mode = new ZenMode("id", rule, zenConfigRuleFor(rule, false));
+
+ assertThat(mode.isCustomManual()).isTrue();
+ }
+
+ @Test
+ public void isCustomManual_scheduleTime_false() {
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Mode", Uri.parse("x"))
+ .setPackage(SystemZenRules.PACKAGE_ANDROID)
+ .setType(AutomaticZenRule.TYPE_SCHEDULE_TIME)
+ .build();
+ ZenMode mode = new ZenMode("id", rule, zenConfigRuleFor(rule, false));
+
+ assertThat(mode.isCustomManual()).isFalse();
+ }
+
+ @Test
+ public void isCustomManual_scheduleCalendar_false() {
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Mode", Uri.parse("x"))
+ .setPackage(SystemZenRules.PACKAGE_ANDROID)
+ .setType(AutomaticZenRule.TYPE_SCHEDULE_CALENDAR)
+ .build();
+ ZenMode mode = new ZenMode("id", rule, zenConfigRuleFor(rule, false));
+
+ assertThat(mode.isCustomManual()).isFalse();
+ }
+
+ @Test
+ public void isCustomManual_appProvidedMode_false() {
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Mode", Uri.parse("x"))
+ .setPackage("com.some.package")
+ .setType(AutomaticZenRule.TYPE_OTHER)
+ .build();
+ ZenMode mode = new ZenMode("id", rule, zenConfigRuleFor(rule, false));
+
+ assertThat(mode.isCustomManual()).isFalse();
+ }
+
+ @Test
+ public void isCustomManual_manualDnd_false() {
+ AutomaticZenRule dndRule = new AutomaticZenRule.Builder("Mode", Uri.parse("x"))
+ .setPackage(SystemZenRules.PACKAGE_ANDROID)
+ .setType(AutomaticZenRule.TYPE_OTHER)
+ .build();
+ ZenMode mode = ZenMode.manualDndMode(dndRule, false);
+
+ assertThat(mode.isCustomManual()).isFalse();
+ }
+
+ @Test
public void getPolicy_interruptionFilterPriority_returnsZenPolicy() {
AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 2b8b23e..40a8199 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -253,6 +253,7 @@
Settings.Secure.CUSTOM_BUGREPORT_HANDLER_APP,
Settings.Secure.CUSTOM_BUGREPORT_HANDLER_USER,
Settings.Secure.CONTEXTUAL_SCREEN_TIMEOUT_ENABLED,
+ Settings.Secure.HINGE_ANGLE_LIDEVENT_ENABLED,
Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED,
Settings.Secure.HEARING_AID_RINGTONE_ROUTING,
Settings.Secure.HEARING_AID_CALL_ROUTING,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index cc5302b..3b9c683 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -406,6 +406,7 @@
VALIDATORS.put(Secure.CUSTOM_BUGREPORT_HANDLER_USER, ANY_INTEGER_VALIDATOR);
VALIDATORS.put(Secure.LOCK_SCREEN_WEATHER_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.CONTEXTUAL_SCREEN_TIMEOUT_ENABLED, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(Secure.HINGE_ANGLE_LIDEVENT_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.HEARING_AID_RINGTONE_ROUTING,
new DiscreteValueValidator(new String[] {"0", "1", "2"}));
VALIDATORS.put(Secure.HEARING_AID_CALL_ROUTING,
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 666d939..92abc4c 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -482,7 +482,7 @@
android:exported="true"
android:theme="@style/Theme.AppCompat.NoActionBar">
<intent-filter>
- <action android:name="com.android.systemui.action.TOUCHPAD_TUTORIAL"/>
+ <action android:name="com.android.systemui.action.TOUCHPAD_KEYBOARD_TUTORIAL"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index 8245cc5..3be5231 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -7,6 +7,7 @@
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
@@ -25,6 +26,9 @@
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.Back
@@ -41,6 +45,7 @@
import com.android.compose.animation.scene.observableTransitionState
import com.android.compose.animation.scene.transitions
import com.android.compose.theme.LocalAndroidColorScheme
+import com.android.internal.R.attr.focusable
import com.android.systemui.Flags.glanceableHubBackGesture
import com.android.systemui.communal.shared.model.CommunalBackgroundType
import com.android.systemui.communal.shared.model.CommunalScenes
@@ -207,6 +212,8 @@
backgroundType = backgroundType,
colors = colors,
content = content,
+ viewModel = viewModel,
+ modifier = Modifier.horizontalNestedScrollToScene(),
)
}
}
@@ -222,17 +229,41 @@
backgroundType: CommunalBackgroundType,
colors: CommunalColors,
content: CommunalContent,
+ viewModel: CommunalViewModel,
modifier: Modifier = Modifier,
) {
- Box(modifier = Modifier.element(Communal.Elements.Scrim).fillMaxSize()) {
+ val isFocusable by viewModel.isFocusable.collectAsStateWithLifecycle(initialValue = false)
+
+ Box(
+ modifier =
+ Modifier.element(Communal.Elements.Scrim)
+ .fillMaxSize()
+ .then(
+ if (isFocusable) {
+ Modifier.focusable()
+ } else {
+ Modifier.semantics { contentDescription = "" }.clearAndSetSemantics {}
+ }
+ )
+ ) {
when (backgroundType) {
CommunalBackgroundType.STATIC -> DefaultBackground(colors = colors)
CommunalBackgroundType.STATIC_GRADIENT -> StaticLinearGradient()
CommunalBackgroundType.ANIMATED -> AnimatedLinearGradient()
CommunalBackgroundType.NONE -> BackgroundTopScrim()
}
+
+ with(content) {
+ Content(
+ modifier =
+ modifier.focusable(isFocusable).semantics {
+ if (!isFocusable) {
+ contentDescription = ""
+ }
+ }
+ )
+ }
}
- with(content) { Content(modifier = modifier) }
}
/** Default background of the hub, a single color */
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 2d81f3c..4c29e79 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -223,7 +223,7 @@
val layoutDirection = LocalLayoutDirection.current
if (viewModel.isEditMode) {
- ScrollOnNewWidgetAddedEffect(communalContent, gridState)
+ ObserveNewWidgetAddedEffect(communalContent, gridState, viewModel)
} else {
ScrollOnUpdatedLiveContentEffect(communalContent, gridState)
}
@@ -555,19 +555,37 @@
}
}
-/** Observes communal content and scrolls to a newly added widget if any. */
+/**
+ * Observes communal content and determines whether a new widget has been added, upon which case:
+ * - Announce for accessibility
+ * - Scroll if the new widget is not visible
+ */
@Composable
-private fun ScrollOnNewWidgetAddedEffect(
+private fun ObserveNewWidgetAddedEffect(
communalContent: List<CommunalContentModel>,
gridState: LazyGridState,
+ viewModel: BaseCommunalViewModel,
) {
val coroutineScope = rememberCoroutineScope()
val widgetKeys = remember { mutableListOf<String>() }
+ var communalContentPending by remember { mutableStateOf(true) }
LaunchedEffect(communalContent) {
+ // Do nothing until any communal content comes in
+ if (communalContentPending && communalContent.isEmpty()) {
+ return@LaunchedEffect
+ }
+
val oldWidgetKeys = widgetKeys.toList()
+ val widgets = communalContent.filterIsInstance<CommunalContentModel.WidgetContent.Widget>()
widgetKeys.clear()
- widgetKeys.addAll(communalContent.filter { it.isWidgetContent() }.map { it.key })
+ widgetKeys.addAll(widgets.map { it.key })
+
+ // Do nothing on first communal content since we don't have a delta
+ if (communalContentPending) {
+ communalContentPending = false
+ return@LaunchedEffect
+ }
// Do nothing if there is no new widget
val indexOfFirstNewWidget = widgetKeys.indexOfFirst { !oldWidgetKeys.contains(it) }
@@ -575,6 +593,8 @@
return@LaunchedEffect
}
+ viewModel.onNewWidgetAdded(widgets[indexOfFirstNewWidget].providerInfo)
+
// Scroll if the new widget is not visible
val lastVisibleItemIndex = gridState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
if (lastVisibleItemIndex != null && indexOfFirstNewWidget > lastVisibleItemIndex) {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt
index 9e857deb..5eca5b4 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt
@@ -18,9 +18,11 @@
import android.provider.Settings
import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
+import com.android.systemui.shared.settings.data.repository.SystemSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
@@ -33,6 +35,7 @@
private val scope: CoroutineScope,
private val backgroundDispatcher: CoroutineDispatcher,
private val secureSettingsRepository: SecureSettingsRepository,
+ private val systemSettingsRepository: SystemSettingsRepository,
) {
val isNotificationHistoryEnabled: Flow<Boolean> =
secureSettingsRepository
@@ -60,4 +63,15 @@
)
}
}
+
+ val isCooldownEnabled: StateFlow<Boolean> =
+ systemSettingsRepository
+ .intSetting(name = Settings.System.NOTIFICATION_COOLDOWN_ENABLED)
+ .map { it == 1 }
+ .flowOn(backgroundDispatcher)
+ .stateIn(
+ scope = scope,
+ started = SharingStarted.Eagerly,
+ initialValue = false,
+ )
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt
index b4105bd..a274f1d 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt
@@ -38,4 +38,5 @@
val current = repository.isShowNotificationsOnLockScreenEnabled().value
repository.setShowNotificationsOnLockscreenEnabled(!current)
}
-}
+
+ val isCooldownEnabled = repository.isCooldownEnabled}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepository.kt
new file mode 100644
index 0000000..afe82fb
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepository.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.shared.settings.data.repository
+
+import android.content.ContentResolver
+import android.database.ContentObserver
+import android.provider.Settings
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+
+/**
+ * Defines interface for classes that can provide access to data from [Settings.System]. This
+ * repository doesn't guarantee to provide value across different users. For that see:
+ * [UserAwareSecureSettingsRepository] which does that for secure settings.
+ */
+interface SystemSettingsRepository {
+
+ /** Returns a [Flow] tracking the value of a setting as an [Int]. */
+ fun intSetting(
+ name: String,
+ defaultValue: Int = 0,
+ ): Flow<Int>
+
+ /** Updates the value of the setting with the given name. */
+ suspend fun setInt(
+ name: String,
+ value: Int,
+ )
+
+ suspend fun getInt(
+ name: String,
+ defaultValue: Int = 0,
+ ): Int
+
+ suspend fun getString(name: String): String?
+}
+
+class SystemSettingsRepositoryImpl(
+ private val contentResolver: ContentResolver,
+ private val backgroundDispatcher: CoroutineDispatcher,
+) : SystemSettingsRepository {
+
+ override fun intSetting(
+ name: String,
+ defaultValue: Int,
+ ): Flow<Int> {
+ return callbackFlow {
+ val observer =
+ object : ContentObserver(null) {
+ override fun onChange(selfChange: Boolean) {
+ trySend(Unit)
+ }
+ }
+
+ contentResolver.registerContentObserver(
+ Settings.System.getUriFor(name),
+ /* notifyForDescendants= */ false,
+ observer,
+ )
+ send(Unit)
+
+ awaitClose { contentResolver.unregisterContentObserver(observer) }
+ }
+ .map { Settings.System.getInt(contentResolver, name, defaultValue) }
+ // The above work is done on the background thread (which is important for accessing
+ // settings through the content resolver).
+ .flowOn(backgroundDispatcher)
+ }
+
+ override suspend fun setInt(name: String, value: Int) {
+ withContext(backgroundDispatcher) {
+ Settings.System.putInt(
+ contentResolver,
+ name,
+ value,
+ )
+ }
+ }
+
+ override suspend fun getInt(name: String, defaultValue: Int): Int {
+ return withContext(backgroundDispatcher) {
+ Settings.System.getInt(
+ contentResolver,
+ name,
+ defaultValue,
+ )
+ }
+ }
+
+ override suspend fun getString(name: String): String? {
+ return withContext(backgroundDispatcher) {
+ Settings.System.getString(
+ contentResolver,
+ name,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSystemSettingsRepository.kt b/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSystemSettingsRepository.kt
new file mode 100644
index 0000000..7da2b40
--- /dev/null
+++ b/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSystemSettingsRepository.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.shared.settings.data.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.map
+
+class FakeSystemSettingsRepository : SystemSettingsRepository {
+
+ private val settings = MutableStateFlow<Map<String, String>>(mutableMapOf())
+
+ override fun intSetting(name: String, defaultValue: Int): Flow<Int> {
+ return settings.map { it.getOrDefault(name, defaultValue.toString()) }.map { it.toInt() }
+ }
+
+ override suspend fun setInt(name: String, value: Int) {
+ settings.value = settings.value.toMutableMap().apply { this[name] = value.toString() }
+ }
+
+ override suspend fun getInt(name: String, defaultValue: Int): Int {
+ return settings.value[name]?.toInt() ?: defaultValue
+ }
+
+ override suspend fun getString(name: String): String? {
+ return settings.value[name]
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 61487b0..57ce9de 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.communal.view.viewmodel
+import android.appwidget.AppWidgetProviderInfo
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Intent
@@ -24,6 +25,9 @@
import android.content.pm.ResolveInfo
import android.content.pm.UserInfo
import android.provider.Settings
+import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityManager
+import android.view.accessibility.accessibilityManager
import android.widget.RemoteViews
import androidx.activity.result.ActivityResultLauncher
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -42,7 +46,6 @@
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.communal.domain.interactor.communalInteractor
-import com.android.systemui.communal.domain.interactor.communalPrefsInteractor
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.communal.domain.model.CommunalContentModel
@@ -61,8 +64,6 @@
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.fakeUserRepository
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
@@ -77,8 +78,12 @@
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -98,6 +103,7 @@
private lateinit var mediaRepository: FakeCommunalMediaRepository
private lateinit var communalSceneInteractor: CommunalSceneInteractor
private lateinit var communalInteractor: CommunalInteractor
+ private lateinit var accessibilityManager: AccessibilityManager
private val testableResources = context.orCreateTestableResources
@@ -119,6 +125,7 @@
selectedUserIndex = 0,
)
kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
+ accessibilityManager = kosmos.accessibilityManager
underTest =
CommunalEditModeViewModel(
@@ -130,8 +137,10 @@
uiEventLogger,
logcatLogBuffer("CommunalEditModeViewModelTest"),
kosmos.testDispatcher,
- kosmos.communalPrefsInteractor,
metricsLogger,
+ context,
+ accessibilityManager,
+ packageManager,
)
}
@@ -356,6 +365,37 @@
verify(communalInteractor).setScrollPosition(eq(index), eq(offset))
}
+ @Test
+ fun onNewWidgetAdded_accessibilityDisabled_doNothing() {
+ whenever(accessibilityManager.isEnabled).thenReturn(false)
+
+ val provider =
+ mock<AppWidgetProviderInfo> {
+ on { loadLabel(packageManager) }.thenReturn("Test Clock")
+ }
+ underTest.onNewWidgetAdded(provider)
+
+ verify(accessibilityManager, never()).sendAccessibilityEvent(any())
+ }
+
+ @Test
+ fun onNewWidgetAdded_accessibilityEnabled_sendAccessibilityAnnouncement() {
+ whenever(accessibilityManager.isEnabled).thenReturn(true)
+
+ val provider =
+ mock<AppWidgetProviderInfo> {
+ on { loadLabel(packageManager) }.thenReturn("Test Clock")
+ }
+ underTest.onNewWidgetAdded(provider)
+
+ val captor = argumentCaptor<AccessibilityEvent>()
+ verify(accessibilityManager).sendAccessibilityEvent(captor.capture())
+
+ val event = captor.firstValue
+ assertThat(event.eventType).isEqualTo(AccessibilityEvent.TYPE_ANNOUNCEMENT)
+ assertThat(event.contentDescription).isEqualTo("Test Clock widget added to lock screen")
+ }
+
private companion object {
val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
const val WIDGET_PICKER_PACKAGE_NAME = "widget_picker_package_name"
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
index 4c24ce2..5c09777 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
@@ -62,12 +62,13 @@
import com.android.systemui.dreams.complication.HideComplicationTouchHandler
import com.android.systemui.dreams.dagger.DreamOverlayComponent
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.gesture.domain.gestureInteractor
import com.android.systemui.kosmos.testScope
+import com.android.systemui.navigationbar.gestural.domain.GestureInteractor
import com.android.systemui.testKosmos
import com.android.systemui.touch.TouchInsetManager
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
@@ -84,9 +85,11 @@
import org.mockito.Mockito
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.isNull
-import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.spy
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -166,6 +169,7 @@
private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
private lateinit var communalRepository: FakeCommunalSceneRepository
private var viewCaptureSpy = spy(ViewCaptureFactory.getInstance(context))
+ private lateinit var gestureInteractor: GestureInteractor
@Captor var mViewCaptor: ArgumentCaptor<View>? = null
private lateinit var mService: DreamOverlayService
@@ -177,6 +181,7 @@
lifecycleRegistry = FakeLifecycleRegistry(mLifecycleOwner)
bouncerRepository = kosmos.fakeKeyguardBouncerRepository
communalRepository = kosmos.fakeCommunalSceneRepository
+ gestureInteractor = spy(kosmos.gestureInteractor)
whenever(mDreamOverlayComponent.getDreamOverlayContainerViewController())
.thenReturn(mDreamOverlayContainerViewController)
@@ -231,6 +236,7 @@
HOME_CONTROL_PANEL_DREAM_COMPONENT,
mDreamOverlayCallbackController,
kosmos.keyguardInteractor,
+ gestureInteractor,
WINDOW_NAME
)
}
@@ -955,6 +961,47 @@
assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
}
+ @Test
+ fun testDreamActivityGesturesBlockedOnStart() {
+ val client = client
+
+ // Inform the overlay service of dream starting.
+ client.startDream(
+ mWindowParams,
+ mDreamOverlayCallback,
+ DREAM_COMPONENT,
+ false /*shouldShowComplication*/
+ )
+ mMainExecutor.runAllReady()
+ val captor = argumentCaptor<ComponentName>()
+ verify(gestureInteractor)
+ .addGestureBlockedActivity(captor.capture(), eq(GestureInteractor.Scope.Global))
+ assertThat(captor.firstValue.packageName)
+ .isEqualTo(ComponentName.unflattenFromString(DREAM_COMPONENT)?.packageName)
+ }
+
+ @Test
+ fun testDreamActivityGesturesUnblockedOnEnd() {
+ val client = client
+
+ // Inform the overlay service of dream starting.
+ client.startDream(
+ mWindowParams,
+ mDreamOverlayCallback,
+ DREAM_COMPONENT,
+ false /*shouldShowComplication*/
+ )
+ mMainExecutor.runAllReady()
+
+ client.endDream()
+ mMainExecutor.runAllReady()
+ val captor = argumentCaptor<ComponentName>()
+ verify(gestureInteractor)
+ .removeGestureBlockedActivity(captor.capture(), eq(GestureInteractor.Scope.Global))
+ assertThat(captor.firstValue.packageName)
+ .isEqualTo(ComponentName.unflattenFromString(DREAM_COMPONENT)?.packageName)
+ }
+
internal class FakeLifecycleRegistry(provider: LifecycleOwner) : LifecycleRegistry(provider) {
val mLifecycles: MutableList<State> = ArrayList()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/data/GestureRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/data/GestureRepositoryTest.kt
new file mode 100644
index 0000000..91d37cf
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/data/GestureRepositoryTest.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.gesture.data
+
+import android.content.ComponentName
+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.navigationbar.gestural.data.respository.GestureRepositoryImpl
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class GestureRepositoryTest : SysuiTestCase() {
+ private val testDispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+ private val underTest by lazy { GestureRepositoryImpl(testDispatcher) }
+
+ @Test
+ fun addRemoveComponentToBlock_updatesBlockedComponentSet() =
+ testScope.runTest {
+ val component = mock<ComponentName>()
+
+ underTest.addGestureBlockedActivity(component)
+ val addedBlockedComponents by collectLastValue(underTest.gestureBlockedActivities)
+ assertThat(addedBlockedComponents).contains(component)
+
+ underTest.removeGestureBlockedActivity(component)
+ val removedBlockedComponents by collectLastValue(underTest.gestureBlockedActivities)
+ assertThat(removedBlockedComponents).isEmpty()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt
new file mode 100644
index 0000000..bc142e6
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.gesture.domain
+
+import android.content.ComponentName
+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.navigationbar.gestural.data.respository.GestureRepository
+import com.android.systemui.navigationbar.gestural.domain.GestureInteractor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class GestureInteractorTest : SysuiTestCase() {
+ @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ val dispatcher = StandardTestDispatcher()
+ val testScope = TestScope(dispatcher)
+
+ @Mock private lateinit var gestureRepository: GestureRepository
+
+ private val underTest by lazy {
+ GestureInteractor(gestureRepository, testScope.backgroundScope)
+ }
+
+ @Before
+ fun setup() {
+ Dispatchers.setMain(dispatcher)
+ whenever(gestureRepository.gestureBlockedActivities).thenReturn(MutableStateFlow(setOf()))
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun addBlockedActivity_testCombination() =
+ testScope.runTest {
+ val globalComponent = mock<ComponentName>()
+ whenever(gestureRepository.gestureBlockedActivities)
+ .thenReturn(MutableStateFlow(setOf(globalComponent)))
+ val localComponent = mock<ComponentName>()
+ underTest.addGestureBlockedActivity(localComponent, GestureInteractor.Scope.Local)
+ val lastSeen by collectLastValue(underTest.gestureBlockedActivities)
+ testScope.runCurrent()
+ verify(gestureRepository, never()).addGestureBlockedActivity(any())
+ assertThat(lastSeen).hasSize(2)
+ assertThat(lastSeen).containsExactly(globalComponent, localComponent)
+ }
+
+ @Test
+ fun addBlockedActivityLocally_onlyAffectsLocalInteractor() =
+ testScope.runTest {
+ val component = mock<ComponentName>()
+ underTest.addGestureBlockedActivity(component, GestureInteractor.Scope.Local)
+ val lastSeen by collectLastValue(underTest.gestureBlockedActivities)
+ testScope.runCurrent()
+ verify(gestureRepository, never()).addGestureBlockedActivity(any())
+ assertThat(lastSeen).contains(component)
+ }
+
+ @Test
+ fun removeBlockedActivityLocally_onlyAffectsLocalInteractor() =
+ testScope.runTest {
+ val component = mock<ComponentName>()
+ underTest.addGestureBlockedActivity(component, GestureInteractor.Scope.Local)
+ val lastSeen by collectLastValue(underTest.gestureBlockedActivities)
+ testScope.runCurrent()
+ underTest.removeGestureBlockedActivity(component, GestureInteractor.Scope.Local)
+ testScope.runCurrent()
+ verify(gestureRepository, never()).removeGestureBlockedActivity(any())
+ assertThat(lastSeen).isEmpty()
+ }
+
+ @Test
+ fun addBlockedActivity_invokesRepository() =
+ testScope.runTest {
+ val component = mock<ComponentName>()
+ underTest.addGestureBlockedActivity(component, GestureInteractor.Scope.Global)
+ runCurrent()
+ val captor = argumentCaptor<ComponentName>()
+ verify(gestureRepository).addGestureBlockedActivity(captor.capture())
+ assertThat(captor.firstValue).isEqualTo(component)
+ }
+
+ @Test
+ fun removeBlockedActivity_invokesRepository() =
+ testScope.runTest {
+ val component = mock<ComponentName>()
+ underTest.removeGestureBlockedActivity(component, GestureInteractor.Scope.Global)
+ runCurrent()
+ val captor = argumentCaptor<ComponentName>()
+ verify(gestureRepository).removeGestureBlockedActivity(captor.capture())
+ assertThat(captor.firstValue).isEqualTo(component)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
index 59802ef..273e3cb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
@@ -25,6 +25,7 @@
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.haptics.vibratorHelper
import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.core.FakeLogBuffer
import com.android.systemui.qs.qsTileFactory
import com.android.systemui.statusbar.policy.keyguardStateController
import com.android.systemui.testKosmos
@@ -72,6 +73,7 @@
QSLongPressEffect(
vibratorHelper,
kosmos.keyguardStateController,
+ FakeLogBuffer.Factory.create(),
)
longPressEffect.callback = callback
longPressEffect.qsTile = qsTile
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt
index 5115f5a..3b8ffcd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt
@@ -173,6 +173,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
fun transitionToGone_whenOpeningGlanceableHubEditMode() =
testScope.runTest {
kosmos.fakeKeyguardBouncerRepository.setAlternateVisible(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToLockscreenTransitionViewModelTest.kt
new file mode 100644
index 0000000..f74b74a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToLockscreenTransitionViewModelTest.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
+import com.android.systemui.keyguard.shared.model.TransitionStep
+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.Test
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AlternateBouncerToLockscreenTransitionViewModelTest : SysuiTestCase() {
+ val kosmos = testKosmos()
+ val testScope = kosmos.testScope
+
+ val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository
+
+ val underTest = kosmos.alternateBouncerToLockscreenTransitionViewModel
+
+ @Test
+ fun lockscreenAlpha_zeroInitialAlpha() =
+ testScope.runTest {
+ // ViewState starts at 0 alpha.
+ val viewState = ViewStateAccessor(alpha = { 0f })
+ val alpha by collectValues(underTest.lockscreenAlpha(viewState))
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.ALTERNATE_BOUNCER,
+ to = KeyguardState.LOCKSCREEN,
+ testScope
+ )
+
+ assertThat(alpha[0]).isEqualTo(0f)
+ // alpha duration is 250ms of the 300ms total, so 0.5f of the total is 0.6
+ assertThat(alpha[1]).isEqualTo(0.6f)
+ assertThat(alpha[2]).isEqualTo(1f)
+ }
+
+ @Test
+ fun deviceEntryParentViewAlpha() =
+ testScope.runTest {
+ val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+
+ // immediately 1f
+ keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+
+ keyguardTransitionRepository.sendTransitionStep(step(0.4f))
+ assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+
+ keyguardTransitionRepository.sendTransitionStep(step(.85f))
+ assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+
+ keyguardTransitionRepository.sendTransitionStep(step(1f))
+ assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+ }
+
+ @Test
+ fun deviceEntryBackgroundViewAlpha_udfpsEnrolled_show() =
+ testScope.runTest {
+ fingerprintPropertyRepository.supportsUdfps()
+ val bgViewAlpha by collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+ runCurrent()
+
+ // immediately 1f
+ keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ assertThat(bgViewAlpha).isEqualTo(1f)
+
+ keyguardTransitionRepository.sendTransitionStep(step(0.1f))
+ assertThat(bgViewAlpha).isEqualTo(1f)
+
+ keyguardTransitionRepository.sendTransitionStep(step(.3f))
+ assertThat(bgViewAlpha).isEqualTo(1f)
+
+ keyguardTransitionRepository.sendTransitionStep(step(.5f))
+ assertThat(bgViewAlpha).isEqualTo(1f)
+
+ keyguardTransitionRepository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+ assertThat(bgViewAlpha).isEqualTo(1f)
+ }
+
+ private fun step(
+ value: Float,
+ state: TransitionState = TransitionState.RUNNING
+ ): TransitionStep {
+ return TransitionStep(
+ from = KeyguardState.ALTERNATE_BOUNCER,
+ to = KeyguardState.LOCKSCREEN,
+ value = value,
+ transitionState = state,
+ ownerName = "AlternateBouncerToLockscreenTransitionViewModelTest"
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
index 09dca25..69b8ee1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
@@ -22,11 +22,14 @@
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.settingslib.notification.data.repository.FakeZenModeRepository
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectValues
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
+import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
+import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
@@ -40,51 +43,72 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
class ModesTileDataInteractorTest : SysuiTestCase() {
- private val zenModeRepository = FakeZenModeRepository()
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val dispatcher = kosmos.testDispatcher
+ private val zenModeRepository = kosmos.fakeZenModeRepository
- private val underTest = ModesTileDataInteractor(zenModeRepository)
+ private val underTest = ModesTileDataInteractor(zenModeRepository, dispatcher)
@EnableFlags(Flags.FLAG_MODES_UI)
@Test
- fun availableWhenFlagIsOn() = runTest {
- val availability = underTest.availability(TEST_USER).toCollection(mutableListOf())
+ fun availableWhenFlagIsOn() =
+ testScope.runTest {
+ val availability = underTest.availability(TEST_USER).toCollection(mutableListOf())
- assertThat(availability).containsExactly(true)
- }
+ assertThat(availability).containsExactly(true)
+ }
@DisableFlags(Flags.FLAG_MODES_UI)
@Test
- fun unavailableWhenFlagIsOff() = runTest {
- val availability = underTest.availability(TEST_USER).toCollection(mutableListOf())
+ fun unavailableWhenFlagIsOff() =
+ testScope.runTest {
+ val availability = underTest.availability(TEST_USER).toCollection(mutableListOf())
- assertThat(availability).containsExactly(false)
- }
+ assertThat(availability).containsExactly(false)
+ }
@EnableFlags(Flags.FLAG_MODES_UI)
@Test
- fun isActivatedWhenModesChange() = runTest {
- val dataList: List<ModesTileModel> by
- collectValues(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)))
- runCurrent()
- assertThat(dataList.map { it.isActivated }).containsExactly(false).inOrder()
+ fun isActivatedWhenModesChange() =
+ testScope.runTest {
+ val dataList: List<ModesTileModel> by
+ collectValues(
+ underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+ runCurrent()
+ assertThat(dataList.map { it.isActivated }).containsExactly(false).inOrder()
- // Add active mode
- zenModeRepository.addMode(id = "One", active = true)
- runCurrent()
- assertThat(dataList.map { it.isActivated }).containsExactly(false, true).inOrder()
+ // Add active mode
+ zenModeRepository.addMode(id = "One", active = true)
+ runCurrent()
+ assertThat(dataList.map { it.isActivated }).containsExactly(false, true).inOrder()
+ assertThat(dataList.map { it.activeModes }.last()).containsExactly("Mode One")
- // Add another mode: state hasn't changed, so this shouldn't cause another emission
- zenModeRepository.addMode(id = "Two", active = true)
- runCurrent()
- assertThat(dataList.map { it.isActivated }).containsExactly(false, true).inOrder()
+ // Add an inactive mode: state hasn't changed, so this shouldn't cause another emission
+ zenModeRepository.addMode(id = "Two", active = false)
+ runCurrent()
+ assertThat(dataList.map { it.isActivated }).containsExactly(false, true).inOrder()
+ assertThat(dataList.map { it.activeModes }.last()).containsExactly("Mode One")
- // Remove a mode and disable the other
- zenModeRepository.removeMode("One")
- runCurrent()
- zenModeRepository.deactivateMode("Two")
- runCurrent()
- assertThat(dataList.map { it.isActivated }).containsExactly(false, true, false).inOrder()
- }
+ // Add another active mode
+ zenModeRepository.addMode(id = "Three", active = true)
+ runCurrent()
+ assertThat(dataList.map { it.isActivated }).containsExactly(false, true, true).inOrder()
+ assertThat(dataList.map { it.activeModes }.last())
+ .containsExactly("Mode One", "Mode Three")
+ .inOrder()
+
+ // Remove a mode and deactivate the other
+ zenModeRepository.removeMode("One")
+ runCurrent()
+ zenModeRepository.deactivateMode("Three")
+ runCurrent()
+ assertThat(dataList.map { it.isActivated })
+ .containsExactly(false, true, true, true, false)
+ .inOrder()
+ assertThat(dataList.map { it.activeModes }.last()).isEmpty()
+ }
private companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt
index a67e7c6..4b75649 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt
@@ -54,7 +54,11 @@
fun handleClick_active() = runTest {
val expandable = mock<Expandable>()
underTest.handleInput(
- QSTileInputTestKtx.click(data = ModesTileModel(true), expandable = expandable))
+ QSTileInputTestKtx.click(
+ data = ModesTileModel(true, listOf("DND")),
+ expandable = expandable
+ )
+ )
verify(mockDialogDelegate).showDialog(eq(expandable))
}
@@ -63,14 +67,18 @@
fun handleClick_inactive() = runTest {
val expandable = mock<Expandable>()
underTest.handleInput(
- QSTileInputTestKtx.click(data = ModesTileModel(false), expandable = expandable))
+ QSTileInputTestKtx.click(
+ data = ModesTileModel(false, emptyList()),
+ expandable = expandable
+ )
+ )
verify(mockDialogDelegate).showDialog(eq(expandable))
}
@Test
fun handleLongClick_active() = runTest {
- underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(true)))
+ underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(true, listOf("DND"))))
QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS)
@@ -79,7 +87,7 @@
@Test
fun handleLongClick_inactive() = runTest {
- underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(false)))
+ underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(false, emptyList())))
QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt
index 3baf2f4..dd9711e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt
@@ -54,21 +54,35 @@
@Test
fun inactiveState() {
- val model = ModesTileModel(isActivated = false)
+ val model = ModesTileModel(isActivated = false, activeModes = emptyList())
val state = underTest.map(config, model)
assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.INACTIVE)
assertThat(state.iconRes).isEqualTo(R.drawable.qs_dnd_icon_off)
+ assertThat(state.secondaryLabel).isEqualTo("No active modes")
}
@Test
- fun activeState() {
- val model = ModesTileModel(isActivated = true)
+ fun activeState_oneMode() {
+ val model = ModesTileModel(isActivated = true, activeModes = listOf("DND"))
val state = underTest.map(config, model)
assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.ACTIVE)
assertThat(state.iconRes).isEqualTo(R.drawable.qs_dnd_icon_on)
+ assertThat(state.secondaryLabel).isEqualTo("DND is active")
+ }
+
+ @Test
+ fun activeState_multipleModes() {
+ val model =
+ ModesTileModel(isActivated = true, activeModes = listOf("Mode 1", "Mode 2", "Mode 3"))
+
+ val state = underTest.map(config, model)
+
+ assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.ACTIVE)
+ assertThat(state.iconRes).isEqualTo(R.drawable.qs_dnd_icon_on)
+ assertThat(state.secondaryLabel).isEqualTo("3 modes are active")
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 6f1bc7e..9fea7a2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -19,12 +19,10 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
-import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX
import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
@@ -38,8 +36,8 @@
import com.android.systemui.flags.DisableSceneContainer
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.flags.Flags
-import com.android.systemui.flags.andSceneContainer
import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.flags.parameterizeSceneContainerFlag
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
@@ -89,10 +87,7 @@
@JvmStatic
@Parameters(name = "{0}")
fun getParams(): List<FlagsParameterization> {
- return FlagsParameterization.allCombinationsOf(
- FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX,
- )
- .andSceneContainer()
+ return parameterizeSceneContainerFlag()
}
}
@@ -177,25 +172,6 @@
}
@Test
- @DisableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
- fun validatePaddingTopInSplitShade_refactorFlagOff_usesLargeHeaderResource() =
- testScope.runTest {
- whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5)
- overrideResource(R.bool.config_use_split_notification_shade, true)
- overrideResource(R.bool.config_use_large_screen_shade_header, true)
- overrideResource(R.dimen.large_screen_shade_header_height, 10)
- overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
-
- val paddingTop by collectLastValue(underTest.paddingTopDimen)
-
- configurationRepository.onAnyConfigurationChange()
-
- // Should directly use the header height (flagged off value)
- assertThat(paddingTop).isEqualTo(10)
- }
-
- @Test
- @EnableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
fun validatePaddingTopInSplitShade_refactorFlagOn_usesLargeHeaderHelper() =
testScope.runTest {
whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5)
@@ -267,49 +243,8 @@
}
@Test
- @DisableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
@DisableSceneContainer
- fun validateMarginTopWithLargeScreenHeader_refactorFlagOff_usesResource() =
- testScope.runTest {
- val headerResourceHeight = 50
- val headerHelperHeight = 100
- whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
- .thenReturn(headerHelperHeight)
- overrideResource(R.bool.config_use_large_screen_shade_header, true)
- overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight)
- overrideResource(R.dimen.notification_panel_margin_top, 0)
-
- val dimens by collectLastValue(underTest.configurationBasedDimensions)
-
- configurationRepository.onAnyConfigurationChange()
-
- assertThat(dimens!!.marginTop).isEqualTo(headerResourceHeight)
- }
-
- @Test
- @DisableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
- @EnableSceneContainer
- fun validateMarginTopWithLargeScreenHeader_refactorFlagOff_sceneContainerFlagOn_stillZero() =
- testScope.runTest {
- val headerResourceHeight = 50
- val headerHelperHeight = 100
- whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
- .thenReturn(headerHelperHeight)
- overrideResource(R.bool.config_use_large_screen_shade_header, true)
- overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight)
- overrideResource(R.dimen.notification_panel_margin_top, 0)
-
- val dimens by collectLastValue(underTest.configurationBasedDimensions)
-
- configurationRepository.onAnyConfigurationChange()
-
- assertThat(dimens!!.marginTop).isEqualTo(0)
- }
-
- @Test
- @DisableSceneContainer
- @EnableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
- fun validateMarginTopWithLargeScreenHeader_refactorFlagOn_usesHelper() =
+ fun validateMarginTopWithLargeScreenHeader_usesHelper() =
testScope.runTest {
val headerResourceHeight = 50
val headerHelperHeight = 100
@@ -328,7 +263,6 @@
@Test
@EnableSceneContainer
- @EnableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
fun validateMarginTopWithLargeScreenHeader_sceneContainerFlagOn_stillZero() =
testScope.runTest {
val headerResourceHeight = 50
@@ -626,44 +560,8 @@
}
@Test
- @DisableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
@DisableSceneContainer
- fun boundsOnLockscreenInSplitShade_refactorFlagOff_usesLargeHeaderResource() =
- testScope.runTest {
- val bounds by collectLastValue(underTest.bounds)
-
- // When in split shade
- whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5)
- overrideResource(R.bool.config_use_split_notification_shade, true)
- overrideResource(R.bool.config_use_large_screen_shade_header, true)
- overrideResource(R.dimen.large_screen_shade_header_height, 10)
- overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
-
- configurationRepository.onAnyConfigurationChange()
- runCurrent()
-
- // Start on lockscreen
- showLockscreen()
-
- keyguardInteractor.setNotificationContainerBounds(
- NotificationContainerBounds(top = 1f, bottom = 52f)
- )
- runCurrent()
-
- // Top should be equal to bounds (1) - padding adjustment (10)
- assertThat(bounds)
- .isEqualTo(
- NotificationContainerBounds(
- top = -9f,
- bottom = 2f,
- )
- )
- }
-
- @Test
- @EnableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
- @DisableSceneContainer
- fun boundsOnLockscreenInSplitShade_refactorFlagOn_usesLargeHeaderHelper() =
+ fun boundsOnLockscreenInSplitShade_usesLargeHeaderHelper() =
testScope.runTest {
val bounds by collectLastValue(underTest.bounds)
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 30f23bf..c29c236 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -104,7 +104,7 @@
<!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" -->
<string name="quick_settings_tiles_stock" translatable="false">
- internet,bt,flashlight,dnd,modes,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices
+ internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices
</string>
<!-- The tiles to display in QuickSettings -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 28138a5..d7c3527 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1109,6 +1109,14 @@
<!-- Priority modes: label for a mode that cannot be manually turned on [CHAR LIMIT=35] -->
<string name="zen_mode_no_manual_invocation">Manage in settings</string>
+ <string name="zen_mode_active_modes">
+ {count, plural,
+ =0 {No active modes}
+ =1 {{mode} is active}
+ other {# modes are active}
+ }
+ </string>
+
<!-- Zen mode: Priority only introduction message on first use -->
<string name="zen_priority_introduction">You won\'t be disturbed by sounds and vibrations, except from alarms, reminders, events, and callers you specify. You\'ll still hear anything you choose to play including music, videos, and games.</string>
@@ -1217,6 +1225,9 @@
<!-- Label for accessibility action that shows widgets on lock screen on click. [CHAR LIMIT=NONE] -->
<string name="accessibility_action_open_communal_hub">Widgets on lock screen</string>
+ <!-- Label for an accessibility announcement when a widget has been added to the lock screen. [CHAR LIMIT=NONE] -->
+ <string name="accessibility_announcement_communal_widget_added"><xliff:g id="widget_name" example="Calendar month view">%1$s</xliff:g> widget added to lock screen</string>
+
<!-- Indicator on keyguard to start the communal tutorial. [CHAR LIMIT=100] -->
<string name="communal_tutorial_indicator_text">Swipe left to start the communal tutorial</string>
diff --git a/packages/SystemUI/res/values/tiles_states_strings.xml b/packages/SystemUI/res/values/tiles_states_strings.xml
index c702927..ad09b46 100644
--- a/packages/SystemUI/res/values/tiles_states_strings.xml
+++ b/packages/SystemUI/res/values/tiles_states_strings.xml
@@ -85,16 +85,6 @@
<item>On</item>
</string-array>
- <!-- State names for modes (Priority modes) tile: unavailable, off, on.
- This subtitle is shown when the tile is in that particular state but does not set its own
- subtitle, so some of these may never appear on screen. They should still be translated as
- if they could appear. [CHAR LIMIT=32] -->
- <string-array name="tile_states_modes">
- <item>Unavailable</item>
- <item>Off</item>
- <item>On</item>
- </string-array>
-
<!-- State names for flashlight tile: unavailable, off, on.
This subtitle is shown when the tile is in that particular state but does not set its own
subtitle, so some of these may never appear on screen. They should still be translated as
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 428cd0e..93ee179 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -724,7 +724,10 @@
@Override
public void onResume(int reason) {
if (DEBUG) Log.d(TAG, "screen on, instance " + Integer.toHexString(hashCode()));
+ mView.clearFocus();
+ mView.clearAccessibilityFocus();
mView.requestFocus();
+ mView.requestAccessibilityFocus();
if (mCurrentSecurityMode != SecurityMode.None) {
int state = SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SHOWN;
if (mView.isSidedSecurityMode()) {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index 4be93cc..d1a5a4b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -16,6 +16,7 @@
package com.android.systemui.communal.ui.viewmodel
+import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName
import android.os.UserHandle
import android.view.View
@@ -197,6 +198,9 @@
/** Called as the user request to show the customize widget button. */
open fun onLongClick() {}
+ /** Called as the UI determines that a new widget has been added to the grid. */
+ open fun onNewWidgetAdded(provider: AppWidgetProviderInfo) {}
+
/** Called when the grid scroll position has been updated. */
open fun onScrollPositionUpdated(firstVisibleItemIndex: Int, firstVisibleItemScroll: Int) {
currentScrollIndex = firstVisibleItemIndex
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 5b825d8..1a86c71 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -19,16 +19,18 @@
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName
+import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Resources
import android.os.UserHandle
import android.util.Log
+import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityManager
import androidx.activity.result.ActivityResultLauncher
import com.android.internal.logging.UiEventLogger
import com.android.systemui.communal.data.model.CommunalWidgetCategories
import com.android.systemui.communal.domain.interactor.CommunalInteractor
-import com.android.systemui.communal.domain.interactor.CommunalPrefsInteractor
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
import com.android.systemui.communal.domain.model.CommunalContentModel
@@ -37,6 +39,7 @@
import com.android.systemui.communal.shared.model.EditModeState
import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -74,8 +77,10 @@
private val uiEventLogger: UiEventLogger,
@CommunalLog logBuffer: LogBuffer,
@Background private val backgroundDispatcher: CoroutineDispatcher,
- private val communalPrefsInteractor: CommunalPrefsInteractor,
private val metricsLogger: CommunalMetricsLogger,
+ @Application private val context: Context,
+ private val accessibilityManager: AccessibilityManager,
+ private val packageManager: PackageManager,
) : BaseCommunalViewModel(communalSceneInteractor, communalInteractor, mediaHost) {
private val logger = Logger(logBuffer, "CommunalEditModeViewModel")
@@ -156,6 +161,25 @@
uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
}
+ override fun onNewWidgetAdded(provider: AppWidgetProviderInfo) {
+ if (!accessibilityManager.isEnabled) {
+ return
+ }
+
+ // Send an accessibility announcement for the newly added widget
+ val widgetLabel = provider.loadLabel(packageManager)
+ val announcementText =
+ context.getString(
+ R.string.accessibility_announcement_communal_widget_added,
+ widgetLabel
+ )
+ accessibilityManager.sendAccessibilityEvent(
+ AccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT).apply {
+ contentDescription = announcementText
+ }
+ )
+ }
+
val isIdleOnCommunal: StateFlow<Boolean> = communalInteractor.isIdleOnCommunal
/** Launch the widget picker activity using the given {@link ActivityResultLauncher}. */
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 1771f4d..15ddf5b 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -80,6 +80,7 @@
import com.android.systemui.model.SysUiState;
import com.android.systemui.motiontool.MotionToolModule;
import com.android.systemui.navigationbar.NavigationBarComponent;
+import com.android.systemui.navigationbar.gestural.dagger.GestureModule;
import com.android.systemui.notetask.NoteTaskModule;
import com.android.systemui.people.PeopleModule;
import com.android.systemui.plugins.BcSmartspaceConfigPlugin;
@@ -215,6 +216,7 @@
FlagsModule.class,
FlagDependenciesModule.class,
FooterActionsModule.class,
+ GestureModule.class,
InputMethodModule.class,
KeyEventRepositoryModule.class,
KeyboardModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 9823985..931066d 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -27,7 +27,9 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.ActivityInfo;
import android.graphics.drawable.ColorDrawable;
+import android.service.dreams.DreamActivity;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
@@ -63,6 +65,7 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.dagger.DreamOverlayComponent;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.navigationbar.gestural.domain.GestureInteractor;
import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.touch.TouchInsetManager;
import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -135,6 +138,8 @@
private final DreamOverlayComponent mDreamOverlayComponent;
+ private ComponentName mCurrentBlockedGestureDreamActivityComponent;
+
/**
* This {@link LifecycleRegistry} controls when dream overlay functionality, like touch
* handling, should be active. It will automatically be paused when the dream overlay is hidden
@@ -222,6 +227,8 @@
private final DreamOverlayStateController mStateController;
+ private final GestureInteractor mGestureInteractor;
+
@VisibleForTesting
public enum DreamOverlayEvent implements UiEventLogger.UiEventEnum {
@UiEvent(doc = "The dream overlay has entered start.")
@@ -265,6 +272,7 @@
ComponentName homeControlPanelDreamComponent,
DreamOverlayCallbackController dreamOverlayCallbackController,
KeyguardInteractor keyguardInteractor,
+ GestureInteractor gestureInteractor,
@Named(DREAM_OVERLAY_WINDOW_TITLE) String windowTitle) {
super(executor);
mContext = context;
@@ -281,6 +289,7 @@
mWindowTitle = windowTitle;
mCommunalInteractor = communalInteractor;
mSystemDialogsCloser = systemDialogsCloser;
+ mGestureInteractor = gestureInteractor;
final ViewModelStore viewModelStore = new ViewModelStore();
final Complication.Host host =
@@ -391,6 +400,7 @@
mStarted = true;
updateRedirectWakeup();
+ updateBlockedGestureDreamActivityComponent();
}
private void updateRedirectWakeup() {
@@ -401,6 +411,18 @@
redirectWake(mCommunalAvailable && !glanceableHubAllowKeyguardWhenDreaming());
}
+ private void updateBlockedGestureDreamActivityComponent() {
+ // TODO(b/343815446): We should not be crafting this ActivityInfo ourselves. It should be
+ // in a common place, Such as DreamActivity itself.
+ final ActivityInfo info = new ActivityInfo();
+ info.name = DreamActivity.class.getName();
+ info.packageName = getDreamComponent().getPackageName();
+ mCurrentBlockedGestureDreamActivityComponent = info.getComponentName();
+
+ mGestureInteractor.addGestureBlockedActivity(mCurrentBlockedGestureDreamActivityComponent,
+ GestureInteractor.Scope.Global);
+ }
+
@Override
public void onEndDream() {
resetCurrentDreamOverlayLocked();
@@ -472,6 +494,7 @@
* into the dream window.
*/
private boolean addOverlayWindowLocked(WindowManager.LayoutParams layoutParams) {
+
mWindow = new PhoneWindow(mContext);
// Default to SystemUI name for TalkBack.
mWindow.setTitle(mWindowTitle);
@@ -554,6 +577,14 @@
}
mWindow = null;
+
+ // Always unregister the any set DreamActivity from being blocked from gestures.
+ if (mCurrentBlockedGestureDreamActivityComponent != null) {
+ mGestureInteractor.removeGestureBlockedActivity(
+ mCurrentBlockedGestureDreamActivityComponent, GestureInteractor.Scope.Global);
+ mCurrentBlockedGestureDreamActivityComponent = null;
+ }
+
mStarted = false;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 0e06117..6e4038d 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -40,6 +40,7 @@
import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
+import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
import javax.inject.Inject
@@ -51,6 +52,7 @@
// Internal notification backend dependencies
crossAppPoliteNotifications dependsOn politeNotifications
vibrateWhileUnlockedToken dependsOn politeNotifications
+ modesUi dependsOn modesApi
// Internal notification frontend dependencies
NotificationsLiveDataStoreRefactor.token dependsOn NotificationIconContainerRefactor.token
@@ -58,6 +60,7 @@
NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token
NotificationMinimalismPrototype.token dependsOn NotificationsHeadsUpRefactor.token
+ NotificationsHeadsUpRefactor.token dependsOn NotificationThrottleHun.token
// SceneContainer dependencies
SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
@@ -85,6 +88,12 @@
private inline val vibrateWhileUnlockedToken: FlagToken
get() = FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked())
+ private inline val modesUi
+ get() = FlagToken(android.app.Flags.FLAG_MODES_UI, android.app.Flags.modesUi())
+
+ private inline val modesApi
+ get() = FlagToken(android.app.Flags.FLAG_MODES_API, android.app.Flags.modesApi())
+
private inline val communalHub
get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub())
}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
index 27c20aa..4652b2a 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
@@ -26,6 +26,9 @@
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.animation.Expandable
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.QSLog
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -47,6 +50,7 @@
constructor(
private val vibratorHelper: VibratorHelper?,
private val keyguardStateController: KeyguardStateController,
+ @QSLog private val logBuffer: LogBuffer,
) {
var effectDuration = 0
@@ -101,6 +105,7 @@
}
fun handleActionDown() {
+ logEvent(qsTile?.tileSpec, state, "action down received")
when (state) {
State.IDLE -> {
setState(State.TIMEOUT_WAIT)
@@ -112,6 +117,7 @@
}
fun handleActionUp() {
+ logEvent(qsTile?.tileSpec, state, "action up received")
if (state == State.RUNNING_FORWARD) {
setState(State.RUNNING_BACKWARDS_FROM_UP)
callback?.onReverseAnimator()
@@ -130,6 +136,7 @@
}
fun handleAnimationStart() {
+ logEvent(qsTile?.tileSpec, state, "animation started")
if (state == State.TIMEOUT_WAIT) {
vibrate(longPressHint)
setState(State.RUNNING_FORWARD)
@@ -138,6 +145,7 @@
/** This function is called both when an animator completes or gets cancelled */
fun handleAnimationComplete() {
+ logEvent(qsTile?.tileSpec, state, "animation completed")
when (state) {
State.RUNNING_FORWARD -> {
vibrate(snapEffect)
@@ -147,11 +155,13 @@
callback?.onResetProperties()
setState(State.IDLE)
}
+ logEvent(qsTile?.tileSpec, state, "long click action triggered")
qsTile?.longClick(expandable)
}
State.RUNNING_BACKWARDS_FROM_UP -> {
callback?.onEffectFinishedReversing()
setState(getStateForClick())
+ logEvent(qsTile?.tileSpec, state, "click action triggered")
qsTile?.click(expandable)
}
State.RUNNING_BACKWARDS_FROM_CANCEL -> {
@@ -179,6 +189,7 @@
if (keyguardStateController.isPrimaryBouncerShowing || !isStateClickable) return false
setState(getStateForClick())
+ logEvent(qsTile?.tileSpec, state, "click action triggered")
qsTile?.click(expandable)
return true
}
@@ -273,6 +284,20 @@
return delegated
}
+ private fun logEvent(tileSpec: String?, state: State, event: String) {
+ if (!DEBUG) return
+ logBuffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ str1 = tileSpec
+ str2 = event
+ str3 = state.name
+ },
+ { "[long-press effect on $str1 tile] $str2 on state: $str3" }
+ )
+ }
+
enum class State {
IDLE, /* The effect is idle waiting for touch input */
TIMEOUT_WAIT, /* The effect is waiting for a tap timeout period */
@@ -304,4 +329,9 @@
/** Cancel the effect animator */
fun onCancelAnimator()
}
+
+ companion object {
+ private const val TAG = "QSLongPressEffect"
+ private const val DEBUG = true
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/DismissCallbackRegistry.java b/packages/SystemUI/src/com/android/systemui/keyguard/DismissCallbackRegistry.java
index d1bbc33..0d01ee1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/DismissCallbackRegistry.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/DismissCallbackRegistry.java
@@ -16,6 +16,7 @@
package com.android.systemui.keyguard;
+import android.util.Log;
import com.android.internal.policy.IKeyguardDismissCallback;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.UiBackground;
@@ -33,6 +34,7 @@
private final ArrayList<DismissCallbackWrapper> mDismissCallbacks = new ArrayList<>();
private final Executor mUiBgExecutor;
+ private final static String TAG = "DismissCallbackRegistry";
@Inject
public DismissCallbackRegistry(@UiBackground Executor uiBgExecutor) {
@@ -40,10 +42,12 @@
}
public void addCallback(IKeyguardDismissCallback callback) {
+ Log.d(TAG, "Adding callback: " + callback);
mDismissCallbacks.add(new DismissCallbackWrapper(callback));
}
public void notifyDismissCancelled() {
+ Log.d(TAG, "notifyDismissCancelled(" + mDismissCallbacks.size() + ")");
for (int i = mDismissCallbacks.size() - 1; i >= 0; i--) {
DismissCallbackWrapper callback = mDismissCallbacks.get(i);
mUiBgExecutor.execute(callback::notifyDismissCancelled);
@@ -52,6 +56,7 @@
}
public void notifyDismissSucceeded() {
+ Log.d(TAG, "notifyDismissSucceeded(" + mDismissCallbacks.size() + ")");
for (int i = mDismissCallbacks.size() - 1; i >= 0; i--) {
DismissCallbackWrapper callback = mDismissCallbacks.get(i);
mUiBgExecutor.execute(callback::notifyDismissSucceeded);
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
index a915241..ae830ee 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
@@ -198,7 +198,12 @@
interpolator = Interpolators.LINEAR
duration =
when (toState) {
+ KeyguardState.AOD -> TO_AOD_DURATION
+ KeyguardState.DOZING -> TO_DOZING_DURATION
KeyguardState.GONE -> TO_GONE_DURATION
+ KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION
+ KeyguardState.OCCLUDED -> TO_OCCLUDED_DURATION
+ KeyguardState.PRIMARY_BOUNCER -> TO_PRIMARY_BOUNCER_DURATION
else -> TRANSITION_DURATION_MS
}.inWholeMilliseconds
}
@@ -211,10 +216,11 @@
companion object {
const val TAG = "FromAlternateBouncerTransitionInteractor"
val TRANSITION_DURATION_MS = 300.milliseconds
- val TO_GONE_DURATION = 500.milliseconds
val TO_AOD_DURATION = TRANSITION_DURATION_MS
- val TO_PRIMARY_BOUNCER_DURATION = TRANSITION_DURATION_MS
val TO_DOZING_DURATION = TRANSITION_DURATION_MS
+ val TO_GONE_DURATION = 500.milliseconds
+ val TO_LOCKSCREEN_DURATION = 300.milliseconds
val TO_OCCLUDED_DURATION = TRANSITION_DURATION_MS
+ val TO_PRIMARY_BOUNCER_DURATION = TRANSITION_DURATION_MS
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
index db33acb..a250b22 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
@@ -37,6 +37,7 @@
import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder
import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay
import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel
+import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel
@@ -66,6 +67,7 @@
private val alternateBouncerDependencies: Lazy<AlternateBouncerDependencies>,
private val windowManager: Lazy<WindowManager>,
private val layoutInflater: Lazy<LayoutInflater>,
+ private val dismissCallbackRegistry: DismissCallbackRegistry,
) : CoreStartable {
private val layoutParams: WindowManager.LayoutParams
get() =
@@ -162,6 +164,7 @@
fun onBackRequested() {
alternateBouncerDependencies.get().viewModel.hideAlternateBouncer()
+ dismissCallbackRegistry.notifyDismissCancelled()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index 1b9788f..4d6577c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -74,15 +74,26 @@
val bgView = view.bgView
longPressHandlingView.listener =
object : LongPressHandlingView.Listener {
- override fun onLongPressDetected(view: View, x: Int, y: Int, isA11yAction: Boolean) {
- if (!isA11yAction && falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
+ override fun onLongPressDetected(
+ view: View,
+ x: Int,
+ y: Int,
+ isA11yAction: Boolean
+ ) {
+ if (
+ !isA11yAction && falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)
+ ) {
return
}
vibratorHelper.performHapticFeedback(
view,
HapticFeedbackConstants.CONFIRM,
)
- applicationScope.launch { viewModel.onUserInteraction() }
+ applicationScope.launch {
+ view.clearFocus()
+ view.clearAccessibilityFocus()
+ viewModel.onUserInteraction()
+ }
}
}
@@ -95,6 +106,7 @@
launch("$TAG#viewModel.isVisible") {
viewModel.isVisible.collect { isVisible ->
longPressHandlingView.isInvisible = !isVisible
+ view.isClickable = isVisible
}
}
launch("$TAG#viewModel.isLongPressEnabled") {
@@ -131,7 +143,11 @@
view,
HapticFeedbackConstants.CONFIRM,
)
- applicationScope.launch { viewModel.onUserInteraction() }
+ applicationScope.launch {
+ view.clearFocus()
+ view.clearAccessibilityFocus()
+ viewModel.onUserInteraction()
+ }
}
} else {
view.setOnClickListener(null)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
index dc7a649..0032c2f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
@@ -18,6 +18,7 @@
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToAodTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToDozingTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToOccludedTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBouncerTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel
@@ -80,6 +81,12 @@
@Binds
@IntoSet
+ abstract fun alternateBouncerToLockscreen(
+ impl: AlternateBouncerToLockscreenTransitionViewModel
+ ): DeviceEntryIconTransition
+
+ @Binds
+ @IntoSet
abstract fun alternateBouncerToOccluded(
impl: AlternateBouncerToOccludedTransitionViewModel
): DeviceEntryIconTransition
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToLockscreenTransitionViewModel.kt
new file mode 100644
index 0000000..b04521c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToLockscreenTransitionViewModel.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import android.util.MathUtils
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor
+import com.android.systemui.keyguard.shared.model.Edge
+import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down ALTERNATE_BOUNCER->LOCKSCREEN transition into discrete steps for corresponding views
+ * to consume.
+ */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class AlternateBouncerToLockscreenTransitionViewModel
+@Inject
+constructor(
+ animationFlow: KeyguardTransitionAnimationFlow,
+) : DeviceEntryIconTransition {
+ private val transitionAnimation =
+ animationFlow.setup(
+ duration = FromAlternateBouncerTransitionInteractor.TO_LOCKSCREEN_DURATION,
+ edge = Edge.create(from = ALTERNATE_BOUNCER, to = LOCKSCREEN),
+ )
+
+ fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> {
+ var startAlpha = 1f
+ return transitionAnimation.sharedFlow(
+ duration = 250.milliseconds,
+ onStart = { startAlpha = viewState.alpha() },
+ onStep = { MathUtils.lerp(startAlpha, 1f, it) },
+ )
+ }
+
+ val deviceEntryBackgroundViewAlpha: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(1f)
+ override val deviceEntryParentViewAlpha: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(1f)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 350ceb4..11889c5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -85,6 +85,8 @@
private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
private val alternateBouncerToGoneTransitionViewModel:
AlternateBouncerToGoneTransitionViewModel,
+ private val alternateBouncerToLockscreenTransitionViewModel:
+ AlternateBouncerToLockscreenTransitionViewModel,
private val aodToGoneTransitionViewModel: AodToGoneTransitionViewModel,
private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel,
@@ -238,6 +240,7 @@
alphaOnShadeExpansion,
keyguardInteractor.dismissAlpha,
alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState),
+ alternateBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
aodToGoneTransitionViewModel.lockscreenAlpha(viewState),
aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
aodToOccludedTransitionViewModel.lockscreenAlpha(viewState),
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 947336d..9eca34f 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -76,6 +76,7 @@
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationModeController;
+import com.android.systemui.navigationbar.gestural.domain.GestureInteractor;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.NavigationEdgeBackPlugin;
import com.android.systemui.plugins.PluginListener;
@@ -95,15 +96,14 @@
import com.android.systemui.statusbar.phone.LightBarController;
import com.android.systemui.util.concurrency.BackPanelUiThread;
import com.android.systemui.util.concurrency.UiThreadContext;
+import com.android.systemui.util.kotlin.JavaAdapter;
import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.desktopmode.DesktopMode;
import com.android.wm.shell.pip.Pip;
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;
@@ -157,12 +157,7 @@
private TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
@Override
public void onTaskStackChanged() {
- if (edgebackGestureHandlerGetRunningTasksBackground()) {
- mBackgroundExecutor.execute(() -> mGestureBlockingActivityRunning.set(
- isGestureBlockingActivityRunning()));
- } else {
- mGestureBlockingActivityRunning.set(isGestureBlockingActivityRunning());
- }
+ updateRunningActivityGesturesBlocked();
}
@Override
public void onTaskCreated(int taskId, ComponentName componentName) {
@@ -209,8 +204,6 @@
private final Optional<DesktopMode> mDesktopModeOptional;
private final FalsingManager mFalsingManager;
private final Configuration mLastReportedConfig = new Configuration();
- // Activities which should not trigger Back gesture.
- private final List<ComponentName> mGestureBlockingActivities = new ArrayList<>();
private final Point mDisplaySize = new Point();
private final int mDisplayId;
@@ -227,6 +220,10 @@
mBackGestureTfClassifierProviderProvider;
private final Provider<LightBarController> mLightBarControllerProvider;
+ private final GestureInteractor mGestureInteractor;
+
+ private final JavaAdapter mJavaAdapter;
+
// The left side edge width where touch down is allowed
private int mEdgeWidthLeft;
// The right side edge width where touch down is allowed
@@ -426,7 +423,9 @@
FalsingManager falsingManager,
Provider<BackGestureTfClassifierProvider> backGestureTfClassifierProviderProvider,
Provider<LightBarController> lightBarControllerProvider,
- NotificationShadeWindowController notificationShadeWindowController) {
+ NotificationShadeWindowController notificationShadeWindowController,
+ GestureInteractor gestureInteractor,
+ JavaAdapter javaAdapter) {
mContext = context;
mDisplayId = context.getDisplayId();
mUiThreadContext = uiThreadContext;
@@ -446,7 +445,13 @@
mFalsingManager = falsingManager;
mBackGestureTfClassifierProviderProvider = backGestureTfClassifierProviderProvider;
mLightBarControllerProvider = lightBarControllerProvider;
+ mGestureInteractor = gestureInteractor;
+ mJavaAdapter = javaAdapter;
mLastReportedConfig.setTo(mContext.getResources().getConfiguration());
+
+ mJavaAdapter.alwaysCollectFlow(mGestureInteractor.getGestureBlockedActivities(),
+ componentNames -> updateRunningActivityGesturesBlocked());
+
ComponentName recentsComponentName = ComponentName.unflattenFromString(
context.getString(com.android.internal.R.string.config_recentsComponentName));
if (recentsComponentName != null) {
@@ -466,8 +471,9 @@
} else {
String[] gestureBlockingActivities = resources.getStringArray(resId);
for (String gestureBlockingActivity : gestureBlockingActivities) {
- mGestureBlockingActivities.add(
- ComponentName.unflattenFromString(gestureBlockingActivity));
+ mGestureInteractor.addGestureBlockedActivity(
+ ComponentName.unflattenFromString(gestureBlockingActivity),
+ GestureInteractor.Scope.Local);
}
}
} catch (NameNotFoundException e) {
@@ -561,6 +567,15 @@
}
}
+ private void updateRunningActivityGesturesBlocked() {
+ if (edgebackGestureHandlerGetRunningTasksBackground()) {
+ mBackgroundExecutor.execute(() -> mGestureBlockingActivityRunning.set(
+ isGestureBlockingActivityRunning()));
+ } else {
+ mGestureBlockingActivityRunning.set(isGestureBlockingActivityRunning());
+ }
+ }
+
/**
* Called when the nav/task bar is attached.
*/
@@ -1293,7 +1308,8 @@
} else {
mPackageName = "_UNKNOWN";
}
- return topActivity != null && mGestureBlockingActivities.contains(topActivity);
+
+ return topActivity != null && mGestureInteractor.areGesturesBlocked(topActivity);
}
public void setBackAnimation(BackAnimation backAnimation) {
@@ -1342,6 +1358,10 @@
private final Provider<LightBarController> mLightBarControllerProvider;
private final NotificationShadeWindowController mNotificationShadeWindowController;
+ private final GestureInteractor mGestureInteractor;
+
+ private final JavaAdapter mJavaAdapter;
+
@Inject
public Factory(OverviewProxyService overviewProxyService,
SysUiState sysUiState,
@@ -1361,8 +1381,10 @@
FalsingManager falsingManager,
Provider<BackGestureTfClassifierProvider>
backGestureTfClassifierProviderProvider,
- Provider<LightBarController> lightBarControllerProvider,
- NotificationShadeWindowController notificationShadeWindowController) {
+ Provider<LightBarController> lightBarControllerProvider,
+ NotificationShadeWindowController notificationShadeWindowController,
+ GestureInteractor gestureInteractor,
+ JavaAdapter javaAdapter) {
mOverviewProxyService = overviewProxyService;
mSysUiState = sysUiState;
mPluginManager = pluginManager;
@@ -1382,6 +1404,8 @@
mBackGestureTfClassifierProviderProvider = backGestureTfClassifierProviderProvider;
mLightBarControllerProvider = lightBarControllerProvider;
mNotificationShadeWindowController = notificationShadeWindowController;
+ mGestureInteractor = gestureInteractor;
+ mJavaAdapter = javaAdapter;
}
/** Construct a {@link EdgeBackGestureHandler}. */
@@ -1407,7 +1431,9 @@
mFalsingManager,
mBackGestureTfClassifierProviderProvider,
mLightBarControllerProvider,
- mNotificationShadeWindowController));
+ mNotificationShadeWindowController,
+ mGestureInteractor,
+ mJavaAdapter));
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/dagger/GestureModule.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/dagger/GestureModule.kt
new file mode 100644
index 0000000..72a84f5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/dagger/GestureModule.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.navigationbar.gestural.dagger
+
+import com.android.systemui.navigationbar.gestural.data.respository.GestureRepository
+import com.android.systemui.navigationbar.gestural.data.respository.GestureRepositoryImpl
+import dagger.Binds
+import dagger.Module
+
+/** {@link Module} for gesture related dependencies */
+@Module
+interface GestureModule {
+ /** */
+ @Binds fun gestureRespoitory(impl: GestureRepositoryImpl): GestureRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/data/respository/GestureRepository.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/data/respository/GestureRepository.kt
new file mode 100644
index 0000000..8f35343
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/data/respository/GestureRepository.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.navigationbar.gestural.data.respository
+
+import android.content.ComponentName
+import android.util.ArraySet
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.withContext
+
+/** A repository for storing gesture related information */
+interface GestureRepository {
+ /** A {@link StateFlow} tracking activities currently blocked from gestures. */
+ val gestureBlockedActivities: StateFlow<Set<ComponentName>>
+
+ /** Adds an activity to be blocked from gestures. */
+ suspend fun addGestureBlockedActivity(activity: ComponentName)
+
+ /** Removes an activity from being blocked from gestures. */
+ suspend fun removeGestureBlockedActivity(activity: ComponentName)
+}
+
+@SysUISingleton
+class GestureRepositoryImpl
+@Inject
+constructor(@Main private val mainDispatcher: CoroutineDispatcher) : GestureRepository {
+ private val _gestureBlockedActivities = MutableStateFlow<Set<ComponentName>>(ArraySet())
+
+ override val gestureBlockedActivities: StateFlow<Set<ComponentName>>
+ get() = _gestureBlockedActivities
+
+ override suspend fun addGestureBlockedActivity(activity: ComponentName) =
+ withContext(mainDispatcher) {
+ _gestureBlockedActivities.emit(
+ _gestureBlockedActivities.value.toMutableSet().apply { add(activity) }
+ )
+ }
+
+ override suspend fun removeGestureBlockedActivity(activity: ComponentName) =
+ withContext(mainDispatcher) {
+ _gestureBlockedActivities.emit(
+ _gestureBlockedActivities.value.toMutableSet().apply { remove(activity) }
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt
new file mode 100644
index 0000000..6dc5939
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.navigationbar.gestural.domain
+
+import android.content.ComponentName
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.navigationbar.gestural.data.respository.GestureRepository
+import javax.inject.Inject
+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.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/**
+ * {@link GestureInteractor} helps interact with gesture-related logic, including accessing the
+ * underlying {@link GestureRepository}.
+ */
+class GestureInteractor
+@Inject
+constructor(
+ private val gestureRepository: GestureRepository,
+ @Application private val scope: CoroutineScope
+) {
+ enum class Scope {
+ Local,
+ Global
+ }
+
+ private val _localGestureBlockedActivities = MutableStateFlow<Set<ComponentName>>(setOf())
+ /** A {@link StateFlow} for listening to changes in Activities where gestures are blocked */
+ val gestureBlockedActivities: StateFlow<Set<ComponentName>>
+ get() =
+ combine(
+ gestureRepository.gestureBlockedActivities,
+ _localGestureBlockedActivities.asStateFlow()
+ ) { global, local ->
+ global + local
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), setOf())
+
+ /**
+ * Adds an {@link Activity} to be blocked based on component when the topmost, focused {@link
+ * Activity}.
+ */
+ fun addGestureBlockedActivity(activity: ComponentName, gestureScope: Scope) {
+ scope.launch {
+ when (gestureScope) {
+ Scope.Local -> {
+ _localGestureBlockedActivities.emit(
+ _localGestureBlockedActivities.value.toMutableSet().apply { add(activity) }
+ )
+ }
+ Scope.Global -> {
+ gestureRepository.addGestureBlockedActivity(activity)
+ }
+ }
+ }
+ }
+
+ /** Removes an {@link Activity} from being blocked from gestures. */
+ fun removeGestureBlockedActivity(activity: ComponentName, gestureScope: Scope) {
+ scope.launch {
+ when (gestureScope) {
+ Scope.Local -> {
+ _localGestureBlockedActivities.emit(
+ _localGestureBlockedActivities.value.toMutableSet().apply {
+ remove(activity)
+ }
+ )
+ }
+ Scope.Global -> {
+ gestureRepository.removeGestureBlockedActivity(activity)
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks whether the specified {@link Activity} {@link ComponentName} is being blocked from
+ * gestures.
+ */
+ fun areGesturesBlocked(activity: ComponentName): Boolean {
+ return gestureBlockedActivities.value.contains(activity)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
index 53594bb..f702da4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
@@ -27,7 +27,6 @@
subtitleIdsMap["cell"] = R.array.tile_states_cell
subtitleIdsMap["battery"] = R.array.tile_states_battery
subtitleIdsMap["dnd"] = R.array.tile_states_dnd
- subtitleIdsMap["modes"] = R.array.tile_states_modes
subtitleIdsMap["flashlight"] = R.array.tile_states_flashlight
subtitleIdsMap["rotation"] = R.array.tile_states_rotation
subtitleIdsMap["bt"] = R.array.tile_states_bt
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
index bdf935e..b927134 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
@@ -49,6 +49,7 @@
import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.RefactorFlagUtils;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.qs.QSTile.BooleanState;
@@ -105,6 +106,11 @@
) {
super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger,
statusBarStateController, activityStarter, qsLogger);
+
+ // If the flag is on, this shouldn't run at all since the modes tile replaces the DND tile.
+ RefactorFlagUtils.INSTANCE.assertInLegacyMode(android.app.Flags.modesUi(),
+ android.app.Flags.FLAG_MODES_UI);
+
mController = zenModeController;
mSharedPreferences = sharedPreferences;
mController.observe(getLifecycle(), mZenCallback);
@@ -253,18 +259,20 @@
case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS:
state.contentDescription =
mContext.getString(R.string.accessibility_quick_settings_dnd) + ", "
- + state.secondaryLabel;
+ + state.secondaryLabel;
break;
case Global.ZEN_MODE_NO_INTERRUPTIONS:
state.contentDescription =
mContext.getString(R.string.accessibility_quick_settings_dnd) + ", " +
- mContext.getString(R.string.accessibility_quick_settings_dnd_none_on)
+ mContext.getString(
+ R.string.accessibility_quick_settings_dnd_none_on)
+ ", " + state.secondaryLabel;
break;
case ZEN_MODE_ALARMS:
state.contentDescription =
mContext.getString(R.string.accessibility_quick_settings_dnd) + ", " +
- mContext.getString(R.string.accessibility_quick_settings_dnd_alarms_on)
+ mContext.getString(
+ R.string.accessibility_quick_settings_dnd_alarms_on)
+ ", " + state.secondaryLabel;
break;
default:
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
index a300031..2a33a16 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
@@ -23,13 +23,15 @@
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import androidx.lifecycle.repeatOnLifecycle
+import com.android.internal.R.attr.contentDescription
import com.android.internal.logging.MetricsLogger
import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.RefactorFlagUtils.isUnexpectedlyInLegacyMode
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.plugins.qs.QSTile.BooleanState
+import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.QsEventLogger
@@ -63,7 +65,7 @@
private val tileMapper: ModesTileMapper,
private val userActionInteractor: ModesTileUserActionInteractor,
) :
- QSTileImpl<BooleanState>(
+ QSTileImpl<QSTile.State>(
host,
uiEventLogger,
backgroundLooper,
@@ -79,6 +81,8 @@
private val config = qsTileConfigProvider.getConfig(TILE_SPEC)
init {
+ /* Check if */ isUnexpectedlyInLegacyMode(Flags.modesUi(), Flags.FLAG_MODES_UI)
+
lifecycle.coroutineScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
dataInteractor.tileData().collect { refreshState(it) }
@@ -90,7 +94,7 @@
override fun getTileLabel(): CharSequence = tileState.label
- override fun newTileState() = BooleanState()
+ override fun newTileState() = QSTile.State()
override fun handleClick(expandable: Expandable?) = runBlocking {
userActionInteractor.handleClick(expandable)
@@ -98,22 +102,22 @@
override fun getLongClickIntent(): Intent = userActionInteractor.longClickIntent
- override fun handleUpdateState(booleanState: BooleanState?, arg: Any?) {
+ override fun handleUpdateState(state: QSTile.State?, arg: Any?) {
if (arg is ModesTileModel) {
tileState = tileMapper.map(config, arg)
- booleanState?.apply {
- state = tileState.activationState.legacyState
+ state?.apply {
+ this.state = tileState.activationState.legacyState
icon = ResourceIcon.get(tileState.iconRes ?: R.drawable.qs_dnd_icon_off)
label = tileLabel
secondaryLabel = tileState.secondaryLabel
contentDescription = tileState.contentDescription
- forceExpandIcon = true
+ expandedAccessibilityClassName = tileState.expandedAccessibilityClassName
}
}
}
companion object {
- const val TILE_SPEC = "modes"
+ const val TILE_SPEC = "dnd"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
index 31e91aa..92efa40 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
@@ -19,20 +19,27 @@
import android.app.Flags
import android.os.UserHandle
import com.android.settingslib.notification.data.repository.ZenModeRepository
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
-class ModesTileDataInteractor @Inject constructor(val zenModeRepository: ZenModeRepository) :
- QSTileDataInteractor<ModesTileModel> {
- private val zenModeActive =
+class ModesTileDataInteractor
+@Inject
+constructor(
+ val zenModeRepository: ZenModeRepository,
+ @Background val bgDispatcher: CoroutineDispatcher,
+) : QSTileDataInteractor<ModesTileModel> {
+ private val activeModes =
zenModeRepository.modes
- .map { modes -> modes.any { mode -> mode.isActive } }
+ .map { modes -> modes.filter { mode -> mode.isActive }.map { it.name } }
.distinctUntilChanged()
override fun tileData(
@@ -45,7 +52,10 @@
*
* TODO(b/299909989): Remove after the transition.
*/
- fun tileData() = zenModeActive.map { ModesTileModel(isActivated = it) }
+ fun tileData() =
+ activeModes
+ .map { ModesTileModel(isActivated = it.isNotEmpty(), activeModes = it) }
+ .flowOn(bgDispatcher)
override fun availability(user: UserHandle): Flow<Boolean> = flowOf(Flags.modesUi())
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt
index e44413a..cc509ea 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt
@@ -15,4 +15,4 @@
*/
package com.android.systemui.qs.tiles.impl.modes.domain.model
-data class ModesTileModel(val isActivated: Boolean)
+data class ModesTileModel(val isActivated: Boolean, val activeModes: List<String>)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
index 7048ada..7afdb75 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
@@ -17,6 +17,8 @@
package com.android.systemui.qs.tiles.impl.modes.ui
import android.content.res.Resources
+import android.icu.text.MessageFormat
+import android.widget.Button
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
@@ -24,6 +26,7 @@
import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
import com.android.systemui.qs.tiles.viewmodel.QSTileState
import com.android.systemui.res.R
+import java.util.Locale
import javax.inject.Inject
class ModesTileMapper
@@ -46,19 +49,32 @@
contentDescription = null,
)
this.icon = { icon }
- if (data.isActivated) {
- activationState = QSTileState.ActivationState.ACTIVE
- secondaryLabel = "Some modes enabled idk" // TODO(b/346519570)
- } else {
- activationState = QSTileState.ActivationState.INACTIVE
- secondaryLabel = "Off" // TODO(b/346519570)
- }
- contentDescription = label
+ activationState =
+ if (data.isActivated) {
+ QSTileState.ActivationState.ACTIVE
+ } else {
+ QSTileState.ActivationState.INACTIVE
+ }
+ secondaryLabel = getModesStatus(data, resources)
+ contentDescription = "$label. $secondaryLabel"
supportedActions =
setOf(
QSTileState.UserAction.CLICK,
QSTileState.UserAction.LONG_CLICK,
)
sideViewIcon = QSTileState.SideViewIcon.Chevron
+ expandedAccessibilityClass = Button::class
}
+
+ private fun getModesStatus(data: ModesTileModel, resources: Resources): String {
+ val msgFormat =
+ MessageFormat(resources.getString(R.string.zen_mode_active_modes), Locale.getDefault())
+ val count = data.activeModes.count()
+ val args: MutableMap<String, Any> = HashMap()
+ args["count"] = count
+ if (count >= 1) {
+ args["mode"] = data.activeModes[0]
+ }
+ return msgFormat.format(args)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 3fca84e..5b50133 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -172,12 +172,28 @@
private fun resetShadeSessions() {
applicationScope.launch {
- sceneBackInteractor.backStack
- // We are in a session if either Shade or QuickSettings is on the back stack
- .map { backStack ->
- backStack.asIterable().any { it == Scenes.Shade || it == Scenes.QuickSettings }
+ combine(
+ sceneBackInteractor.backStack
+ // We are in a session if either Shade or QuickSettings is on the back stack
+ .map { backStack ->
+ backStack.asIterable().any {
+ it == Scenes.Shade || it == Scenes.QuickSettings
+ }
+ }
+ .distinctUntilChanged(),
+ sceneInteractor.transitionState
+ .mapNotNull { state ->
+ // We are also in a session if either Shade or QuickSettings is the
+ // current scene
+ when (state) {
+ is ObservableTransitionState.Idle -> state.currentScene
+ is ObservableTransitionState.Transition -> state.fromScene
+ }.let { it == Scenes.Shade || it == Scenes.QuickSettings }
+ }
+ .distinctUntilChanged()
+ ) { inBackStack, isCurrentScene ->
+ inBackStack || isCurrentScene
}
- .distinctUntilChanged()
// Once a session has ended, clear the session storage.
.filter { inSession -> !inSession }
.collect { shadeSessionStorage.clear() }
diff --git a/packages/SystemUI/src/com/android/systemui/settings/SystemSettingsRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/settings/SystemSettingsRepositoryModule.kt
new file mode 100644
index 0000000..02ce74a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/settings/SystemSettingsRepositoryModule.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.settings
+
+import android.content.ContentResolver
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.shared.settings.data.repository.SystemSettingsRepository
+import com.android.systemui.shared.settings.data.repository.SystemSettingsRepositoryImpl
+import dagger.Module
+import dagger.Provides
+import kotlinx.coroutines.CoroutineDispatcher
+
+@Module
+object SystemSettingsRepositoryModule {
+ @JvmStatic
+ @Provides
+ @SysUISingleton
+ fun provideSystemSettingsRepository(
+ contentResolver: ContentResolver,
+ @Background backgroundDispatcher: CoroutineDispatcher,
+ ): SystemSettingsRepository =
+ SystemSettingsRepositoryImpl(contentResolver, backgroundDispatcher)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index aff57bd..e50d64b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -50,10 +50,10 @@
import javax.inject.Inject
/**
- * Coordinates heads up notification (HUN) interactions with the notification pipeline based on
- * the HUN state reported by the [HeadsUpManager]. In this class we only consider one
- * notification, in particular the [HeadsUpManager.getTopEntry], to be HeadsUpping at a
- * time even though other notifications may be queued to heads up next.
+ * Coordinates heads up notification (HUN) interactions with the notification pipeline based on the
+ * HUN state reported by the [HeadsUpManager]. In this class we only consider one notification, in
+ * particular the [HeadsUpManager.getTopEntry], to be HeadsUpping at a time even though other
+ * notifications may be queued to heads up next.
*
* The current HUN, but not HUNs that are queued to heads up, will be:
* - Lifetime extended until it's no longer heads upping.
@@ -64,7 +64,9 @@
* Note: The inflation callback in [PreparationCoordinator] handles showing HUNs.
*/
@CoordinatorScope
-class HeadsUpCoordinator @Inject constructor(
+class HeadsUpCoordinator
+@Inject
+constructor(
private val mLogger: HeadsUpCoordinatorLogger,
private val mSystemClock: SystemClock,
private val mHeadsUpManager: HeadsUpManager,
@@ -104,8 +106,8 @@
}
/**
- * Once the pipeline starts running, we can look through posted entries and quickly process
- * any that don't have groups, and thus will never gave a group heads up edge case.
+ * Once the pipeline starts running, we can look through posted entries and quickly process any
+ * that don't have groups, and thus will never gave a group heads up edge case.
*/
fun onBeforeTransformGroups(list: List<ListEntry>) {
mNow = mSystemClock.currentTimeMillis()
@@ -128,120 +130,137 @@
* we know that stability and [NotifPromoter]s have been applied, so we can use the location of
* notifications in this list to determine what kind of group heads up behavior should happen.
*/
- fun onBeforeFinalizeFilter(list: List<ListEntry>) = mHeadsUpManager.modifyHuns { hunMutator ->
- // Nothing to do if there are no other adds/updates
- if (mPostedEntries.isEmpty()) {
- return@modifyHuns
- }
- // Calculate a bunch of information about the logical group and the locations of group
- // entries in the nearly-finalized shade list. These may be used in the per-group loop.
- val postedEntriesByGroup = mPostedEntries.values.groupBy { it.entry.sbn.groupKey }
- val logicalMembersByGroup = mNotifPipeline.allNotifs.asSequence()
- .filter { postedEntriesByGroup.contains(it.sbn.groupKey) }
- .groupBy { it.sbn.groupKey }
- val groupLocationsByKey: Map<String, GroupLocation> by lazy { getGroupLocationsByKey(list) }
- mLogger.logEvaluatingGroups(postedEntriesByGroup.size)
- // For each group, determine which notification(s) for a group should heads up.
- postedEntriesByGroup.forEach { (groupKey, postedEntries) ->
- // get and classify the logical members
- val logicalMembers = logicalMembersByGroup[groupKey] ?: emptyList()
- val logicalSummary = logicalMembers.find { it.sbn.notification.isGroupSummary }
+ fun onBeforeFinalizeFilter(list: List<ListEntry>) =
+ mHeadsUpManager.modifyHuns { hunMutator ->
+ // Nothing to do if there are no other adds/updates
+ if (mPostedEntries.isEmpty()) {
+ return@modifyHuns
+ }
+ // Calculate a bunch of information about the logical group and the locations of group
+ // entries in the nearly-finalized shade list. These may be used in the per-group loop.
+ val postedEntriesByGroup = mPostedEntries.values.groupBy { it.entry.sbn.groupKey }
+ val logicalMembersByGroup =
+ mNotifPipeline.allNotifs
+ .asSequence()
+ .filter { postedEntriesByGroup.contains(it.sbn.groupKey) }
+ .groupBy { it.sbn.groupKey }
+ val groupLocationsByKey: Map<String, GroupLocation> by lazy {
+ getGroupLocationsByKey(list)
+ }
+ mLogger.logEvaluatingGroups(postedEntriesByGroup.size)
+ // For each group, determine which notification(s) for a group should heads up.
+ postedEntriesByGroup.forEach { (groupKey, postedEntries) ->
+ // get and classify the logical members
+ val logicalMembers = logicalMembersByGroup[groupKey] ?: emptyList()
+ val logicalSummary = logicalMembers.find { it.sbn.notification.isGroupSummary }
- // Report the start of this group's evaluation
- mLogger.logEvaluatingGroup(groupKey, postedEntries.size, logicalMembers.size)
+ // Report the start of this group's evaluation
+ mLogger.logEvaluatingGroup(groupKey, postedEntries.size, logicalMembers.size)
- // If there is no logical summary, then there is no heads up to transfer
- if (logicalSummary == null) {
- postedEntries.forEach {
- handlePostedEntry(it, hunMutator, scenario = "logical-summary-missing")
+ // If there is no logical summary, then there is no heads up to transfer
+ if (logicalSummary == null) {
+ postedEntries.forEach {
+ handlePostedEntry(it, hunMutator, scenario = "logical-summary-missing")
+ }
+ return@forEach
}
- return@forEach
- }
- // If summary isn't wanted to be heads up, then there is no heads up to transfer
- if (!isGoingToShowHunStrict(logicalSummary)) {
- postedEntries.forEach {
- handlePostedEntry(it, hunMutator, scenario = "logical-summary-not-heads-up")
+ // If summary isn't wanted to be heads up, then there is no heads up to transfer
+ if (!isGoingToShowHunStrict(logicalSummary)) {
+ postedEntries.forEach {
+ handlePostedEntry(it, hunMutator, scenario = "logical-summary-not-heads-up")
+ }
+ return@forEach
}
- return@forEach
- }
- // The group is heads up! Overall goals:
- // - Maybe transfer its heads up to a child
- // - Also let any/all newly heads up children still heads up
- var childToReceiveParentHeadsUp: NotificationEntry?
- var targetType = "undefined"
+ // The group is heads up! Overall goals:
+ // - Maybe transfer its heads up to a child
+ // - Also let any/all newly heads up children still heads up
+ var childToReceiveParentHeadsUp: NotificationEntry?
+ var targetType = "undefined"
- // If the parent is heads up, always look at the posted notification with the newest
- // 'when', and if it is isolated with GROUP_ALERT_SUMMARY, then it should receive the
- // parent's heads up.
- childToReceiveParentHeadsUp =
- findHeadsUpOverride(postedEntries, groupLocationsByKey::getLocation)
- if (childToReceiveParentHeadsUp != null) {
- targetType = "headsUpOverride"
- }
-
- // If the summary is Detached and we have not picked a receiver of the heads up, then we
- // need to look for the best child to heads up in place of the summary.
- val isSummaryAttached = groupLocationsByKey.contains(logicalSummary.key)
- if (!isSummaryAttached && childToReceiveParentHeadsUp == null) {
+ // If the parent is heads up, always look at the posted notification with the newest
+ // 'when', and if it is isolated with GROUP_ALERT_SUMMARY, then it should receive
+ // the
+ // parent's heads up.
childToReceiveParentHeadsUp =
- findBestTransferChild(logicalMembers, groupLocationsByKey::getLocation)
+ findHeadsUpOverride(postedEntries, groupLocationsByKey::getLocation)
if (childToReceiveParentHeadsUp != null) {
- targetType = "bestChild"
+ targetType = "headsUpOverride"
}
- }
- // If there is no child to receive the parent heads up, then just handle the posted
- // entries and return.
- if (childToReceiveParentHeadsUp == null) {
- postedEntries.forEach {
- handlePostedEntry(it, hunMutator, scenario = "no-transfer-target")
+ // If the summary is Detached and we have not picked a receiver of the heads up,
+ // then we
+ // need to look for the best child to heads up in place of the summary.
+ val isSummaryAttached = groupLocationsByKey.contains(logicalSummary.key)
+ if (!isSummaryAttached && childToReceiveParentHeadsUp == null) {
+ childToReceiveParentHeadsUp =
+ findBestTransferChild(logicalMembers, groupLocationsByKey::getLocation)
+ if (childToReceiveParentHeadsUp != null) {
+ targetType = "bestChild"
+ }
}
- return@forEach
- }
- // At this point we just need to initiate the transfer
- val summaryUpdate = mPostedEntries[logicalSummary.key]
+ // If there is no child to receive the parent heads up, then just handle the posted
+ // entries and return.
+ if (childToReceiveParentHeadsUp == null) {
+ postedEntries.forEach {
+ handlePostedEntry(it, hunMutator, scenario = "no-transfer-target")
+ }
+ return@forEach
+ }
- // Because we now know for certain that some child is going to heads up for this summary
- // (as we have found a child to transfer the heads up to), mark the group as having
- // interrupted. This will allow us to know in the future that the "should heads up"
- // state of this group has already been handled, just not via the summary entry itself.
- logicalSummary.setInterruption()
- mLogger.logSummaryMarkedInterrupted(logicalSummary.key, childToReceiveParentHeadsUp.key)
+ // At this point we just need to initiate the transfer
+ val summaryUpdate = mPostedEntries[logicalSummary.key]
- // If the summary was not attached, then remove the heads up from the detached summary.
- // Otherwise we can simply ignore its posted update.
- if (!isSummaryAttached) {
- val summaryUpdateForRemoval = summaryUpdate?.also {
- it.shouldHeadsUpEver = false
- } ?: PostedEntry(
- logicalSummary,
- wasAdded = false,
- wasUpdated = false,
- shouldHeadsUpEver = false,
- shouldHeadsUpAgain = false,
- isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(logicalSummary.key),
- isBinding = isEntryBinding(logicalSummary),
+ // Because we now know for certain that some child is going to heads up for this
+ // summary
+ // (as we have found a child to transfer the heads up to), mark the group as having
+ // interrupted. This will allow us to know in the future that the "should heads up"
+ // state of this group has already been handled, just not via the summary entry
+ // itself.
+ logicalSummary.setInterruption()
+ mLogger.logSummaryMarkedInterrupted(
+ logicalSummary.key,
+ childToReceiveParentHeadsUp.key
)
- // If we transfer the heads up notification and the summary isn't even attached,
- // that means we should ensure the summary is no longer a heads up notification,
- // so we remove it here.
- handlePostedEntry(
+
+ // If the summary was not attached, then remove the heads up from the detached
+ // summary.
+ // Otherwise we can simply ignore its posted update.
+ if (!isSummaryAttached) {
+ val summaryUpdateForRemoval =
+ summaryUpdate?.also { it.shouldHeadsUpEver = false }
+ ?: PostedEntry(
+ logicalSummary,
+ wasAdded = false,
+ wasUpdated = false,
+ shouldHeadsUpEver = false,
+ shouldHeadsUpAgain = false,
+ isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(logicalSummary.key),
+ isBinding = isEntryBinding(logicalSummary),
+ )
+ // If we transfer the heads up notification and the summary isn't even attached,
+ // that means we should ensure the summary is no longer a heads up notification,
+ // so we remove it here.
+ handlePostedEntry(
summaryUpdateForRemoval,
hunMutator,
- scenario = "detached-summary-remove-heads-up")
- } else if (summaryUpdate != null) {
- mLogger.logPostedEntryWillNotEvaluate(
+ scenario = "detached-summary-remove-heads-up"
+ )
+ } else if (summaryUpdate != null) {
+ mLogger.logPostedEntryWillNotEvaluate(
summaryUpdate,
- reason = "attached-summary-transferred")
- }
+ reason = "attached-summary-transferred"
+ )
+ }
- // Handle all posted entries -- if the child receiving the parent's heads up is in the
- // list, then set its flags to ensure it heads up.
- var didHeadsUpChildToReceiveParentHeadsUp = false
- postedEntries.asSequence()
+ // Handle all posted entries -- if the child receiving the parent's heads up is in
+ // the
+ // list, then set its flags to ensure it heads up.
+ var didHeadsUpChildToReceiveParentHeadsUp = false
+ postedEntries
+ .asSequence()
.filter { it.key != logicalSummary.key }
.forEach { postedEntry ->
if (childToReceiveParentHeadsUp.key == postedEntry.key) {
@@ -249,44 +268,49 @@
postedEntry.shouldHeadsUpEver = true
postedEntry.shouldHeadsUpAgain = true
handlePostedEntry(
- postedEntry,
- hunMutator,
- scenario = "child-heads-up-transfer-target-$targetType")
+ postedEntry,
+ hunMutator,
+ scenario = "child-heads-up-transfer-target-$targetType"
+ )
didHeadsUpChildToReceiveParentHeadsUp = true
} else {
handlePostedEntry(
- postedEntry,
- hunMutator,
- scenario = "child-heads-up-non-target")
+ postedEntry,
+ hunMutator,
+ scenario = "child-heads-up-non-target"
+ )
}
}
- // If the child receiving the heads up notification was not updated on this tick
- // (which can happen in a standard heads up transfer scenario), then construct an update
- // so that we can apply it.
- if (!didHeadsUpChildToReceiveParentHeadsUp) {
- val posted = PostedEntry(
- childToReceiveParentHeadsUp,
- wasAdded = false,
- wasUpdated = false,
- shouldHeadsUpEver = true,
- shouldHeadsUpAgain = true,
- isHeadsUpEntry =
+ // If the child receiving the heads up notification was not updated on this tick
+ // (which can happen in a standard heads up transfer scenario), then construct an
+ // update
+ // so that we can apply it.
+ if (!didHeadsUpChildToReceiveParentHeadsUp) {
+ val posted =
+ PostedEntry(
+ childToReceiveParentHeadsUp,
+ wasAdded = false,
+ wasUpdated = false,
+ shouldHeadsUpEver = true,
+ shouldHeadsUpAgain = true,
+ isHeadsUpEntry =
mHeadsUpManager.isHeadsUpEntry(childToReceiveParentHeadsUp.key),
- isBinding = isEntryBinding(childToReceiveParentHeadsUp),
- )
- handlePostedEntry(
+ isBinding = isEntryBinding(childToReceiveParentHeadsUp),
+ )
+ handlePostedEntry(
posted,
hunMutator,
- scenario = "non-posted-child-heads-up-transfer-target-$targetType")
+ scenario = "non-posted-child-heads-up-transfer-target-$targetType"
+ )
+ }
}
- }
- // After this method runs, all posted entries should have been handled (or skipped).
- mPostedEntries.clear()
+ // After this method runs, all posted entries should have been handled (or skipped).
+ mPostedEntries.clear()
- // Also take this opportunity to clean up any stale entry update times
- cleanUpEntryTimes()
- }
+ // Also take this opportunity to clean up any stale entry update times
+ cleanUpEntryTimes()
+ }
/**
* Find the posted child with the newest when, and return it if it is isolated and has
@@ -295,34 +319,38 @@
private fun findHeadsUpOverride(
postedEntries: List<PostedEntry>,
locationLookupByKey: (String) -> GroupLocation,
- ): NotificationEntry? = postedEntries.asSequence()
- .filter { posted -> !posted.entry.sbn.notification.isGroupSummary }
- .sortedBy { posted ->
- -posted.entry.sbn.notification.getWhen()
- }
- .firstOrNull()
- ?.let { posted ->
- posted.entry.takeIf { entry ->
- locationLookupByKey(entry.key) == GroupLocation.Isolated &&
+ ): NotificationEntry? =
+ postedEntries
+ .asSequence()
+ .filter { posted -> !posted.entry.sbn.notification.isGroupSummary }
+ .sortedBy { posted -> -posted.entry.sbn.notification.getWhen() }
+ .firstOrNull()
+ ?.let { posted ->
+ posted.entry.takeIf { entry ->
+ locationLookupByKey(entry.key) == GroupLocation.Isolated &&
entry.sbn.notification.groupAlertBehavior == GROUP_ALERT_SUMMARY
+ }
}
- }
/**
- * Of children which are attached, look for the child to receive the notification:
- * First prefer children which were updated, then looking for the ones with the newest 'when'
+ * Of children which are attached, look for the child to receive the notification: First prefer
+ * children which were updated, then looking for the ones with the newest 'when'
*/
private fun findBestTransferChild(
logicalMembers: List<NotificationEntry>,
locationLookupByKey: (String) -> GroupLocation,
- ): NotificationEntry? = logicalMembers.asSequence()
- .filter { !it.sbn.notification.isGroupSummary }
- .filter { locationLookupByKey(it.key) != GroupLocation.Detached }
- .sortedWith(compareBy(
- { !mPostedEntries.contains(it.key) },
- { -it.sbn.notification.getWhen() },
- ))
- .firstOrNull()
+ ): NotificationEntry? =
+ logicalMembers
+ .asSequence()
+ .filter { !it.sbn.notification.isGroupSummary }
+ .filter { locationLookupByKey(it.key) != GroupLocation.Detached }
+ .sortedWith(
+ compareBy(
+ { !mPostedEntries.contains(it.key) },
+ { -it.sbn.notification.getWhen() },
+ )
+ )
+ .firstOrNull()
private fun getGroupLocationsByKey(list: List<ListEntry>): Map<String, GroupLocation> =
mutableMapOf<String, GroupLocation>().also { map ->
@@ -387,197 +415,217 @@
mHeadsUpViewBinder.bindHeadsUpView(posted.entry, this::onHeadsUpViewBound)
}
- private val mNotifCollectionListener = object : NotifCollectionListener {
- /**
- * Notification was just added and if it should heads up, bind the view and then show it.
- */
- override fun onEntryAdded(entry: NotificationEntry) {
- // First check whether this notification should launch a full screen intent, and
- // launch it if needed.
- val fsiDecision =
- mVisualInterruptionDecisionProvider.makeUnloggedFullScreenIntentDecision(entry)
- mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(fsiDecision)
- if (fsiDecision.shouldInterrupt) {
- mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
- } else if (fsiDecision.wouldInterruptWithoutDnd) {
- // If DND was the only reason this entry was suppressed, note it for potential
- // reconsideration on later ranking updates.
- addForFSIReconsideration(entry, mSystemClock.currentTimeMillis())
- }
-
- // makeAndLogHeadsUpDecision includes check for whether this notification should be
- // filtered
- val shouldHeadsUpEver =
- mVisualInterruptionDecisionProvider.makeAndLogHeadsUpDecision(entry).shouldInterrupt
- mPostedEntries[entry.key] = PostedEntry(
- entry,
- wasAdded = true,
- wasUpdated = false,
- shouldHeadsUpEver = shouldHeadsUpEver,
- shouldHeadsUpAgain = true,
- isHeadsUpEntry = false,
- isBinding = false,
- )
-
- // Record the last updated time for this key
- setUpdateTime(entry, mSystemClock.currentTimeMillis())
- }
-
- /**
- * Notification could've updated to be heads up or not heads up. Even if it did update to
- * heads up, if the notification specified that it only wants to heads up once, don't heads
- * up again.
- */
- override fun onEntryUpdated(entry: NotificationEntry) {
- val shouldHeadsUpEver =
- mVisualInterruptionDecisionProvider.makeAndLogHeadsUpDecision(entry).shouldInterrupt
- val shouldHeadsUpAgain = shouldHunAgain(entry)
- val isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(entry.key)
- val isBinding = isEntryBinding(entry)
- val posted = mPostedEntries.compute(entry.key) { _, value ->
- value?.also { update ->
- update.wasUpdated = true
- update.shouldHeadsUpEver = shouldHeadsUpEver
- update.shouldHeadsUpAgain = update.shouldHeadsUpAgain || shouldHeadsUpAgain
- update.isHeadsUpEntry = isHeadsUpEntry
- update.isBinding = isBinding
- } ?: PostedEntry(
- entry,
- wasAdded = false,
- wasUpdated = true,
- shouldHeadsUpEver = shouldHeadsUpEver,
- shouldHeadsUpAgain = shouldHeadsUpAgain,
- isHeadsUpEntry = isHeadsUpEntry,
- isBinding = isBinding,
- )
- }
- // Handle cancelling heads up here, rather than in the OnBeforeFinalizeFilter, so that
- // work can be done before the ShadeListBuilder is run. This prevents re-entrant
- // behavior between this Coordinator, HeadsUpManager, and VisualStabilityManager.
- if (posted?.shouldHeadsUpEver == false) {
- if (posted.isHeadsUpEntry) {
- // We don't want this to be interrupting anymore, let's remove it
- mHeadsUpManager.removeNotification(posted.key, false /*removeImmediately*/)
- } else if (posted.isBinding) {
- // Don't let the bind finish
- cancelHeadsUpBind(posted.entry)
+ private val mNotifCollectionListener =
+ object : NotifCollectionListener {
+ /**
+ * Notification was just added and if it should heads up, bind the view and then show
+ * it.
+ */
+ override fun onEntryAdded(entry: NotificationEntry) {
+ // First check whether this notification should launch a full screen intent, and
+ // launch it if needed.
+ val fsiDecision =
+ mVisualInterruptionDecisionProvider.makeUnloggedFullScreenIntentDecision(entry)
+ mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(fsiDecision)
+ if (fsiDecision.shouldInterrupt) {
+ mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
+ } else if (fsiDecision.wouldInterruptWithoutDnd) {
+ // If DND was the only reason this entry was suppressed, note it for potential
+ // reconsideration on later ranking updates.
+ addForFSIReconsideration(entry, mSystemClock.currentTimeMillis())
}
+
+ // makeAndLogHeadsUpDecision includes check for whether this notification should be
+ // filtered
+ val shouldHeadsUpEver =
+ mVisualInterruptionDecisionProvider
+ .makeAndLogHeadsUpDecision(entry)
+ .shouldInterrupt
+ mPostedEntries[entry.key] =
+ PostedEntry(
+ entry,
+ wasAdded = true,
+ wasUpdated = false,
+ shouldHeadsUpEver = shouldHeadsUpEver,
+ shouldHeadsUpAgain = true,
+ isHeadsUpEntry = false,
+ isBinding = false,
+ )
+
+ // Record the last updated time for this key
+ setUpdateTime(entry, mSystemClock.currentTimeMillis())
}
- // Update last updated time for this entry
- setUpdateTime(entry, mSystemClock.currentTimeMillis())
- }
-
- /**
- * Stop showing as heads up once removed from the notification collection
- */
- override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
- mPostedEntries.remove(entry.key)
- mEntriesUpdateTimes.remove(entry.key)
- cancelHeadsUpBind(entry)
- val entryKey = entry.key
- if (mHeadsUpManager.isHeadsUpEntry(entryKey)) {
- // TODO: This should probably know the RemoteInputCoordinator's conditions,
- // or otherwise reference that coordinator's state, rather than replicate its logic
- val removeImmediatelyForRemoteInput = (mRemoteInputManager.isSpinning(entryKey) &&
- !NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY)
- mHeadsUpManager.removeNotification(entry.key, removeImmediatelyForRemoteInput)
- }
- }
-
- override fun onEntryCleanUp(entry: NotificationEntry) {
- mHeadsUpViewBinder.abortBindCallback(entry)
- }
-
- /**
- * Identify notifications whose heads-up state changes when the notification rankings are
- * updated, and have those changed notifications heads up if necessary.
- *
- * This method will occur after any operations in onEntryAdded or onEntryUpdated, so any
- * handling of ranking changes needs to take into account that we may have just made a
- * PostedEntry for some of these notifications.
- */
- override fun onRankingApplied() {
- // Because a ranking update may cause some notifications that are no longer (or were
- // never) in mPostedEntries to need to heads up, we need to check every notification
- // known to the pipeline.
- for (entry in mNotifPipeline.allNotifs) {
- // Only consider entries that are recent enough, since we want to apply a fairly
- // strict threshold for when an entry should be updated via only ranking and not an
- // app-provided notification update.
- if (!isNewEnoughForRankingUpdate(entry)) continue
-
- // The only entries we consider heads up for here are entries that have never
- // interrupted and that now say they should heads up or FSI; if they've heads uped in
- // the past, we don't want to incorrectly heads up a second time if there wasn't an
- // explicit notification update.
- if (entry.hasInterrupted()) continue
-
- // Before potentially allowing heads-up, check for any candidates for a FSI launch.
- // Any entry that is a candidate meets two criteria:
- // - was suppressed from FSI launch only by a DND suppression
- // - is within the recency window for reconsideration
- // If any of these entries are no longer suppressed, launch the FSI now.
- if (isCandidateForFSIReconsideration(entry)) {
- val decision =
- mVisualInterruptionDecisionProvider.makeUnloggedFullScreenIntentDecision(
- entry
- )
- if (decision.shouldInterrupt) {
- // Log both the launch of the full screen and also that this was via a
- // ranking update, and finally revoke candidacy for FSI reconsideration
- mLogger.logEntryUpdatedToFullScreen(entry.key, decision.logReason)
- mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(decision)
- mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
- mFSIUpdateCandidates.remove(entry.key)
-
- // if we launch the FSI then this is no longer a candidate for HUN
- continue
- } else if (decision.wouldInterruptWithoutDnd) {
- // decision has not changed; no need to log
- } else {
- // some other condition is now blocking FSI; log that and revoke candidacy
- // for FSI reconsideration
- mLogger.logEntryDisqualifiedFromFullScreen(entry.key, decision.logReason)
- mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(decision)
- mFSIUpdateCandidates.remove(entry.key)
+ /**
+ * Notification could've updated to be heads up or not heads up. Even if it did update
+ * to heads up, if the notification specified that it only wants to heads up once, don't
+ * heads up again.
+ */
+ override fun onEntryUpdated(entry: NotificationEntry) {
+ val shouldHeadsUpEver =
+ mVisualInterruptionDecisionProvider
+ .makeAndLogHeadsUpDecision(entry)
+ .shouldInterrupt
+ val shouldHeadsUpAgain = shouldHunAgain(entry)
+ val isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(entry.key)
+ val isBinding = isEntryBinding(entry)
+ val posted =
+ mPostedEntries.compute(entry.key) { _, value ->
+ value?.also { update ->
+ update.wasUpdated = true
+ update.shouldHeadsUpEver = shouldHeadsUpEver
+ update.shouldHeadsUpAgain =
+ update.shouldHeadsUpAgain || shouldHeadsUpAgain
+ update.isHeadsUpEntry = isHeadsUpEntry
+ update.isBinding = isBinding
+ }
+ ?: PostedEntry(
+ entry,
+ wasAdded = false,
+ wasUpdated = true,
+ shouldHeadsUpEver = shouldHeadsUpEver,
+ shouldHeadsUpAgain = shouldHeadsUpAgain,
+ isHeadsUpEntry = isHeadsUpEntry,
+ isBinding = isBinding,
+ )
+ }
+ // Handle cancelling heads up here, rather than in the OnBeforeFinalizeFilter, so
+ // that
+ // work can be done before the ShadeListBuilder is run. This prevents re-entrant
+ // behavior between this Coordinator, HeadsUpManager, and VisualStabilityManager.
+ if (posted?.shouldHeadsUpEver == false) {
+ if (posted.isHeadsUpEntry) {
+ // We don't want this to be interrupting anymore, let's remove it
+ mHeadsUpManager.removeNotification(posted.key, false /*removeImmediately*/)
+ } else if (posted.isBinding) {
+ // Don't let the bind finish
+ cancelHeadsUpBind(posted.entry)
}
}
- // The cases where we should consider this notification to be updated:
- // - if this entry is not present in PostedEntries, and is now in a shouldHeadsUp
- // state
- // - if it is present in PostedEntries and the previous state of shouldHeadsUp
- // differs from the updated one
- val decision =
- mVisualInterruptionDecisionProvider.makeUnloggedHeadsUpDecision(entry)
- val shouldHeadsUpEver = decision.shouldInterrupt
- val postedShouldHeadsUpEver = mPostedEntries[entry.key]?.shouldHeadsUpEver ?: false
- val shouldUpdateEntry = postedShouldHeadsUpEver != shouldHeadsUpEver
+ // Update last updated time for this entry
+ setUpdateTime(entry, mSystemClock.currentTimeMillis())
+ }
- if (shouldUpdateEntry) {
- mLogger.logEntryUpdatedByRanking(
- entry.key,
- shouldHeadsUpEver,
- decision.logReason
- )
- onEntryUpdated(entry)
+ /** Stop showing as heads up once removed from the notification collection */
+ override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+ mPostedEntries.remove(entry.key)
+ mEntriesUpdateTimes.remove(entry.key)
+ cancelHeadsUpBind(entry)
+ val entryKey = entry.key
+ if (mHeadsUpManager.isHeadsUpEntry(entryKey)) {
+ // TODO: This should probably know the RemoteInputCoordinator's conditions,
+ // or otherwise reference that coordinator's state, rather than replicate its
+ // logic
+ val removeImmediatelyForRemoteInput =
+ (mRemoteInputManager.isSpinning(entryKey) &&
+ !NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY)
+ mHeadsUpManager.removeNotification(entry.key, removeImmediatelyForRemoteInput)
+ }
+ }
+
+ override fun onEntryCleanUp(entry: NotificationEntry) {
+ mHeadsUpViewBinder.abortBindCallback(entry)
+ }
+
+ /**
+ * Identify notifications whose heads-up state changes when the notification rankings
+ * are updated, and have those changed notifications heads up if necessary.
+ *
+ * This method will occur after any operations in onEntryAdded or onEntryUpdated, so any
+ * handling of ranking changes needs to take into account that we may have just made a
+ * PostedEntry for some of these notifications.
+ */
+ override fun onRankingApplied() {
+ // Because a ranking update may cause some notifications that are no longer (or were
+ // never) in mPostedEntries to need to heads up, we need to check every notification
+ // known to the pipeline.
+ for (entry in mNotifPipeline.allNotifs) {
+ // Only consider entries that are recent enough, since we want to apply a fairly
+ // strict threshold for when an entry should be updated via only ranking and not
+ // an
+ // app-provided notification update.
+ if (!isNewEnoughForRankingUpdate(entry)) continue
+
+ // The only entries we consider heads up for here are entries that have never
+ // interrupted and that now say they should heads up or FSI; if they've heads
+ // uped in
+ // the past, we don't want to incorrectly heads up a second time if there wasn't
+ // an
+ // explicit notification update.
+ if (entry.hasInterrupted()) continue
+
+ // Before potentially allowing heads-up, check for any candidates for a FSI
+ // launch.
+ // Any entry that is a candidate meets two criteria:
+ // - was suppressed from FSI launch only by a DND suppression
+ // - is within the recency window for reconsideration
+ // If any of these entries are no longer suppressed, launch the FSI now.
+ if (isCandidateForFSIReconsideration(entry)) {
+ val decision =
+ mVisualInterruptionDecisionProvider
+ .makeUnloggedFullScreenIntentDecision(entry)
+ if (decision.shouldInterrupt) {
+ // Log both the launch of the full screen and also that this was via a
+ // ranking update, and finally revoke candidacy for FSI reconsideration
+ mLogger.logEntryUpdatedToFullScreen(entry.key, decision.logReason)
+ mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(
+ decision
+ )
+ mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
+ mFSIUpdateCandidates.remove(entry.key)
+
+ // if we launch the FSI then this is no longer a candidate for HUN
+ continue
+ } else if (decision.wouldInterruptWithoutDnd) {
+ // decision has not changed; no need to log
+ } else {
+ // some other condition is now blocking FSI; log that and revoke
+ // candidacy
+ // for FSI reconsideration
+ mLogger.logEntryDisqualifiedFromFullScreen(
+ entry.key,
+ decision.logReason
+ )
+ mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(
+ decision
+ )
+ mFSIUpdateCandidates.remove(entry.key)
+ }
+ }
+
+ // The cases where we should consider this notification to be updated:
+ // - if this entry is not present in PostedEntries, and is now in a
+ // shouldHeadsUp
+ // state
+ // - if it is present in PostedEntries and the previous state of shouldHeadsUp
+ // differs from the updated one
+ val decision =
+ mVisualInterruptionDecisionProvider.makeUnloggedHeadsUpDecision(entry)
+ val shouldHeadsUpEver = decision.shouldInterrupt
+ val postedShouldHeadsUpEver =
+ mPostedEntries[entry.key]?.shouldHeadsUpEver ?: false
+ val shouldUpdateEntry = postedShouldHeadsUpEver != shouldHeadsUpEver
+
+ if (shouldUpdateEntry) {
+ mLogger.logEntryUpdatedByRanking(
+ entry.key,
+ shouldHeadsUpEver,
+ decision.logReason
+ )
+ onEntryUpdated(entry)
+ }
}
}
}
- }
- /**
- * Checks whether an update for a notification warrants an heads up for the user.
- */
+ /** Checks whether an update for a notification warrants an heads up for the user. */
private fun shouldHunAgain(entry: NotificationEntry): Boolean {
return (!entry.hasInterrupted() ||
- (entry.sbn.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE) == 0)
+ (entry.sbn.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE) == 0)
}
- /**
- * Sets the updated time for the given entry to the specified time.
- */
+ /** Sets the updated time for the given entry to the specified time. */
@VisibleForTesting
fun setUpdateTime(entry: NotificationEntry, time: Long) {
mEntriesUpdateTimes[entry.key] = time
@@ -593,10 +641,10 @@
}
/**
- * Checks whether the entry is new enough to be updated via ranking update.
- * We want to avoid updating an entry too long after it was originally posted/updated when we're
- * only reacting to a ranking change, as relevant ranking updates are expected to come in
- * fairly soon after the posting of a notification.
+ * Checks whether the entry is new enough to be updated via ranking update. We want to avoid
+ * updating an entry too long after it was originally posted/updated when we're only reacting to
+ * a ranking change, as relevant ranking updates are expected to come in fairly soon after the
+ * posting of a notification.
*/
private fun isNewEnoughForRankingUpdate(entry: NotificationEntry): Boolean {
// If we don't have an update time for this key, default to "too old"
@@ -648,72 +696,92 @@
* @see HeadsUpManager.setUserActionMayIndirectlyRemove
* @see HeadsUpManager.canRemoveImmediately
*/
- private val mActionPressListener = Consumer<NotificationEntry> { entry ->
- mHeadsUpManager.setUserActionMayIndirectlyRemove(entry)
- mExecutor.execute { endNotifLifetimeExtensionIfExtended(entry) }
- }
-
- private val mLifetimeExtender = object : NotifLifetimeExtender {
- override fun getName() = TAG
-
- override fun setCallback(callback: OnEndLifetimeExtensionCallback) {
- mEndLifetimeExtension = callback
+ private val mActionPressListener =
+ Consumer<NotificationEntry> { entry ->
+ mHeadsUpManager.setUserActionMayIndirectlyRemove(entry)
+ mExecutor.execute { endNotifLifetimeExtensionIfExtended(entry) }
}
- override fun maybeExtendLifetime(entry: NotificationEntry, reason: Int): Boolean {
- if (mHeadsUpManager.canRemoveImmediately(entry.key)) {
- return false
+ private val mLifetimeExtender =
+ object : NotifLifetimeExtender {
+ override fun getName() = TAG
+
+ override fun setCallback(callback: OnEndLifetimeExtensionCallback) {
+ mEndLifetimeExtension = callback
}
- if (isSticky(entry)) {
- val removeAfterMillis = mHeadsUpManager.getEarliestRemovalTime(entry.key)
- mNotifsExtendingLifetime[entry] = mExecutor.executeDelayed({
- mHeadsUpManager.removeNotification(entry.key, /* releaseImmediately */ true)
- }, removeAfterMillis)
- } else {
- mExecutor.execute {
- mHeadsUpManager.removeNotification(entry.key, /* releaseImmediately */ false)
+
+ override fun maybeExtendLifetime(entry: NotificationEntry, reason: Int): Boolean {
+ if (mHeadsUpManager.canRemoveImmediately(entry.key)) {
+ return false
}
- mNotifsExtendingLifetime[entry] = null
+ if (isSticky(entry)) {
+ val removeAfterMillis = mHeadsUpManager.getEarliestRemovalTime(entry.key)
+ mNotifsExtendingLifetime[entry] =
+ mExecutor.executeDelayed(
+ {
+ mHeadsUpManager.removeNotification(
+ entry.key, /* releaseImmediately */
+ true
+ )
+ },
+ removeAfterMillis
+ )
+ } else {
+ mExecutor.execute {
+ mHeadsUpManager.removeNotification(
+ entry.key, /* releaseImmediately */
+ false
+ )
+ }
+ mNotifsExtendingLifetime[entry] = null
+ }
+ return true
}
- return true
- }
- override fun cancelLifetimeExtension(entry: NotificationEntry) {
- mNotifsExtendingLifetime.remove(entry)?.run()
- }
- }
-
- private val mNotifPromoter = object : NotifPromoter(TAG) {
- override fun shouldPromoteToTopLevel(entry: NotificationEntry): Boolean =
- isGoingToShowHunNoRetract(entry)
- }
-
- val sectioner = object : NotifSectioner("HeadsUp", BUCKET_HEADS_UP) {
- override fun isInSection(entry: ListEntry): Boolean =
- // TODO: This check won't notice if a child of the group is going to HUN...
- isGoingToShowHunNoRetract(entry)
-
- override fun getComparator(): NotifComparator {
- return object : NotifComparator("HeadsUp") {
- override fun compare(o1: ListEntry, o2: ListEntry): Int =
- mHeadsUpManager.compare(o1.representativeEntry, o2.representativeEntry)
+ override fun cancelLifetimeExtension(entry: NotificationEntry) {
+ mNotifsExtendingLifetime.remove(entry)?.run()
}
}
- override fun getHeaderNodeController(): NodeController? =
- // TODO: remove SHOW_ALL_SECTIONS, this redundant method, and mIncomingHeaderController
- if (RankingCoordinator.SHOW_ALL_SECTIONS) mIncomingHeaderController else null
- }
+ private val mNotifPromoter =
+ object : NotifPromoter(TAG) {
+ override fun shouldPromoteToTopLevel(entry: NotificationEntry): Boolean =
+ isGoingToShowHunNoRetract(entry)
+ }
- private val mOnHeadsUpChangedListener = object : OnHeadsUpChangedListener {
- override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
- if (!isHeadsUp) {
- mNotifPromoter.invalidateList("headsUpEnded: ${entry.logKey}")
- mHeadsUpViewBinder.unbindHeadsUpView(entry)
- endNotifLifetimeExtensionIfExtended(entry)
+ val sectioner =
+ object : NotifSectioner("HeadsUp", BUCKET_HEADS_UP) {
+ override fun isInSection(entry: ListEntry): Boolean =
+ // TODO: This check won't notice if a child of the group is going to HUN...
+ isGoingToShowHunNoRetract(entry)
+
+ override fun getComparator(): NotifComparator {
+ return object : NotifComparator("HeadsUp") {
+ override fun compare(o1: ListEntry, o2: ListEntry): Int =
+ mHeadsUpManager.compare(o1.representativeEntry, o2.representativeEntry)
+ }
+ }
+
+ override fun getHeaderNodeController(): NodeController? =
+ // TODO: remove SHOW_ALL_SECTIONS, this redundant method, and
+ // mIncomingHeaderController
+ if (RankingCoordinator.SHOW_ALL_SECTIONS) mIncomingHeaderController else null
+ }
+
+ private val mOnHeadsUpChangedListener =
+ object : OnHeadsUpChangedListener {
+ override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
+ if (!isHeadsUp) {
+ mNotifPromoter.invalidateList("headsUpEnded: ${entry.logKey}")
+ mHeadsUpViewBinder.unbindHeadsUpView(entry)
+ endNotifLifetimeExtensionIfExtended(entry)
+ }
+ }
+
+ override fun onHeadsUpAnimatingAwayEnded(entry: NotificationEntry) {
+ mNotifPromoter.invalidateList("headsUpAnimatingAwayEnded: ${entry.logKey}")
}
}
- }
private fun isSticky(entry: NotificationEntry) = mHeadsUpManager.isSticky(entry.key)
@@ -726,8 +794,9 @@
* Whether the notification is already heads up or binding so that it can imminently heads up
*/
private fun isAttemptingToShowHun(entry: ListEntry) =
- mHeadsUpManager.isHeadsUpEntry(entry.key) || isEntryBinding(entry)
- || isHeadsUpAnimatingAway(entry)
+ mHeadsUpManager.isHeadsUpEntry(entry.key) ||
+ isEntryBinding(entry) ||
+ isHeadsUpAnimatingAway(entry)
private fun isHeadsUpAnimatingAway(entry: ListEntry): Boolean {
if (!GroupHunAnimationFix.isEnabled) return false
@@ -735,19 +804,19 @@
}
/**
- * Whether the notification is already heads up/binding per [isAttemptingToShowHun] OR if it
- * has been updated so that it should heads up this update. This method is permissive because
- * it returns `true` even if the update would (in isolation of its group) cause the heads up to
- * be retracted. This is important for not retracting transferred group heads ups.
+ * Whether the notification is already heads up/binding per [isAttemptingToShowHun] OR if it has
+ * been updated so that it should heads up this update. This method is permissive because it
+ * returns `true` even if the update would (in isolation of its group) cause the heads up to be
+ * retracted. This is important for not retracting transferred group heads ups.
*/
private fun isGoingToShowHunNoRetract(entry: ListEntry) =
mPostedEntries[entry.key]?.calculateShouldBeHeadsUpNoRetract ?: isAttemptingToShowHun(entry)
/**
* If the notification has been updated, then whether it should HUN in isolation, otherwise
- * defers to the already heads up/binding state of [isAttemptingToShowHun]. This method is
- * strict because any update which would revoke the heads up supersedes the current
- * heads up/binding state.
+ * defers to the already heads up/binding state of [isAttemptingToShowHun]. This method is
+ * strict because any update which would revoke the heads up supersedes the current heads
+ * up/binding state.
*/
private fun isGoingToShowHunStrict(entry: ListEntry) =
mPostedEntries[entry.key]?.calculateShouldBeHeadsUpStrict ?: isAttemptingToShowHun(entry)
@@ -779,14 +848,21 @@
val key = entry.key
val isHeadsUpAlready: Boolean
get() = isHeadsUpEntry || isBinding
+
val calculateShouldBeHeadsUpStrict: Boolean
get() = shouldHeadsUpEver && (wasAdded || shouldHeadsUpAgain || isHeadsUpAlready)
+
val calculateShouldBeHeadsUpNoRetract: Boolean
get() = isHeadsUpAlready || (shouldHeadsUpEver && (wasAdded || shouldHeadsUpAgain))
}
}
-private enum class GroupLocation { Detached, Isolated, Summary, Child }
+private enum class GroupLocation {
+ Detached,
+ Isolated,
+ Summary,
+ Child
+}
private fun Map<String, GroupLocation>.getLocation(key: String): GroupLocation =
getOrDefault(key, GroupLocation.Detached)
@@ -804,6 +880,7 @@
/** Mutates the HeadsUp state of notifications. */
private interface HunMutator {
fun updateNotification(key: String, shouldHeadsUpAgain: Boolean)
+
fun removeNotification(key: String, releaseImmediately: Boolean)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt
index a7970c7..af21e75 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt
@@ -19,14 +19,16 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.settings.SecureSettingsRepositoryModule
+import com.android.systemui.settings.SystemSettingsRepositoryModule
import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
+import com.android.systemui.shared.settings.data.repository.SystemSettingsRepository
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-@Module(includes = [SecureSettingsRepositoryModule::class])
+@Module(includes = [SecureSettingsRepositoryModule::class, SystemSettingsRepositoryModule::class])
object NotificationSettingsRepositoryModule {
@Provides
@SysUISingleton
@@ -34,10 +36,12 @@
@Background backgroundScope: CoroutineScope,
@Background backgroundDispatcher: CoroutineDispatcher,
secureSettingsRepository: SecureSettingsRepository,
+ systemSettingsRepository: SystemSettingsRepository,
): NotificationSettingsRepository =
NotificationSettingsRepository(
backgroundScope,
backgroundDispatcher,
- secureSettingsRepository
+ secureSettingsRepository,
+ systemSettingsRepository
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
index 9d09595..2a8db56 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
@@ -46,6 +46,7 @@
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.settings.UserTracker
+import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
import com.android.systemui.statusbar.StatusBarState.SHADE
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.MAX_HUN_WHEN_AGE_MS
@@ -57,7 +58,6 @@
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.util.NotificationChannels
import com.android.systemui.util.settings.GlobalSettings
-import com.android.systemui.util.settings.SystemSettings
import com.android.systemui.util.time.SystemClock
import com.android.wm.shell.bubbles.Bubbles
import java.util.Optional
@@ -98,8 +98,7 @@
globalSettings.registerContentObserverSync(
globalSettings.getUriFor(HEADS_UP_NOTIFICATIONS_ENABLED),
/* notifyForDescendants = */ true,
- observer
- )
+ observer)
// QQQ: Do we need to register for SETTING_HEADS_UP_TICKER? It seems unused.
@@ -171,10 +170,7 @@
class PeekOldWhenSuppressor(private val systemClock: SystemClock) :
VisualInterruptionFilter(
- types = setOf(PEEK),
- reason = "has old `when`",
- uiEventId = HUN_SUPPRESSED_OLD_WHEN
- ) {
+ types = setOf(PEEK), reason = "has old `when`", uiEventId = HUN_SUPPRESSED_OLD_WHEN) {
private fun whenAge(entry: NotificationEntry) =
systemClock.currentTimeMillis() - entry.sbn.notification.getWhen()
@@ -201,9 +197,7 @@
class PulseLockscreenVisibilityPrivateSuppressor() :
VisualInterruptionFilter(
- types = setOf(PULSE),
- reason = "hidden by lockscreen visibility override"
- ) {
+ types = setOf(PULSE), reason = "hidden by lockscreen visibility override") {
override fun shouldSuppress(entry: NotificationEntry) =
entry.ranking.lockscreenVisibilityOverride == VISIBILITY_PRIVATE
}
@@ -215,18 +209,13 @@
class HunGroupAlertBehaviorSuppressor() :
VisualInterruptionFilter(
- types = setOf(PEEK, PULSE),
- reason = "suppressive group alert behavior"
- ) {
+ types = setOf(PEEK, PULSE), reason = "suppressive group alert behavior") {
override fun shouldSuppress(entry: NotificationEntry) =
entry.sbn.let { it.isGroup && it.notification.suppressAlertingDueToGrouping() }
}
class HunSilentNotificationSuppressor() :
- VisualInterruptionFilter(
- types = setOf(PEEK, PULSE),
- reason = "notification isSilent"
- ) {
+ VisualInterruptionFilter(types = setOf(PEEK, PULSE), reason = "notification isSilent") {
override fun shouldSuppress(entry: NotificationEntry) =
entry.sbn.let { Flags.notificationSilentFlag() && it.notification.isSilent }
}
@@ -273,7 +262,7 @@
class AvalancheSuppressor(
private val avalancheProvider: AvalancheProvider,
private val systemClock: SystemClock,
- private val systemSettings: SystemSettings,
+ private val settingsInteractor: NotificationSettingsInteractor,
private val packageManager: PackageManager,
private val uiEventLogger: UiEventLogger,
private val context: Context,
@@ -298,7 +287,7 @@
// education HUNs.
private var hasShownOnceForDebug = false
- private fun shouldShowEdu() : Boolean {
+ private fun shouldShowEdu(): Boolean {
val forceShowOnce = SystemProperties.get(FORCE_SHOW_AVALANCHE_EDU_ONCE, "").equals("1")
return !hasSeenEdu || (forceShowOnce && !hasShownOnceForDebug)
}
@@ -361,28 +350,25 @@
return true
}
- /**
- * Show avalanche education HUN from SystemUI.
- */
+ /** Show avalanche education HUN from SystemUI. */
private fun showEdu() {
val res = context.resources
- val titleStr = res.getString(
- com.android.systemui.res.R.string.adaptive_notification_edu_hun_title)
- val textStr = res.getString(
- com.android.systemui.res.R.string.adaptive_notification_edu_hun_text)
- val actionStr = res.getString(
- com.android.systemui.res.R.string.go_to_adaptive_notification_settings)
+ val titleStr =
+ res.getString(com.android.systemui.res.R.string.adaptive_notification_edu_hun_title)
+ val textStr =
+ res.getString(com.android.systemui.res.R.string.adaptive_notification_edu_hun_text)
+ val actionStr =
+ res.getString(com.android.systemui.res.R.string.go_to_adaptive_notification_settings)
val intent = Intent(Settings.ACTION_MANAGE_ADAPTIVE_NOTIFICATIONS)
- val pendingIntent = PendingIntent.getActivity(
- context, 0, intent,
- PendingIntent.FLAG_IMMUTABLE
- )
+ val pendingIntent =
+ PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
// Replace "System UI" app name with "Android System"
val bundle = Bundle()
- bundle.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
- context.getString(com.android.internal.R.string.android_system_label))
+ bundle.putString(
+ Notification.EXTRA_SUBSTITUTE_APP_NAME,
+ context.getString(com.android.internal.R.string.android_system_label))
val builder =
Notification.Builder(context, NotificationChannels.ALERTS)
@@ -400,14 +386,12 @@
notificationManager.notify(SystemMessage.NOTE_ADAPTIVE_NOTIFICATIONS, builder.build())
hasSeenEdu = true
- hasShownOnceForDebug = true;
+ hasShownOnceForDebug = true
}
private fun calculateState(entry: NotificationEntry): State {
- if (
- entry.ranking.isConversation &&
- entry.sbn.notification.getWhen() > avalancheProvider.startTime
- ) {
+ if (entry.ranking.isConversation &&
+ entry.sbn.notification.getWhen() > avalancheProvider.startTime) {
uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_NEW_CONVERSATION)
return State.ALLOW_CONVERSATION_AFTER_AVALANCHE
}
@@ -440,10 +424,8 @@
uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_COLORIZED)
return State.ALLOW_COLORIZED
}
- if (
- packageManager.checkPermission(RECEIVE_EMERGENCY_BROADCAST, entry.sbn.packageName) ==
- PERMISSION_GRANTED
- ) {
+ if (packageManager.checkPermission(RECEIVE_EMERGENCY_BROADCAST, entry.sbn.packageName) ==
+ PERMISSION_GRANTED) {
uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_EMERGENCY)
return State.ALLOW_EMERGENCY
}
@@ -452,7 +434,6 @@
}
private fun isCooldownEnabled(): Boolean {
- return systemSettings.getInt(Settings.System.NOTIFICATION_COOLDOWN_ENABLED, /* def */ 1) ==
- 1
+ return settingsInteractor.isCooldownEnabled.value
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
index 1c476ce..c0d27cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
@@ -29,6 +29,7 @@
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.settings.UserTracker
+import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.Decision
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.FullScreenIntentDecision
@@ -72,7 +73,8 @@
private val packageManager: PackageManager,
private val bubbles: Optional<Bubbles>,
private val context: Context,
- private val notificationManager: NotificationManager
+ private val notificationManager: NotificationManager,
+ private val settingsInteractor: NotificationSettingsInteractor
) : VisualInterruptionDecisionProvider {
init {
@@ -183,8 +185,8 @@
if (NotificationAvalancheSuppression.isEnabled) {
addFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
- uiEventLogger, context, notificationManager)
+ AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor,
+ packageManager, uiEventLogger, context, notificationManager)
)
avalancheProvider.register()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/GroupHunAnimationFix.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/GroupHunAnimationFix.kt
index 5867612..3b30c86 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/GroupHunAnimationFix.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/GroupHunAnimationFix.kt
@@ -29,7 +29,7 @@
val token: FlagToken
get() = FlagToken(FLAG_NAME, isEnabled)
- /** Are sections sorted by time? */
+ /** Return whether the fix is enabled */
@JvmStatic
inline val isEnabled
get() = Flags.notificationGroupHunRemovalAnimationFix()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index a072ea6..fb1c525 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -124,6 +124,7 @@
import com.android.systemui.statusbar.notification.row.NotificationGuts;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.notification.row.NotificationSnooze;
+import com.android.systemui.statusbar.notification.shared.GroupHunAnimationFix;
import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
@@ -1987,6 +1988,10 @@
NotificationEntry entry = row.getEntry();
mHeadsUpAppearanceController.updateHeader(entry);
mHeadsUpAppearanceController.updateHeadsUpAndPulsingRoundness(entry);
+ if (GroupHunAnimationFix.isEnabled() && !animatingAway) {
+ // invalidate list to make sure the row is sorted to the correct section
+ mHeadsUpManager.onEntryAnimatingAwayEnded(entry);
+ }
});
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index b2035e1..25d9cc7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -117,7 +117,7 @@
@Override
public HeadsUpEntryPhone acquire() {
- NotificationsHeadsUpRefactor.assertInLegacyMode();
+ NotificationThrottleHun.assertInLegacyMode();
if (!mPoolObjects.isEmpty()) {
return mPoolObjects.pop();
}
@@ -126,7 +126,7 @@
@Override
public boolean release(@NonNull HeadsUpEntryPhone instance) {
- NotificationsHeadsUpRefactor.assertInLegacyMode();
+ NotificationThrottleHun.assertInLegacyMode();
mPoolObjects.push(instance);
return true;
}
@@ -428,7 +428,7 @@
@NonNull
@Override
protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) {
- if (NotificationsHeadsUpRefactor.isEnabled()) {
+ if (NotificationThrottleHun.isEnabled()) {
return new HeadsUpEntryPhone(entry);
} else {
HeadsUpEntryPhone headsUpEntry = mEntryPool.acquire();
@@ -454,7 +454,7 @@
@Override
protected void onEntryRemoved(HeadsUpEntry headsUpEntry) {
super.onEntryRemoved(headsUpEntry);
- if (!NotificationsHeadsUpRefactor.isEnabled()) {
+ if (!NotificationThrottleHun.isEnabled()) {
mEntryPool.release((HeadsUpEntryPhone) headsUpEntry);
}
updateTopHeadsUpFlow();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index e23f6ad..3786958 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -171,7 +171,6 @@
mLogger.logShowNotificationRequest(entry);
Runnable runnable = () -> {
- // TODO(b/315362456) log outside runnable too
mLogger.logShowNotification(entry);
// Add new entry and begin managing it
@@ -244,8 +243,10 @@
return;
}
// TODO(b/328390331) move accessibility events to the view layer
- headsUpEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
-
+ if (headsUpEntry.mEntry != null) {
+ headsUpEntry.mEntry.sendAccessibilityEvent(
+ AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+ }
if (shouldHeadsUpAgain) {
headsUpEntry.updateEntry(true /* updatePostTime */, "updateNotification");
if (headsUpEntry != null) {
@@ -334,6 +335,9 @@
}
protected boolean shouldHeadsUpBecomePinned(@NonNull NotificationEntry entry) {
+ if (entry == null) {
+ return false;
+ }
final HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey());
if (headsUpEntry == null) {
// This should not happen since shouldHeadsUpBecomePinned is always called after adding
@@ -435,7 +439,7 @@
onEntryRemoved(finalHeadsUpEntry);
// TODO(b/328390331) move accessibility events to the view layer
entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
- if (NotificationsHeadsUpRefactor.isEnabled()) {
+ if (NotificationThrottleHun.isEnabled()) {
finalHeadsUpEntry.cancelAutoRemovalCallbacks("removeEntry");
} else {
finalHeadsUpEntry.reset();
@@ -460,6 +464,15 @@
}
/**
+ * Called to notify the listeners that the HUN animating away animation has ended.
+ */
+ public void onEntryAnimatingAwayEnded(@NonNull NotificationEntry entry) {
+ for (OnHeadsUpChangedListener listener : mListeners) {
+ listener.onHeadsUpAnimatingAwayEnded(entry);
+ }
+ }
+
+ /**
* Manager-specific logic, that should occur, when the entry is updated, and its posted time has
* changed.
*
@@ -508,6 +521,9 @@
keySet.addAll(mAvalancheController.getWaitingKeys());
for (String key : keySet) {
HeadsUpEntry entry = getHeadsUpEntry(key);
+ if (entry.mEntry == null) {
+ continue;
+ }
String packageName = entry.mEntry.getSbn().getPackageName();
String snoozeKey = snoozeKey(packageName, mUser);
mLogger.logPackageSnoozed(snoozeKey);
@@ -575,7 +591,7 @@
pw.print(" now="); pw.println(mSystemClock.elapsedRealtime());
pw.print(" mUser="); pw.println(mUser);
for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) {
- pw.print(" HeadsUpEntry="); pw.println(entry.mEntry);
+ pw.println(entry.mEntry == null ? "null" : entry.mEntry);
}
int n = mSnoozedPackages.size();
pw.println(" snoozed packages: " + n);
@@ -595,7 +611,7 @@
private boolean hasPinnedNotificationInternal() {
for (String key : mHeadsUpEntryMap.keySet()) {
HeadsUpEntry entry = getHeadsUpEntry(key);
- if (entry.mEntry.isRowPinned()) {
+ if (entry.mEntry != null && entry.mEntry.isRowPinned()) {
return true;
}
}
@@ -620,7 +636,7 @@
// when the user unpinned all of HUNs by moving one HUN, all of HUNs should not stay
// on the screen.
if (userUnPinned && headsUpEntry.mEntry != null) {
- if (headsUpEntry.mEntry.mustStayOnScreen()) {
+ if (headsUpEntry.mEntry != null && headsUpEntry.mEntry.mustStayOnScreen()) {
headsUpEntry.mEntry.setHeadsUpIsVisible();
}
}
@@ -696,7 +712,7 @@
return true;
}
return headsUpEntry == null || headsUpEntry.wasShownLongEnough()
- || headsUpEntry.mEntry.isRowDismissed();
+ || (headsUpEntry.mEntry != null && headsUpEntry.mEntry.isRowDismissed());
}
/**
@@ -752,7 +768,7 @@
@Nullable private Runnable mCancelRemoveRunnable;
public HeadsUpEntry() {
- NotificationsHeadsUpRefactor.assertInLegacyMode();
+ NotificationThrottleHun.assertInLegacyMode();
}
public HeadsUpEntry(NotificationEntry entry) {
@@ -763,7 +779,7 @@
/** Attach a NotificationEntry. */
public void setEntry(@NonNull final NotificationEntry entry) {
- NotificationsHeadsUpRefactor.assertInLegacyMode();
+ NotificationThrottleHun.assertInLegacyMode();
setEntry(entry, createRemoveRunnable(entry));
}
@@ -875,6 +891,14 @@
}
public int compareNonTimeFields(HeadsUpEntry headsUpEntry) {
+ if (mEntry == null && headsUpEntry.mEntry == null) {
+ return 0;
+ } else if (headsUpEntry.mEntry == null) {
+ return -1;
+ } else if (mEntry == null) {
+ return 1;
+ }
+
boolean selfFullscreen = hasFullScreenIntent(mEntry);
boolean otherFullscreen = hasFullScreenIntent(headsUpEntry.mEntry);
if (selfFullscreen && !otherFullscreen) {
@@ -901,6 +925,13 @@
}
public int compareTo(@NonNull HeadsUpEntry headsUpEntry) {
+ if (mEntry == null && headsUpEntry.mEntry == null) {
+ return 0;
+ } else if (headsUpEntry.mEntry == null) {
+ return -1;
+ } else if (mEntry == null) {
+ return 1;
+ }
boolean isPinned = mEntry.isRowPinned();
boolean otherPinned = headsUpEntry.mEntry.isRowPinned();
if (isPinned && !otherPinned) {
@@ -945,7 +976,7 @@
}
public void reset() {
- NotificationsHeadsUpRefactor.assertInLegacyMode();
+ NotificationThrottleHun.assertInLegacyMode();
cancelAutoRemovalCallbacks("reset()");
mEntry = null;
mRemoveRunnable = null;
@@ -965,7 +996,7 @@
mLogger.logAutoRemoveCanceled(mEntry, reason);
}
};
- if (isHeadsUpEntry(this.mEntry.getKey())) {
+ if (mEntry != null && isHeadsUpEntry(mEntry.getKey())) {
mAvalancheController.update(this, runnable, reason + " cancelAutoRemovalCallbacks");
} else {
// Just removed
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
index 28a2a1f..fcf77d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
@@ -42,6 +42,7 @@
* should be ranked higher and 0 if they are equal.
*/
fun compare(a: NotificationEntry?, b: NotificationEntry?): Int
+
/**
* Extends the lifetime of the currently showing pulsing notification so that the pulse lasts
* longer.
@@ -184,6 +185,8 @@
fun unpinAll(userUnPinned: Boolean)
fun updateNotification(key: String, shouldHeadsUpAgain: Boolean)
+
+ fun onEntryAnimatingAwayEnded(entry: NotificationEntry)
}
/** Sets the animation state of the HeadsUpManager. */
@@ -204,41 +207,77 @@
/* No op impl of HeadsUpManager. */
class HeadsUpManagerEmptyImpl @Inject constructor() : HeadsUpManager {
override val allEntries = Stream.empty<NotificationEntry>()
+
override fun addHeadsUpPhoneListener(listener: OnHeadsUpPhoneListenerChange) {}
+
override fun addListener(listener: OnHeadsUpChangedListener) {}
+
override fun addSwipedOutNotification(key: String) {}
+
override fun canRemoveImmediately(key: String) = false
+
override fun compare(a: NotificationEntry?, b: NotificationEntry?) = 0
+
override fun dump(pw: PrintWriter, args: Array<out String>) {}
+
override fun extendHeadsUp() {}
+
override fun getEarliestRemovalTime(key: String?) = 0L
+
override fun getTouchableRegion(): Region? = null
+
override fun getTopEntry() = null
+
override fun hasPinnedHeadsUp() = false
+
override fun isHeadsUpEntry(key: String) = false
+
override fun isHeadsUpAnimatingAwayValue() = false
+
override fun isSnoozed(packageName: String) = false
+
override fun isSticky(key: String?) = false
+
override fun isTrackingHeadsUp() = false
+
override fun onExpandingFinished() {}
+
override fun releaseAllImmediately() {}
+
override fun removeListener(listener: OnHeadsUpChangedListener) {}
+
override fun removeNotification(key: String, releaseImmediately: Boolean) = false
+
override fun removeNotification(key: String, releaseImmediately: Boolean, animate: Boolean) =
false
+
override fun setAnimationStateHandler(handler: AnimationStateHandler) {}
+
override fun setExpanded(entry: NotificationEntry, expanded: Boolean) {}
+
override fun setGutsShown(entry: NotificationEntry, gutsShown: Boolean) {}
+
override fun setHeadsUpAnimatingAway(headsUpAnimatingAway: Boolean) {}
+
override fun setRemoteInputActive(entry: NotificationEntry, remoteInputActive: Boolean) {}
+
override fun setTrackingHeadsUp(tracking: Boolean) {}
+
override fun setUser(user: Int) {}
+
override fun setUserActionMayIndirectlyRemove(entry: NotificationEntry) {}
+
override fun shouldSwallowClick(key: String): Boolean = false
+
override fun showNotification(entry: NotificationEntry) {}
+
override fun snooze() {}
+
override fun unpinAll(userUnPinned: Boolean) {}
+
override fun updateNotification(key: String, alert: Boolean) {}
+
+ override fun onEntryAnimatingAwayEnded(entry: NotificationEntry) {}
}
@Module
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java
index 86998ab..de3bf04 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java
@@ -48,4 +48,9 @@
* @param isHeadsUp whether the notification is now a headsUp notification
*/
default void onHeadsUpStateChanged(@NonNull NotificationEntry entry, boolean isHeadsUp) {}
+
+ /**
+ * Called on HUN disappearing animation ends
+ */
+ default void onHeadsUpAnimatingAwayEnded(@NonNull NotificationEntry entry) {}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
index cf9a78f..f693409 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
@@ -20,6 +20,7 @@
import android.os.UserManager.DISALLOW_CONFIG_LOCATION
import android.os.UserManager.DISALLOW_MICROPHONE_TOGGLE
import android.os.UserManager.DISALLOW_SHARE_LOCATION
+import com.android.systemui.Flags
import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tileimpl.QSTileImpl
@@ -67,25 +68,18 @@
import com.android.systemui.qs.tiles.viewmodel.QSTilePolicy
import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
+import com.android.systemui.qs.tiles.viewmodel.StubQSTileViewModel
import com.android.systemui.res.R
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoMap
import dagger.multibindings.StringKey
+import javax.inject.Provider
@Module
interface PolicyModule {
- /** Inject DndTile into tileMap in QSModule */
- @Binds @IntoMap @StringKey(DndTile.TILE_SPEC) fun bindDndTile(dndTile: DndTile): QSTileImpl<*>
-
- /** Inject ModesTile into tileMap in QSModule */
- @Binds
- @IntoMap
- @StringKey(ModesTile.TILE_SPEC)
- fun bindModesTile(modesTile: ModesTile): QSTileImpl<*>
-
/** Inject WorkModeTile into tileMap in QSModule */
@Binds
@IntoMap
@@ -136,7 +130,19 @@
const val CAMERA_TOGGLE_TILE_SPEC = "cameratoggle"
const val MIC_TOGGLE_TILE_SPEC = "mictoggle"
const val DND_TILE_SPEC = "dnd"
- const val MODES_TILE_SPEC = "modes"
+
+ /** Inject DndTile or ModesTile into tileMap in QSModule based on feature flag */
+ @Provides
+ @IntoMap
+ @StringKey(DND_TILE_SPEC)
+ fun bindDndOrModesTile(
+ // Using providers to make sure that the unused tile isn't initialised at all if the
+ // flag is off.
+ dndTile: Provider<DndTile>,
+ modesTile: Provider<ModesTile>,
+ ): QSTileImpl<*> {
+ return if (android.app.Flags.modesUi()) modesTile.get() else dndTile.get()
+ }
/** Inject flashlight config */
@Provides
@@ -386,51 +392,51 @@
return factory.create(MICROPHONE)
}
- /** Inject microphone toggle config */
+ /** Inject DND tile or Modes tile config based on feature flag */
@Provides
@IntoMap
@StringKey(DND_TILE_SPEC)
- fun provideDndTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
- QSTileConfig(
- tileSpec = TileSpec.create(DND_TILE_SPEC),
- uiConfig =
- QSTileUIConfig.Resource(
- iconRes = R.drawable.qs_dnd_icon_off,
- labelRes = R.string.quick_settings_dnd_label,
- ),
- instanceId = uiEventLogger.getNewInstanceId(),
- )
-
- @Provides
- @IntoMap
- @StringKey(MODES_TILE_SPEC)
- fun provideModesTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
- QSTileConfig(
- tileSpec = TileSpec.create(MODES_TILE_SPEC),
- uiConfig =
- QSTileUIConfig.Resource(
- iconRes = R.drawable.qs_dnd_icon_off,
- labelRes = R.string.quick_settings_modes_label,
- ),
- instanceId = uiEventLogger.getNewInstanceId(),
- )
+ fun provideDndOrModesTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+ if (android.app.Flags.modesUi()) {
+ QSTileConfig(
+ tileSpec = TileSpec.create(DND_TILE_SPEC),
+ uiConfig =
+ QSTileUIConfig.Resource(
+ iconRes = R.drawable.qs_dnd_icon_off,
+ labelRes = R.string.quick_settings_modes_label,
+ ),
+ instanceId = uiEventLogger.getNewInstanceId(),
+ )
+ } else {
+ QSTileConfig(
+ tileSpec = TileSpec.create(DND_TILE_SPEC),
+ uiConfig =
+ QSTileUIConfig.Resource(
+ iconRes = R.drawable.qs_dnd_icon_off,
+ labelRes = R.string.quick_settings_dnd_label,
+ ),
+ instanceId = uiEventLogger.getNewInstanceId(),
+ )
+ }
/** Inject ModesTile into tileViewModelMap in QSModule */
@Provides
@IntoMap
- @StringKey(MODES_TILE_SPEC)
+ @StringKey(DND_TILE_SPEC)
fun provideModesTileViewModel(
factory: QSTileViewModelFactory.Static<ModesTileModel>,
mapper: ModesTileMapper,
stateInteractor: ModesTileDataInteractor,
userActionInteractor: ModesTileUserActionInteractor
): QSTileViewModel =
- factory.create(
- TileSpec.create(MODES_TILE_SPEC),
- userActionInteractor,
- stateInteractor,
- mapper,
- )
+ if (android.app.Flags.modesUi() && Flags.qsNewTilesFuture())
+ factory.create(
+ TileSpec.create(DND_TILE_SPEC),
+ userActionInteractor,
+ stateInteractor,
+ mapper,
+ )
+ else StubQSTileViewModel
}
/** Inject FlashlightTile into tileMap in QSModule */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt
index 5bd26cc..7c1cb6a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt
@@ -29,7 +29,6 @@
val text: String,
val subtext: String,
val enabled: Boolean,
- val contentDescription: String,
val onClick: () -> Unit,
val onLongClick: () -> Unit,
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
index c4aa03a..5ffcb34 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
@@ -84,9 +84,6 @@
text = mode.rule.name,
subtext = getTileSubtext(mode),
enabled = mode.isActive,
- // TODO(b/346519570): This should be some combination of the above, e.g.
- // "ON: Do Not Disturb, Until Mon 08:09"; see DndTile.
- contentDescription = "",
onClick = {
if (!mode.rule.isEnabled) {
openSettings(mode)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
index 74d9692..a5de7cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
@@ -47,8 +47,8 @@
import com.android.systemui.util.settings.SecureSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.After
@@ -82,9 +82,12 @@
@Mock private lateinit var dialogDelegate: ModesDialogDelegate
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
private val inputHandler = FakeQSTileIntentUserInputHandler()
private val zenModeRepository = FakeZenModeRepository()
- private val tileDataInteractor = ModesTileDataInteractor(zenModeRepository)
+ private val tileDataInteractor = ModesTileDataInteractor(zenModeRepository, testDispatcher)
private val mapper =
ModesTileMapper(
context.orCreateTestableResources
@@ -96,9 +99,6 @@
context.theme,
)
- private val testDispatcher = StandardTestDispatcher()
- private val testScope = TestScope(testDispatcher)
-
private lateinit var userActionInteractor: ModesTileUserActionInteractor
private lateinit var secureSettings: SecureSettings
private lateinit var testableLooper: TestableLooper
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt
index 69cc9d5..30326a57 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt
@@ -22,6 +22,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.shared.settings.data.repository.FakeSecureSettingsRepository
+import com.android.systemui.shared.settings.data.repository.FakeSystemSettingsRepository
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
@@ -38,18 +39,21 @@
private lateinit var testScope: TestScope
private lateinit var secureSettingsRepository: FakeSecureSettingsRepository
+ private lateinit var systemSettingsRepository: FakeSystemSettingsRepository
@Before
fun setUp() {
val testDispatcher = StandardTestDispatcher()
testScope = TestScope(testDispatcher)
secureSettingsRepository = FakeSecureSettingsRepository()
+ systemSettingsRepository = FakeSystemSettingsRepository()
underTest =
NotificationSettingsRepository(
scope = testScope.backgroundScope,
backgroundDispatcher = testDispatcher,
secureSettingsRepository = secureSettingsRepository,
+ systemSettingsRepository = systemSettingsRepository,
)
}
@@ -100,4 +104,22 @@
)
assertThat(historyEnabled).isEqualTo(false)
}
+
+ @Test
+ fun testGetIsCooldownEnabled() =
+ testScope.runTest {
+ val cooldownEnabled by collectLastValue(underTest.isCooldownEnabled)
+
+ systemSettingsRepository.setInt(
+ name = Settings.System.NOTIFICATION_COOLDOWN_ENABLED,
+ value = 1,
+ )
+ assertThat(cooldownEnabled).isEqualTo(true)
+
+ systemSettingsRepository.setInt(
+ name = Settings.System.NOTIFICATION_COOLDOWN_ENABLED,
+ value = 0,
+ )
+ assertThat(cooldownEnabled).isEqualTo(false)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
index a7f36c3..f9509d2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
@@ -66,7 +66,8 @@
packageManager,
Optional.of(bubbles),
context,
- notificationManager
+ notificationManager,
+ settingsInteractor
)
}
@@ -101,7 +102,7 @@
whenever(avalancheProvider.startTime).thenReturn(whenAgo(10))
val avalancheSuppressor = AvalancheSuppressor(
- avalancheProvider, systemClock, systemSettings, packageManager,
+ avalancheProvider, systemClock, settingsInteractor, packageManager,
uiEventLogger, context, notificationManager
)
avalancheSuppressor.hasSeenEdu = false
@@ -125,7 +126,7 @@
whenever(avalancheProvider.startTime).thenReturn(whenAgo(10))
val avalancheSuppressor = AvalancheSuppressor(
- avalancheProvider, systemClock, systemSettings, packageManager,
+ avalancheProvider, systemClock, settingsInteractor, packageManager,
uiEventLogger, context, notificationManager
)
avalancheSuppressor.hasSeenEdu = true
@@ -147,7 +148,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+ AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
uiEventLogger, context, notificationManager)
) {
ensurePeekState()
@@ -167,7 +168,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+ AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
uiEventLogger, context, notificationManager)
) {
ensurePeekState()
@@ -187,7 +188,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+ AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
uiEventLogger, context, notificationManager)
) {
ensurePeekState()
@@ -205,7 +206,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+ AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
uiEventLogger, context, notificationManager)
) {
ensurePeekState()
@@ -223,7 +224,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+ AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
uiEventLogger, context, notificationManager)
) {
ensurePeekState()
@@ -241,7 +242,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+ AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
uiEventLogger, context, notificationManager)
) {
ensurePeekState()
@@ -259,7 +260,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+ AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
uiEventLogger, context, notificationManager)
) {
assertFsiNotSuppressed()
@@ -271,7 +272,7 @@
avalancheProvider.startTime = whenAgo(10)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+ AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
uiEventLogger, context, notificationManager)
) {
ensurePeekState()
@@ -300,7 +301,7 @@
setAllowedEmergencyPkg(true)
withFilter(
- AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+ AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
uiEventLogger, context, notificationManager)
) {
ensurePeekState()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
index d5ab62b..9d3d9c1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
@@ -61,6 +61,7 @@
import com.android.systemui.log.core.LogLevel
import com.android.systemui.res.R
import com.android.systemui.settings.FakeUserTracker
+import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
import com.android.systemui.statusbar.FakeStatusBarStateController
import com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking
import com.android.systemui.statusbar.StatusBarState.KEYGUARD
@@ -88,6 +89,7 @@
import com.android.wm.shell.bubbles.Bubbles
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
+import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
@@ -134,6 +136,7 @@
protected val avalancheProvider: AvalancheProvider = mock()
protected val bubbles: Bubbles = mock()
lateinit var systemSettings: SystemSettings
+ protected val settingsInteractor: NotificationSettingsInteractor = mock()
protected val packageManager: PackageManager = mock()
protected val notificationManager: NotificationManager = mock()
protected abstract val provider: VisualInterruptionDecisionProvider
@@ -164,7 +167,7 @@
userTracker.set(listOf(user), /* currentUserIndex = */ 0)
systemSettings = FakeSettings()
whenever(bubbles.canShowBubbleNotification()).thenReturn(true)
-
+ whenever(settingsInteractor.isCooldownEnabled).thenReturn(MutableStateFlow(true))
provider.start()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt
index f4cebd7..7fd9c9f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt
@@ -25,6 +25,7 @@
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.settings.UserTracker
+import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
import com.android.systemui.statusbar.notification.NotifPipelineFlags
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.DeviceProvisionedController
@@ -61,7 +62,8 @@
packageManager: PackageManager,
bubbles: Optional<Bubbles>,
context: Context,
- notificationManager: NotificationManager
+ notificationManager: NotificationManager,
+ settingsInteractor: NotificationSettingsInteractor
): VisualInterruptionDecisionProvider {
return if (VisualInterruptionRefactor.isEnabled) {
VisualInterruptionDecisionProviderImpl(
@@ -85,7 +87,8 @@
packageManager,
bubbles,
context,
- notificationManager
+ notificationManager,
+ settingsInteractor
)
} else {
NotificationInterruptStateProviderWrapper(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index c36a046..3df4a67 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -20,6 +20,7 @@
import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.StatusBarState.SHADE;
+import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL;
import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
@@ -96,9 +97,12 @@
import com.android.systemui.statusbar.notification.init.NotificationsController;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
+import com.android.systemui.statusbar.notification.shared.GroupHunAnimationFix;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController.NotificationPanelEvent;
import com.android.systemui.statusbar.notification.stack.NotificationSwipeHelper.NotificationCallback;
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
+import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -192,6 +196,8 @@
private NotificationStackScrollLayoutController mController;
+ private NotificationTestHelper mNotificationTestHelper;
+
@Before
public void setUp() {
allowTestableLooperAsMainThread();
@@ -199,6 +205,11 @@
when(mNotificationSwipeHelperBuilder.build()).thenReturn(mNotificationSwipeHelper);
when(mKeyguardTransitionRepo.getTransitions()).thenReturn(emptyFlow());
+ mNotificationTestHelper = new NotificationTestHelper(
+ mContext,
+ mDependency,
+ TestableLooper.get(this));
+ mNotificationTestHelper.setDefaultInflationFlags(FLAG_CONTENT_VIEW_ALL);
}
@Test
@@ -222,6 +233,42 @@
}
@Test
+ @EnableFlags(GroupHunAnimationFix.FLAG_NAME)
+ public void changeHeadsUpAnimatingAwayToTrue_onEntryAnimatingAwayEndedNotCalled()
+ throws Exception {
+ // Before: bind an ExpandableNotificationRow,
+ initController(/* viewIsAttached= */ true);
+ mController.setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class));
+ NotificationListContainer listContainer = mController.getNotificationListContainer();
+ ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ listContainer.bindRow(row);
+
+ // When: call setHeadsUpAnimatingAway to change set mHeadsupDisappearRunning to true
+ row.setHeadsUpAnimatingAway(true);
+
+ // Then: mHeadsUpManager.onEntryAnimatingAwayEnded is not called
+ verify(mHeadsUpManager, never()).onEntryAnimatingAwayEnded(row.getEntry());
+ }
+
+ @Test
+ @EnableFlags(GroupHunAnimationFix.FLAG_NAME)
+ public void changeHeadsUpAnimatingAwayToFalse_onEntryAnimatingAwayEndedCalled()
+ throws Exception {
+ // Before: bind an ExpandableNotificationRow, set its mHeadsupDisappearRunning to true
+ initController(/* viewIsAttached= */ true);
+ mController.setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class));
+ NotificationListContainer listContainer = mController.getNotificationListContainer();
+ ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ listContainer.bindRow(row);
+ row.setHeadsUpAnimatingAway(true);
+
+ // When: call setHeadsUpAnimatingAway to change set mHeadsupDisappearRunning to false
+ row.setHeadsUpAnimatingAway(false);
+
+ // Then: mHeadsUpManager.onEntryAnimatingAwayEnded is called
+ verify(mHeadsUpManager).onEntryAnimatingAwayEnded(row.getEntry());
+ }
+ @Test
public void testOnDensityOrFontScaleChanged_reInflatesFooterViews() {
initController(/* viewIsAttached= */ true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index d2540a6..bd9cccd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -150,6 +150,7 @@
import com.android.systemui.shade.ShadeControllerImpl;
import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.shade.ShadeLogger;
+import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.KeyboardShortcutListSearch;
import com.android.systemui.statusbar.KeyboardShortcuts;
@@ -343,7 +344,7 @@
@Mock private NotificationManager mNotificationManager;
@Mock private GlanceableHubContainerController mGlanceableHubContainerController;
@Mock private EmergencyGestureIntentFactory mEmergencyGestureIntentFactory;
-
+ @Mock private NotificationSettingsInteractor mNotificationSettingsInteractor;
private ShadeController mShadeController;
private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
private final FakeGlobalSettings mFakeGlobalSettings = new FakeGlobalSettings();
@@ -403,7 +404,8 @@
mPackageManager,
Optional.of(mBubbles),
mContext,
- mNotificationManager);
+ mNotificationManager,
+ mNotificationSettingsInteractor);
mVisualInterruptionDecisionProvider.start();
mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
index cf87afb..7f33c23 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
@@ -299,27 +299,7 @@
}
@Test
- @DisableFlags(Flags.FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
- public void notifPaddingMakesUpToFullMarginInSplitShade_refactorFlagOff_usesResource() {
- int keyguardSplitShadeTopMargin = 100;
- int largeScreenHeaderHeightResource = 70;
- when(mResources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin))
- .thenReturn(keyguardSplitShadeTopMargin);
- when(mResources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height))
- .thenReturn(largeScreenHeaderHeightResource);
- mClockPositionAlgorithm.loadDimens(mContext, mResources);
- givenLockScreen();
- mIsSplitShade = true;
- // WHEN the position algorithm is run
- positionClock();
- // THEN the notif padding makes up lacking margin (margin - header height).
- int expectedPadding = keyguardSplitShadeTopMargin - largeScreenHeaderHeightResource;
- assertThat(mClockPosition.stackScrollerPadding).isEqualTo(expectedPadding);
- }
-
- @Test
- @EnableFlags(Flags.FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
- public void notifPaddingMakesUpToFullMarginInSplitShade_refactorFlagOn_usesHelper() {
+ public void notifPaddingMakesUpToFullMarginInSplitShade_usesHelper() {
int keyguardSplitShadeTopMargin = 100;
int largeScreenHeaderHeightHelper = 50;
int largeScreenHeaderHeightResource = 70;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index d6b4d2b..2457eb7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -123,6 +123,7 @@
import com.android.systemui.shade.ShadeController;
import com.android.systemui.shade.ShadeWindowLogger;
import com.android.systemui.shade.domain.interactor.ShadeInteractor;
+import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.statusbar.NotificationEntryHelper;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
@@ -486,7 +487,8 @@
mock(PackageManager.class),
Optional.of(mock(Bubbles.class)),
mContext,
- mock(NotificationManager.class)
+ mock(NotificationManager.class),
+ mock(NotificationSettingsInteractor.class)
);
interruptionDecisionProvider.start();
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt
index eff99e04..ca748b66 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt
@@ -18,7 +18,14 @@
import com.android.systemui.haptics.vibratorHelper
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.core.FakeLogBuffer
import com.android.systemui.statusbar.policy.keyguardStateController
val Kosmos.qsLongPressEffect by
- Kosmos.Fixture { QSLongPressEffect(vibratorHelper, keyguardStateController) }
+ Kosmos.Fixture {
+ QSLongPressEffect(
+ vibratorHelper,
+ keyguardStateController,
+ FakeLogBuffer.Factory.create(),
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt
new file mode 100644
index 0000000..9bd346e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.keyguard.gesture.data
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.navigationbar.gestural.data.respository.GestureRepository
+import com.android.systemui.navigationbar.gestural.data.respository.GestureRepositoryImpl
+
+val Kosmos.gestureRepository: GestureRepository by
+ Kosmos.Fixture { GestureRepositoryImpl(testDispatcher) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt
new file mode 100644
index 0000000..658aaa6
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.keyguard.gesture.domain
+
+import com.android.systemui.keyguard.gesture.data.gestureRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.navigationbar.gestural.domain.GestureInteractor
+
+val Kosmos.gestureInteractor: GestureInteractor by
+ Kosmos.Fixture {
+ GestureInteractor(gestureRepository = gestureRepository, scope = applicationCoroutineScope)
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt
index 6eb8a49..2919d3f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt
@@ -25,6 +25,7 @@
import com.android.systemui.common.ui.domain.interactor.configurationInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel
+import com.android.systemui.keyguard.dismissCallbackRegistry
import com.android.systemui.keyguard.ui.SwipeUpAnywhereGestureHandler
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerMessageAreaViewModel
@@ -49,6 +50,7 @@
alternateBouncerDependencies = { alternateBouncerDependencies },
windowManager = { windowManager },
layoutInflater = { mockedLayoutInflater },
+ dismissCallbackRegistry = dismissCallbackRegistry,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToLockscreenTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..6c644ee
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToLockscreenTransitionViewModelKosmos.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.alternateBouncerToLockscreenTransitionViewModel by Fixture {
+ AlternateBouncerToLockscreenTransitionViewModel(
+ animationFlow = keyguardTransitionAnimationFlow,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index 3c5baa5..82860fc 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -40,6 +40,8 @@
keyguardTransitionInteractor = keyguardTransitionInteractor,
notificationsKeyguardInteractor = notificationsKeyguardInteractor,
alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel,
+ alternateBouncerToLockscreenTransitionViewModel =
+ alternateBouncerToLockscreenTransitionViewModel,
aodToGoneTransitionViewModel = aodToGoneTransitionViewModel,
aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt
index a75d2bc..ecfc168 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt
@@ -20,6 +20,7 @@
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.shared.settings.data.repository.secureSettingsRepository
+import com.android.systemui.shared.settings.data.repository.systemSettingsRepository
val Kosmos.notificationSettingsRepository by
Kosmos.Fixture {
@@ -27,5 +28,6 @@
scope = testScope.backgroundScope,
backgroundDispatcher = testDispatcher,
secureSettingsRepository = secureSettingsRepository,
+ systemSettingsRepository = systemSettingsRepository,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepositoryKosmos.kt
new file mode 100644
index 0000000..01f19ae
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepositoryKosmos.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.shared.settings.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.systemSettingsRepository: SystemSettingsRepository by
+ Kosmos.Fixture { fakeSystemSettingsRepository }
+val Kosmos.fakeSystemSettingsRepository by Kosmos.Fixture { FakeSystemSettingsRepository() }
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 8d89cc1..7c8fd42 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -68,7 +68,10 @@
srcs: [
"runtime-common-ravenwood-src/**/*.java",
],
- visibility: ["//frameworks/base"],
+ visibility: [
+ // Some tests need to access the utilities.
+ ":__subpackages__",
+ ],
}
java_library {
@@ -318,6 +321,9 @@
android_ravenwood_libgroup {
name: "ravenwood-runtime",
+ data: [
+ "framework-res",
+ ],
libs: [
"100-framework-minus-apex.ravenwood",
"200-kxml2-android",
@@ -330,6 +336,11 @@
"services.core.ravenwood-jarjar",
"services.fakes.ravenwood-jarjar",
+ // ICU
+ "core-icu4j-for-host.ravenwood",
+ "icu4j-icudata-jarjar",
+ "icu4j-icutzdata-jarjar",
+
// Provide runtime versions of utils linked in below
"junit",
"truth",
diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING
index f6885e1..fbf27fa 100644
--- a/ravenwood/TEST_MAPPING
+++ b/ravenwood/TEST_MAPPING
@@ -12,6 +12,9 @@
{
"name": "RavenwoodBivalentTest_device"
},
+ {
+ "name": "RavenwoodResApkTest"
+ },
// The sysui tests should match vendor/unbundled_google/packages/SystemUIGoogle/TEST_MAPPING
{
"name": "SystemUIGoogleTests",
diff --git a/ravenwood/resapk_test/Android.bp b/ravenwood/resapk_test/Android.bp
new file mode 100644
index 0000000..c145765
--- /dev/null
+++ b/ravenwood/resapk_test/Android.bp
@@ -0,0 +1,30 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_ravenwood_test {
+ name: "RavenwoodResApkTest",
+
+ resource_apk: "RavenwoodResApkTest-apk",
+
+ libs: [
+ // Normally, tests shouldn't directly access it, but we need to access RavenwoodCommonUtils
+ // in this test.
+ "ravenwood-runtime-common-ravenwood",
+ ],
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ ],
+ srcs: [
+ "test/**/*.java",
+ ],
+ sdk_version: "test_current",
+ auto_gen_config: true,
+}
diff --git a/ravenwood/resapk_test/apk/Android.bp b/ravenwood/resapk_test/apk/Android.bp
new file mode 100644
index 0000000..10ed5e2
--- /dev/null
+++ b/ravenwood/resapk_test/apk/Android.bp
@@ -0,0 +1,14 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_app {
+ name: "RavenwoodResApkTest-apk",
+
+ sdk_version: "current",
+}
diff --git a/ravenwood/resapk_test/apk/AndroidManifest.xml b/ravenwood/resapk_test/apk/AndroidManifest.xml
new file mode 100644
index 0000000..f34d8b2
--- /dev/null
+++ b/ravenwood/resapk_test/apk/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.ravenwood.restest_apk">
+</manifest>
diff --git a/ravenwood/resapk_test/apk/res/values/strings.xml b/ravenwood/resapk_test/apk/res/values/strings.xml
new file mode 100644
index 0000000..23d4c0f
--- /dev/null
+++ b/ravenwood/resapk_test/apk/res/values/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Test string 1 -->
+ <string name="test_string_1" translatable="false" >Test String 1</string>
+</resources>
diff --git a/ravenwood/resapk_test/test/com/android/ravenwood/resapk_test/RavenwoodResApkTest.java b/ravenwood/resapk_test/test/com/android/ravenwood/resapk_test/RavenwoodResApkTest.java
new file mode 100644
index 0000000..1029ed2
--- /dev/null
+++ b/ravenwood/resapk_test/test/com/android/ravenwood/resapk_test/RavenwoodResApkTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.ravenwood.resapk_test;
+
+
+import static junit.framework.TestCase.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.ravenwood.common.RavenwoodCommonUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodResApkTest {
+ /**
+ * Ensure the file "ravenwood-res.apk" exists.
+ * TODO Check the content of it, once Ravenwood supports resources. The file should
+ * be a copy of RavenwoodResApkTest-apk.apk
+ */
+ @Test
+ public void testResApkExists() {
+ var file = "ravenwood-res-apks/ravenwood-res.apk";
+
+ assertTrue(new File(file).exists());
+ }
+
+ @Test
+ public void testFrameworkResExists() {
+ var file = "ravenwood-data/framework-res.apk";
+
+ assertTrue(new File(
+ RavenwoodCommonUtils.getRavenwoodRuntimePath() + "/" + file).exists());
+ }
+}
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
index eae516e..9f7fb57 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
@@ -1985,12 +1985,13 @@
}
@Override
- public void setAutofillFailure(int sessionId, @NonNull List<AutofillId> ids, int userId) {
+ public void setAutofillFailure(
+ int sessionId, @NonNull List<AutofillId> ids, boolean isRefill, int userId) {
synchronized (mLock) {
final AutofillManagerServiceImpl service =
peekServiceForUserWithLocalBinderIdentityLocked(userId);
if (service != null) {
- service.setAutofillFailureLocked(sessionId, getCallingUid(), ids);
+ service.setAutofillFailureLocked(sessionId, getCallingUid(), ids, isRefill);
} else if (sVerbose) {
Slog.v(TAG, "setAutofillFailure(): no service for " + userId);
}
@@ -2011,6 +2012,46 @@
}
@Override
+ public void notifyNotExpiringResponseDuringAuth(int sessionId, int userId) {
+ synchronized (mLock) {
+ final AutofillManagerServiceImpl service =
+ peekServiceForUserWithLocalBinderIdentityLocked(userId);
+ if (service != null) {
+ service.notifyNotExpiringResponseDuringAuth(sessionId, getCallingUid());
+ } else if (sVerbose) {
+ Slog.v(TAG, "notifyNotExpiringResponseDuringAuth(): no service for " + userId);
+ }
+ }
+ }
+
+ @Override
+ public void notifyViewEnteredIgnoredDuringAuthCount(int sessionId, int userId) {
+ synchronized (mLock) {
+ final AutofillManagerServiceImpl service =
+ peekServiceForUserWithLocalBinderIdentityLocked(userId);
+ if (service != null) {
+ service.notifyViewEnteredIgnoredDuringAuthCount(sessionId, getCallingUid());
+ } else if (sVerbose) {
+ Slog.v(TAG, "notifyNotExpiringResponseDuringAuth(): no service for " + userId);
+ }
+ }
+ }
+
+ @Override
+ public void setAutofillIdsAttemptedForRefill(
+ int sessionId, @NonNull List<AutofillId> ids, int userId) {
+ synchronized (mLock) {
+ final AutofillManagerServiceImpl service =
+ peekServiceForUserWithLocalBinderIdentityLocked(userId);
+ if (service != null) {
+ service.setAutofillIdsAttemptedForRefill(sessionId, ids, getCallingUid());
+ } else if (sVerbose) {
+ Slog.v(TAG, "setAutofillIdsAttemptedForRefill(): no service for " + userId);
+ }
+ }
+ }
+
+ @Override
public void finishSession(int sessionId, int userId,
@AutofillCommitReason int commitReason) {
synchronized (mLock) {
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index 2bf319e..c9f8929 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -464,7 +464,8 @@
}
@GuardedBy("mLock")
- void setAutofillFailureLocked(int sessionId, int uid, @NonNull List<AutofillId> ids) {
+ void setAutofillFailureLocked(
+ int sessionId, int uid, @NonNull List<AutofillId> ids, boolean isRefill) {
if (!isEnabledLocked()) {
Slog.wtf(TAG, "Service not enabled");
return;
@@ -474,7 +475,7 @@
Slog.v(TAG, "setAutofillFailure(): no session for " + sessionId + "(" + uid + ")");
return;
}
- session.setAutofillFailureLocked(ids);
+ session.setAutofillFailureLocked(ids, isRefill);
}
@GuardedBy("mLock")
@@ -492,6 +493,52 @@
}
@GuardedBy("mLock")
+ void notifyNotExpiringResponseDuringAuth(int sessionId, int uid) {
+ if (!isEnabledLocked()) {
+ Slog.wtf(TAG, "Service not enabled");
+ return;
+ }
+ final Session session = mSessions.get(sessionId);
+ if (session == null || uid != session.uid) {
+ Slog.v(TAG, "notifyNotExpiringResponseDuringAuth(): no session for "
+ + sessionId + "(" + uid + ")");
+ return;
+ }
+ session.setNotifyNotExpiringResponseDuringAuth();
+ }
+
+ @GuardedBy("mLock")
+ void notifyViewEnteredIgnoredDuringAuthCount(int sessionId, int uid) {
+ if (!isEnabledLocked()) {
+ Slog.wtf(TAG, "Service not enabled");
+ return;
+ }
+ final Session session = mSessions.get(sessionId);
+ if (session == null || uid != session.uid) {
+ Slog.v(TAG, "notifyViewEnteredIgnoredDuringAuthCount(): no session for "
+ + sessionId + "(" + uid + ")");
+ return;
+ }
+ session.setLogViewEnteredIgnoredDuringAuth();
+ }
+
+ @GuardedBy("mLock")
+ public void setAutofillIdsAttemptedForRefill(
+ int sessionId, @NonNull List<AutofillId> ids, int uid) {
+ if (!isEnabledLocked()) {
+ Slog.wtf(TAG, "Service not enabled");
+ return;
+ }
+ final Session session = mSessions.get(sessionId);
+ if (session == null || uid != session.uid) {
+ Slog.v(TAG, "setAutofillIdsAttemptedForRefill(): no session for "
+ + sessionId + "(" + uid + ")");
+ return;
+ }
+ session.setAutofillIdsAttemptedForRefillLocked(ids);
+ }
+
+ @GuardedBy("mLock")
void finishSessionLocked(int sessionId, int uid, @AutofillCommitReason int commitReason) {
if (!isEnabledLocked()) {
Slog.wtf(TAG, "Service not enabled");
diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
index 49ca297..930af5e 100644
--- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
@@ -58,6 +58,7 @@
import static com.android.server.autofill.Helper.sVerbose;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
@@ -685,6 +686,19 @@
}
/**
+ * Set views_fillable_total_count as long as mEventInternal presents.
+ */
+ public void maybeUpdateViewFillablesForRefillAttempt(List<AutofillId> autofillIds) {
+ mEventInternal.ifPresent(event -> {
+ // These autofill ids would be the ones being re-attempted.
+ event.mAutofillIdsAttemptedAutofill = new ArraySet<>(autofillIds);
+ // These autofill id's are being refilled, so they had failed previously.
+ // Note that these autofillIds correspond to the new autofill ids after relayout.
+ event.mFailedAutofillIds = new ArraySet<>(autofillIds);
+ });
+ }
+
+ /**
* Set how many views are filtered from fill because they are not in current session
*/
public void maybeSetFilteredFillableViewsCount(int filteredViewsCount) {
@@ -697,9 +711,16 @@
* Set views_filled_failure_count using failure count as long as mEventInternal
* presents.
*/
- public void maybeSetViewFillFailureCounts(int failureCount) {
+ public void maybeSetViewFillFailureCounts(@NonNull List<AutofillId> ids, boolean isRefill) {
mEventInternal.ifPresent(event -> {
- event.mViewFillFailureCount = failureCount;
+ int failureCount = ids.size();
+ if (isRefill) {
+ event.mViewFailedOnRefillCount = failureCount;
+ } else {
+ event.mViewFillFailureCount = failureCount;
+ event.mViewFailedPriorToRefillCount = failureCount;
+ event.mFailedAutofillIds = new ArraySet<>(ids);
+ }
});
}
@@ -719,7 +740,7 @@
* Set views_filled_failure_count using failure count as long as mEventInternal
* presents.
*/
- public void maybeAddSuccessId(AutofillId autofillId) {
+ public synchronized void maybeAddSuccessId(AutofillId autofillId) {
mEventInternal.ifPresent(event -> {
ArraySet<AutofillId> autofillIds = event.mAutofillIdsAttemptedAutofill;
if (autofillIds == null) {
@@ -727,9 +748,21 @@
+ " successfully filled");
event.mViewFilledButUnexpectedCount++;
} else if (autofillIds.contains(autofillId)) {
- if (sVerbose) {
- Slog.v(TAG, "Logging autofill for id:" + autofillId);
+ ArraySet<AutofillId> failedIds = event.mFailedAutofillIds;
+ if (failedIds.contains(autofillId)) {
+ if (sVerbose) {
+ Slog.v(TAG, "Logging autofill refill of id:" + autofillId);
+ }
+ // This indicates the success after refill attempt
+ event.mViewFilledSuccessfullyOnRefillCount++;
+ // Remove so if we don't reprocess duplicate requests
+ failedIds.remove(autofillId);
+ } else {
+ if (sVerbose) {
+ Slog.v(TAG, "Logging autofill for id:" + autofillId);
+ }
}
+ // Common actions to take irrespective of being filled by refill attempt or not.
event.mViewFillSuccessCount++;
autofillIds.remove(autofillId);
event.mAlreadyFilledAutofillIds.add(autofillId);
@@ -746,6 +779,23 @@
});
}
+ /**
+ * Set how many views are filtered from fill because they are not in current session
+ */
+ public void maybeSetNotifyNotExpiringResponseDuringAuth() {
+ mEventInternal.ifPresent(event -> {
+ event.mFixExpireResponseDuringAuthCount++;
+ });
+ }
+ /**
+ * Set how many views are filtered from fill because they are not in current session
+ */
+ public void notifyViewEnteredIgnoredDuringAuthCount() {
+ mEventInternal.ifPresent(event -> {
+ event.mNotifyViewEnteredIgnoredDuringAuthCount++;
+ });
+ }
+
public void logAndEndEvent() {
if (!mEventInternal.isPresent()) {
Slog.w(TAG, "Shouldn't be logging AutofillPresentationEventReported again for same "
@@ -933,6 +983,7 @@
int mNotifyViewEnteredIgnoredDuringAuthCount = 0;
ArraySet<AutofillId> mAutofillIdsAttemptedAutofill;
+ ArraySet<AutofillId> mFailedAutofillIds = new ArraySet<>();
ArraySet<AutofillId> mAlreadyFilledAutofillIds = new ArraySet<>();
// Not logged - used for internal logic
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 21df7a5..b7508b4 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -5360,6 +5360,8 @@
saveTriggerId = null;
}
+ boolean hasAuthentication = (response.getAuthentication() != null);
+
// Must also track that are part of datasets, otherwise the FillUI won't be hidden when
// they go away (if they're not savable).
@@ -5379,6 +5381,9 @@
}
}
}
+ if (dataset.getAuthentication() != null) {
+ hasAuthentication = true;
+ }
}
}
@@ -5390,7 +5395,7 @@
+ " hasSaveInfo: " + (saveInfo != null));
}
mClient.setTrackedViews(id, toArray(trackedViews), mSaveOnAllViewsInvisible,
- saveOnFinish, toArray(fillableIds), saveTriggerId);
+ saveOnFinish, toArray(fillableIds), saveTriggerId, hasAuthentication);
} catch (RemoteException e) {
Slog.w(TAG, "Cannot set tracked ids", e);
}
@@ -5400,7 +5405,7 @@
* Sets the state of views that failed to autofill.
*/
@GuardedBy("mLock")
- void setAutofillFailureLocked(@NonNull List<AutofillId> ids) {
+ void setAutofillFailureLocked(@NonNull List<AutofillId> ids, boolean isRefill) {
if (sVerbose && !ids.isEmpty()) {
Slog.v(TAG, "Total views that failed to populate: " + ids.size());
}
@@ -5418,7 +5423,7 @@
Slog.v(TAG, "Changed state of " + id + " to " + viewState.getStateAsString());
}
}
- mPresentationStatsEventLogger.maybeSetViewFillFailureCounts(ids.size());
+ mPresentationStatsEventLogger.maybeSetViewFillFailureCounts(ids, isRefill);
}
/**
@@ -5435,6 +5440,23 @@
mPresentationStatsEventLogger.maybeAddSuccessId(id);
}
+ /**
+ * Sets the state of views that failed to autofill.
+ */
+ void setNotifyNotExpiringResponseDuringAuth() {
+ synchronized (mLock) {
+ mPresentationStatsEventLogger.maybeSetNotifyNotExpiringResponseDuringAuth();
+ }
+ }
+ /**
+ * Sets the state of views that failed to autofill.
+ */
+ void setLogViewEnteredIgnoredDuringAuth() {
+ synchronized (mLock) {
+ mPresentationStatsEventLogger.notifyViewEnteredIgnoredDuringAuthCount();
+ }
+ }
+
@GuardedBy("mLock")
private void replaceResponseLocked(@NonNull FillResponse oldResponse,
@NonNull FillResponse newResponse, @Nullable Bundle newClientState) {
@@ -6665,6 +6687,11 @@
}
}
+ @GuardedBy("mLock")
+ public void setAutofillIdsAttemptedForRefillLocked(@NonNull List<AutofillId> ids) {
+ mPresentationStatsEventLogger.maybeUpdateViewFillablesForRefillAttempt(ids);
+ }
+
private AutoFillUI getUiForShowing() {
synchronized (mLock) {
mUi.setCallback(this);
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 0fa5260..2e1416b 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -6653,9 +6653,10 @@
// If unbound while waiting to start and there is no connection left in this service,
// remove the pending service
- if (s.getConnections().isEmpty()) {
+ if (s.getConnections().isEmpty() && !s.startRequested) {
mPendingServices.remove(s);
mPendingBringups.remove(s);
+ if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Removed pending service: " + s);
}
if (c.hasFlag(Context.BIND_AUTO_CREATE)) {
diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java
index 8cf47d0..430be03 100644
--- a/services/core/java/com/android/server/appop/AttributedOp.java
+++ b/services/core/java/com/android/server/appop/AttributedOp.java
@@ -109,7 +109,7 @@
uidState, flags);
mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid,
- parent.packageName, tag, uidState, flags, accessTime,
+ parent.packageName, persistentDeviceId, tag, uidState, flags, accessTime,
AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE);
}
@@ -253,8 +253,8 @@
if (isStarted) {
mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid,
- parent.packageName, tag, uidState, flags, startTime, attributionFlags,
- attributionChainId);
+ parent.packageName, persistentDeviceId, tag, uidState, flags, startTime,
+ attributionFlags, attributionChainId);
}
}
@@ -333,7 +333,7 @@
finishedEvent);
mAppOpsService.mHistoricalRegistry.increaseOpAccessDuration(parent.op, parent.uid,
- parent.packageName, tag, event.getUidState(),
+ parent.packageName, persistentDeviceId, tag, event.getUidState(),
event.getFlags(), finishedEvent.getNoteTime(), finishedEvent.getDuration(),
event.getAttributionFlags(), event.getAttributionChainId());
@@ -441,8 +441,9 @@
event.setStartElapsedTime(SystemClock.elapsedRealtime());
event.setStartTime(startTime);
mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid,
- parent.packageName, tag, event.getUidState(), event.getFlags(), startTime,
- event.getAttributionFlags(), event.getAttributionChainId());
+ parent.packageName, persistentDeviceId, tag, event.getUidState(),
+ event.getFlags(), startTime, event.getAttributionFlags(),
+ event.getAttributionChainId());
if (shouldSendActive) {
mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid,
parent.packageName, tag, event.getVirtualDeviceId(), true,
diff --git a/services/core/java/com/android/server/appop/DiscreteRegistry.java b/services/core/java/com/android/server/appop/DiscreteRegistry.java
index 5d83ad6..539dbca 100644
--- a/services/core/java/com/android/server/appop/DiscreteRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteRegistry.java
@@ -41,6 +41,7 @@
import static android.app.AppOpsManager.OP_RESERVED_FOR_TESTING;
import static android.app.AppOpsManager.flagsToString;
import static android.app.AppOpsManager.getUidStateName;
+import static android.companion.virtual.VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT;
import static java.lang.Long.min;
import static java.lang.Math.max;
@@ -52,6 +53,7 @@
import android.os.Build;
import android.os.Environment;
import android.os.FileUtils;
+import android.permission.flags.Flags;
import android.provider.DeviceConfig;
import android.util.ArrayMap;
import android.util.AtomicFile;
@@ -76,7 +78,7 @@
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
+import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@@ -164,6 +166,8 @@
private static final String TAG_OP = "o";
private static final String ATTR_OP_ID = "op";
+ private static final String ATTR_DEVICE_ID = "di";
+
private static final String TAG_TAG = "a";
private static final String ATTR_TAG = "at";
@@ -196,11 +200,14 @@
private boolean mDebugMode = false;
DiscreteRegistry(Object inMemoryLock) {
+ this(inMemoryLock, new File(new File(Environment.getDataSystemDirectory(), "appops"),
+ "discrete"));
+ }
+
+ DiscreteRegistry(Object inMemoryLock, File discreteAccessDir) {
mInMemoryLock = inMemoryLock;
synchronized (mOnDiskLock) {
- mDiscreteAccessDir = new File(
- new File(Environment.getDataSystemDirectory(), "appops"),
- "discrete");
+ mDiscreteAccessDir = discreteAccessDir;
createDiscreteAccessDirLocked();
int largestChainId = readLargestChainIdFromDiskLocked();
synchronized (mInMemoryLock) {
@@ -245,16 +252,24 @@
DEFAULT_DISCRETE_OPS);
}
- void recordDiscreteAccess(int uid, String packageName, int op, @Nullable String attributionTag,
- @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState, long accessTime,
- long accessDuration, @AppOpsManager.AttributionFlags int attributionFlags,
- int attributionChainId) {
+ void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, int op,
+ @Nullable String attributionTag, @AppOpsManager.OpFlags int flags,
+ @AppOpsManager.UidState int uidState, long accessTime, long accessDuration,
+ @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) {
if (!isDiscreteOp(op, flags)) {
return;
}
synchronized (mInMemoryLock) {
- mDiscreteOps.addDiscreteAccess(op, uid, packageName, attributionTag, flags, uidState,
- accessTime, accessDuration, attributionFlags, attributionChainId);
+ if (Flags.deviceAwareAppOpNewSchemaEnabled()) {
+ mDiscreteOps.addDiscreteAccess(op, uid, packageName, deviceId, attributionTag,
+ flags, uidState, accessTime, accessDuration, attributionFlags,
+ attributionChainId);
+ } else {
+ mDiscreteOps.addDiscreteAccess(op, uid, packageName, PERSISTENT_DEVICE_ID_DEFAULT,
+ attributionTag, flags, uidState, accessTime, accessDuration,
+ attributionFlags, attributionChainId);
+ }
+
}
}
@@ -294,7 +309,6 @@
discreteOps.filter(beginTimeMillis, endTimeMillis, filter, uidFilter, packageNameFilter,
opNamesFilter, attributionTagFilter, flagsFilter, attributionChains);
discreteOps.applyToHistoricalOps(result, attributionChains);
- return;
}
private int readLargestChainIdFromDiskLocked() {
@@ -355,28 +369,36 @@
String pkg = pkgs.keyAt(pkgNum);
int nOps = ops.size();
for (int opNum = 0; opNum < nOps; opNum++) {
- ArrayMap<String, List<DiscreteOpEvent>> attrOps =
- ops.valueAt(opNum).mAttributedOps;
int op = ops.keyAt(opNum);
- int nAttrOps = attrOps.size();
- for (int attrOpNum = 0; attrOpNum < nAttrOps; attrOpNum++) {
- List<DiscreteOpEvent> opEvents = attrOps.valueAt(attrOpNum);
- String attributionTag = attrOps.keyAt(attrOpNum);
- int nOpEvents = opEvents.size();
- for (int opEventNum = 0; opEventNum < nOpEvents; opEventNum++) {
- DiscreteOpEvent event = opEvents.get(opEventNum);
- if (event == null
- || event.mAttributionChainId == ATTRIBUTION_CHAIN_ID_NONE
- || (event.mAttributionFlags & ATTRIBUTION_FLAG_TRUSTED) == 0) {
- continue;
- }
+ ArrayMap<String, DiscreteDeviceOp> deviceOps =
+ ops.valueAt(opNum).mDeviceAttributedOps;
- if (!chains.containsKey(event.mAttributionChainId)) {
- chains.put(event.mAttributionChainId,
- new AttributionChain(attributionExemptPkgs));
+ int nDeviceOps = deviceOps.size();
+ for (int deviceNum = 0; deviceNum < nDeviceOps; deviceNum++) {
+ ArrayMap<String, List<DiscreteOpEvent>> attrOps =
+ deviceOps.valueAt(deviceNum).mAttributedOps;
+
+ int nAttrOps = attrOps.size();
+ for (int attrOpNum = 0; attrOpNum < nAttrOps; attrOpNum++) {
+ List<DiscreteOpEvent> opEvents = attrOps.valueAt(attrOpNum);
+ String attributionTag = attrOps.keyAt(attrOpNum);
+ int nOpEvents = opEvents.size();
+ for (int opEventNum = 0; opEventNum < nOpEvents; opEventNum++) {
+ DiscreteOpEvent event = opEvents.get(opEventNum);
+ if (event == null
+ || event.mAttributionChainId == ATTRIBUTION_CHAIN_ID_NONE
+ || (event.mAttributionFlags & ATTRIBUTION_FLAG_TRUSTED)
+ == 0) {
+ continue;
+ }
+
+ if (!chains.containsKey(event.mAttributionChainId)) {
+ chains.put(event.mAttributionChainId,
+ new AttributionChain(attributionExemptPkgs));
+ }
+ chains.get(event.mAttributionChainId)
+ .addEvent(pkg, uid, attributionTag, op, event);
}
- chains.get(event.mAttributionChainId)
- .addEvent(pkg, uid, attributionTag, op, event);
}
}
}
@@ -464,7 +486,7 @@
createDiscreteAccessDir();
}
- private DiscreteOps getAllDiscreteOps() {
+ DiscreteOps getAllDiscreteOps() {
DiscreteOps discreteOps = new DiscreteOps(0);
synchronized (mOnDiskLock) {
@@ -608,7 +630,7 @@
}
}
- private final class DiscreteOps {
+ static final class DiscreteOps {
ArrayMap<Integer, DiscreteUidOps> mUids;
int mChainIdOffset;
int mLargestChainId;
@@ -634,8 +656,9 @@
}
void addDiscreteAccess(int op, int uid, @NonNull String packageName,
- @Nullable String attributionTag, @AppOpsManager.OpFlags int flags,
- @AppOpsManager.UidState int uidState, long accessTime, long accessDuration,
+ @NonNull String deviceId, @Nullable String attributionTag,
+ @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState,
+ long accessTime, long accessDuration,
@AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) {
int offsetChainId = attributionChainId;
if (attributionChainId != ATTRIBUTION_CHAIN_ID_NONE) {
@@ -649,8 +672,9 @@
mChainIdOffset = -1 * attributionChainId;
}
}
- getOrCreateDiscreteUidOps(uid).addDiscreteAccess(op, packageName, attributionTag, flags,
- uidState, accessTime, accessDuration, attributionFlags, offsetChainId);
+ getOrCreateDiscreteUidOps(uid).addDiscreteAccess(op, packageName, deviceId,
+ attributionTag, flags, uidState, accessTime, accessDuration, attributionFlags,
+ offsetChainId);
}
private void filter(long beginTimeMillis, long endTimeMillis,
@@ -837,7 +861,7 @@
}
}
- private final class DiscreteUidOps {
+ static final class DiscreteUidOps {
ArrayMap<String, DiscretePackageOps> mPackages;
DiscreteUidOps() {
@@ -889,12 +913,13 @@
mPackages.remove(packageName);
}
- void addDiscreteAccess(int op, @NonNull String packageName, @Nullable String attributionTag,
- @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState,
- long accessTime, long accessDuration,
+ void addDiscreteAccess(int op, @NonNull String packageName, @NonNull String deviceId,
+ @Nullable String attributionTag, @AppOpsManager.OpFlags int flags,
+ @AppOpsManager.UidState int uidState, long accessTime, long accessDuration,
@AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) {
- getOrCreateDiscretePackageOps(packageName).addDiscreteAccess(op, attributionTag, flags,
- uidState, accessTime, accessDuration, attributionFlags, attributionChainId);
+ getOrCreateDiscretePackageOps(packageName).addDiscreteAccess(op, deviceId,
+ attributionTag, flags, uidState, accessTime, accessDuration,
+ attributionFlags, attributionChainId);
}
private DiscretePackageOps getOrCreateDiscretePackageOps(String packageName) {
@@ -948,7 +973,7 @@
}
}
- private final class DiscretePackageOps {
+ static final class DiscretePackageOps {
ArrayMap<Integer, DiscreteOp> mPackageOps;
DiscretePackageOps() {
@@ -959,12 +984,12 @@
return mPackageOps.isEmpty();
}
- void addDiscreteAccess(int op, @Nullable String attributionTag,
+ void addDiscreteAccess(int op, @NonNull String deviceId, @Nullable String attributionTag,
@AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState,
long accessTime, long accessDuration,
@AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) {
- getOrCreateDiscreteOp(op).addDiscreteAccess(attributionTag, flags, uidState, accessTime,
- accessDuration, attributionFlags, attributionChainId);
+ getOrCreateDiscreteOp(op).addDiscreteAccess(deviceId, attributionTag, flags, uidState,
+ accessTime, accessDuration, attributionFlags, attributionChainId);
}
void merge(DiscretePackageOps other) {
@@ -1056,10 +1081,148 @@
}
}
- private final class DiscreteOp {
- ArrayMap<String, List<DiscreteOpEvent>> mAttributedOps;
+ static final class DiscreteOp {
+ ArrayMap<String, DiscreteDeviceOp> mDeviceAttributedOps;
DiscreteOp() {
+ mDeviceAttributedOps = new ArrayMap<>();
+ }
+
+ boolean isEmpty() {
+ return mDeviceAttributedOps.isEmpty();
+ }
+
+ void merge(DiscreteOp other) {
+ int nDevices = other.mDeviceAttributedOps.size();
+ for (int i = 0; i < nDevices; i++) {
+ String deviceId = other.mDeviceAttributedOps.keyAt(i);
+ DiscreteDeviceOp otherDeviceOps = other.mDeviceAttributedOps.valueAt(i);
+ getOrCreateDiscreteDeviceOp(deviceId).merge(otherDeviceOps);
+ }
+ }
+
+ // Note: Update this method when we want to filter by device Id.
+ private void filter(long beginTimeMillis, long endTimeMillis,
+ @AppOpsManager.HistoricalOpsRequestFilter int filter,
+ @Nullable String attributionTagFilter, @AppOpsManager.OpFlags int flagsFilter,
+ int currentUid, String currentPkgName, int currentOp,
+ ArrayMap<Integer, AttributionChain> attributionChains) {
+ int nDevices = mDeviceAttributedOps.size();
+ for (int i = nDevices - 1; i >= 0; i--) {
+ mDeviceAttributedOps.valueAt(i).filter(beginTimeMillis, endTimeMillis, filter,
+ attributionTagFilter, flagsFilter, currentUid, currentPkgName, currentOp,
+ attributionChains);
+ if (mDeviceAttributedOps.valueAt(i).isEmpty()) {
+ mDeviceAttributedOps.removeAt(i);
+ }
+ }
+ }
+
+ private void offsetHistory(long offset) {
+ int nDevices = mDeviceAttributedOps.size();
+ for (int i = 0; i < nDevices; i++) {
+ mDeviceAttributedOps.valueAt(i).offsetHistory(offset);
+ }
+ }
+
+ void addDiscreteAccess(@NonNull String deviceId, @Nullable String attributionTag,
+ @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState,
+ long accessTime, long accessDuration,
+ @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) {
+ getOrCreateDiscreteDeviceOp(deviceId).addDiscreteAccess(attributionTag, flags, uidState,
+ accessTime, accessDuration, attributionFlags, attributionChainId);
+ }
+
+ private DiscreteDeviceOp getOrCreateDiscreteDeviceOp(String deviceId) {
+ return mDeviceAttributedOps.computeIfAbsent(deviceId, k -> new DiscreteDeviceOp());
+ }
+
+ // TODO: b/308716962 Retrieve discrete histories from all devices and integrate them with
+ // HistoricalOps
+ private void applyToHistory(AppOpsManager.HistoricalOps result, int uid,
+ @NonNull String packageName, int op,
+ @NonNull ArrayMap<Integer, AttributionChain> attributionChains) {
+ if (mDeviceAttributedOps.get(PERSISTENT_DEVICE_ID_DEFAULT) != null) {
+ mDeviceAttributedOps.get(PERSISTENT_DEVICE_ID_DEFAULT).applyToHistory(result, uid,
+ packageName, op, attributionChains);
+ }
+ }
+
+ private void dump(@NonNull PrintWriter pw, @NonNull SimpleDateFormat sdf,
+ @NonNull Date date, @NonNull String prefix, int nDiscreteOps) {
+ int nDevices = mDeviceAttributedOps.size();
+ for (int i = 0; i < nDevices; i++) {
+ pw.print(prefix);
+ pw.print("Device: ");
+ pw.print(mDeviceAttributedOps.keyAt(i));
+ pw.println();
+ mDeviceAttributedOps.valueAt(i).dump(pw, sdf, date, prefix + " ",
+ nDiscreteOps);
+ }
+ }
+
+ void serialize(TypedXmlSerializer out) throws Exception {
+ int nDevices = mDeviceAttributedOps.size();
+ for (int i = 0; i < nDevices; i++) {
+ String deviceId = mDeviceAttributedOps.keyAt(i);
+ mDeviceAttributedOps.valueAt(i).serialize(out, deviceId);
+ }
+ }
+
+ void deserialize(TypedXmlPullParser parser, long beginTimeMillis) throws Exception {
+ int outerDepth = parser.getDepth();
+ while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+ if (TAG_TAG.equals(parser.getName())) {
+ String attributionTag = parser.getAttributeValue(null, ATTR_TAG);
+
+ int innerDepth = parser.getDepth();
+ while (XmlUtils.nextElementWithin(parser, innerDepth)) {
+ if (TAG_ENTRY.equals(parser.getName())) {
+ long noteTime = parser.getAttributeLong(null, ATTR_NOTE_TIME);
+ long noteDuration = parser.getAttributeLong(null, ATTR_NOTE_DURATION,
+ -1);
+ int uidState = parser.getAttributeInt(null, ATTR_UID_STATE);
+ int opFlags = parser.getAttributeInt(null, ATTR_FLAGS);
+ int attributionFlags = parser.getAttributeInt(null,
+ ATTR_ATTRIBUTION_FLAGS, AppOpsManager.ATTRIBUTION_FLAGS_NONE);
+ int attributionChainId = parser.getAttributeInt(null, ATTR_CHAIN_ID,
+ AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE);
+ String deviceId = parser.getAttributeValue(null, ATTR_DEVICE_ID);
+ if (deviceId == null) {
+ deviceId = PERSISTENT_DEVICE_ID_DEFAULT;
+ }
+ if (noteTime + noteDuration < beginTimeMillis) {
+ continue;
+ }
+
+ DiscreteDeviceOp deviceOps = getOrCreateDiscreteDeviceOp(deviceId);
+ List<DiscreteOpEvent> events =
+ deviceOps.getOrCreateDiscreteOpEventsList(attributionTag);
+ DiscreteOpEvent event = new DiscreteOpEvent(noteTime, noteDuration,
+ uidState, opFlags, attributionFlags, attributionChainId);
+ events.add(event);
+ }
+ }
+ }
+ }
+
+ int nDeviceOps = mDeviceAttributedOps.size();
+ for (int i = 0; i < nDeviceOps; i++) {
+ DiscreteDeviceOp deviceOp = mDeviceAttributedOps.valueAt(i);
+
+ int nAttrOps = deviceOp.mAttributedOps.size();
+ for (int j = 0; j < nAttrOps; j++) {
+ List<DiscreteOpEvent> events = deviceOp.mAttributedOps.valueAt(j);
+ events.sort(Comparator.comparingLong(a -> a.mNoteTime));
+ }
+ }
+ }
+ }
+
+ static final class DiscreteDeviceOp {
+ ArrayMap<String, List<DiscreteOpEvent>> mAttributedOps;
+
+ DiscreteDeviceOp() {
mAttributedOps = new ArrayMap<>();
}
@@ -1067,7 +1230,7 @@
return mAttributedOps.isEmpty();
}
- void merge(DiscreteOp other) {
+ void merge(DiscreteDeviceOp other) {
int nTags = other.mAttributedOps.size();
for (int i = 0; i < nTags; i++) {
String tag = other.mAttributedOps.keyAt(i);
@@ -1097,7 +1260,7 @@
currentUid, currentPkgName, currentOp, mAttributedOps.keyAt(i),
attributionChains);
mAttributedOps.put(tag, list);
- if (list.size() == 0) {
+ if (list.isEmpty()) {
mAttributedOps.removeAt(i);
}
}
@@ -1125,8 +1288,7 @@
List<DiscreteOpEvent> attributedOps = getOrCreateDiscreteOpEventsList(
attributionTag);
- int nAttributedOps = attributedOps.size();
- int i = nAttributedOps;
+ int i = attributedOps.size();
for (; i > 0; i--) {
DiscreteOpEvent previousOp = attributedOps.get(i - 1);
if (discretizeTimeStamp(previousOp.mNoteTime) < discretizeTimeStamp(accessTime)) {
@@ -1148,12 +1310,8 @@
}
private List<DiscreteOpEvent> getOrCreateDiscreteOpEventsList(String attributionTag) {
- List<DiscreteOpEvent> result = mAttributedOps.get(attributionTag);
- if (result == null) {
- result = new ArrayList<>();
- mAttributedOps.put(attributionTag, result);
- }
- return result;
+ return mAttributedOps.computeIfAbsent(attributionTag,
+ k -> new ArrayList<>());
}
private void applyToHistory(AppOpsManager.HistoricalOps result, int uid,
@@ -1167,8 +1325,7 @@
for (int j = 0; j < nEvents; j++) {
DiscreteOpEvent event = events.get(j);
AppOpsManager.OpEventProxyInfo proxy = null;
- if (event.mAttributionChainId != ATTRIBUTION_CHAIN_ID_NONE
- && attributionChains != null) {
+ if (event.mAttributionChainId != ATTRIBUTION_CHAIN_ID_NONE) {
AttributionChain chain = attributionChains.get(event.mAttributionChainId);
if (chain != null && chain.isComplete()
&& chain.isStart(packageName, uid, tag, op, event)
@@ -1198,65 +1355,31 @@
int first = nDiscreteOps < 1 ? 0 : max(0, nOps - nDiscreteOps);
for (int j = first; j < nOps; j++) {
ops.get(j).dump(pw, sdf, date, prefix + " ");
-
}
}
}
- void serialize(TypedXmlSerializer out) throws Exception {
+ void serialize(TypedXmlSerializer out, String deviceId) throws Exception {
int nAttributions = mAttributedOps.size();
for (int i = 0; i < nAttributions; i++) {
out.startTag(null, TAG_TAG);
String tag = mAttributedOps.keyAt(i);
if (tag != null) {
- out.attribute(null, ATTR_TAG, mAttributedOps.keyAt(i));
+ out.attribute(null, ATTR_TAG, tag);
}
List<DiscreteOpEvent> ops = mAttributedOps.valueAt(i);
int nOps = ops.size();
for (int j = 0; j < nOps; j++) {
out.startTag(null, TAG_ENTRY);
- ops.get(j).serialize(out);
+ ops.get(j).serialize(out, deviceId);
out.endTag(null, TAG_ENTRY);
}
out.endTag(null, TAG_TAG);
}
}
-
- void deserialize(TypedXmlPullParser parser, long beginTimeMillis) throws Exception {
- int outerDepth = parser.getDepth();
- while (XmlUtils.nextElementWithin(parser, outerDepth)) {
- if (TAG_TAG.equals(parser.getName())) {
- String attributionTag = parser.getAttributeValue(null, ATTR_TAG);
- List<DiscreteOpEvent> events = getOrCreateDiscreteOpEventsList(
- attributionTag);
- int innerDepth = parser.getDepth();
- while (XmlUtils.nextElementWithin(parser, innerDepth)) {
- if (TAG_ENTRY.equals(parser.getName())) {
- long noteTime = parser.getAttributeLong(null, ATTR_NOTE_TIME);
- long noteDuration = parser.getAttributeLong(null, ATTR_NOTE_DURATION,
- -1);
- int uidState = parser.getAttributeInt(null, ATTR_UID_STATE);
- int opFlags = parser.getAttributeInt(null, ATTR_FLAGS);
- int attributionFlags = parser.getAttributeInt(null,
- ATTR_ATTRIBUTION_FLAGS, AppOpsManager.ATTRIBUTION_FLAGS_NONE);
- int attributionChainId = parser.getAttributeInt(null, ATTR_CHAIN_ID,
- AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE);
- if (noteTime + noteDuration < beginTimeMillis) {
- continue;
- }
- DiscreteOpEvent event = new DiscreteOpEvent(noteTime, noteDuration,
- uidState, opFlags, attributionFlags, attributionChainId);
- events.add(event);
- }
- }
- Collections.sort(events, (a, b) -> a.mNoteTime < b.mNoteTime ? -1
- : (a.mNoteTime == b.mNoteTime ? 0 : 1));
- }
- }
- }
}
- private final class DiscreteOpEvent {
+ static final class DiscreteOpEvent {
final long mNoteTime;
final long mNoteDuration;
final @AppOpsManager.UidState int mUidState;
@@ -1306,7 +1429,7 @@
pw.println();
}
- private void serialize(TypedXmlSerializer out) throws Exception {
+ private void serialize(TypedXmlSerializer out, String deviceId) throws Exception {
out.attributeLong(null, ATTR_NOTE_TIME, mNoteTime);
if (mNoteDuration != -1) {
out.attributeLong(null, ATTR_NOTE_DURATION, mNoteDuration);
@@ -1317,6 +1440,9 @@
if (mAttributionChainId != AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE) {
out.attributeInt(null, ATTR_CHAIN_ID, mAttributionChainId);
}
+ if (!Objects.equals(deviceId, PERSISTENT_DEVICE_ID_DEFAULT)) {
+ out.attribute(null, ATTR_DEVICE_ID, deviceId);
+ }
out.attributeInt(null, ATTR_UID_STATE, mUidState);
out.attributeInt(null, ATTR_FLAGS, mOpFlag);
}
diff --git a/services/core/java/com/android/server/appop/HistoricalRegistry.java b/services/core/java/com/android/server/appop/HistoricalRegistry.java
index dbd47d0..fffb108 100644
--- a/services/core/java/com/android/server/appop/HistoricalRegistry.java
+++ b/services/core/java/com/android/server/appop/HistoricalRegistry.java
@@ -472,9 +472,9 @@
}
void incrementOpAccessedCount(int op, int uid, @NonNull String packageName,
- @Nullable String attributionTag, @UidState int uidState, @OpFlags int flags,
- long accessTime, @AppOpsManager.AttributionFlags int attributionFlags,
- int attributionChainId) {
+ @NonNull String deviceId, @Nullable String attributionTag, @UidState int uidState,
+ @OpFlags int flags, long accessTime,
+ @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) {
synchronized (mInMemoryLock) {
if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) {
if (!isPersistenceInitializedMLocked()) {
@@ -485,8 +485,9 @@
System.currentTimeMillis()).increaseAccessCount(op, uid, packageName,
attributionTag, uidState, flags, 1);
- mDiscreteRegistry.recordDiscreteAccess(uid, packageName, op, attributionTag,
- flags, uidState, accessTime, -1, attributionFlags, attributionChainId);
+ mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op,
+ attributionTag, flags, uidState, accessTime, -1, attributionFlags,
+ attributionChainId);
}
}
}
@@ -507,8 +508,8 @@
}
void increaseOpAccessDuration(int op, int uid, @NonNull String packageName,
- @Nullable String attributionTag, @UidState int uidState, @OpFlags int flags,
- long eventStartTime, long increment,
+ @NonNull String deviceId, @Nullable String attributionTag, @UidState int uidState,
+ @OpFlags int flags, long eventStartTime, long increment,
@AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) {
synchronized (mInMemoryLock) {
if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) {
@@ -519,9 +520,9 @@
getUpdatedPendingHistoricalOpsMLocked(
System.currentTimeMillis()).increaseAccessDuration(op, uid, packageName,
attributionTag, uidState, flags, increment);
- mDiscreteRegistry.recordDiscreteAccess(uid, packageName, op, attributionTag,
- flags, uidState, eventStartTime, increment, attributionFlags,
- attributionChainId);
+ mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op,
+ attributionTag, flags, uidState, eventStartTime, increment,
+ attributionFlags, attributionChainId);
}
}
}
diff --git a/services/core/java/com/android/server/hdmi/RequestActiveSourceAction.java b/services/core/java/com/android/server/hdmi/RequestActiveSourceAction.java
index 539a00d..a33d70a 100644
--- a/services/core/java/com/android/server/hdmi/RequestActiveSourceAction.java
+++ b/services/core/java/com/android/server/hdmi/RequestActiveSourceAction.java
@@ -19,6 +19,7 @@
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.IHdmiControlCallback;
import android.util.Slog;
+import com.android.internal.annotations.VisibleForTesting;
/**
* Feature action that sends <Request Active Source> message and waits for <Active Source> on TV
@@ -39,6 +40,10 @@
// Number of retries <Request Active Source> is sent if no device answers this message.
private static final int MAX_SEND_RETRY_COUNT = 1;
+ // Timeout to wait for the LauncherX API call to be completed.
+ @VisibleForTesting
+ protected static final int TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS = 10000;
+
private int mSendRetryCount = 0;
@@ -55,7 +60,7 @@
// We wait for default timeout to allow the message triggered by the LauncherX API call to
// be sent by the TV and another default timeout in case the message has to be answered
// (e.g. TV sent a <Set Stream Path> or <Routing Change>).
- addTimer(mState, HdmiConfig.TIMEOUT_MS * 2);
+ addTimer(mState, TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS);
return true;
}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 55de9aa..a06ad14 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -1254,9 +1254,10 @@
/**
* Start drag and drop.
*
- * @param fromChannel The input channel that is currently receiving a touch gesture that should
- * be turned into the drag pointer.
- * @param dragAndDropChannel The input channel associated with the system drag window.
+ * @param fromChannelToken The token of the input channel that is currently receiving a touch
+ * gesture that should be turned into the drag pointer.
+ * @param dragAndDropChannelToken The token of the input channel associated with the system drag
+ * window.
* @return true if drag and drop was successfully started, false otherwise.
*/
public boolean startDragAndDrop(@NonNull IBinder fromChannelToken,
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
index a7280e6..7f7ae10 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
@@ -154,7 +154,7 @@
void reportPerceptibleAsync(IBinder windowToken, boolean perceptible);
@PermissionVerified(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
- void removeImeSurface();
+ void removeImeSurface(int displayId);
void removeImeSurfaceFromWindowAsync(IBinder windowToken);
@@ -384,10 +384,10 @@
@EnforcePermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
@Override
- public void removeImeSurface() {
+ public void removeImeSurface(int displayId) {
super.removeImeSurface_enforcePermission();
- mCallback.removeImeSurface();
+ mCallback.removeImeSurface(displayId);
}
@Override
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
index cdea6ff..0b3f3f0 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
@@ -227,7 +227,6 @@
? overlayWindowToken : null;
synchronized (ImfLock.class) {
mCurVisibleImeLayeringOverlay = overlay;
-
}
}
@@ -577,7 +576,6 @@
}
@GuardedBy("ImfLock.class")
- @VisibleForTesting
ImeVisibilityResult onInteractiveChanged(IBinder windowToken, boolean interactive) {
final ImeTargetWindowState state = getWindowStateOrNull(windowToken);
if (state != null && state.isRequestedImeVisible() && mInputShown && !interactive) {
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index effecd4..7ff03c2 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -254,8 +254,6 @@
private @interface MultiUserUnawareField {
}
- private static final int MSG_SHOW_IM_SUBTYPE_PICKER = 1;
-
private static final int MSG_HIDE_ALL_INPUT_METHODS = 1035;
private static final int MSG_REMOVE_IME_SURFACE = 1060;
private static final int MSG_REMOVE_IME_SURFACE_FROM_WINDOW = 1061;
@@ -474,6 +472,9 @@
IInputMethodSession mSession;
InputChannel mChannel;
+ @UserIdInt
+ final int mUserId;
+
@Override
public String toString() {
return "SessionState{uid=" + mClient.mUid + " pid=" + mClient.mPid
@@ -482,15 +483,17 @@
+ " session=" + Integer.toHexString(
System.identityHashCode(mSession))
+ " channel=" + mChannel
+ + " userId=" + mUserId
+ "}";
}
SessionState(ClientState client, IInputMethodInvoker method,
- IInputMethodSession session, InputChannel channel) {
+ IInputMethodSession session, InputChannel channel, @UserIdInt int userId) {
mClient = client;
mMethod = method;
mSession = session;
mChannel = channel;
+ mUserId = userId;
}
}
@@ -2349,7 +2352,7 @@
if (userData.mCurClient != null) {
clearClientSessionLocked(userData.mCurClient);
userData.mCurClient.mCurSession = new SessionState(
- userData.mCurClient, method, session, channel);
+ userData.mCurClient, method, session, channel, userId);
InputBindResult res = attachNewInputLocked(
StartInputReason.SESSION_CREATED_BY_IME, true, userId);
attachNewAccessibilityLocked(StartInputReason.SESSION_CREATED_BY_IME, true,
@@ -2490,9 +2493,7 @@
sessionState.mSession.finishSession();
} catch (RemoteException e) {
Slog.w(TAG, "Session failed to close due to remote exception", e);
- // TODO(b/350386877): Propagate userId from the caller or infer it from
- // sessionState
- final int userId = mCurrentUserId;
+ final int userId = sessionState.mUserId;
final var bindingController = getInputMethodBindingController(userId);
updateSystemUiLocked(0 /* vis */, bindingController.getBackDisposition(),
userId);
@@ -3947,10 +3948,9 @@
}
@GuardedBy("ImfLock.class")
- private boolean canShowInputMethodPickerLocked(IInputMethodClient client) {
+ private boolean canShowInputMethodPickerLocked(IInputMethodClient client,
+ @UserIdInt int userId) {
final int uid = Binder.getCallingUid();
- // TODO(b/305849394): Get userId from callers.
- final int userId = mCurrentUserId;
final var userData = getUserData(userId);
if (userData.mImeBindingState.mFocusedWindowClient != null && client != null
&& userData.mImeBindingState.mFocusedWindowClient.mClient.asBinder()
@@ -3977,19 +3977,22 @@
}
final int callingUserId = UserHandle.getCallingUserId();
synchronized (ImfLock.class) {
- if (!canShowInputMethodPickerLocked(client)) {
+ final int userId = resolveImeUserIdLocked(callingUserId);
+ if (!canShowInputMethodPickerLocked(client, userId)) {
Slog.w(TAG, "Ignoring showInputMethodPickerFromClient of uid "
+ Binder.getCallingUid() + ": " + client);
return;
}
- final int userId = resolveImeUserIdLocked(callingUserId);
final var userData = getUserData(userId);
// Always call subtype picker, because subtype picker is a superset of input method
// picker.
final int displayId = (userData.mCurClient != null)
? userData.mCurClient.mSelfReportedDisplayId : DEFAULT_DISPLAY;
- mHandler.obtainMessage(MSG_SHOW_IM_SUBTYPE_PICKER, auxiliarySubtypeMode, displayId)
- .sendToTarget();
+ mHandler.post(() -> {
+ synchronized (ImfLock.class) {
+ showInputMethodPickerLocked(auxiliarySubtypeMode, displayId, userId);
+ }
+ });
}
}
@@ -3998,8 +4001,12 @@
public void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) {
// Always call subtype picker, because subtype picker is a superset of input method
// picker.
- mHandler.obtainMessage(MSG_SHOW_IM_SUBTYPE_PICKER, auxiliarySubtypeMode, displayId)
- .sendToTarget();
+ mHandler.post(() -> {
+ synchronized (ImfLock.class) {
+ final int userId = resolveImeUserIdFromDisplayIdLocked(displayId);
+ showInputMethodPickerLocked(auxiliarySubtypeMode, displayId, userId);
+ }
+ });
}
/**
@@ -4427,7 +4434,7 @@
@IInputMethodManagerImpl.PermissionVerified(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
@Override
- public void removeImeSurface() {
+ public void removeImeSurface(int displayId) {
mHandler.obtainMessage(MSG_REMOVE_IME_SURFACE).sendToTarget();
}
@@ -4990,89 +4997,80 @@
userData.mEnabledAccessibilitySessions = accessibilitySessions;
}
+ @GuardedBy("ImfLock.class")
+ private void showInputMethodPickerLocked(int auxiliarySubtypeMode, int displayId,
+ @UserIdInt int userId) {
+ final boolean showAuxSubtypes;
+ switch (auxiliarySubtypeMode) {
+ // This is undocumented so far, but IMM#showInputMethodPicker() has been
+ // implemented so that auxiliary subtypes will be excluded when the soft
+ // keyboard is invisible.
+ case InputMethodManager.SHOW_IM_PICKER_MODE_AUTO ->
+ showAuxSubtypes = mVisibilityStateComputer.isInputShown();
+ case InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES ->
+ showAuxSubtypes = true;
+ case InputMethodManager.SHOW_IM_PICKER_MODE_EXCLUDE_AUXILIARY_SUBTYPES ->
+ showAuxSubtypes = false;
+ default -> {
+ Slog.e(TAG, "Unknown subtype picker mode=" + auxiliarySubtypeMode);
+ return;
+ }
+ }
+ final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
+ final boolean isScreenLocked = mWindowManagerInternal.isKeyguardLocked()
+ && mWindowManagerInternal.isKeyguardSecure(userId);
+ final String lastInputMethodId = settings.getSelectedInputMethod();
+ int lastInputMethodSubtypeId = settings.getSelectedInputMethodSubtypeId(lastInputMethodId);
+
+ final List<ImeSubtypeListItem> imList = InputMethodSubtypeSwitchingController
+ .getSortedInputMethodAndSubtypeList(
+ showAuxSubtypes, isScreenLocked, true /* forImeMenu */,
+ mContext, settings);
+ if (imList.isEmpty()) {
+ Slog.w(TAG, "Show switching menu failed, imList is empty,"
+ + " showAuxSubtypes: " + showAuxSubtypes
+ + " isScreenLocked: " + isScreenLocked
+ + " userId: " + userId);
+ return;
+ }
+
+ if (Flags.imeSwitcherRevamp()) {
+ if (DEBUG) {
+ Slog.v(TAG, "Show IME switcher menu,"
+ + " showAuxSubtypes=" + showAuxSubtypes
+ + " displayId=" + displayId
+ + " preferredInputMethodId=" + lastInputMethodId
+ + " preferredInputMethodSubtypeId=" + lastInputMethodSubtypeId);
+ }
+
+ final var itemsAndIndex = getInputMethodPickerItems(imList,
+ lastInputMethodId, lastInputMethodSubtypeId, userId);
+ final var menuItems = itemsAndIndex.first;
+ final int selectedIndex = itemsAndIndex.second;
+
+ if (selectedIndex == -1) {
+ Slog.w(TAG, "Switching menu shown with no item selected"
+ + ", IME id: " + lastInputMethodId
+ + ", subtype index: " + lastInputMethodSubtypeId);
+ }
+
+ mMenuControllerNew.show(menuItems, selectedIndex, displayId, userId);
+ } else {
+ mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId,
+ lastInputMethodId, lastInputMethodSubtypeId, imList);
+ }
+ }
+
@SuppressWarnings("unchecked")
@UiThread
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
- case MSG_SHOW_IM_SUBTYPE_PICKER:
- final boolean showAuxSubtypes;
- final int displayId = msg.arg2;
- switch (msg.arg1) {
- case InputMethodManager.SHOW_IM_PICKER_MODE_AUTO:
- // This is undocumented so far, but IMM#showInputMethodPicker() has been
- // implemented so that auxiliary subtypes will be excluded when the soft
- // keyboard is invisible.
- synchronized (ImfLock.class) {
- showAuxSubtypes = mVisibilityStateComputer.isInputShown();
- }
- break;
- case InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES:
- showAuxSubtypes = true;
- break;
- case InputMethodManager.SHOW_IM_PICKER_MODE_EXCLUDE_AUXILIARY_SUBTYPES:
- showAuxSubtypes = false;
- break;
- default:
- Slog.e(TAG, "Unknown subtype picker mode = " + msg.arg1);
- return false;
- }
+ case MSG_HIDE_ALL_INPUT_METHODS: {
+ @SoftInputShowHideReason final int reason = msg.arg1;
+ final int originatingDisplayId = msg.arg2;
synchronized (ImfLock.class) {
- final InputMethodSettings settings =
- InputMethodSettingsRepository.get(mCurrentUserId);
- final int userId = settings.getUserId();
- final boolean isScreenLocked = mWindowManagerInternal.isKeyguardLocked()
- && mWindowManagerInternal.isKeyguardSecure(userId);
- final String lastInputMethodId = settings.getSelectedInputMethod();
- int lastInputMethodSubtypeId =
- settings.getSelectedInputMethodSubtypeId(lastInputMethodId);
-
- final List<ImeSubtypeListItem> imList = InputMethodSubtypeSwitchingController
- .getSortedInputMethodAndSubtypeList(
- showAuxSubtypes, isScreenLocked, true /* forImeMenu */,
- mContext, settings);
- if (imList.isEmpty()) {
- Slog.w(TAG, "Show switching menu failed, imList is empty,"
- + " showAuxSubtypes: " + showAuxSubtypes
- + " isScreenLocked: " + isScreenLocked
- + " userId: " + userId);
- return false;
- }
-
- if (mNewInputMethodSwitcherMenuEnabled) {
- if (DEBUG) {
- Slog.v(TAG, "Show IME switcher menu,"
- + " showAuxSubtypes=" + showAuxSubtypes
- + " displayId=" + displayId
- + " preferredInputMethodId=" + lastInputMethodId
- + " preferredInputMethodSubtypeId=" + lastInputMethodSubtypeId);
- }
-
- final var itemsAndIndex = getInputMethodPickerItems(imList,
- lastInputMethodId, lastInputMethodSubtypeId, userId);
- final var menuItems = itemsAndIndex.first;
- final int selectedIndex = itemsAndIndex.second;
-
- if (selectedIndex == -1) {
- Slog.w(TAG, "Switching menu shown with no item selected"
- + ", IME id: " + lastInputMethodId
- + ", subtype index: " + lastInputMethodSubtypeId);
- }
-
- mMenuControllerNew.show(menuItems, selectedIndex, displayId, userId);
- } else {
- mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId,
- lastInputMethodId, lastInputMethodSubtypeId, imList);
- }
- }
- return true;
-
- // ---------------------------------------------------------
-
- case MSG_HIDE_ALL_INPUT_METHODS:
- synchronized (ImfLock.class) {
- // TODO(b/305849394): Needs to figure out what to do where for background users.
- final int userId = mCurrentUserId;
+ final int userId = resolveImeUserIdFromDisplayIdLocked(originatingDisplayId);
final var userData = getUserData(userId);
if (Flags.refactorInsetsController()) {
if (userData.mImeBindingState != null
@@ -5080,15 +5078,16 @@
&& userData.mImeBindingState.mFocusedWindowClient.mClient != null) {
userData.mImeBindingState.mFocusedWindowClient.mClient
.setImeVisibility(false,
- null /* TODO(b329229469) check statsToken */);
+ null /* TODO(b329229469) check statsToken */);
}
} else {
- @SoftInputShowHideReason final int reason = (int) msg.obj;
+
hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
0 /* flags */, reason, userId);
}
}
return true;
+ }
case MSG_REMOVE_IME_SURFACE: {
synchronized (ImfLock.class) {
// TODO(b/305849394): Needs to figure out what to do where for background users.
@@ -5806,7 +5805,8 @@
public void hideAllInputMethods(@SoftInputShowHideReason int reason,
int originatingDisplayId) {
mHandler.removeMessages(MSG_HIDE_ALL_INPUT_METHODS);
- mHandler.obtainMessage(MSG_HIDE_ALL_INPUT_METHODS, reason).sendToTarget();
+ mHandler.obtainMessage(MSG_HIDE_ALL_INPUT_METHODS, reason, originatingDisplayId)
+ .sendToTarget();
}
@ImfLockFree
diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
index fdb2e6f..f603ff3 100644
--- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
+++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
@@ -300,8 +300,8 @@
@IInputMethodManagerImpl.PermissionVerified(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
@Override
- public void removeImeSurface() {
- mInner.removeImeSurface();
+ public void removeImeSurface(int displayId) {
+ mInner.removeImeSurface(displayId);
}
@Override
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 016abff..4179edd 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1315,10 +1315,24 @@
nv.rank, nv.count);
StatusBarNotification sbn = r.getSbn();
- cancelNotification(callingUid, callingPid, sbn.getPackageName(), sbn.getTag(),
- sbn.getId(), FLAG_AUTO_CANCEL,
- FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB | FLAG_BUBBLE,
- false, r.getUserId(), REASON_CLICK, nv.rank, nv.count, null);
+ // Notifications should be cancelled on click if they have been lifetime extended,
+ // regardless of presence or absence of FLAG_AUTO_CANCEL.
+ if (lifetimeExtensionRefactor()
+ && (sbn.getNotification().flags
+ & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) != 0) {
+ cancelNotification(callingUid, callingPid, sbn.getPackageName(), sbn.getTag(),
+ sbn.getId(), FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY,
+ FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB
+ | FLAG_BUBBLE,
+ false, r.getUserId(), REASON_CLICK, nv.rank, nv.count, null);
+
+ } else {
+ // Otherwise, only FLAG_AUTO_CANCEL notifications should be canceled on click.
+ cancelNotification(callingUid, callingPid, sbn.getPackageName(), sbn.getTag(),
+ sbn.getId(), FLAG_AUTO_CANCEL,
+ FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB | FLAG_BUBBLE,
+ false, r.getUserId(), REASON_CLICK, nv.rank, nv.count, null);
+ }
nv.recycle();
reportUserInteraction(r);
mAssistants.notifyAssistantNotificationClicked(r);
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index a17c48d..4425079 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -3825,13 +3825,13 @@
// This also has the (beneficial) side effect where if a package disappears from an
// APEX, leaving only a /data copy, it will lose its apexModuleName.
//
- // This must be done before scanSystemPackageLI as that will throw in the case of a
+ // This must be done before scanPackageForInitLI as that will throw in the case of a
// system -> data package.
disabledPkgSetting.setApexModuleName(activeApexInfo.apexModuleName);
}
}
- final Pair<ScanResult, Boolean> scanResultPair = scanSystemPackageLI(
+ final Pair<ScanResult, Boolean> scanResultPair = scanPackageForInitLI(
parsedPackage, parseFlags, scanFlags, user);
final ScanResult scanResult = scanResultPair.first;
boolean shouldHideSystemApp = scanResultPair.second;
@@ -4066,7 +4066,7 @@
}
}
- private Pair<ScanResult, Boolean> scanSystemPackageLI(ParsedPackage parsedPackage,
+ private Pair<ScanResult, Boolean> scanPackageForInitLI(ParsedPackage parsedPackage,
@ParsingPackageUtils.ParseFlags int parseFlags,
@PackageManagerService.ScanFlags int scanFlags,
@Nullable UserHandle user) throws PackageManagerException {
@@ -4179,7 +4179,7 @@
ParsingPackageUtils.getSigningDetails(input, parsedPackage,
false /*skipVerify*/);
if (result.isError()) {
- throw new PrepareFailure("Failed collect during scanSystemPackageLI",
+ throw new PrepareFailure("Failed collect during scanPackageForInitLI",
result.getException());
}
disabledPkgSetting.setSigningDetails(result.getResult());
diff --git a/services/core/java/com/android/server/power/hint/TEST_MAPPING b/services/core/java/com/android/server/power/hint/TEST_MAPPING
index 34c25c6..9988786 100644
--- a/services/core/java/com/android/server/power/hint/TEST_MAPPING
+++ b/services/core/java/com/android/server/power/hint/TEST_MAPPING
@@ -1,27 +1,18 @@
{
- "presubmit": [
- {
- "name": "FrameworksServicesTests",
- "options": [
- {
- "include-filter": "com.android.server.power.hint"
- },
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
- }
- ]
- }
- ],
"postsubmit": [
{
+ "name": "PerformanceHintTests",
+ "options": [
+ {"exclude-annotation": "androidx.test.filters.FlakyTest"},
+ {"exclude-annotation": "org.junit.Ignore"}
+ ]
+ },
+ {
"name": "CtsStatsdAtomHostTestCases",
"options": [
+ {"include-filter": "android.cts.statsdatom.performancehintmanager"},
{"exclude-annotation": "androidx.test.filters.FlakyTest"},
- {"exclude-annotation": "org.junit.Ignore"},
- {"include-filter": "android.cts.statsdatom.performancehintmanager"}
- ],
- "file_patterns": [
- "(/|^)HintManagerService.java"
+ {"exclude-annotation": "org.junit.Ignore"}
]
}
]
diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java
index 6abef8b..c849a37 100644
--- a/services/core/java/com/android/server/wm/DragDropController.java
+++ b/services/core/java/com/android/server/wm/DragDropController.java
@@ -16,7 +16,6 @@
package com.android.server.wm;
-import static android.content.ClipDescription.EXTRA_HIDE_DRAG_SOURCE_TASK_ID;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.view.View.DRAG_FLAG_GLOBAL;
import static android.view.View.DRAG_FLAG_GLOBAL_SAME_APPLICATION;
@@ -228,7 +227,7 @@
final Display display = displayContent.getDisplay();
touchFocusTransferredFuture = mCallback.get().registerInputChannel(
mDragState, display, mService.mInputManager,
- callingWin.mInputChannel);
+ callingWin.mInputChannelToken);
} else {
// Skip surface logic for a drag triggered by an AccessibilityAction
mDragState.broadcastDragStartedLocked(touchX, touchY);
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index 3d07744..7a0fd3e 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -416,8 +416,9 @@
}
final TransitionController tc = mRootWindowContainer.mTransitionController;
+ final KeyguardDisplayState state = getDisplayState(displayId);
- final boolean occluded = getDisplayState(displayId).mOccluded;
+ final boolean occluded = state.mOccluded;
final boolean performTransition = isKeyguardLocked(displayId);
final boolean executeTransition = performTransition && !tc.isCollecting();
@@ -461,7 +462,7 @@
/**
* Called when keyguard going away state changed.
*/
- private void handleKeyguardGoingAwayChanged(DisplayContent dc) {
+ private void handleDismissInsecureKeyguard(DisplayContent dc) {
mService.deferWindowLayout();
try {
dc.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY, 0 /* transitFlags */);
@@ -705,16 +706,16 @@
}
boolean hasChange = false;
- if (lastOccluded != mOccluded) {
- writeEventLog("updateVisibility");
+ if (!lastKeyguardGoingAway && mKeyguardGoingAway) {
+ writeEventLog("dismissIfInsecure");
+ controller.handleDismissInsecureKeyguard(display);
+ hasChange = true;
+ } else if (lastOccluded != mOccluded) {
controller.handleOccludedChanged(mDisplayId, mTopOccludesActivity);
hasChange = true;
- } else if (!lastKeyguardGoingAway && mKeyguardGoingAway) {
- writeEventLog("dismissIfInsecure");
- controller.handleKeyguardGoingAwayChanged(display);
- hasChange = true;
}
- // Collect the participates for shell transition, so that transition won't happen too
+
+ // Collect the participants for shell transition, so that transition won't happen too
// early since the transition was set ready.
if (hasChange && top != null && (mOccluded || mKeyguardGoingAway)) {
display.mTransitionController.collect(top);
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index c6aaf4e..48a5050 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -41,7 +41,6 @@
import android.view.IInputFilter;
import android.view.IRemoteAnimationFinishedCallback;
import android.view.IWindow;
-import android.view.InputChannel;
import android.view.MagnificationSpec;
import android.view.RemoteAnimationTarget;
import android.view.Surface;
@@ -377,10 +376,10 @@
public interface IDragDropCallback {
default CompletableFuture<Boolean> registerInputChannel(
DragState state, Display display, InputManagerService service,
- InputChannel source) {
+ IBinder sourceInputChannelToken) {
return state.register(display)
.thenApply(unused ->
- service.startDragAndDrop(source.getToken(), state.getInputToken()));
+ service.startDragAndDrop(sourceInputChannelToken, state.getInputToken()));
}
/**
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index a218068..4db62478 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -9778,7 +9778,7 @@
Slog.e(TAG, "Host window not found");
return;
}
- if (hostWindow.mInputChannel == null) {
+ if (hostWindow.mInputChannelToken == null) {
Slog.e(TAG, "Host window does not have an input channel");
return;
}
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 1cc5a8b..153d41b 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -638,7 +638,7 @@
/**
* Only populated if flag REMOVE_INPUT_CHANNEL_FROM_WINDOWSTATE is disabled.
*/
- InputChannel mInputChannel;
+ private InputChannel mInputChannel;
/**
* The token will be assigned to {@link InputWindowHandle#token} if this window can receive
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
index 8981b7a..dd3b33e 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
@@ -311,10 +311,8 @@
final ArgumentCaptor<IBinder> targetCaptor = ArgumentCaptor.forClass(IBinder.class);
final ArgumentCaptor<ImeVisibilityResult> resultCaptor = ArgumentCaptor.forClass(
ImeVisibilityResult.class);
- synchronized (ImfLock.class) {
- verify(mInputMethodManagerService).onApplyImeVisibilityFromComputerLocked(
- targetCaptor.capture(), notNull() /* statsToken */, resultCaptor.capture());
- }
+ verify(mInputMethodManagerService).onApplyImeVisibilityFromComputerLocked(
+ targetCaptor.capture(), notNull() /* statsToken */, resultCaptor.capture());
final IBinder imeInputTarget = targetCaptor.getValue();
final ImeVisibilityResult result = resultCaptor.getValue();
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
index d427a6d..1dc5126 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
@@ -348,7 +348,7 @@
ClientState cs = mInputMethodManagerService.getClientStateLocked(client);
cs.mCurSession = new InputMethodManagerService.SessionState(cs,
mMockInputMethodInvoker, mMockInputMethodSession, mock(
- InputChannel.class));
+ InputChannel.class), mCallingUserId);
}
}
}
diff --git a/services/tests/performancehinttests/Android.bp b/services/tests/performancehinttests/Android.bp
new file mode 100644
index 0000000..1692921c
--- /dev/null
+++ b/services/tests/performancehinttests/Android.bp
@@ -0,0 +1,34 @@
+package {
+ default_team: "trendy_team_games",
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+ name: "PerformanceHintTests",
+ srcs: [
+ "src/**/*.java",
+ ],
+ static_libs: [
+ "androidx.test.runner",
+ "androidx.test.rules",
+ "flag-junit",
+ "junit",
+ "mockito-target-minus-junit4",
+ "platform-test-annotations",
+ "services.core",
+ "truth",
+ ],
+ libs: [
+ "android.test.base",
+ ],
+ test_suites: [
+ "automotive-tests",
+ "device-tests",
+ ],
+ platform_apis: true,
+ certificate: "platform",
+ dxflags: ["--multi-dex"],
+ optimize: {
+ enabled: false,
+ },
+}
diff --git a/services/tests/performancehinttests/AndroidManifest.xml b/services/tests/performancehinttests/AndroidManifest.xml
new file mode 100644
index 0000000..d955234
--- /dev/null
+++ b/services/tests/performancehinttests/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.server.power.hinttests">
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.server.power.hinttests"
+ android:label="ADPF Performance Hint Service Test"/>
+</manifest>
diff --git a/services/tests/performancehinttests/AndroidTest.xml b/services/tests/performancehinttests/AndroidTest.xml
new file mode 100644
index 0000000..578b7d6
--- /dev/null
+++ b/services/tests/performancehinttests/AndroidTest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Runs Performance Hint Tests.">
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="apct-instrumentation" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="install-arg" value="-t" />
+ <option name="test-file-name" value="PerformanceHintTests.apk" />
+ </target_preparer>
+
+ <option name="test-tag" value="PerformanceHintTests" />
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.server.power.hinttests" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ <option name="exclude-annotation" value="androidx.test.filters.FlakyTest" />
+ </test>
+</configuration>
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/OWNERS b/services/tests/performancehinttests/OWNERS
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/power/hint/OWNERS
rename to services/tests/performancehinttests/OWNERS
diff --git a/services/tests/performancehinttests/TEST_MAPPING b/services/tests/performancehinttests/TEST_MAPPING
new file mode 100644
index 0000000..faffe35
--- /dev/null
+++ b/services/tests/performancehinttests/TEST_MAPPING
@@ -0,0 +1,19 @@
+{
+ "ravenwood-postsubmit": [
+ {
+ "name": "PerformanceHintTestsRavenwood",
+ "host": true,
+ "options": [
+ {"exclude-annotation": "android.platform.test.annotations.DisabledOnRavenwood"}
+ ]
+ }
+ ],
+ "postsubmit": [
+ {
+ "name": "PerformanceHintTests",
+ "options": [
+ {"exclude-annotation": "org.junit.Ignore"}
+ ]
+ }
+ ]
+}
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
similarity index 98%
rename from services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
rename to services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
index 1decd36..7d04470 100644
--- a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -596,7 +596,7 @@
ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
service.mUidObserver.onUidStateChanged(UID,
ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
- LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1000) + TimeUnit.MILLISECONDS.toNanos(
CLEAN_UP_UID_DELAY_MILLIS));
verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any());
reset(mNativeWrapperMock);
@@ -653,7 +653,7 @@
ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
service.mUidObserver.onUidStateChanged(UID,
ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
- LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1000) + TimeUnit.MILLISECONDS.toNanos(
CLEAN_UP_UID_DELAY_MILLIS));
verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any());
verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr2), any());
@@ -666,7 +666,7 @@
service.mUidObserver.onUidStateChanged(UID,
ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
// wait for the async uid state change to trigger resume and setThreads
- LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500));
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1000));
verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr2), eq(expectedTids2));
reset(mNativeWrapperMock);
@@ -675,7 +675,7 @@
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
service.mUidObserver.onUidStateChanged(UID,
ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
- LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1000) + TimeUnit.MILLISECONDS.toNanos(
CLEAN_UP_UID_DELAY_MILLIS));
verify(mNativeWrapperMock, times(1)).halPauseHintSession(eq(sessionPtr1));
verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any());
@@ -684,7 +684,7 @@
verifyAllHintsEnabled(session2, false);
service.mUidObserver.onUidStateChanged(UID,
ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
- LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500));
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1000));
verifyAllHintsEnabled(session1, false);
verifyAllHintsEnabled(session2, true);
reset(mNativeWrapperMock);
@@ -705,7 +705,7 @@
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
service.mUidObserver.onUidStateChanged(UID,
ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
- LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1000) + TimeUnit.MILLISECONDS.toNanos(
CLEAN_UP_UID_DELAY_MILLIS));
verify(mNativeWrapperMock, times(1)).halPauseHintSession(eq(sessionPtr1));
verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any());
@@ -721,7 +721,7 @@
service.mUidObserver.onUidStateChanged(UID,
ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
- LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1000) + TimeUnit.MILLISECONDS.toNanos(
CLEAN_UP_UID_DELAY_MILLIS));
verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr1),
eq(SESSION_TIDS_A));
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index a888dad..a86289b 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -78,7 +78,6 @@
// TODO: remove once Android migrates to JUnit 4.12,
// which provides assertThrows
"testng",
- "truth",
"junit",
"junit-params",
"ActivityContext",
diff --git a/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpPersistenceTest.java b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpPersistenceTest.java
new file mode 100644
index 0000000..bc3a5ca
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpPersistenceTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.server.appop;
+
+import static android.app.AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR;
+import static android.app.AppOpsManager.ATTRIBUTION_FLAG_RECEIVER;
+import static android.app.AppOpsManager.OP_FLAGS_ALL_TRUSTED;
+import static android.app.AppOpsManager.OP_FLAG_SELF;
+import static android.app.AppOpsManager.UID_STATE_FOREGROUND;
+import static android.app.AppOpsManager.UID_STATE_FOREGROUND_SERVICE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.AppOpsManager;
+import android.companion.virtual.VirtualDeviceManager;
+import android.content.Context;
+import android.os.FileUtils;
+import android.os.Process;
+import android.permission.flags.Flags;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class DiscreteAppOpPersistenceTest {
+ private DiscreteRegistry mDiscreteRegistry;
+ private final Object mLock = new Object();
+ private File mMockDataDirectory;
+ private final Context mContext =
+ InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ @Before
+ public void setUp() {
+ mMockDataDirectory = mContext.getDir("mock_data", Context.MODE_PRIVATE);
+ mDiscreteRegistry = new DiscreteRegistry(mLock, mMockDataDirectory);
+ mDiscreteRegistry.systemReady();
+ }
+
+ @After
+ public void cleanUp() {
+ mDiscreteRegistry.writeAndClearAccessHistory();
+ FileUtils.deleteContents(mMockDataDirectory);
+ }
+
+ @RequiresFlagsEnabled(Flags.FLAG_DEVICE_AWARE_APP_OP_NEW_SCHEMA_ENABLED)
+ @Test
+ public void defaultDevice_recordAccess_persistToDisk() {
+ int uid = Process.myUid();
+ String packageName = mContext.getOpPackageName();
+ int op = AppOpsManager.OP_CAMERA;
+ String deviceId = VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT;
+ long accessTime = System.currentTimeMillis();
+ long duration = 60000L;
+ int uidState = UID_STATE_FOREGROUND;
+ int opFlags = OP_FLAGS_ALL_TRUSTED;
+ int attributionFlags = ATTRIBUTION_FLAG_ACCESSOR;
+ int attributionChainId = AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE;
+
+ mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, null, opFlags,
+ uidState, accessTime, duration, attributionFlags, attributionChainId);
+
+ // Verify in-memory object is correct
+ fetchDiscreteOpsAndValidate(uid, packageName, op, deviceId, null, accessTime,
+ duration, uidState, opFlags, attributionFlags, attributionChainId);
+
+ // Write to disk and clear the in-memory object
+ mDiscreteRegistry.writeAndClearAccessHistory();
+
+ // Verify the storage file is created and then verify its content is correct
+ File[] files = FileUtils.listFilesOrEmpty(mMockDataDirectory);
+ assertThat(files.length).isEqualTo(1);
+ fetchDiscreteOpsAndValidate(uid, packageName, op, deviceId, null, accessTime,
+ duration, uidState, opFlags, attributionFlags, attributionChainId);
+ }
+
+ @RequiresFlagsEnabled(Flags.FLAG_DEVICE_AWARE_APP_OP_NEW_SCHEMA_ENABLED)
+ @Test
+ public void externalDevice_recordAccess_persistToDisk() {
+ int uid = Process.myUid();
+ String packageName = mContext.getOpPackageName();
+ int op = AppOpsManager.OP_CAMERA;
+ String deviceId = "companion:1";
+ long accessTime = System.currentTimeMillis();
+ long duration = -1;
+ int uidState = UID_STATE_FOREGROUND_SERVICE;
+ int opFlags = OP_FLAG_SELF;
+ int attributionFlags = ATTRIBUTION_FLAG_RECEIVER;
+ int attributionChainId = 10;
+
+ mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, null, opFlags,
+ uidState, accessTime, duration, attributionFlags, attributionChainId);
+
+ fetchDiscreteOpsAndValidate(uid, packageName, op, deviceId, null, accessTime,
+ duration, uidState, opFlags, attributionFlags, attributionChainId);
+
+ mDiscreteRegistry.writeAndClearAccessHistory();
+
+ File[] files = FileUtils.listFilesOrEmpty(mMockDataDirectory);
+ assertThat(files.length).isEqualTo(1);
+ fetchDiscreteOpsAndValidate(uid, packageName, op, deviceId, null, accessTime,
+ duration, uidState, opFlags, attributionFlags, attributionChainId);
+ }
+
+ private void fetchDiscreteOpsAndValidate(int expectedUid, String expectedPackageName,
+ int expectedOp, String expectedDeviceId, String expectedAttrTag,
+ long expectedAccessTime, long expectedAccessDuration, int expectedUidState,
+ int expectedOpFlags, int expectedAttrFlags, int expectedAttrChainId) {
+ DiscreteRegistry.DiscreteOps discreteOps = mDiscreteRegistry.getAllDiscreteOps();
+
+ assertThat(discreteOps.isEmpty()).isFalse();
+ assertThat(discreteOps.mUids.size()).isEqualTo(1);
+
+ DiscreteRegistry.DiscreteUidOps discreteUidOps = discreteOps.mUids.get(expectedUid);
+ assertThat(discreteUidOps.mPackages.size()).isEqualTo(1);
+
+ DiscreteRegistry.DiscretePackageOps discretePackageOps =
+ discreteUidOps.mPackages.get(expectedPackageName);
+ assertThat(discretePackageOps.mPackageOps.size()).isEqualTo(1);
+
+ DiscreteRegistry.DiscreteOp discreteOp = discretePackageOps.mPackageOps.get(expectedOp);
+ assertThat(discreteOp.mDeviceAttributedOps.size()).isEqualTo(1);
+
+ DiscreteRegistry.DiscreteDeviceOp discreteDeviceOp =
+ discreteOp.mDeviceAttributedOps.get(expectedDeviceId);
+ assertThat(discreteDeviceOp.mAttributedOps.size()).isEqualTo(1);
+
+ List<DiscreteRegistry.DiscreteOpEvent> discreteOpEvents =
+ discreteDeviceOp.mAttributedOps.get(expectedAttrTag);
+ assertThat(discreteOpEvents.size()).isEqualTo(1);
+
+ DiscreteRegistry.DiscreteOpEvent discreteOpEvent = discreteOpEvents.get(0);
+ assertThat(discreteOpEvent.mNoteTime).isEqualTo(expectedAccessTime);
+ assertThat(discreteOpEvent.mNoteDuration).isEqualTo(expectedAccessDuration);
+ assertThat(discreteOpEvent.mUidState).isEqualTo(expectedUidState);
+ assertThat(discreteOpEvent.mOpFlag).isEqualTo(expectedOpFlags);
+ assertThat(discreteOpEvent.mAttributionFlags).isEqualTo(expectedAttrFlags);
+ assertThat(discreteOpEvent.mAttributionChainId).isEqualTo(expectedAttrChainId);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index 87b52e6..f98bbf9 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -28,6 +28,7 @@
import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_WAKE_UP_MESSAGE;
import static com.android.server.hdmi.HdmiControlService.STANDBY_SCREEN_OFF;
import static com.android.server.hdmi.HdmiControlService.WAKE_UP_SCREEN_ON;
+import static com.android.server.hdmi.RequestActiveSourceAction.TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS;
import static com.google.common.truth.Truth.assertThat;
@@ -1792,7 +1793,7 @@
mTestLooper.dispatchAll();
// Skip the LauncherX API timeout.
- mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS * 2);
+ mTestLooper.moveTimeForward(TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS);
mTestLooper.dispatchAll();
assertThat(mNativeWrapper.getResultMessages()).contains(requestActiveSource);
@@ -1825,7 +1826,7 @@
mTestLooper.dispatchAll();
// Skip the LauncherX API timeout.
- mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS * 2);
+ mTestLooper.moveTimeForward(TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS);
mTestLooper.dispatchAll();
assertThat(mNativeWrapper.getResultMessages()).contains(requestActiveSource);
@@ -1861,7 +1862,7 @@
mTestLooper.dispatchAll();
// Skip the LauncherX API timeout.
- mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS * 2);
+ mTestLooper.moveTimeForward(TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS);
mTestLooper.dispatchAll();
assertThat(mNativeWrapper.getResultMessages()).contains(requestActiveSource);
@@ -1904,7 +1905,7 @@
mTestLooper.dispatchAll();
// Skip the LauncherX API timeout.
- mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS * 2);
+ mTestLooper.moveTimeForward(TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS);
mTestLooper.dispatchAll();
assertThat(mNativeWrapper.getResultMessages()).contains(requestActiveSource);
@@ -1941,7 +1942,7 @@
mHdmiControlService.sendCecCommand(setStreamPathFromTv);
// Skip the LauncherX API timeout.
- mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS * 2);
+ mTestLooper.moveTimeForward(TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS);
mTestLooper.dispatchAll();
assertThat(mNativeWrapper.getResultMessages()).doesNotContain(requestActiveSource);
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING
deleted file mode 100644
index 874eec7..0000000
--- a/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "presubmit": [
- {
- "name": "FrameworksServicesTests",
- "options": [
- {
- "include-filter": "com.android.server.power.hint"
- },
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
- }
- ]
- }
- ]
-}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index c1f5a01..1b999e4 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -3105,6 +3105,29 @@
}
@Test
+ @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
+ public void testLifetimeExtendedCancelledOnClick() throws Exception {
+ // Adds a lifetime extended notification.
+ final NotificationRecord notif = generateNotificationRecord(mTestNotificationChannel, 1,
+ null, false);
+ notif.getSbn().getNotification().flags =
+ Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+ mService.addNotification(notif);
+ // Verify that the notification is posted and active.
+ assertThat(mBinderService.getActiveNotifications(mPkg).length).isEqualTo(1);
+
+ // Click the notification.
+ final NotificationVisibility nv = NotificationVisibility.obtain(notif.getKey(), 1, 2, true);
+ mService.mNotificationDelegate.onNotificationClick(mUid, Binder.getCallingPid(),
+ notif.getKey(), nv);
+ waitForIdle();
+
+ // The notification has been cancelled.
+ StatusBarNotification[] notifs = mBinderService.getActiveNotifications(mPkg);
+ assertThat(notifs.length).isEqualTo(0);
+ }
+
+ @Test
public void testCancelNotificationWithTag_fromApp_cannotCancelFgsChild()
throws Exception {
when(mAmi.applyForegroundServiceNotification(
diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
index 1072ef0..4a9d5c7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
@@ -156,7 +156,6 @@
window.openInputChannel(channel);
window.mHasSurface = true;
mWm.mWindowMap.put(window.mClient.asBinder(), window);
- mWm.mInputToWindowMap.put(window.mInputChannelToken, window);
return window;
}