Merge "Do not purge process groups on system_server startup" into tm-dev
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index f49cdbf..4710322 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -1777,7 +1777,10 @@
* {@link Build.VERSION_CODES#S}, but starting from Android version
* {@link Build.VERSION_CODES#TIRAMISU}, expedited jobs for the foreground app are
* guaranteed to be started before {@link JobScheduler#schedule(JobInfo)} returns (assuming
- * all requested constraints are satisfied), similar to foreground services.
+ * all requested constraints are satisfied), similar to foreground services. However, this
+ * start guarantee means there is a higher chance of overlapping executions, as noted in
+ * {@link JobService}, so be sure to handle that properly if you intend to reschedule the
+ * job while it's actively running.
*
* @see JobInfo#isExpedited()
*/
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
index 1f4ef04..388bbf1 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
@@ -107,7 +107,9 @@
/**
* Schedule a job to be executed. Will replace any currently scheduled job with the same
* ID with the new information in the {@link JobInfo}. If a job with the given ID is currently
- * running, it will be stopped.
+ * running, it will be stopped. Note that in some cases, the newly scheduled job may be started
+ * before the previously running job has been fully stopped. See {@link JobService} for
+ * additional details.
*
* <p class="caution"><strong>Note:</strong> Scheduling a job can have a high cost, even if it's
* rescheduling the same job and the job didn't execute, especially on platform versions before
@@ -131,7 +133,7 @@
* job. If a job with the same ID is already scheduled, it will be replaced with the
* new {@link JobInfo}, but any previously enqueued work will remain and be dispatched the
* next time it runs. If a job with the same ID is already running, the new work will be
- * enqueued for it.
+ * enqueued for it without stopping the job.
*
* <p>The work you enqueue is later retrieved through
* {@link JobParameters#dequeueWork() JobParameters.dequeueWork}. Be sure to see there
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobService.java b/apex/jobscheduler/framework/java/android/app/job/JobService.java
index e5b0742..7ed4b62 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobService.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobService.java
@@ -31,6 +31,17 @@
* in blocking any future callbacks from the JobManager - specifically
* {@link #onStopJob(android.app.job.JobParameters)}, which is meant to inform you that the
* scheduling requirements are no longer being met.</p>
+ *
+ * As a subclass of {@link Service}, there will only be one active instance of any JobService
+ * subclasses, regardless of job ID. This means that if you schedule multiple jobs with different
+ * job IDs but using the same JobService class, that JobService may receive multiple calls to
+ * {@link #onStartJob(JobParameters)} and {@link #onStopJob(JobParameters)}, with each call being
+ * for the separate jobs.
+ *
+ * <p class="note">Note that if you cancel and reschedule an already executing job,
+ * there may be a small period of time where {@link #onStartJob(JobParameters)} has been called for
+ * the newly scheduled job instance before {@link #onStopJob(JobParameters)} has been called or
+ * fully processed for the old job.</p>
*/
public abstract class JobService extends Service {
private static final String TAG = "JobService";
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
index c86353c..afe36b5 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -542,6 +542,22 @@
return mRunningJobs.contains(job);
}
+ /**
+ * Returns true if a job that is "similar" to the provided job is currently running.
+ * "Similar" in this context means any job that the {@link JobStore} would consider equivalent
+ * and replace one with the other.
+ */
+ @GuardedBy("mLock")
+ private boolean isSimilarJobRunningLocked(JobStatus job) {
+ for (int i = mRunningJobs.size() - 1; i >= 0; --i) {
+ JobStatus js = mRunningJobs.valueAt(i);
+ if (job.getUid() == js.getUid() && job.getJobId() == js.getJobId()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/** Return {@code true} if the state was updated. */
@GuardedBy("mLock")
private boolean refreshSystemStateLocked() {
@@ -699,6 +715,21 @@
continue;
}
+ final boolean isTopEj = nextPending.shouldTreatAsExpeditedJob()
+ && nextPending.lastEvaluatedBias == JobInfo.BIAS_TOP_APP;
+ // Avoid overlapping job execution as much as possible.
+ if (!isTopEj && isSimilarJobRunningLocked(nextPending)) {
+ if (DEBUG) {
+ Slog.w(TAG, "Delaying execution of job because of similarly running one: "
+ + nextPending);
+ }
+ // It would be nice to let the JobService running the other similar job know about
+ // this new job so that it doesn't unbind from the JobService and we can call
+ // onStartJob as soon as the older job finishes.
+ // TODO: optimize the job reschedule flow to reduce service binding churn
+ continue;
+ }
+
// Find an available slot for nextPending. The context should be one of the following:
// 1. Unused
// 2. Its job should have used up its minimum execution guarantee so it
@@ -707,8 +738,6 @@
ContextAssignment selectedContext = null;
final int allWorkTypes = getJobWorkTypes(nextPending);
final boolean pkgConcurrencyOkay = !isPkgConcurrencyLimitedLocked(nextPending);
- final boolean isTopEj = nextPending.shouldTreatAsExpeditedJob()
- && nextPending.lastEvaluatedBias == JobInfo.BIAS_TOP_APP;
final boolean isInOverage = projectedRunningCount > STANDARD_CONCURRENCY_LIMIT;
boolean startingJob = false;
if (idle.size() > 0) {
@@ -1177,6 +1206,15 @@
continue;
}
+ // Avoid overlapping job execution as much as possible.
+ if (isSimilarJobRunningLocked(nextPending)) {
+ if (DEBUG) {
+ Slog.w(TAG, "Avoiding execution of job because of similarly running one: "
+ + nextPending);
+ }
+ continue;
+ }
+
if (worker.getPreferredUid() != nextPending.getUid()) {
if (backupJob == null && !isPkgConcurrencyLimitedLocked(nextPending)) {
int allWorkTypes = getJobWorkTypes(nextPending);
@@ -1260,6 +1298,15 @@
continue;
}
+ // Avoid overlapping job execution as much as possible.
+ if (isSimilarJobRunningLocked(nextPending)) {
+ if (DEBUG) {
+ Slog.w(TAG, "Avoiding execution of job because of similarly running one: "
+ + nextPending);
+ }
+ continue;
+ }
+
if (isPkgConcurrencyLimitedLocked(nextPending)) {
continue;
}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 358c327..cd70e88 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -1208,12 +1208,22 @@
// This may throw a SecurityException.
jobStatus.prepareLocked();
+ final boolean canExecuteImmediately;
if (toCancel != null) {
// Implicitly replaces the existing job record with the new instance
- cancelJobImplLocked(toCancel, jobStatus, JobParameters.STOP_REASON_CANCELLED_BY_APP,
- JobParameters.INTERNAL_STOP_REASON_CANCELED, "job rescheduled by app");
+ final boolean wasJobExecuting = cancelJobImplLocked(toCancel, jobStatus,
+ JobParameters.STOP_REASON_CANCELLED_BY_APP,
+ JobParameters.INTERNAL_STOP_REASON_CANCELED,
+ "job rescheduled by app");
+ // Avoid overlapping job executions. Don't push for immediate execution if an old
+ // job with the same ID was running, but let TOP EJs start immediately.
+ canExecuteImmediately = !wasJobExecuting
+ || (jobStatus.isRequestedExpeditedJob()
+ && mUidBiasOverride.get(jobStatus.getSourceUid(), JobInfo.BIAS_DEFAULT)
+ == JobInfo.BIAS_TOP_APP);
} else {
startTrackingJobLocked(jobStatus, null);
+ canExecuteImmediately = true;
}
if (work != null) {
@@ -1256,7 +1266,12 @@
// list and try to run it.
mJobPackageTracker.notePending(jobStatus);
mPendingJobQueue.add(jobStatus);
- maybeRunPendingJobsLocked();
+ if (canExecuteImmediately) {
+ // Don't ask the JobConcurrencyManager to try to run the job immediately. The
+ // JobServiceContext will ask the JobConcurrencyManager for another job once
+ // it finishes cleaning up the old job.
+ maybeRunPendingJobsLocked();
+ }
} else {
evaluateControllerStatesLocked(jobStatus);
}
@@ -1377,8 +1392,10 @@
* is null, the cancelled job is removed outright from the system. If
* {@code incomingJob} is non-null, it replaces {@code cancelled} in the store of
* currently scheduled jobs.
+ *
+ * @return true if the cancelled job was running
*/
- private void cancelJobImplLocked(JobStatus cancelled, JobStatus incomingJob,
+ private boolean cancelJobImplLocked(JobStatus cancelled, JobStatus incomingJob,
@JobParameters.StopReason int reason, int internalReasonCode, String debugReason) {
if (DEBUG) Slog.d(TAG, "CANCEL: " + cancelled.toShortString());
cancelled.unprepareLocked();
@@ -1389,7 +1406,7 @@
}
mChangedJobList.remove(cancelled);
// Cancel if running.
- mConcurrencyManager.stopJobOnServiceContextLocked(
+ boolean wasRunning = mConcurrencyManager.stopJobOnServiceContextLocked(
cancelled, reason, internalReasonCode, debugReason);
// If this is a replacement, bring in the new version of the job
if (incomingJob != null) {
@@ -1397,6 +1414,7 @@
startTrackingJobLocked(incomingJob, cancelled);
}
reportActiveLocked();
+ return wasRunning;
}
void updateUidState(int uid, int procState) {
@@ -1755,7 +1773,7 @@
// same job ID), we remove it from the JobStore and tell the JobServiceContext to stop
// running the job. Once the job stops running, we then call this method again.
// TODO: rework code so we don't intentionally call this method twice for the same job
- Slog.w(TAG, "Job didn't exist in JobStore");
+ Slog.w(TAG, "Job didn't exist in JobStore: " + jobStatus.toShortString());
}
if (mReadyToRock) {
for (int i = 0; i < mControllers.size(); i++) {
@@ -3813,6 +3831,7 @@
// Double indent for readability
pw.increaseIndent();
pw.increaseIndent();
+ pw.println(job.toShortString());
job.dump(pw, true, nowElapsed);
pw.decreaseIndent();
pw.decreaseIndent();
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
index 80f3fea..c90291e 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
@@ -979,6 +979,7 @@
dumpBucketExpiryTimes(idpw, appUsageHistory, totalElapsedTime);
idpw.print(" lastJob=");
TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastJobRunTime, idpw);
+ idpw.print(" lastInformedBucket=" + appUsageHistory.lastInformedBucket);
if (appUsageHistory.lastRestrictAttemptElapsedTime > 0) {
idpw.print(" lastRestrictAttempt=");
TimeUtils.formatDuration(
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 c3d6b73..1891e06 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -1155,6 +1155,12 @@
final int appId = getAppId(packageName);
if (appId < 0) return;
+ final int minBucket = getAppMinBucket(packageName, appId, userId);
+ if (idle && minBucket < AppIdleHistory.IDLE_BUCKET_CUTOFF) {
+ Slog.e(TAG, "Tried to force an app to be idle when its min bucket is "
+ + standbyBucketToString(minBucket));
+ return;
+ }
final long elapsedRealtime = mInjector.elapsedRealtime();
final boolean previouslyIdle = isAppIdleFiltered(packageName, appId,
@@ -1166,12 +1172,10 @@
final boolean stillIdle = isAppIdleFiltered(packageName, appId,
userId, elapsedRealtime);
// Inform listeners if necessary
+ maybeInformListeners(packageName, userId, elapsedRealtime, standbyBucket,
+ REASON_MAIN_FORCED_BY_USER, false);
if (previouslyIdle != stillIdle) {
- maybeInformListeners(packageName, userId, elapsedRealtime, standbyBucket,
- REASON_MAIN_FORCED_BY_USER, false);
- if (!stillIdle) {
- notifyBatteryStats(packageName, userId, idle);
- }
+ notifyBatteryStats(packageName, userId, stillIdle);
}
}
@@ -1934,6 +1938,8 @@
}
mAppIdleHistory.setAppStandbyBucket(
packageName, userId, elapsedRealtime, newBucket, newReason);
+ maybeInformListeners(packageName, userId, elapsedRealtime, newBucket,
+ newReason, false);
}
}
@@ -2490,6 +2496,8 @@
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INFORM_LISTENERS:
+ // TODO(230875908): Properly notify BatteryStats when apps change from active to
+ // idle, and vice versa
StandbyUpdateRecord r = (StandbyUpdateRecord) msg.obj;
informListeners(r.packageName, r.userId, r.bucket, r.reason,
r.isUserInteraction);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 4681d49..d6a067d35 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -919,7 +919,7 @@
field public int id;
field public String lastLoggedInFingerprint;
field public long lastLoggedInTime;
- field public String name;
+ field @Nullable public String name;
field public boolean partial;
field public boolean preCreated;
field public int profileBadge;
diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java
index 76e9fcb..d6e13ac 100644
--- a/core/java/android/content/pm/UserInfo.java
+++ b/core/java/android/content/pm/UserInfo.java
@@ -18,6 +18,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.TestApi;
import android.annotation.UserIdInt;
import android.compat.annotation.UnsupportedAppUsage;
@@ -170,7 +171,7 @@
@UnsupportedAppUsage
public int serialNumber;
@UnsupportedAppUsage
- public String name;
+ public @Nullable String name;
@UnsupportedAppUsage
public String iconPath;
@UnsupportedAppUsage
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index a64e63e..196f2f9 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -2179,7 +2179,10 @@
}
} else {
UserInfo userInfo = getUserInfo(mUserId);
- return userInfo == null ? "" : userInfo.name;
+ if (userInfo != null && userInfo.name != null) {
+ return userInfo.name;
+ }
+ return "";
}
}
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index f0a685e..3fee914 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -245,7 +245,7 @@
SystemUiDeviceConfigFlags.IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP,
DEFAULT_IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP);
- private static final int DEFAULT_LIST_VIEW_UPDATE_DELAY_MS = 250;
+ private static final int DEFAULT_LIST_VIEW_UPDATE_DELAY_MS = 125;
@VisibleForTesting
int mListViewUpdateDelayMs = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
index 46b4630..ef8f2db 100644
--- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
@@ -88,7 +88,7 @@
in int notificationLocation, boolean modifiedBeforeSending);
void onNotificationSettingsViewed(String key);
void onNotificationBubbleChanged(String key, boolean isBubble, int flags);
- void onBubbleNotificationSuppressionChanged(String key, boolean isNotifSuppressed, boolean isBubbleSuppressed);
+ void onBubbleMetadataFlagChanged(String key, int flags);
void hideCurrentInputMethodForBubbles();
void grantInlineReplyUriPermission(String key, in Uri uri, in UserHandle user, String packageName);
oneway void clearInlineReplyUriPermissions(String key);
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index d32acaf..0f328b0 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7056,6 +7056,10 @@
android:permission="android.permission.BIND_JOB_SERVICE">
</service>
+ <service android:name="com.android.server.notification.ReviewNotificationPermissionsJobService"
+ android:permission="android.permission.BIND_JOB_SERVICE">
+ </service>
+
<service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader"
android:exported="false">
<intent-filter>
diff --git a/core/res/OWNERS b/core/res/OWNERS
index 95d2712..c54638a 100644
--- a/core/res/OWNERS
+++ b/core/res/OWNERS
@@ -45,3 +45,4 @@
# Telephony
per-file res/values/config_telephony.xml = file:/platform/frameworks/opt/telephony:/OWNERS
+per-file res/xml/sms_short_codes.xml = file:/platform/frameworks/opt/telephony:/OWNERS
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 82f8a131..faada1a 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -777,22 +777,14 @@
return null;
}
- private void updateCallbackIfNecessary() {
- updateCallbackIfNecessary(true /* deferCallbackUntilAllActivitiesCreated */);
- }
-
/**
* Notifies listeners about changes to split states if necessary.
- *
- * @param deferCallbackUntilAllActivitiesCreated boolean to indicate whether the split info
- * callback should be deferred until all the
- * organized activities have been created.
*/
- private void updateCallbackIfNecessary(boolean deferCallbackUntilAllActivitiesCreated) {
+ private void updateCallbackIfNecessary() {
if (mEmbeddingCallback == null) {
return;
}
- if (deferCallbackUntilAllActivitiesCreated && !allActivitiesCreated()) {
+ if (!allActivitiesCreated()) {
return;
}
List<SplitInfo> currentSplitStates = getActiveSplitStates();
@@ -848,9 +840,7 @@
for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers;
for (TaskFragmentContainer container : containers) {
- if (container.getInfo() == null
- || container.getInfo().getActivities().size()
- != container.collectActivities().size()) {
+ if (!container.taskInfoActivityCountMatchesCreated()) {
return false;
}
}
@@ -1035,11 +1025,8 @@
&& container.getTaskFragmentToken().equals(initialTaskFragmentToken)) {
// The onTaskFragmentInfoChanged callback containing this activity has not
// reached the client yet, so add the activity to the pending appeared
- // activities and send a split info callback to the client before
- // {@link Activity#onCreate} is called.
+ // activities.
container.addPendingAppearedActivity(activity);
- updateCallbackIfNecessary(
- false /* deferCallbackUntilAllActivitiesCreated */);
return;
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 35981d3..26bbcbb 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -145,6 +145,18 @@
return allActivities;
}
+ /**
+ * Checks if the count of activities from the same process in task fragment info corresponds to
+ * the ones created and available on the client side.
+ */
+ boolean taskInfoActivityCountMatchesCreated() {
+ if (mInfo == null) {
+ return false;
+ }
+ return mPendingAppearedActivities.isEmpty()
+ && mInfo.getActivities().size() == collectActivities().size();
+ }
+
ActivityStack toActivityStack() {
return new ActivityStack(collectActivities(), isEmpty());
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 227494c..31fc6a5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -71,7 +71,7 @@
private long mLastAccessed;
@Nullable
- private Bubbles.SuppressionChangedListener mSuppressionListener;
+ private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener;
/** Whether the bubble should show a dot for the notification indicating updated content. */
private boolean mShowBubbleUpdateDot = true;
@@ -192,13 +192,13 @@
@VisibleForTesting(visibility = PRIVATE)
public Bubble(@NonNull final BubbleEntry entry,
- @Nullable final Bubbles.SuppressionChangedListener listener,
+ @Nullable final Bubbles.BubbleMetadataFlagListener listener,
final Bubbles.PendingIntentCanceledListener intentCancelListener,
Executor mainExecutor) {
mKey = entry.getKey();
mGroupKey = entry.getGroupKey();
mLocusId = entry.getLocusId();
- mSuppressionListener = listener;
+ mBubbleMetadataFlagListener = listener;
mIntentCancelListener = intent -> {
if (mIntent != null) {
mIntent.unregisterCancelListener(mIntentCancelListener);
@@ -606,8 +606,8 @@
mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
}
- if (showInShade() != prevShowInShade && mSuppressionListener != null) {
- mSuppressionListener.onBubbleNotificationSuppressionChange(this);
+ if (showInShade() != prevShowInShade && mBubbleMetadataFlagListener != null) {
+ mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this);
}
}
@@ -626,8 +626,8 @@
} else {
mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE;
}
- if (prevSuppressed != suppressBubble && mSuppressionListener != null) {
- mSuppressionListener.onBubbleNotificationSuppressionChange(this);
+ if (prevSuppressed != suppressBubble && mBubbleMetadataFlagListener != null) {
+ mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this);
}
}
@@ -771,12 +771,17 @@
return isEnabled(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
}
- void setShouldAutoExpand(boolean shouldAutoExpand) {
+ @VisibleForTesting
+ public void setShouldAutoExpand(boolean shouldAutoExpand) {
+ boolean prevAutoExpand = shouldAutoExpand();
if (shouldAutoExpand) {
enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
} else {
disable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
}
+ if (prevAutoExpand != shouldAutoExpand && mBubbleMetadataFlagListener != null) {
+ mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this);
+ }
}
public void setIsBubble(final boolean isBubble) {
@@ -799,6 +804,10 @@
return (mFlags & option) != 0;
}
+ public int getFlags() {
+ return mFlags;
+ }
+
@Override
public String toString() {
return "Bubble{" + mKey + '}';
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 806c395..f407bdc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -323,7 +323,7 @@
public void initialize() {
mBubbleData.setListener(mBubbleDataListener);
- mBubbleData.setSuppressionChangedListener(this::onBubbleNotificationSuppressionChanged);
+ mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
mBubbleData.setPendingIntentCancelledListener(bubble -> {
if (bubble.getBubbleIntent() == null) {
@@ -554,11 +554,10 @@
}
@VisibleForTesting
- public void onBubbleNotificationSuppressionChanged(Bubble bubble) {
+ public void onBubbleMetadataFlagChanged(Bubble bubble) {
// Make sure NoMan knows suppression state so that anyone querying it can tell.
try {
- mBarService.onBubbleNotificationSuppressionChanged(bubble.getKey(),
- !bubble.showInShade(), bubble.isSuppressed());
+ mBarService.onBubbleMetadataFlagChanged(bubble.getKey(), bubble.getFlags());
} catch (RemoteException e) {
// Bad things have happened
}
@@ -1038,7 +1037,15 @@
}
} else {
Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */);
- inflateAndAdd(bubble, suppressFlyout, showInShade);
+ if (notif.shouldSuppressNotificationList()) {
+ // If we're suppressing notifs for DND, we don't want the bubbles to randomly
+ // expand when DND turns off so flip the flag.
+ if (bubble.shouldAutoExpand()) {
+ bubble.setShouldAutoExpand(false);
+ }
+ } else {
+ inflateAndAdd(bubble, suppressFlyout, showInShade);
+ }
}
}
@@ -1070,7 +1077,8 @@
}
}
- private void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) {
+ @VisibleForTesting
+ public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) {
// shouldBubbleUp checks canBubble & for bubble metadata
boolean shouldBubble = shouldBubbleUp && canLaunchInTaskView(mContext, entry);
if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) {
@@ -1096,7 +1104,8 @@
}
}
- private void onRankingUpdated(RankingMap rankingMap,
+ @VisibleForTesting
+ public void onRankingUpdated(RankingMap rankingMap,
HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey) {
if (mTmpRanking == null) {
mTmpRanking = new NotificationListenerService.Ranking();
@@ -1107,19 +1116,22 @@
Pair<BubbleEntry, Boolean> entryData = entryDataByKey.get(key);
BubbleEntry entry = entryData.first;
boolean shouldBubbleUp = entryData.second;
-
if (entry != null && !isCurrentProfile(
entry.getStatusBarNotification().getUser().getIdentifier())) {
return;
}
-
+ if (entry != null && (entry.shouldSuppressNotificationList()
+ || entry.getRanking().isSuspended())) {
+ shouldBubbleUp = false;
+ }
rankingMap.getRanking(key, mTmpRanking);
- boolean isActiveBubble = mBubbleData.hasAnyBubbleWithKey(key);
- if (isActiveBubble && !mTmpRanking.canBubble()) {
+ boolean isActiveOrInOverflow = mBubbleData.hasAnyBubbleWithKey(key);
+ boolean isActive = mBubbleData.hasBubbleInStackWithKey(key);
+ if (isActiveOrInOverflow && !mTmpRanking.canBubble()) {
// If this entry is no longer allowed to bubble, dismiss with the BLOCKED reason.
// This means that the app or channel's ability to bubble has been revoked.
mBubbleData.dismissBubbleWithKey(key, DISMISS_BLOCKED);
- } else if (isActiveBubble && (!shouldBubbleUp || entry.getRanking().isSuspended())) {
+ } else if (isActiveOrInOverflow && !shouldBubbleUp) {
// If this entry is allowed to bubble, but cannot currently bubble up or is
// suspended, dismiss it. This happens when DND is enabled and configured to hide
// bubbles, or focus mode is enabled and the app is designated as distracting.
@@ -1127,9 +1139,9 @@
// notification, so that the bubble will be re-created if shouldBubbleUp returns
// true.
mBubbleData.dismissBubbleWithKey(key, DISMISS_NO_BUBBLE_UP);
- } else if (entry != null && mTmpRanking.isBubble() && !isActiveBubble) {
+ } else if (entry != null && mTmpRanking.isBubble() && !isActive) {
entry.setFlagBubble(true);
- onEntryUpdated(entry, shouldBubbleUp && !entry.getRanking().isSuspended());
+ onEntryUpdated(entry, shouldBubbleUp);
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index c98c0e6..e4a0fd0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -159,7 +159,7 @@
private Listener mListener;
@Nullable
- private Bubbles.SuppressionChangedListener mSuppressionListener;
+ private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener;
private Bubbles.PendingIntentCanceledListener mCancelledListener;
/**
@@ -190,9 +190,8 @@
mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow);
}
- public void setSuppressionChangedListener(
- Bubbles.SuppressionChangedListener listener) {
- mSuppressionListener = listener;
+ public void setSuppressionChangedListener(Bubbles.BubbleMetadataFlagListener listener) {
+ mBubbleMetadataFlagListener = listener;
}
public void setPendingIntentCancelledListener(
@@ -311,7 +310,7 @@
bubbleToReturn = mPendingBubbles.get(key);
} else if (entry != null) {
// New bubble
- bubbleToReturn = new Bubble(entry, mSuppressionListener, mCancelledListener,
+ bubbleToReturn = new Bubble(entry, mBubbleMetadataFlagListener, mCancelledListener,
mMainExecutor);
} else {
// Persisted bubble being promoted
@@ -1058,6 +1057,22 @@
return null;
}
+ /**
+ * Get a pending bubble with given notification <code>key</code>
+ *
+ * @param key notification key
+ * @return bubble that matches or null
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ public Bubble getPendingBubbleWithKey(String key) {
+ for (Bubble b : mPendingBubbles.values()) {
+ if (b.getKey().equals(key)) {
+ return b;
+ }
+ }
+ return null;
+ }
+
@VisibleForTesting(visibility = PRIVATE)
void setTimeSource(TimeSource timeSource) {
mTimeSource = timeSource;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index 2b2a2f7..c7db8d8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -263,10 +263,10 @@
void onBubbleExpandChanged(boolean isExpanding, String key);
}
- /** Listener to be notified when the flags for notification or bubble suppression changes.*/
- interface SuppressionChangedListener {
- /** Called when the notification suppression state of a bubble changes. */
- void onBubbleNotificationSuppressionChange(Bubble bubble);
+ /** Listener to be notified when the flags on BubbleMetadata have changed. */
+ interface BubbleMetadataFlagListener {
+ /** Called when the flags on BubbleMetadata have changed for the provided bubble. */
+ void onBubbleMetadataFlagChanged(Bubble bubble);
}
/** Listener to be notified when a pending intent has been canceled for a bubble. */
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
index 169f03e..bde94d9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
@@ -115,7 +115,7 @@
private ArgumentCaptor<BubbleData.Update> mUpdateCaptor;
@Mock
- private Bubbles.SuppressionChangedListener mSuppressionListener;
+ private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener;
@Mock
private Bubbles.PendingIntentCanceledListener mPendingIntentCanceledListener;
@@ -136,30 +136,47 @@
mock(NotificationListenerService.Ranking.class);
when(ranking.isTextChanged()).thenReturn(true);
mEntryInterruptive = createBubbleEntry(1, "interruptive", "package.d", ranking);
- mBubbleInterruptive = new Bubble(mEntryInterruptive, mSuppressionListener, null,
+ mBubbleInterruptive = new Bubble(mEntryInterruptive, mBubbleMetadataFlagListener, null,
mMainExecutor);
mEntryDismissed = createBubbleEntry(1, "dismissed", "package.d", null);
- mBubbleDismissed = new Bubble(mEntryDismissed, mSuppressionListener, null,
+ mBubbleDismissed = new Bubble(mEntryDismissed, mBubbleMetadataFlagListener, null,
mMainExecutor);
mEntryLocusId = createBubbleEntry(1, "keyLocus", "package.e", null,
new LocusId("locusId1"));
- mBubbleLocusId = new Bubble(mEntryLocusId, mSuppressionListener, null, mMainExecutor);
+ mBubbleLocusId = new Bubble(mEntryLocusId,
+ mBubbleMetadataFlagListener,
+ null /* pendingIntentCanceledListener */,
+ mMainExecutor);
- mBubbleA1 = new Bubble(mEntryA1, mSuppressionListener, mPendingIntentCanceledListener,
+ mBubbleA1 = new Bubble(mEntryA1,
+ mBubbleMetadataFlagListener,
+ mPendingIntentCanceledListener,
mMainExecutor);
- mBubbleA2 = new Bubble(mEntryA2, mSuppressionListener, mPendingIntentCanceledListener,
+ mBubbleA2 = new Bubble(mEntryA2,
+ mBubbleMetadataFlagListener,
+ mPendingIntentCanceledListener,
mMainExecutor);
- mBubbleA3 = new Bubble(mEntryA3, mSuppressionListener, mPendingIntentCanceledListener,
+ mBubbleA3 = new Bubble(mEntryA3,
+ mBubbleMetadataFlagListener,
+ mPendingIntentCanceledListener,
mMainExecutor);
- mBubbleB1 = new Bubble(mEntryB1, mSuppressionListener, mPendingIntentCanceledListener,
+ mBubbleB1 = new Bubble(mEntryB1,
+ mBubbleMetadataFlagListener,
+ mPendingIntentCanceledListener,
mMainExecutor);
- mBubbleB2 = new Bubble(mEntryB2, mSuppressionListener, mPendingIntentCanceledListener,
+ mBubbleB2 = new Bubble(mEntryB2,
+ mBubbleMetadataFlagListener,
+ mPendingIntentCanceledListener,
mMainExecutor);
- mBubbleB3 = new Bubble(mEntryB3, mSuppressionListener, mPendingIntentCanceledListener,
+ mBubbleB3 = new Bubble(mEntryB3,
+ mBubbleMetadataFlagListener,
+ mPendingIntentCanceledListener,
mMainExecutor);
- mBubbleC1 = new Bubble(mEntryC1, mSuppressionListener, mPendingIntentCanceledListener,
+ mBubbleC1 = new Bubble(mEntryC1,
+ mBubbleMetadataFlagListener,
+ mPendingIntentCanceledListener,
mMainExecutor);
mPositioner = new TestableBubblePositioner(mContext,
mock(WindowManager.class));
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java
index 819a984..e8f3f69 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java
@@ -63,7 +63,7 @@
private Bubble mBubble;
@Mock
- private Bubbles.SuppressionChangedListener mSuppressionListener;
+ private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener;
@Before
public void setUp() {
@@ -81,7 +81,7 @@
when(mNotif.getBubbleMetadata()).thenReturn(metadata);
when(mSbn.getKey()).thenReturn("mock");
mBubbleEntry = new BubbleEntry(mSbn, null, true, false, false, false);
- mBubble = new Bubble(mBubbleEntry, mSuppressionListener, null, mMainExecutor);
+ mBubble = new Bubble(mBubbleEntry, mBubbleMetadataFlagListener, null, mMainExecutor);
}
@Test
@@ -144,22 +144,22 @@
}
@Test
- public void testSuppressionListener_change_notified() {
+ public void testBubbleMetadataFlagListener_change_notified() {
assertThat(mBubble.showInShade()).isTrue();
mBubble.setSuppressNotification(true);
assertThat(mBubble.showInShade()).isFalse();
- verify(mSuppressionListener).onBubbleNotificationSuppressionChange(mBubble);
+ verify(mBubbleMetadataFlagListener).onBubbleMetadataFlagChanged(mBubble);
}
@Test
- public void testSuppressionListener_noChange_doesntNotify() {
+ public void testBubbleMetadataFlagListener_noChange_doesntNotify() {
assertThat(mBubble.showInShade()).isTrue();
mBubble.setSuppressNotification(false);
- verify(mSuppressionListener, never()).onBubbleNotificationSuppressionChange(any());
+ verify(mBubbleMetadataFlagListener, never()).onBubbleMetadataFlagChanged(any());
}
}
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
index e8a1a5c..f333b86 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
@@ -150,8 +150,6 @@
mBtAdapter = mBtManager.getAdapter();
mBleScanner = mBtAdapter.getBluetoothLeScanner();
mWifiManager = getSystemService(WifiManager.class);
-
- sScanResultsLiveData.setValue(Collections.emptyList());
}
@Override
@@ -175,6 +173,7 @@
@Override
public void onDestroy() {
+ sScanResultsLiveData.setValue(Collections.emptyList());
super.onDestroy();
if (DEBUG) Log.d(TAG, "onDestroy()");
}
@@ -188,6 +187,7 @@
mStopAfterFirstMatch = request.isSingleDevice();
mDiscoveryStarted = true;
sStateLiveData.setValue(DiscoveryState.DISCOVERY_IN_PROGRESS);
+ sScanResultsLiveData.setValue(Collections.emptyList());
final List<DeviceFilter<?>> allFilters = request.getDeviceFilters();
final List<BluetoothDeviceFilter> btFilters =
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
index 76c1dbc..d9aa1ba 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
@@ -80,6 +80,7 @@
@VisibleForTesting
internal var startLightRevealScrimOnKeyguardFadingAway = false
+ var lightRevealScrimAnimator: ValueAnimator? = null
var fingerprintSensorLocation: PointF? = null
private var faceSensorLocation: PointF? = null
private var circleReveal: LightRevealEffect? = null
@@ -163,7 +164,8 @@
if (keyguardStateController.isKeyguardFadingAway) {
val lightRevealScrim = centralSurfaces.lightRevealScrim
if (startLightRevealScrimOnKeyguardFadingAway && lightRevealScrim != null) {
- ValueAnimator.ofFloat(.1f, 1f).apply {
+ lightRevealScrimAnimator?.cancel()
+ lightRevealScrimAnimator = ValueAnimator.ofFloat(.1f, 1f).apply {
interpolator = Interpolators.LINEAR_OUT_SLOW_IN
duration = RIPPLE_ANIMATION_DURATION
startDelay = keyguardStateController.keyguardFadingAwayDelay
@@ -183,6 +185,8 @@
if (lightRevealScrim.revealEffect == circleReveal) {
lightRevealScrim.revealEffect = LiftReveal
}
+
+ lightRevealScrimAnimator = null
}
})
start()
@@ -192,6 +196,13 @@
}
}
+ /**
+ * Whether we're animating the light reveal scrim from a call to [onKeyguardFadingAwayChanged].
+ */
+ fun isAnimatingLightRevealScrim(): Boolean {
+ return lightRevealScrimAnimator?.isRunning ?: false
+ }
+
override fun onStartedGoingToSleep() {
// reset the light reveal start in case we were pending an unlock
startLightRevealScrimOnKeyguardFadingAway = false
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
index f9115b2..3eb58bb 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
@@ -343,7 +343,7 @@
info.className = Switch::class.java.name
}
- override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean {
+ override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
if (super.performAccessibilityAction(host, action, args)) {
return true
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index e7f97d2..58b6ad3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -55,8 +55,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.graphics.drawable.IconCompat;
-import androidx.mediarouter.media.MediaRouter;
-import androidx.mediarouter.media.MediaRouterParams;
import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.Utils;
@@ -215,9 +213,8 @@
}
boolean shouldShowLaunchSection() {
- MediaRouterParams routerParams = MediaRouter.getInstance(mContext).getRouterParams();
- Log.d(TAG, "try to get routerParams: " + routerParams);
- return routerParams != null && !routerParams.isMediaTransferReceiverEnabled();
+ // TODO(b/231398073): Implements this when available.
+ return false;
}
void setRefreshing(boolean refreshing) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
index 3eb4b10..3e8cdf3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
@@ -420,7 +420,7 @@
PowerExemptionManager.REASON_SYSTEM_UID,
PowerExemptionManager.REASON_DEVICE_DEMO_MODE -> UIControl.HIDE_ENTRY
- PowerExemptionManager.REASON_ALLOWLISTED_PACKAGE,
+ PowerExemptionManager.REASON_SYSTEM_ALLOW_LISTED,
PowerExemptionManager.REASON_DEVICE_OWNER,
PowerExemptionManager.REASON_DISALLOW_APPS_CONTROL,
PowerExemptionManager.REASON_DPO_PROTECTED_APP,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
index ebd610b..0c9e1ec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
@@ -180,13 +180,6 @@
default void setRequestTopUi(boolean requestTopUi, String componentTag) {}
/**
- * Under low light conditions, we might want to increase the display brightness on devices that
- * don't have an IR camera.
- * @param brightness float from 0 to 1 or {@code LayoutParams.BRIGHTNESS_OVERRIDE_NONE}
- */
- default void setFaceAuthDisplayBrightness(float brightness) {}
-
- /**
* If {@link LightRevealScrim} obscures the UI.
* @param opaque if the scrim is opaque
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt
index a390e9f..15ad312 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt
@@ -20,11 +20,13 @@
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
+import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
import com.android.systemui.statusbar.notification.collection.render.NodeController
import com.android.systemui.statusbar.notification.dagger.PeopleHeader
+import com.android.systemui.statusbar.notification.icon.ConversationIconManager
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.PeopleNotificationType
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON
@@ -39,12 +41,40 @@
@CoordinatorScope
class ConversationCoordinator @Inject constructor(
private val peopleNotificationIdentifier: PeopleNotificationIdentifier,
+ private val conversationIconManager: ConversationIconManager,
@PeopleHeader peopleHeaderController: NodeController
) : Coordinator {
+ private val promotedEntriesToSummaryOfSameChannel =
+ mutableMapOf<NotificationEntry, NotificationEntry>()
+
+ private val onBeforeRenderListListener = OnBeforeRenderListListener { _ ->
+ val unimportantSummaries = promotedEntriesToSummaryOfSameChannel
+ .mapNotNull { (promoted, summary) ->
+ val originalGroup = summary.parent
+ when {
+ originalGroup == null -> null
+ originalGroup == promoted.parent -> null
+ originalGroup.parent == null -> null
+ originalGroup.summary != summary -> null
+ originalGroup.children.any { it.channel == summary.channel } -> null
+ else -> summary.key
+ }
+ }
+ conversationIconManager.setUnimportantConversations(unimportantSummaries)
+ promotedEntriesToSummaryOfSameChannel.clear()
+ }
+
private val notificationPromoter = object : NotifPromoter(TAG) {
override fun shouldPromoteToTopLevel(entry: NotificationEntry): Boolean {
- return entry.channel?.isImportantConversation == true
+ val shouldPromote = entry.channel?.isImportantConversation == true
+ if (shouldPromote) {
+ val summary = entry.parent?.summary
+ if (summary != null && entry.channel == summary.channel) {
+ promotedEntriesToSummaryOfSameChannel[entry] = summary
+ }
+ }
+ return shouldPromote
}
}
@@ -67,6 +97,7 @@
override fun attach(pipeline: NotifPipeline) {
pipeline.addPromoter(notificationPromoter)
+ pipeline.addOnBeforeRenderListListener(onBeforeRenderListListener)
}
private fun isConversation(entry: ListEntry): Boolean =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index 34c8044..d96590a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -70,6 +70,8 @@
import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewManager;
import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
+import com.android.systemui.statusbar.notification.icon.ConversationIconManager;
+import com.android.systemui.statusbar.notification.icon.IconManager;
import com.android.systemui.statusbar.notification.init.NotificationsController;
import com.android.systemui.statusbar.notification.init.NotificationsControllerImpl;
import com.android.systemui.statusbar.notification.init.NotificationsControllerStub;
@@ -370,6 +372,10 @@
/** */
@Binds
+ ConversationIconManager bindConversationIconManager(IconManager iconManager);
+
+ /** */
+ @Binds
BindEventManager bindBindEventManagerImpl(BindEventManagerImpl bindEventManagerImpl);
/** */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
index 5375ac3..d896541 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
@@ -27,6 +27,7 @@
import android.widget.ImageView
import com.android.internal.statusbar.StatusBarIcon
import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.notification.InflationException
import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -44,11 +45,14 @@
* TODO: Much of this code was copied whole-sale in order to get it out of NotificationEntry.
* Long-term, it should probably live somewhere in the content inflation pipeline.
*/
+@SysUISingleton
class IconManager @Inject constructor(
private val notifCollection: CommonNotifCollection,
private val launcherApps: LauncherApps,
private val iconBuilder: IconBuilder
-) {
+) : ConversationIconManager {
+ private var unimportantConversationKeys: Set<String> = emptySet()
+
fun attach() {
notifCollection.addCollectionListener(entryListener)
}
@@ -63,16 +67,8 @@
}
override fun onRankingApplied() {
- // When the sensitivity changes OR when the isImportantConversation status changes,
- // we need to update the icons
- for (entry in notifCollection.allNotifs) {
- val isImportant = isImportantConversation(entry)
- if (entry.icons.areIconsAvailable &&
- isImportant != entry.icons.isImportantConversation) {
- updateIconsSafe(entry)
- }
- entry.icons.isImportantConversation = isImportant
- }
+ // rankings affect whether a conversation is important, which can change the icons
+ recalculateForImportantConversationChange()
}
}
@@ -80,6 +76,18 @@
entry -> updateIconsSafe(entry)
}
+ private fun recalculateForImportantConversationChange() {
+ for (entry in notifCollection.allNotifs) {
+ val isImportant = isImportantConversation(entry)
+ if (entry.icons.areIconsAvailable &&
+ isImportant != entry.icons.isImportantConversation
+ ) {
+ updateIconsSafe(entry)
+ }
+ entry.icons.isImportantConversation = isImportant
+ }
+ }
+
/**
* Inflate icon views for each icon variant and assign appropriate icons to them. Stores the
* result in [NotificationEntry.getIcons].
@@ -306,8 +314,28 @@
}
private fun isImportantConversation(entry: NotificationEntry): Boolean {
- return entry.ranking.channel != null && entry.ranking.channel.isImportantConversation
+ return entry.ranking.channel != null &&
+ entry.ranking.channel.isImportantConversation &&
+ entry.key !in unimportantConversationKeys
+ }
+
+ override fun setUnimportantConversations(keys: Collection<String>) {
+ val newKeys = keys.toSet()
+ val changed = unimportantConversationKeys != newKeys
+ unimportantConversationKeys = newKeys
+ if (changed) {
+ recalculateForImportantConversationChange()
+ }
}
}
-private const val TAG = "IconManager"
\ No newline at end of file
+private const val TAG = "IconManager"
+
+interface ConversationIconManager {
+ /**
+ * Sets the complete current set of notification keys which should (for the purposes of icon
+ * presentation) be considered unimportant. This tells the icon manager to remove the avatar
+ * of a group from which the priority notification has been removed.
+ */
+ fun setUnimportantConversations(keys: Collection<String>)
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
index 5646545..0ff15238 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
@@ -164,12 +164,23 @@
!lockscreenUserManager.shouldShowLockscreenNotifications() -> true
// User settings do not allow this notification on the lockscreen, so hide it.
userSettingsDisallowNotification(entry) -> true
+ // if entry is silent, apply custom logic to see if should hide
+ shouldHideIfEntrySilent(entry) -> true
+ else -> false
+ }
+
+ private fun shouldHideIfEntrySilent(entry: ListEntry): Boolean = when {
+ // Show if high priority (not hidden)
+ highPriorityProvider.isHighPriority(entry) -> false
+ // Ambient notifications are hidden always from lock screen
+ entry.representativeEntry?.isAmbient == true -> true
+ // [Now notification is silent]
+ // Hide regardless of parent priority if user wants silent notifs hidden
+ hideSilentNotificationsOnLockscreen -> true
// Parent priority is high enough to be shown on the lockscreen, do not hide.
- entry.parent?.let(::priorityExceedsLockscreenShowingThreshold) == true -> false
- // Entry priority is high enough to be shown on the lockscreen, do not hide.
- priorityExceedsLockscreenShowingThreshold(entry) -> false
- // Priority is too low, hide.
- else -> true
+ entry.parent?.let(::shouldHideIfEntrySilent) == false -> false
+ // Show when silent notifications are allowed on lockscreen
+ else -> false
}
private fun userSettingsDisallowNotification(entry: NotificationEntry): Boolean {
@@ -193,11 +204,6 @@
}
}
- private fun priorityExceedsLockscreenShowingThreshold(entry: ListEntry): Boolean = when {
- hideSilentNotificationsOnLockscreen -> highPriorityProvider.isHighPriority(entry)
- else -> entry.representativeEntry?.ranking?.isAmbient == false
- }
-
private fun readShowSilentNotificationSetting() {
val showSilentNotifs =
secureSettings.getBoolForUser(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index ef24d77..5c12671 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -1022,6 +1022,23 @@
public void onUnlockedChanged() {
logStateToEventlog();
}
+
+ @Override
+ public void onKeyguardGoingAwayChanged() {
+ // The light reveal scrim should always be fully revealed by the time the keyguard
+ // is done going away. Double check that this is true.
+ if (!mKeyguardStateController.isKeyguardGoingAway()) {
+ if (mLightRevealScrim.getRevealAmount() != 1f) {
+ Log.e(TAG, "Keyguard is done going away, but someone left the light reveal "
+ + "scrim at reveal amount: " + mLightRevealScrim.getRevealAmount());
+ }
+
+ // If the auth ripple is still playing, let it finish.
+ if (!mAuthRippleController.isAnimatingLightRevealScrim()) {
+ mLightRevealScrim.setRevealAmount(1f);
+ }
+ }
+ }
});
startKeyguard();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java
index 24660b2..01aa2ec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java
@@ -111,7 +111,6 @@
private final SysuiColorExtractor mColorExtractor;
private final ScreenOffAnimationController mScreenOffAnimationController;
- private float mFaceAuthDisplayBrightness = LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
/**
* Layout params would be aggregated and dispatched all at once if this is > 0.
*
@@ -266,12 +265,6 @@
mScreenBrightnessDoze = value / 255f;
}
- @Override
- public void setFaceAuthDisplayBrightness(float brightness) {
- mFaceAuthDisplayBrightness = brightness;
- apply(mCurrentState);
- }
-
private void setKeyguardDark(boolean dark) {
int vis = mNotificationShadeView.getSystemUiVisibility();
if (dark) {
@@ -455,7 +448,9 @@
private void applyWindowLayoutParams() {
if (mDeferWindowLayoutParams == 0 && mLp != null && mLp.copyFrom(mLpChanged) != 0) {
+ Trace.beginSection("updateViewLayout");
mWindowManager.updateViewLayout(mNotificationShadeView, mLp);
+ Trace.endSection();
}
}
@@ -523,7 +518,7 @@
if (state.mForceDozeBrightness) {
mLpChanged.screenBrightness = mScreenBrightnessDoze;
} else {
- mLpChanged.screenBrightness = mFaceAuthDisplayBrightness;
+ mLpChanged.screenBrightness = LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
}
}
@@ -572,6 +567,10 @@
@Override
public void setPanelVisible(boolean visible) {
+ if (mCurrentState.mPanelVisible == visible
+ && mCurrentState.mNotificationShadeFocusable == visible) {
+ return;
+ }
mCurrentState.mPanelVisible = visible;
mCurrentState.mNotificationShadeFocusable = visible;
apply(mCurrentState);
@@ -626,8 +625,14 @@
@Override
public void setScrimsVisibility(int scrimsVisibility) {
+ if (scrimsVisibility == mCurrentState.mScrimsVisibility) {
+ return;
+ }
+ boolean wasExpanded = isExpanded(mCurrentState);
mCurrentState.mScrimsVisibility = scrimsVisibility;
- apply(mCurrentState);
+ if (wasExpanded != isExpanded(mCurrentState)) {
+ apply(mCurrentState);
+ }
mScrimsVisibilityListener.accept(scrimsVisibility);
}
@@ -687,6 +692,9 @@
@Override
public void setPanelExpanded(boolean isExpanded) {
+ if (mCurrentState.mPanelExpanded == isExpanded) {
+ return;
+ }
mCurrentState.mPanelExpanded = isExpanded;
apply(mCurrentState);
}
@@ -703,6 +711,9 @@
*/
@Override
public void setForceDozeBrightness(boolean forceDozeBrightness) {
+ if (mCurrentState.mForceDozeBrightness == forceDozeBrightness) {
+ return;
+ }
mCurrentState.mForceDozeBrightness = forceDozeBrightness;
apply(mCurrentState);
}
@@ -841,7 +852,6 @@
boolean mLightRevealScrimOpaque;
boolean mForceCollapsed;
boolean mForceDozeBrightness;
- int mFaceAuthDisplayBrightness;
boolean mForceUserActivity;
boolean mLaunchingActivity;
boolean mBackdropShowing;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
index 935f87d..c1d0769 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
@@ -175,28 +175,36 @@
.setDuration(duration.toLong())
.setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
.alpha(1f)
+ .withEndAction {
+ aodUiAnimationPlaying = false
+
+ // Lock the keyguard if it was waiting for the screen off animation to end.
+ keyguardViewMediatorLazy.get().maybeHandlePendingLock()
+
+ // Tell the CentralSurfaces to become keyguard for real - we waited on that
+ // since it is slow and would have caused the animation to jank.
+ mCentralSurfaces.updateIsKeyguard()
+
+ // Run the callback given to us by the KeyguardVisibilityHelper.
+ after.run()
+
+ // Done going to sleep, reset this flag.
+ decidedToAnimateGoingToSleep = null
+
+ // We need to unset the listener. These are persistent for future animators
+ keyguardView.animate().setListener(null)
+ interactionJankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD)
+ }
.setListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator?) {
- aodUiAnimationPlaying = false
-
- // Lock the keyguard if it was waiting for the screen off animation to end.
- keyguardViewMediatorLazy.get().maybeHandlePendingLock()
-
- // Tell the CentralSurfaces to become keyguard for real - we waited on that
- // since it is slow and would have caused the animation to jank.
- mCentralSurfaces.updateIsKeyguard()
-
- // Run the callback given to us by the KeyguardVisibilityHelper.
- after.run()
-
- // Done going to sleep, reset this flag.
- decidedToAnimateGoingToSleep = null
- // We need to unset the listener. These are persistent for future animators
- keyguardView.animate().setListener(null)
- interactionJankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD)
- }
-
override fun onAnimationCancel(animation: Animator?) {
+ // If we're cancelled, reset state flags/listeners. The end action above
+ // will not be called, which is what we want since that will finish the
+ // screen off animation and show the lockscreen, which we don't want if we
+ // were cancelled.
+ aodUiAnimationPlaying = false
+ decidedToAnimateGoingToSleep = null
+ keyguardView.animate().setListener(null)
+
interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index 3751721..5a33603 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -269,8 +269,12 @@
super.onEnd(animation);
if (animation.getTypeMask() == WindowInsets.Type.ime()) {
mEntry.mRemoteEditImeAnimatingAway = false;
- mEntry.mRemoteEditImeVisible =
- mEditText.getRootWindowInsets().isVisible(WindowInsets.Type.ime());
+ WindowInsets editTextRootWindowInsets = mEditText.getRootWindowInsets();
+ if (editTextRootWindowInsets == null) {
+ Log.w(TAG, "onEnd called on detached view", new Exception());
+ }
+ mEntry.mRemoteEditImeVisible = editTextRootWindowInsets != null
+ && editTextRootWindowInsets.isVisible(WindowInsets.Type.ime());
if (!mEntry.mRemoteEditImeVisible && !mEditText.mShowImeOnInputConnection) {
mController.removeRemoteInput(mEntry, mToken);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
index b45d78d..4b458f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
@@ -88,4 +88,7 @@
return this;
}
+ public static List<NotificationEntry> getRawChildren(GroupEntry groupEntry) {
+ return groupEntry.getRawChildren();
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt
index 7692a05..742fcf5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt
@@ -21,17 +21,22 @@
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.GroupEntry
+import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
+import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
import com.android.systemui.statusbar.notification.collection.render.NodeController
+import com.android.systemui.statusbar.notification.icon.ConversationIconManager
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_IMPORTANT_PERSON
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_PERSON
+import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertFalse
@@ -52,8 +57,10 @@
private lateinit var promoter: NotifPromoter
private lateinit var peopleSectioner: NotifSectioner
private lateinit var peopleComparator: NotifComparator
+ private lateinit var beforeRenderListListener: OnBeforeRenderListListener
@Mock private lateinit var pipeline: NotifPipeline
+ @Mock private lateinit var conversationIconManager: ConversationIconManager
@Mock private lateinit var peopleNotificationIdentifier: PeopleNotificationIdentifier
@Mock private lateinit var channel: NotificationChannel
@Mock private lateinit var headerController: NodeController
@@ -66,7 +73,11 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- coordinator = ConversationCoordinator(peopleNotificationIdentifier, headerController)
+ coordinator = ConversationCoordinator(
+ peopleNotificationIdentifier,
+ conversationIconManager,
+ headerController
+ )
whenever(channel.isImportantConversation).thenReturn(true)
coordinator.attach(pipeline)
@@ -75,6 +86,9 @@
promoter = withArgCaptor {
verify(pipeline).addPromoter(capture())
}
+ beforeRenderListListener = withArgCaptor {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
peopleSectioner = coordinator.sectioner
peopleComparator = peopleSectioner.comparator!!
@@ -96,6 +110,25 @@
}
@Test
+ fun testPromotedImportantConversationsMakesSummaryUnimportant() {
+ val altChildA = NotificationEntryBuilder().setTag("A").build()
+ val altChildB = NotificationEntryBuilder().setTag("B").build()
+ val summary = NotificationEntryBuilder().setId(2).setChannel(channel).build()
+ val groupEntry = GroupEntryBuilder()
+ .setParent(GroupEntry.ROOT_ENTRY)
+ .setSummary(summary)
+ .setChildren(listOf(entry, altChildA, altChildB))
+ .build()
+ assertTrue(promoter.shouldPromoteToTopLevel(entry))
+ assertFalse(promoter.shouldPromoteToTopLevel(altChildA))
+ assertFalse(promoter.shouldPromoteToTopLevel(altChildB))
+ NotificationEntryBuilder.setNewParent(entry, GroupEntry.ROOT_ENTRY)
+ GroupEntryBuilder.getRawChildren(groupEntry).remove(entry)
+ beforeRenderListListener.onBeforeRenderList(listOf(entry, groupEntry))
+ verify(conversationIconManager).setUnimportantConversations(eq(listOf(summary.key)))
+ }
+
+ @Test
fun testInPeopleSection() {
whenever(peopleNotificationIdentifier.getPeopleNotificationType(entry))
.thenReturn(TYPE_PERSON)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java
index cf99607..ed455a3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java
@@ -27,6 +27,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
@@ -228,6 +229,41 @@
}
@Test
+ public void hideSilentNotificationsPerUserSettingWithHighPriorityParent() {
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, true);
+ mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, false);
+ GroupEntry parent = new GroupEntryBuilder()
+ .setKey("parent")
+ .addChild(mEntry)
+ .setSummary(new NotificationEntryBuilder()
+ .setUser(new UserHandle(NOTIF_USER_ID))
+ .setImportance(IMPORTANCE_LOW)
+ .build())
+ .build();
+ mEntry = new NotificationEntryBuilder()
+ .setUser(new UserHandle(NOTIF_USER_ID))
+ .setImportance(IMPORTANCE_LOW)
+ .setParent(parent)
+ .build();
+ when(mHighPriorityProvider.isHighPriority(any())).thenReturn(false);
+ assertTrue(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry));
+ }
+
+ @Test
+ public void hideSilentNotificationsPerUserSetting() {
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, true);
+ mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, false);
+ mEntry = new NotificationEntryBuilder()
+ .setUser(new UserHandle(NOTIF_USER_ID))
+ .setImportance(IMPORTANCE_LOW)
+ .build();
+ when(mHighPriorityProvider.isHighPriority(any())).thenReturn(false);
+ assertTrue(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry));
+ }
+
+ @Test
public void notifyListeners_onSettingChange_zenMode() {
when(mKeyguardStateController.isShowing()).thenReturn(true);
Consumer<String> listener = mock(Consumer.class);
@@ -384,8 +420,8 @@
mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, false);
when(mHighPriorityProvider.isHighPriority(parent)).thenReturn(true);
- // THEN don't filter out the entry
- assertFalse(
+ // THEN filter out the entry regardless of parent
+ assertTrue(
mKeyguardNotificationVisibilityProvider.shouldHideNotification(entryWithParent));
// WHEN its parent doesn't exceed threshold to show on lockscreen
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java
index ddccd83..26199d53 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java
@@ -30,6 +30,7 @@
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.app.IActivityManager;
@@ -103,6 +104,7 @@
mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView);
mNotificationShadeWindowController.attach();
+ verify(mWindowManager).addView(eq(mNotificationShadeWindowView), any());
}
@Test
@@ -174,6 +176,14 @@
}
@Test
+ public void setScrimsVisibility_earlyReturn() {
+ clearInvocations(mWindowManager);
+ mNotificationShadeWindowController.setScrimsVisibility(ScrimController.TRANSPARENT);
+ // Abort early if value didn't change
+ verify(mWindowManager, never()).updateViewLayout(any(), mLayoutParameters.capture());
+ }
+
+ @Test
public void attach_animatingKeyguardAndSurface_wallpaperVisible() {
clearInvocations(mWindowManager);
when(mKeyguardViewMediator.isShowingAndNotOccluded()).thenReturn(true);
@@ -221,6 +231,8 @@
public void setPanelExpanded_notFocusable_altFocusable_whenPanelIsOpen() {
mNotificationShadeWindowController.setPanelExpanded(true);
clearInvocations(mWindowManager);
+ mNotificationShadeWindowController.setPanelExpanded(true);
+ verifyNoMoreInteractions(mWindowManager);
mNotificationShadeWindowController.setNotificationShadeFocusable(true);
verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture());
@@ -287,6 +299,8 @@
public void batchApplyWindowLayoutParams_doesNotDispatchEvents() {
mNotificationShadeWindowController.setForceDozeBrightness(true);
verify(mWindowManager).updateViewLayout(any(), any());
+ mNotificationShadeWindowController.setForceDozeBrightness(true);
+ verifyNoMoreInteractions(mWindowManager);
clearInvocations(mWindowManager);
mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
index 0936b77..0112797 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
@@ -117,11 +117,18 @@
val keyguardSpy = spy(keyguardView)
Mockito.`when`(keyguardSpy.animate()).thenReturn(animator)
val listener = ArgumentCaptor.forClass(Animator.AnimatorListener::class.java)
+ val endAction = ArgumentCaptor.forClass(Runnable::class.java)
controller.animateInKeyguard(keyguardSpy, Runnable {})
Mockito.verify(animator).setListener(listener.capture())
- // Verify that the listener is cleared when it ends
- listener.value.onAnimationEnd(null)
+ Mockito.verify(animator).withEndAction(endAction.capture())
+
+ // Verify that the listener is cleared if we cancel it.
+ listener.value.onAnimationCancel(null)
Mockito.verify(animator).setListener(null)
+
+ // Verify that the listener is also cleared if the end action is triggered.
+ endAction.value.run()
+ verify(animator, times(2)).setListener(null)
}
/**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 193879e..238a4d3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -57,6 +57,7 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
@@ -70,6 +71,8 @@
import android.service.notification.ZenModeConfig;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
+import android.util.Pair;
+import android.util.SparseArray;
import android.view.View;
import android.view.WindowManager;
@@ -147,6 +150,7 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.HashMap;
import java.util.List;
import java.util.Optional;
@@ -1010,7 +1014,7 @@
assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
// Should notify delegate that shade state changed
- verify(mBubbleController).onBubbleNotificationSuppressionChanged(
+ verify(mBubbleController).onBubbleMetadataFlagChanged(
mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
}
@@ -1027,7 +1031,7 @@
assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
// Should notify delegate that shade state changed
- verify(mBubbleController).onBubbleNotificationSuppressionChanged(
+ verify(mBubbleController).onBubbleMetadataFlagChanged(
mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
}
@@ -1447,6 +1451,69 @@
assertThat(stackView.getVisibility()).isEqualTo(View.VISIBLE);
}
+ @Test
+ public void testSetShouldAutoExpand_notifiesFlagChanged() {
+ mEntryListener.onPendingEntryAdded(mRow);
+
+ assertTrue(mBubbleController.hasBubbles());
+ Bubble b = mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey());
+ assertThat(b.shouldAutoExpand()).isFalse();
+
+ // Set it to the same thing
+ b.setShouldAutoExpand(false);
+
+ // Verify it doesn't notify
+ verify(mBubbleController, never()).onBubbleMetadataFlagChanged(any());
+
+ // Set it to something different
+ b.setShouldAutoExpand(true);
+ verify(mBubbleController).onBubbleMetadataFlagChanged(b);
+ }
+
+ @Test
+ public void testUpdateBubble_skipsDndSuppressListNotifs() {
+ mBubbleEntry = new BubbleEntry(mRow.getSbn(), mRow.getRanking(), mRow.isDismissable(),
+ mRow.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */,
+ mRow.shouldSuppressPeek());
+ mBubbleEntry.getBubbleMetadata().setFlags(
+ Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
+
+ mBubbleController.updateBubble(mBubbleEntry);
+
+ Bubble b = mBubbleData.getPendingBubbleWithKey(mBubbleEntry.getKey());
+ assertThat(b.shouldAutoExpand()).isFalse();
+ assertThat(mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey())).isNull();
+ }
+
+ @Test
+ public void testOnRankingUpdate_DndSuppressListNotif() {
+ // It's in the stack
+ mBubbleController.updateBubble(mBubbleEntry);
+ assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isTrue();
+
+ // Set current user profile
+ SparseArray<UserInfo> userInfos = new SparseArray<>();
+ userInfos.put(mBubbleEntry.getStatusBarNotification().getUser().getIdentifier(),
+ mock(UserInfo.class));
+ mBubbleController.onCurrentProfilesChanged(userInfos);
+
+ // Send ranking update that the notif is suppressed from the list.
+ HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey = new HashMap<>();
+ mBubbleEntry = new BubbleEntry(mRow.getSbn(), mRow.getRanking(), mRow.isDismissable(),
+ mRow.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */,
+ mRow.shouldSuppressPeek());
+ Pair<BubbleEntry, Boolean> pair = new Pair(mBubbleEntry, true);
+ entryDataByKey.put(mBubbleEntry.getKey(), pair);
+
+ NotificationListenerService.RankingMap rankingMap =
+ mock(NotificationListenerService.RankingMap.class);
+ when(rankingMap.getOrderedKeys()).thenReturn(new String[] { mBubbleEntry.getKey() });
+ mBubbleController.onRankingUpdated(rankingMap, entryDataByKey);
+
+ // Should no longer be in the stack
+ assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isFalse();
+ }
+
/** Creates a bubble using the userId and package. */
private Bubble createBubble(int userId, String pkg) {
final UserHandle userHandle = new UserHandle(userId);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
index 02d8691..dff89e0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
@@ -50,6 +50,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.LauncherApps;
+import android.content.pm.UserInfo;
import android.hardware.display.AmbientDisplayConfiguration;
import android.os.Handler;
import android.os.PowerManager;
@@ -59,6 +60,8 @@
import android.service.notification.ZenModeConfig;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
+import android.util.Pair;
+import android.util.SparseArray;
import android.view.View;
import android.view.WindowManager;
@@ -128,6 +131,7 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.HashMap;
import java.util.List;
import java.util.Optional;
@@ -880,7 +884,7 @@
assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
// Should notify delegate that shade state changed
- verify(mBubbleController).onBubbleNotificationSuppressionChanged(
+ verify(mBubbleController).onBubbleMetadataFlagChanged(
mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
}
@@ -897,7 +901,7 @@
assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
// Should notify delegate that shade state changed
- verify(mBubbleController).onBubbleNotificationSuppressionChanged(
+ verify(mBubbleController).onBubbleMetadataFlagChanged(
mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
}
@@ -1267,6 +1271,69 @@
assertThat(stackView.getVisibility()).isEqualTo(View.VISIBLE);
}
+ @Test
+ public void testSetShouldAutoExpand_notifiesFlagChanged() {
+ mBubbleController.updateBubble(mBubbleEntry);
+
+ assertTrue(mBubbleController.hasBubbles());
+ Bubble b = mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey());
+ assertThat(b.shouldAutoExpand()).isFalse();
+
+ // Set it to the same thing
+ b.setShouldAutoExpand(false);
+
+ // Verify it doesn't notify
+ verify(mBubbleController, never()).onBubbleMetadataFlagChanged(any());
+
+ // Set it to something different
+ b.setShouldAutoExpand(true);
+ verify(mBubbleController).onBubbleMetadataFlagChanged(b);
+ }
+
+ @Test
+ public void testUpdateBubble_skipsDndSuppressListNotifs() {
+ mBubbleEntry = new BubbleEntry(mRow.getSbn(), mRow.getRanking(), mRow.isDismissable(),
+ mRow.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */,
+ mRow.shouldSuppressPeek());
+ mBubbleEntry.getBubbleMetadata().setFlags(
+ Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
+
+ mBubbleController.updateBubble(mBubbleEntry);
+
+ Bubble b = mBubbleData.getPendingBubbleWithKey(mBubbleEntry.getKey());
+ assertThat(b.shouldAutoExpand()).isFalse();
+ assertThat(mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey())).isNull();
+ }
+
+ @Test
+ public void testOnRankingUpdate_DndSuppressListNotif() {
+ // It's in the stack
+ mBubbleController.updateBubble(mBubbleEntry);
+ assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isTrue();
+
+ // Set current user profile
+ SparseArray<UserInfo> userInfos = new SparseArray<>();
+ userInfos.put(mBubbleEntry.getStatusBarNotification().getUser().getIdentifier(),
+ mock(UserInfo.class));
+ mBubbleController.onCurrentProfilesChanged(userInfos);
+
+ // Send ranking update that the notif is suppressed from the list.
+ HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey = new HashMap<>();
+ mBubbleEntry = new BubbleEntry(mRow.getSbn(), mRow.getRanking(), mRow.isDismissable(),
+ mRow.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */,
+ mRow.shouldSuppressPeek());
+ Pair<BubbleEntry, Boolean> pair = new Pair(mBubbleEntry, true);
+ entryDataByKey.put(mBubbleEntry.getKey(), pair);
+
+ NotificationListenerService.RankingMap rankingMap =
+ mock(NotificationListenerService.RankingMap.class);
+ when(rankingMap.getOrderedKeys()).thenReturn(new String[] { mBubbleEntry.getKey() });
+ mBubbleController.onRankingUpdated(rankingMap, entryDataByKey);
+
+ // Should no longer be in the stack
+ assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isFalse();
+ }
+
/**
* Sets the bubble metadata flags for this entry. These flags are normally set by
* NotificationManagerService when the notification is sent, however, these tests do not
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index b4c107c..62bb9f1 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -294,6 +294,15 @@
private boolean onCompanionApplicationBindingDiedInternal(
@UserIdInt int userId, @NonNull String packageName) {
+ // Update the current connected devices sets when binderDied, so that application is able
+ // to call notifyDeviceAppeared after re-launch the application.
+ for (AssociationInfo ai :
+ mAssociationStore.getAssociationsForPackage(userId, packageName)) {
+ int id = ai.getId();
+ Slog.i(TAG, "Removing association id: " + id + " for package: "
+ + packageName + " due to binderDied.");
+ mDevicePresenceMonitor.removeDeviceFromMonitoring(id);
+ }
// TODO(b/218613015): implement.
return false;
}
diff --git a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
index 6371b25..24be1b6 100644
--- a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
+++ b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
@@ -206,6 +206,15 @@
}
/**
+ * Remove the current connected devices by associationId.
+ */
+ public void removeDeviceFromMonitoring(int associationId) {
+ mConnectedBtDevices.remove(associationId);
+ mNearbyBleDevices.remove(associationId);
+ mReportedSelfManagedDevices.remove(associationId);
+ }
+
+ /**
* Implements
* {@link AssociationStore.OnChangeListener#onAssociationRemoved(AssociationInfo)}
*/
@@ -217,9 +226,7 @@
Log.d(TAG, " > association=" + association);
}
- mConnectedBtDevices.remove(id);
- mNearbyBleDevices.remove(id);
- mReportedSelfManagedDevices.remove(id);
+ removeDeviceFromMonitoring(id);
// Do NOT call mCallback.onDeviceDisappeared()!
// CompanionDeviceManagerService will know that the association is removed, and will do
diff --git a/services/core/java/com/android/server/am/AppRestrictionController.java b/services/core/java/com/android/server/am/AppRestrictionController.java
index f7abb11..6f5d87b 100644
--- a/services/core/java/com/android/server/am/AppRestrictionController.java
+++ b/services/core/java/com/android/server/am/AppRestrictionController.java
@@ -265,6 +265,20 @@
*/
private int[] mDeviceIdleExceptIdleAllowlist = new int[0]; // No lock is needed.
+ /**
+ * The pre-configured system app-ids in the power-save allow list.
+ *
+ * @see #mDeviceIdleAllowlist.
+ */
+ private final ArraySet<Integer> mSystemDeviceIdleAllowlist = new ArraySet<>();
+
+ /**
+ * The pre-configured system app-ids in the power-save allow list, except-idle.
+ *
+ * @see #mDeviceIdleExceptIdleAllowlist.
+ */
+ private final ArraySet<Integer> mSystemDeviceIdleExceptIdleAllowlist = new ArraySet<>();
+
private final Object mLock = new Object();
private final Object mSettingsLock = new Object();
private final Injector mInjector;
@@ -1511,14 +1525,33 @@
}
private void initBgRestrictionExemptioFromSysConfig() {
- mBgRestrictionExemptioFromSysConfig =
- SystemConfig.getInstance().getBgRestrictionExemption();
+ final SystemConfig sysConfig = SystemConfig.getInstance();
+ mBgRestrictionExemptioFromSysConfig = sysConfig.getBgRestrictionExemption();
if (DEBUG_BG_RESTRICTION_CONTROLLER) {
final ArraySet<String> exemptedPkgs = mBgRestrictionExemptioFromSysConfig;
for (int i = exemptedPkgs.size() - 1; i >= 0; i--) {
Slog.i(TAG, "bg-restriction-exemption: " + exemptedPkgs.valueAt(i));
}
}
+ loadAppIdsFromPackageList(sysConfig.getAllowInPowerSaveExceptIdle(),
+ mSystemDeviceIdleExceptIdleAllowlist);
+ loadAppIdsFromPackageList(sysConfig.getAllowInPowerSave(), mSystemDeviceIdleAllowlist);
+ }
+
+ private void loadAppIdsFromPackageList(ArraySet<String> packages, ArraySet<Integer> apps) {
+ final PackageManager pm = mInjector.getPackageManager();
+ for (int i = packages.size() - 1; i >= 0; i--) {
+ final String pkg = packages.valueAt(i);
+ try {
+ final ApplicationInfo ai = pm.getApplicationInfo(pkg,
+ PackageManager.MATCH_SYSTEM_ONLY);
+ if (ai == null) {
+ continue;
+ }
+ apps.add(UserHandle.getAppId(ai.uid));
+ } catch (PackageManager.NameNotFoundException e) {
+ }
+ }
}
private boolean isExemptedFromSysConfig(String packageName) {
@@ -2685,6 +2718,13 @@
|| Arrays.binarySearch(mDeviceIdleExceptIdleAllowlist, appId) >= 0;
}
+ boolean isOnSystemDeviceIdleAllowlist(int uid) {
+ final int appId = UserHandle.getAppId(uid);
+
+ return mSystemDeviceIdleAllowlist.contains(appId)
+ || mSystemDeviceIdleExceptIdleAllowlist.contains(appId);
+ }
+
void setDeviceIdleAllowlist(int[] allAppids, int[] exceptIdleAppids) {
mDeviceIdleAllowlist = allAppids;
mDeviceIdleExceptIdleAllowlist = exceptIdleAppids;
@@ -2703,6 +2743,9 @@
if (UserHandle.isCore(uid)) {
return REASON_SYSTEM_UID;
}
+ if (isOnSystemDeviceIdleAllowlist(uid)) {
+ return REASON_SYSTEM_ALLOW_LISTED;
+ }
if (isOnDeviceIdleAllowlist(uid)) {
return REASON_ALLOWLISTED_PACKAGE;
}
@@ -2748,7 +2791,7 @@
} else if (isExemptedFromSysConfig(pkg)) {
return REASON_SYSTEM_ALLOW_LISTED;
} else if (mConstantsObserver.mBgRestrictionExemptedPackages.contains(pkg)) {
- return REASON_ALLOWLISTED_PACKAGE;
+ return REASON_SYSTEM_ALLOW_LISTED;
} else if (pm.isPackageStateProtected(pkg, userId)) {
return REASON_DPO_PROTECTED_APP;
}
diff --git a/services/core/java/com/android/server/notification/NotificationDelegate.java b/services/core/java/com/android/server/notification/NotificationDelegate.java
index 02f9ceb..89902f7 100644
--- a/services/core/java/com/android/server/notification/NotificationDelegate.java
+++ b/services/core/java/com/android/server/notification/NotificationDelegate.java
@@ -51,14 +51,16 @@
void onNotificationSettingsViewed(String key);
/**
* Called when the state of {@link Notification#FLAG_BUBBLE} is changed.
+ *
+ * @param key the notification key
+ * @param isBubble whether the notification should have {@link Notification#FLAG_BUBBLE} applied
+ * @param flags the flags to apply to the notification's {@link Notification.BubbleMetadata}
*/
void onNotificationBubbleChanged(String key, boolean isBubble, int flags);
/**
- * Called when the state of {@link Notification.BubbleMetadata#FLAG_SUPPRESS_NOTIFICATION}
- * or {@link Notification.BubbleMetadata#FLAG_SUPPRESS_BUBBLE} changes.
+ * Called when the flags on {@link Notification.BubbleMetadata} are changed.
*/
- void onBubbleNotificationSuppressionChanged(String key, boolean isNotifSuppressed,
- boolean isBubbleSuppressed);
+ void onBubbleMetadataFlagChanged(String key, int flags);
/**
* Grant permission to read the specified URI to the package associated with the
diff --git a/services/core/java/com/android/server/notification/NotificationManagerInternal.java b/services/core/java/com/android/server/notification/NotificationManagerInternal.java
index c548e7e..8a62736 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerInternal.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerInternal.java
@@ -42,4 +42,7 @@
/** Does the specified package/uid have permission to post notifications? */
boolean areNotificationsEnabledForPackage(String pkg, int uid);
+
+ /** Send a notification to the user prompting them to review their notification permissions. */
+ void sendReviewPermissionsNotification();
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 6078bfc..83c576e 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -18,6 +18,7 @@
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY;
import static android.app.Notification.FLAG_BUBBLE;
import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
@@ -274,6 +275,7 @@
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.messages.nano.SystemMessageProto;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.os.BackgroundThread;
import com.android.internal.os.SomeArgs;
@@ -442,6 +444,18 @@
private static final int NOTIFICATION_INSTANCE_ID_MAX = (1 << 13);
+ // States for the review permissions notification
+ static final int REVIEW_NOTIF_STATE_UNKNOWN = -1;
+ static final int REVIEW_NOTIF_STATE_SHOULD_SHOW = 0;
+ static final int REVIEW_NOTIF_STATE_USER_INTERACTED = 1;
+ static final int REVIEW_NOTIF_STATE_DISMISSED = 2;
+ static final int REVIEW_NOTIF_STATE_RESHOWN = 3;
+
+ // Action strings for review permissions notification
+ static final String REVIEW_NOTIF_ACTION_REMIND = "REVIEW_NOTIF_ACTION_REMIND";
+ static final String REVIEW_NOTIF_ACTION_DISMISS = "REVIEW_NOTIF_ACTION_DISMISS";
+ static final String REVIEW_NOTIF_ACTION_CANCELED = "REVIEW_NOTIF_ACTION_CANCELED";
+
/**
* Apps that post custom toasts in the background will have those blocked. Apps can
* still post toasts created with
@@ -652,6 +666,9 @@
private InstanceIdSequence mNotificationInstanceIdSequence;
private Set<String> mMsgPkgsAllowedAsConvos = new HashSet();
+ // Broadcast intent receiver for notification permissions review-related intents
+ private ReviewNotificationPermissionsReceiver mReviewNotificationPermissionsReceiver;
+
static class Archive {
final SparseArray<Boolean> mEnabled;
final int mBufferSize;
@@ -1413,8 +1430,7 @@
}
@Override
- public void onBubbleNotificationSuppressionChanged(String key, boolean isNotifSuppressed,
- boolean isBubbleSuppressed) {
+ public void onBubbleMetadataFlagChanged(String key, int flags) {
synchronized (mNotificationLock) {
NotificationRecord r = mNotificationsByKey.get(key);
if (r != null) {
@@ -1424,17 +1440,12 @@
return;
}
- boolean flagChanged = false;
- if (data.isNotificationSuppressed() != isNotifSuppressed) {
- flagChanged = true;
- data.setSuppressNotification(isNotifSuppressed);
- }
- if (data.isBubbleSuppressed() != isBubbleSuppressed) {
- flagChanged = true;
- data.setSuppressBubble(isBubbleSuppressed);
- }
- if (flagChanged) {
+ if (flags != data.getFlags()) {
+ data.setFlags(flags);
+ // Shouldn't alert again just because of a flag change.
r.getNotification().flags |= FLAG_ONLY_ALERT_ONCE;
+ // Force isAppForeground true here, because for sysui's purposes we
+ // want to be able to adjust the flag behaviour.
mHandler.post(
new EnqueueNotificationRunnable(r.getUser().getIdentifier(), r,
true /* isAppForeground */, SystemClock.elapsedRealtime()));
@@ -2416,6 +2427,11 @@
IntentFilter localeChangedFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
getContext().registerReceiver(mLocaleChangeReceiver, localeChangedFilter);
+
+ mReviewNotificationPermissionsReceiver = new ReviewNotificationPermissionsReceiver();
+ getContext().registerReceiver(mReviewNotificationPermissionsReceiver,
+ ReviewNotificationPermissionsReceiver.getFilter(),
+ Context.RECEIVER_NOT_EXPORTED);
}
/**
@@ -2709,6 +2725,7 @@
mHistoryManager.onBootPhaseAppsCanStart();
registerDeviceConfigChange();
migrateDefaultNAS();
+ maybeShowInitialReviewPermissionsNotification();
} else if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
mSnoozeHelper.scheduleRepostsForPersistedNotifications(System.currentTimeMillis());
}
@@ -6336,6 +6353,21 @@
public boolean areNotificationsEnabledForPackage(String pkg, int uid) {
return areNotificationsEnabledForPackageInt(pkg, uid);
}
+
+ @Override
+ public void sendReviewPermissionsNotification() {
+ // This method is meant to be called from the JobService upon running the job for this
+ // notification having been rescheduled; so without checking any other state, it will
+ // send the notification.
+ checkCallerIsSystem();
+ NotificationManager nm = getContext().getSystemService(NotificationManager.class);
+ nm.notify(TAG,
+ SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS,
+ createReviewPermissionsNotification());
+ Settings.Global.putInt(getContext().getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN);
+ }
};
int getNumNotificationChannelsForPackage(String pkg, int uid, boolean includeDeleted) {
@@ -7145,10 +7177,12 @@
&& r.getNotification().isBubbleNotification())
|| (mReason == REASON_CLICK && r.canBubble()
&& r.isFlagBubbleRemoved())) {
- boolean isBubbleSuppressed = r.getNotification().getBubbleMetadata() != null
- && r.getNotification().getBubbleMetadata().isBubbleSuppressed();
- mNotificationDelegate.onBubbleNotificationSuppressionChanged(
- r.getKey(), true /* notifSuppressed */, isBubbleSuppressed);
+ int flags = 0;
+ if (r.getNotification().getBubbleMetadata() != null) {
+ flags = r.getNotification().getBubbleMetadata().getFlags();
+ }
+ flags |= FLAG_SUPPRESS_NOTIFICATION;
+ mNotificationDelegate.onBubbleMetadataFlagChanged(r.getKey(), flags);
return;
}
if ((r.getNotification().flags & mMustHaveFlags) != mMustHaveFlags) {
@@ -11608,6 +11642,76 @@
out.endTag(null, LOCKSCREEN_ALLOW_SECURE_NOTIFICATIONS_TAG);
}
+ // Creates a notification that informs the user about changes due to the migration to
+ // use permissions for notifications.
+ protected Notification createReviewPermissionsNotification() {
+ int title = R.string.review_notification_settings_title;
+ int content = R.string.review_notification_settings_text;
+
+ // Tapping on the notification leads to the settings screen for managing app notifications,
+ // using the intent reserved for system services to indicate it comes from this notification
+ Intent tapIntent = new Intent(Settings.ACTION_ALL_APPS_NOTIFICATION_SETTINGS_FOR_REVIEW);
+ Intent remindIntent = new Intent(REVIEW_NOTIF_ACTION_REMIND);
+ Intent dismissIntent = new Intent(REVIEW_NOTIF_ACTION_DISMISS);
+ Intent swipeIntent = new Intent(REVIEW_NOTIF_ACTION_CANCELED);
+
+ // Both "remind me" and "dismiss" actions will be actions received by the BroadcastReceiver
+ final Notification.Action remindMe = new Notification.Action.Builder(null,
+ getContext().getResources().getString(
+ R.string.review_notification_settings_remind_me_action),
+ PendingIntent.getBroadcast(
+ getContext(), 0, remindIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
+ .build();
+ final Notification.Action dismiss = new Notification.Action.Builder(null,
+ getContext().getResources().getString(
+ R.string.review_notification_settings_dismiss),
+ PendingIntent.getBroadcast(
+ getContext(), 0, dismissIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
+ .build();
+
+ return new Notification.Builder(getContext(), SystemNotificationChannels.SYSTEM_CHANGES)
+ .setSmallIcon(R.drawable.stat_sys_adb)
+ .setContentTitle(getContext().getResources().getString(title))
+ .setContentText(getContext().getResources().getString(content))
+ .setContentIntent(PendingIntent.getActivity(getContext(), 0, tapIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
+ .setStyle(new Notification.BigTextStyle())
+ .setFlag(Notification.FLAG_NO_CLEAR, true)
+ .setAutoCancel(true)
+ .addAction(remindMe)
+ .addAction(dismiss)
+ .setDeleteIntent(PendingIntent.getBroadcast(getContext(), 0, swipeIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
+ .build();
+ }
+
+ protected void maybeShowInitialReviewPermissionsNotification() {
+ int currentState = Settings.Global.getInt(getContext().getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ REVIEW_NOTIF_STATE_UNKNOWN);
+
+ // now check the last known state of the notification -- this determination of whether the
+ // user is in the correct target audience occurs elsewhere, and will have written the
+ // REVIEW_NOTIF_STATE_SHOULD_SHOW to indicate it should be shown in the future.
+ //
+ // alternatively, if the user has rescheduled the notification (so it has been shown
+ // again) but not yet interacted with the new notification, then show it again on boot,
+ // as this state indicates that the user had the notification open before rebooting.
+ //
+ // sending the notification here does not record a new state for the notification;
+ // that will be written by parts of the system further down the line if at any point
+ // the user interacts with the notification.
+ if (currentState == REVIEW_NOTIF_STATE_SHOULD_SHOW
+ || currentState == REVIEW_NOTIF_STATE_RESHOWN) {
+ NotificationManager nm = getContext().getSystemService(NotificationManager.class);
+ nm.notify(TAG,
+ SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS,
+ createReviewPermissionsNotification());
+ }
+ }
+
/**
* Shows a warning on logcat. Shows the toast only once per package. This is to avoid being too
* aggressive and annoying the user.
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 0525b1e..ef3c770 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -96,6 +96,10 @@
private final int XML_VERSION;
/** What version to check to do the upgrade for bubbles. */
private static final int XML_VERSION_BUBBLES_UPGRADE = 1;
+ /** The first xml version with notification permissions enabled. */
+ private static final int XML_VERSION_NOTIF_PERMISSION = 3;
+ /** The first xml version that notifies users to review their notification permissions */
+ private static final int XML_VERSION_REVIEW_PERMISSIONS_NOTIFICATION = 4;
@VisibleForTesting
static final int UNKNOWN_UID = UserHandle.USER_NULL;
private static final String NON_BLOCKABLE_CHANNEL_DELIM = ":";
@@ -206,7 +210,7 @@
mStatsEventBuilderFactory = statsEventBuilderFactory;
if (mPermissionHelper.isMigrationEnabled()) {
- XML_VERSION = 3;
+ XML_VERSION = 4;
} else {
XML_VERSION = 2;
}
@@ -226,8 +230,16 @@
final int xmlVersion = parser.getAttributeInt(null, ATT_VERSION, -1);
boolean upgradeForBubbles = xmlVersion == XML_VERSION_BUBBLES_UPGRADE;
- boolean migrateToPermission =
- (xmlVersion < XML_VERSION) && mPermissionHelper.isMigrationEnabled();
+ boolean migrateToPermission = (xmlVersion < XML_VERSION_NOTIF_PERMISSION)
+ && mPermissionHelper.isMigrationEnabled();
+ if (xmlVersion < XML_VERSION_REVIEW_PERMISSIONS_NOTIFICATION) {
+ // make a note that we should show the notification at some point.
+ // it shouldn't be possible for the user to already have seen it, as the XML version
+ // would be newer then.
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW);
+ }
ArrayList<PermissionHelper.PackagePermission> pkgPerms = new ArrayList<>();
synchronized (mPackagePreferences) {
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
diff --git a/services/core/java/com/android/server/notification/ReviewNotificationPermissionsJobService.java b/services/core/java/com/android/server/notification/ReviewNotificationPermissionsJobService.java
new file mode 100644
index 0000000..fde45f71
--- /dev/null
+++ b/services/core/java/com/android/server/notification/ReviewNotificationPermissionsJobService.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2022 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.notification;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalServices;
+
+/**
+ * JobService implementation for scheduling the notification informing users about
+ * notification permissions updates and taking them to review their existing permissions.
+ * @hide
+ */
+public class ReviewNotificationPermissionsJobService extends JobService {
+ public static final String TAG = "ReviewNotificationPermissionsJobService";
+
+ @VisibleForTesting
+ protected static final int JOB_ID = 225373531;
+
+ /**
+ * Schedule a new job that will show a notification the specified amount of time in the future.
+ */
+ public static void scheduleJob(Context context, long rescheduleTimeMillis) {
+ JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+ // if the job already exists for some reason, cancel & reschedule
+ if (jobScheduler.getPendingJob(JOB_ID) != null) {
+ jobScheduler.cancel(JOB_ID);
+ }
+ ComponentName component = new ComponentName(
+ context, ReviewNotificationPermissionsJobService.class);
+ JobInfo newJob = new JobInfo.Builder(JOB_ID, component)
+ .setPersisted(true) // make sure it'll still get rescheduled after reboot
+ .setMinimumLatency(rescheduleTimeMillis) // run after specified amount of time
+ .build();
+ jobScheduler.schedule(newJob);
+ }
+
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ // While jobs typically should be run on different threads, this
+ // job only posts a notification, which is not a long-running operation
+ // as notification posting is asynchronous.
+ NotificationManagerInternal nmi =
+ LocalServices.getService(NotificationManagerInternal.class);
+ nmi.sendReviewPermissionsNotification();
+
+ // once the notification is posted, the job is done, so no need to
+ // keep it alive afterwards
+ return false;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ // If we're interrupted for some reason, try again (though this may not
+ // ever happen due to onStartJob not leaving a job running after being
+ // called)
+ return true;
+ }
+}
diff --git a/services/core/java/com/android/server/notification/ReviewNotificationPermissionsReceiver.java b/services/core/java/com/android/server/notification/ReviewNotificationPermissionsReceiver.java
new file mode 100644
index 0000000..b99aeac
--- /dev/null
+++ b/services/core/java/com/android/server/notification/ReviewNotificationPermissionsReceiver.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2022 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.notification;
+
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.provider.Settings;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.temporal.ChronoUnit;
+
+/**
+ * Broadcast receiver for intents that come from the "review notification permissions" notification,
+ * shown to users who upgrade to T from an earlier OS to inform them of notification setup changes
+ * and invite them to review their notification permissions.
+ */
+public class ReviewNotificationPermissionsReceiver extends BroadcastReceiver {
+ public static final String TAG = "ReviewNotifPermissions";
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ // 7 days in millis, as the amount of time to wait before re-sending the notification
+ private static final long JOB_RESCHEDULE_TIME = 1000 /* millis */ * 60 /* seconds */
+ * 60 /* minutes */ * 24 /* hours */ * 7 /* days */;
+
+ static IntentFilter getFilter() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND);
+ filter.addAction(NotificationManagerService.REVIEW_NOTIF_ACTION_DISMISS);
+ filter.addAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED);
+ return filter;
+ }
+
+ // Cancels the "review notification permissions" notification.
+ @VisibleForTesting
+ protected void cancelNotification(Context context) {
+ NotificationManager nm = context.getSystemService(NotificationManager.class);
+ if (nm != null) {
+ nm.cancel(NotificationManagerService.TAG,
+ SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS);
+ } else {
+ Slog.w(TAG, "could not cancel notification: NotificationManager not found");
+ }
+ }
+
+ @VisibleForTesting
+ protected void rescheduleNotification(Context context) {
+ ReviewNotificationPermissionsJobService.scheduleJob(context, JOB_RESCHEDULE_TIME);
+ // log if needed
+ if (DEBUG) {
+ Slog.d(TAG, "Scheduled review permissions notification for on or after: "
+ + LocalDateTime.now(ZoneId.systemDefault())
+ .plus(JOB_RESCHEDULE_TIME, ChronoUnit.MILLIS));
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND)) {
+ // Reschedule the notification for 7 days in the future
+ rescheduleNotification(context);
+
+ // note that the user has interacted; no longer needed to show the initial
+ // notification
+ Settings.Global.putInt(context.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED);
+ cancelNotification(context);
+ } else if (action.equals(NotificationManagerService.REVIEW_NOTIF_ACTION_DISMISS)) {
+ // user dismissed; write to settings so we don't show ever again
+ Settings.Global.putInt(context.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED);
+ cancelNotification(context);
+ } else if (action.equals(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED)) {
+ // we may get here from the user swiping away the notification,
+ // or from the notification being canceled in any other way.
+ // only in the case that the user hasn't interacted with it in
+ // any other way yet, reschedule
+ int notifState = Settings.Global.getInt(context.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ /* default */ NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN);
+ if (notifState == NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW) {
+ // user hasn't interacted in the past, so reschedule once and then note that the
+ // user *has* interacted now so we don't re-reschedule if they swipe again
+ rescheduleNotification(context);
+ Settings.Global.putInt(context.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED);
+ } else if (notifState == NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN) {
+ // swiping away on a rescheduled notification; mark as interacted and
+ // don't reschedule again.
+ Settings.Global.putInt(context.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED);
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java b/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java
index 89f8be2..daac7c0 100644
--- a/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java
+++ b/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java
@@ -236,13 +236,6 @@
verifyActivityCanHandleIntent(launchIntent, callingUid, userId);
- // Always show the cross profile animation
- if (options == null) {
- options = ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle();
- } else {
- options.putAll(ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle());
- }
-
mInjector.getActivityTaskManagerInternal()
.startActivityAsUser(
caller,
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index fef6ce1..4df54b7 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -349,7 +349,7 @@
private ShortcutInfo forceDeleteShortcutInner(@NonNull String id) {
final ShortcutInfo shortcut = mShortcuts.remove(id);
if (shortcut != null) {
- mShortcutUser.mService.removeIconLocked(shortcut);
+ removeIcon(shortcut);
shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED
| ShortcutInfo.FLAG_MANIFEST | ShortcutInfo.FLAG_CACHED_ALL);
}
@@ -366,7 +366,7 @@
forceDeleteShortcutInner(newShortcut.getId());
// Extract Icon and update the icon res ID and the bitmap path.
- s.saveIconAndFixUpShortcutLocked(newShortcut);
+ s.saveIconAndFixUpShortcutLocked(this, newShortcut);
s.fixUpShortcutResourceNamesAndValues(newShortcut);
saveShortcut(newShortcut);
}
@@ -972,7 +972,8 @@
/**
* Return the filenames (excluding path names) of icon bitmap files from this package.
*/
- public ArraySet<String> getUsedBitmapFiles() {
+ @GuardedBy("mLock")
+ private ArraySet<String> getUsedBitmapFilesLocked() {
final ArraySet<String> usedFiles = new ArraySet<>(1);
forEachShortcut(si -> {
if (si.getBitmapPath() != null) {
@@ -982,6 +983,26 @@
return usedFiles;
}
+ public void cleanupDanglingBitmapFiles(@NonNull File path) {
+ synchronized (mLock) {
+ mShortcutBitmapSaver.waitForAllSavesLocked();
+ final ArraySet<String> usedFiles = getUsedBitmapFilesLocked();
+
+ for (File child : path.listFiles()) {
+ if (!child.isFile()) {
+ continue;
+ }
+ final String name = child.getName();
+ if (!usedFiles.contains(name)) {
+ if (ShortcutService.DEBUG) {
+ Slog.d(TAG, "Removing dangling bitmap file: " + child.getAbsolutePath());
+ }
+ child.delete();
+ }
+ }
+ }
+ }
+
private static String getFileName(@NonNull String path) {
final int sep = path.lastIndexOf(File.separatorChar);
if (sep == -1) {
@@ -1608,6 +1629,11 @@
pw.print(" (");
pw.print(Formatter.formatFileSize(mShortcutUser.mService.mContext, totalBitmapSize[0]));
pw.println(")");
+
+ pw.println();
+ synchronized (mLock) {
+ mShortcutBitmapSaver.dumpLocked(pw, " ");
+ }
}
public void dumpShortcuts(@NonNull PrintWriter pw, int matchFlags) {
@@ -1729,7 +1755,7 @@
// Note: at this point no shortcuts should have bitmaps pending save, but if they do,
// just remove the bitmap.
if (si.isIconPendingSave()) {
- s.removeIconLocked(si);
+ removeIcon(si);
}
out.startTag(null, TAG_SHORTCUT);
ShortcutService.writeAttr(out, ATTR_ID, si.getId());
diff --git a/services/core/java/com/android/server/pm/ShortcutPackageItem.java b/services/core/java/com/android/server/pm/ShortcutPackageItem.java
index 6e0436f..7800183 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackageItem.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackageItem.java
@@ -16,8 +16,10 @@
package com.android.server.pm;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.pm.PackageInfo;
import android.content.pm.ShortcutInfo;
+import android.graphics.Bitmap;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.TypedXmlSerializer;
@@ -50,6 +52,9 @@
protected ShortcutUser mShortcutUser;
+ @GuardedBy("mLock")
+ protected ShortcutBitmapSaver mShortcutBitmapSaver;
+
protected final Object mLock = new Object();
protected ShortcutPackageItem(@NonNull ShortcutUser shortcutUser,
@@ -59,6 +64,7 @@
mPackageUserId = packageUserId;
mPackageName = Preconditions.checkStringNotEmpty(packageName);
mPackageInfo = Objects.requireNonNull(packageInfo);
+ mShortcutBitmapSaver = new ShortcutBitmapSaver(shortcutUser.mService);
}
/**
@@ -206,7 +212,7 @@
void saveShortcutPackageItem() {
// Wait for bitmap saves to conclude before proceeding to saving shortcuts.
- mShortcutUser.mService.waitForBitmapSaves();
+ waitForBitmapSaves();
// Save each ShortcutPackageItem in a separate Xml file.
final File path = getShortcutPackageItemFile();
if (ShortcutService.DEBUG || ShortcutService.DEBUG_REBOOT) {
@@ -221,6 +227,35 @@
}
}
+ public boolean waitForBitmapSaves() {
+ synchronized (mLock) {
+ return mShortcutBitmapSaver.waitForAllSavesLocked();
+ }
+ }
+
+ public void saveBitmap(ShortcutInfo shortcut,
+ int maxDimension, Bitmap.CompressFormat format, int quality) {
+ synchronized (mLock) {
+ mShortcutBitmapSaver.saveBitmapLocked(shortcut, maxDimension, format, quality);
+ }
+ }
+
+ /**
+ * Wait for all pending saves to finish, and then return the given shortcut's bitmap path.
+ */
+ @Nullable
+ public String getBitmapPathMayWait(ShortcutInfo shortcut) {
+ synchronized (mLock) {
+ return mShortcutBitmapSaver.getBitmapPathMayWaitLocked(shortcut);
+ }
+ }
+
+ public void removeIcon(ShortcutInfo shortcut) {
+ synchronized (mLock) {
+ mShortcutBitmapSaver.removeIcon(shortcut);
+ }
+ }
+
void removeShortcutPackageItem() {
synchronized (mLock) {
getShortcutPackageItemFile().delete();
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 780f976..f2bcf5e 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -363,7 +363,6 @@
private final RoleManager mRoleManager;
private final ShortcutRequestPinProcessor mShortcutRequestPinProcessor;
- private final ShortcutBitmapSaver mShortcutBitmapSaver;
private final ShortcutDumpFiles mShortcutDumpFiles;
@GuardedBy("mLock")
@@ -490,7 +489,6 @@
mRoleManager = Objects.requireNonNull(mContext.getSystemService(RoleManager.class));
mShortcutRequestPinProcessor = new ShortcutRequestPinProcessor(this, mLock);
- mShortcutBitmapSaver = new ShortcutBitmapSaver(this);
mShortcutDumpFiles = new ShortcutDumpFiles(this);
mIsAppSearchEnabled = DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags.SHORTCUT_APPSEARCH_INTEGRATION, true)
@@ -1063,8 +1061,6 @@
Slog.d(TAG, "Saving to " + path);
}
- mShortcutBitmapSaver.waitForAllSavesLocked();
-
path.getParentFile().mkdirs();
final AtomicFile file = new AtomicFile(path);
FileOutputStream os = null;
@@ -1388,15 +1384,12 @@
// === Caller validation ===
- void removeIconLocked(ShortcutInfo shortcut) {
- mShortcutBitmapSaver.removeIcon(shortcut);
- }
-
public void cleanupBitmapsForPackage(@UserIdInt int userId, String packageName) {
final File packagePath = new File(getUserBitmapFilePath(userId), packageName);
if (!packagePath.isDirectory()) {
return;
}
+ // ShortcutPackage is already removed at this point, we can safely remove the folder.
if (!(FileUtils.deleteContents(packagePath) && packagePath.delete())) {
Slog.w(TAG, "Unable to remove directory " + packagePath);
}
@@ -1437,38 +1430,12 @@
}
cleanupBitmapsForPackage(userId, packageName);
} else {
- cleanupDanglingBitmapFilesLocked(userId, user, packageName, child);
+ user.getPackageShortcuts(packageName).cleanupDanglingBitmapFiles(child);
}
}
logDurationStat(Stats.CLEANUP_DANGLING_BITMAPS, start);
}
- /**
- * Remove dangling bitmap files for a package.
- *
- * Note this method must be called with the lock held after calling
- * {@link ShortcutBitmapSaver#waitForAllSavesLocked()} to make sure there's no pending bitmap
- * saves are going on.
- */
- private void cleanupDanglingBitmapFilesLocked(@UserIdInt int userId, @NonNull ShortcutUser user,
- @NonNull String packageName, @NonNull File path) {
- final ArraySet<String> usedFiles =
- user.getPackageShortcuts(packageName).getUsedBitmapFiles();
-
- for (File child : path.listFiles()) {
- if (!child.isFile()) {
- continue;
- }
- final String name = child.getName();
- if (!usedFiles.contains(name)) {
- if (DEBUG) {
- Slog.d(TAG, "Removing dangling bitmap file: " + child.getAbsolutePath());
- }
- child.delete();
- }
- }
- }
-
@VisibleForTesting
static class FileOutputStreamWithPath extends FileOutputStream {
private final File mFile;
@@ -1513,7 +1480,7 @@
}
}
- void saveIconAndFixUpShortcutLocked(ShortcutInfo shortcut) {
+ void saveIconAndFixUpShortcutLocked(ShortcutPackage p, ShortcutInfo shortcut) {
if (shortcut.hasIconFile() || shortcut.hasIconResource() || shortcut.hasIconUri()) {
return;
}
@@ -1521,7 +1488,7 @@
final long token = injectClearCallingIdentity();
try {
// Clear icon info on the shortcut.
- removeIconLocked(shortcut);
+ p.removeIcon(shortcut);
final Icon icon = shortcut.getIcon();
if (icon == null) {
@@ -1560,8 +1527,7 @@
// just in case.
throw ShortcutInfo.getInvalidIconException();
}
- mShortcutBitmapSaver.saveBitmapLocked(shortcut,
- maxIconDimension, mIconPersistFormat, mIconPersistQuality);
+ p.saveBitmap(shortcut, maxIconDimension, mIconPersistFormat, mIconPersistQuality);
} finally {
// Once saved, we won't use the original icon information, so null it out.
shortcut.clearIcon();
@@ -2110,7 +2076,7 @@
final boolean replacingIcon = (source.getIcon() != null);
if (replacingIcon) {
- removeIconLocked(target);
+ ps.removeIcon(target);
}
// Note copyNonNullFieldsFrom() does the "updatable with?" check too.
@@ -2118,7 +2084,7 @@
target.setTimestamp(injectCurrentTimeMillis());
if (replacingIcon) {
- saveIconAndFixUpShortcutLocked(target);
+ saveIconAndFixUpShortcutLocked(ps, target);
}
// When we're updating any resource related fields, re-extract the res
@@ -3463,7 +3429,7 @@
if (shortcutInfo == null) {
return null;
}
- return getShortcutIconParcelFileDescriptor(shortcutInfo);
+ return getShortcutIconParcelFileDescriptor(p, shortcutInfo);
}
}
@@ -3476,6 +3442,7 @@
Objects.requireNonNull(shortcutId, "shortcutId");
// Checks shortcuts in memory first
+ final ShortcutPackage p;
synchronized (mLock) {
throwIfUserLockedL(userId);
throwIfUserLockedL(launcherUserId);
@@ -3483,8 +3450,7 @@
getLauncherShortcutsLocked(callingPackage, userId, launcherUserId)
.attemptToRestoreIfNeededAndSave();
- final ShortcutPackage p = getUserShortcutsLocked(userId)
- .getPackageShortcutsIfExists(packageName);
+ p = getUserShortcutsLocked(userId).getPackageShortcutsIfExists(packageName);
if (p == null) {
cb.complete(null);
return;
@@ -3492,24 +3458,23 @@
final ShortcutInfo shortcutInfo = p.findShortcutById(shortcutId);
if (shortcutInfo != null) {
- cb.complete(getShortcutIconParcelFileDescriptor(shortcutInfo));
+ cb.complete(getShortcutIconParcelFileDescriptor(p, shortcutInfo));
return;
}
}
// Otherwise check persisted shortcuts
- getShortcutInfoAsync(launcherUserId, packageName, shortcutId, userId, si -> {
- cb.complete(getShortcutIconParcelFileDescriptor(si));
- });
+ getShortcutInfoAsync(launcherUserId, packageName, shortcutId, userId, si ->
+ cb.complete(getShortcutIconParcelFileDescriptor(p, si)));
}
@Nullable
private ParcelFileDescriptor getShortcutIconParcelFileDescriptor(
- @Nullable final ShortcutInfo shortcutInfo) {
- if (shortcutInfo == null || !shortcutInfo.hasIconFile()) {
+ @Nullable final ShortcutPackage p, @Nullable final ShortcutInfo shortcutInfo) {
+ if (p == null || shortcutInfo == null || !shortcutInfo.hasIconFile()) {
return null;
}
- final String path = mShortcutBitmapSaver.getBitmapPathMayWaitLocked(shortcutInfo);
+ final String path = p.getBitmapPathMayWait(shortcutInfo);
if (path == null) {
Slog.w(TAG, "null bitmap detected in getShortcutIconFd()");
return null;
@@ -4772,9 +4737,6 @@
}
pw.println();
- mShortcutBitmapSaver.dumpLocked(pw, " ");
-
- pw.println();
}
for (int i = 0; i < mUsers.size(); i++) {
@@ -5347,9 +5309,11 @@
}
}
- void waitForBitmapSaves() {
+ @VisibleForTesting
+ void waitForBitmapSavesForTest() {
synchronized (mLock) {
- mShortcutBitmapSaver.waitForAllSavesLocked();
+ forEachLoadedUserLocked(u ->
+ u.forAllPackageItems(ShortcutPackageItem::waitForBitmapSaves));
}
}
diff --git a/services/core/java/com/android/server/pm/ShortcutUser.java b/services/core/java/com/android/server/pm/ShortcutUser.java
index 75e18b5..b9fd2fd 100644
--- a/services/core/java/com/android/server/pm/ShortcutUser.java
+++ b/services/core/java/com/android/server/pm/ShortcutUser.java
@@ -401,6 +401,7 @@
private void saveShortcutPackageItem(TypedXmlSerializer out, ShortcutPackageItem spi,
boolean forBackup) throws IOException, XmlPullParserException {
+ spi.waitForBitmapSaves();
if (forBackup) {
if (spi.getPackageUserId() != spi.getOwnerUserId()) {
return; // Don't save cross-user information.
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 0dabff8..bcdf429 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1430,6 +1430,8 @@
/**
* Returns a UserInfo object with the name filled in, for Owner and Guest, or the original
* if the name is already set.
+ *
+ * Note: Currently, the resulting name can be null if a user was truly created with a null name.
*/
private UserInfo userWithName(UserInfo orig) {
if (orig != null && orig.name == null) {
@@ -1638,7 +1640,7 @@
}
@Override
- public String getUserName() {
+ public @NonNull String getUserName() {
final int callingUid = Binder.getCallingUid();
if (!hasQueryOrCreateUsersPermission()
&& !hasPermissionGranted(
@@ -1649,7 +1651,10 @@
final int userId = UserHandle.getUserId(callingUid);
synchronized (mUsersLock) {
UserInfo userInfo = userWithName(getUserInfoLU(userId));
- return userInfo == null ? "" : userInfo.name;
+ if (userInfo != null && userInfo.name != null) {
+ return userInfo.name;
+ }
+ return "";
}
}
@@ -4165,7 +4170,7 @@
* @return the converted user, or {@code null} if no pre-created user could be converted.
*/
private @Nullable UserInfo convertPreCreatedUserIfPossible(String userType,
- @UserInfoFlag int flags, String name, @Nullable Object token) {
+ @UserInfoFlag int flags, @Nullable String name, @Nullable Object token) {
final UserData preCreatedUserData;
synchronized (mUsersLock) {
preCreatedUserData = getPreCreatedUserLU(userType);
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index b685572..d48f263 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -1650,13 +1650,11 @@
}
@Override
- public void onBubbleNotificationSuppressionChanged(String key, boolean isNotifSuppressed,
- boolean isBubbleSuppressed) {
+ public void onBubbleMetadataFlagChanged(String key, int flags) {
enforceStatusBarService();
final long identity = Binder.clearCallingIdentity();
try {
- mNotificationDelegate.onBubbleNotificationSuppressionChanged(key, isNotifSuppressed,
- isBubbleSuppressed);
+ mNotificationDelegate.onBubbleMetadataFlagChanged(key, flags);
} finally {
Binder.restoreCallingIdentity(identity);
}
diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
index 4094377..fdf9354 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -1975,7 +1975,7 @@
if (si == null) {
return null;
}
- mService.waitForBitmapSaves();
+ mService.waitForBitmapSavesForTest();
return new File(si.getBitmapPath()).getName();
}
@@ -1984,7 +1984,7 @@
if (si == null) {
return null;
}
- mService.waitForBitmapSaves();
+ mService.waitForBitmapSavesForTest();
return new File(si.getBitmapPath()).getAbsolutePath();
}
@@ -2139,7 +2139,7 @@
}
protected boolean bitmapDirectoryExists(String packageName, int userId) {
- mService.waitForBitmapSaves();
+ mService.waitForBitmapSavesForTest();
final File path = new File(mService.getUserBitmapFilePath(userId), packageName);
return path.isDirectory();
}
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
index 411b521..867890f 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
@@ -1040,7 +1040,7 @@
dumpsysOnLogcat();
- mService.waitForBitmapSaves();
+ mService.waitForBitmapSavesForTest();
// Check files and directories.
// Package 3 has no bitmaps, so we don't create a directory.
assertBitmapDirectories(USER_0, CALLING_PACKAGE_1, CALLING_PACKAGE_2);
@@ -1096,7 +1096,7 @@
makeFile(mService.getUserBitmapFilePath(USER_10), CALLING_PACKAGE_2, "3").createNewFile();
makeFile(mService.getUserBitmapFilePath(USER_10), CALLING_PACKAGE_2, "4").createNewFile();
- mService.waitForBitmapSaves();
+ mService.waitForBitmapSavesForTest();
assertBitmapDirectories(USER_0, CALLING_PACKAGE_1, CALLING_PACKAGE_2, CALLING_PACKAGE_3,
"a.b.c", "d.e.f");
@@ -1111,7 +1111,7 @@
// The below check is the same as above, except this time USER_0 use the CALLING_PACKAGE_3
// directory.
- mService.waitForBitmapSaves();
+ mService.waitForBitmapSavesForTest();
assertBitmapDirectories(USER_0, CALLING_PACKAGE_1, CALLING_PACKAGE_2, CALLING_PACKAGE_3);
assertBitmapDirectories(USER_10, CALLING_PACKAGE_1, CALLING_PACKAGE_2);
@@ -1390,7 +1390,7 @@
.setIcon(Icon.createWithContentUri("test_uri"))
.build()
)));
- mService.waitForBitmapSaves();
+ mService.waitForBitmapSavesForTest();
assertWith(getCallerShortcuts())
.forShortcutWithId("s1", si -> {
assertTrue(si.hasIconUri());
@@ -1402,13 +1402,13 @@
.setIcon(Icon.createWithResource(getTestContext(), R.drawable.black_32x32))
.build()
)));
- mService.waitForBitmapSaves();
+ mService.waitForBitmapSavesForTest();
assertWith(getCallerShortcuts())
.forShortcutWithId("s1", si -> {
assertTrue(si.hasIconResource());
assertEquals(R.drawable.black_32x32, si.getIconResourceId());
});
- mService.waitForBitmapSaves();
+ mService.waitForBitmapSavesForTest();
mInjectedCurrentTimeMillis += INTERVAL; // reset throttling
@@ -1419,7 +1419,7 @@
getTestContext().getResources(), R.drawable.black_64x64)))
.build()
)));
- mService.waitForBitmapSaves();
+ mService.waitForBitmapSavesForTest();
assertWith(getCallerShortcuts())
.forShortcutWithId("s1", si -> {
assertTrue(si.hasIconFile());
@@ -1437,7 +1437,7 @@
getTestContext().getResources(), R.drawable.black_64x64)))
.build()
)));
- mService.waitForBitmapSaves();
+ mService.waitForBitmapSavesForTest();
assertWith(getCallerShortcuts())
.forShortcutWithId("s1", si -> {
assertTrue(si.hasIconFile());
@@ -1451,7 +1451,7 @@
.setIcon(Icon.createWithResource(getTestContext(), R.drawable.black_32x32))
.build()
)));
- mService.waitForBitmapSaves();
+ mService.waitForBitmapSavesForTest();
assertWith(getCallerShortcuts())
.forShortcutWithId("s1", si -> {
assertTrue(si.hasIconResource());
@@ -1463,7 +1463,7 @@
.setIcon(Icon.createWithContentUri("test_uri"))
.build()
)));
- mService.waitForBitmapSaves();
+ mService.waitForBitmapSavesForTest();
assertWith(getCallerShortcuts())
.forShortcutWithId("s1", si -> {
assertTrue(si.hasIconUri());
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 348e015..c0cd7a7 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -198,6 +198,7 @@
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.logging.InstanceIdSequence;
import com.android.internal.logging.InstanceIdSequenceFake;
+import com.android.internal.messages.nano.SystemMessageProto;
import com.android.internal.statusbar.NotificationVisibility;
import com.android.server.DeviceIdleInternal;
import com.android.server.LocalServices;
@@ -303,6 +304,8 @@
ActivityManagerInternal mAmi;
@Mock
private Looper mMainLooper;
+ @Mock
+ private NotificationManager mMockNm;
@Mock
IIntentSender pi1;
@@ -405,6 +408,7 @@
LocalServices.removeServiceForTest(PermissionPolicyInternal.class);
LocalServices.addService(PermissionPolicyInternal.class, mPermissionPolicyInternal);
mContext.addMockSystemService(Context.ALARM_SERVICE, mAlarmManager);
+ mContext.addMockSystemService(NotificationManager.class, mMockNm);
doNothing().when(mContext).sendBroadcastAsUser(any(), any(), any());
@@ -7516,46 +7520,53 @@
}
@Test
- public void testOnBubbleNotificationSuppressionChanged() throws Exception {
+ public void testOnBubbleMetadataFlagChanged() throws Exception {
setUpPrefsForBubbles(PKG, mUid,
true /* global */,
BUBBLE_PREFERENCE_ALL /* app */,
true /* channel */);
- // Bubble notification
+ // Post a bubble notification
NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel, "tag");
-
+ // Set this so that the bubble can be suppressed
+ nr.getNotification().getBubbleMetadata().setFlags(
+ Notification.BubbleMetadata.FLAG_SUPPRESSABLE_BUBBLE);
mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(),
nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
waitForIdle();
- // NOT suppressed
+ // Check the flags
Notification n = mBinderService.getActiveNotifications(PKG)[0].getNotification();
assertFalse(n.getBubbleMetadata().isNotificationSuppressed());
+ assertFalse(n.getBubbleMetadata().getAutoExpandBubble());
+ assertFalse(n.getBubbleMetadata().isBubbleSuppressed());
+ assertTrue(n.getBubbleMetadata().isBubbleSuppressable());
// Reset as this is called when the notif is first sent
reset(mListeners);
- // Test: update suppression to true
- mService.mNotificationDelegate.onBubbleNotificationSuppressionChanged(nr.getKey(), true,
- false);
+ // Test: change the flags
+ int flags = Notification.BubbleMetadata.FLAG_SUPPRESSABLE_BUBBLE;
+ flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE;
+ flags |= Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
+ flags |= Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE;
+ mService.mNotificationDelegate.onBubbleMetadataFlagChanged(nr.getKey(), flags);
waitForIdle();
// Check
n = mBinderService.getActiveNotifications(PKG)[0].getNotification();
- assertTrue(n.getBubbleMetadata().isNotificationSuppressed());
+ assertEquals(flags, n.getBubbleMetadata().getFlags());
// Reset to check again
reset(mListeners);
- // Test: update suppression to false
- mService.mNotificationDelegate.onBubbleNotificationSuppressionChanged(nr.getKey(), false,
- false);
+ // Test: clear flags
+ mService.mNotificationDelegate.onBubbleMetadataFlagChanged(nr.getKey(), 0);
waitForIdle();
// Check
n = mBinderService.getActiveNotifications(PKG)[0].getNotification();
- assertFalse(n.getBubbleMetadata().isNotificationSuppressed());
+ assertEquals(0, n.getBubbleMetadata().getFlags());
}
@Test
@@ -9294,4 +9305,77 @@
// the notifyPostedLocked function is called twice.
verify(mListeners, times(2)).notifyPostedLocked(any(), any());
}
+
+ @Test
+ public void testMaybeShowReviewPermissionsNotification_unknown() {
+ // Set up various possible states of the settings int and confirm whether or not the
+ // notification is shown as expected
+
+ // Initial state: default/unknown setting, make sure nothing happens
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN);
+ mService.maybeShowInitialReviewPermissionsNotification();
+ verify(mMockNm, never()).notify(anyString(), anyInt(), any(Notification.class));
+ }
+
+ @Test
+ public void testMaybeShowReviewPermissionsNotification_shouldShow() {
+ // If state is SHOULD_SHOW, it ... should show
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW);
+ mService.maybeShowInitialReviewPermissionsNotification();
+ verify(mMockNm, times(1)).notify(eq(NotificationManagerService.TAG),
+ eq(SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS),
+ any(Notification.class));
+ }
+
+ @Test
+ public void testMaybeShowReviewPermissionsNotification_alreadyShown() {
+ // If state is either USER_INTERACTED or DISMISSED, we should not show this on boot
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED);
+ mService.maybeShowInitialReviewPermissionsNotification();
+
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED);
+ mService.maybeShowInitialReviewPermissionsNotification();
+
+ verify(mMockNm, never()).notify(anyString(), anyInt(), any(Notification.class));
+ }
+
+ @Test
+ public void testMaybeShowReviewPermissionsNotification_reshown() {
+ // If we have re-shown the notification and the user did not subsequently interacted with
+ // it, then make sure we show when trying on boot
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN);
+ mService.maybeShowInitialReviewPermissionsNotification();
+ verify(mMockNm, times(1)).notify(eq(NotificationManagerService.TAG),
+ eq(SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS),
+ any(Notification.class));
+ }
+
+ @Test
+ public void testRescheduledReviewPermissionsNotification() {
+ // when rescheduled, the notification goes through the NotificationManagerInternal service
+ // this call doesn't need to know anything about previously scheduled state -- if called,
+ // it should send the notification & write the appropriate int to Settings
+ mInternalService.sendReviewPermissionsNotification();
+
+ // Notification should be sent
+ verify(mMockNm, times(1)).notify(eq(NotificationManagerService.TAG),
+ eq(SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS),
+ any(Notification.class));
+
+ // write STATE_RESHOWN to settings
+ assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN,
+ Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
+ }
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 63d7453..6d08959 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -289,6 +289,11 @@
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
.build();
+
+ // make sure that the settings for review notification permissions are unset to begin with
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN);
}
private ByteArrayOutputStream writeXmlAndPurge(
@@ -656,6 +661,13 @@
verify(mPermissionHelper).setNotificationPermission(nMr1Expected);
verify(mPermissionHelper).setNotificationPermission(oExpected);
verify(mPermissionHelper).setNotificationPermission(pExpected);
+
+ // verify that we also write a state for review_permissions_notification to eventually
+ // show a notification
+ assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW,
+ Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
}
@Test
@@ -738,7 +750,7 @@
}
@Test
- public void testReadXml_newXml_noMigration() throws Exception {
+ public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception {
when(mPermissionHelper.isMigrationEnabled()).thenReturn(true);
mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory);
@@ -786,6 +798,70 @@
compareChannels(idp, mHelper.getNotificationChannel(PKG_P, UID_P, idp.getId(), false));
verify(mPermissionHelper, never()).setNotificationPermission(any());
+
+ // verify that we do, however, write a state for review_permissions_notification to
+ // eventually show a notification, since this XML version is older than the notification
+ assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW,
+ Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
+ }
+
+ @Test
+ public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception {
+ when(mPermissionHelper.isMigrationEnabled()).thenReturn(true);
+ mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+ mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory);
+
+ String xml = "<ranking version=\"4\">\n"
+ + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n"
+ + "<channel id=\"idn\" name=\"name\" importance=\"2\"/>\n"
+ + "<channel id=\"miscellaneous\" name=\"Uncategorized\" />\n"
+ + "</package>\n"
+ + "<package name=\"" + PKG_O + "\" >\n"
+ + "<channel id=\"ido\" name=\"name2\" importance=\"2\" show_badge=\"true\"/>\n"
+ + "</package>\n"
+ + "<package name=\"" + PKG_P + "\" >\n"
+ + "<channel id=\"idp\" name=\"name3\" importance=\"4\" locked=\"2\" />\n"
+ + "</package>\n"
+ + "</ranking>\n";
+ NotificationChannel idn = new NotificationChannel("idn", "name", IMPORTANCE_LOW);
+ idn.setSound(null, new AudioAttributes.Builder()
+ .setUsage(USAGE_NOTIFICATION)
+ .setContentType(CONTENT_TYPE_SONIFICATION)
+ .setFlags(0)
+ .build());
+ idn.setShowBadge(false);
+ NotificationChannel ido = new NotificationChannel("ido", "name2", IMPORTANCE_LOW);
+ ido.setShowBadge(true);
+ ido.setSound(null, new AudioAttributes.Builder()
+ .setUsage(USAGE_NOTIFICATION)
+ .setContentType(CONTENT_TYPE_SONIFICATION)
+ .setFlags(0)
+ .build());
+ NotificationChannel idp = new NotificationChannel("idp", "name3", IMPORTANCE_HIGH);
+ idp.lockFields(2);
+ idp.setSound(null, new AudioAttributes.Builder()
+ .setUsage(USAGE_NOTIFICATION)
+ .setContentType(CONTENT_TYPE_SONIFICATION)
+ .setFlags(0)
+ .build());
+
+ loadByteArrayXml(xml.getBytes(), true, USER_SYSTEM);
+
+ assertTrue(mHelper.canShowBadge(PKG_N_MR1, UID_N_MR1));
+
+ assertEquals(idn, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, idn.getId(), false));
+ compareChannels(ido, mHelper.getNotificationChannel(PKG_O, UID_O, ido.getId(), false));
+ compareChannels(idp, mHelper.getNotificationChannel(PKG_P, UID_P, idp.getId(), false));
+
+ verify(mPermissionHelper, never()).setNotificationPermission(any());
+
+ // this XML is new enough, we should not be attempting to show a notification or anything
+ assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN,
+ Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
}
@Test
@@ -903,7 +979,7 @@
ByteArrayOutputStream baos = writeXmlAndPurge(
PKG_N_MR1, UID_N_MR1, false, USER_SYSTEM);
- String expected = "<ranking version=\"3\">\n"
+ String expected = "<ranking version=\"4\">\n"
+ "<package name=\"com.example.o\" show_badge=\"true\" "
+ "app_user_locked_fields=\"0\" sent_invalid_msg=\"false\" "
+ "sent_valid_msg=\"false\" user_demote_msg_app=\"false\" uid=\"1111\">\n"
@@ -984,7 +1060,7 @@
ByteArrayOutputStream baos = writeXmlAndPurge(
PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM);
- String expected = "<ranking version=\"3\">\n"
+ String expected = "<ranking version=\"4\">\n"
// Importance 0 because off in permissionhelper
+ "<package name=\"com.example.o\" importance=\"0\" show_badge=\"true\" "
+ "app_user_locked_fields=\"0\" sent_invalid_msg=\"false\" "
@@ -1067,7 +1143,7 @@
ByteArrayOutputStream baos = writeXmlAndPurge(
PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM);
- String expected = "<ranking version=\"3\">\n"
+ String expected = "<ranking version=\"4\">\n"
// Importance 0 because off in permissionhelper
+ "<package name=\"com.example.o\" importance=\"0\" show_badge=\"true\" "
+ "app_user_locked_fields=\"0\" sent_invalid_msg=\"false\" "
@@ -1121,7 +1197,7 @@
ByteArrayOutputStream baos = writeXmlAndPurge(
PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM);
- String expected = "<ranking version=\"3\">\n"
+ String expected = "<ranking version=\"4\">\n"
// Packages that exist solely in permissionhelper
+ "<package name=\"" + PKG_P + "\" importance=\"3\" />\n"
+ "<package name=\"" + PKG_O + "\" importance=\"0\" />\n"
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsJobServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsJobServiceTest.java
new file mode 100644
index 0000000..5a4ce5da
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsJobServiceTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2022 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.notification;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.rule.ServiceTestRule;
+
+import com.android.server.LocalServices;
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+@RunWith(AndroidTestingRunner.class)
+public class ReviewNotificationPermissionsJobServiceTest extends UiServiceTestCase {
+ private ReviewNotificationPermissionsJobService mJobService;
+ private JobParameters mJobParams = new JobParameters(null,
+ ReviewNotificationPermissionsJobService.JOB_ID, null, null, null,
+ 0, false, false, null, null, null);
+
+ @Captor
+ ArgumentCaptor<JobInfo> mJobInfoCaptor;
+
+ @Mock
+ private JobScheduler mMockJobScheduler;
+
+ @Mock
+ private NotificationManagerInternal mMockNotificationManagerInternal;
+
+ @Rule
+ public final ServiceTestRule mServiceRule = new ServiceTestRule();
+
+ @Before
+ public void setUp() throws Exception {
+ mJobService = new ReviewNotificationPermissionsJobService();
+ mContext.addMockSystemService(JobScheduler.class, mMockJobScheduler);
+
+ // add NotificationManagerInternal to LocalServices
+ LocalServices.removeServiceForTest(NotificationManagerInternal.class);
+ LocalServices.addService(NotificationManagerInternal.class,
+ mMockNotificationManagerInternal);
+ }
+
+ @Test
+ public void testScheduleJob() {
+ // if asked, the job doesn't currently exist yet
+ when(mMockJobScheduler.getPendingJob(anyInt())).thenReturn(null);
+
+ final int rescheduleTimeMillis = 350; // arbitrary number
+
+ // attempt to schedule the job
+ ReviewNotificationPermissionsJobService.scheduleJob(mContext, rescheduleTimeMillis);
+ verify(mMockJobScheduler, times(1)).schedule(mJobInfoCaptor.capture());
+
+ // verify various properties of the job that is passed in to the job scheduler
+ JobInfo jobInfo = mJobInfoCaptor.getValue();
+ assertEquals(ReviewNotificationPermissionsJobService.JOB_ID, jobInfo.getId());
+ assertEquals(rescheduleTimeMillis, jobInfo.getMinLatencyMillis());
+ assertTrue(jobInfo.isPersisted()); // should continue after reboot
+ assertFalse(jobInfo.isPeriodic()); // one time
+ }
+
+ @Test
+ public void testOnStartJob() {
+ // the job need not be persisted after it does its work, so it'll return
+ // false
+ assertFalse(mJobService.onStartJob(mJobParams));
+
+ // verify that starting the job causes the notification to be sent
+ verify(mMockNotificationManagerInternal).sendReviewPermissionsNotification();
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsReceiverTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsReceiverTest.java
new file mode 100644
index 0000000..12281a7
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsReceiverTest.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2022 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.notification;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Settings;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class ReviewNotificationPermissionsReceiverTest extends UiServiceTestCase {
+
+ // Simple mock class that just overrides the reschedule and cancel behavior so that it's easy
+ // to tell whether the receiver has sent requests to either reschedule or cancel the
+ // notification (or both).
+ private class MockReviewNotificationPermissionsReceiver
+ extends ReviewNotificationPermissionsReceiver {
+ boolean mCanceled = false;
+ boolean mRescheduled = false;
+
+ @Override
+ protected void cancelNotification(Context context) {
+ mCanceled = true;
+ }
+
+ @Override
+ protected void rescheduleNotification(Context context) {
+ mRescheduled = true;
+ }
+ }
+
+ private MockReviewNotificationPermissionsReceiver mReceiver;
+ private Intent mIntent;
+
+ @Before
+ public void setUp() {
+ mReceiver = new MockReviewNotificationPermissionsReceiver();
+ mIntent = new Intent(); // actions will be set in test cases
+ }
+
+ @Test
+ public void testReceive_remindMeLater_firstTime() {
+ // Test what happens when we receive a "remind me later" intent coming from
+ // a previously-not-interacted notification
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW);
+
+ // set up Intent action
+ mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND);
+
+ // Upon receipt of the intent, the following things should happen:
+ // - notification rescheduled
+ // - notification explicitly canceled
+ // - settings state updated to indicate user has interacted
+ mReceiver.onReceive(mContext, mIntent);
+ assertTrue(mReceiver.mRescheduled);
+ assertTrue(mReceiver.mCanceled);
+ assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED,
+ Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
+ }
+
+ @Test
+ public void testReceive_remindMeLater_laterTimes() {
+ // Test what happens when we receive a "remind me later" intent coming from
+ // a previously-interacted notification that has been rescheduled
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN);
+
+ // set up Intent action
+ mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND);
+
+ // Upon receipt of the intent, the following things should still happen
+ // regardless of the fact that the user has interacted before:
+ // - notification rescheduled
+ // - notification explicitly canceled
+ // - settings state still indicate user has interacted
+ mReceiver.onReceive(mContext, mIntent);
+ assertTrue(mReceiver.mRescheduled);
+ assertTrue(mReceiver.mCanceled);
+ assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED,
+ Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
+ }
+
+ @Test
+ public void testReceive_dismiss() {
+ // Test that dismissing the notification does *not* reschedule the notification,
+ // does cancel it, and writes that it has been dismissed to settings
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW);
+
+ // set up Intent action
+ mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_DISMISS);
+
+ // send intent, watch what happens
+ mReceiver.onReceive(mContext, mIntent);
+ assertFalse(mReceiver.mRescheduled);
+ assertTrue(mReceiver.mCanceled);
+ assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED,
+ Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
+ }
+
+ @Test
+ public void testReceive_notificationCanceled_firstSwipe() {
+ // Test the basic swipe away case: the first time the user swipes the notification
+ // away, it will not have been interacted with yet, so make sure it's rescheduled
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW);
+
+ // set up Intent action, would be called from notification's delete intent
+ mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED);
+
+ // send intent, make sure it gets:
+ // - rescheduled
+ // - not explicitly canceled, the notification was already canceled
+ // - noted that it's been interacted with
+ mReceiver.onReceive(mContext, mIntent);
+ assertTrue(mReceiver.mRescheduled);
+ assertFalse(mReceiver.mCanceled);
+ assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED,
+ Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
+ }
+
+ @Test
+ public void testReceive_notificationCanceled_secondSwipe() {
+ // Test the swipe away case for a rescheduled notification: in this case
+ // it should not be rescheduled anymore
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN);
+
+ // set up Intent action, would be called from notification's delete intent
+ mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED);
+
+ // send intent, make sure it gets:
+ // - not rescheduled on the second+ swipe
+ // - not explicitly canceled, the notification was already canceled
+ // - mark as user interacted
+ mReceiver.onReceive(mContext, mIntent);
+ assertFalse(mReceiver.mRescheduled);
+ assertFalse(mReceiver.mCanceled);
+ assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED,
+ Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
+ }
+
+ @Test
+ public void testReceive_notificationCanceled_fromDismiss() {
+ // Test that if the notification delete intent is called due to us canceling
+ // the notification from the receiver, we don't do anything extra
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED);
+
+ // set up Intent action, would be called from notification's delete intent
+ mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED);
+
+ // nothing should happen, nothing at all
+ mReceiver.onReceive(mContext, mIntent);
+ assertFalse(mReceiver.mRescheduled);
+ assertFalse(mReceiver.mCanceled);
+ assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED,
+ Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+ NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN));
+ }
+}