Merge "Don't update the standby bucket for apps that were just restored." into tm-qpr-dev
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 2587462..a6f47d4 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -109,6 +109,7 @@
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseLongArray;
+import android.util.SparseSetArray;
import android.util.TimeUtils;
import android.view.Display;
import android.widget.Toast;
@@ -452,6 +453,12 @@
private final Map<String, String> mAppStandbyProperties = new ArrayMap<>();
/**
+ * Set of apps that were restored via backup & restore, per user, that need their
+ * standby buckets to be adjusted when installed.
+ */
+ private final SparseSetArray<String> mAppsToRestoreToRare = new SparseSetArray<>();
+
+ /**
* List of app-ids of system packages, populated on boot, when system services are ready.
*/
private final ArrayList<Integer> mSystemPackagesAppIds = new ArrayList<>();
@@ -1614,18 +1621,29 @@
final int reason = REASON_MAIN_DEFAULT | REASON_SUB_DEFAULT_APP_RESTORED;
final long nowElapsed = mInjector.elapsedRealtime();
for (String packageName : restoredApps) {
- // If the package is not installed, don't allow the bucket to be set.
+ // If the package is not installed, don't allow the bucket to be set. Instead, add it
+ // to a list of all packages whose buckets need to be adjusted when installed.
if (!mInjector.isPackageInstalled(packageName, 0, userId)) {
- Slog.e(TAG, "Tried to restore bucket for uninstalled app: " + packageName);
+ Slog.i(TAG, "Tried to restore bucket for uninstalled app: " + packageName);
+ mAppsToRestoreToRare.add(userId, packageName);
continue;
}
- final int standbyBucket = getAppStandbyBucket(packageName, userId, nowElapsed, false);
- // Only update the standby bucket to RARE if the app is still in the NEVER bucket.
- if (standbyBucket == STANDBY_BUCKET_NEVER) {
- setAppStandbyBucket(packageName, userId, STANDBY_BUCKET_RARE, reason,
- nowElapsed, false);
- }
+ restoreAppToRare(packageName, userId, nowElapsed, reason);
+ }
+ // Clear out the list of restored apps that need to have their standby buckets adjusted
+ // if they still haven't been installed eight hours after restore.
+ // Note: if the device reboots within these first 8 hours, this list will be lost since it's
+ // not persisted - this is the expected behavior for now and may be updated in the future.
+ mHandler.postDelayed(() -> mAppsToRestoreToRare.remove(userId), 8 * ONE_HOUR);
+ }
+
+ /** Adjust the standby bucket of the given package for the user to RARE. */
+ private void restoreAppToRare(String pkgName, int userId, long nowElapsed, int reason) {
+ final int standbyBucket = getAppStandbyBucket(pkgName, userId, nowElapsed, false);
+ // Only update the standby bucket to RARE if the app is still in the NEVER bucket.
+ if (standbyBucket == STANDBY_BUCKET_NEVER) {
+ setAppStandbyBucket(pkgName, userId, STANDBY_BUCKET_RARE, reason, nowElapsed, false);
}
}
@@ -2120,15 +2138,24 @@
}
// component-level enable/disable can affect bucketing, so we always
// reevaluate that for any PACKAGE_CHANGED
- mHandler.obtainMessage(MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, pkgName)
- .sendToTarget();
+ if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
+ mHandler.obtainMessage(MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, pkgName)
+ .sendToTarget();
+ }
}
if ((Intent.ACTION_PACKAGE_REMOVED.equals(action) ||
Intent.ACTION_PACKAGE_ADDED.equals(action))) {
if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
maybeUnrestrictBuggyApp(pkgName, userId);
- } else {
+ } else if (!Intent.ACTION_PACKAGE_ADDED.equals(action)) {
clearAppIdleForPackage(pkgName, userId);
+ } else {
+ // Package was just added and it's not being replaced.
+ if (mAppsToRestoreToRare.contains(userId, pkgName)) {
+ restoreAppToRare(pkgName, userId, mInjector.elapsedRealtime(),
+ REASON_MAIN_DEFAULT | REASON_SUB_DEFAULT_APP_RESTORED);
+ mAppsToRestoreToRare.remove(userId, pkgName);
+ }
}
}
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index d6fa02c6..94a6382 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -9514,6 +9514,16 @@
"lock_screen_show_silent_notifications";
/**
+ * Indicates whether "seen" notifications should be suppressed from the lockscreen.
+ * <p>
+ * Type: int (0 for false, 1 for true)
+ *
+ * @hide
+ */
+ public static final String LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS =
+ "lock_screen_show_only_unseen_notifications";
+
+ /**
* Indicates whether snooze options should be shown on notifications
* <p>
* Type: int (0 for false, 1 for true)
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java
index 7624b69..fe60037 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java
@@ -27,9 +27,10 @@
*/
public class AcceptOnceConsumer<T> implements Consumer<T> {
private final Consumer<T> mCallback;
- private final DataProducer<T> mProducer;
+ private final AcceptOnceProducerCallback<T> mProducer;
- public AcceptOnceConsumer(@NonNull DataProducer<T> producer, @NonNull Consumer<T> callback) {
+ public AcceptOnceConsumer(@NonNull AcceptOnceProducerCallback<T> producer,
+ @NonNull Consumer<T> callback) {
mProducer = producer;
mCallback = callback;
}
@@ -37,6 +38,20 @@
@Override
public void accept(@NonNull T t) {
mCallback.accept(t);
- mProducer.removeDataChangedCallback(this);
+ mProducer.onConsumerReadyToBeRemoved(this);
+ }
+
+ /**
+ * Interface to allow the {@link AcceptOnceConsumer} to notify the client that created it,
+ * when it is ready to be removed. This allows the client to remove the consumer object
+ * when it deems it is safe to do so.
+ * @param <T> The type of data this callback accepts through {@link #onConsumerReadyToBeRemoved}
+ */
+ public interface AcceptOnceProducerCallback<T> {
+
+ /**
+ * Notifies that the given {@code callback} is ready to be removed
+ */
+ void onConsumerReadyToBeRemoved(Consumer<T> callback);
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
index cbaa277..46c925a 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
@@ -19,6 +19,7 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
+import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.Set;
@@ -31,11 +32,14 @@
*
* @param <T> The type of data this producer returns through {@link DataProducer#getData}.
*/
-public abstract class BaseDataProducer<T> implements DataProducer<T> {
+public abstract class BaseDataProducer<T> implements DataProducer<T>,
+ AcceptOnceConsumer.AcceptOnceProducerCallback<T> {
private final Object mLock = new Object();
@GuardedBy("mLock")
private final Set<Consumer<T>> mCallbacks = new LinkedHashSet<>();
+ @GuardedBy("mLock")
+ private final Set<Consumer<T>> mCallbacksToRemove = new HashSet<>();
/**
* Adds a callback to the set of callbacks listening for data. Data is delivered through
@@ -85,6 +89,26 @@
for (Consumer<T> callback : mCallbacks) {
callback.accept(value);
}
+ removeFinishedCallbacksLocked();
+ }
+ }
+
+ /**
+ * Removes any callbacks that notified us through {@link #onConsumerReadyToBeRemoved(Consumer)}
+ * that they are ready to be removed.
+ */
+ @GuardedBy("mLock")
+ private void removeFinishedCallbacksLocked() {
+ for (Consumer<T> callback: mCallbacksToRemove) {
+ mCallbacks.remove(callback);
+ }
+ mCallbacksToRemove.clear();
+ }
+
+ @Override
+ public void onConsumerReadyToBeRemoved(Consumer<T> callback) {
+ synchronized (mLock) {
+ mCallbacksToRemove.add(callback);
}
}
}
\ No newline at end of file
diff --git a/media/java/android/media/ImageWriter.java b/media/java/android/media/ImageWriter.java
index 9f52bf1..308bb3e 100644
--- a/media/java/android/media/ImageWriter.java
+++ b/media/java/android/media/ImageWriter.java
@@ -100,6 +100,8 @@
private final Object mListenerLock = new Object();
private OnImageReleasedListener mListener;
private ListenerHandler mListenerHandler;
+ private final Object mCloseLock = new Object();
+ private boolean mIsWriterValid = false;
private long mNativeContext;
private int mWidth;
@@ -305,6 +307,8 @@
ImageUtils.getEstimatedNativeAllocBytes(mWidth, mHeight,
useLegacyImageFormat ? imageFormat : hardwareBufferFormat, /*buffer count*/ 1);
VMRuntime.getRuntime().registerNativeAllocation(mEstimatedNativeAllocBytes);
+
+ mIsWriterValid = true;
}
private ImageWriter(Surface surface, int maxImages, boolean useSurfaceImageFormatInfo,
@@ -448,14 +452,17 @@
* @see Image#close
*/
public Image dequeueInputImage() {
- if (mDequeuedImages.size() >= mMaxImages) {
- throw new IllegalStateException("Already dequeued max number of Images " + mMaxImages);
+ synchronized (mCloseLock) {
+ if (mDequeuedImages.size() >= mMaxImages) {
+ throw new IllegalStateException(
+ "Already dequeued max number of Images " + mMaxImages);
+ }
+ WriterSurfaceImage newImage = new WriterSurfaceImage(this);
+ nativeDequeueInputImage(mNativeContext, newImage);
+ mDequeuedImages.add(newImage);
+ newImage.mIsImageValid = true;
+ return newImage;
}
- WriterSurfaceImage newImage = new WriterSurfaceImage(this);
- nativeDequeueInputImage(mNativeContext, newImage);
- mDequeuedImages.add(newImage);
- newImage.mIsImageValid = true;
- return newImage;
}
/**
@@ -513,49 +520,53 @@
if (image == null) {
throw new IllegalArgumentException("image shouldn't be null");
}
- boolean ownedByMe = isImageOwnedByMe(image);
- if (ownedByMe && !(((WriterSurfaceImage) image).mIsImageValid)) {
- throw new IllegalStateException("Image from ImageWriter is invalid");
- }
- // For images from other components that have non-null owner, need to detach first,
- // then attach. Images without owners must already be attachable.
- if (!ownedByMe) {
- if ((image.getOwner() instanceof ImageReader)) {
- ImageReader prevOwner = (ImageReader) image.getOwner();
-
- prevOwner.detachImage(image);
- } else if (image.getOwner() != null) {
- throw new IllegalArgumentException("Only images from ImageReader can be queued to"
- + " ImageWriter, other image source is not supported yet!");
+ synchronized (mCloseLock) {
+ boolean ownedByMe = isImageOwnedByMe(image);
+ if (ownedByMe && !(((WriterSurfaceImage) image).mIsImageValid)) {
+ throw new IllegalStateException("Image from ImageWriter is invalid");
}
- attachAndQueueInputImage(image);
- // This clears the native reference held by the original owner.
- // When this Image is detached later by this ImageWriter, the
- // native memory won't be leaked.
- image.close();
- return;
- }
+ // For images from other components that have non-null owner, need to detach first,
+ // then attach. Images without owners must already be attachable.
+ if (!ownedByMe) {
+ if ((image.getOwner() instanceof ImageReader)) {
+ ImageReader prevOwner = (ImageReader) image.getOwner();
- Rect crop = image.getCropRect();
- nativeQueueInputImage(mNativeContext, image, image.getTimestamp(), image.getDataSpace(),
- crop.left, crop.top, crop.right, crop.bottom, image.getTransform(),
- image.getScalingMode());
+ prevOwner.detachImage(image);
+ } else if (image.getOwner() != null) {
+ throw new IllegalArgumentException(
+ "Only images from ImageReader can be queued to"
+ + " ImageWriter, other image source is not supported yet!");
+ }
- /**
- * Only remove and cleanup the Images that are owned by this
- * ImageWriter. Images detached from other owners are only temporarily
- * owned by this ImageWriter and will be detached immediately after they
- * are released by downstream consumers, so there is no need to keep
- * track of them in mDequeuedImages.
- */
- if (ownedByMe) {
- mDequeuedImages.remove(image);
- // Do not call close here, as close is essentially cancel image.
- WriterSurfaceImage wi = (WriterSurfaceImage) image;
- wi.clearSurfacePlanes();
- wi.mIsImageValid = false;
+ attachAndQueueInputImage(image);
+ // This clears the native reference held by the original owner.
+ // When this Image is detached later by this ImageWriter, the
+ // native memory won't be leaked.
+ image.close();
+ return;
+ }
+
+ Rect crop = image.getCropRect();
+ nativeQueueInputImage(mNativeContext, image, image.getTimestamp(), image.getDataSpace(),
+ crop.left, crop.top, crop.right, crop.bottom, image.getTransform(),
+ image.getScalingMode());
+
+ /**
+ * Only remove and cleanup the Images that are owned by this
+ * ImageWriter. Images detached from other owners are only temporarily
+ * owned by this ImageWriter and will be detached immediately after they
+ * are released by downstream consumers, so there is no need to keep
+ * track of them in mDequeuedImages.
+ */
+ if (ownedByMe) {
+ mDequeuedImages.remove(image);
+ // Do not call close here, as close is essentially cancel image.
+ WriterSurfaceImage wi = (WriterSurfaceImage) image;
+ wi.clearSurfacePlanes();
+ wi.mIsImageValid = false;
+ }
}
}
@@ -691,17 +702,23 @@
*/
@Override
public void close() {
- setOnImageReleasedListener(null, null);
- for (Image image : mDequeuedImages) {
- image.close();
- }
- mDequeuedImages.clear();
- nativeClose(mNativeContext);
- mNativeContext = 0;
+ synchronized (mCloseLock) {
+ if (!mIsWriterValid) {
+ return;
+ }
+ setOnImageReleasedListener(null, null);
+ for (Image image : mDequeuedImages) {
+ image.close();
+ }
+ mDequeuedImages.clear();
+ nativeClose(mNativeContext);
+ mNativeContext = 0;
- if (mEstimatedNativeAllocBytes > 0) {
- VMRuntime.getRuntime().registerNativeFree(mEstimatedNativeAllocBytes);
- mEstimatedNativeAllocBytes = 0;
+ if (mEstimatedNativeAllocBytes > 0) {
+ VMRuntime.getRuntime().registerNativeFree(mEstimatedNativeAllocBytes);
+ mEstimatedNativeAllocBytes = 0;
+ }
+ mIsWriterValid = false;
}
}
@@ -790,10 +807,16 @@
@Override
public void handleMessage(Message msg) {
OnImageReleasedListener listener;
- synchronized (mListenerLock) {
+ boolean isWriterValid;
+ synchronized (ImageWriter.this.mListenerLock) {
listener = mListener;
}
- if (listener != null) {
+ // Check to make sure we don't accidentally queue images after the writer is
+ // closed or closing
+ synchronized (ImageWriter.this.mCloseLock) {
+ isWriterValid = ImageWriter.this.mIsWriterValid;
+ }
+ if (listener != null && isWriterValid) {
listener.onImageReleased(ImageWriter.this);
}
}
@@ -813,10 +836,14 @@
}
final Handler handler;
+ final boolean isWriterValid;
synchronized (iw.mListenerLock) {
handler = iw.mListenerHandler;
}
- if (handler != null) {
+ synchronized (iw.mCloseLock) {
+ isWriterValid = iw.mIsWriterValid;
+ }
+ if (handler != null && isWriterValid) {
handler.sendEmptyMessage(0);
}
}
@@ -1050,6 +1077,9 @@
private int mTransform = 0; //Default no transform
private int mScalingMode = 0; //Default frozen scaling mode
+ private final Object mCloseLock = new Object(); // lock to protect against multiple
+ // simultaneous calls to close()
+
public WriterSurfaceImage(ImageWriter writer) {
mOwner = writer;
mWidth = writer.mWidth;
@@ -1192,8 +1222,10 @@
@Override
public void close() {
- if (mIsImageValid) {
- getOwner().abortImage(this);
+ synchronized (mCloseLock) {
+ if (mIsImageValid) {
+ getOwner().abortImage(this);
+ }
}
}
diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml
index 46a94fd..99b15db 100644
--- a/packages/SettingsProvider/res/values/defaults.xml
+++ b/packages/SettingsProvider/res/values/defaults.xml
@@ -165,6 +165,9 @@
<!-- Default for Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS -->
<bool name="def_lock_screen_allow_private_notifications">true</bool>
+ <!-- Default for Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS -->
+ <bool name="def_lock_screen_show_only_unseen_notifications">false</bool>
+
<!-- Default for Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED, 1==on -->
<integer name="def_heads_up_enabled">1</integer>
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 92a938c..1b0b6b4 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -146,6 +146,7 @@
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS,
+ Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
Settings.Secure.SHOW_NOTIFICATION_SNOOZE,
Settings.Secure.NOTIFICATION_HISTORY_ENABLED,
Settings.Secure.ZEN_DURATION,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index eabf4cc..4fa490f 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -219,6 +219,7 @@
VALIDATORS.put(Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.SHOW_NOTIFICATION_SNOOZE, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.NOTIFICATION_HISTORY_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.ZEN_DURATION, ANY_INTEGER_VALIDATOR);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index ded7e785..84a5593 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -3631,7 +3631,7 @@
}
private final class UpgradeController {
- private static final int SETTINGS_VERSION = 211;
+ private static final int SETTINGS_VERSION = 212;
private final int mUserId;
@@ -5527,6 +5527,26 @@
}
currentVersion = 211;
}
+ if (currentVersion == 211) {
+ // Version 211: Set default value for
+ // Secure#LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS
+ final SettingsState secureSettings = getSecureSettingsLocked(userId);
+ final Setting lockScreenUnseenSetting = secureSettings
+ .getSettingLocked(Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS);
+ if (lockScreenUnseenSetting.isNull()) {
+ final boolean defSetting = getContext().getResources()
+ .getBoolean(R.bool.def_lock_screen_show_only_unseen_notifications);
+ secureSettings.insertSettingOverrideableByRestoreLocked(
+ Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+ defSetting ? "1" : "0",
+ null /* tag */,
+ true /* makeDefault */,
+ SettingsState.SYSTEM_PACKAGE_NAME);
+ }
+
+ currentVersion = 212;
+ }
+
// vXXX: Add new settings above this point.
if (currentVersion != newVersion) {
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 6d88e51..2d756ae 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -779,6 +779,10 @@
<!-- Duration in milliseconds of the y-translation animation when entering a dream -->
<integer name="config_dreamOverlayInTranslationYDurationMs">917</integer>
+ <!-- Delay in milliseconds before switching to the dock user and dreaming if a secondary user is
+ active when the device is locked and docked. 0 indicates disabled. Default is 1 minute. -->
+ <integer name="config_defaultDockUserTimeoutMs">60000</integer>
+
<!-- Icons that don't show in a collapsed non-keyguard statusbar -->
<string-array name="config_collapsed_statusbar_icon_blocklist" translatable="false">
<item>@*android:string/status_bar_volume</item>
@@ -820,6 +824,8 @@
slot. If the user did make a choice, even if the choice is the "None" option, the default is
ignored. -->
<string-array name="config_keyguardQuickAffordanceDefaults" translatable="false">
+ <item>bottom_start:home</item>
+ <item>bottom_end:wallet</item>
</string-array>
</resources>
diff --git a/packages/SystemUI/res/xml/media_session_expanded.xml b/packages/SystemUI/res/xml/media_session_expanded.xml
index 64c2ef1..7de0a5e 100644
--- a/packages/SystemUI/res/xml/media_session_expanded.xml
+++ b/packages/SystemUI/res/xml/media_session_expanded.xml
@@ -88,20 +88,18 @@
The chain is set to "spread" so that the progress bar can be weighted to fill any empty space.
-->
<Constraint
- android:id="@+id/media_scrubbing_elapsed_time"
+ android:id="@+id/actionPrev"
android:layout_width="48dp"
android:layout_height="48dp"
app:layout_constraintLeft_toLeftOf="parent"
- app:layout_constraintRight_toLeftOf="@id/actionPrev"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_chainStyle="spread" />
<Constraint
- android:id="@+id/actionPrev"
+ android:id="@+id/media_scrubbing_elapsed_time"
android:layout_width="48dp"
android:layout_height="48dp"
- app:layout_constraintLeft_toRightOf="@id/media_scrubbing_elapsed_time"
- app:layout_constraintRight_toLeftOf="@id/media_progress_bar"
+ app:layout_constraintLeft_toRightOf="@id/actionPrev"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_chainStyle="spread" />
@@ -109,7 +107,7 @@
android:id="@+id/media_progress_bar"
android:layout_width="0dp"
android:layout_height="48dp"
- app:layout_constraintLeft_toRightOf="@id/actionPrev"
+ app:layout_constraintLeft_toRightOf="@id/media_scrubbing_elapsed_time"
app:layout_constraintRight_toLeftOf="@id/actionNext"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_weight="1" />
@@ -118,7 +116,6 @@
android:id="@+id/actionNext"
android:layout_width="48dp"
android:layout_height="48dp"
- app:layout_constraintLeft_toRightOf="@id/media_progress_bar"
app:layout_constraintRight_toLeftOf="@id/media_scrubbing_total_time"
app:layout_constraintBottom_toBottomOf="parent" />
@@ -126,7 +123,6 @@
android:id="@+id/media_scrubbing_total_time"
android:layout_width="48dp"
android:layout_height="48dp"
- app:layout_constraintLeft_toRightOf="@id/actionNext"
app:layout_constraintRight_toLeftOf="@id/action0"
app:layout_constraintBottom_toBottomOf="parent" />
@@ -134,7 +130,6 @@
android:id="@+id/action0"
android:layout_width="48dp"
android:layout_height="48dp"
- app:layout_constraintLeft_toRightOf="@id/media_scrubbing_total_time"
app:layout_constraintRight_toLeftOf="@id/action1"
app:layout_constraintBottom_toBottomOf="parent" />
@@ -142,7 +137,6 @@
android:id="@+id/action1"
android:layout_width="48dp"
android:layout_height="48dp"
- app:layout_constraintLeft_toRightOf="@id/action0"
app:layout_constraintRight_toLeftOf="@id/action2"
app:layout_constraintBottom_toBottomOf="parent" />
@@ -150,7 +144,6 @@
android:id="@+id/action2"
android:layout_width="48dp"
android:layout_height="48dp"
- app:layout_constraintLeft_toRightOf="@id/action1"
app:layout_constraintRight_toLeftOf="@id/action3"
app:layout_constraintBottom_toBottomOf="parent" />
@@ -158,7 +151,6 @@
android:id="@+id/action3"
android:layout_width="48dp"
android:layout_height="48dp"
- app:layout_constraintLeft_toRightOf="@id/action2"
app:layout_constraintRight_toLeftOf="@id/action4"
app:layout_constraintBottom_toBottomOf="parent" />
@@ -166,7 +158,6 @@
android:id="@+id/action4"
android:layout_width="48dp"
android:layout_height="48dp"
- app:layout_constraintLeft_toRightOf="@id/action3"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</ConstraintSet>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
index 4133802..76252d0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
@@ -16,8 +16,13 @@
package com.android.systemui.statusbar.notification.collection.coordinator
+import android.database.ContentObserver
+import android.os.UserHandle
+import android.provider.Settings
import androidx.annotation.VisibleForTesting
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.StatusBarState
@@ -30,11 +35,20 @@
import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProviderImpl
import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxy
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
/**
@@ -45,16 +59,19 @@
class KeyguardCoordinator
@Inject
constructor(
+ @Background private val bgDispatcher: CoroutineDispatcher,
private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
private val keyguardRepository: KeyguardRepository,
private val notifPipelineFlags: NotifPipelineFlags,
@Application private val scope: CoroutineScope,
private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider,
+ private val secureSettings: SecureSettings,
private val seenNotifsProvider: SeenNotificationsProviderImpl,
private val statusBarStateController: StatusBarStateController,
) : Coordinator {
private val unseenNotifications = mutableSetOf<NotificationEntry>()
+ private var unseenFilterEnabled = false
override fun attach(pipeline: NotifPipeline) {
setupInvalidateNotifListCallbacks()
@@ -71,6 +88,7 @@
pipeline.addFinalizeFilter(unseenNotifFilter)
pipeline.addCollectionListener(collectionListener)
scope.launch { clearUnseenWhenKeyguardIsDismissed() }
+ scope.launch { invalidateWhenUnseenSettingChanges() }
}
private suspend fun clearUnseenWhenKeyguardIsDismissed() {
@@ -85,6 +103,36 @@
}
}
+ private suspend fun invalidateWhenUnseenSettingChanges() {
+ secureSettings
+ // emit whenever the setting has changed
+ .settingChangesForUser(
+ Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+ UserHandle.USER_ALL,
+ )
+ // perform a query immediately
+ .onStart { emit(Unit) }
+ // for each change, lookup the new value
+ .map {
+ secureSettings.getBoolForUser(
+ Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+ UserHandle.USER_CURRENT,
+ )
+ }
+ // perform lookups on the bg thread pool
+ .flowOn(bgDispatcher)
+ // only track the most recent emission, if events are happening faster than they can be
+ // consumed
+ .conflate()
+ // update local field and invalidate if necessary
+ .collect { setting ->
+ if (setting != unseenFilterEnabled) {
+ unseenFilterEnabled = setting
+ unseenNotifFilter.invalidateList("unseen setting changed")
+ }
+ }
+ }
+
private val collectionListener =
object : NotifCollectionListener {
override fun onEntryAdded(entry: NotificationEntry) {
@@ -112,6 +160,8 @@
override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean =
when {
+ // Don't apply filter if the setting is disabled
+ !unseenFilterEnabled -> false
// Don't apply filter if the keyguard isn't currently showing
!keyguardRepository.isKeyguardShowing() -> false
// Don't apply the filter if the notification is unseen
@@ -165,3 +215,15 @@
private val SEEN_TIMEOUT = 5.seconds
}
}
+
+private fun SettingsProxy.settingChangesForUser(name: String, userHandle: Int): Flow<Unit> =
+ conflatedCallbackFlow {
+ val observer =
+ object : ContentObserver(null) {
+ override fun onChange(selfChange: Boolean) {
+ trySend(Unit)
+ }
+ }
+ registerContentObserverForUser(name, observer, userHandle)
+ awaitClose { unregisterContentObserver(observer) }
+ }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt
index 67091a9..3d65713 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt
@@ -165,6 +165,10 @@
@Test
fun `remembers selections by user`() = runTest {
+ overrideResource(
+ R.array.config_keyguardQuickAffordanceDefaults,
+ arrayOf<String>(),
+ )
val slot1 = "slot_1"
val slot2 = "slot_2"
val affordance1 = "affordance_1"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
index c40488a..c187a3f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
@@ -114,6 +114,11 @@
userHandle = UserHandle.SYSTEM,
)
+ overrideResource(
+ R.array.config_keyguardQuickAffordanceDefaults,
+ arrayOf<String>(),
+ )
+
underTest =
KeyguardQuickAffordanceRepository(
appContext = context,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
index d97571bc..58a8530 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
@@ -20,6 +20,7 @@
import android.os.UserHandle
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
@@ -290,6 +291,11 @@
@Test
fun select() =
testScope.runTest {
+ overrideResource(
+ R.array.config_keyguardQuickAffordanceDefaults,
+ arrayOf<String>(),
+ )
+
featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
homeControls.setState(
KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
index 5f19fac..be6b1dc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
@@ -18,6 +18,8 @@
package com.android.systemui.statusbar.notification.collection.coordinator
import android.app.Notification
+import android.os.UserHandle
+import android.provider.Settings
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -29,6 +31,7 @@
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProvider
@@ -38,6 +41,7 @@
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -47,6 +51,8 @@
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.same
+import org.mockito.Mockito.anyString
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.verify
import java.util.function.Consumer
@@ -176,6 +182,42 @@
}
@Test
+ fun unseenFilterInvalidatesWhenSettingChanges() {
+ whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
+
+ // GIVEN: Keyguard is not showing
+ keyguardRepository.setKeyguardShowing(false)
+ runKeyguardCoordinatorTest {
+ // GIVEN: A notification is present
+ val fakeEntry = NotificationEntryBuilder().build()
+ collectionListener.onEntryAdded(fakeEntry)
+
+ // GIVEN: The setting for filtering unseen notifications is disabled
+ showOnlyUnseenNotifsOnKeyguardSetting = false
+
+ // GIVEN: The pipeline has registered the unseen filter for invalidation
+ val invalidationListener: Pluggable.PluggableListener<NotifFilter> = mock()
+ unseenFilter.setInvalidationListener(invalidationListener)
+
+ // WHEN: The keyguard is now showing
+ keyguardRepository.setKeyguardShowing(true)
+ testScheduler.runCurrent()
+
+ // THEN: The notification is not filtered out
+ assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+
+ // WHEN: The secure setting is changed
+ showOnlyUnseenNotifsOnKeyguardSetting = true
+
+ // THEN: The pipeline is invalidated
+ verify(invalidationListener).onPluggableInvalidated(same(unseenFilter), anyString())
+
+ // THEN: The notification is recognized as "seen" and is filtered out.
+ assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
+ }
+ }
+
+ @Test
fun unseenFilterAllowsNewNotif() {
whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
@@ -276,22 +318,32 @@
private fun runKeyguardCoordinatorTest(
testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit
) {
- val testScope = TestScope(UnconfinedTestDispatcher())
+ val testDispatcher = UnconfinedTestDispatcher()
+ val testScope = TestScope(testDispatcher)
+ val fakeSettings = FakeSettings().apply {
+ putBool(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, true)
+ }
val seenNotificationsProvider = SeenNotificationsProviderImpl()
val keyguardCoordinator =
KeyguardCoordinator(
+ testDispatcher,
keyguardNotifVisibilityProvider,
keyguardRepository,
notifPipelineFlags,
testScope.backgroundScope,
sectionHeaderVisibilityProvider,
+ fakeSettings,
seenNotificationsProvider,
statusBarStateController,
)
keyguardCoordinator.attach(notifPipeline)
testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) {
- KeyguardCoordinatorTestScope(keyguardCoordinator, testScope, seenNotificationsProvider)
- .testBlock()
+ KeyguardCoordinatorTestScope(
+ keyguardCoordinator,
+ testScope,
+ seenNotificationsProvider,
+ fakeSettings,
+ ).testBlock()
}
}
@@ -299,6 +351,7 @@
private val keyguardCoordinator: KeyguardCoordinator,
private val scope: TestScope,
val seenNotificationsProvider: SeenNotificationsProvider,
+ private val fakeSettings: FakeSettings,
) : CoroutineScope by scope {
val testScheduler: TestCoroutineScheduler
get() = scope.testScheduler
@@ -316,5 +369,19 @@
val collectionListener: NotifCollectionListener by lazy {
withArgCaptor { verify(notifPipeline).addCollectionListener(capture()) }
}
+
+ var showOnlyUnseenNotifsOnKeyguardSetting: Boolean
+ get() =
+ fakeSettings.getBoolForUser(
+ Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+ UserHandle.USER_CURRENT,
+ )
+ set(value) {
+ fakeSettings.putBoolForUser(
+ Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+ value,
+ UserHandle.USER_CURRENT,
+ )
+ }
}
}