Merge "Add ZenMode.isCustomManual()" into main
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/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 55278f6..19de793 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -117,3 +117,11 @@
description: "Enable high resolution scroll"
bug: "335160780"
}
+
+flag {
+ name: "camera_multiple_input_streams"
+ is_exported: true
+ namespace: "virtual_devices"
+ description: "Expose multiple surface for the virtual camera owner for different stream resolution"
+ bug: "341083465"
+}
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/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index f4e2a7e..62b3682 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -64,3 +64,13 @@
purpose: PURPOSE_FEATURE
}
}
+
+flag {
+ namespace: "haptics"
+ name: "throttle_vibration_params_requests"
+ description: "Control the frequency of vibration params requests to prevent overloading the vendor service"
+ bug: "355320860"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
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/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 76989f9..725d496 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -158,17 +158,6 @@
}
flag {
- name: "keyguard_appear_transition"
- namespace: "windowing_frontend"
- description: "Add transition when keyguard appears"
- bug: "327970608"
- is_fixed_read_only: true
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
name: "get_dimmer_on_closing"
namespace: "windowing_frontend"
description: "Change check for when to ignore a closing task's dim"
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/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 ede7c63d..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/volume/data/repository/AudioSharingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt
index e78b8a7..99d5891 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt
@@ -62,6 +62,9 @@
/** Whether the device is in audio sharing. */
val inAudioSharing: Flow<Boolean>
+ /** The primary headset groupId in audio sharing. */
+ val primaryGroupId: StateFlow<Int>
+
/** The secondary headset groupId in audio sharing. */
val secondaryGroupId: StateFlow<Int>
@@ -109,6 +112,16 @@
awaitClose { contentResolver.unregisterContentObserver(callback) }
}
+ override val primaryGroupId: StateFlow<Int> =
+ primaryChange
+ .map { BluetoothUtils.getPrimaryGroupIdForBroadcast(contentResolver) }
+ .onStart { emit(BluetoothUtils.getPrimaryGroupIdForBroadcast(contentResolver)) }
+ .flowOn(backgroundCoroutineContext)
+ .stateIn(
+ coroutineScope,
+ SharingStarted.WhileSubscribed(),
+ BluetoothUtils.getPrimaryGroupIdForBroadcast(contentResolver))
+
override val secondaryGroupId: StateFlow<Int> =
merge(
btManager.profileManager.leAudioBroadcastAssistantProfile
@@ -121,7 +134,7 @@
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
}
.map { getSecondaryGroupId() },
- primaryChange.map { getSecondaryGroupId() })
+ primaryGroupId.map { getSecondaryGroupId() })
.onStart { emit(getSecondaryGroupId()) }
.flowOn(backgroundCoroutineContext)
.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), getSecondaryGroupId())
@@ -193,6 +206,8 @@
class AudioSharingRepositoryEmptyImpl : AudioSharingRepository {
override val inAudioSharing: Flow<Boolean> = flowOf(false)
+ override val primaryGroupId: StateFlow<Int> =
+ MutableStateFlow(BluetoothCsipSetCoordinator.GROUP_ID_INVALID)
override val secondaryGroupId: StateFlow<Int> =
MutableStateFlow(BluetoothCsipSetCoordinator.GROUP_ID_INVALID)
override val volumeMap: StateFlow<GroupIdToVolumes> = MutableStateFlow(emptyMap())
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt
index 94595d3..c54a2e4 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt
@@ -131,6 +131,10 @@
`when`(deviceManager.findDevice(device2)).thenReturn(cachedDevice2)
`when`(receiveState.bisSyncState).thenReturn(arrayListOf(TEST_RECEIVE_STATE_CONTENT))
`when`(assistant.getAllSources(any())).thenReturn(listOf(receiveState))
+ Settings.Secure.putInt(
+ contentResolver,
+ BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
+ TEST_GROUP_ID_INVALID)
underTest =
AudioSharingRepositoryImpl(
contentResolver,
@@ -156,6 +160,22 @@
}
@Test
+ fun primaryGroupIdChange_emitValues() {
+ testScope.runTest {
+ val groupIds = mutableListOf<Int?>()
+ underTest.primaryGroupId.onEach { groupIds.add(it) }.launchIn(backgroundScope)
+ runCurrent()
+ triggerContentObserverChange()
+ runCurrent()
+
+ Truth.assertThat(groupIds)
+ .containsExactly(
+ TEST_GROUP_ID_INVALID,
+ TEST_GROUP_ID2)
+ }
+ }
+
+ @Test
fun secondaryGroupIdChange_emitValues() {
testScope.runTest {
val groupIds = mutableListOf<Int?>()
@@ -217,7 +237,7 @@
fun setSecondaryVolume_setValue() {
testScope.runTest {
Settings.Secure.putInt(
- context.contentResolver,
+ contentResolver,
BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
TEST_GROUP_ID2)
`when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2))
@@ -248,7 +268,7 @@
private fun triggerSourceAdded() {
verify(assistant).registerServiceCallBack(any(), assistantCallbackCaptor.capture())
Settings.Secure.putInt(
- context.contentResolver,
+ contentResolver,
BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
TEST_GROUP_ID1)
`when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2))
@@ -259,7 +279,7 @@
verify(assistant).registerServiceCallBack(any(), assistantCallbackCaptor.capture())
`when`(assistant.allConnectedDevices).thenReturn(listOf(device1))
Settings.Secure.putInt(
- context.contentResolver,
+ contentResolver,
BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
TEST_GROUP_ID1)
assistantCallbackCaptor.value.sourceRemoved(device2)
@@ -269,7 +289,7 @@
verify(eventManager).registerCallback(btCallbackCaptor.capture())
`when`(assistant.allConnectedDevices).thenReturn(listOf(device1))
Settings.Secure.putInt(
- context.contentResolver,
+ contentResolver,
BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
TEST_GROUP_ID1)
btCallbackCaptor.value.onProfileConnectionStateChanged(cachedDevice2, state, profile)
@@ -283,7 +303,7 @@
contentObserverCaptor.capture())
`when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2))
Settings.Secure.putInt(
- context.contentResolver,
+ contentResolver,
BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
TEST_GROUP_ID2)
contentObserverCaptor.value.primaryChanged()
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/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 3baf2f40..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/volume/domain/interactor/AudioSharingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorTest.kt
index 142631e6..a1fcfcd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorTest.kt
@@ -16,8 +16,10 @@
package com.android.systemui.volume.domain.interactor
+import android.media.AudioManager.STREAM_MUSIC
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.settingslib.volume.shared.model.AudioStream
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
@@ -40,17 +42,57 @@
@Before
fun setUp() {
- with(kosmos) { underTest = audioSharingInteractor }
+ with(kosmos) {
+ with(audioSharingRepository) { setVolumeMap(mapOf(TEST_GROUP_ID to TEST_VOLUME)) }
+ underTest = audioSharingInteractor
+ }
}
@Test
- fun volumeChanges_returnVolume() {
+ fun handlePrimaryGroupChange_nullVolume() {
with(kosmos) {
testScope.runTest {
- with(audioSharingRepository) {
- setSecondaryGroupId(TEST_GROUP_ID)
- setVolumeMap(mapOf(TEST_GROUP_ID to TEST_VOLUME))
- }
+ with(audioSharingRepository) { setPrimaryGroupId(TEST_GROUP_ID_INVALID) }
+ val preMusicStream by
+ collectLastValue(
+ audioVolumeInteractor.getAudioStream(AudioStream(STREAM_MUSIC))
+ )
+ val preVolume = preMusicStream?.volume
+ runCurrent()
+ underTest.handlePrimaryGroupChange()
+ val musicStream by
+ collectLastValue(
+ audioVolumeInteractor.getAudioStream(AudioStream(STREAM_MUSIC))
+ )
+ runCurrent()
+
+ Truth.assertThat(musicStream?.volume).isEqualTo(preVolume)
+ }
+ }
+ }
+
+ @Test
+ fun handlePrimaryGroupChange_setStreamVolume() {
+ with(kosmos) {
+ testScope.runTest {
+ with(audioSharingRepository) { setPrimaryGroupId(TEST_GROUP_ID) }
+ underTest.handlePrimaryGroupChange()
+ val musicStream by
+ collectLastValue(
+ audioVolumeInteractor.getAudioStream(AudioStream(STREAM_MUSIC))
+ )
+ runCurrent()
+
+ Truth.assertThat(musicStream?.volume).isEqualTo(TEST_MUSIC_VOLUME)
+ }
+ }
+ }
+
+ @Test
+ fun secondaryGroupVolumeChanges_returnVolume() {
+ with(kosmos) {
+ testScope.runTest {
+ with(audioSharingRepository) { setSecondaryGroupId(TEST_GROUP_ID) }
val volume by collectLastValue(underTest.volume)
runCurrent()
@@ -60,13 +102,10 @@
}
@Test
- fun volumeChanges_returnNull() {
+ fun secondaryGroupVolumeChanges_returnNull() {
with(kosmos) {
testScope.runTest {
- with(audioSharingRepository) {
- setSecondaryGroupId(TEST_GROUP_ID_INVALID)
- setVolumeMap(mapOf(TEST_GROUP_ID to TEST_VOLUME))
- }
+ with(audioSharingRepository) { setSecondaryGroupId(TEST_GROUP_ID_INVALID) }
val volume by collectLastValue(underTest.volume)
runCurrent()
@@ -76,7 +115,7 @@
}
@Test
- fun volumeChanges_returnDefaultVolume() {
+ fun secondaryGroupVolumeChanges_returnDefaultVolume() {
with(kosmos) {
testScope.runTest {
with(audioSharingRepository) {
@@ -94,7 +133,8 @@
private companion object {
const val TEST_GROUP_ID = 1
const val TEST_GROUP_ID_INVALID = -1
- const val TEST_VOLUME = 10
+ const val TEST_MUSIC_VOLUME = 10
+ const val TEST_VOLUME = 255
const val TEST_VOLUME_DEFAULT = 20
}
}
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..089db2d 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>
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/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 0e06117..32530d6 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -51,6 +51,7 @@
// Internal notification backend dependencies
crossAppPoliteNotifications dependsOn politeNotifications
vibrateWhileUnlockedToken dependsOn politeNotifications
+ modesUi dependsOn modesApi
// Internal notification frontend dependencies
NotificationsLiveDataStoreRefactor.token dependsOn NotificationIconContainerRefactor.token
@@ -85,6 +86,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..2b31f2a 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()
+ }
}
}
@@ -131,7 +142,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/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index dbfe818..abc0453 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -586,6 +586,15 @@
)
)
)
+ } else {
+ if (isLongClickable) {
+ info.addAction(
+ AccessibilityNodeInfo.AccessibilityAction(
+ AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id,
+ resources.getString(R.string.accessibility_long_click_tile)
+ )
+ )
+ }
}
if (!TextUtils.isEmpty(accessibilityClass)) {
info.className =
@@ -597,14 +606,6 @@
if (Switch::class.java.name == accessibilityClass) {
info.isChecked = tileState
info.isCheckable = true
- if (isLongClickable) {
- info.addAction(
- AccessibilityNodeInfo.AccessibilityAction(
- AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id,
- resources.getString(R.string.accessibility_long_click_tile)
- )
- )
- }
}
}
if (position != INVALID) {
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 31e91aa8..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/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index ba0a8d6..2cdcc24 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -19,7 +19,6 @@
import android.content.Context
import android.os.UserHandle
import android.util.Log
-import androidx.annotation.GuardedBy
import com.android.internal.logging.InstanceId
import com.android.systemui.Dumpable
import com.android.systemui.animation.Expandable
@@ -34,6 +33,7 @@
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.io.PrintWriter
+import java.util.concurrent.CopyOnWriteArraySet
import java.util.function.Supplier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@@ -57,10 +57,8 @@
private val context
get() = qsHost.context
- @GuardedBy("callbacks")
- private val callbacks: MutableCollection<QSTile.Callback> = mutableSetOf()
- @GuardedBy("listeningClients")
- private val listeningClients: MutableCollection<Any> = mutableSetOf()
+ private val callbacks = CopyOnWriteArraySet<QSTile.Callback>()
+ private val listeningClients = CopyOnWriteArraySet<Any>()
// Cancels the jobs when the adapter is no longer alive
private var tileAdapterJob: Job? = null
@@ -113,19 +111,17 @@
override fun addCallback(callback: QSTile.Callback?) {
callback ?: return
- synchronized(callbacks) {
- callbacks.add(callback)
- state?.let(callback::onStateChanged)
- }
+ callbacks.add(callback)
+ state?.let(callback::onStateChanged)
}
override fun removeCallback(callback: QSTile.Callback?) {
callback ?: return
- synchronized(callbacks) { callbacks.remove(callback) }
+ callbacks.remove(callback)
}
override fun removeCallbacks() {
- synchronized(callbacks) { callbacks.clear() }
+ callbacks.clear()
}
override fun click(expandable: Expandable?) {
@@ -163,32 +159,28 @@
override fun setListening(client: Any?, listening: Boolean) {
client ?: return
- synchronized(listeningClients) {
- if (listening) {
- listeningClients.add(client)
- if (listeningClients.size == 1) {
- stateJob =
- qsTileViewModel.state
- .filterNotNull()
- .map { mapState(context, it, qsTileViewModel.config) }
- .onEach { legacyState ->
- synchronized(callbacks) {
- callbacks.forEach { it.onStateChanged(legacyState) }
- }
- }
- .launchIn(applicationScope)
- }
- } else {
- listeningClients.remove(client)
- if (listeningClients.isEmpty()) {
- stateJob?.cancel()
- }
+ if (listening) {
+ listeningClients.add(client)
+ if (listeningClients.size == 1) {
+ stateJob =
+ qsTileViewModel.state
+ .filterNotNull()
+ .map { mapState(context, it, qsTileViewModel.config) }
+ .onEach { legacyState ->
+ callbacks.forEach { it.onStateChanged(legacyState) }
+ }
+ .launchIn(applicationScope)
+ }
+ } else {
+ listeningClients.remove(client)
+ if (listeningClients.isEmpty()) {
+ stateJob?.cancel()
}
}
}
override fun isListening(): Boolean =
- synchronized(listeningClients) { listeningClients.isNotEmpty() }
+ listeningClients.isNotEmpty()
override fun setDetailListening(show: Boolean) {
// do nothing like QSTileImpl
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/domain/interactor/SeenNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
index 90a05ef..2956432 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
@@ -109,7 +109,7 @@
.map {
secureSettings.getIntForUser(
name = Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
- def = 0,
+ default = 0,
userHandle = UserHandle.USER_CURRENT,
) == 1
}
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/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index e23f6ad..4bd0f22 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
@@ -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());
}
/**
@@ -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) {
@@ -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/src/com/android/systemui/util/settings/SettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
index b5934ec..9125a91 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
@@ -336,7 +336,7 @@
* @param name to look up in the table
* @return the corresponding value, or null if not present
*/
- fun getString(name: String): String
+ fun getString(name: String): String?
/**
* Store a name/value pair into the database.
@@ -385,15 +385,15 @@
* an integer.
*
* @param name The name of the setting to retrieve.
- * @param def Value to return if the setting is not defined.
- * @return The setting's current value, or 'def' if it is not defined or not a valid integer.
+ * @param default Value to return if the setting is not defined.
+ * @return The setting's current value, or default if it is not defined or not a valid integer.
*/
- fun getInt(name: String, def: Int): Int {
+ fun getInt(name: String, default: Int): Int {
val v = getString(name)
return try {
- v.toInt()
+ v?.toInt() ?: default
} catch (e: NumberFormatException) {
- def
+ default
}
}
@@ -412,7 +412,7 @@
*/
@Throws(SettingNotFoundException::class)
fun getInt(name: String): Int {
- val v = getString(name)
+ val v = getString(name) ?: throw SettingNotFoundException(name)
return try {
v.toInt()
} catch (e: NumberFormatException) {
@@ -441,11 +441,11 @@
* boolean.
*
* @param name The name of the setting to retrieve.
- * @param def Value to return if the setting is not defined.
- * @return The setting's current value, or 'def' if it is not defined or not a valid boolean.
+ * @param default Value to return if the setting is not defined.
+ * @return The setting's current value, or default if it is not defined or not a valid boolean.
*/
- fun getBool(name: String, def: Boolean): Boolean {
- return getInt(name, if (def) 1 else 0) != 0
+ fun getBool(name: String, default: Boolean): Boolean {
+ return getInt(name, if (default) 1 else 0) != 0
}
/**
@@ -579,13 +579,12 @@
companion object {
/** Convert a string to a long, or uses a default if the string is malformed or null */
@JvmStatic
- fun parseLongOrUseDefault(valString: String, def: Long): Long {
- val value: Long
- value =
+ fun parseLongOrUseDefault(valString: String?, default: Long): Long {
+ val value: Long =
try {
- valString.toLong()
+ valString?.toLong() ?: default
} catch (e: NumberFormatException) {
- def
+ default
}
return value
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
index 848a6e6..ac7c1ce 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
@@ -354,12 +354,12 @@
* @param name to look up in the table
* @return the corresponding value, or null if not present
*/
- override fun getString(name: String): String {
+ override fun getString(name: String): String? {
return getStringForUser(name, userId)
}
/** See [getString]. */
- fun getStringForUser(name: String, userHandle: Int): String
+ fun getStringForUser(name: String, userHandle: Int): String?
/**
* Store a name/value pair into the database. Values written by this method will be overridden
@@ -388,17 +388,17 @@
overrideableByRestore: Boolean
): Boolean
- override fun getInt(name: String, def: Int): Int {
- return getIntForUser(name, def, userId)
+ override fun getInt(name: String, default: Int): Int {
+ return getIntForUser(name, default, userId)
}
/** Similar implementation to [getInt] for the specified [userHandle]. */
- fun getIntForUser(name: String, def: Int, userHandle: Int): Int {
+ fun getIntForUser(name: String, default: Int, userHandle: Int): Int {
val v = getStringForUser(name, userHandle)
return try {
- v.toInt()
+ v?.toInt() ?: default
} catch (e: NumberFormatException) {
- def
+ default
}
}
@@ -408,7 +408,7 @@
/** Similar implementation to [getInt] for the specified [userHandle]. */
@Throws(SettingNotFoundException::class)
fun getIntForUser(name: String, userHandle: Int): Int {
- val v = getStringForUser(name, userHandle)
+ val v = getStringForUser(name, userHandle) ?: throw SettingNotFoundException(name)
return try {
v.toInt()
} catch (e: NumberFormatException) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
index dc2b80c..68d12f6 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
@@ -16,6 +16,8 @@
package com.android.systemui.volume;
+import static com.android.settingslib.flags.Flags.volumeDialogAudioSharingFix;
+
import android.content.Context;
import android.content.res.Configuration;
import android.os.Handler;
@@ -26,6 +28,7 @@
import com.android.systemui.qs.tiles.DndTile;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.volume.domain.interactor.AudioSharingInteractor;
import java.io.PrintWriter;
@@ -41,23 +44,29 @@
private boolean mEnabled;
private final Context mContext;
private VolumeDialogComponent mVolumeComponent;
+ private AudioSharingInteractor mAudioSharingInteractor;
@Inject
- public VolumeUI(Context context, VolumeDialogComponent volumeDialogComponent) {
+ public VolumeUI(Context context, VolumeDialogComponent volumeDialogComponent,
+ AudioSharingInteractor audioSharingInteractor) {
mContext = context;
mVolumeComponent = volumeDialogComponent;
+ mAudioSharingInteractor = audioSharingInteractor;
}
@Override
public void start() {
boolean enableVolumeUi = mContext.getResources().getBoolean(R.bool.enable_volume_ui);
boolean enableSafetyWarning =
- mContext.getResources().getBoolean(R.bool.enable_safety_warning);
+ mContext.getResources().getBoolean(R.bool.enable_safety_warning);
mEnabled = enableVolumeUi || enableSafetyWarning;
if (!mEnabled) return;
mVolumeComponent.setEnableDialogs(enableVolumeUi, enableSafetyWarning);
setDefaultVolumeController();
+ if (volumeDialogAudioSharingFix()) {
+ mAudioSharingInteractor.handlePrimaryGroupChange();
+ }
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
index 0c1bc21..efaca7a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
@@ -75,7 +75,6 @@
@Provides
@SysUISingleton
fun provideAudioSharingRepository(
- @Application context: Context,
contentResolver: ContentResolver,
localBluetoothManager: LocalBluetoothManager?,
@Application coroutineScope: CoroutineScope,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt
index aba3015..2170c36 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt
@@ -17,18 +17,28 @@
package com.android.systemui.volume.domain.interactor
import android.bluetooth.BluetoothCsipSetCoordinator
+import android.media.AudioManager.STREAM_MUSIC
import androidx.annotation.IntRange
import com.android.settingslib.volume.data.repository.AudioSharingRepository
import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MAX
import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MIN
+import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor
+import com.android.settingslib.volume.shared.model.AudioStream
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
interface AudioSharingInteractor {
/** Audio sharing secondary headset volume changes. */
@@ -45,6 +55,16 @@
@IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong())
level: Int
)
+
+ /**
+ * Handle primary group change in audio sharing.
+ *
+ * Once the primary group is changed, we need to sync its volume to STREAM_MUSIC to make sure
+ * the volume adjustment during audio sharing can be kept after the sharing ends.
+ *
+ * TODO(b/355396988) Migrate to audio framework solution once it is in place.
+ */
+ fun handlePrimaryGroupChange()
}
@SysUISingleton
@@ -52,26 +72,60 @@
@Inject
constructor(
@Application private val coroutineScope: CoroutineScope,
+ @Background private val backgroundCoroutineContext: CoroutineContext,
+ private val audioVolumeInteractor: AudioVolumeInteractor,
private val audioSharingRepository: AudioSharingRepository
) : AudioSharingInteractor {
override val volume: Flow<Int?> =
combine(audioSharingRepository.secondaryGroupId, audioSharingRepository.volumeMap) {
- secondaryGroupId,
- volumeMap ->
- if (secondaryGroupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) null
- else volumeMap.getOrDefault(secondaryGroupId, DEFAULT_VOLUME)
- }
+ secondaryGroupId,
+ volumeMap ->
+ if (secondaryGroupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) null
+ else volumeMap.getOrDefault(secondaryGroupId, DEFAULT_VOLUME)
+ }
+ .distinctUntilChanged()
override val volumeMin: Int = AUDIO_SHARING_VOLUME_MIN
override val volumeMax: Int = AUDIO_SHARING_VOLUME_MAX
- override fun setStreamVolume(level: Int) {
+ override fun setStreamVolume(
+ @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong())
+ level: Int
+ ) {
coroutineScope.launch { audioSharingRepository.setSecondaryVolume(level) }
}
+ override fun handlePrimaryGroupChange() {
+ coroutineScope.launch {
+ audioSharingRepository.primaryGroupId
+ .map { primaryGroupId -> audioSharingRepository.volumeMap.value[primaryGroupId] }
+ .filterNotNull()
+ .distinctUntilChanged()
+ .collect {
+ // Once primary device change, we need to update the STREAM_MUSIC volume to get
+ // align with the primary device's volume
+ setMusicStreamVolume(it)
+ }
+ }
+ }
+
+ private suspend fun setMusicStreamVolume(volume: Int) {
+ withContext(backgroundCoroutineContext) {
+ val musicStream =
+ audioVolumeInteractor.getAudioStream(AudioStream(STREAM_MUSIC)).first()
+ val musicVolume =
+ Math.round(
+ volume.toFloat() * (musicStream.maxVolume - musicStream.minVolume) /
+ (AUDIO_SHARING_VOLUME_MAX - AUDIO_SHARING_VOLUME_MIN)
+ )
+ audioVolumeInteractor.setVolume(AudioStream(STREAM_MUSIC), musicVolume)
+ }
+ }
+
private companion object {
+ const val TAG = "AudioSharingInteractor"
const val DEFAULT_VOLUME = 20
}
}
@@ -82,7 +136,12 @@
override val volumeMin: Int = EMPTY_VOLUME
override val volumeMax: Int = EMPTY_VOLUME
- override fun setStreamVolume(level: Int) {}
+ override fun setStreamVolume(
+ @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong())
+ level: Int
+ ) {}
+
+ override fun handlePrimaryGroupChange() {}
private companion object {
const val EMPTY_VOLUME = 0
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
index 988769f..90ffaf1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
@@ -25,6 +25,7 @@
import android.view.ContextThemeWrapper
import android.view.View
import android.view.accessibility.AccessibilityNodeInfo
+import android.widget.Button
import android.widget.TextView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -381,6 +382,47 @@
}
@Test
+ fun testNonSwitchA11yClass_longClickActionHasCorrectLabel() {
+ val state =
+ QSTile.State().apply {
+ expandedAccessibilityClassName = Button::class.java.name
+ handlesLongClick = true
+ }
+ tileView.changeState(state)
+ val info = AccessibilityNodeInfo(tileView)
+ tileView.onInitializeAccessibilityNodeInfo(info)
+
+ assertThat(
+ info.actionList
+ .find {
+ it.id == AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id
+ }
+ ?.label
+ )
+ .isEqualTo(context.getString(R.string.accessibility_long_click_tile))
+ }
+
+ @Test
+ fun testNonSwitchA11yClass_disabledByPolicy_noLongClickAction() {
+ val state =
+ QSTile.State().apply {
+ expandedAccessibilityClassName = Button::class.java.name
+ handlesLongClick = true
+ disabledByPolicy = true
+ }
+ tileView.changeState(state)
+ val info = AccessibilityNodeInfo(tileView)
+ tileView.onInitializeAccessibilityNodeInfo(info)
+
+ assertThat(
+ info.actionList.find {
+ it.id == AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id
+ }
+ )
+ .isNull()
+ }
+
+ @Test
fun onStateChange_longPressEffectActive_withInvalidDuration_doesNotInitializeEffect() {
val state = QSTile.State() // A state that handles longPress
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/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/util/settings/SettingsProxyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt
index 5ac6110..b0acd03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt
@@ -275,6 +275,18 @@
}
@Test
+ fun getInt_keyMalformed_returnDefaultValue() {
+ mSettings.putString(TEST_SETTING, "nan")
+ assertThat(mSettings.getInt(TEST_SETTING, 5)).isEqualTo(5)
+ }
+
+ @Test
+ fun getInt_keyMalformed_throwException() {
+ mSettings.putString(TEST_SETTING, "nan")
+ assertThrows(SettingNotFoundException::class.java) { mSettings.getInt(TEST_SETTING) }
+ }
+
+ @Test
fun getBool_keyPresent_returnValidValue() {
mSettings.putBool(TEST_SETTING, true)
assertThat(mSettings.getBool(TEST_SETTING)).isTrue()
@@ -323,6 +335,18 @@
}
@Test
+ fun getLong_keyMalformed_throwException() {
+ mSettings.putString(TEST_SETTING, "nan")
+ assertThrows(SettingNotFoundException::class.java) { mSettings.getLong(TEST_SETTING) }
+ }
+
+ @Test
+ fun getLong_keyMalformed_returnDefaultValue() {
+ mSettings.putString(TEST_SETTING, "nan")
+ assertThat(mSettings.getLong(TEST_SETTING, 2L)).isEqualTo(2L)
+ }
+
+ @Test
fun getFloat_keyPresent_returnValidValue() {
mSettings.putFloat(TEST_SETTING, 2.5F)
assertThat(mSettings.getFloat(TEST_SETTING)).isEqualTo(2.5F)
@@ -346,6 +370,18 @@
assertThat(mSettings.getFloat(TEST_SETTING, 2.5F)).isEqualTo(2.5F)
}
+ @Test
+ fun getFloat_keyMalformed_throwException() {
+ mSettings.putString(TEST_SETTING, "nan")
+ assertThrows(SettingNotFoundException::class.java) { mSettings.getFloat(TEST_SETTING) }
+ }
+
+ @Test
+ fun getFloat_keyMalformed_returnDefaultValue() {
+ mSettings.putString(TEST_SETTING, "nan")
+ assertThat(mSettings.getFloat(TEST_SETTING, 2.5F)).isEqualTo(2.5F)
+ }
+
private class FakeSettingsProxy(val testDispatcher: CoroutineDispatcher) : SettingsProxy {
private val mContentResolver = mock(ContentResolver::class.java)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
index 5f7420d..ead9939 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
@@ -23,6 +23,7 @@
import android.os.Handler
import android.os.Looper
import android.provider.Settings
+import android.provider.Settings.SettingNotFoundException
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -433,6 +434,18 @@
}
@Test
+ fun getInt_keyMalformed_returnDefaultValue() {
+ mSettings.putString(TEST_SETTING, "nan")
+ assertThat(mSettings.getInt(TEST_SETTING, 5)).isEqualTo(5)
+ }
+
+ @Test
+ fun getInt_keyMalformed_throwException() {
+ mSettings.putString(TEST_SETTING, "nan")
+ assertThrows(SettingNotFoundException::class.java) { mSettings.getInt(TEST_SETTING) }
+ }
+
+ @Test
fun getIntForUser_multipleUsers__validResult() {
mSettings.putIntForUser(TEST_SETTING, 1, MAIN_USER_ID)
mSettings.putIntForUser(TEST_SETTING, 2, SECONDARY_USER_ID)
@@ -501,6 +514,18 @@
}
@Test
+ fun getLong_keyMalformed_throwException() {
+ mSettings.putString(TEST_SETTING, "nan")
+ assertThrows(SettingNotFoundException::class.java) { mSettings.getLong(TEST_SETTING) }
+ }
+
+ @Test
+ fun getLong_keyMalformed_returnDefaultValue() {
+ mSettings.putString(TEST_SETTING, "nan")
+ assertThat(mSettings.getLong(TEST_SETTING, 2L)).isEqualTo(2L)
+ }
+
+ @Test
fun getLongForUser_multipleUsers__validResult() {
mSettings.putLongForUser(TEST_SETTING, 1L, MAIN_USER_ID)
mSettings.putLongForUser(TEST_SETTING, 2L, SECONDARY_USER_ID)
@@ -535,6 +560,18 @@
}
@Test
+ fun getFloat_keyMalformed_throwException() {
+ mSettings.putString(TEST_SETTING, "nan")
+ assertThrows(SettingNotFoundException::class.java) { mSettings.getFloat(TEST_SETTING) }
+ }
+
+ @Test
+ fun getFloat_keyMalformed_returnDefaultValue() {
+ mSettings.putString(TEST_SETTING, "nan")
+ assertThat(mSettings.getFloat(TEST_SETTING, 2.5F)).isEqualTo(2.5F)
+ }
+
+ @Test
fun getFloatForUser_multipleUsers__validResult() {
mSettings.putFloatForUser(TEST_SETTING, 1F, MAIN_USER_ID)
mSettings.putFloatForUser(TEST_SETTING, 2F, SECONDARY_USER_ID)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
index 3f5dc82..8b7d921 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
@@ -252,7 +252,6 @@
}
@Test
- @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
public void handleAudioSharingStreamVolumeChanges_updateState() {
ArgumentCaptor<VolumeDialogController.State> stateCaptor =
ArgumentCaptor.forClass(VolumeDialogController.State.class);
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/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/util/settings/FakeGlobalSettings.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettings.java
index 476b7d8..65f4122 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettings.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettings.java
@@ -38,6 +38,11 @@
public static final Uri CONTENT_URI = Uri.parse("content://settings/fake_global");
+ /**
+ * @deprecated Please use FakeGlobalSettings(testDispatcher) to provide the same dispatcher used
+ * by main test scope.
+ */
+ @Deprecated
public FakeGlobalSettings() {
mDispatcher = StandardTestDispatcher(/* scheduler = */ null, /* name = */ null);
}
@@ -46,6 +51,7 @@
mDispatcher = dispatcher;
}
+ @NonNull
@Override
public ContentResolver getContentResolver() {
throw new UnsupportedOperationException(
@@ -53,6 +59,7 @@
+ "GlobalSettings.registerContentObserver helpful instead.");
}
+ @NonNull
@Override
public CoroutineDispatcher getBackgroundDispatcher() {
return mDispatcher;
@@ -60,7 +67,7 @@
@Override
public void registerContentObserverSync(Uri uri, boolean notifyDescendants,
- ContentObserver settingsObserver) {
+ @NonNull ContentObserver settingsObserver) {
List<ContentObserver> observers;
mContentObserversAllUsers.putIfAbsent(uri.toString(), new ArrayList<>());
observers = mContentObserversAllUsers.get(uri.toString());
@@ -68,25 +75,26 @@
}
@Override
- public void unregisterContentObserverSync(ContentObserver settingsObserver) {
+ public void unregisterContentObserverSync(@NonNull ContentObserver settingsObserver) {
for (Map.Entry<String, List<ContentObserver>> entry :
mContentObserversAllUsers.entrySet()) {
entry.getValue().remove(settingsObserver);
}
}
+ @NonNull
@Override
- public Uri getUriFor(String name) {
+ public Uri getUriFor(@NonNull String name) {
return Uri.withAppendedPath(CONTENT_URI, name);
}
@Override
- public String getString(String name) {
+ public String getString(@NonNull String name) {
return mValues.get(getUriFor(name).toString());
}
@Override
- public boolean putString(String name, String value) {
+ public boolean putString(@NonNull String name, @NonNull String value) {
return putString(name, value, null, false);
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettingsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettingsKosmos.kt
index df6fc41..35fa2af 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettingsKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettingsKosmos.kt
@@ -18,5 +18,6 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testDispatcher
-val Kosmos.fakeGlobalSettings: FakeGlobalSettings by Fixture { FakeGlobalSettings() }
+val Kosmos.fakeGlobalSettings: FakeGlobalSettings by Fixture { FakeGlobalSettings(testDispatcher) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java
index e35da11..3f0318b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java
@@ -26,7 +26,9 @@
import android.util.Pair;
import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import com.android.systemui.settings.FakeUserTracker;
import com.android.systemui.settings.UserTracker;
import kotlinx.coroutines.CoroutineDispatcher;
@@ -42,49 +44,68 @@
new HashMap<>();
private final Map<String, List<ContentObserver>> mContentObserversAllUsers = new HashMap<>();
private final CoroutineDispatcher mDispatcher;
+ private final UserTracker mUserTracker;
public static final Uri CONTENT_URI = Uri.parse("content://settings/fake");
@UserIdInt
private int mUserId = UserHandle.USER_CURRENT;
+ /**
+ * @deprecated Please use FakeSettings(testDispatcher) to provide the same dispatcher used
+ * by main test scope.
+ */
+ @Deprecated
public FakeSettings() {
mDispatcher = StandardTestDispatcher(/* scheduler = */ null, /* name = */ null);
+ mUserTracker = new FakeUserTracker();
}
public FakeSettings(CoroutineDispatcher dispatcher) {
mDispatcher = dispatcher;
+ mUserTracker = new FakeUserTracker();
}
- public FakeSettings(String initialKey, String initialValue) {
- mDispatcher = StandardTestDispatcher(/* scheduler = */ null, /* name = */ null);
+ public FakeSettings(CoroutineDispatcher dispatcher, UserTracker userTracker) {
+ mDispatcher = dispatcher;
+ mUserTracker = userTracker;
+ }
+
+ @VisibleForTesting
+ FakeSettings(String initialKey, String initialValue) {
+ this();
putString(initialKey, initialValue);
}
- public FakeSettings(Map<String, String> initialValues) {
- mDispatcher = StandardTestDispatcher(/* scheduler = */ null, /* name = */ null);
+ @VisibleForTesting
+ FakeSettings(Map<String, String> initialValues) {
+ this();
for (Map.Entry<String, String> kv : initialValues.entrySet()) {
putString(kv.getKey(), kv.getValue());
}
}
@Override
+ @NonNull
public ContentResolver getContentResolver() {
- return null;
+ throw new UnsupportedOperationException(
+ "FakeSettings.getContentResolver is not implemented");
}
+ @NonNull
@Override
public UserTracker getUserTracker() {
- return null;
+ return mUserTracker;
}
+ @NonNull
@Override
public CoroutineDispatcher getBackgroundDispatcher() {
return mDispatcher;
}
@Override
- public void registerContentObserverForUserSync(Uri uri, boolean notifyDescendants,
- ContentObserver settingsObserver, int userHandle) {
+ public void registerContentObserverForUserSync(@NonNull Uri uri, boolean notifyDescendants,
+ @NonNull ContentObserver settingsObserver, int userHandle) {
List<ContentObserver> observers;
if (userHandle == UserHandle.USER_ALL) {
mContentObserversAllUsers.putIfAbsent(uri.toString(), new ArrayList<>());
@@ -98,19 +119,18 @@
}
@Override
- public void unregisterContentObserverSync(ContentObserver settingsObserver) {
- for (SettingsKey key : mContentObservers.keySet()) {
- List<ContentObserver> observers = mContentObservers.get(key);
+ public void unregisterContentObserverSync(@NonNull ContentObserver settingsObserver) {
+ for (List<ContentObserver> observers : mContentObservers.values()) {
observers.remove(settingsObserver);
}
- for (String key : mContentObserversAllUsers.keySet()) {
- List<ContentObserver> observers = mContentObserversAllUsers.get(key);
+ for (List<ContentObserver> observers : mContentObserversAllUsers.values()) {
observers.remove(settingsObserver);
}
}
+ @NonNull
@Override
- public Uri getUriFor(String name) {
+ public Uri getUriFor(@NonNull String name) {
return Uri.withAppendedPath(CONTENT_URI, name);
}
@@ -124,33 +144,34 @@
}
@Override
- public String getString(String name) {
+ public String getString(@NonNull String name) {
return getStringForUser(name, getUserId());
}
@Override
- public String getStringForUser(String name, int userHandle) {
+ public String getStringForUser(@NonNull String name, int userHandle) {
return mValues.get(new SettingsKey(userHandle, getUriFor(name).toString()));
}
@Override
- public boolean putString(String name, String value, boolean overrideableByRestore) {
+ public boolean putString(@NonNull String name, @NonNull String value,
+ boolean overrideableByRestore) {
return putStringForUser(name, value, null, false, getUserId(), overrideableByRestore);
}
@Override
- public boolean putString(String name, String value) {
+ public boolean putString(@NonNull String name, @NonNull String value) {
return putString(name, value, false);
}
@Override
- public boolean putStringForUser(String name, String value, int userHandle) {
+ public boolean putStringForUser(@NonNull String name, @NonNull String value, int userHandle) {
return putStringForUser(name, value, null, false, userHandle, false);
}
@Override
- public boolean putStringForUser(String name, String value, String tag, boolean makeDefault,
- int userHandle, boolean overrideableByRestore) {
+ public boolean putStringForUser(@NonNull String name, @NonNull String value, String tag,
+ boolean makeDefault, int userHandle, boolean overrideableByRestore) {
SettingsKey key = new SettingsKey(userHandle, getUriFor(name).toString());
mValues.put(key, value);
@@ -166,7 +187,8 @@
}
@Override
- public boolean putString(@NonNull String name, String value, String tag, boolean makeDefault) {
+ public boolean putString(@NonNull String name, @NonNull String value, @NonNull String tag,
+ boolean makeDefault) {
return putString(name, value);
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt
index bcb5848..55044bf 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt
@@ -18,5 +18,7 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.settings.fakeUserTracker
-val Kosmos.fakeSettings: FakeSettings by Fixture { FakeSettings() }
+val Kosmos.fakeSettings: FakeSettings by Fixture { FakeSettings(testDispatcher, fakeUserTracker) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt
index d391750..0a617d1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt
@@ -16,10 +16,7 @@
package com.android.systemui.volume.data.repository
-import androidx.annotation.IntRange
import com.android.settingslib.volume.data.repository.AudioSharingRepository
-import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MAX
-import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MIN
import com.android.settingslib.volume.data.repository.GroupIdToVolumes
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -27,11 +24,14 @@
class FakeAudioSharingRepository : AudioSharingRepository {
private val mutableInAudioSharing: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ private val mutablePrimaryGroupId: MutableStateFlow<Int> =
+ MutableStateFlow(TEST_GROUP_ID_INVALID)
private val mutableSecondaryGroupId: MutableStateFlow<Int> =
MutableStateFlow(TEST_GROUP_ID_INVALID)
private val mutableVolumeMap: MutableStateFlow<GroupIdToVolumes> = MutableStateFlow(emptyMap())
override val inAudioSharing: Flow<Boolean> = mutableInAudioSharing
+ override val primaryGroupId: StateFlow<Int> = mutablePrimaryGroupId
override val secondaryGroupId: StateFlow<Int> = mutableSecondaryGroupId
override val volumeMap: StateFlow<GroupIdToVolumes> = mutableVolumeMap
@@ -41,6 +41,10 @@
mutableInAudioSharing.value = state
}
+ fun setPrimaryGroupId(groupId: Int) {
+ mutablePrimaryGroupId.value = groupId
+ }
+
fun setSecondaryGroupId(groupId: Int) {
mutableSecondaryGroupId.value = groupId
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorKosmos.kt
index 03981bb..ce8aba5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorKosmos.kt
@@ -18,12 +18,15 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.backgroundCoroutineContext
import com.android.systemui.volume.data.repository.audioSharingRepository
val Kosmos.audioSharingInteractor by
Kosmos.Fixture {
AudioSharingInteractorImpl(
applicationCoroutineScope,
+ backgroundCoroutineContext,
+ audioVolumeInteractor,
audioSharingRepository,
)
}
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 8d89cc1..cc8ccc3 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -330,6 +330,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/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/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/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index effecd4..cd23872 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -474,6 +474,9 @@
IInputMethodSession mSession;
InputChannel mChannel;
+ @UserIdInt
+ final int mUserId;
+
@Override
public String toString() {
return "SessionState{uid=" + mClient.mUid + " pid=" + mClient.mPid
@@ -482,15 +485,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 +2354,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 +2495,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);
@@ -4427,7 +4430,7 @@
@IInputMethodManagerImpl.PermissionVerified(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
@Override
- public void removeImeSurface() {
+ public void removeImeSurface(int displayId) {
mHandler.obtainMessage(MSG_REMOVE_IME_SURFACE).sendToTarget();
}
@@ -5069,10 +5072,11 @@
// ---------------------------------------------------------
- case MSG_HIDE_ALL_INPUT_METHODS:
+ case MSG_HIDE_ALL_INPUT_METHODS: {
+ @SoftInputShowHideReason final int reason = msg.arg1;
+ final int originatingDisplayId = msg.arg2;
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 +5084,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 +5811,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/power/hint/TEST_MAPPING b/services/core/java/com/android/server/power/hint/TEST_MAPPING
index 34c25c6e..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/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index bca81f52..c21f783 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -64,6 +64,7 @@
import static com.android.internal.util.FrameworkStatsLog.TIME_ZONE_DETECTOR_STATE__DETECTION_MODE__UNKNOWN;
import static com.android.server.am.MemoryStatUtil.readMemoryStatFromFilesystem;
import static com.android.server.stats.Flags.addMobileBytesTransferByProcStatePuller;
+import static com.android.server.stats.Flags.applyNetworkStatsPollRateLimit;
import static com.android.server.stats.pull.IonMemoryUtil.readProcessSystemIonHeapSizesFromDebugfs;
import static com.android.server.stats.pull.IonMemoryUtil.readSystemIonHeapSizeFromDebugfs;
import static com.android.server.stats.pull.ProcfsMemoryUtil.getProcessCmdlines;
@@ -298,6 +299,13 @@
*/
private static final long NETSTATS_UID_DEFAULT_BUCKET_DURATION_MS = HOURS.toMillis(2);
+ /**
+ * Polling NetworkStats is a heavy operation and it should be done sparingly. Atom pulls may
+ * happen in bursts, but these should be infrequent. The poll rate limit ensures that data is
+ * sufficiently fresh (i.e. not stale) while reducing system load during atom pull bursts.
+ */
+ private static final long NETSTATS_POLL_RATE_LIMIT_MS = 15000;
+
private static final int CPU_TIME_PER_THREAD_FREQ_MAX_NUM_FREQUENCIES = 8;
private static final int OP_FLAGS_PULLED = OP_FLAG_SELF | OP_FLAG_TRUSTED_PROXIED;
private static final String COMMON_PERMISSION_PREFIX = "android.permission.";
@@ -415,6 +423,9 @@
@GuardedBy("mDataBytesTransferLock")
private final ArrayList<NetworkStatsExt> mNetworkStatsBaselines = new ArrayList<>();
+ @GuardedBy("mDataBytesTransferLock")
+ private long mLastNetworkStatsPollTime = -NETSTATS_POLL_RATE_LIMIT_MS;
+
// Listener for monitoring subscriptions changed event.
private StatsSubscriptionsListener mStatsSubscriptionsListener;
// List that stores SubInfo of subscriptions that ever appeared since boot.
@@ -1063,24 +1074,26 @@
// Initialize NetworkStats baselines.
synchronized (mDataBytesTransferLock) {
mNetworkStatsBaselines.addAll(
- collectNetworkStatsSnapshotForAtom(FrameworkStatsLog.WIFI_BYTES_TRANSFER));
+ collectNetworkStatsSnapshotForAtomLocked(
+ FrameworkStatsLog.WIFI_BYTES_TRANSFER));
mNetworkStatsBaselines.addAll(
- collectNetworkStatsSnapshotForAtom(
+ collectNetworkStatsSnapshotForAtomLocked(
FrameworkStatsLog.WIFI_BYTES_TRANSFER_BY_FG_BG));
mNetworkStatsBaselines.addAll(
- collectNetworkStatsSnapshotForAtom(FrameworkStatsLog.MOBILE_BYTES_TRANSFER));
- mNetworkStatsBaselines.addAll(collectNetworkStatsSnapshotForAtom(
+ collectNetworkStatsSnapshotForAtomLocked(
+ FrameworkStatsLog.MOBILE_BYTES_TRANSFER));
+ mNetworkStatsBaselines.addAll(collectNetworkStatsSnapshotForAtomLocked(
FrameworkStatsLog.MOBILE_BYTES_TRANSFER_BY_FG_BG));
- mNetworkStatsBaselines.addAll(collectNetworkStatsSnapshotForAtom(
+ mNetworkStatsBaselines.addAll(collectNetworkStatsSnapshotForAtomLocked(
FrameworkStatsLog.BYTES_TRANSFER_BY_TAG_AND_METERED));
mNetworkStatsBaselines.addAll(
- collectNetworkStatsSnapshotForAtom(
+ collectNetworkStatsSnapshotForAtomLocked(
FrameworkStatsLog.DATA_USAGE_BYTES_TRANSFER));
mNetworkStatsBaselines.addAll(
- collectNetworkStatsSnapshotForAtom(
+ collectNetworkStatsSnapshotForAtomLocked(
FrameworkStatsLog.OEM_MANAGED_BYTES_TRANSFER));
if (canQueryTypeProxy) {
- mNetworkStatsBaselines.addAll(collectNetworkStatsSnapshotForAtom(
+ mNetworkStatsBaselines.addAll(collectNetworkStatsSnapshotForAtomLocked(
FrameworkStatsLog.PROXY_BYTES_TRANSFER_BY_FG_BG));
}
}
@@ -1243,12 +1256,14 @@
);
}
+ @GuardedBy("mDataBytesTransferLock")
@NonNull
- private List<NetworkStatsExt> collectNetworkStatsSnapshotForAtom(int atomTag) {
+ private List<NetworkStatsExt> collectNetworkStatsSnapshotForAtomLocked(int atomTag) {
List<NetworkStatsExt> ret = new ArrayList<>();
switch (atomTag) {
case FrameworkStatsLog.WIFI_BYTES_TRANSFER: {
- final NetworkStats stats = getUidNetworkStatsSnapshotForTransport(TRANSPORT_WIFI);
+ final NetworkStats stats = getUidNetworkStatsSnapshotForTransportLocked(
+ TRANSPORT_WIFI);
if (stats != null) {
ret.add(new NetworkStatsExt(sliceNetworkStatsByUid(stats),
new int[]{TRANSPORT_WIFI}, /*slicedByFgbg=*/false));
@@ -1256,7 +1271,8 @@
break;
}
case FrameworkStatsLog.WIFI_BYTES_TRANSFER_BY_FG_BG: {
- final NetworkStats stats = getUidNetworkStatsSnapshotForTransport(TRANSPORT_WIFI);
+ final NetworkStats stats = getUidNetworkStatsSnapshotForTransportLocked(
+ TRANSPORT_WIFI);
if (stats != null) {
ret.add(new NetworkStatsExt(sliceNetworkStatsByUidAndFgbg(stats),
new int[]{TRANSPORT_WIFI}, /*slicedByFgbg=*/true));
@@ -1265,7 +1281,7 @@
}
case FrameworkStatsLog.MOBILE_BYTES_TRANSFER: {
final NetworkStats stats =
- getUidNetworkStatsSnapshotForTransport(TRANSPORT_CELLULAR);
+ getUidNetworkStatsSnapshotForTransportLocked(TRANSPORT_CELLULAR);
if (stats != null) {
ret.add(new NetworkStatsExt(sliceNetworkStatsByUid(stats),
new int[]{TRANSPORT_CELLULAR}, /*slicedByFgbg=*/false));
@@ -1274,7 +1290,7 @@
}
case FrameworkStatsLog.MOBILE_BYTES_TRANSFER_BY_FG_BG: {
final NetworkStats stats =
- getUidNetworkStatsSnapshotForTransport(TRANSPORT_CELLULAR);
+ getUidNetworkStatsSnapshotForTransportLocked(TRANSPORT_CELLULAR);
if (stats != null) {
ret.add(new NetworkStatsExt(sliceNetworkStatsByUidAndFgbg(stats),
new int[]{TRANSPORT_CELLULAR}, /*slicedByFgbg=*/true));
@@ -1282,7 +1298,7 @@
break;
}
case FrameworkStatsLog.PROXY_BYTES_TRANSFER_BY_FG_BG: {
- final NetworkStats stats = getUidNetworkStatsSnapshotForTemplate(
+ final NetworkStats stats = getUidNetworkStatsSnapshotForTemplateLocked(
new NetworkTemplate.Builder(MATCH_PROXY).build(), /*includeTags=*/false);
if (stats != null) {
ret.add(new NetworkStatsExt(sliceNetworkStatsByUidTagAndMetered(stats),
@@ -1294,9 +1310,9 @@
break;
}
case FrameworkStatsLog.BYTES_TRANSFER_BY_TAG_AND_METERED: {
- final NetworkStats wifiStats = getUidNetworkStatsSnapshotForTemplate(
+ final NetworkStats wifiStats = getUidNetworkStatsSnapshotForTemplateLocked(
new NetworkTemplate.Builder(MATCH_WIFI).build(), /*includeTags=*/true);
- final NetworkStats cellularStats = getUidNetworkStatsSnapshotForTemplate(
+ final NetworkStats cellularStats = getUidNetworkStatsSnapshotForTemplateLocked(
new NetworkTemplate.Builder(MATCH_MOBILE)
.setMeteredness(METERED_YES).build(), /*includeTags=*/true);
if (wifiStats != null && cellularStats != null) {
@@ -1311,12 +1327,12 @@
}
case FrameworkStatsLog.DATA_USAGE_BYTES_TRANSFER: {
for (final SubInfo subInfo : mHistoricalSubs) {
- ret.addAll(getDataUsageBytesTransferSnapshotForSub(subInfo));
+ ret.addAll(getDataUsageBytesTransferSnapshotForSubLocked(subInfo));
}
break;
}
case FrameworkStatsLog.OEM_MANAGED_BYTES_TRANSFER: {
- ret.addAll(getDataUsageBytesTransferSnapshotForOemManaged());
+ ret.addAll(getDataUsageBytesTransferSnapshotForOemManagedLocked());
break;
}
default:
@@ -1325,8 +1341,9 @@
return ret;
}
+ @GuardedBy("mDataBytesTransferLock")
private int pullDataBytesTransferLocked(int atomTag, @NonNull List<StatsEvent> pulledData) {
- final List<NetworkStatsExt> current = collectNetworkStatsSnapshotForAtom(atomTag);
+ final List<NetworkStatsExt> current = collectNetworkStatsSnapshotForAtomLocked(atomTag);
if (current == null) {
Slog.e(TAG, "current snapshot is null for " + atomTag + ", return.");
@@ -1459,8 +1476,9 @@
}
}
+ @GuardedBy("mDataBytesTransferLock")
@NonNull
- private List<NetworkStatsExt> getDataUsageBytesTransferSnapshotForOemManaged() {
+ private List<NetworkStatsExt> getDataUsageBytesTransferSnapshotForOemManagedLocked() {
final List<Pair<Integer, Integer>> matchRulesAndTransports = List.of(
new Pair(MATCH_ETHERNET, TRANSPORT_ETHERNET),
new Pair(MATCH_MOBILE, TRANSPORT_CELLULAR),
@@ -1479,7 +1497,8 @@
// Thus, specifying networks through their identifiers are not needed.
final NetworkTemplate template = new NetworkTemplate.Builder(matchRule)
.setOemManaged(oemManaged).build();
- final NetworkStats stats = getUidNetworkStatsSnapshotForTemplate(template, false);
+ final NetworkStats stats = getUidNetworkStatsSnapshotForTemplateLocked(
+ template, false);
final Integer transport = ruleAndTransport.second;
if (stats != null) {
ret.add(new NetworkStatsExt(sliceNetworkStatsByUidAndFgbg(stats),
@@ -1496,8 +1515,9 @@
/**
* Create a snapshot of NetworkStats for a given transport.
*/
+ @GuardedBy("mDataBytesTransferLock")
@Nullable
- private NetworkStats getUidNetworkStatsSnapshotForTransport(int transport) {
+ private NetworkStats getUidNetworkStatsSnapshotForTransportLocked(int transport) {
NetworkTemplate template = null;
switch (transport) {
case TRANSPORT_CELLULAR:
@@ -1510,7 +1530,7 @@
default:
Log.wtf(TAG, "Unexpected transport.");
}
- return getUidNetworkStatsSnapshotForTemplate(template, /*includeTags=*/false);
+ return getUidNetworkStatsSnapshotForTemplateLocked(template, /*includeTags=*/false);
}
/**
@@ -1534,8 +1554,9 @@
* Note that this should be only used to calculate diff since the snapshot might contains
* some traffic before boot.
*/
+ @GuardedBy("mDataBytesTransferLock")
@Nullable
- private NetworkStats getUidNetworkStatsSnapshotForTemplate(
+ private NetworkStats getUidNetworkStatsSnapshotForTemplateLocked(
@NonNull NetworkTemplate template, boolean includeTags) {
final long elapsedMillisSinceBoot = SystemClock.elapsedRealtime();
final long currentTimeInMillis = MICROSECONDS.toMillis(SystemClock.currentTimeMicro());
@@ -1547,13 +1568,19 @@
final long startTime = currentTimeInMillis - elapsedMillisSinceBoot - bucketDuration;
final long endTime = currentTimeInMillis + bucketDuration;
- // TODO (b/156313635): This is short-term hack to allow perfd gets updated networkStats
- // history when query in every second in order to show realtime statistics. However,
- // this is not a good long-term solution since NetworkStatsService will make frequent
- // I/O and also block main thread when polling.
- // Consider making perfd queries NetworkStatsService directly.
- if (template.getMatchRule() == MATCH_WIFI && template.getSubscriberIds().isEmpty()) {
- getNetworkStatsManager().forceUpdate();
+ // NetworkStatsManager#forceUpdate updates stats for all networks
+ if (applyNetworkStatsPollRateLimit()) {
+ // The new way: rate-limit force-polling for all NetworkStats queries
+ if (elapsedMillisSinceBoot - mLastNetworkStatsPollTime >= NETSTATS_POLL_RATE_LIMIT_MS) {
+ mLastNetworkStatsPollTime = elapsedMillisSinceBoot;
+ getNetworkStatsManager().forceUpdate();
+ }
+ } else {
+ // The old way: force-poll only on WiFi queries. Data for other queries can be stale
+ // if there was no recent poll beforehand (e.g. for WiFi or scheduled poll)
+ if (template.getMatchRule() == MATCH_WIFI && template.getSubscriberIds().isEmpty()) {
+ getNetworkStatsManager().forceUpdate();
+ }
}
final android.app.usage.NetworkStats queryNonTaggedStats =
@@ -1572,8 +1599,9 @@
return nonTaggedStats.add(taggedStats);
}
+ @GuardedBy("mDataBytesTransferLock")
@NonNull
- private List<NetworkStatsExt> getDataUsageBytesTransferSnapshotForSub(
+ private List<NetworkStatsExt> getDataUsageBytesTransferSnapshotForSubLocked(
@NonNull SubInfo subInfo) {
final List<NetworkStatsExt> ret = new ArrayList<>();
for (final int ratType : getAllCollapsedRatTypes()) {
@@ -1583,7 +1611,7 @@
.setRatType(ratType)
.setMeteredness(METERED_YES).build();
final NetworkStats stats =
- getUidNetworkStatsSnapshotForTemplate(template, /*includeTags=*/false);
+ getUidNetworkStatsSnapshotForTemplateLocked(template, /*includeTags=*/false);
if (stats != null) {
ret.add(new NetworkStatsExt(sliceNetworkStatsByFgbg(stats),
new int[]{TRANSPORT_CELLULAR}, /*slicedByFgbg=*/true,
@@ -5273,6 +5301,13 @@
@Override
public void onSubscriptionsChanged() {
+ synchronized (mDataBytesTransferLock) {
+ onSubscriptionsChangedLocked();
+ }
+ }
+
+ @GuardedBy("mDataBytesTransferLock")
+ private void onSubscriptionsChangedLocked() {
final List<SubscriptionInfo> currentSubs = mSm.getCompleteActiveSubscriptionInfoList();
for (final SubscriptionInfo sub : currentSubs) {
final SubInfo match = CollectionUtils.find(mHistoricalSubs,
@@ -5295,12 +5330,11 @@
subscriberId, sub.isOpportunistic());
Slog.i(TAG, "subId " + subId + " added into historical sub list");
- synchronized (mDataBytesTransferLock) {
- mHistoricalSubs.add(subInfo);
- // Since getting snapshot when pulling will also include data before boot,
- // query stats as baseline to prevent double count is needed.
- mNetworkStatsBaselines.addAll(getDataUsageBytesTransferSnapshotForSub(subInfo));
- }
+ mHistoricalSubs.add(subInfo);
+ // Since getting snapshot when pulling will also include data before boot,
+ // query stats as baseline to prevent double count is needed.
+ mNetworkStatsBaselines.addAll(
+ getDataUsageBytesTransferSnapshotForSubLocked(subInfo));
}
}
}
diff --git a/services/core/java/com/android/server/stats/stats_flags.aconfig b/services/core/java/com/android/server/stats/stats_flags.aconfig
index 6faa273..f360837 100644
--- a/services/core/java/com/android/server/stats/stats_flags.aconfig
+++ b/services/core/java/com/android/server/stats/stats_flags.aconfig
@@ -8,3 +8,11 @@
bug: "309512867"
is_fixed_read_only: true
}
+
+flag {
+ name: "apply_network_stats_poll_rate_limit"
+ namespace: "statsd"
+ description: "Apply a rate limit for polling network stats when pulling relevant atoms"
+ bug: "352495181"
+ is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java b/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java
index aa2b74e..58c3ba5 100644
--- a/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java
+++ b/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java
@@ -56,12 +56,17 @@
// enables immediate failover to a secondary provider, one that might provide valid IDs for
// the same location, which should provide better behavior than just ignoring the event.
if (hasInvalidZones(event)) {
- TimeZoneProviderStatus providerStatus = new TimeZoneProviderStatus.Builder(
- event.getTimeZoneProviderStatus())
- .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_FAILED)
- .build();
- return TimeZoneProviderEvent.createUncertainEvent(
- event.getCreationElapsedMillis(), providerStatus);
+ TimeZoneProviderStatus providerStatus = event.getTimeZoneProviderStatus();
+ TimeZoneProviderStatus.Builder providerStatusBuilder;
+ if (providerStatus != null) {
+ providerStatusBuilder = new TimeZoneProviderStatus.Builder(providerStatus);
+ } else {
+ providerStatusBuilder = new TimeZoneProviderStatus.Builder();
+ }
+ return TimeZoneProviderEvent.createUncertainEvent(event.getCreationElapsedMillis(),
+ providerStatusBuilder
+ .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_FAILED)
+ .build());
}
return event;
diff --git a/services/core/java/com/android/server/vibrator/VibratorControlService.java b/services/core/java/com/android/server/vibrator/VibratorControlService.java
index 4da6585..de5e662 100644
--- a/services/core/java/com/android/server/vibrator/VibratorControlService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorControlService.java
@@ -41,6 +41,7 @@
import android.os.SystemClock;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
+import android.os.vibrator.Flags;
import android.util.IndentingPrintWriter;
import android.util.IntArray;
import android.util.Slog;
@@ -265,9 +266,15 @@
return null;
}
+ if (Flags.throttleVibrationParamsRequests() && mVibrationParamRequest != null
+ && mVibrationParamRequest.usage == usage) {
+ // Reuse existing future for ongoing request with same usage.
+ return mVibrationParamRequest.future;
+ }
+
try {
endOngoingRequestVibrationParamsLocked(/* wasCancelled= */ true);
- mVibrationParamRequest = new VibrationParamRequest(uid);
+ mVibrationParamRequest = new VibrationParamRequest(uid, usage);
vibratorController.requestVibrationParams(vibrationType, timeoutInMillis,
mVibrationParamRequest.token);
return mVibrationParamRequest.future;
@@ -533,10 +540,12 @@
public final CompletableFuture<Void> future = new CompletableFuture<>();
public final IBinder token = new Binder();
public final int uid;
+ public final @VibrationAttributes.Usage int usage;
public final long uptimeMs;
- VibrationParamRequest(int uid) {
+ VibrationParamRequest(int uid, @VibrationAttributes.Usage int usage) {
this.uid = uid;
+ this.usage = usage;
uptimeMs = SystemClock.uptimeMillis();
}
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index 872b4e1..7a0fd3e 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -60,7 +60,6 @@
import com.android.internal.policy.IKeyguardDismissCallback;
import com.android.server.inputmethod.InputMethodManagerInternal;
import com.android.server.policy.WindowManagerPolicy;
-import com.android.window.flags.Flags;
import java.io.PrintWriter;
@@ -198,14 +197,6 @@
setWakeTransitionReady();
return;
}
- EventLogTags.writeWmSetKeyguardShown(
- displayId,
- keyguardShowing ? 1 : 0,
- aodShowing ? 1 : 0,
- state.mKeyguardGoingAway ? 1 : 0,
- state.mOccluded ? 1 : 0,
- "setKeyguardShown");
-
// Update the task snapshot if the screen will not be turned off. To make sure that the
// unlocking animation can animate consistent content. The conditions are:
// - Either AOD or keyguard changes to be showing. So if the states change individually,
@@ -224,6 +215,7 @@
state.mKeyguardShowing = keyguardShowing;
state.mAodShowing = aodShowing;
+ state.writeEventLog("setKeyguardShown");
if (keyguardChanged) {
// Irrelevant to AOD.
@@ -232,19 +224,13 @@
state.mDismissalRequested = false;
}
if (goingAwayRemoved
- || (Flags.keyguardAppearTransition() && keyguardShowing
- && !Display.isOffState(dc.getDisplayInfo().state))) {
+ || (keyguardShowing && !Display.isOffState(dc.getDisplayInfo().state))) {
// Keyguard decided to show or stopped going away. Send a transition to animate back
// to the locked state before holding the sleep token again
- final DisplayContent transitionDc = Flags.keyguardAppearTransition()
- ? dc
- : mRootWindowContainer.getDefaultDisplay();
- transitionDc.requestTransitionAndLegacyPrepare(
+ dc.requestTransitionAndLegacyPrepare(
TRANSIT_TO_FRONT, TRANSIT_FLAG_KEYGUARD_APPEARING);
- if (Flags.keyguardAppearTransition()) {
- dc.mWallpaperController.adjustWallpaperWindows();
- }
- transitionDc.executeAppTransition();
+ dc.mWallpaperController.adjustWallpaperWindows();
+ dc.executeAppTransition();
}
}
@@ -284,13 +270,7 @@
mService.deferWindowLayout();
state.mKeyguardGoingAway = true;
try {
- EventLogTags.writeWmSetKeyguardShown(
- displayId,
- state.mKeyguardShowing ? 1 : 0,
- state.mAodShowing ? 1 : 0,
- 1 /* keyguardGoingAway */,
- state.mOccluded ? 1 : 0,
- "keyguardGoingAway");
+ state.writeEventLog("keyguardGoingAway");
final int transitFlags = convertTransitFlags(flags);
final DisplayContent dc = mRootWindowContainer.getDefaultDisplay();
dc.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY, transitFlags);
@@ -436,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();
@@ -481,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 */);
@@ -646,6 +627,16 @@
mSleepTokenAcquirer.release(mDisplayId);
}
+ void writeEventLog(String reason) {
+ EventLogTags.writeWmSetKeyguardShown(
+ mDisplayId,
+ mKeyguardShowing ? 1 : 0,
+ mAodShowing ? 1 : 0,
+ mKeyguardGoingAway ? 1 : 0,
+ mOccluded ? 1 : 0,
+ reason);
+ }
+
/**
* Updates keyguard status if the top task could be visible. The top task may occlude
* keyguard, request to dismiss keyguard or make insecure keyguard go away based on its
@@ -715,23 +706,16 @@
}
boolean hasChange = false;
- if (lastOccluded != mOccluded) {
- if (mDisplayId == DEFAULT_DISPLAY) {
- EventLogTags.writeWmSetKeyguardShown(
- mDisplayId,
- mKeyguardShowing ? 1 : 0,
- mAodShowing ? 1 : 0,
- mKeyguardGoingAway ? 1 : 0,
- mOccluded ? 1 : 0,
- "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) {
- 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/Task.java b/services/core/java/com/android/server/wm/Task.java
index 5bd01fe..1c9aaf9 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -2287,7 +2287,7 @@
// Apply crop to root tasks only and clear the crops of the descendant tasks.
int width = 0;
int height = 0;
- if (isRootTask()) {
+ if (isRootTask() && !mTransitionController.mIsWaitingForDisplayEnabled) {
final Rect taskBounds = getBounds();
width = taskBounds.width();
height = taskBounds.height();
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..79a1f58
--- /dev/null
+++ b/services/tests/performancehinttests/Android.bp
@@ -0,0 +1,49 @@
+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,
+ },
+}
+
+android_ravenwood_test {
+ name: "PerformanceHintTestsRavenwood",
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.test.rules",
+ "flag-junit",
+ "services.core",
+
+ ],
+ srcs: [
+ "src/**/*.java",
+ ],
+ auto_gen_config: true,
+}
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/timetests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java b/services/tests/timetests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java
index f3440f7..ea3b409 100644
--- a/services/tests/timetests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java
+++ b/services/tests/timetests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java
@@ -39,13 +39,23 @@
private static final long ARBITRARY_TIME_MILLIS = 11223344;
+ private final List<String> mNonExistingTimeZones = Arrays.asList(
+ "SystemV/HST10", "Atlantic/Atlantis", "EUROPE/LONDON", "Etc/GMT-5:30");
private final ZoneInfoDbTimeZoneProviderEventPreProcessor mPreProcessor =
new ZoneInfoDbTimeZoneProviderEventPreProcessor();
+ private static final TimeZoneProviderStatus ARBITRARY_TIME_ZONE_PROVIDER_STATUS =
+ new TimeZoneProviderStatus.Builder()
+ .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK)
+ .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_OK)
+ .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_OK)
+ .build();
+
@Test
public void timeZoneIdsFromZoneInfoDbAreValid() {
for (String timeZone : TimeZone.getAvailableIDs()) {
- TimeZoneProviderEvent event = timeZoneProviderEvent(timeZone);
+ TimeZoneProviderEvent event = timeZoneProviderEvent(timeZone,
+ ARBITRARY_TIME_ZONE_PROVIDER_STATUS);
assertWithMessage("Time zone %s should be supported", timeZone)
.that(mPreProcessor.preProcess(event)).isEqualTo(event);
}
@@ -53,11 +63,9 @@
@Test
public void eventWithNonExistingZones_areMappedToUncertainEvent() {
- List<String> nonExistingTimeZones = Arrays.asList(
- "SystemV/HST10", "Atlantic/Atlantis", "EUROPE/LONDON", "Etc/GMT-5:30");
-
- for (String timeZone : nonExistingTimeZones) {
- TimeZoneProviderEvent event = timeZoneProviderEvent(timeZone);
+ for (String timeZone : mNonExistingTimeZones) {
+ TimeZoneProviderEvent event = timeZoneProviderEvent(timeZone,
+ ARBITRARY_TIME_ZONE_PROVIDER_STATUS);
TimeZoneProviderStatus expectedProviderStatus =
new TimeZoneProviderStatus.Builder(event.getTimeZoneProviderStatus())
@@ -73,14 +81,31 @@
}
}
- private static TimeZoneProviderEvent timeZoneProviderEvent(String... timeZoneIds) {
- TimeZoneProviderStatus providerStatus = new TimeZoneProviderStatus.Builder()
- .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_OK)
- .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK)
- .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_OK)
- .build();
+ @Test
+ public void eventWithNullProviderStatus_areMappedToUncertainEvent() {
+ for (String timeZone : mNonExistingTimeZones) {
+ TimeZoneProviderEvent eventWithNullStatus = timeZoneProviderEvent(timeZone,
+ /* providerStatus= */ null);
+
+ TimeZoneProviderStatus expectedProviderStatus =
+ new TimeZoneProviderStatus.Builder()
+ .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_FAILED)
+ .build();
+
+ TimeZoneProviderEvent expectedResultEvent =
+ TimeZoneProviderEvent.createUncertainEvent(
+ eventWithNullStatus.getCreationElapsedMillis(),
+ expectedProviderStatus);
+ assertWithMessage(timeZone + " with null time zone provider status")
+ .that(mPreProcessor.preProcess(eventWithNullStatus))
+ .isEqualTo(expectedResultEvent);
+ }
+ }
+
+ private static TimeZoneProviderEvent timeZoneProviderEvent(String timeZoneId,
+ TimeZoneProviderStatus providerStatus) {
TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder()
- .setTimeZoneIds(Arrays.asList(timeZoneIds))
+ .setTimeZoneIds(Arrays.asList(timeZoneId))
.setElapsedRealtimeMillis(ARBITRARY_TIME_MILLIS)
.build();
return TimeZoneProviderEvent.createSuggestionEvent(
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java
index 8ca8623..c496bbb 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java
@@ -45,6 +45,11 @@
import android.os.IBinder;
import android.os.Process;
import android.os.test.TestLooper;
+import android.os.vibrator.Flags;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.SparseArray;
import androidx.test.InstrumentationRegistry;
@@ -63,7 +68,6 @@
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.TimeUnit;
public class VibratorControlServiceTest {
@@ -71,6 +75,8 @@
@Rule
public MockitoRule rule = MockitoJUnit.rule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
@Mock private VibrationScaler mMockVibrationScaler;
@Mock private PackageManagerInternal mPackageManagerInternalMock;
@@ -98,6 +104,7 @@
mVibratorControlService = new VibratorControlService(
InstrumentationRegistry.getContext(), new VibratorControllerHolder(),
mMockVibrationScaler, mVibrationSettings, mStatsLoggerMock, mLock);
+ mFakeVibratorController.setVibratorControlService(mVibratorControlService);
}
@Test
@@ -280,10 +287,10 @@
CompletableFuture<Void> future =
mVibratorControlService.triggerVibrationParamsRequest(UID, USAGE_RINGTONE,
timeoutInMillis);
- try {
- future.orTimeout(timeoutInMillis, TimeUnit.MILLISECONDS).get();
- } catch (Throwable ignored) {
- }
+ mTestLooper.dispatchAll();
+
+ assertThat(future).isNotNull();
+ assertThat(future.isDone()).isTrue();
assertThat(mFakeVibratorController.didRequestVibrationParams).isTrue();
assertThat(mFakeVibratorController.requestVibrationType).isEqualTo(
ScaleParam.TYPE_RINGTONE);
@@ -315,6 +322,46 @@
}
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_THROTTLE_VIBRATION_PARAMS_REQUESTS)
+ public void testRequestVibrationParams_withOngoingRequestAndSameUsage_returnOngoingFuture() {
+ int timeoutInMillis = 10;
+ mVibratorControlService.registerVibratorController(mFakeVibratorController);
+ CompletableFuture<Void> future =
+ mVibratorControlService.triggerVibrationParamsRequest(UID, USAGE_RINGTONE,
+ timeoutInMillis);
+ CompletableFuture<Void> future2 =
+ mVibratorControlService.triggerVibrationParamsRequest(UID, USAGE_RINGTONE,
+ timeoutInMillis);
+ mTestLooper.dispatchAll();
+
+ assertThat(future).isNotNull();
+ assertThat(future).isEqualTo(future2);
+ assertThat(future.isDone()).isTrue();
+ assertThat(mFakeVibratorController.requestVibrationParamsCounter).isEqualTo(1);
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_THROTTLE_VIBRATION_PARAMS_REQUESTS)
+ public void testRequestVibrationParams_withOngoingRequestAndSameUsage_returnNewFuture() {
+ int timeoutInMillis = 10;
+ mVibratorControlService.registerVibratorController(mFakeVibratorController);
+ CompletableFuture<Void> future =
+ mVibratorControlService.triggerVibrationParamsRequest(UID, USAGE_RINGTONE,
+ timeoutInMillis);
+ CompletableFuture<Void> future2 =
+ mVibratorControlService.triggerVibrationParamsRequest(UID, USAGE_RINGTONE,
+ timeoutInMillis);
+ mTestLooper.dispatchAll();
+
+ assertThat(future).isNotNull();
+ assertThat(future2).isNotNull();
+ assertThat(future).isNotEqualTo(future2);
+ assertThat(future.isDone()).isTrue();
+ assertThat(future2.isDone()).isTrue();
+ assertThat(mFakeVibratorController.requestVibrationParamsCounter).isEqualTo(2);
+ }
+
private static int buildVibrationTypesMask(int... types) {
int typesMask = 0;
for (int type : types) {
diff --git a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorController.java b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorController.java
index 0cd88ef..c0e1407 100644
--- a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorController.java
+++ b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorController.java
@@ -39,6 +39,7 @@
public boolean didRequestVibrationParams = false;
public int requestVibrationType = VibrationAttributes.USAGE_UNKNOWN;
public long requestTimeoutInMillis = 0;
+ public int requestVibrationParamsCounter = 0;
public FakeVibratorController(Looper looper) {
mHandler = new Handler(looper);
@@ -58,6 +59,7 @@
didRequestVibrationParams = true;
requestVibrationType = vibrationType;
requestTimeoutInMillis = timeoutInMillis;
+ requestVibrationParamsCounter++;
mHandler.post(() -> {
if (mVibratorControlService != null) {
mVibratorControlService.onRequestVibrationParamsComplete(token, mRequestResult);