Merge "Fix the issue where the first brightness for a device was set to 0"
diff --git a/ApiDocs.bp b/ApiDocs.bp
index a46ecce..90b6603 100644
--- a/ApiDocs.bp
+++ b/ApiDocs.bp
@@ -422,26 +422,27 @@
"$(location merge_zips) $(out) $(location :ds-docs-java{.docs.zip}) $(genDir)/ds-docs-kt-moved.zip",
}
-java_genrule {
- name: "ds-docs-switched",
- tools: [
- "switcher4",
- "soong_zip",
- ],
- srcs: [
- ":ds-docs-java{.docs.zip}",
- ":ds-docs-kt{.docs.zip}",
- ],
- out: ["ds-docs-switched.zip"],
- dist: {
- targets: ["docs"],
- },
- cmd: "unzip -q $(location :ds-docs-java{.docs.zip}) -d $(genDir) && " +
- "unzip -q $(location :ds-docs-kt{.docs.zip}) -d $(genDir)/en/reference/kotlin && " +
- "SWITCHER=$$(cd $$(dirname $(location switcher4)) && pwd)/$$(basename $(location switcher4)) && " +
- "(cd $(genDir)/en/reference && $$SWITCHER --work platform) > /dev/null && " +
- "$(location soong_zip) -o $(out) -C $(genDir) -D $(genDir)",
-}
+// Disable doc generation until Doclava is migrated to JDK 17 (b/240421555).
+// java_genrule {
+// name: "ds-docs-switched",
+// tools: [
+// "switcher4",
+// "soong_zip",
+// ],
+// srcs: [
+// ":ds-docs-java{.docs.zip}",
+// ":ds-docs-kt{.docs.zip}",
+// ],
+// out: ["ds-docs-switched.zip"],
+// dist: {
+// targets: ["docs"],
+// },
+// cmd: "unzip -q $(location :ds-docs-java{.docs.zip}) -d $(genDir) && " +
+// "unzip -q $(location :ds-docs-kt{.docs.zip}) -d $(genDir)/en/reference/kotlin && " +
+// "SWITCHER=$$(cd $$(dirname $(location switcher4)) && pwd)/$$(basename $(location switcher4)) && " +
+// "(cd $(genDir)/en/reference && $$SWITCHER --work platform) > /dev/null && " +
+// "$(location soong_zip) -o $(out) -C $(genDir) -D $(genDir)",
+// }
droiddoc {
name: "ds-static-docs",
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
index b71d596..051dde0 100644
--- a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
+++ b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
@@ -271,34 +271,110 @@
/**
* Tests starting an uninitialized user, with wait times in between iterations.
- * Measures the time until the ProgressListener callback.
+ *
+ * The first iteration will take longer due to the process of setting policy permissions for
+ * a new user.
*/
@Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
- public void startUser_realistic() throws RemoteException {
+ public void startUser_uninitializedUser() throws RemoteException {
+ startUser_measuresAfterFirstIterations(/* numberOfIterationsToSkip */0);
+ }
+
+ /**
+ * Tests the second iteration of start user that has a problem that it takes too long to run, a
+ * bug has been created (b/266574680) and after investigating or fix this problem,
+ * this test can be removed.
+ */
+ @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
+ public void startUser_startedOnceBefore() throws RemoteException {
+ startUser_measuresAfterFirstIterations(/* numberOfIterationsToSkip */1);
+ }
+
+ /**
+ * Tests a specific iteration of the start user process.
+ * Measures the time until ACTION_USER_STARTED is received.
+ * @param numberOfIterationsToSkip number of iterations that must be skipped in the preStartUser
+ * method.
+ */
+ private void startUser_measuresAfterFirstIterations(int numberOfIterationsToSkip)
+ throws RemoteException {
+ /**
+ * Run start user and stop for the next iteration, measures time while mRunner.keepRunning()
+ * return true.
+ */
while (mRunner.keepRunning()) {
mRunner.pauseTiming();
+
final int userId = createUserNoFlags();
- final ProgressWaiter waiter = new ProgressWaiter();
+
+ preStartUser(userId, numberOfIterationsToSkip);
waitForBroadcastIdle();
- mRunner.resumeTiming();
- Log.i(TAG, "Starting timer");
+ waitCoolDownPeriod();
- final boolean success = mIam.startUserInBackgroundWithListener(userId, waiter)
- && waiter.waitForFinish(TIMEOUT_IN_SECOND * 1000);
+ runThenWaitForBroadcasts(userId, () -> {
+ mRunner.resumeTiming();
+ Log.i(TAG, "Starting timer");
+
+ mIam.startUserInBackground(userId);
+ }, Intent.ACTION_USER_STARTED);
mRunner.pauseTiming();
Log.i(TAG, "Stopping timer");
- assertTrue("Error: could not start user " + userId, success);
-
removeUser(userId);
- waitCoolDownPeriod();
mRunner.resumeTimingForNextIteration();
}
}
/**
+ * Tests starting an initialized user, with wait times in between iterations stopping between
+ * iterations,this test will skip the first two iterations and only measure the next ones.
+ *
+ * The first iteration will take longer due to the process of setting policy permissions for
+ * a new user.
+ *
+ * The second iteration takes longer than expected and has a bug (b/266574680) to investigate
+ * it.
+ *
+ * The next iterations take the expected time to start a user.
+ */
+ @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
+ public void startUser_startedTwiceBefore() throws RemoteException {
+ final int userId = createUserNoFlags();
+
+ //TODO(b/266681181) Reduce iteration number by 1 after investigation and possible fix.
+ preStartUser(userId, /* numberOfIterations */2);
+
+ /**
+ * Run start user and stop for the next iteration, measures time while mRunner.keepRunning()
+ * return true.
+ */
+ while (mRunner.keepRunning()) {
+ mRunner.pauseTiming();
+
+ waitForBroadcastIdle();
+ waitCoolDownPeriod();
+
+ runThenWaitForBroadcasts(userId, () -> {
+ mRunner.resumeTiming();
+ Log.i(TAG, "Starting timer");
+
+ mIam.startUserInBackground(userId);
+ }, Intent.ACTION_USER_STARTED);
+
+ mRunner.pauseTiming();
+ Log.i(TAG, "Stopping timer");
+
+ stopUser(userId, /* force */true);
+ mRunner.resumeTimingForNextIteration();
+ }
+
+ removeUser(userId);
+ }
+
+
+ /**
* Tests starting & unlocking an uninitialized user.
* Measures the time until unlock listener is triggered and user is unlocked.
*/
@@ -321,6 +397,46 @@
}
/**
+ * Tests starting & unlocking an initialized user, stopping the user at the end simulating real
+ * usage where the user is not removed after created and initialized.
+ * Measures the time until unlock listener is triggered and user is unlocked.
+ * This test will skip the first two iterations and only measure the next ones.
+ *
+ * The first iteration will take longer due to the process of setting policy permissions for a
+ * new user.
+ *
+ * The second iteration takes longer than expected and has a bug (b/266574680) to investigate
+ * it.
+ *
+ * The next iterations take the expected time to start a user.
+ */
+ @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
+ public void startAndUnlockUser_startedTwiceBefore() throws RemoteException {
+ final int userId = createUserNoFlags();
+
+ //TODO(b/266681181) Reduce iteration number by 1 after investigation and possible fix.
+ preStartUser(userId, /* numberOfIterations */2);
+
+ while (mRunner.keepRunning()) {
+ mRunner.pauseTiming();
+
+ waitCoolDownPeriod();
+ mRunner.resumeTiming();
+ Log.i(TAG, "Starting timer");
+
+ // Waits for UserState.mUnlockProgress.finish().
+ startUserInBackgroundAndWaitForUnlock(userId);
+
+ mRunner.pauseTiming();
+ Log.i(TAG, "Stopping timer");
+ stopUser(userId, /* force */true);
+ mRunner.resumeTimingForNextIteration();
+ }
+
+ removeUser(userId);
+ }
+
+ /**
* Tests starting & unlocking an uninitialized user.
* Measures the time until unlock listener is triggered and user is unlocked.
*/
@@ -404,28 +520,36 @@
}
}
- /** Tests switching to a previously-started, but no-longer-running, user with wait
- * times between iterations */
+ /**
+ * Tests switching to a previously-started, but no-longer-running, user with wait
+ * times between iterations
+ **/
@Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
public void switchUser_stopped_realistic() throws RemoteException {
- final int startUser = ActivityManager.getCurrentUser();
- final int testUser = initializeNewUserAndSwitchBack(/* stopNewUser */ true);
+ final int currentUserId = ActivityManager.getCurrentUser();
+ final int userId = initializeNewUserAndSwitchBack(/* stopNewUser */ true);
+
+ /**
+ * Skip the second iteration of start user process that is taking a long time to finish.
+ */
+ preStartUser(userId, /* numberOfIterations */1);
+
while (mRunner.keepRunning()) {
mRunner.pauseTiming();
waitCoolDownPeriod();
Log.d(TAG, "Starting timer");
mRunner.resumeTiming();
- switchUser(testUser);
+ switchUser(userId);
mRunner.pauseTiming();
Log.d(TAG, "Stopping timer");
- switchUserNoCheck(startUser);
- stopUserAfterWaitingForBroadcastIdle(testUser, true);
- attestFalse("Failed to stop user " + testUser, mAm.isUserRunning(testUser));
+ switchUserNoCheck(currentUserId);
+ stopUserAfterWaitingForBroadcastIdle(userId, /* force */true);
+ attestFalse("Failed to stop user " + userId, mAm.isUserRunning(userId));
mRunner.resumeTimingForNextIteration();
}
- removeUser(testUser);
+ removeUser(userId);
}
/** Tests switching to a previously-started, but no-longer-running, user with wait
@@ -1378,6 +1502,24 @@
}
}
+ /**
+ * Start the user and stop after that, will repeat numberOfIterations times.
+ * Make sure the user is started before proceeding with the test.
+ * @param userId identifier of the user that will be started.
+ * @param numberOfIterations number of iterations that must be skipped.
+ */
+ private void preStartUser(int userId, int numberOfIterations) throws RemoteException {
+ for (int i = 0; i < numberOfIterations; i++) {
+ final ProgressWaiter preWaiter = new ProgressWaiter();
+
+ final boolean preStartComplete = mIam.startUserInBackgroundWithListener(userId,
+ preWaiter) && preWaiter.waitForFinish(TIMEOUT_IN_SECOND * 1000);
+ stopUserAfterWaitingForBroadcastIdle(userId, /* force */true);
+
+ assertTrue("Pre start was not performed for user" + userId, preStartComplete);
+ }
+ }
+
private void fail(@NonNull String message) {
Log.e(TAG, "Test failed on iteration #" + mRunner.getIteration() + ": " + message);
mRunner.markAsFailed(new AssertionError(message));
diff --git a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
index 5d67c96..50a7a19 100644
--- a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
+++ b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
@@ -230,9 +230,10 @@
android.Manifest.permission.MANAGE_ACTIVITY_TASKS,
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
@Override
- public void stopUserVisibleJobsForUser(@NonNull String packageName, int userId) {
+ public void notePendingUserRequestedAppStop(@NonNull String packageName, int userId,
+ @Nullable String debugReason) {
try {
- mBinder.stopUserVisibleJobsForUser(packageName, userId);
+ mBinder.notePendingUserRequestedAppStop(packageName, userId, debugReason);
} catch (RemoteException e) {
}
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
index a1f1954..1ea0c5b 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
@@ -48,5 +48,5 @@
@EnforcePermission(allOf={"MANAGE_ACTIVITY_TASKS", "INTERACT_ACROSS_USERS_FULL"})
void unregisterUserVisibleJobObserver(in IUserVisibleJobObserver observer);
@EnforcePermission(allOf={"MANAGE_ACTIVITY_TASKS", "INTERACT_ACROSS_USERS_FULL"})
- void stopUserVisibleJobsForUser(String packageName, int userId);
+ void notePendingUserRequestedAppStop(String packageName, int userId, String debugReason);
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index e0fffb4..d4110a3 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -1897,7 +1897,11 @@
* <p>
* All user-initiated jobs must have an associated notification, set via
* {@link JobService#setNotification(JobParameters, int, Notification, int)}, and will be
- * shown in the Task Manager when running.
+ * shown in the Task Manager when running. These jobs cannot be rescheduled by the app
+ * if the user stops the job via system provided affordance (such as the Task Manager).
+ * Thus, it is best practice and recommended to provide action buttons in the
+ * associated notification to allow the user to stop the job gracefully
+ * and allow for rescheduling.
*
* <p>
* If the app doesn't hold the {@link android.Manifest.permission#RUN_LONG_JOBS} permission
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
index 4aec484..3ba5c31 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
@@ -515,5 +515,6 @@
android.Manifest.permission.MANAGE_ACTIVITY_TASKS,
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
@SuppressWarnings("HiddenAbstractMethod")
- public abstract void stopUserVisibleJobsForUser(@NonNull String packageName, int userId);
+ public abstract void notePendingUserRequestedAppStop(@NonNull String packageName, int userId,
+ @Nullable String debugReason);
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobService.java b/apex/jobscheduler/framework/java/android/app/job/JobService.java
index 1a205d0..2ab4324 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobService.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobService.java
@@ -156,6 +156,12 @@
* a future idle maintenance window.
* </p>
*
+ * <p class="note">
+ * Any {@link JobInfo.Builder#setUserInitiated(boolean) user-initiated job}
+ * cannot be rescheduled when the user has asked to stop the app
+ * via a system provided affordance (such as the Task Manager).
+ * In such situations, the value of {@code wantsReschedule} is always treated as {@code false}.
+ *
* @param params The parameters identifying this job, as supplied to
* the job in the {@link #onStartJob(JobParameters)} callback.
* @param wantsReschedule {@code true} if this job should be rescheduled according
@@ -220,6 +226,12 @@
* Once this method returns (or times out), the system releases the wakelock that it is holding
* on behalf of the job.</p>
*
+ * <p class="note">
+ * Any {@link JobInfo.Builder#setUserInitiated(boolean) user-initiated job}
+ * cannot be rescheduled when stopped by the user via a system provided affordance (such as
+ * the Task Manager). In such situations, the returned value from this method call is always
+ * treated as {@code false}.
+ *
* <p class="caution"><strong>Note:</strong> When a job is stopped and rescheduled via this
* method call, the deadline constraint is excluded from the rescheduled job's constraint set.
* The rescheduled job will run again once all remaining constraints are satisfied.
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index d7163d8..5599e54 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -1933,8 +1933,9 @@
Intent.FLAG_RECEIVER_REGISTERED_ONLY
| Intent.FLAG_RECEIVER_FOREGROUND
| Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
- mTimeTickOptions = BroadcastOptions
- .makeRemovingMatchingFilter(new IntentFilter(Intent.ACTION_TIME_TICK))
+ mTimeTickOptions = BroadcastOptions.makeBasic()
+ .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
+ .setDeferUntilActive(true)
.toBundle();
mTimeTickTrigger = new IAlarmListener.Stub() {
@Override
@@ -4252,8 +4253,8 @@
}
}
// And send a TIME_TICK right now, since it is important to get the UI updated.
- mHandler.post(() ->
- getContext().sendBroadcastAsUser(mTimeTickIntent, UserHandle.ALL));
+ mHandler.post(() -> getContext().sendBroadcastAsUser(mTimeTickIntent,
+ UserHandle.ALL, null, mTimeTickOptions));
} else {
mNonInteractiveStartTime = nowELAPSED;
}
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 62d97358..a474cc8 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -1282,17 +1282,20 @@
}
@GuardedBy("mLock")
- void stopUserVisibleJobsLocked(int userId, @NonNull String packageName,
- @JobParameters.StopReason int reason, int internalReasonCode) {
+ void markJobsForUserStopLocked(int userId, @NonNull String packageName,
+ @Nullable String debugReason) {
for (int i = mActiveServices.size() - 1; i >= 0; --i) {
final JobServiceContext jsc = mActiveServices.get(i);
final JobStatus jobStatus = jsc.getRunningJobLocked();
- if (jobStatus != null && userId == jobStatus.getSourceUserId()
- && jobStatus.getSourcePackageName().equals(packageName)
- && jobStatus.isUserVisibleJob()) {
- jsc.cancelExecutingJobLocked(reason, internalReasonCode,
- JobParameters.getInternalReasonCodeDescription(internalReasonCode));
+ // Normally, we handle jobs primarily using the source package and userId,
+ // however, user-visible jobs are shown as coming from the calling app, so we
+ // need to operate on the jobs from that perspective here.
+ if (jobStatus != null && userId == jobStatus.getUserId()
+ && jobStatus.getServiceComponent().getPackageName().equals(packageName)) {
+ jsc.markForProcessDeathLocked(JobParameters.STOP_REASON_USER,
+ JobParameters.INTERNAL_STOP_REASON_USER_UI_STOP,
+ debugReason);
}
}
}
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 e8fcdd2..fce75a2 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -1701,16 +1701,15 @@
}
@VisibleForTesting
- void stopUserVisibleJobsInternal(@NonNull String packageName, int userId) {
+ void notePendingUserRequestedAppStopInternal(@NonNull String packageName, int userId,
+ @Nullable String debugReason) {
final int packageUid = mLocalPM.getPackageUid(packageName, 0, userId);
if (packageUid < 0) {
Slog.wtf(TAG, "Asked to stop jobs of an unknown package");
return;
}
synchronized (mLock) {
- mConcurrencyManager.stopUserVisibleJobsLocked(userId, packageName,
- JobParameters.STOP_REASON_USER,
- JobParameters.INTERNAL_STOP_REASON_USER_UI_STOP);
+ mConcurrencyManager.markJobsForUserStopLocked(userId, packageName, debugReason);
final ArraySet<JobStatus> jobs = mJobs.getJobsByUid(packageUid);
for (int i = jobs.size() - 1; i >= 0; i--) {
final JobStatus job = jobs.valueAt(i);
@@ -2387,12 +2386,25 @@
*
* @param failureToReschedule Provided job status that we will reschedule.
* @return A newly instantiated JobStatus with the same constraints as the last job except
- * with adjusted timing constraints.
+ * with adjusted timing constraints, or {@code null} if the job shouldn't be rescheduled for
+ * some policy reason.
* @see #maybeQueueReadyJobsForExecutionLocked
*/
+ @Nullable
@VisibleForTesting
JobStatus getRescheduleJobForFailureLocked(JobStatus failureToReschedule,
@JobParameters.StopReason int stopReason, int internalStopReason) {
+ if (internalStopReason == JobParameters.INTERNAL_STOP_REASON_USER_UI_STOP
+ && failureToReschedule.isUserVisibleJob()) {
+ // If a user stops an app via Task Manager and the job was user-visible, then assume
+ // the user wanted to stop that task and not let it run in the future. It's in the
+ // app's best interests to provide action buttons in their notification to avoid this
+ // scenario.
+ Slog.i(TAG,
+ "Dropping " + failureToReschedule.toShortString() + " because of user stop");
+ return null;
+ }
+
final long elapsedNowMillis = sElapsedRealtimeClock.millis();
final JobInfo job = failureToReschedule.getJob();
@@ -4225,12 +4237,13 @@
@Override
@EnforcePermission(allOf = {MANAGE_ACTIVITY_TASKS, INTERACT_ACROSS_USERS_FULL})
- public void stopUserVisibleJobsForUser(@NonNull String packageName, int userId) {
- super.stopUserVisibleJobsForUser_enforcePermission();
+ public void notePendingUserRequestedAppStop(@NonNull String packageName, int userId,
+ @Nullable String debugReason) {
+ super.notePendingUserRequestedAppStop_enforcePermission();
if (packageName == null) {
throw new NullPointerException("packageName");
}
- JobSchedulerService.this.stopUserVisibleJobsInternal(packageName, userId);
+ notePendingUserRequestedAppStopInternal(packageName, userId, debugReason);
}
}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index 911744f..cc9e517 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -190,6 +190,14 @@
private Network mPendingNetworkChange;
+ /**
+ * The reason this job is marked for death. If it's not marked for death,
+ * then the value should be {@link JobParameters#STOP_REASON_UNDEFINED}.
+ */
+ private int mDeathMarkStopReason = JobParameters.STOP_REASON_UNDEFINED;
+ private int mDeathMarkInternalStopReason;
+ private String mDeathMarkDebugReason;
+
// Debugging: reason this job was last stopped.
public String mStoppedReason;
@@ -452,6 +460,7 @@
mStoppedReason = null;
mStoppedTime = 0;
job.startedAsExpeditedJob = job.shouldTreatAsExpeditedJob();
+ job.startedAsUserInitiatedJob = job.shouldTreatAsUserInitiatedJob();
return true;
}
}
@@ -502,6 +511,34 @@
doCancelLocked(reason, internalStopReason, debugReason);
}
+ /**
+ * Called when an app's process is about to be killed and we want to update the job's stop
+ * reasons without telling the job it's going to be stopped.
+ */
+ @GuardedBy("mLock")
+ void markForProcessDeathLocked(@JobParameters.StopReason int reason,
+ int internalStopReason, @NonNull String debugReason) {
+ if (mVerb == VERB_FINISHED) {
+ if (DEBUG) {
+ Slog.d(TAG, "Too late to mark for death (verb=" + mVerb + "), ignoring.");
+ }
+ return;
+ }
+ if (DEBUG) {
+ Slog.d(TAG,
+ "Marking " + mRunningJob.toShortString() + " for death because "
+ + reason + ":" + debugReason);
+ }
+ mDeathMarkStopReason = reason;
+ mDeathMarkInternalStopReason = internalStopReason;
+ mDeathMarkDebugReason = debugReason;
+ if (mParams.getStopReason() == JobParameters.STOP_REASON_UNDEFINED) {
+ // Only set the stop reason if we're not already trying to stop the job for some
+ // other reason in case that other stop is successful before the process dies.
+ mParams.setStopReason(reason, internalStopReason, debugReason);
+ }
+ }
+
int getPreferredUid() {
return mPreferredUid;
}
@@ -648,7 +685,7 @@
"last work dequeued");
// This will finish the job.
doCallbackLocked(false, "last work dequeued");
- } else {
+ } else if (work != null) {
// Delivery count has been updated, so persist JobWorkItem change.
mService.mJobs.touchJob(mRunningJob);
}
@@ -754,6 +791,12 @@
@Override
public void onServiceDisconnected(ComponentName name) {
synchronized (mLock) {
+ if (mDeathMarkStopReason != JobParameters.STOP_REASON_UNDEFINED) {
+ // Service "unexpectedly" disconnected, but we knew the process was going to die.
+ // Use that as the stop reason for logging/debugging purposes.
+ mParams.setStopReason(
+ mDeathMarkStopReason, mDeathMarkInternalStopReason, mDeathMarkDebugReason);
+ }
closeAndCleanupJobLocked(true /* needsReschedule */, "unexpectedly disconnected");
}
}
@@ -1182,29 +1225,51 @@
* we want to clean up internally.
*/
@GuardedBy("mLock")
- private void closeAndCleanupJobLocked(boolean reschedule, @Nullable String reason) {
+ private void closeAndCleanupJobLocked(boolean reschedule, @Nullable String loggingDebugReason) {
final JobStatus completedJob;
if (mVerb == VERB_FINISHED) {
return;
}
if (DEBUG) {
Slog.d(TAG, "Cleaning up " + mRunningJob.toShortString()
- + " reschedule=" + reschedule + " reason=" + reason);
+ + " reschedule=" + reschedule + " reason=" + loggingDebugReason);
}
- applyStoppedReasonLocked(reason);
+ applyStoppedReasonLocked(loggingDebugReason);
completedJob = mRunningJob;
- final int internalStopReason = mParams.getInternalStopReasonCode();
- final int stopReason = mParams.getStopReason();
+ // Use the JobParameters stop reasons for logging and metric purposes,
+ // but if the job was marked for death, use that reason for rescheduling purposes.
+ // The discrepancy could happen if a job ends up stopping for some reason
+ // in the time between the job being marked and the process actually dying.
+ // Since the job stopped for another reason, we want to log the actual stop reason
+ // for the sake of accurate metrics and debugging,
+ // but we should use the death mark reasons when determining reschedule policy.
+ final int loggingStopReason = mParams.getStopReason();
+ final int loggingInternalStopReason = mParams.getInternalStopReasonCode();
+ final int reschedulingStopReason, reschedulingInternalStopReason;
+ if (mDeathMarkStopReason != JobParameters.STOP_REASON_UNDEFINED) {
+ if (DEBUG) {
+ Slog.d(TAG, "Job marked for death because of "
+ + JobParameters.getInternalReasonCodeDescription(
+ mDeathMarkInternalStopReason)
+ + ": " + mDeathMarkDebugReason);
+ }
+ reschedulingStopReason = mDeathMarkStopReason;
+ reschedulingInternalStopReason = mDeathMarkInternalStopReason;
+ } else {
+ reschedulingStopReason = loggingStopReason;
+ reschedulingInternalStopReason = loggingInternalStopReason;
+ }
mPreviousJobHadSuccessfulFinish =
- (internalStopReason == JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH);
+ (loggingInternalStopReason == JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH);
if (!mPreviousJobHadSuccessfulFinish) {
mLastUnsuccessfulFinishElapsed = sElapsedRealtimeClock.millis();
}
- mJobPackageTracker.noteInactive(completedJob, internalStopReason, reason);
+ mJobPackageTracker.noteInactive(completedJob,
+ loggingInternalStopReason, loggingDebugReason);
FrameworkStatsLog.write_non_chained(FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED,
completedJob.getSourceUid(), null, completedJob.getBatteryName(),
FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED__STATE__FINISHED,
- internalStopReason, completedJob.getStandbyBucket(), completedJob.getJobId(),
+ loggingInternalStopReason, completedJob.getStandbyBucket(), completedJob.getJobId(),
completedJob.hasChargingConstraint(),
completedJob.hasBatteryNotLowConstraint(),
completedJob.hasStorageNotLowConstraint(),
@@ -1215,7 +1280,7 @@
completedJob.hasContentTriggerConstraint(),
completedJob.isRequestedExpeditedJob(),
completedJob.startedAsExpeditedJob,
- stopReason,
+ loggingStopReason,
completedJob.getJob().isPrefetch(),
completedJob.getJob().getPriority(),
completedJob.getEffectivePriority(),
@@ -1235,11 +1300,11 @@
}
try {
mBatteryStats.noteJobFinish(mRunningJob.getBatteryName(), mRunningJob.getSourceUid(),
- internalStopReason);
+ loggingInternalStopReason);
} catch (RemoteException e) {
// Whatever.
}
- if (mParams.getStopReason() == JobParameters.STOP_REASON_TIMEOUT) {
+ if (loggingStopReason == JobParameters.STOP_REASON_TIMEOUT) {
mEconomyManagerInternal.noteInstantaneousEvent(
mRunningJob.getSourceUserId(), mRunningJob.getSourcePackageName(),
JobSchedulerEconomicPolicy.ACTION_JOB_TIMEOUT,
@@ -1260,6 +1325,9 @@
mCancelled = false;
service = null;
mAvailable = true;
+ mDeathMarkStopReason = JobParameters.STOP_REASON_UNDEFINED;
+ mDeathMarkInternalStopReason = 0;
+ mDeathMarkDebugReason = null;
mPendingStopReason = JobParameters.STOP_REASON_UNDEFINED;
mPendingInternalStopReason = 0;
mPendingDebugStopReason = null;
@@ -1268,8 +1336,8 @@
if (completedJob.isUserVisibleJob()) {
mService.informObserversOfUserVisibleJobChange(this, completedJob, false);
}
- mCompletedListener.onJobCompletedLocked(completedJob, stopReason, internalStopReason,
- reschedule);
+ mCompletedListener.onJobCompletedLocked(completedJob,
+ reschedulingStopReason, reschedulingInternalStopReason, reschedule);
mJobConcurrencyManager.onJobCompletedLocked(this, completedJob, workType);
}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index ce33a8e..1971a11 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -402,6 +402,11 @@
* running. This isn't copied over when a job is rescheduled.
*/
public boolean startedAsExpeditedJob = false;
+ /**
+ * Whether or not this particular JobStatus instance was treated as a user-initiated job
+ * when it started running. This isn't copied over when a job is rescheduled.
+ */
+ public boolean startedAsUserInitiatedJob = false;
public boolean startedWithImmediacyPrivilege = false;
@@ -1407,7 +1412,7 @@
* @return true if this is a job whose execution should be made visible to the user.
*/
public boolean isUserVisibleJob() {
- return shouldTreatAsUserInitiatedJob();
+ return shouldTreatAsUserInitiatedJob() || startedAsUserInitiatedJob;
}
/**
@@ -2568,6 +2573,13 @@
pw.print(startedAsExpeditedJob);
pw.println(")");
}
+ if ((getFlags() & JobInfo.FLAG_USER_INITIATED) != 0) {
+ pw.print("userInitiatedApproved: ");
+ pw.print(shouldTreatAsUserInitiatedJob());
+ pw.print(" (started as UIJ: ");
+ pw.print(startedAsUserInitiatedJob);
+ pw.println(")");
+ }
pw.decreaseIndent();
if (changedAuthorities != null) {
diff --git a/core/api/current.txt b/core/api/current.txt
index e27f6eb..de540bf 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -121,6 +121,7 @@
field public static final String INTERACT_ACROSS_PROFILES = "android.permission.INTERACT_ACROSS_PROFILES";
field public static final String INTERNET = "android.permission.INTERNET";
field public static final String KILL_BACKGROUND_PROCESSES = "android.permission.KILL_BACKGROUND_PROCESSES";
+ field public static final String LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE = "android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE";
field public static final String LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK = "android.permission.LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK";
field public static final String LOADER_USAGE_STATS = "android.permission.LOADER_USAGE_STATS";
field public static final String LOCATION_HARDWARE = "android.permission.LOCATION_HARDWARE";
@@ -4756,6 +4757,7 @@
method public int getLaunchDisplayId();
method public boolean getLockTaskMode();
method public int getPendingIntentBackgroundActivityStartMode();
+ method public int getPendingIntentCreatorBackgroundActivityStartMode();
method public int getSplashScreenStyle();
method @Deprecated public boolean isPendingIntentBackgroundActivityLaunchAllowed();
method public boolean isShareIdentityEnabled();
@@ -4776,6 +4778,7 @@
method public android.app.ActivityOptions setLockTaskEnabled(boolean);
method @Deprecated public void setPendingIntentBackgroundActivityLaunchAllowed(boolean);
method @NonNull public android.app.ActivityOptions setPendingIntentBackgroundActivityStartMode(int);
+ method @NonNull public android.app.ActivityOptions setPendingIntentCreatorBackgroundActivityStartMode(int);
method @NonNull public android.app.ActivityOptions setShareIdentityEnabled(boolean);
method @NonNull public android.app.ActivityOptions setSplashScreenStyle(int);
method public android.os.Bundle toBundle();
@@ -5206,6 +5209,13 @@
field @NonNull public static final android.os.Parcelable.Creator<android.app.BackgroundServiceStartNotAllowedException> CREATOR;
}
+ public class BroadcastOptions {
+ method public boolean isShareIdentityEnabled();
+ method @NonNull public static android.app.BroadcastOptions makeBasic();
+ method @NonNull public android.app.BroadcastOptions setShareIdentityEnabled(boolean);
+ method @NonNull public android.os.Bundle toBundle();
+ }
+
public class DatePickerDialog extends android.app.AlertDialog implements android.widget.DatePicker.OnDateChangedListener android.content.DialogInterface.OnClickListener {
ctor public DatePickerDialog(@NonNull android.content.Context);
ctor public DatePickerDialog(@NonNull android.content.Context, @StyleRes int);
@@ -7190,6 +7200,7 @@
}
public class StatusBarManager {
+ method @RequiresPermission(android.Manifest.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE) public boolean canLaunchCaptureContentActivityForNote(@NonNull android.app.Activity);
method public void requestAddTileService(@NonNull android.content.ComponentName, @NonNull CharSequence, @NonNull android.graphics.drawable.Icon, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
field public static final int TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND = 1004; // 0x3ec
field public static final int TILE_ADD_REQUEST_ERROR_BAD_COMPONENT = 1002; // 0x3ea
@@ -9569,6 +9580,8 @@
method public final int getResultCode();
method public final String getResultData();
method public final android.os.Bundle getResultExtras(boolean);
+ method @Nullable public String getSentFromPackage();
+ method public int getSentFromUid();
method public final android.content.BroadcastReceiver.PendingResult goAsync();
method public final boolean isInitialStickyBroadcast();
method public final boolean isOrderedBroadcast();
@@ -10169,11 +10182,14 @@
method public abstract void revokeUriPermission(String, android.net.Uri, int);
method public abstract void sendBroadcast(@RequiresPermission android.content.Intent);
method public abstract void sendBroadcast(@RequiresPermission android.content.Intent, @Nullable String);
+ method public void sendBroadcast(@NonNull android.content.Intent, @Nullable String, @Nullable android.os.Bundle);
method @RequiresPermission("android.permission.INTERACT_ACROSS_USERS") public abstract void sendBroadcastAsUser(@RequiresPermission android.content.Intent, android.os.UserHandle);
method @RequiresPermission("android.permission.INTERACT_ACROSS_USERS") public abstract void sendBroadcastAsUser(@RequiresPermission android.content.Intent, android.os.UserHandle, @Nullable String);
method public void sendBroadcastWithMultiplePermissions(@NonNull android.content.Intent, @NonNull String[]);
method public abstract void sendOrderedBroadcast(@RequiresPermission android.content.Intent, @Nullable String);
+ method public void sendOrderedBroadcast(@NonNull android.content.Intent, @Nullable String, @Nullable android.os.Bundle);
method public abstract void sendOrderedBroadcast(@NonNull @RequiresPermission android.content.Intent, @Nullable String, @Nullable android.content.BroadcastReceiver, @Nullable android.os.Handler, int, @Nullable String, @Nullable android.os.Bundle);
+ method public void sendOrderedBroadcast(@NonNull android.content.Intent, @Nullable String, @Nullable android.os.Bundle, @Nullable android.content.BroadcastReceiver, @Nullable android.os.Handler, int, @Nullable String, @Nullable android.os.Bundle);
method public void sendOrderedBroadcast(@NonNull android.content.Intent, @Nullable String, @Nullable String, @Nullable android.content.BroadcastReceiver, @Nullable android.os.Handler, int, @Nullable String, @Nullable android.os.Bundle);
method @RequiresPermission("android.permission.INTERACT_ACROSS_USERS") public abstract void sendOrderedBroadcastAsUser(@RequiresPermission android.content.Intent, android.os.UserHandle, @Nullable String, android.content.BroadcastReceiver, @Nullable android.os.Handler, int, @Nullable String, @Nullable android.os.Bundle);
method @Deprecated @RequiresPermission(android.Manifest.permission.BROADCAST_STICKY) public abstract void sendStickyBroadcast(@RequiresPermission android.content.Intent);
@@ -10699,6 +10715,7 @@
field public static final String ACTION_INSERT_OR_EDIT = "android.intent.action.INSERT_OR_EDIT";
field public static final String ACTION_INSTALL_FAILURE = "android.intent.action.INSTALL_FAILURE";
field @Deprecated public static final String ACTION_INSTALL_PACKAGE = "android.intent.action.INSTALL_PACKAGE";
+ field @RequiresPermission(android.Manifest.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE) public static final String ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE = "android.intent.action.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE";
field public static final String ACTION_LOCALE_CHANGED = "android.intent.action.LOCALE_CHANGED";
field public static final String ACTION_LOCKED_BOOT_COMPLETED = "android.intent.action.LOCKED_BOOT_COMPLETED";
field public static final String ACTION_MAIN = "android.intent.action.MAIN";
@@ -10792,6 +10809,11 @@
field public static final String ACTION_VOICE_COMMAND = "android.intent.action.VOICE_COMMAND";
field @Deprecated public static final String ACTION_WALLPAPER_CHANGED = "android.intent.action.WALLPAPER_CHANGED";
field public static final String ACTION_WEB_SEARCH = "android.intent.action.WEB_SEARCH";
+ field public static final int CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN = 4; // 0x4
+ field public static final int CAPTURE_CONTENT_FOR_NOTE_FAILED = 1; // 0x1
+ field public static final int CAPTURE_CONTENT_FOR_NOTE_SUCCESS = 0; // 0x0
+ field public static final int CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED = 2; // 0x2
+ field public static final int CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED = 3; // 0x3
field public static final String CATEGORY_ACCESSIBILITY_SHORTCUT_TARGET = "android.intent.category.ACCESSIBILITY_SHORTCUT_TARGET";
field public static final String CATEGORY_ALTERNATIVE = "android.intent.category.ALTERNATIVE";
field public static final String CATEGORY_APP_BROWSER = "android.intent.category.APP_BROWSER";
@@ -10847,12 +10869,14 @@
field public static final String EXTRA_AUTO_LAUNCH_SINGLE_CHOICE = "android.intent.extra.AUTO_LAUNCH_SINGLE_CHOICE";
field public static final String EXTRA_BCC = "android.intent.extra.BCC";
field public static final String EXTRA_BUG_REPORT = "android.intent.extra.BUG_REPORT";
+ field public static final String EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE = "android.intent.extra.CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE";
field public static final String EXTRA_CC = "android.intent.extra.CC";
field @Deprecated public static final String EXTRA_CHANGED_COMPONENT_NAME = "android.intent.extra.changed_component_name";
field public static final String EXTRA_CHANGED_COMPONENT_NAME_LIST = "android.intent.extra.changed_component_name_list";
field public static final String EXTRA_CHANGED_PACKAGE_LIST = "android.intent.extra.changed_package_list";
field public static final String EXTRA_CHANGED_UID_LIST = "android.intent.extra.changed_uid_list";
field public static final String EXTRA_CHOOSER_CUSTOM_ACTIONS = "android.intent.extra.CHOOSER_CUSTOM_ACTIONS";
+ field public static final String EXTRA_CHOOSER_PAYLOAD_RESELECTION_ACTION = "android.intent.extra.CHOOSER_PAYLOAD_RESELECTION_ACTION";
field public static final String EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER = "android.intent.extra.CHOOSER_REFINEMENT_INTENT_SENDER";
field public static final String EXTRA_CHOOSER_TARGETS = "android.intent.extra.CHOOSER_TARGETS";
field public static final String EXTRA_CHOSEN_COMPONENT = "android.intent.extra.CHOSEN_COMPONENT";
@@ -12095,11 +12119,11 @@
public static final class PackageInstaller.InstallConstraints implements android.os.Parcelable {
method public int describeContents();
- method public boolean isRequireAppNotForeground();
- method public boolean isRequireAppNotInteracting();
- method public boolean isRequireAppNotTopVisible();
- method public boolean isRequireDeviceIdle();
- method public boolean isRequireNotInCall();
+ method public boolean isAppNotForegroundRequired();
+ method public boolean isAppNotInteractingRequired();
+ method public boolean isAppNotTopVisibleRequired();
+ method public boolean isDeviceIdleRequired();
+ method public boolean isNotInCallRequired();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.PackageInstaller.InstallConstraints> CREATOR;
field @NonNull public static final android.content.pm.PackageInstaller.InstallConstraints GENTLE_UPDATE;
@@ -12108,16 +12132,16 @@
public static final class PackageInstaller.InstallConstraints.Builder {
ctor public PackageInstaller.InstallConstraints.Builder();
method @NonNull public android.content.pm.PackageInstaller.InstallConstraints build();
- method @NonNull public android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotForeground();
- method @NonNull public android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotInteracting();
- method @NonNull public android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotTopVisible();
- method @NonNull public android.content.pm.PackageInstaller.InstallConstraints.Builder requireDeviceIdle();
- method @NonNull public android.content.pm.PackageInstaller.InstallConstraints.Builder requireNotInCall();
+ method @NonNull public android.content.pm.PackageInstaller.InstallConstraints.Builder setAppNotForegroundRequired();
+ method @NonNull public android.content.pm.PackageInstaller.InstallConstraints.Builder setAppNotInteractingRequired();
+ method @NonNull public android.content.pm.PackageInstaller.InstallConstraints.Builder setAppNotTopVisibleRequired();
+ method @NonNull public android.content.pm.PackageInstaller.InstallConstraints.Builder setDeviceIdleRequired();
+ method @NonNull public android.content.pm.PackageInstaller.InstallConstraints.Builder setNotInCallRequired();
}
public static final class PackageInstaller.InstallConstraintsResult implements android.os.Parcelable {
+ method public boolean areAllConstraintsSatisfied();
method public int describeContents();
- method public boolean isAllConstraintsSatisfied();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.PackageInstaller.InstallConstraintsResult> CREATOR;
}
@@ -12184,12 +12208,12 @@
method @Nullable public String getAppPackageName();
method @NonNull public int[] getChildSessionIds();
method public long getCreatedMillis();
+ method public boolean getDontKillApp();
method public int getInstallLocation();
method public int getInstallReason();
method @Nullable public String getInstallerAttributionTag();
method @Nullable public String getInstallerPackageName();
method public int getInstallerUid();
- method @NonNull public boolean getIsPreApprovalRequested();
method public int getMode();
method public int getOriginatingUid();
method @Nullable public android.net.Uri getOriginatingUri();
@@ -12209,6 +12233,7 @@
method public boolean isApplicationEnabledSettingPersistent();
method public boolean isCommitted();
method public boolean isMultiPackage();
+ method public boolean isPreApprovalRequested();
method public boolean isRequestUpdateOwnership();
method public boolean isSealed();
method public boolean isStaged();
@@ -12239,6 +12264,7 @@
method public void setAppPackageName(@Nullable String);
method public void setApplicationEnabledSettingPersistent();
method @Deprecated public void setAutoRevokePermissionsMode(boolean);
+ method public void setDontKillApp(boolean);
method public void setInstallLocation(int);
method public void setInstallReason(int);
method public void setInstallScenario(int);
@@ -13485,6 +13511,18 @@
method public void unregisterCredentialDescription(@NonNull android.credentials.UnregisterCredentialDescriptionRequest);
}
+ public final class CredentialOption implements android.os.Parcelable {
+ ctor public CredentialOption(@NonNull String, @NonNull android.os.Bundle, @NonNull android.os.Bundle, boolean);
+ method public int describeContents();
+ method @NonNull public android.os.Bundle getCandidateQueryData();
+ method @NonNull public android.os.Bundle getCredentialRetrievalData();
+ method @NonNull public String getType();
+ method public boolean isSystemProviderRequired();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.credentials.CredentialOption> CREATOR;
+ field public static final String FLATTENED_REQUEST = "android.credentials.GetCredentialOption.FLATTENED_REQUEST_STRING";
+ }
+
public class GetCredentialException extends java.lang.Exception {
ctor public GetCredentialException(@NonNull String, @Nullable String);
ctor public GetCredentialException(@NonNull String, @Nullable String, @Nullable Throwable);
@@ -13497,31 +13535,19 @@
field @NonNull public static final String TYPE_USER_CANCELED = "android.credentials.GetCredentialException.TYPE_USER_CANCELED";
}
- public final class GetCredentialOption implements android.os.Parcelable {
- ctor public GetCredentialOption(@NonNull String, @NonNull android.os.Bundle, @NonNull android.os.Bundle, boolean);
- method public int describeContents();
- method @NonNull public android.os.Bundle getCandidateQueryData();
- method @NonNull public android.os.Bundle getCredentialRetrievalData();
- method @NonNull public String getType();
- method public boolean isSystemProviderRequired();
- method public void writeToParcel(@NonNull android.os.Parcel, int);
- field @NonNull public static final android.os.Parcelable.Creator<android.credentials.GetCredentialOption> CREATOR;
- field public static final String FLATTENED_REQUEST = "android.credentials.GetCredentialOption.FLATTENED_REQUEST_STRING";
- }
-
public final class GetCredentialRequest implements android.os.Parcelable {
method public int describeContents();
+ method @NonNull public java.util.List<android.credentials.CredentialOption> getCredentialOptions();
method @NonNull public android.os.Bundle getData();
- method @NonNull public java.util.List<android.credentials.GetCredentialOption> getGetCredentialOptions();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.credentials.GetCredentialRequest> CREATOR;
}
public static final class GetCredentialRequest.Builder {
ctor public GetCredentialRequest.Builder(@NonNull android.os.Bundle);
- method @NonNull public android.credentials.GetCredentialRequest.Builder addGetCredentialOption(@NonNull android.credentials.GetCredentialOption);
+ method @NonNull public android.credentials.GetCredentialRequest.Builder addCredentialOption(@NonNull android.credentials.CredentialOption);
method @NonNull public android.credentials.GetCredentialRequest build();
- method @NonNull public android.credentials.GetCredentialRequest.Builder setGetCredentialOptions(@NonNull java.util.List<android.credentials.GetCredentialOption>);
+ method @NonNull public android.credentials.GetCredentialRequest.Builder setCredentialOptions(@NonNull java.util.List<android.credentials.CredentialOption>);
}
public final class GetCredentialResponse implements android.os.Parcelable {
@@ -15312,6 +15338,29 @@
ctor @Deprecated public EmbossMaskFilter(float[], float, float, float);
}
+ public class HardwareBufferRenderer implements java.lang.AutoCloseable {
+ ctor public HardwareBufferRenderer(@NonNull android.hardware.HardwareBuffer);
+ method public void close();
+ method public boolean isClosed();
+ method @NonNull public android.graphics.HardwareBufferRenderer.RenderRequest obtainRenderRequest();
+ method public void setContentRoot(@Nullable android.graphics.RenderNode);
+ method public void setLightSourceAlpha(@FloatRange(from=0.0f, to=1.0f) float, @FloatRange(from=0.0f, to=1.0f) float);
+ method public void setLightSourceGeometry(float, float, @FloatRange(from=0.0f) float, @FloatRange(from=0.0f) float);
+ }
+
+ public final class HardwareBufferRenderer.RenderRequest {
+ method public void draw(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.graphics.HardwareBufferRenderer.RenderResult>);
+ method @NonNull public android.graphics.HardwareBufferRenderer.RenderRequest setBufferTransform(int);
+ method @NonNull public android.graphics.HardwareBufferRenderer.RenderRequest setColorSpace(@Nullable android.graphics.ColorSpace);
+ }
+
+ public static final class HardwareBufferRenderer.RenderResult {
+ method @NonNull public android.hardware.SyncFence getFence();
+ method public int getStatus();
+ field public static final int ERROR_UNKNOWN = 1; // 0x1
+ field public static final int SUCCESS = 0; // 0x0
+ }
+
public class HardwareRenderer {
ctor public HardwareRenderer();
method public void clearContent();
@@ -19320,7 +19369,9 @@
public final class DisplayManager {
method public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, @IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, @Nullable android.view.Surface, int);
+ method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, @IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, float, @Nullable android.view.Surface, int);
method public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, @IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, @Nullable android.view.Surface, int, @Nullable android.hardware.display.VirtualDisplay.Callback, @Nullable android.os.Handler);
+ method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, @IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, float, @Nullable android.view.Surface, int, @Nullable android.os.Handler, @Nullable android.hardware.display.VirtualDisplay.Callback);
method public android.view.Display getDisplay(int);
method public android.view.Display[] getDisplays();
method public android.view.Display[] getDisplays(String);
@@ -23981,6 +24032,7 @@
method @Nullable public android.net.Uri getIconUri();
method @NonNull public String getId();
method @NonNull public CharSequence getName();
+ method public int getType();
method public int getVolume();
method public int getVolumeHandling();
method public int getVolumeMax();
@@ -23997,6 +24049,22 @@
field public static final String FEATURE_REMOTE_VIDEO_PLAYBACK = "android.media.route.feature.REMOTE_VIDEO_PLAYBACK";
field public static final int PLAYBACK_VOLUME_FIXED = 0; // 0x0
field public static final int PLAYBACK_VOLUME_VARIABLE = 1; // 0x1
+ field public static final int TYPE_BLE_HEADSET = 26; // 0x1a
+ field public static final int TYPE_BLUETOOTH_A2DP = 8; // 0x8
+ field public static final int TYPE_BUILTIN_SPEAKER = 2; // 0x2
+ field public static final int TYPE_DOCK = 13; // 0xd
+ field public static final int TYPE_GROUP = 2000; // 0x7d0
+ field public static final int TYPE_HDMI = 9; // 0x9
+ field public static final int TYPE_HEARING_AID = 23; // 0x17
+ field public static final int TYPE_REMOTE_AUDIO_VIDEO_RECEIVER = 1003; // 0x3eb
+ field public static final int TYPE_REMOTE_SPEAKER = 1002; // 0x3ea
+ field public static final int TYPE_REMOTE_TV = 1001; // 0x3e9
+ field public static final int TYPE_UNKNOWN = 0; // 0x0
+ field public static final int TYPE_USB_ACCESSORY = 12; // 0xc
+ field public static final int TYPE_USB_DEVICE = 11; // 0xb
+ field public static final int TYPE_USB_HEADSET = 22; // 0x16
+ field public static final int TYPE_WIRED_HEADPHONES = 4; // 0x4
+ field public static final int TYPE_WIRED_HEADSET = 3; // 0x3
}
public static final class MediaRoute2Info.Builder {
@@ -24012,6 +24080,7 @@
method @NonNull public android.media.MediaRoute2Info.Builder setDescription(@Nullable CharSequence);
method @NonNull public android.media.MediaRoute2Info.Builder setExtras(@Nullable android.os.Bundle);
method @NonNull public android.media.MediaRoute2Info.Builder setIconUri(@Nullable android.net.Uri);
+ method @NonNull public android.media.MediaRoute2Info.Builder setType(int);
method @NonNull public android.media.MediaRoute2Info.Builder setVisibilityPublic();
method @NonNull public android.media.MediaRoute2Info.Builder setVisibilityRestricted(@NonNull java.util.Set<java.lang.String>);
method @NonNull public android.media.MediaRoute2Info.Builder setVolume(int);
@@ -24557,8 +24626,8 @@
public final class RouteListingPreference implements android.os.Parcelable {
method public int describeContents();
- method @Nullable public android.content.ComponentName getInAppOnlyItemRoutingReceiver();
method @NonNull public java.util.List<android.media.RouteListingPreference.Item> getItems();
+ method @Nullable public android.content.ComponentName getLinkedItemComponentName();
method public boolean getUseSystemOrdering();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field public static final String ACTION_TRANSFER_MEDIA = "android.media.action.TRANSFER_MEDIA";
@@ -24569,37 +24638,40 @@
public static final class RouteListingPreference.Builder {
ctor public RouteListingPreference.Builder();
method @NonNull public android.media.RouteListingPreference build();
- method @NonNull public android.media.RouteListingPreference.Builder setInAppOnlyItemRoutingReceiver(@Nullable android.content.ComponentName);
method @NonNull public android.media.RouteListingPreference.Builder setItems(@NonNull java.util.List<android.media.RouteListingPreference.Item>);
+ method @NonNull public android.media.RouteListingPreference.Builder setLinkedItemComponentName(@Nullable android.content.ComponentName);
method @NonNull public android.media.RouteListingPreference.Builder setUseSystemOrdering(boolean);
}
public static final class RouteListingPreference.Item implements android.os.Parcelable {
method public int describeContents();
- method @Nullable public CharSequence getCustomDisableReasonMessage();
- method public int getDisableReason();
+ method @Nullable public CharSequence getCustomSubtextMessage();
method public int getFlags();
method @NonNull public String getRouteId();
- method public int getSessionParticipantCount();
+ method public int getSelectionBehavior();
+ method public int getSubText();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.media.RouteListingPreference.Item> CREATOR;
- field public static final int DISABLE_REASON_AD = 3; // 0x3
- field public static final int DISABLE_REASON_CUSTOM = 5; // 0x5
- field public static final int DISABLE_REASON_DOWNLOADED_CONTENT = 2; // 0x2
- field public static final int DISABLE_REASON_IN_APP_ONLY = 4; // 0x4
- field public static final int DISABLE_REASON_NONE = 0; // 0x0
- field public static final int DISABLE_REASON_SUBSCRIPTION_REQUIRED = 1; // 0x1
field public static final int FLAG_ONGOING_SESSION = 1; // 0x1
- field public static final int FLAG_SUGGESTED_ROUTE = 2; // 0x2
+ field public static final int FLAG_ONGOING_SESSION_MANAGED = 2; // 0x2
+ field public static final int FLAG_SUGGESTED = 4; // 0x4
+ field public static final int SELECTION_BEHAVIOR_GO_TO_APP = 2; // 0x2
+ field public static final int SELECTION_BEHAVIOR_NONE = 0; // 0x0
+ field public static final int SELECTION_BEHAVIOR_TRANSFER = 1; // 0x1
+ field public static final int SUBTEXT_AD_ROUTING_DISALLOWED = 3; // 0x3
+ field public static final int SUBTEXT_CUSTOM = 10000; // 0x2710
+ field public static final int SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED = 2; // 0x2
+ field public static final int SUBTEXT_NONE = 0; // 0x0
+ field public static final int SUBTEXT_SUBSCRIPTION_REQUIRED = 1; // 0x1
}
public static final class RouteListingPreference.Item.Builder {
ctor public RouteListingPreference.Item.Builder(@NonNull String);
method @NonNull public android.media.RouteListingPreference.Item build();
- method @NonNull public android.media.RouteListingPreference.Item.Builder setCustomDisableReasonMessage(@Nullable CharSequence);
- method @NonNull public android.media.RouteListingPreference.Item.Builder setDisableReason(int);
+ method @NonNull public android.media.RouteListingPreference.Item.Builder setCustomSubtextMessage(@Nullable CharSequence);
method @NonNull public android.media.RouteListingPreference.Item.Builder setFlags(int);
- method @NonNull public android.media.RouteListingPreference.Item.Builder setSessionParticipantCount(@IntRange(from=0) int);
+ method @NonNull public android.media.RouteListingPreference.Item.Builder setSelectionBehavior(int);
+ method @NonNull public android.media.RouteListingPreference.Item.Builder setSubText(int);
}
public final class RoutingSessionInfo implements android.os.Parcelable {
@@ -26276,8 +26348,23 @@
package android.media.tv {
+ public final class AdBuffer implements android.os.Parcelable {
+ ctor public AdBuffer(int, @NonNull String, @NonNull android.os.SharedMemory, int, int, long, int);
+ method public int describeContents();
+ method public int getFlags();
+ method public int getId();
+ method public int getLength();
+ method @NonNull public String getMimeType();
+ method public int getOffset();
+ method public long getPresentationTimeUs();
+ method @NonNull public android.os.SharedMemory getSharedMemory();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.media.tv.AdBuffer> CREATOR;
+ }
+
public final class AdRequest implements android.os.Parcelable {
ctor public AdRequest(int, int, @Nullable android.os.ParcelFileDescriptor, long, long, long, @Nullable String, @NonNull android.os.Bundle);
+ ctor public AdRequest(int, int, @Nullable android.net.Uri, long, long, long, @NonNull android.os.Bundle);
method public int describeContents();
method public long getEchoIntervalMillis();
method @Nullable public android.os.ParcelFileDescriptor getFileDescriptor();
@@ -26287,6 +26374,7 @@
method public int getRequestType();
method public long getStartTimeMillis();
method public long getStopTimeMillis();
+ method @Nullable public android.net.Uri getUri();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.media.tv.AdRequest> CREATOR;
field public static final int REQUEST_TYPE_START = 1; // 0x1
@@ -26301,6 +26389,7 @@
method public int getResponseType();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.media.tv.AdResponse> CREATOR;
+ field public static final int RESPONSE_TYPE_BUFFERING = 5; // 0x5
field public static final int RESPONSE_TYPE_ERROR = 4; // 0x4
field public static final int RESPONSE_TYPE_FINISHED = 2; // 0x2
field public static final int RESPONSE_TYPE_PLAYING = 1; // 0x1
@@ -27054,6 +27143,7 @@
public abstract static class TvInputService.Session implements android.view.KeyEvent.Callback {
ctor public TvInputService.Session(android.content.Context);
method public void layoutSurface(int, int, int, int);
+ method public void notifyAdBufferConsumed(@NonNull android.media.tv.AdBuffer);
method public void notifyAdResponse(@NonNull android.media.tv.AdResponse);
method public void notifyAitInfoUpdated(@NonNull android.media.tv.AitInfo);
method public void notifyAudioPresentationChanged(@NonNull java.util.List<android.media.AudioPresentation>);
@@ -27069,6 +27159,7 @@
method public void notifyTuned(@NonNull android.net.Uri);
method public void notifyVideoAvailable();
method public void notifyVideoUnavailable(int);
+ method public void onAdBuffer(@NonNull android.media.tv.AdBuffer);
method public void onAppPrivateCommand(@NonNull String, android.os.Bundle);
method public android.view.View onCreateOverlayView();
method public boolean onGenericMotionEvent(android.view.MotionEvent);
@@ -27355,9 +27446,11 @@
ctor public TvInteractiveAppService.Session(@NonNull android.content.Context);
method public boolean isMediaViewEnabled();
method @CallSuper public void layoutSurface(int, int, int, int);
+ method @CallSuper public void notifyAdBuffer(@NonNull android.media.tv.AdBuffer);
method @CallSuper public final void notifyBiInteractiveAppCreated(@NonNull android.net.Uri, @Nullable String);
method @CallSuper public void notifySessionStateChanged(int, int);
method @CallSuper public final void notifyTeletextAppStateChanged(int);
+ method public void onAdBufferConsumed(@NonNull android.media.tv.AdBuffer);
method public void onAdResponse(@NonNull android.media.tv.AdResponse);
method public void onBroadcastInfoResponse(@NonNull android.media.tv.BroadcastInfoResponse);
method public void onContentAllowed();
@@ -27393,6 +27486,8 @@
method public boolean onTrackballEvent(@NonNull android.view.MotionEvent);
method public void onTracksChanged(@NonNull java.util.List<android.media.tv.TvTrackInfo>);
method public void onTuned(@NonNull android.net.Uri);
+ method public void onTvRecordingInfo(@Nullable android.media.tv.TvRecordingInfo);
+ method public void onTvRecordingInfoList(@NonNull java.util.List<android.media.tv.TvRecordingInfo>);
method public void onVideoAvailable();
method public void onVideoUnavailable(int);
method @CallSuper public void removeBroadcastInfo(int);
@@ -27455,6 +27550,8 @@
method public void sendSigningResult(@NonNull String, @NonNull byte[]);
method public void sendStreamVolume(float);
method public void sendTrackInfoList(@Nullable java.util.List<android.media.tv.TvTrackInfo>);
+ method public void sendTvRecordingInfo(@Nullable android.media.tv.TvRecordingInfo);
+ method public void sendTvRecordingInfoList(@NonNull java.util.List<android.media.tv.TvRecordingInfo>);
method public void setCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.interactive.TvInteractiveAppView.TvInteractiveAppCallback);
method public void setOnUnhandledInputEventListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.interactive.TvInteractiveAppView.OnUnhandledInputEventListener);
method public void setTeletextAppEnabled(boolean);
@@ -32094,9 +32191,11 @@
method public void onEarlyReportFinished();
method public void onError(int);
method public void onFinished();
+ method public void onFinished(@NonNull String);
method public void onProgress(@FloatRange(from=0.0f, to=100.0f) float);
field public static final int BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS = 5; // 0x5
field public static final int BUGREPORT_ERROR_INVALID_INPUT = 1; // 0x1
+ field public static final int BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE = 6; // 0x6
field public static final int BUGREPORT_ERROR_RUNTIME = 2; // 0x2
field public static final int BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT = 4; // 0x4
field public static final int BUGREPORT_ERROR_USER_DENIED_CONSENT = 3; // 0x3
@@ -39272,6 +39371,19 @@
}
+package android.service.assist.classification {
+
+ public final class FieldClassification implements android.os.Parcelable {
+ ctor public FieldClassification(@NonNull android.view.autofill.AutofillId, @NonNull java.util.Set<java.lang.String>);
+ method public int describeContents();
+ method @NonNull public android.view.autofill.AutofillId getAutofillId();
+ method @NonNull public java.util.Set<java.lang.String> getHints();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.service.assist.classification.FieldClassification> CREATOR;
+ }
+
+}
+
package android.service.autofill {
public abstract class AutofillService extends android.app.Service {
@@ -40238,10 +40350,10 @@
}
public final class GetCredentialRequest implements android.os.Parcelable {
- ctor public GetCredentialRequest(@NonNull android.service.credentials.CallingAppInfo, @NonNull android.credentials.GetCredentialOption);
+ ctor public GetCredentialRequest(@NonNull android.service.credentials.CallingAppInfo, @NonNull android.credentials.CredentialOption);
method public int describeContents();
method @NonNull public android.service.credentials.CallingAppInfo getCallingAppInfo();
- method @NonNull public android.credentials.GetCredentialOption getGetCredentialOption();
+ method @NonNull public android.credentials.CredentialOption getCredentialOption();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.service.credentials.GetCredentialRequest> CREATOR;
}
@@ -41603,7 +41715,6 @@
public final class CallControl {
method public void disconnect(@NonNull android.telecom.DisconnectCause, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
method @NonNull public android.os.ParcelUuid getCallId();
- method public void rejectCall(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
method public void requestCallEndpointChange(@NonNull android.telecom.CallEndpoint, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
method public void setActive(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
method public void setInactive(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
@@ -41652,7 +41763,6 @@
}
public final class CallException extends java.lang.RuntimeException implements android.os.Parcelable {
- ctor public CallException(@Nullable String);
ctor public CallException(@Nullable String, int);
method public int describeContents();
method public int getCode();
@@ -42402,7 +42512,7 @@
method @NonNull public String getDisplayName();
method @NonNull public android.os.Bundle getExtras();
method public int getState();
- method public void setStreamingState(int);
+ method public void requestStreamingState(int);
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.telecom.StreamingCall> CREATOR;
field public static final int STATE_DISCONNECTED = 3; // 0x3
@@ -45303,6 +45413,7 @@
field public static final String EXTRA_HIDE_PUBLIC_SETTINGS = "android.telephony.extra.HIDE_PUBLIC_SETTINGS";
field @Deprecated public static final String EXTRA_INCOMING_NUMBER = "incoming_number";
field public static final String EXTRA_IS_REFRESH = "android.telephony.extra.IS_REFRESH";
+ field public static final String EXTRA_LAST_KNOWN_NETWORK_COUNTRY = "android.telephony.extra.LAST_KNOWN_NETWORK_COUNTRY";
field public static final String EXTRA_LAUNCH_VOICEMAIL_SETTINGS_INTENT = "android.telephony.extra.LAUNCH_VOICEMAIL_SETTINGS_INTENT";
field public static final String EXTRA_NETWORK_COUNTRY = "android.telephony.extra.NETWORK_COUNTRY";
field public static final String EXTRA_NOTIFICATION_COUNT = "android.telephony.extra.NOTIFICATION_COUNT";
@@ -51263,7 +51374,7 @@
method public void onTransactionCommitted();
}
- public static class SurfaceControl.TrustedPresentationThresholds {
+ public static final class SurfaceControl.TrustedPresentationThresholds {
ctor public SurfaceControl.TrustedPresentationThresholds(@FloatRange(from=0.0f, fromInclusive=false, to=1.0f) float, @FloatRange(from=0.0f, fromInclusive=false, to=1.0f) float, @IntRange(from=1) int);
}
@@ -51335,8 +51446,12 @@
method public android.view.SurfaceControl getSurfaceControl();
method public void setChildSurfacePackage(@NonNull android.view.SurfaceControlViewHost.SurfacePackage);
method public void setSecure(boolean);
+ method public void setSurfaceLifecycle(int);
method public void setZOrderMediaOverlay(boolean);
method public void setZOrderOnTop(boolean);
+ field public static final int SURFACE_LIFECYCLE_DEFAULT = 0; // 0x0
+ field public static final int SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT = 2; // 0x2
+ field public static final int SURFACE_LIFECYCLE_FOLLOWS_VISIBILITY = 1; // 0x1
}
public class TextureView extends android.view.View {
@@ -52423,7 +52538,9 @@
method public int getScaledHoverSlop();
method public int getScaledMaximumDrawingCacheSize();
method public int getScaledMaximumFlingVelocity();
+ method public int getScaledMaximumFlingVelocity(int, int, int);
method public int getScaledMinimumFlingVelocity();
+ method public int getScaledMinimumFlingVelocity(int, int, int);
method public int getScaledMinimumScalingSpan();
method public int getScaledOverflingDistance();
method public int getScaledOverscrollDistance();
@@ -60271,7 +60388,7 @@
method @UiThread public void remove();
}
- public class SurfaceSyncGroup {
+ public final class SurfaceSyncGroup {
ctor public SurfaceSyncGroup(@NonNull String);
method @UiThread public boolean add(@Nullable android.view.AttachedSurfaceControl, @Nullable Runnable);
method public boolean add(@NonNull android.view.SurfaceControlViewHost.SurfacePackage, @Nullable Runnable);
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index b25e904..4a0b2eb 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -55,6 +55,7 @@
field public static final String BIND_DOMAIN_VERIFICATION_AGENT = "android.permission.BIND_DOMAIN_VERIFICATION_AGENT";
field public static final String BIND_EUICC_SERVICE = "android.permission.BIND_EUICC_SERVICE";
field public static final String BIND_EXTERNAL_STORAGE_SERVICE = "android.permission.BIND_EXTERNAL_STORAGE_SERVICE";
+ field public static final String BIND_FIELD_CLASSIFICATION_SERVICE = "android.permission.BIND_FIELD_CLASSIFICATION_SERVICE";
field public static final String BIND_GBA_SERVICE = "android.permission.BIND_GBA_SERVICE";
field public static final String BIND_HOTWORD_DETECTION_SERVICE = "android.permission.BIND_HOTWORD_DETECTION_SERVICE";
field public static final String BIND_IMS_SERVICE = "android.permission.BIND_IMS_SERVICE";
@@ -817,7 +818,6 @@
method public int getPendingIntentBackgroundActivityStartMode();
method public boolean isDeferUntilActive();
method @Deprecated public boolean isPendingIntentBackgroundActivityLaunchAllowed();
- method public static android.app.BroadcastOptions makeBasic();
method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS) public void recordResponseEventWhileInBackground(@IntRange(from=0) long);
method @RequiresPermission(android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND) public void setBackgroundActivityStartsAllowed(boolean);
method @NonNull public android.app.BroadcastOptions setDeferUntilActive(boolean);
@@ -832,7 +832,6 @@
method public void setRequireNoneOfPermissions(@Nullable String[]);
method @RequiresPermission(anyOf={android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND, android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND}) public void setTemporaryAppAllowlist(long, int, int, @Nullable String);
method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND, android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND}) public void setTemporaryAppWhitelistDuration(long);
- method public android.os.Bundle toBundle();
field public static final int DELIVERY_GROUP_POLICY_ALL = 0; // 0x0
field public static final int DELIVERY_GROUP_POLICY_MOST_RECENT = 1; // 0x1
}
@@ -2121,6 +2120,13 @@
method protected void finalize();
method public void notifyEvent(@NonNull android.app.search.Query, @NonNull android.app.search.SearchTargetEvent);
method @Nullable public void query(@NonNull android.app.search.Query, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.app.search.SearchTarget>>);
+ method public void registerEmptyQueryResultUpdateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.app.search.SearchSession.Callback);
+ method public void requestEmptyQueryResultUpdate();
+ method public void unregisterEmptyQueryResultUpdateCallback(@NonNull android.app.search.SearchSession.Callback);
+ }
+
+ public static interface SearchSession.Callback {
+ method public void onTargetsAvailable(@NonNull java.util.List<android.app.search.SearchTarget>);
}
public final class SearchSessionId implements android.os.Parcelable {
@@ -3232,10 +3238,8 @@
method public abstract boolean isCredentialProtectedStorage();
method @Nullable @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public android.content.Intent registerReceiverForAllUsers(@Nullable android.content.BroadcastReceiver, @NonNull android.content.IntentFilter, @Nullable String, @Nullable android.os.Handler);
method @Nullable @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public android.content.Intent registerReceiverForAllUsers(@Nullable android.content.BroadcastReceiver, @NonNull android.content.IntentFilter, @Nullable String, @Nullable android.os.Handler, int);
- method public abstract void sendBroadcast(android.content.Intent, @Nullable String, @Nullable android.os.Bundle);
method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public abstract void sendBroadcastAsUser(@RequiresPermission android.content.Intent, android.os.UserHandle, @Nullable String, @Nullable android.os.Bundle);
method public void sendBroadcastMultiplePermissions(@NonNull android.content.Intent, @NonNull String[], @Nullable android.app.BroadcastOptions);
- method public abstract void sendOrderedBroadcast(@NonNull android.content.Intent, @Nullable String, @Nullable android.os.Bundle, @Nullable android.content.BroadcastReceiver, @Nullable android.os.Handler, int, @Nullable String, @Nullable android.os.Bundle);
method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public void startActivityAsUser(@NonNull @RequiresPermission android.content.Intent, @NonNull android.os.UserHandle);
method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public void startActivityAsUser(@NonNull @RequiresPermission android.content.Intent, @Nullable android.os.Bundle, @NonNull android.os.UserHandle);
field public static final String AMBIENT_CONTEXT_SERVICE = "ambient_context";
@@ -3298,9 +3302,7 @@
method public android.content.Context createCredentialProtectedStorageContext();
method @Nullable public java.io.File getPreloadsFileCache();
method public boolean isCredentialProtectedStorage();
- method public void sendBroadcast(android.content.Intent, @Nullable String, @Nullable android.os.Bundle);
method public void sendBroadcastAsUser(android.content.Intent, android.os.UserHandle, @Nullable String, @Nullable android.os.Bundle);
- method public void sendOrderedBroadcast(android.content.Intent, @Nullable String, @Nullable android.os.Bundle, @Nullable android.content.BroadcastReceiver, @Nullable android.os.Handler, int, @Nullable String, @Nullable android.os.Bundle);
}
public class Intent implements java.lang.Cloneable android.os.Parcelable {
@@ -3592,7 +3594,7 @@
}
public class PackageInstaller {
- method @NonNull public android.content.pm.PackageInstaller.InstallInfo getInstallInfo(@NonNull java.io.File, int) throws android.content.pm.PackageInstaller.PackageParsingException;
+ method @NonNull public android.content.pm.PackageInstaller.InstallInfo readInstallInfo(@NonNull java.io.File, int) throws android.content.pm.PackageInstaller.PackageParsingException;
method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public void setPermissionsResult(int, boolean);
field public static final String ACTION_CONFIRM_INSTALL = "android.content.pm.action.CONFIRM_INSTALL";
field public static final String ACTION_CONFIRM_PRE_APPROVAL = "android.content.pm.action.CONFIRM_PRE_APPROVAL";
@@ -3632,7 +3634,6 @@
method public boolean getAllocateAggressive();
method @Deprecated public boolean getAllowDowngrade();
method public int getAutoRevokePermissionsMode();
- method public boolean getDontKillApp();
method public boolean getEnableRollback();
method @Nullable public String[] getGrantedRuntimePermissions();
method public boolean getInstallAsFullApp(boolean);
@@ -3648,7 +3649,6 @@
method @RequiresPermission(android.Manifest.permission.ALLOCATE_AGGRESSIVE) public void setAllocateAggressive(boolean);
method @Deprecated public void setAllowDowngrade(boolean);
method @RequiresPermission(allOf={android.Manifest.permission.INSTALL_PACKAGES, "com.android.permission.USE_INSTALLER_V2"}) public void setDataLoaderParams(@NonNull android.content.pm.DataLoaderParams);
- method public void setDontKillApp(boolean);
method public void setEnableRollback(boolean);
method public void setEnableRollback(boolean, int);
method @RequiresPermission(android.Manifest.permission.INSTALL_GRANT_RUNTIME_PERMISSIONS) public void setGrantedRuntimePermissions(String[]);
@@ -7961,6 +7961,7 @@
public class MediaEvent extends android.media.tv.tuner.filter.FilterEvent {
method public long getAudioHandle();
+ method @NonNull public java.util.List<android.media.AudioPresentation> getAudioPresentations();
method public long getAvDataId();
method public long getDataLength();
method public long getDts();
@@ -8911,7 +8912,6 @@
}
public class IptvFrontendSettings extends android.media.tv.tuner.frontend.FrontendSettings {
- ctor public IptvFrontendSettings(@NonNull byte[], @NonNull byte[], int, int, @NonNull android.media.tv.tuner.frontend.IptvFrontendSettingsFec, int, int, long, @NonNull String);
method @NonNull public static android.media.tv.tuner.frontend.IptvFrontendSettings.Builder builder();
method @IntRange(from=0) public long getBitrate();
method @NonNull public String getContentUrl();
@@ -8946,7 +8946,6 @@
}
public class IptvFrontendSettingsFec {
- ctor public IptvFrontendSettingsFec(int, int, int);
method @NonNull public static android.media.tv.tuner.frontend.IptvFrontendSettingsFec.Builder builder();
method @IntRange(from=0) public int getFecColNum();
method @IntRange(from=0) public int getFecRowNum();
@@ -9784,6 +9783,133 @@
}
+package android.net.wifi.sharedconnectivity.app {
+
+ public final class DeviceInfo implements android.os.Parcelable {
+ method public int describeContents();
+ method @IntRange(from=0, to=100) @NonNull public int getBatteryPercentage();
+ method @IntRange(from=0, to=3) @NonNull public int getConnectionStrength();
+ method @NonNull public String getDeviceName();
+ method public int getDeviceType();
+ method @NonNull public String getModelName();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.wifi.sharedconnectivity.app.DeviceInfo> CREATOR;
+ field public static final int DEVICE_TYPE_LAPTOP = 3; // 0x3
+ field public static final int DEVICE_TYPE_PHONE = 1; // 0x1
+ field public static final int DEVICE_TYPE_TABLET = 2; // 0x2
+ field public static final int DEVICE_TYPE_UNKNOWN = 0; // 0x0
+ }
+
+ public static final class DeviceInfo.Builder {
+ ctor public DeviceInfo.Builder();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.DeviceInfo build();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.DeviceInfo.Builder setBatteryPercentage(@IntRange(from=0, to=100) int);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.DeviceInfo.Builder setConnectionStrength(@IntRange(from=0, to=3) int);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.DeviceInfo.Builder setDeviceName(@NonNull String);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.DeviceInfo.Builder setDeviceType(int);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.DeviceInfo.Builder setModelName(@NonNull String);
+ }
+
+ public final class KnownNetwork implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.DeviceInfo getDeviceInfo();
+ method @NonNull public int getNetworkSource();
+ method @NonNull public int[] getSecurityTypes();
+ method @NonNull public String getSsid();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.wifi.sharedconnectivity.app.KnownNetwork> CREATOR;
+ field public static final int NETWORK_SOURCE_CLOUD_SELF = 1; // 0x1
+ field public static final int NETWORK_SOURCE_NEARBY_SELF = 0; // 0x0
+ }
+
+ public static final class KnownNetwork.Builder {
+ ctor public KnownNetwork.Builder();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.KnownNetwork build();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.KnownNetwork.Builder setDeviceInfo(@NonNull android.net.wifi.sharedconnectivity.app.DeviceInfo);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.KnownNetwork.Builder setNetworkSource(int);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.KnownNetwork.Builder setSecurityTypes(@NonNull int[]);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.KnownNetwork.Builder setSsid(@NonNull String);
+ }
+
+ public interface SharedConnectivityClientCallback {
+ method public void onKnownNetworksUpdated(@NonNull java.util.List<android.net.wifi.sharedconnectivity.app.KnownNetwork>);
+ method public void onSharedConnectivitySettingsChanged(@NonNull android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState);
+ method public void onTetherNetworksUpdated(@NonNull java.util.List<android.net.wifi.sharedconnectivity.app.TetherNetwork>);
+ }
+
+ public class SharedConnectivityManager {
+ ctor public SharedConnectivityManager(@NonNull android.content.Context);
+ method public boolean connectKnownNetwork(@NonNull android.net.wifi.sharedconnectivity.app.KnownNetwork);
+ method public boolean connectTetherNetwork(@NonNull android.net.wifi.sharedconnectivity.app.TetherNetwork);
+ method public boolean disconnectTetherNetwork();
+ method public boolean forgetKnownNetwork(@NonNull android.net.wifi.sharedconnectivity.app.KnownNetwork);
+ method public boolean registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.wifi.sharedconnectivity.app.SharedConnectivityClientCallback);
+ method public boolean unregisterCallback(@NonNull android.net.wifi.sharedconnectivity.app.SharedConnectivityClientCallback);
+ }
+
+ public final class SharedConnectivitySettingsState implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public android.os.Bundle getExtras();
+ method @NonNull public boolean isInstantTetherEnabled();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState> CREATOR;
+ }
+
+ public static final class SharedConnectivitySettingsState.Builder {
+ ctor public SharedConnectivitySettingsState.Builder();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState build();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState.Builder setExtras(@NonNull android.os.Bundle);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState.Builder setInstantTetherEnabled(boolean);
+ }
+
+ public final class TetherNetwork implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public long getDeviceId();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.DeviceInfo getDeviceInfo();
+ method @Nullable public String getHotspotBssid();
+ method @Nullable public int[] getHotspotSecurityTypes();
+ method @Nullable public String getHotspotSsid();
+ method @NonNull public String getNetworkName();
+ method @NonNull public int getNetworkType();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.wifi.sharedconnectivity.app.TetherNetwork> CREATOR;
+ field public static final int NETWORK_TYPE_CELLULAR = 1; // 0x1
+ field public static final int NETWORK_TYPE_ETHERNET = 3; // 0x3
+ field public static final int NETWORK_TYPE_UNKNOWN = 0; // 0x0
+ field public static final int NETWORK_TYPE_WIFI = 2; // 0x2
+ }
+
+ public static final class TetherNetwork.Builder {
+ ctor public TetherNetwork.Builder();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.TetherNetwork build();
+ method @NonNull public android.net.wifi.sharedconnectivity.app.TetherNetwork.Builder setDeviceId(long);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.TetherNetwork.Builder setDeviceInfo(@NonNull android.net.wifi.sharedconnectivity.app.DeviceInfo);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.TetherNetwork.Builder setHotspotBssid(@NonNull String);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.TetherNetwork.Builder setHotspotSecurityTypes(@NonNull int[]);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.TetherNetwork.Builder setHotspotSsid(@NonNull String);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.TetherNetwork.Builder setNetworkName(@NonNull String);
+ method @NonNull public android.net.wifi.sharedconnectivity.app.TetherNetwork.Builder setNetworkType(int);
+ }
+
+}
+
+package android.net.wifi.sharedconnectivity.service {
+
+ public abstract class SharedConnectivityService extends android.app.Service {
+ ctor public SharedConnectivityService();
+ ctor public SharedConnectivityService(@NonNull android.os.Handler);
+ method @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent);
+ method public abstract void onConnectKnownNetwork(@NonNull android.net.wifi.sharedconnectivity.app.KnownNetwork);
+ method public abstract void onConnectTetherNetwork(@NonNull android.net.wifi.sharedconnectivity.app.TetherNetwork);
+ method public abstract void onDisconnectTetherNetwork();
+ method public abstract void onForgetKnownNetwork(@NonNull android.net.wifi.sharedconnectivity.app.KnownNetwork);
+ method public final void setKnownNetworks(@NonNull java.util.List<android.net.wifi.sharedconnectivity.app.KnownNetwork>);
+ method public final void setSettingsState(@NonNull android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState);
+ method public final void setTetherNetworks(@NonNull java.util.List<android.net.wifi.sharedconnectivity.app.TetherNetwork>);
+ }
+
+}
+
package android.nfc {
public final class NfcAdapter {
@@ -9941,6 +10067,7 @@
public final class BugreportManager {
method @RequiresPermission(android.Manifest.permission.DUMP) @WorkerThread public void preDumpUiData();
method @RequiresPermission(android.Manifest.permission.DUMP) public void requestBugreport(@NonNull android.os.BugreportParams, @Nullable CharSequence, @Nullable CharSequence);
+ method @RequiresPermission(android.Manifest.permission.DUMP) @WorkerThread public void retrieveBugreport(@NonNull String, @NonNull android.os.ParcelFileDescriptor, @NonNull java.util.concurrent.Executor, @NonNull android.os.BugreportManager.BugreportCallback);
method @RequiresPermission(android.Manifest.permission.DUMP) @WorkerThread public void startBugreport(@NonNull android.os.ParcelFileDescriptor, @Nullable android.os.ParcelFileDescriptor, @NonNull android.os.BugreportParams, @NonNull java.util.concurrent.Executor, @NonNull android.os.BugreportManager.BugreportCallback);
}
@@ -9949,6 +10076,7 @@
ctor public BugreportParams(int, int);
method public int getFlags();
method public int getMode();
+ field public static final int BUGREPORT_FLAG_DEFER_CONSENT = 2; // 0x2
field public static final int BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA = 1; // 0x1
field public static final int BUGREPORT_MODE_FULL = 0; // 0x0
field public static final int BUGREPORT_MODE_INTERACTIVE = 1; // 0x1
@@ -11548,6 +11676,39 @@
}
+package android.service.assist.classification {
+
+ public final class FieldClassification implements android.os.Parcelable {
+ ctor public FieldClassification(@NonNull android.view.autofill.AutofillId, @NonNull java.util.Set<java.lang.String>, @NonNull java.util.Set<java.lang.String>);
+ method @NonNull public java.util.Set<java.lang.String> getGroupHints();
+ }
+
+ public final class FieldClassificationRequest implements android.os.Parcelable {
+ ctor public FieldClassificationRequest(@NonNull android.app.assist.AssistStructure);
+ method public int describeContents();
+ method @NonNull public android.app.assist.AssistStructure getAssistStructure();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.service.assist.classification.FieldClassificationRequest> CREATOR;
+ }
+
+ public final class FieldClassificationResponse implements android.os.Parcelable {
+ ctor public FieldClassificationResponse(@NonNull java.util.Set<android.service.assist.classification.FieldClassification>);
+ method public int describeContents();
+ method @NonNull public java.util.Set<android.service.assist.classification.FieldClassification> getClassifications();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.service.assist.classification.FieldClassificationResponse> CREATOR;
+ }
+
+ public abstract class FieldClassificationService extends android.app.Service {
+ ctor public FieldClassificationService();
+ method public abstract void onClassificationRequest(@NonNull android.service.assist.classification.FieldClassificationRequest, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.service.assist.classification.FieldClassificationResponse,java.lang.Exception>);
+ method public void onConnected();
+ method public void onDisconnected();
+ field public static final String SERVICE_INTERFACE = "android.service.assist.classification.FieldClassificationService";
+ }
+
+}
+
package android.service.attention {
public abstract class AttentionService extends android.app.Service {
@@ -12275,7 +12436,11 @@
method @MainThread public abstract void onDestroy(@NonNull android.app.search.SearchSessionId);
method @MainThread public abstract void onNotifyEvent(@NonNull android.app.search.SearchSessionId, @NonNull android.app.search.Query, @NonNull android.app.search.SearchTargetEvent);
method @MainThread public abstract void onQuery(@NonNull android.app.search.SearchSessionId, @NonNull android.app.search.Query, @NonNull java.util.function.Consumer<java.util.List<android.app.search.SearchTarget>>);
+ method @MainThread public void onRequestEmptyQueryResultUpdate(@NonNull android.app.search.SearchSessionId);
method public void onSearchSessionCreated(@NonNull android.app.search.SearchContext, @NonNull android.app.search.SearchSessionId);
+ method @MainThread public void onStartUpdateEmptyQueryResult();
+ method @MainThread public void onStopUpdateEmptyQueryResult();
+ method public final void updateEmptyQueryResult(@NonNull android.app.search.SearchSessionId, @NonNull java.util.List<android.app.search.SearchTarget>);
}
}
@@ -14347,7 +14512,6 @@
field public static final String ACTION_SIM_SLOT_STATUS_CHANGED = "android.telephony.action.SIM_SLOT_STATUS_CHANGED";
field public static final int ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G = 3; // 0x3
field public static final int ALLOWED_NETWORK_TYPES_REASON_POWER = 1; // 0x1
- field public static final int ALLOWED_NETWORK_TYPES_REASON_USER_RESTRICTIONS = 4; // 0x4
field public static final int CALL_WAITING_STATUS_DISABLED = 2; // 0x2
field public static final int CALL_WAITING_STATUS_ENABLED = 1; // 0x1
field public static final int CALL_WAITING_STATUS_FDN_CHECK_FAILURE = 5; // 0x5
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 5f2f623..b3e3f35 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -144,8 +144,6 @@
method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public boolean updateMccMncConfiguration(@NonNull String, @NonNull String);
method @RequiresPermission(android.Manifest.permission.DUMP) public void waitForBroadcastIdle();
field public static final long LOCK_DOWN_CLOSE_SYSTEM_DIALOGS = 174664365L; // 0xa692aadL
- field public static final int PROCESS_CAPABILITY_ALL = 15; // 0xf
- field public static final int PROCESS_CAPABILITY_ALL_EXPLICIT = 1; // 0x1
field public static final int PROCESS_CAPABILITY_ALL_IMPLICIT = 6; // 0x6
field public static final int PROCESS_CAPABILITY_NETWORK = 8; // 0x8
field public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4; // 0x4
@@ -165,7 +163,7 @@
method @Nullable public String getIconResourcePackage();
}
- public class ActivityOptions {
+ public class ActivityOptions extends android.app.ComponentOptions {
method public boolean isEligibleForLegacyPermissionPrompt();
method @NonNull public static android.app.ActivityOptions makeCustomAnimation(@NonNull android.content.Context, int, int, int, @Nullable android.os.Handler, @Nullable android.app.ActivityOptions.OnAnimationStartedListener, @Nullable android.app.ActivityOptions.OnAnimationFinishedListener);
method @NonNull @RequiresPermission(android.Manifest.permission.START_TASKS_FROM_RECENTS) public static android.app.ActivityOptions makeCustomTaskAnimation(@NonNull android.content.Context, int, int, @Nullable android.os.Handler, @Nullable android.app.ActivityOptions.OnAnimationStartedListener, @Nullable android.app.ActivityOptions.OnAnimationFinishedListener);
@@ -269,7 +267,7 @@
method public default void onOpActiveChanged(@NonNull String, int, @NonNull String, @Nullable String, boolean, int, int);
}
- public class BroadcastOptions {
+ public class BroadcastOptions extends android.app.ComponentOptions {
ctor public BroadcastOptions(@NonNull android.os.Bundle);
method @Deprecated public int getMaxManifestReceiverApiLevel();
method public long getTemporaryAppAllowlistDuration();
@@ -282,6 +280,12 @@
field public static final long CHANGE_ALWAYS_ENABLED = 209888056L; // 0xc82a338L
}
+ public class ComponentOptions {
+ field public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOWED = 1; // 0x1
+ field public static final int MODE_BACKGROUND_ACTIVITY_START_DENIED = 2; // 0x2
+ field public static final int MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED = 0; // 0x0
+ }
+
public class DownloadManager {
field public static final String COLUMN_MEDIASTORE_URI = "mediastore_uri";
}
@@ -464,8 +468,11 @@
public class WallpaperManager {
method @Nullable public android.graphics.Bitmap getBitmap();
+ method @Nullable public android.graphics.Bitmap getBitmapAsUser(int, boolean, int);
+ method public boolean isLockscreenLiveWallpaperEnabled();
method @Nullable public android.graphics.Rect peekBitmapDimensions();
method @Nullable public android.graphics.Rect peekBitmapDimensions(int);
+ method public void setWallpaperZoomOut(@NonNull android.os.IBinder, float);
method public boolean shouldEnableWideColorGamut();
method @RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) public boolean wallpaperSupportsWcg(int);
}
@@ -1856,6 +1863,14 @@
}
+package android.net.wifi.sharedconnectivity.app {
+
+ public class SharedConnectivityManager {
+ method public void setService(@Nullable android.os.IInterface);
+ }
+
+}
+
package android.os {
public final class BatteryStatsManager {
@@ -1869,7 +1884,10 @@
public class Build {
method public static boolean is64BitAbi(String);
method public static boolean isDebuggable();
+ field @Nullable public static final String BRAND_FOR_ATTESTATION;
field public static final boolean IS_EMULATOR;
+ field @Nullable public static final String MODEL_FOR_ATTESTATION;
+ field @Nullable public static final String PRODUCT_FOR_ATTESTATION;
}
public static class Build.VERSION {
@@ -2067,6 +2085,7 @@
method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public java.util.List<android.content.pm.UserInfo> getUsers(boolean, boolean, boolean);
method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean hasBaseUserRestriction(@NonNull String, @NonNull android.os.UserHandle);
method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean isUserTypeEnabled(@NonNull String);
+ method public boolean isVisibleBackgroundUsersOnDefaultDisplaySupported();
method public boolean isVisibleBackgroundUsersSupported();
method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo preCreateUser(@NonNull String) throws android.os.UserManager.UserOperationException;
}
@@ -2395,6 +2414,8 @@
field public static final String DISABLE_WINDOW_BLURS = "disable_window_blurs";
field public static final String DYNAMIC_POWER_SAVINGS_DISABLE_THRESHOLD = "dynamic_power_savings_disable_threshold";
field public static final String DYNAMIC_POWER_SAVINGS_ENABLED = "dynamic_power_savings_enabled";
+ field public static final String HDR_CONVERSION_MODE = "hdr_conversion_mode";
+ field public static final String HDR_FORCE_CONVERSION_TYPE = "hdr_force_conversion_type";
field public static final String HIDDEN_API_BLACKLIST_EXEMPTIONS = "hidden_api_blacklist_exemptions";
field public static final String HIDDEN_API_POLICY = "hidden_api_policy";
field public static final String HIDE_ERROR_DIALOGS = "hide_error_dialogs";
@@ -3132,6 +3153,9 @@
public final class SurfaceControl implements android.os.Parcelable {
ctor public SurfaceControl(@NonNull android.view.SurfaceControl, @NonNull String);
+ method @NonNull public android.view.Choreographer getChoreographer();
+ method @NonNull public android.view.Choreographer getChoreographer(@NonNull android.os.Looper);
+ method public boolean hasChoreographer();
method public boolean isSameSurface(@NonNull android.view.SurfaceControl);
}
@@ -3249,6 +3273,7 @@
field public static final String DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_ENABLED = "autofill_credential_manager_enabled";
field public static final String DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_IGNORE_VIEWS = "autofill_credential_manager_ignore_views";
field public static final String DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED = "autofill_dialog_enabled";
+ field public static final String DEVICE_CONFIG_AUTOFILL_PCC_CLASSIFICATION_ENABLED = "pcc_classification_enabled";
field public static final String DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES = "smart_suggestion_supported_modes";
field public static final String DEVICE_CONFIG_NON_AUTOFILLABLE_IME_ACTION_IDS = "non_autofillable_ime_action_ids";
field public static final String DEVICE_CONFIG_PACKAGE_DENYLIST_FOR_UNIMPORTANT_VIEW = "package_deny_list_for_unimportant_view";
diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt
index e849cdbc..4a97280 100644
--- a/core/api/test-lint-baseline.txt
+++ b/core/api/test-lint-baseline.txt
@@ -855,6 +855,10 @@
New setting keys are not allowed (Field: DYNAMIC_POWER_SAVINGS_DISABLE_THRESHOLD); use getters/setters in relevant manager class
NoSettingsProvider: android.provider.Settings.Global#DYNAMIC_POWER_SAVINGS_ENABLED:
New setting keys are not allowed (Field: DYNAMIC_POWER_SAVINGS_ENABLED); use getters/setters in relevant manager class
+NoSettingsProvider: android.provider.Settings.Global#HDR_CONVERSION_MODE:
+ New setting keys are not allowed (Field: HDR_CONVERSION_MODE); use getters/setters in relevant manager class
+NoSettingsProvider: android.provider.Settings.Global#HDR_FORCE_CONVERSION_TYPE:
+ New setting keys are not allowed (Field: HDR_FORCE_CONVERSION_TYPE); use getters/setters in relevant manager class
NoSettingsProvider: android.provider.Settings.Global#HIDDEN_API_BLACKLIST_EXEMPTIONS:
New setting keys are not allowed (Field: HIDDEN_API_BLACKLIST_EXEMPTIONS); use getters/setters in relevant manager class
NoSettingsProvider: android.provider.Settings.Global#HIDDEN_API_POLICY:
diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
index ae57959..65b0cfb 100644
--- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
+++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
@@ -48,6 +48,7 @@
import android.util.SparseArray;
import android.util.TypedValue;
import android.util.Xml;
+import android.view.InputDevice;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
@@ -612,9 +613,29 @@
private boolean mIsAccessibilityTool = false;
/**
+ * {@link InputDevice} sources which may send {@link android.view.MotionEvent}s.
+ * @see #setMotionEventSources(int)
+ * @hide
+ */
+ @IntDef(flag = true, prefix = { "SOURCE_" }, value = {
+ InputDevice.SOURCE_MOUSE,
+ InputDevice.SOURCE_STYLUS,
+ InputDevice.SOURCE_BLUETOOTH_STYLUS,
+ InputDevice.SOURCE_TRACKBALL,
+ InputDevice.SOURCE_MOUSE_RELATIVE,
+ InputDevice.SOURCE_TOUCHPAD,
+ InputDevice.SOURCE_TOUCH_NAVIGATION,
+ InputDevice.SOURCE_ROTARY_ENCODER,
+ InputDevice.SOURCE_JOYSTICK,
+ InputDevice.SOURCE_SENSOR
+ })
+ public @interface MotionEventSources {}
+
+ /**
* The bit mask of {@link android.view.InputDevice} sources that the accessibility
* service wants to listen to for generic {@link android.view.MotionEvent}s.
*/
+ @MotionEventSources
private int mMotionEventSources = 0;
/**
@@ -966,6 +987,7 @@
* Returns the bit mask of {@link android.view.InputDevice} sources that the accessibility
* service wants to listen to for generic {@link android.view.MotionEvent}s.
*/
+ @MotionEventSources
public int getMotionEventSources() {
return mMotionEventSources;
}
@@ -975,28 +997,29 @@
* service wants to listen to for generic {@link android.view.MotionEvent}s.
*
* <p>
- * Note: including an {@link android.view.InputDevice} source that does not send
+ * Including an {@link android.view.InputDevice} source that does not send
* {@link android.view.MotionEvent}s is effectively a no-op for that source, since you will
* not receive any events from that source.
* </p>
+ *
* <p>
- * Allowed sources include:
- * <li>{@link android.view.InputDevice#SOURCE_MOUSE}</li>
- * <li>{@link android.view.InputDevice#SOURCE_STYLUS}</li>
- * <li>{@link android.view.InputDevice#SOURCE_BLUETOOTH_STYLUS}</li>
- * <li>{@link android.view.InputDevice#SOURCE_TRACKBALL}</li>
- * <li>{@link android.view.InputDevice#SOURCE_MOUSE_RELATIVE}</li>
- * <li>{@link android.view.InputDevice#SOURCE_TOUCHPAD}</li>
- * <li>{@link android.view.InputDevice#SOURCE_TOUCH_NAVIGATION}</li>
- * <li>{@link android.view.InputDevice#SOURCE_ROTARY_ENCODER}</li>
- * <li>{@link android.view.InputDevice#SOURCE_JOYSTICK}</li>
- * <li>{@link android.view.InputDevice#SOURCE_SENSOR}</li>
+ * See {@link android.view.InputDevice} for complete source definitions.
+ * Many input devices send {@link android.view.InputEvent}s from more than one type of source so
+ * you may need to include multiple {@link android.view.MotionEvent} sources here, in addition
+ * to using {@link AccessibilityService#onKeyEvent} to listen to {@link android.view.KeyEvent}s.
+ * </p>
+ *
+ * <p>
+ * <strong>Note:</strong> {@link android.view.InputDevice} sources contain source class bits
+ * that complicate bitwise flag removal operations. To remove a specific source you should
+ * rebuild the entire value using bitwise OR operations on the individual source constants.
* </p>
*
* @param motionEventSources A bit mask of {@link android.view.InputDevice} sources.
* @see AccessibilityService#onMotionEvent
+ * @see MotionEventSources
*/
- public void setMotionEventSources(int motionEventSources) {
+ public void setMotionEventSources(@MotionEventSources int motionEventSources) {
mMotionEventSources = motionEventSources;
}
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index c4d6ad7..183357a 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -630,6 +630,8 @@
PROCESS_CAPABILITY_FOREGROUND_LOCATION,
PROCESS_CAPABILITY_FOREGROUND_CAMERA,
PROCESS_CAPABILITY_FOREGROUND_MICROPHONE,
+ PROCESS_CAPABILITY_NETWORK,
+ PROCESS_CAPABILITY_BFSL,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ProcessCapability {}
@@ -654,20 +656,36 @@
@TestApi
public static final int PROCESS_CAPABILITY_NETWORK = 1 << 3;
- /** @hide all capabilities, the ORing of all flags in {@link ProcessCapability}*/
- @TestApi
+ /**
+ * Flag used to indicate whether an app is allowed to start a foreground service from the
+ * background, decided by the procstates. ("BFSL" == "background foreground service launch")
+ *
+ * - BFSL has a number of exemptions -- e.g. when an app is power-allowlisted, including
+ * temp-allowlist -- but this capability is *not* used to represent such exemptions.
+ * This is set only based on the procstate and the foreground service type.
+ * - Basically, procstates <= BFGS (i.e. BFGS, FGS, BTOP, TOP, ...) are BFSL-allowed,
+ * and that's how things worked on Android S/T.
+ * However, Android U added a "SHORT_SERVICE" FGS type, which gets the FGS procstate
+ * *but* can't start another FGS. So now we use this flag to decide whether FGS/BFGS
+ * procstates are BFSL-allowed. (higher procstates, such as BTOP, will still always be
+ * BFSL-allowed.)
+ * We propagate this flag across via service bindings and provider references.
+ *
+ * @hide
+ */
+ public static final int PROCESS_CAPABILITY_BFSL = 1 << 4;
+
+ /**
+ * @hide all capabilities, the ORing of all flags in {@link ProcessCapability}.
+ *
+ * Don't expose it as TestApi -- we may add new capabilities any time, which could
+ * break CTS tests if they relied on it.
+ */
public static final int PROCESS_CAPABILITY_ALL = PROCESS_CAPABILITY_FOREGROUND_LOCATION
| PROCESS_CAPABILITY_FOREGROUND_CAMERA
| PROCESS_CAPABILITY_FOREGROUND_MICROPHONE
- | PROCESS_CAPABILITY_NETWORK;
- /**
- * All explicit capabilities. These are capabilities that need to be specified from manifest
- * file.
- * @hide
- */
- @TestApi
- public static final int PROCESS_CAPABILITY_ALL_EXPLICIT =
- PROCESS_CAPABILITY_FOREGROUND_LOCATION;
+ | PROCESS_CAPABILITY_NETWORK
+ | PROCESS_CAPABILITY_BFSL;
/**
* All implicit capabilities. There are capabilities that process automatically have.
@@ -686,6 +704,7 @@
pw.print((caps & PROCESS_CAPABILITY_FOREGROUND_CAMERA) != 0 ? 'C' : '-');
pw.print((caps & PROCESS_CAPABILITY_FOREGROUND_MICROPHONE) != 0 ? 'M' : '-');
pw.print((caps & PROCESS_CAPABILITY_NETWORK) != 0 ? 'N' : '-');
+ pw.print((caps & PROCESS_CAPABILITY_BFSL) != 0 ? 'F' : '-');
}
/** @hide */
@@ -694,6 +713,7 @@
sb.append((caps & PROCESS_CAPABILITY_FOREGROUND_CAMERA) != 0 ? 'C' : '-');
sb.append((caps & PROCESS_CAPABILITY_FOREGROUND_MICROPHONE) != 0 ? 'M' : '-');
sb.append((caps & PROCESS_CAPABILITY_NETWORK) != 0 ? 'N' : '-');
+ sb.append((caps & PROCESS_CAPABILITY_BFSL) != 0 ? 'F' : '-');
}
/**
@@ -702,13 +722,10 @@
*/
public static void printCapabilitiesFull(PrintWriter pw, @ProcessCapability int caps) {
printCapabilitiesSummary(pw, caps);
- final int remain = caps & ~(PROCESS_CAPABILITY_FOREGROUND_LOCATION
- | PROCESS_CAPABILITY_FOREGROUND_CAMERA
- | PROCESS_CAPABILITY_FOREGROUND_MICROPHONE
- | PROCESS_CAPABILITY_NETWORK);
+ final int remain = caps & ~PROCESS_CAPABILITY_ALL;
if (remain != 0) {
- pw.print('+');
- pw.print(remain);
+ pw.print("+0x");
+ pw.print(Integer.toHexString(remain));
}
}
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 2a390a7..3b88257 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -88,6 +88,16 @@
*/
public static final String EXTRA_USAGE_TIME_REPORT_PACKAGES = "android.usage_time_packages";
+ /** No explicit value chosen. The system will decide whether to grant privileges. */
+ public static final int MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED =
+ ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
+ /** Allow the {@link PendingIntent} to use the background activity start privileges. */
+ public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOWED =
+ ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
+ /** Deny the {@link PendingIntent} to use the background activity start privileges. */
+ public static final int MODE_BACKGROUND_ACTIVITY_START_DENIED =
+ ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
+
/**
* The package name that created the options.
* @hide
@@ -333,6 +343,13 @@
"android:activity.applyActivityFlagsForBubbles";
/**
+ * Indicates to apply {@link Intent#FLAG_ACTIVITY_MULTIPLE_TASK} to the launching shortcut.
+ * @hide
+ */
+ private static final String KEY_APPLY_MULTIPLE_TASK_FLAG_FOR_SHORTCUT =
+ "android:activity.applyMultipleTaskFlagForShortcut";
+
+ /**
* For Activity transitions, the calling Activity's TransitionListener used to
* notify the called Activity when the shared element and the exit transitions
* complete.
@@ -387,8 +404,8 @@
/** See {@link #setDismissKeyguard()}. */
private static final String KEY_DISMISS_KEYGUARD = "android.activity.dismissKeyguard";
- private static final String KEY_IGNORE_PENDING_INTENT_CREATOR_FOREGROUND_STATE =
- "android.activity.ignorePendingIntentCreatorForegroundState";
+ private static final String KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE =
+ "android.activity.pendingIntentCreatorBackgroundActivityStartMode";
/**
* @see #setLaunchCookie
@@ -466,6 +483,7 @@
private boolean mShareIdentity = false;
private boolean mDisallowEnterPictureInPictureWhileLaunching;
private boolean mApplyActivityFlagsForBubbles;
+ private boolean mApplyMultipleTaskFlagForShortcut;
private boolean mTaskAlwaysOnTop;
private boolean mTaskOverlay;
private boolean mTaskOverlayCanResume;
@@ -489,7 +507,9 @@
private boolean mTransientLaunch;
private PictureInPictureParams mLaunchIntoPipParams;
private boolean mDismissKeyguard;
- private boolean mIgnorePendingIntentCreatorForegroundState;
+ @BackgroundActivityStartMode
+ private int mPendingIntentCreatorBackgroundActivityStartMode =
+ MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
private boolean mDisableStartingWindow;
/**
@@ -1266,6 +1286,8 @@
KEY_DISALLOW_ENTER_PICTURE_IN_PICTURE_WHILE_LAUNCHING, false);
mApplyActivityFlagsForBubbles = opts.getBoolean(
KEY_APPLY_ACTIVITY_FLAGS_FOR_BUBBLES, false);
+ mApplyMultipleTaskFlagForShortcut = opts.getBoolean(
+ KEY_APPLY_MULTIPLE_TASK_FLAG_FOR_SHORTCUT, false);
if (opts.containsKey(KEY_ANIM_SPECS)) {
Parcelable[] specs = opts.getParcelableArray(KEY_ANIM_SPECS);
mAnimSpecs = new AppTransitionAnimationSpec[specs.length];
@@ -1297,8 +1319,9 @@
mIsEligibleForLegacyPermissionPrompt =
opts.getBoolean(KEY_LEGACY_PERMISSION_PROMPT_ELIGIBLE);
mDismissKeyguard = opts.getBoolean(KEY_DISMISS_KEYGUARD);
- mIgnorePendingIntentCreatorForegroundState = opts.getBoolean(
- KEY_IGNORE_PENDING_INTENT_CREATOR_FOREGROUND_STATE);
+ mPendingIntentCreatorBackgroundActivityStartMode = opts.getInt(
+ KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE,
+ MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED);
mDisableStartingWindow = opts.getBoolean(KEY_DISABLE_STARTING_WINDOW);
}
@@ -1893,6 +1916,16 @@
return mApplyActivityFlagsForBubbles;
}
+ /** @hide */
+ public void setApplyMultipleTaskFlagForShortcut(boolean apply) {
+ mApplyMultipleTaskFlagForShortcut = apply;
+ }
+
+ /** @hide */
+ public boolean isApplyMultipleTaskFlagForShortcut() {
+ return mApplyMultipleTaskFlagForShortcut;
+ }
+
/**
* Sets a launch cookie that can be used to track the activity and task that are launch as a
* result of this option. If the launched activity is a trampoline that starts another activity
@@ -1999,19 +2032,38 @@
* Sets background activity launch logic won't use pending intent creator foreground state.
*
* @hide
+ * @deprecated use {@link #setPendingIntentCreatorBackgroundActivityStartMode(int)} instead
*/
- public ActivityOptions setIgnorePendingIntentCreatorForegroundState(boolean state) {
- mIgnorePendingIntentCreatorForegroundState = state;
+ @Deprecated
+ public ActivityOptions setIgnorePendingIntentCreatorForegroundState(boolean ignore) {
+ mPendingIntentCreatorBackgroundActivityStartMode = ignore
+ ? MODE_BACKGROUND_ACTIVITY_START_DENIED : MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
return this;
}
/**
- * @return whether background activity launch logic should use pending intent creator
- * foreground state.
- * @hide
+ * Allow a {@link PendingIntent} to use the privilege of its creator to start background
+ * activities.
+ *
+ * @param mode the {@link android.app.ComponentOptions.BackgroundActivityStartMode} being set
+ * @throws IllegalArgumentException is the value is not a valid
+ * {@link android.app.ComponentOptions.BackgroundActivityStartMode}
*/
- public boolean getIgnorePendingIntentCreatorForegroundState() {
- return mIgnorePendingIntentCreatorForegroundState;
+ @NonNull
+ public ActivityOptions setPendingIntentCreatorBackgroundActivityStartMode(
+ @BackgroundActivityStartMode int mode) {
+ mPendingIntentCreatorBackgroundActivityStartMode = mode;
+ return this;
+ }
+
+ /**
+ * Returns the mode to start background activities granted by the creator of the
+ * {@link PendingIntent}.
+ *
+ * @return the {@link android.app.ComponentOptions.BackgroundActivityStartMode} currently set
+ */
+ public @BackgroundActivityStartMode int getPendingIntentCreatorBackgroundActivityStartMode() {
+ return mPendingIntentCreatorBackgroundActivityStartMode;
}
/**
@@ -2230,6 +2282,10 @@
if (mApplyActivityFlagsForBubbles) {
b.putBoolean(KEY_APPLY_ACTIVITY_FLAGS_FOR_BUBBLES, mApplyActivityFlagsForBubbles);
}
+ if (mApplyMultipleTaskFlagForShortcut) {
+ b.putBoolean(KEY_APPLY_MULTIPLE_TASK_FLAG_FOR_SHORTCUT,
+ mApplyMultipleTaskFlagForShortcut);
+ }
if (mAnimSpecs != null) {
b.putParcelableArray(KEY_ANIM_SPECS, mAnimSpecs);
}
@@ -2285,9 +2341,10 @@
if (mDismissKeyguard) {
b.putBoolean(KEY_DISMISS_KEYGUARD, mDismissKeyguard);
}
- if (mIgnorePendingIntentCreatorForegroundState) {
- b.putBoolean(KEY_IGNORE_PENDING_INTENT_CREATOR_FOREGROUND_STATE,
- mIgnorePendingIntentCreatorForegroundState);
+ if (mPendingIntentCreatorBackgroundActivityStartMode
+ != MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) {
+ b.putInt(KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE,
+ mPendingIntentCreatorBackgroundActivityStartMode);
}
if (mDisableStartingWindow) {
b.putBoolean(KEY_DISABLE_STARTING_WINDOW, mDisableStartingWindow);
@@ -2430,6 +2487,30 @@
return super.getPendingIntentBackgroundActivityStartMode();
}
+ /**
+ * Set PendingIntent activity is allowed to be started in the background if the caller
+ * can start background activities.
+ *
+ * @deprecated use #setPendingIntentBackgroundActivityStartMode(int) to set the full range
+ * of states
+ */
+ @Override
+ @Deprecated public void setPendingIntentBackgroundActivityLaunchAllowed(boolean allowed) {
+ super.setPendingIntentBackgroundActivityLaunchAllowed(allowed);
+ }
+
+ /**
+ * Get PendingIntent activity is allowed to be started in the background if the caller can start
+ * background activities.
+ *
+ * @deprecated use {@link #getPendingIntentBackgroundActivityStartMode()} since for apps
+ * targeting {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or higher this value might
+ * not match the actual behavior if the value was not explicitly set.
+ */
+ @Deprecated public boolean isPendingIntentBackgroundActivityLaunchAllowed() {
+ return super.isPendingIntentBackgroundActivityLaunchAllowed();
+ }
+
/** @hide */
@Override
public String toString() {
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 3761251..170c0b4 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -785,9 +785,10 @@
static final class ReceiverData extends BroadcastReceiver.PendingResult {
public ReceiverData(Intent intent, int resultCode, String resultData, Bundle resultExtras,
boolean ordered, boolean sticky, boolean assumeDelivered, IBinder token,
- int sendingUser) {
+ int sendingUser, int sentFromUid, String sentFromPackage) {
super(resultCode, resultData, resultExtras, TYPE_COMPONENT, ordered, sticky,
- assumeDelivered, token, sendingUser, intent.getFlags());
+ assumeDelivered, token, sendingUser, intent.getFlags(), sentFromUid,
+ sentFromPackage);
this.intent = intent;
}
@@ -801,7 +802,8 @@
return "ReceiverData{intent=" + intent + " packageName=" +
info.packageName + " resultCode=" + getResultCode()
+ " resultData=" + getResultData() + " resultExtras="
- + getResultExtras(false) + "}";
+ + getResultExtras(false) + " sentFromUid="
+ + getSentFromUid() + " sentFromPackage=" + getSentFromPackage() + "}";
}
}
@@ -1041,10 +1043,12 @@
public final void scheduleReceiver(Intent intent, ActivityInfo info,
CompatibilityInfo compatInfo, int resultCode, String data, Bundle extras,
- boolean ordered, boolean assumeDelivered, int sendingUser, int processState) {
+ boolean ordered, boolean assumeDelivered, int sendingUser, int processState,
+ int sentFromUid, String sentFromPackage) {
updateProcessState(processState, false);
ReceiverData r = new ReceiverData(intent, resultCode, data, extras,
- ordered, false, assumeDelivered, mAppThread.asBinder(), sendingUser);
+ ordered, false, assumeDelivered, mAppThread.asBinder(), sendingUser,
+ sentFromUid, sentFromPackage);
r.info = info;
sendMessage(H.RECEIVER, r);
}
@@ -1055,11 +1059,13 @@
if (r.registered) {
scheduleRegisteredReceiver(r.receiver, r.intent,
r.resultCode, r.data, r.extras, r.ordered, r.sticky,
- r.assumeDelivered, r.sendingUser, r.processState);
+ r.assumeDelivered, r.sendingUser, r.processState,
+ r.sentFromUid, r.sentFromPackage);
} else {
scheduleReceiver(r.intent, r.activityInfo, r.compatInfo,
r.resultCode, r.data, r.extras, r.sync,
- r.assumeDelivered, r.sendingUser, r.processState);
+ r.assumeDelivered, r.sendingUser, r.processState,
+ r.sentFromUid, r.sentFromPackage);
}
}
}
@@ -1289,7 +1295,8 @@
// applies transaction ordering per object for such calls.
public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent,
int resultCode, String dataStr, Bundle extras, boolean ordered,
- boolean sticky, boolean assumeDelivered, int sendingUser, int processState)
+ boolean sticky, boolean assumeDelivered, int sendingUser, int processState,
+ int sentFromUid, String sentFromPackage)
throws RemoteException {
updateProcessState(processState, false);
@@ -1299,12 +1306,19 @@
// report an expected delivery event
if (receiver instanceof LoadedApk.ReceiverDispatcher.InnerReceiver) {
((LoadedApk.ReceiverDispatcher.InnerReceiver) receiver).performReceive(intent,
- resultCode, dataStr, extras, ordered, sticky, assumeDelivered, sendingUser);
+ resultCode, dataStr, extras, ordered, sticky, assumeDelivered, sendingUser,
+ sentFromUid, sentFromPackage);
} else {
if (!assumeDelivered) {
Log.wtf(TAG, "scheduleRegisteredReceiver() called for " + receiver
+ " and " + intent + " without mechanism to finish delivery");
}
+ if (sentFromUid != Process.INVALID_UID || sentFromPackage != null) {
+ Log.wtf(TAG,
+ "scheduleRegisteredReceiver() called for " + receiver + " and " + intent
+ + " from " + sentFromPackage + " (UID: " + sentFromUid
+ + ") without mechanism to propagate the sender's identity");
+ }
receiver.performReceive(intent, resultCode, dataStr, extras, ordered, sticky,
sendingUser);
}
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index f9e4081..7d40a22 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -42,7 +42,7 @@
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.database.DatabaseUtils;
-import android.healthconnect.HealthConnectManager;
+import android.health.connect.HealthConnectManager;
import android.media.AudioAttributes.AttributeUsage;
import android.os.Binder;
import android.os.Build;
@@ -1432,14 +1432,14 @@
.APP_OP_SYSTEM_EXEMPT_FROM_FGS_BG_START_WHILE_IN_USE_PERMISSION_RESTRICTION;
/**
- * Hide foreground service stop button in quick settings.
+ * Allows an application to start an activity while running in the background.
*
* Only to be used by the system.
*
* @hide
*/
- public static final int OP_SYSTEM_EXEMPT_FROM_FGS_STOP_BUTTON =
- AppProtoEnums.APP_OP_SYSTEM_EXEMPT_FROM_FGS_STOP_BUTTON;
+ public static final int OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION =
+ AppProtoEnums.APP_OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION;
/**
* Allows an application to capture bugreport directly without consent dialog when using the
@@ -2027,14 +2027,14 @@
"android:system_exempt_from_fgs_bg_start_while_in_use_permission_restriction";
/**
- * Hide foreground service stop button in quick settings.
+ * Allows an application to start an activity while running in the background.
*
* Only to be used by the system.
*
* @hide
*/
- public static final String OPSTR_SYSTEM_EXEMPT_FROM_FGS_STOP_BUTTON =
- "android:system_exempt_from_fgs_stop_button";
+ public static final String OPSTR_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION =
+ "android:system_exempt_from_activity_bg_start_restriction";
/**
* Allows an application to capture bugreport directly without consent dialog when using the
@@ -2562,9 +2562,9 @@
OPSTR_SYSTEM_EXEMPT_FROM_FGS_BG_START_WHILE_IN_USE_PERMISSION_RESTRICTION,
"SYSTEM_EXEMPT_FROM_FGS_BG_START_WHILE_IN_USE_PERMISSION_RESTRICTION")
.build(),
- new AppOpInfo.Builder(OP_SYSTEM_EXEMPT_FROM_FGS_STOP_BUTTON,
- OPSTR_SYSTEM_EXEMPT_FROM_FGS_STOP_BUTTON,
- "SYSTEM_EXEMPT_FROM_FGS_STOP_BUTTON").build(),
+ new AppOpInfo.Builder(OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION,
+ OPSTR_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION,
+ "SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION").build(),
new AppOpInfo.Builder(
OP_CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD,
OPSTR_CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD,
diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java
index 88765c3..d3b03c0 100644
--- a/core/java/android/app/BroadcastOptions.java
+++ b/core/java/android/app/BroadcastOptions.java
@@ -27,6 +27,7 @@
import android.compat.annotation.ChangeId;
import android.compat.annotation.Disabled;
import android.compat.annotation.EnabledSince;
+import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
@@ -46,9 +47,7 @@
* Helper class for building an options Bundle that can be used with
* {@link android.content.Context#sendBroadcast(android.content.Intent)
* Context.sendBroadcast(Intent)} and related methods.
- * {@hide}
*/
-@SystemApi
public class BroadcastOptions extends ComponentOptions {
private long mTemporaryAppAllowlistDuration;
private @TempAllowListType int mTemporaryAppAllowlistType;
@@ -64,6 +63,7 @@
private boolean mRequireCompatChangeEnabled = true;
private boolean mIsAlarmBroadcast = false;
private boolean mIsDeferUntilActive = false;
+ private boolean mShareIdentity = false;
private long mIdForResponseEvent;
private @Nullable IntentFilter mRemoveMatchingFilter;
private @DeliveryGroupPolicy int mDeliveryGroupPolicy;
@@ -172,6 +172,12 @@
"android:broadcast.is_alarm";
/**
+ * Whether the broadcasting app's identity should be available to the receiver.
+ * @see #setShareIdentityEnabled(boolean)
+ */
+ private static final String KEY_SHARE_IDENTITY = "android:broadcast.share_identity";
+
+ /**
* @hide
* @deprecated Use {@link android.os.PowerExemptionManager#
* TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED} instead.
@@ -270,7 +276,12 @@
*/
public static final int DELIVERY_GROUP_POLICY_MERGED = 2;
- public static BroadcastOptions makeBasic() {
+ /**
+ * Creates a basic {@link BroadcastOptions} with no options initially set.
+ *
+ * @return an instance of {@code BroadcastOptions} against which options can be set
+ */
+ public static @NonNull BroadcastOptions makeBasic() {
BroadcastOptions opts = new BroadcastOptions();
return opts;
}
@@ -318,6 +329,7 @@
mRequireCompatChangeEnabled = opts.getBoolean(KEY_REQUIRE_COMPAT_CHANGE_ENABLED, true);
mIdForResponseEvent = opts.getLong(KEY_ID_FOR_RESPONSE_EVENT);
mIsAlarmBroadcast = opts.getBoolean(KEY_ALARM_BROADCAST, false);
+ mShareIdentity = opts.getBoolean(KEY_SHARE_IDENTITY, false);
mRemoveMatchingFilter = opts.getParcelable(KEY_REMOVE_MATCHING_FILTER,
IntentFilter.class);
mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY,
@@ -335,8 +347,10 @@
* power allowlist when this broadcast is being delivered to it.
* @param duration The duration in milliseconds; 0 means to not place on allowlist.
* @deprecated use {@link #setTemporaryAppAllowlist(long, int, int, String)} instead.
+ * @hide
*/
@Deprecated
+ @SystemApi
@RequiresPermission(anyOf = {android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST,
android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND,
android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND})
@@ -350,6 +364,8 @@
* Set a duration for which the system should temporary place an application on the
* power allowlist when this broadcast is being delivered to it, specify the temp allowlist
* type.
+ * @hide
+ *
* @param duration the duration in milliseconds.
* 0 means to not place on allowlist, and clears previous call to this method.
* @param type one of {@link TempAllowListType}.
@@ -360,6 +376,7 @@
* @param reason A human-readable reason explaining why the app is temp allowlisted. Only
* used for logging purposes. Could be null or empty string.
*/
+ @SystemApi
@RequiresPermission(anyOf = {android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST,
android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND,
android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND})
@@ -500,7 +517,9 @@
* Sets whether pending intent can be sent for an application with background restrictions
* @param dontSendToRestrictedApps if true, pending intent will not be sent for an application
* with background restrictions. Default value is {@code false}
+ * @hide
*/
+ @SystemApi
public void setDontSendToRestrictedApps(boolean dontSendToRestrictedApps) {
mDontSendToRestrictedApps = dontSendToRestrictedApps;
}
@@ -516,7 +535,9 @@
/**
* Sets the process will be able to start activities from background for the duration of
* the broadcast dispatch. Default value is {@code false}
+ * @hide
*/
+ @SystemApi
@RequiresPermission(android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND)
public void setBackgroundActivityStartsAllowed(boolean allowBackgroundActivityStarts) {
mAllowBackgroundActivityStarts = allowBackgroundActivityStarts;
@@ -578,6 +599,7 @@
* <p>
* This requirement applies to both manifest registered and runtime
* registered receivers.
+ * @hide
*
* @param changeId the {@link ChangeId} to inspect
* @param enabled the required enabled state of the inspected
@@ -585,6 +607,7 @@
* @see CompatChanges#isChangeEnabled
* @see #clearRequireCompatChange()
*/
+ @SystemApi
public void setRequireCompatChange(long changeId, boolean enabled) {
mRequireCompatChangeId = changeId;
mRequireCompatChangeEnabled = enabled;
@@ -593,7 +616,9 @@
/**
* Clear any previously defined requirement for this broadcast requested via
* {@link #setRequireCompatChange(long, boolean)}.
+ * @hide
*/
+ @SystemApi
public void clearRequireCompatChange() {
mRequireCompatChangeId = CHANGE_INVALID;
mRequireCompatChangeEnabled = true;
@@ -621,6 +646,39 @@
}
/**
+ * Sets whether the identity of the broadcasting app should be shared with all receivers
+ * that will receive this broadcast.
+ *
+ * <p>Use this option when broadcasting to a receiver that needs to know the identity of the
+ * broadcaster; with this set to {@code true}, the receiver will have access to the broadcasting
+ * app's package name and uid.
+ *
+ * <p>Defaults to {@code false} if not set.
+ *
+ * @param shareIdentityEnabled whether the broadcasting app's identity should be shared with the
+ * receiver
+ * @return {@code this} {@link BroadcastOptions} instance
+ * @see BroadcastReceiver#getSentFromUid()
+ * @see BroadcastReceiver#getSentFromPackage()
+ */
+ public @NonNull BroadcastOptions setShareIdentityEnabled(boolean shareIdentityEnabled) {
+ mShareIdentity = shareIdentityEnabled;
+ return this;
+ }
+
+ /**
+ * Returns whether the broadcasting app has opted-in to sharing its identity with the receiver.
+ *
+ * @return {@code true} if the broadcasting app has opted in to sharing its identity
+ * @see #setShareIdentityEnabled(boolean)
+ * @see BroadcastReceiver#getSentFromUid()
+ * @see BroadcastReceiver#getSentFromPackage()
+ */
+ public boolean isShareIdentityEnabled() {
+ return mShareIdentity;
+ }
+
+ /**
* Did this broadcast originate from a push message from the server?
*
* @return true if this broadcast is a push message, false otherwise.
@@ -716,7 +774,15 @@
return this;
}
- /** @hide */
+ /**
+ * Returns if this broadcast should not run until the process is in an active process state.
+ *
+ * @return {@code true} if this broadcast should not run until the process is in an active
+ * process state. Otherwise, {@code false}.
+ * @see #setDeferUntilActive(boolean)
+ *
+ * @hide
+ */
@SystemApi
public boolean isDeferUntilActive() {
return mIsDeferUntilActive;
@@ -989,7 +1055,7 @@
* extras merger is supplied.
*/
@Override
- public Bundle toBundle() {
+ public @NonNull Bundle toBundle() {
Bundle b = super.toBundle();
if (isTemporaryAppAllowlistSet()) {
b.putLong(KEY_TEMPORARY_APP_ALLOWLIST_DURATION, mTemporaryAppAllowlistDuration);
@@ -1000,6 +1066,9 @@
if (mIsAlarmBroadcast) {
b.putBoolean(KEY_ALARM_BROADCAST, true);
}
+ if (mShareIdentity) {
+ b.putBoolean(KEY_SHARE_IDENTITY, true);
+ }
if (mMinManifestReceiverApiLevel != 0) {
b.putInt(KEY_MIN_MANIFEST_RECEIVER_API_LEVEL, mMinManifestReceiverApiLevel);
}
diff --git a/core/java/android/app/ComponentOptions.java b/core/java/android/app/ComponentOptions.java
index 3776369..9c262b9 100644
--- a/core/java/android/app/ComponentOptions.java
+++ b/core/java/android/app/ComponentOptions.java
@@ -20,6 +20,8 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
import android.os.Bundle;
import java.lang.annotation.Retention;
@@ -29,6 +31,10 @@
* Base class for {@link ActivityOptions} and {@link BroadcastOptions}.
* @hide
*/
+// Expose the methods and constants required to test the SystemApis in subclasses.
+@TestApi
+// Suppressed since lint is recommending class have a suffix of Params.
+@SuppressLint("UserHandleName")
public class ComponentOptions {
/**
@@ -57,17 +63,30 @@
private boolean mPendingIntentBalAllowedByPermission = false;
private boolean mIsInteractive = false;
+ /** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = {"MODE_BACKGROUND_ACTIVITY_START_"}, value = {
MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED,
MODE_BACKGROUND_ACTIVITY_START_ALLOWED,
MODE_BACKGROUND_ACTIVITY_START_DENIED})
public @interface BackgroundActivityStartMode {}
- /** No explicit value chosen. The system will decide whether to grant privileges. */
+ /**
+ * No explicit value chosen. The system will decide whether to grant privileges.
+ * @hide
+ */
+ @TestApi
public static final int MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED = 0;
- /** Allow the {@link PendingIntent} to use the background activity start privileges. */
+ /**
+ * Allow the {@link PendingIntent} to use the background activity start privileges.
+ * @hide
+ */
+ @TestApi
public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOWED = 1;
- /** Deny the {@link PendingIntent} to use the background activity start privileges. */
+ /**
+ * Deny the {@link PendingIntent} to use the background activity start privileges.
+ * @hide
+ */
+ @TestApi
public static final int MODE_BACKGROUND_ACTIVITY_START_DENIED = 2;
ComponentOptions() {
@@ -118,6 +137,7 @@
*
* @deprecated use #setPendingIntentBackgroundActivityStartMode(int) to set the full range
* of states
+ * @hide
*/
@Deprecated public void setPendingIntentBackgroundActivityLaunchAllowed(boolean allowed) {
mPendingIntentBalAllowed = allowed;
@@ -130,6 +150,7 @@
* @deprecated use {@link #getPendingIntentBackgroundActivityStartMode()} since for apps
* targeting {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or higher this value might
* not match the actual behavior if the value was not explicitly set.
+ * @hide
*/
@Deprecated public boolean isPendingIntentBackgroundActivityLaunchAllowed() {
if (mPendingIntentBalAllowed == null) {
@@ -147,6 +168,7 @@
* methods. A privileged sender of a PendingIntent should only grant
* {@link #MODE_BACKGROUND_ACTIVITY_START_ALLOWED} if the PendingIntent is from a trusted source
* and/or executed on behalf the user.
+ * @hide
*/
public @NonNull ComponentOptions setPendingIntentBackgroundActivityStartMode(
@BackgroundActivityStartMode int state) {
@@ -169,6 +191,7 @@
/**
* Gets the mode for allowing or denying the senders privileges to start background activities
* to the PendingIntent.
+ * @hide
*
* @see #setPendingIntentBackgroundActivityStartMode(int)
*/
@@ -199,6 +222,7 @@
return mPendingIntentBalAllowedByPermission;
}
+ /** @hide */
public Bundle toBundle() {
Bundle b = new Bundle();
if (mPendingIntentBalAllowed != null) {
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 12899f2..bed75db 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -25,6 +25,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.annotation.UiContext;
import android.companion.virtual.VirtualDeviceManager;
import android.compat.annotation.UnsupportedAppUsage;
@@ -1358,7 +1359,13 @@
}
@Override
+ @SuppressLint("AndroidFrameworkRequiresPermission")
public void sendOrderedBroadcast(Intent intent, String receiverPermission) {
+ sendOrderedBroadcast(intent, receiverPermission, /*options=*/ null);
+ }
+
+ @Override
+ public void sendOrderedBroadcast(Intent intent, String receiverPermission, Bundle options) {
warnIfCallingFromSystemProcess();
String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
String[] receiverPermissions = receiverPermission == null ? null
@@ -1368,8 +1375,8 @@
ActivityManager.getService().broadcastIntentWithFeature(
mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
null, Activity.RESULT_OK, null, null, receiverPermissions,
- null /*excludedPermissions=*/, null, AppOpsManager.OP_NONE, null, true, false,
- getUserId());
+ null /*excludedPermissions=*/, null, AppOpsManager.OP_NONE, options, true,
+ false, getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/app/ForegroundServiceTypePolicy.java b/core/java/android/app/ForegroundServiceTypePolicy.java
index 20d19c1..5ef29e4 100644
--- a/core/java/android/app/ForegroundServiceTypePolicy.java
+++ b/core/java/android/app/ForegroundServiceTypePolicy.java
@@ -388,7 +388,6 @@
new RegularPermission(Manifest.permission.SCHEDULE_EXACT_ALARM),
new RegularPermission(Manifest.permission.USE_EXACT_ALARM),
new AppOpPermission(AppOpsManager.OP_ACTIVATE_VPN),
- new AppOpPermission(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN),
}, false)
);
diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl
index 4f69d85..dad9b43 100644
--- a/core/java/android/app/IApplicationThread.aidl
+++ b/core/java/android/app/IApplicationThread.aidl
@@ -66,7 +66,8 @@
void scheduleReceiver(in Intent intent, in ActivityInfo info,
in CompatibilityInfo compatInfo,
int resultCode, in String data, in Bundle extras, boolean ordered,
- boolean assumeDelivered, int sendingUser, int processState);
+ boolean assumeDelivered, int sendingUser, int processState, int sentFromUid,
+ in String sentFromPackage);
void scheduleReceiverList(in List<ReceiverInfo> info);
@@ -102,7 +103,8 @@
in String[] args);
void scheduleRegisteredReceiver(IIntentReceiver receiver, in Intent intent,
int resultCode, in String data, in Bundle extras, boolean ordered,
- boolean sticky, boolean assumeDelivered, int sendingUser, int processState);
+ boolean sticky, boolean assumeDelivered, int sendingUser, int processState,
+ int sentFromUid, in String sentFromPackage);
void scheduleLowMemory();
void profilerControl(boolean start, in ProfilerInfo profilerInfo, int profileType);
void setSchedulingGroup(int group);
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index 70d8a5e..bb91ecd 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -2324,10 +2324,13 @@
mUiAutomation.connect(flags);
return mUiAutomation;
}
+ final long startUptime = SystemClock.uptimeMillis();
try {
mUiAutomation.connectWithTimeout(flags, CONNECT_TIMEOUT_MILLIS);
return mUiAutomation;
} catch (TimeoutException e) {
+ final long waited = SystemClock.uptimeMillis() - startUptime;
+ Log.e(TAG, "Unable to connect to UiAutomation. Waited for " + waited + " ms", e);
mUiAutomation.destroy();
mUiAutomation = null;
}
diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java
index 7c22902..c13da0b 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -1683,12 +1683,13 @@
performReceive(intent, resultCode, data, extras, ordered, sticky,
BroadcastReceiver.PendingResult.guessAssumeDelivered(
BroadcastReceiver.PendingResult.TYPE_REGISTERED, ordered),
- sendingUser);
+ sendingUser, /*sentFromUid=*/ Process.INVALID_UID,
+ /*sentFromPackage=*/ null);
}
public void performReceive(Intent intent, int resultCode, String data,
Bundle extras, boolean ordered, boolean sticky, boolean assumeDelivered,
- int sendingUser) {
+ int sendingUser, int sentFromUid, String sentFromPackage) {
final LoadedApk.ReceiverDispatcher rd;
if (intent == null) {
Log.wtf(TAG, "Null intent received");
@@ -1703,7 +1704,8 @@
}
if (rd != null) {
rd.performReceive(intent, resultCode, data, extras,
- ordered, sticky, assumeDelivered, sendingUser);
+ ordered, sticky, assumeDelivered, sendingUser,
+ sentFromUid, sentFromPackage);
} else if (!assumeDelivered) {
// The activity manager dispatched a broadcast to a registered
// receiver in this process, but before it could be delivered the
@@ -1743,11 +1745,12 @@
private boolean mRunCalled;
public Args(Intent intent, int resultCode, String resultData, Bundle resultExtras,
- boolean ordered, boolean sticky, boolean assumeDelivered, int sendingUser) {
+ boolean ordered, boolean sticky, boolean assumeDelivered, int sendingUser,
+ int sentFromUid, String sentFromPackage) {
super(resultCode, resultData, resultExtras,
mRegistered ? TYPE_REGISTERED : TYPE_UNREGISTERED, ordered,
sticky, assumeDelivered, mAppThread.asBinder(), sendingUser,
- intent.getFlags());
+ intent.getFlags(), sentFromUid, sentFromPackage);
mCurIntent = intent;
}
@@ -1871,9 +1874,9 @@
public void performReceive(Intent intent, int resultCode, String data,
Bundle extras, boolean ordered, boolean sticky, boolean assumeDelivered,
- int sendingUser) {
+ int sendingUser, int sentFromUid, String sentFromPackage) {
final Args args = new Args(intent, resultCode, data, extras, ordered,
- sticky, assumeDelivered, sendingUser);
+ sticky, assumeDelivered, sendingUser, sentFromUid, sentFromPackage);
if (intent == null) {
Log.wtf(TAG, "Null intent received");
} else {
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 1345910..b86b09d 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -703,6 +703,16 @@
*/
public static final int FLAG_BUBBLE = 0x00001000;
+ /**
+ * Bit to be bitswised-ored into the {@link #flags} field that should be
+ * set by the system if this notification is not dismissible.
+ *
+ * This flag is for internal use only; applications cannot set this flag directly.
+ * @hide
+ */
+ public static final int FLAG_NO_DISMISS = 0x00002000;
+
+
private static final List<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Arrays.asList(
BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class,
DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class,
diff --git a/core/java/android/app/ReceiverInfo.aidl b/core/java/android/app/ReceiverInfo.aidl
index 8d7e3c4..7364d0f 100644
--- a/core/java/android/app/ReceiverInfo.aidl
+++ b/core/java/android/app/ReceiverInfo.aidl
@@ -38,6 +38,8 @@
int sendingUser;
int processState;
int resultCode;
+ int sentFromUid = -1;
+ String sentFromPackage;
/**
* True if this instance represents a registered receiver and false if this instance
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt b/core/java/android/app/RemoteLockscreenValidationResult.aidl
similarity index 64%
copy from packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
copy to core/java/android/app/RemoteLockscreenValidationResult.aidl
index 497c272..504f78f 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
+++ b/core/java/android/app/RemoteLockscreenValidationResult.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,12 +14,7 @@
* limitations under the License.
*/
-package com.android.credentialmanager.jetpack.developer
+package android.app;
-/**
- * Internal exception used to indicate a parsing error while converting from a framework type to
- * a jetpack type.
- *
- * @hide
- */
-internal class FrameworkClassParsingException : Exception()
\ No newline at end of file
+/** {@hide} */
+parcelable RemoteLockscreenValidationResult;
diff --git a/core/java/android/app/RemoteLockscreenValidationResult.java b/core/java/android/app/RemoteLockscreenValidationResult.java
new file mode 100644
index 0000000..4f15be2
--- /dev/null
+++ b/core/java/android/app/RemoteLockscreenValidationResult.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.DurationMillisLong;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+/**
+ * Result of lock screen credentials verification.
+ *
+ * @hide
+ */
+public final class RemoteLockscreenValidationResult implements Parcelable {
+
+ /**
+ * The guess was correct
+ */
+ public static final int RESULT_GUESS_VALID = 1;
+
+ /**
+ * Remote device provided incorrect credentials.
+ */
+ public static final int RESULT_GUESS_INVALID = 2;
+
+ /**
+ * The operation was canceled because the API is locked out due to too many attempts. It
+ * usually happens after 5 failed attempts and API may be called again after a short
+ * delay specified by {@code getTimeoutMillis}.
+ */
+ public static final int RESULT_LOCKOUT = 3;
+
+ /**
+ * There were too many invalid guesses.
+ */
+ public static final int RESULT_NO_REMAINING_ATTEMPTS = 4;
+
+ @IntDef({RESULT_GUESS_VALID,
+ RESULT_GUESS_INVALID,
+ RESULT_LOCKOUT,
+ RESULT_NO_REMAINING_ATTEMPTS})
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ResultCode {}
+
+ private int mResultCode;
+ private long mTimeoutMillis;
+
+ public static final @NonNull Parcelable.Creator<RemoteLockscreenValidationResult> CREATOR =
+ new Parcelable.Creator<RemoteLockscreenValidationResult>() {
+ @Override
+ public RemoteLockscreenValidationResult createFromParcel(Parcel source) {
+ return new RemoteLockscreenValidationResult(source);
+ }
+
+ @Override
+ public RemoteLockscreenValidationResult[] newArray(int size) {
+ return new RemoteLockscreenValidationResult[size];
+ }
+ };
+
+ /**
+ * Builder for {@code RemoteLockscreenValidationResult}
+ */
+ public static final class Builder {
+ private RemoteLockscreenValidationResult mInstance = new RemoteLockscreenValidationResult();
+
+ /**
+ * Sets the result code.
+ */
+ public @NonNull Builder setResultCode(@ResultCode int resultCode) {
+ mInstance.mResultCode = resultCode;
+ return this;
+ }
+
+ /**
+ * Sets timeout for {@code RESULT_LOCKOUT}.
+ * Default value is {@code 0}.
+ */
+ public @NonNull Builder setTimeoutMillis(@DurationMillisLong long timeoutMillis) {
+ mInstance.mTimeoutMillis = timeoutMillis;
+ return this;
+ }
+
+ /**
+ * Creates {@code RemoteLockscreenValidationResult}.
+ *
+ * @throws IllegalStateException if result code was not set.
+ */
+ public @NonNull RemoteLockscreenValidationResult build() {
+ if (mInstance.mResultCode == 0) {
+ throw new IllegalStateException("Result code must be set");
+ }
+ return mInstance;
+ }
+ }
+
+ /**
+ * Gets the result code.
+ */
+ public @ResultCode int getResultCode() {
+ return mResultCode;
+ }
+
+ /**
+ * Delay before next attempt to verify credentials.
+ *
+ * Default value is {@code 0}.
+ */
+ public @DurationMillisLong long getTimeoutMillis() {
+ return mTimeoutMillis;
+ }
+
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeInt(mResultCode);
+ out.writeLong(mTimeoutMillis);
+ }
+
+ private RemoteLockscreenValidationResult() {
+ }
+
+ private RemoteLockscreenValidationResult(Parcel in) {
+ mResultCode = in.readInt();
+ mTimeoutMillis = in.readLong();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java
index e485397..a155457 100644
--- a/core/java/android/app/Service.java
+++ b/core/java/android/app/Service.java
@@ -1140,6 +1140,22 @@
* Callback called on timeout for {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SHORT_SERVICE}.
* See {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SHORT_SERVICE} for more details.
*
+ * <p>If the foreground service of type
+ * {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SHORT_SERVICE}
+ * doesn't finish even after it's timed out,
+ * the app will be declared an ANR after a short grace period of several seconds.
+ *
+ * <p>Note, even though
+ * {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SHORT_SERVICE}
+ * was added
+ * on Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * it can be also used on
+ * on prior android versions (just like other new foreground service types can be used).
+ * However, because {@link android.app.Service#onTimeout(int)} did not exist on prior versions,
+ * it will never called on such versions.
+ * Because of this, developers must make sure to stop the foreground service even if
+ * {@link android.app.Service#onTimeout(int)} is not called on such versions.
+ *
* @param startId the startId passed to {@link #onStartCommand(Intent, int, int)} when
* the service started.
*/
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt b/core/java/android/app/StartLockscreenValidationRequest.aidl
similarity index 64%
copy from packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
copy to core/java/android/app/StartLockscreenValidationRequest.aidl
index 497c272..367dfee 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
+++ b/core/java/android/app/StartLockscreenValidationRequest.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,12 +14,7 @@
* limitations under the License.
*/
-package com.android.credentialmanager.jetpack.developer
+package android.app;
-/**
- * Internal exception used to indicate a parsing error while converting from a framework type to
- * a jetpack type.
- *
- * @hide
- */
-internal class FrameworkClassParsingException : Exception()
\ No newline at end of file
+/** {@hide} */
+parcelable StartLockscreenValidationRequest;
diff --git a/core/java/android/app/StartLockscreenValidationRequest.java b/core/java/android/app/StartLockscreenValidationRequest.java
new file mode 100644
index 0000000..69c268bcb
--- /dev/null
+++ b/core/java/android/app/StartLockscreenValidationRequest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.NonNull;
+import android.app.KeyguardManager.LockTypes;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Provides information necessary to perform remote lock screen credentials check.
+ *
+ * @hide
+ */
+public final class StartLockscreenValidationRequest implements Parcelable {
+
+ @LockTypes
+ private int mLockscreenUiType;
+
+ private byte[] mSourcePublicKey;
+
+ private int mRemainingAttempts;
+
+ public static final @NonNull Parcelable.Creator<StartLockscreenValidationRequest> CREATOR = new
+ Parcelable.Creator<StartLockscreenValidationRequest>() {
+ @Override
+ public StartLockscreenValidationRequest createFromParcel(Parcel source) {
+ return new StartLockscreenValidationRequest(source);
+ }
+
+ @Override
+ public StartLockscreenValidationRequest[] newArray(int size) {
+ return new StartLockscreenValidationRequest[size];
+ }
+ };
+
+
+ /**
+ * Builder for {@code StartLockscreenValidationRequest}
+ */
+ public static final class Builder {
+ private StartLockscreenValidationRequest mInstance = new StartLockscreenValidationRequest();
+
+ /**
+ * Sets UI type.
+ * Default value is {@code LockTypes.PASSWORD}
+ *
+ * @param lockscreenUiType The UI format
+ * @return This builder.
+ */
+ public @NonNull Builder setLockscreenUiType(@LockTypes int lockscreenUiType) {
+ mInstance.mLockscreenUiType = lockscreenUiType;
+ return this;
+ }
+
+ /**
+ * Sets public key using secure box encoding
+ * @return This builder.
+ */
+ public @NonNull Builder setSourcePublicKey(@NonNull byte[] publicKey) {
+ mInstance.mSourcePublicKey = publicKey;
+ return this;
+ }
+
+ /**
+ * Sets the number of remaining credentials check
+ * Default value is {@code 0}
+ *
+ * @return This builder.
+ */
+ public @NonNull Builder setRemainingAttempts(int remainingAttempts) {
+ mInstance.mRemainingAttempts = remainingAttempts;
+ return this;
+ }
+
+ /**
+ * Creates {@code StartLockscreenValidationRequest}
+ *
+ * @throws NullPointerException if required fields are not set.
+ */
+ public @NonNull StartLockscreenValidationRequest build() {
+ Objects.requireNonNull(mInstance.mSourcePublicKey);
+ return mInstance;
+ }
+ }
+
+ /**
+ * Specifies lock screen credential type.
+ */
+ public @LockTypes int getLockscreenUiType() {
+ return mLockscreenUiType;
+ }
+
+ /**
+ * Public key used to send encrypted credentials.
+ */
+ public @NonNull byte[] getSourcePublicKey() {
+ return mSourcePublicKey;
+ }
+
+ /**
+ * Number of remaining attempts to verify credentials.
+ *
+ * <p>After correct guess counter is reset to {@code 5}.
+ */
+ public int getRemainingAttempts() {
+ return mRemainingAttempts;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeInt(mLockscreenUiType);
+ out.writeByteArray(mSourcePublicKey);
+ out.writeInt(mRemainingAttempts);
+ }
+
+ private StartLockscreenValidationRequest() {
+ }
+
+ private StartLockscreenValidationRequest(Parcel in) {
+ mLockscreenUiType = in.readInt();
+ mSourcePublicKey = in.createByteArray();
+ mRemainingAttempts = in.readInt();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
index 1c1a558..9e31011 100644
--- a/core/java/android/app/StatusBarManager.java
+++ b/core/java/android/app/StatusBarManager.java
@@ -30,6 +30,7 @@
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.graphics.drawable.Icon;
import android.media.INearbyMediaDevicesProvider;
import android.media.INearbyMediaDevicesUpdateCallback;
@@ -47,6 +48,7 @@
import android.util.Slog;
import android.view.View;
+import com.android.internal.statusbar.AppClipsServiceConnector;
import com.android.internal.statusbar.IAddTileResultCallback;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.IUndoMediaTransferCallback;
@@ -1190,6 +1192,37 @@
return CompatChanges.isChangeEnabled(MEDIA_CONTROL_SESSION_ACTIONS, packageName, user);
}
+ /**
+ * Checks whether the supplied activity can {@link Activity#startActivityForResult(Intent, int)}
+ * a system activity that captures content on the screen to take a screenshot.
+ *
+ * <p>Note: The result should not be cached.
+ *
+ * <p>The system activity displays an editing tool that allows user to edit the screenshot, save
+ * it on device, and return the edited screenshot as {@link android.net.Uri} to the calling
+ * activity. User interaction is required to return the edited screenshot to the calling
+ * activity.
+ *
+ * <p>When {@code true}, callers can use {@link Activity#startActivityForResult(Intent, int)}
+ * to start start the content capture activity using
+ * {@link Intent#ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE}.
+ *
+ * @param activity Calling activity
+ * @return true if the activity supports launching the capture content activity for note.
+ *
+ * @see Intent#ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE
+ * @see Manifest.permission#LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE
+ * @see android.app.role.RoleManager#ROLE_NOTES
+ */
+ @RequiresPermission(Manifest.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE)
+ public boolean canLaunchCaptureContentActivityForNote(@NonNull Activity activity) {
+ Objects.requireNonNull(activity);
+ IBinder activityToken = activity.getActivityToken();
+ int taskId = ActivityClient.getInstance().getTaskForActivity(activityToken, false);
+ return new AppClipsServiceConnector(mContext)
+ .canLaunchCaptureContentActivityForNote(taskId);
+ }
+
/** @hide */
public static String windowStateToString(int state) {
if (state == WINDOW_STATE_HIDING) return "WINDOW_STATE_HIDING";
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 52d1d68..031d351 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -116,7 +116,7 @@
import android.hardware.radio.RadioManager;
import android.hardware.usb.IUsbManager;
import android.hardware.usb.UsbManager;
-import android.healthconnect.HealthServicesInitializer;
+import android.health.connect.HealthServicesInitializer;
import android.location.CountryDetector;
import android.location.ICountryDetector;
import android.location.ILocationManager;
diff --git a/core/java/android/app/WallpaperColors.java b/core/java/android/app/WallpaperColors.java
index 067a4c3..a34a50c 100644
--- a/core/java/android/app/WallpaperColors.java
+++ b/core/java/android/app/WallpaperColors.java
@@ -27,6 +27,7 @@
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.SystemProperties;
import android.util.Log;
import android.util.MathUtils;
import android.util.Size;
@@ -101,11 +102,13 @@
// Decides when dark theme is optimal for this wallpaper
private static final float DARK_THEME_MEAN_LUMINANCE = 0.3f;
// Minimum mean luminosity that an image needs to have to support dark text
- private static final float BRIGHT_IMAGE_MEAN_LUMINANCE = 0.7f;
+ private static final float BRIGHT_IMAGE_MEAN_LUMINANCE = SystemProperties.getInt(
+ "persist.wallpapercolors.threshold", 70) / 100f;
// We also check if the image has dark pixels in it,
// to avoid bright images with some dark spots.
private static final float DARK_PIXEL_CONTRAST = 5.5f;
- private static final float MAX_DARK_AREA = 0.05f;
+ private static final float MAX_DARK_AREA = SystemProperties.getInt(
+ "persist.wallpapercolors.max_dark_area", 5) / 100f;
private final List<Color> mMainColors;
private final Map<Integer, Integer> mAllColors;
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index f3a83d8..ff17824 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -784,6 +784,7 @@
* @return true if the lockscreen wallpaper always uses a wallpaperService, not a static image
* @hide
*/
+ @TestApi
public boolean isLockscreenLiveWallpaperEnabled() {
return mLockscreenLiveWallpaper;
}
@@ -1258,6 +1259,8 @@
* @param which Specifies home or lock screen
* @hide
*/
+ @TestApi
+ @Nullable
public Bitmap getBitmapAsUser(int userId, boolean hardware, @SetWallpaperFlags int which) {
final ColorManagementProxy cmProxy = getColorManagementProxy();
return sGlobals.peekWallpaperBitmap(mContext, true, which, userId, hardware, cmProxy);
@@ -1496,6 +1499,13 @@
/**
* Returns the information about the home screen wallpaper if its current wallpaper is a live
* wallpaper component. Otherwise, if the wallpaper is a static image, this returns null.
+ *
+ * <p>
+ * In order to use this, apps should declare a {@code <queries>} tag with the action
+ * {@code "android.service.wallpaper.WallpaperService"}. Otherwise,
+ * this method will return {@code null} if the caller doesn't otherwise have
+ * <a href="{@docRoot}training/package-visibility">visibility</a> of the wallpaper package.
+ * </p>
*/
public WallpaperInfo getWallpaperInfo() {
return getWallpaperInfoForUser(mContext.getUserId());
@@ -1517,6 +1527,13 @@
* wallpaper component. Otherwise, if the wallpaper is a static image or is not set, this
* returns null.
*
+ * <p>
+ * In order to use this, apps should declare a {@code <queries>} tag with the action
+ * {@code "android.service.wallpaper.WallpaperService"}. Otherwise,
+ * this method will return {@code null} if the caller doesn't otherwise have
+ * <a href="{@docRoot}training/package-visibility">visibility</a> of the wallpaper package.
+ * </p>
+ *
* @param which Specifies wallpaper to request (home or lock).
* @throws IllegalArgumentException if {@code which} is not exactly one of
* {{@link #FLAG_SYSTEM},{@link #FLAG_LOCK}}.
@@ -2421,6 +2438,7 @@
*
* @hide
*/
+ @TestApi
public void setWallpaperZoomOut(@NonNull IBinder windowToken, float zoom) {
if (zoom < 0 || zoom > 1f) {
throw new IllegalArgumentException("zoom must be between 0 and 1: " + zoom);
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index d8ec7cc..b494a20 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -39,6 +39,7 @@
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.annotation.StringDef;
+import android.annotation.SupportsCoexistence;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.SystemService;
@@ -8596,6 +8597,7 @@
* primary user, or a profile owner of an organization-owned managed profile or a holder of the
* permission {@link android.Manifest.permission#SET_TIME_ZONE}.
*/
+ @SupportsCoexistence
@RequiresPermission(value = SET_TIME_ZONE, conditional = true)
public void setAutoTimeZoneEnabled(@NonNull ComponentName admin, boolean enabled) {
throwIfParentInstance("setAutoTimeZone");
@@ -9665,6 +9667,7 @@
* @param activity The Activity that is added as default intent handler.
* @throws SecurityException if {@code admin} is not a device or profile owner.
*/
+ @SupportsCoexistence
public void addPersistentPreferredActivity(@NonNull ComponentName admin, IntentFilter filter,
@NonNull ComponentName activity) {
throwIfParentInstance("addPersistentPreferredActivity");
@@ -10624,6 +10627,7 @@
* profile owner of an organization-owned managed profile and the
* list of permitted input method package names is not null or empty.
*/
+ @SupportsCoexistence
public boolean setPermittedInputMethods(
@NonNull ComponentName admin, List<String> packageNames) {
if (mService != null) {
@@ -11696,6 +11700,7 @@
* @see DeviceAdminReceiver#onLockTaskModeExiting(Context, Intent)
* @see UserManager#DISALLOW_CREATE_WINDOWS
*/
+ @SupportsCoexistence
public void setLockTaskPackages(@NonNull ComponentName admin, @NonNull String[] packages)
throws SecurityException {
throwIfParentInstance("setLockTaskPackages");
@@ -11764,6 +11769,7 @@
* affiliated user or profile, or the profile owner when no device owner is set.
* @see #isAffiliatedUser
**/
+ @SupportsCoexistence
public void setLockTaskFeatures(@NonNull ComponentName admin, @LockTaskFeature int flags) {
throwIfParentInstance("setLockTaskFeatures");
if (mService != null) {
@@ -12267,6 +12273,7 @@
* @see #setDelegatedScopes
* @see #DELEGATION_BLOCK_UNINSTALL
*/
+ @SupportsCoexistence
public void setUninstallBlocked(@Nullable ComponentName admin, String packageName,
boolean uninstallBlocked) {
throwIfParentInstance("setUninstallBlocked");
@@ -12755,6 +12762,7 @@
* @see #setDelegatedScopes
* @see #DELEGATION_PERMISSION_GRANT
*/
+ @SupportsCoexistence
public boolean setPermissionGrantState(@NonNull ComponentName admin,
@NonNull String packageName, @NonNull String permission,
@PermissionGrantState int grantState) {
@@ -15301,6 +15309,7 @@
* @param packages The package names for the apps.
* @throws SecurityException if {@code admin} is not a device owner or a profile owner.
*/
+ @SupportsCoexistence
public void setUserControlDisabledPackages(@NonNull ComponentName admin,
@NonNull List<String> packages) {
throwIfParentInstance("setUserControlDisabledPackages");
diff --git a/core/java/android/app/admin/PreferentialNetworkServiceConfig.java b/core/java/android/app/admin/PreferentialNetworkServiceConfig.java
index 0513099..4c97efb 100644
--- a/core/java/android/app/admin/PreferentialNetworkServiceConfig.java
+++ b/core/java/android/app/admin/PreferentialNetworkServiceConfig.java
@@ -544,9 +544,9 @@
pw.print("blockNonMatchingNetworks=");
pw.println(mShouldBlockNonMatchingNetworks);
pw.print("includedUids=");
- pw.println(mIncludedUids);
+ pw.println(Arrays.toString(mIncludedUids));
pw.print("excludedUids=");
- pw.println(mExcludedUids);
+ pw.println(Arrays.toString(mExcludedUids));
}
@Override
diff --git a/core/java/android/app/admin/SystemUpdatePolicy.java b/core/java/android/app/admin/SystemUpdatePolicy.java
index b100eb2..c760298 100644
--- a/core/java/android/app/admin/SystemUpdatePolicy.java
+++ b/core/java/android/app/admin/SystemUpdatePolicy.java
@@ -50,8 +50,8 @@
/**
* Determines when over-the-air system updates are installed on a device. Only a device policy
- * controller (DPC) running in device owner mode can set an update policy for the device—by calling
- * the {@code DevicePolicyManager} method
+ * controller (DPC) running in device owner mode or in profile owner mode for an organization-owned
+ * device can set an update policy for the device—by calling the {@code DevicePolicyManager} method
* {@link DevicePolicyManager#setSystemUpdatePolicy setSystemUpdatePolicy()}. An update
* policy affects the pending system update (if there is one) and any future updates for the device.
*
diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java
index e323e89..909073e 100644
--- a/core/java/android/app/backup/BackupAgent.java
+++ b/core/java/android/app/backup/BackupAgent.java
@@ -265,15 +265,9 @@
public void onCreate() {
}
- /**
- * Provided as a convenience for agent implementations that need an opportunity
- * to do one-time initialization before the actual backup or restore operation
- * is begun with information about the calling user.
- * <p>
- *
- * @hide
- */
+ /** @hide */
public void onCreate(UserHandle user) {
+ mUser = user;
onCreate();
}
@@ -284,7 +278,6 @@
*/
@Deprecated
public void onCreate(UserHandle user, @BackupDestination int backupDestination) {
- mUser = user;
mBackupDestination = backupDestination;
onCreate(user);
@@ -295,7 +288,6 @@
*/
public void onCreate(UserHandle user, @BackupDestination int backupDestination,
@OperationType int operationType) {
- mUser = user;
mBackupDestination = backupDestination;
mLogger = new BackupRestoreEventLogger(operationType);
diff --git a/core/java/android/app/search/ISearchUiManager.aidl b/core/java/android/app/search/ISearchUiManager.aidl
index a298a2c..fefbd5a 100644
--- a/core/java/android/app/search/ISearchUiManager.aidl
+++ b/core/java/android/app/search/ISearchUiManager.aidl
@@ -36,5 +36,11 @@
void notifyEvent(in SearchSessionId sessionId, in Query input, in SearchTargetEvent event);
+ void registerEmptyQueryResultUpdateCallback(in SearchSessionId sessionId, in ISearchCallback callback);
+
+ void requestEmptyQueryResultUpdate(in SearchSessionId sessionId);
+
+ void unregisterEmptyQueryResultUpdateCallback(in SearchSessionId sessionId, in ISearchCallback callback);
+
void destroySearchSession(in SearchSessionId sessionId);
}
diff --git a/core/java/android/app/search/SearchSession.java b/core/java/android/app/search/SearchSession.java
index 10db337..9e0a1d0 100644
--- a/core/java/android/app/search/SearchSession.java
+++ b/core/java/android/app/search/SearchSession.java
@@ -28,8 +28,11 @@
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
+import android.util.ArrayMap;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
+
import dalvik.system.CloseGuard;
import java.util.List;
@@ -83,6 +86,8 @@
private final SearchSessionId mSessionId;
private final IBinder mToken = new Binder();
+ @GuardedBy("itself")
+ private final ArrayMap<Callback, CallbackWrapper> mRegisteredCallbacks = new ArrayMap<>();
/**
* Creates a new search ui client.
@@ -156,6 +161,94 @@
e.rethrowFromSystemServer();
}
}
+ /**
+ * Request the search ui service provide continuous updates of {@link SearchTarget} list
+ * via the provided callback to render for zero state, until the given callback is
+ * unregistered. Zero state means when user entered search ui but not issued any query yet.
+ *
+ * @see SearchSession.Callback#onTargetsAvailable(List).
+ *
+ * @param callbackExecutor The callback executor to use when calling the callback.
+ * @param callback The Callback to be called when updates of search targets for zero state
+ * are available.
+ */
+ public void registerEmptyQueryResultUpdateCallback(
+ @NonNull @CallbackExecutor Executor callbackExecutor,
+ @NonNull Callback callback) {
+ synchronized (mRegisteredCallbacks) {
+ if (mIsClosed.get()) {
+ throw new IllegalStateException("This client has already been destroyed.");
+ }
+ if (mRegisteredCallbacks.containsKey(callback)) {
+ // Skip if this callback is already registered
+ return;
+ }
+ try {
+ final CallbackWrapper callbackWrapper = new CallbackWrapper(callbackExecutor,
+ callback::onTargetsAvailable);
+ mInterface.registerEmptyQueryResultUpdateCallback(mSessionId, callbackWrapper);
+ mRegisteredCallbacks.put(callback, callbackWrapper);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to register for empty query result updates", e);
+ e.rethrowAsRuntimeException();
+ }
+ }
+ }
+
+ /**
+ * Requests the search ui service to stop providing continuous updates of {@link SearchTarget}
+ * to the provided callback for zero state until the callback is re-registered. Zero state
+ * means when user entered search ui but not issued any query yet.
+ *
+ * @see {@link SearchSession#registerEmptyQueryResultUpdateCallback(Executor, Callback)}
+ * @param callback The callback to be unregistered.
+ */
+ public void unregisterEmptyQueryResultUpdateCallback(
+ @NonNull Callback callback) {
+ synchronized (mRegisteredCallbacks) {
+ if (mIsClosed.get()) {
+ throw new IllegalStateException("This client has already been destroyed.");
+ }
+
+ if (!mRegisteredCallbacks.containsKey(callback)) {
+ // Skip if this callback was never registered
+ return;
+ }
+ try {
+ final CallbackWrapper callbackWrapper = mRegisteredCallbacks.remove(callback);
+ mInterface.unregisterEmptyQueryResultUpdateCallback(mSessionId, callbackWrapper);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to unregister for empty query result updates", e);
+ e.rethrowAsRuntimeException();
+ }
+ }
+ }
+
+ /**
+ * Requests the search ui service to dispatch a new set of search targets to the pre-registered
+ * callback for zero state. Zero state means when user entered search ui but not issued any
+ * query yet. This method can be used for client to sync up with server data if they think data
+ * might be out of sync, for example, after restart.
+ * Pre-register a callback with
+ * {@link SearchSession#registerEmptyQueryResultUpdateCallback(Executor, Callback)}
+ * is required before calling this method. Otherwise this is no-op.
+ *
+ * @see {@link SearchSession#registerEmptyQueryResultUpdateCallback(Executor, Callback)}
+ * @see {@link SearchSession.Callback#onTargetsAvailable(List)}.
+ */
+ public void requestEmptyQueryResultUpdate() {
+ if (mIsClosed.get()) {
+ throw new IllegalStateException("This client has already been destroyed.");
+ }
+
+ try {
+ mInterface.requestEmptyQueryResultUpdate(mSessionId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to request empty query result update", e);
+ e.rethrowAsRuntimeException();
+ }
+ }
+
/**
* Destroys the client and unregisters the callback. Any method on this class after this call
@@ -213,6 +306,19 @@
}
}
+ /**
+ * Callback for receiving {@link SearchTarget} updates for zero state. Zero state
+ * means when user entered search ui but not issued any query yet.
+ */
+ public interface Callback {
+
+ /**
+ * Called when a new set of {@link SearchTarget} are available for zero state.
+ * @param targets Sorted list of search targets.
+ */
+ void onTargetsAvailable(@NonNull List<SearchTarget> targets);
+ }
+
static class CallbackWrapper extends Stub {
private final Consumer<List<SearchTarget>> mCallback;
diff --git a/core/java/android/content/BroadcastReceiver.java b/core/java/android/content/BroadcastReceiver.java
index 64dcc4d..3d76b28 100644
--- a/core/java/android/content/BroadcastReceiver.java
+++ b/core/java/android/content/BroadcastReceiver.java
@@ -17,6 +17,7 @@
package android.content;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.app.ActivityManager;
import android.app.ActivityThread;
@@ -26,6 +27,7 @@
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
+import android.os.Process;
import android.os.RemoteException;
import android.os.Trace;
import android.os.UserHandle;
@@ -101,19 +103,22 @@
@UnsupportedAppUsage
boolean mFinished;
String mReceiverClassName;
+ final int mSentFromUid;
+ final String mSentFromPackage;
/** @hide */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public PendingResult(int resultCode, String resultData, Bundle resultExtras, int type,
boolean ordered, boolean sticky, IBinder token, int userId, int flags) {
this(resultCode, resultData, resultExtras, type, ordered, sticky,
- guessAssumeDelivered(type, ordered), token, userId, flags);
+ guessAssumeDelivered(type, ordered), token, userId, flags,
+ Process.INVALID_UID, null);
}
/** @hide */
public PendingResult(int resultCode, String resultData, Bundle resultExtras, int type,
boolean ordered, boolean sticky, boolean assumeDelivered, IBinder token,
- int userId, int flags) {
+ int userId, int flags, int sentFromUid, String sentFromPackage) {
mResultCode = resultCode;
mResultData = resultData;
mResultExtras = resultExtras;
@@ -124,6 +129,8 @@
mToken = token;
mSendingUser = userId;
mFlags = flags;
+ mSentFromUid = sentFromUid;
+ mSentFromPackage = sentFromPackage;
}
/** @hide */
@@ -322,6 +329,16 @@
return mSendingUser;
}
+ /** @hide */
+ public int getSentFromUid() {
+ return mSentFromUid;
+ }
+
+ /** @hide */
+ public String getSentFromPackage() {
+ return mSentFromPackage;
+ }
+
void checkSynchronousHint() {
// Note that we don't assert when receiving the initial sticky value,
// since that may have come from an ordered broadcast. We'll catch
@@ -687,6 +704,26 @@
}
/**
+ * Returns the uid of the app that initially sent this broadcast.
+ *
+ * @return the uid of the broadcasting app or {@link Process#INVALID_UID} if the current
+ * receiver cannot access the identity of the broadcasting app
+ */
+ public int getSentFromUid() {
+ return mPendingResult != null ? mPendingResult.mSentFromUid : Process.INVALID_UID;
+ }
+
+ /**
+ * Returns the package name of the app that initially sent this broadcast.
+ *
+ * @return the package name of the broadcasting app or {@code null} if the current
+ * receiver cannot access the identity of the broadcasting app
+ */
+ public @Nullable String getSentFromPackage() {
+ return mPendingResult != null ? mPendingResult.mSentFromPackage : null;
+ }
+
+ /**
* Control inclusion of debugging help for mismatched
* calls to {@link Context#registerReceiver(BroadcastReceiver, IntentFilter)
* Context.registerReceiver()}.
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 5f1502f..6aa7f3f 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -2445,13 +2445,12 @@
* @see #sendBroadcast(Intent)
* @see #sendOrderedBroadcast(Intent, String)
* @see #sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle)
- * @hide
*/
- @SuppressWarnings("HiddenAbstractMethod")
- @SystemApi
- public abstract void sendBroadcast(Intent intent,
+ public void sendBroadcast(@NonNull Intent intent,
@Nullable String receiverPermission,
- @Nullable Bundle options);
+ @Nullable Bundle options) {
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
/**
* Like {@link #sendBroadcast(Intent, String)}, but also allows specification
@@ -2487,6 +2486,32 @@
@Nullable String receiverPermission);
/**
+ * Broadcast the given intent to all interested BroadcastReceivers, delivering
+ * them one at a time to allow more preferred receivers to consume the
+ * broadcast before it is delivered to less preferred receivers. This
+ * call is asynchronous; it returns immediately, and you will continue
+ * executing while the receivers are run.
+ *
+ * <p>See {@link BroadcastReceiver} for more information on Intent broadcasts.
+ *
+ * @param intent The Intent to broadcast; all receivers matching this
+ * Intent will receive the broadcast.
+ * @param receiverPermission (optional) String naming a permissions that
+ * a receiver must hold in order to receive your broadcast.
+ * If null, no permission is required.
+ * @param options (optional) Additional sending options, generated from a
+ * {@link android.app.BroadcastOptions}.
+ * @see android.content.BroadcastReceiver
+ * @see #registerReceiver
+ * @see #sendBroadcast(Intent)
+ * @see #sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle)
+ */
+ public void sendOrderedBroadcast(@NonNull Intent intent, @Nullable String receiverPermission,
+ @Nullable Bundle options) {
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+
+ /**
* Version of {@link #sendBroadcast(Intent)} that allows you to
* receive data back from the broadcast. This is accomplished by
* supplying your own BroadcastReceiver when calling, which will be
@@ -2572,14 +2597,13 @@
* @see android.content.BroadcastReceiver
* @see #registerReceiver
* @see android.app.Activity#RESULT_OK
- * @hide
*/
- @SuppressWarnings("HiddenAbstractMethod")
- @SystemApi
- public abstract void sendOrderedBroadcast(@NonNull Intent intent,
+ public void sendOrderedBroadcast(@NonNull Intent intent,
@Nullable String receiverPermission, @Nullable Bundle options,
@Nullable BroadcastReceiver resultReceiver, @Nullable Handler scheduler,
- int initialCode, @Nullable String initialData, @Nullable Bundle initialExtras);
+ int initialCode, @Nullable String initialData, @Nullable Bundle initialExtras) {
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
/**
* Like {@link #sendOrderedBroadcast(Intent, String, BroadcastReceiver, android.os.Handler,
@@ -6151,10 +6175,10 @@
/**
* Use with {@link #getSystemService(String)} to retrieve a
- * {@link android.healthconnect.HealthConnectManager}.
+ * {@link android.health.connect.HealthConnectManager}.
*
* @see #getSystemService(String)
- * @see android.healthconnect.HealthConnectManager
+ * @see android.health.connect.HealthConnectManager
*/
public static final String HEALTHCONNECT_SERVICE = "healthconnect";
@@ -6198,12 +6222,22 @@
public static final String GRAMMATICAL_INFLECTION_SERVICE = "grammatical_inflection";
/**
+ * Use with {@link #getSystemService(String)} to retrieve a
+ * {@link android.telephony.satellite.SatelliteManager} for accessing satellite functionality.
+ *
+ * @see #getSystemService(String)
+ * @see android.telephony.satellite.SatelliteManager
+ * @hide
+ */
+ public static final String SATELLITE_SERVICE = "satellite";
+
+ /**
* Determine whether the given permission is allowed for a particular
* process and user ID running in the system.
*
* @param permission The name of the permission being checked.
* @param pid The process ID being checked against. Must be > 0.
- * @param uid The user ID being checked against. A uid of 0 is the root
+ * @param uid The UID being checked against. A uid of 0 is the root
* user, which will pass every permission check.
*
* @return {@link PackageManager#PERMISSION_GRANTED} if the given
@@ -6293,7 +6327,7 @@
*
* @param permission The name of the permission being checked.
* @param pid The process ID being checked against. Must be > 0.
- * @param uid The user ID being checked against. A uid of 0 is the root
+ * @param uid The UID being checked against. A uid of 0 is the root
* user, which will pass every permission check.
* @param message A message to include in the exception if it is thrown.
*
@@ -6437,7 +6471,7 @@
*
* @param uri The uri that is being checked.
* @param pid The process ID being checked against. Must be > 0.
- * @param uid The user ID being checked against. A uid of 0 is the root
+ * @param uid The UID being checked against. A uid of 0 is the root
* user, which will pass every permission check.
* @param modeFlags The access modes to check.
*
@@ -6465,7 +6499,7 @@
*
* @param uris The list of URIs that is being checked.
* @param pid The process ID being checked against. Must be > 0.
- * @param uid The user ID being checked against. A uid of 0 is the root
+ * @param uid The UID being checked against. A uid of 0 is the root
* user, which will pass every permission check.
* @param modeFlags The access modes to check for the list of uris
*
@@ -6591,7 +6625,7 @@
* @param writePermission The permission that provides overall write
* access, or null to not do this check.
* @param pid The process ID being checked against. Must be > 0.
- * @param uid The user ID being checked against. A uid of 0 is the root
+ * @param uid The UID being checked against. A uid of 0 is the root
* user, which will pass every permission check.
* @param modeFlags The access modes to check.
*
@@ -6615,7 +6649,7 @@
*
* @param uri The uri that is being checked.
* @param pid The process ID being checked against. Must be > 0.
- * @param uid The user ID being checked against. A uid of 0 is the root
+ * @param uid The UID being checked against. A uid of 0 is the root
* user, which will pass every permission check.
* @param modeFlags The access modes to enforce.
* @param message A message to include in the exception if it is thrown.
@@ -6674,7 +6708,7 @@
* @param writePermission The permission that provides overall write
* access, or null to not do this check.
* @param pid The process ID being checked against. Must be > 0.
- * @param uid The user ID being checked against. A uid of 0 is the root
+ * @param uid The UID being checked against. A uid of 0 is the root
* user, which will pass every permission check.
* @param modeFlags The access modes to enforce.
* @param message A message to include in the exception if it is thrown.
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
index 9027e2e..a103128 100644
--- a/core/java/android/content/ContextWrapper.java
+++ b/core/java/android/content/ContextWrapper.java
@@ -20,6 +20,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.annotation.UiContext;
@@ -537,10 +538,8 @@
mBase.sendBroadcastAsUserMultiplePermissions(intent, user, receiverPermissions);
}
- /** @hide */
- @SystemApi
@Override
- public void sendBroadcast(Intent intent, @Nullable String receiverPermission,
+ public void sendBroadcast(@NonNull Intent intent, @Nullable String receiverPermission,
@Nullable Bundle options) {
mBase.sendBroadcast(intent, receiverPermission, options);
}
@@ -557,6 +556,14 @@
mBase.sendOrderedBroadcast(intent, receiverPermission);
}
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ @Override
+ public void sendOrderedBroadcast(@NonNull Intent intent,
+ @Nullable String receiverPermission,
+ @Nullable Bundle options) {
+ mBase.sendOrderedBroadcast(intent, receiverPermission, options);
+ }
+
@Override
public void sendOrderedBroadcast(
Intent intent, @Nullable String receiverPermission,
@@ -567,11 +574,9 @@
initialData, initialExtras);
}
- /** @hide */
- @SystemApi
@Override
public void sendOrderedBroadcast(
- Intent intent, @Nullable String receiverPermission, @Nullable Bundle options,
+ @NonNull Intent intent, @Nullable String receiverPermission, @Nullable Bundle options,
@Nullable BroadcastReceiver resultReceiver, @Nullable Handler scheduler,
int initialCode, @Nullable String initialData, @Nullable Bundle initialExtras) {
mBase.sendOrderedBroadcast(intent, receiverPermission,
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 5714032..d7ab6d7 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -31,8 +31,10 @@
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
+import android.app.Activity;
import android.app.ActivityThread;
import android.app.AppGlobals;
+import android.app.StatusBarManager;
import android.bluetooth.BluetoothDevice;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.pm.ActivityInfo;
@@ -5137,6 +5139,86 @@
public static final String EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE";
/**
+ * Activity Action: Use with startActivityForResult to start a system activity that captures
+ * content on the screen to take a screenshot and present it to the user for editing. The
+ * edited screenshot is saved on device and returned to the calling activity as a {@link Uri}
+ * through {@link #getData()}. User interaction is required to return the edited screenshot to
+ * the calling activity.
+ *
+ * <p>This intent action requires the permission
+ * {@link android.Manifest.permission#LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE}.
+ *
+ * <p>Callers should query
+ * {@link StatusBarManager#canLaunchCaptureContentActivityForNote(Activity)} before showing a UI
+ * element that allows users to trigger this flow.
+ */
+ @RequiresPermission(Manifest.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE)
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE =
+ "android.intent.action.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE";
+
+ /**
+ * An int extra used by activity started with
+ * {@link #ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE} to indicate status of the response.
+ * This extra is used along with result code set to {@link android.app.Activity#RESULT_OK}.
+ *
+ * <p>The value for this extra can be one of the following:
+ * <ul>
+ * <li>{@link #CAPTURE_CONTENT_FOR_NOTE_SUCCESS}</li>
+ * <li>{@link #CAPTURE_CONTENT_FOR_NOTE_FAILED}</li>
+ * <li>{@link #CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED}</li>
+ * <li>{@link #CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED}</li>
+ * <li>{@link #CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN}</li>
+ * </ul>
+ */
+ public static final String EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE =
+ "android.intent.extra.CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE";
+
+ /**
+ * A response code used with {@link #EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE} to indicate
+ * that the request was a success.
+ *
+ * <p>This code will only be returned after the user has interacted with the system screenshot
+ * activity to consent to sharing the data with the note.
+ *
+ * <p>The captured screenshot is returned as a {@link Uri} through {@link #getData()}.
+ */
+ public static final int CAPTURE_CONTENT_FOR_NOTE_SUCCESS = 0;
+
+ /**
+ * A response code used with {@link #EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE} to indicate
+ * that something went wrong.
+ */
+ public static final int CAPTURE_CONTENT_FOR_NOTE_FAILED = 1;
+
+ /**
+ * A response code used with {@link #EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE} to indicate
+ * that user canceled the content capture flow.
+ */
+ public static final int CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED = 2;
+
+ /**
+ * A response code used with {@link #EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE} to indicate
+ * that the intent action {@link #ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE} was started
+ * by an activity that is running in a non-supported window mode.
+ */
+ public static final int CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED = 3;
+
+ /**
+ * A response code used with {@link #EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE} to indicate
+ * that screenshot is blocked by IT admin.
+ */
+ public static final int CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN = 4;
+
+ /** @hide */
+ @IntDef(value = {
+ CAPTURE_CONTENT_FOR_NOTE_SUCCESS, CAPTURE_CONTENT_FOR_NOTE_FAILED,
+ CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED,
+ CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CaptureContentForNoteStatusCodes {}
+
+ /**
* Broadcast Action: Sent to the integrity component when a package
* needs to be verified. The data contains the package URI along with other relevant
* information.
@@ -5829,7 +5911,6 @@
* in the sharesheet.
* A reselection action allows the user to return to the source app to change the content being
* shared.
- * @hide
*/
public static final String EXTRA_CHOOSER_PAYLOAD_RESELECTION_ACTION =
"android.intent.extra.CHOOSER_PAYLOAD_RESELECTION_ACTION";
diff --git a/core/java/android/content/IntentSender.java b/core/java/android/content/IntentSender.java
index 8853b70..ccb53cf 100644
--- a/core/java/android/content/IntentSender.java
+++ b/core/java/android/content/IntentSender.java
@@ -18,9 +18,10 @@
import android.annotation.Nullable;
import android.app.ActivityManager;
+import android.app.ActivityManager.PendingIntentInfo;
+import android.app.ActivityOptions;
import android.app.ActivityThread;
import android.app.IApplicationThread;
-import android.app.ActivityManager.PendingIntentInfo;
import android.compat.annotation.UnsupportedAppUsage;
import android.os.Bundle;
import android.os.Handler;
@@ -160,7 +161,7 @@
*/
public void sendIntent(Context context, int code, Intent intent,
OnFinished onFinished, Handler handler) throws SendIntentException {
- sendIntent(context, code, intent, onFinished, handler, null);
+ sendIntent(context, code, intent, onFinished, handler, null, null /* options */);
}
/**
@@ -192,6 +193,42 @@
public void sendIntent(Context context, int code, Intent intent,
OnFinished onFinished, Handler handler, String requiredPermission)
throws SendIntentException {
+ sendIntent(context, code, intent, onFinished, handler, requiredPermission,
+ null /* options */);
+ }
+
+ /**
+ * Perform the operation associated with this IntentSender, allowing the
+ * caller to specify information about the Intent to use and be notified
+ * when the send has completed.
+ *
+ * @param context The Context of the caller. This may be null if
+ * <var>intent</var> is also null.
+ * @param code Result code to supply back to the IntentSender's target.
+ * @param intent Additional Intent data. See {@link Intent#fillIn
+ * Intent.fillIn()} for information on how this is applied to the
+ * original Intent. Use null to not modify the original Intent.
+ * @param onFinished The object to call back on when the send has
+ * completed, or null for no callback.
+ * @param handler Handler identifying the thread on which the callback
+ * should happen. If null, the callback will happen from the thread
+ * pool of the process.
+ * @param requiredPermission Name of permission that a recipient of the PendingIntent
+ * is required to hold. This is only valid for broadcast intents, and
+ * corresponds to the permission argument in
+ * {@link Context#sendBroadcast(Intent, String) Context.sendOrderedBroadcast(Intent, String)}.
+ * If null, no permission is required.
+ * @param options Additional options the caller would like to provide to modify the sending
+ * behavior. May be built from an {@link ActivityOptions} to apply to an activity start.
+ *
+ * @throws SendIntentException Throws CanceledIntentException if the IntentSender
+ * is no longer allowing more intents to be sent through it.
+ * @hide
+ */
+ public void sendIntent(Context context, int code, Intent intent,
+ OnFinished onFinished, Handler handler, String requiredPermission,
+ @Nullable Bundle options)
+ throws SendIntentException {
try {
String resolvedType = intent != null ?
intent.resolveTypeIfNeeded(context.getContentResolver())
@@ -203,7 +240,7 @@
onFinished != null
? new FinishedDispatcher(this, onFinished, handler)
: null,
- requiredPermission, null);
+ requiredPermission, options);
if (res < 0) {
throw new SendIntentException();
}
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 68b0631..db72e29 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -1103,6 +1103,17 @@
public static final long ALWAYS_SANDBOX_DISPLAY_APIS = 185004937L; // buganizer id
/**
+ * This change id excludes the packages it is applied to from ignoreOrientationRequest behaviour
+ * that can be enabled by the device manufacturers for the com.android.server.wm.DisplayArea
+ * or for the whole display.
+ * @hide
+ */
+ @ChangeId
+ @Overridable
+ @Disabled
+ public static final long OVERRIDE_RESPECT_REQUESTED_ORIENTATION = 236283604L; // buganizer id
+
+ /**
* This change id excludes the packages it is applied to from the camera compat force rotation
* treatment. See com.android.server.wm.DisplayRotationCompatPolicy for context.
* @hide
@@ -1285,6 +1296,18 @@
public static final long OVERRIDE_ANY_ORIENTATION = 265464455L;
/**
+ * When enabled, activates OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE,
+ * OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR and OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT
+ * only when an app is connected to the camera. See
+ * com.android.server.wm.DisplayRotationCompatPolicy for more context.
+ * @hide
+ */
+ @ChangeId
+ @Disabled
+ @Overridable
+ public static final long OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA = 265456536L;
+
+ /**
* This override fixes display orientation to landscape natural orientation when a task is
* fullscreen. While display rotation is fixed to landscape, the orientation requested by the
* activity will be still respected by bounds resolution logic. For instance, if an activity
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index e747a6f..45100be 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -1060,7 +1060,7 @@
PackageInstaller.EXTRA_INSTALL_CONSTRAINTS_RESULT,
InstallConstraintsResult.class);
try {
- if (result.isAllConstraintsSatisfied()) {
+ if (result.areAllConstraintsSatisfied()) {
session.commit(statusReceiver, false);
} else {
// timeout
@@ -2042,7 +2042,7 @@
*/
@SystemApi
@NonNull
- public InstallInfo getInstallInfo(@NonNull File file, int flags)
+ public InstallInfo readInstallInfo(@NonNull File file, int flags)
throws PackageParsingException {
final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
final ParseResult<PackageLite> result = ApkLiteParseUtils.parsePackageLite(
@@ -2616,8 +2616,17 @@
installFlags |= PackageManager.INSTALL_FORCE_PERMISSION_PROMPT;
}
- /** {@hide} */
- @SystemApi
+ /**
+ * Requests that the system not kill any of the package's running
+ * processes as part of a {@link SessionParams#MODE_INHERIT_EXISTING}
+ * session in which splits being added. By default, all installs will
+ * result in the package's running processes being killed before the
+ * install completes.
+ *
+ * @param dontKillApp set to {@code true} to request that the processes
+ * belonging to the package not be killed as part of
+ * this install.
+ */
public void setDontKillApp(boolean dontKillApp) {
if (dontKillApp) {
installFlags |= PackageManager.INSTALL_DONT_KILL_APP;
@@ -3456,10 +3465,7 @@
/**
* Get the value set in {@link SessionParams#setDontKillApp(boolean)}.
- *
- * @hide
*/
- @SystemApi
public boolean getDontKillApp() {
return (installFlags & PackageManager.INSTALL_DONT_KILL_APP) != 0;
}
@@ -3740,7 +3746,7 @@
/**
* Returns whether this session has requested user pre-approval.
*/
- public @NonNull boolean getIsPreApprovalRequested() {
+ public boolean isPreApprovalRequested() {
return isPreapprovalRequested;
}
@@ -4110,12 +4116,20 @@
* The callback result of {@link #checkInstallConstraints(List, InstallConstraints, Executor, Consumer)}.
*/
@DataClass(genParcelable = true, genHiddenConstructor = true)
+ @DataClass.Suppress("isAllConstraintsSatisfied")
public static final class InstallConstraintsResult implements Parcelable {
/**
* True if all constraints are satisfied.
*/
private boolean mAllConstraintsSatisfied;
+ /**
+ * True if all constraints are satisfied.
+ */
+ public boolean areAllConstraintsSatisfied() {
+ return mAllConstraintsSatisfied;
+ }
+
// Code below generated by codegen v1.0.23.
@@ -4146,14 +4160,6 @@
// onConstructed(); // You can define this method to get a callback
}
- /**
- * True if all constraints are satisfied.
- */
- @DataClass.Generated.Member
- public boolean isAllConstraintsSatisfied() {
- return mAllConstraintsSatisfied;
- }
-
@Override
@DataClass.Generated.Member
public void writeToParcel(@NonNull Parcel dest, int flags) {
@@ -4199,10 +4205,10 @@
};
@DataClass.Generated(
- time = 1668650523745L,
+ time = 1675135664641L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/content/pm/PackageInstaller.java",
- inputSignatures = "private boolean mAllConstraintsSatisfied\nclass InstallConstraintsResult extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genHiddenConstructor=true)")
+ inputSignatures = "private boolean mAllConstraintsSatisfied\npublic boolean areAllConstraintsSatisfied()\nclass InstallConstraintsResult extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genHiddenConstructor=true)")
@Deprecated
private void __metadata() {}
@@ -4241,41 +4247,41 @@
*/
@NonNull
public static final InstallConstraints GENTLE_UPDATE =
- new Builder().requireAppNotInteracting().build();
+ new Builder().setAppNotInteractingRequired().build();
- private final boolean mRequireDeviceIdle;
- private final boolean mRequireAppNotForeground;
- private final boolean mRequireAppNotInteracting;
- private final boolean mRequireAppNotTopVisible;
- private final boolean mRequireNotInCall;
+ private final boolean mDeviceIdleRequired;
+ private final boolean mAppNotForegroundRequired;
+ private final boolean mAppNotInteractingRequired;
+ private final boolean mAppNotTopVisibleRequired;
+ private final boolean mNotInCallRequired;
/**
* Builder class for constructing {@link InstallConstraints}.
*/
public static final class Builder {
- private boolean mRequireDeviceIdle;
- private boolean mRequireAppNotForeground;
- private boolean mRequireAppNotInteracting;
- private boolean mRequireAppNotTopVisible;
- private boolean mRequireNotInCall;
+ private boolean mDeviceIdleRequired;
+ private boolean mAppNotForegroundRequired;
+ private boolean mAppNotInteractingRequired;
+ private boolean mAppNotTopVisibleRequired;
+ private boolean mNotInCallRequired;
/**
* This constraint requires the device is idle.
*/
- @SuppressLint("BuilderSetStyle")
+ @SuppressLint("MissingGetterMatchingBuilder")
@NonNull
- public Builder requireDeviceIdle() {
- mRequireDeviceIdle = true;
+ public Builder setDeviceIdleRequired() {
+ mDeviceIdleRequired = true;
return this;
}
/**
* This constraint requires the app in question is not in the foreground.
*/
- @SuppressLint("BuilderSetStyle")
+ @SuppressLint("MissingGetterMatchingBuilder")
@NonNull
- public Builder requireAppNotForeground() {
- mRequireAppNotForeground = true;
+ public Builder setAppNotForegroundRequired() {
+ mAppNotForegroundRequired = true;
return this;
}
@@ -4288,10 +4294,10 @@
* <li>being visible to the user</li>
* </ul>
*/
- @SuppressLint("BuilderSetStyle")
+ @SuppressLint("MissingGetterMatchingBuilder")
@NonNull
- public Builder requireAppNotInteracting() {
- mRequireAppNotInteracting = true;
+ public Builder setAppNotInteractingRequired() {
+ mAppNotInteractingRequired = true;
return this;
}
@@ -4300,25 +4306,27 @@
* A top-visible app is showing UI at the top of the screen that the user is
* interacting with.
*
- * Note this constraint is a subset of {@link #requireAppNotForeground()}
+ * Note this constraint is a subset of {@link #setAppNotForegroundRequired()}
* because a top-visible app is also a foreground app. This is also a subset
- * of {@link #requireAppNotInteracting()} because a top-visible app is interacting
+ * of {@link #setAppNotInteractingRequired()} because a top-visible app is interacting
* with the user.
+ *
+ * @see ActivityManager.RunningAppProcessInfo#IMPORTANCE_FOREGROUND
*/
- @SuppressLint("BuilderSetStyle")
+ @SuppressLint("MissingGetterMatchingBuilder")
@NonNull
- public Builder requireAppNotTopVisible() {
- mRequireAppNotTopVisible = true;
+ public Builder setAppNotTopVisibleRequired() {
+ mAppNotTopVisibleRequired = true;
return this;
}
/**
* This constraint requires there is no ongoing call in the device.
*/
- @SuppressLint("BuilderSetStyle")
+ @SuppressLint("MissingGetterMatchingBuilder")
@NonNull
- public Builder requireNotInCall() {
- mRequireNotInCall = true;
+ public Builder setNotInCallRequired() {
+ mNotInCallRequired = true;
return this;
}
@@ -4327,8 +4335,8 @@
*/
@NonNull
public InstallConstraints build() {
- return new InstallConstraints(mRequireDeviceIdle, mRequireAppNotForeground,
- mRequireAppNotInteracting, mRequireAppNotTopVisible, mRequireNotInCall);
+ return new InstallConstraints(mDeviceIdleRequired, mAppNotForegroundRequired,
+ mAppNotInteractingRequired, mAppNotTopVisibleRequired, mNotInCallRequired);
}
}
@@ -4354,43 +4362,43 @@
*/
@DataClass.Generated.Member
public InstallConstraints(
- boolean requireDeviceIdle,
- boolean requireAppNotForeground,
- boolean requireAppNotInteracting,
- boolean requireAppNotTopVisible,
- boolean requireNotInCall) {
- this.mRequireDeviceIdle = requireDeviceIdle;
- this.mRequireAppNotForeground = requireAppNotForeground;
- this.mRequireAppNotInteracting = requireAppNotInteracting;
- this.mRequireAppNotTopVisible = requireAppNotTopVisible;
- this.mRequireNotInCall = requireNotInCall;
+ boolean deviceIdleRequired,
+ boolean appNotForegroundRequired,
+ boolean appNotInteractingRequired,
+ boolean appNotTopVisibleRequired,
+ boolean notInCallRequired) {
+ this.mDeviceIdleRequired = deviceIdleRequired;
+ this.mAppNotForegroundRequired = appNotForegroundRequired;
+ this.mAppNotInteractingRequired = appNotInteractingRequired;
+ this.mAppNotTopVisibleRequired = appNotTopVisibleRequired;
+ this.mNotInCallRequired = notInCallRequired;
// onConstructed(); // You can define this method to get a callback
}
@DataClass.Generated.Member
- public boolean isRequireDeviceIdle() {
- return mRequireDeviceIdle;
+ public boolean isDeviceIdleRequired() {
+ return mDeviceIdleRequired;
}
@DataClass.Generated.Member
- public boolean isRequireAppNotForeground() {
- return mRequireAppNotForeground;
+ public boolean isAppNotForegroundRequired() {
+ return mAppNotForegroundRequired;
}
@DataClass.Generated.Member
- public boolean isRequireAppNotInteracting() {
- return mRequireAppNotInteracting;
+ public boolean isAppNotInteractingRequired() {
+ return mAppNotInteractingRequired;
}
@DataClass.Generated.Member
- public boolean isRequireAppNotTopVisible() {
- return mRequireAppNotTopVisible;
+ public boolean isAppNotTopVisibleRequired() {
+ return mAppNotTopVisibleRequired;
}
@DataClass.Generated.Member
- public boolean isRequireNotInCall() {
- return mRequireNotInCall;
+ public boolean isNotInCallRequired() {
+ return mNotInCallRequired;
}
@Override
@@ -4406,11 +4414,11 @@
InstallConstraints that = (InstallConstraints) o;
//noinspection PointlessBooleanExpression
return true
- && mRequireDeviceIdle == that.mRequireDeviceIdle
- && mRequireAppNotForeground == that.mRequireAppNotForeground
- && mRequireAppNotInteracting == that.mRequireAppNotInteracting
- && mRequireAppNotTopVisible == that.mRequireAppNotTopVisible
- && mRequireNotInCall == that.mRequireNotInCall;
+ && mDeviceIdleRequired == that.mDeviceIdleRequired
+ && mAppNotForegroundRequired == that.mAppNotForegroundRequired
+ && mAppNotInteractingRequired == that.mAppNotInteractingRequired
+ && mAppNotTopVisibleRequired == that.mAppNotTopVisibleRequired
+ && mNotInCallRequired == that.mNotInCallRequired;
}
@Override
@@ -4420,11 +4428,11 @@
// int fieldNameHashCode() { ... }
int _hash = 1;
- _hash = 31 * _hash + Boolean.hashCode(mRequireDeviceIdle);
- _hash = 31 * _hash + Boolean.hashCode(mRequireAppNotForeground);
- _hash = 31 * _hash + Boolean.hashCode(mRequireAppNotInteracting);
- _hash = 31 * _hash + Boolean.hashCode(mRequireAppNotTopVisible);
- _hash = 31 * _hash + Boolean.hashCode(mRequireNotInCall);
+ _hash = 31 * _hash + Boolean.hashCode(mDeviceIdleRequired);
+ _hash = 31 * _hash + Boolean.hashCode(mAppNotForegroundRequired);
+ _hash = 31 * _hash + Boolean.hashCode(mAppNotInteractingRequired);
+ _hash = 31 * _hash + Boolean.hashCode(mAppNotTopVisibleRequired);
+ _hash = 31 * _hash + Boolean.hashCode(mNotInCallRequired);
return _hash;
}
@@ -4435,11 +4443,11 @@
// void parcelFieldName(Parcel dest, int flags) { ... }
byte flg = 0;
- if (mRequireDeviceIdle) flg |= 0x1;
- if (mRequireAppNotForeground) flg |= 0x2;
- if (mRequireAppNotInteracting) flg |= 0x4;
- if (mRequireAppNotTopVisible) flg |= 0x8;
- if (mRequireNotInCall) flg |= 0x10;
+ if (mDeviceIdleRequired) flg |= 0x1;
+ if (mAppNotForegroundRequired) flg |= 0x2;
+ if (mAppNotInteractingRequired) flg |= 0x4;
+ if (mAppNotTopVisibleRequired) flg |= 0x8;
+ if (mNotInCallRequired) flg |= 0x10;
dest.writeByte(flg);
}
@@ -4455,17 +4463,17 @@
// static FieldType unparcelFieldName(Parcel in) { ... }
byte flg = in.readByte();
- boolean requireDeviceIdle = (flg & 0x1) != 0;
- boolean requireAppNotForeground = (flg & 0x2) != 0;
- boolean requireAppNotInteracting = (flg & 0x4) != 0;
- boolean requireAppNotTopVisible = (flg & 0x8) != 0;
- boolean requireNotInCall = (flg & 0x10) != 0;
+ boolean deviceIdleRequired = (flg & 0x1) != 0;
+ boolean appNotForegroundRequired = (flg & 0x2) != 0;
+ boolean appNotInteractingRequired = (flg & 0x4) != 0;
+ boolean appNotTopVisibleRequired = (flg & 0x8) != 0;
+ boolean notInCallRequired = (flg & 0x10) != 0;
- this.mRequireDeviceIdle = requireDeviceIdle;
- this.mRequireAppNotForeground = requireAppNotForeground;
- this.mRequireAppNotInteracting = requireAppNotInteracting;
- this.mRequireAppNotTopVisible = requireAppNotTopVisible;
- this.mRequireNotInCall = requireNotInCall;
+ this.mDeviceIdleRequired = deviceIdleRequired;
+ this.mAppNotForegroundRequired = appNotForegroundRequired;
+ this.mAppNotInteractingRequired = appNotInteractingRequired;
+ this.mAppNotTopVisibleRequired = appNotTopVisibleRequired;
+ this.mNotInCallRequired = notInCallRequired;
// onConstructed(); // You can define this method to get a callback
}
@@ -4485,10 +4493,10 @@
};
@DataClass.Generated(
- time = 1670207178734L,
+ time = 1675135664653L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/content/pm/PackageInstaller.java",
- inputSignatures = "public static final @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints GENTLE_UPDATE\nprivate final boolean mRequireDeviceIdle\nprivate final boolean mRequireAppNotForeground\nprivate final boolean mRequireAppNotInteracting\nprivate final boolean mRequireAppNotTopVisible\nprivate final boolean mRequireNotInCall\nclass InstallConstraints extends java.lang.Object implements [android.os.Parcelable]\nprivate boolean mRequireDeviceIdle\nprivate boolean mRequireAppNotForeground\nprivate boolean mRequireAppNotInteracting\nprivate boolean mRequireAppNotTopVisible\nprivate boolean mRequireNotInCall\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireDeviceIdle()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotForeground()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotInteracting()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotTopVisible()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireNotInCall()\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints build()\nclass Builder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genParcelable=true, genHiddenConstructor=true, genEqualsHashCode=true)")
+ inputSignatures = "public static final @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints GENTLE_UPDATE\nprivate final boolean mDeviceIdleRequired\nprivate final boolean mAppNotForegroundRequired\nprivate final boolean mAppNotInteractingRequired\nprivate final boolean mAppNotTopVisibleRequired\nprivate final boolean mNotInCallRequired\nclass InstallConstraints extends java.lang.Object implements [android.os.Parcelable]\nprivate boolean mDeviceIdleRequired\nprivate boolean mAppNotForegroundRequired\nprivate boolean mAppNotInteractingRequired\nprivate boolean mAppNotTopVisibleRequired\nprivate boolean mNotInCallRequired\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setDeviceIdleRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setAppNotForegroundRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setAppNotInteractingRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setAppNotTopVisibleRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setNotInCallRequired()\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints build()\nclass Builder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genParcelable=true, genHiddenConstructor=true, genEqualsHashCode=true)")
@Deprecated
private void __metadata() {}
diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java
index f3209f9..49d21da 100644
--- a/core/java/android/content/pm/ServiceInfo.java
+++ b/core/java/android/content/pm/ServiceInfo.java
@@ -401,8 +401,9 @@
*
* <p>If the service isn't stopped within the timeout,
* {@link android.app.Service#onTimeout(int)} will be called.
- * If the service is still not stopped after the callback,
- * the app will be declared an ANR.
+ *
+ * <p>If the service is still not stopped after the callback,
+ * the app will be declared an ANR after a short grace period of several seconds.
*
* <li>
* A foreground service of this type cannot be made "sticky"
@@ -419,6 +420,17 @@
* </a>
* </ul>
*
+ * <p>Note, even though
+ * {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SHORT_SERVICE}
+ * was added
+ * on Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * it can be also used on
+ * on prior android versions (just like other new foreground service types can be used).
+ * However, because {@link android.app.Service#onTimeout(int)} did not exist on prior versions,
+ * it will never called on such versions.
+ * Because of this, developers must make sure to stop the foreground service even if
+ * {@link android.app.Service#onTimeout(int)} is not called on such versions.
+ *
* @see android.app.Service#onTimeout(int)
*/
public static final int FOREGROUND_SERVICE_TYPE_SHORT_SERVICE = 1 << 11;
diff --git a/core/java/android/credentials/GetCredentialOption.java b/core/java/android/credentials/CredentialOption.java
similarity index 79%
rename from core/java/android/credentials/GetCredentialOption.java
rename to core/java/android/credentials/CredentialOption.java
index f2895c7..9a3b46d 100644
--- a/core/java/android/credentials/GetCredentialOption.java
+++ b/core/java/android/credentials/CredentialOption.java
@@ -27,9 +27,11 @@
import com.android.internal.util.Preconditions;
/**
- * A specific type of credential request.
+ * Information about a specific type of credential to be requested during a {@link
+ * CredentialManager#getCredential(GetCredentialRequest, Activity, CancellationSignal, Executor,
+ * OutcomeReceiver)} operation.
*/
-public final class GetCredentialOption implements Parcelable {
+public final class CredentialOption implements Parcelable {
/**
* Bundle key to the flattened version of the JSON request string. Framework will use this key
@@ -118,7 +120,7 @@
@Override
public String toString() {
- return "GetCredentialOption {"
+ return "CredentialOption {"
+ "type=" + mType
+ ", requestData=" + mCredentialRetrievalData
+ ", candidateQueryData=" + mCandidateQueryData
@@ -127,17 +129,17 @@
}
/**
- * Constructs a {@link GetCredentialOption}.
+ * Constructs a {@link CredentialOption}.
*
- * @param type the requested credential type
- * @param credentialRetrievalData the request data
- * @param candidateQueryData the partial request data that will be sent to the provider
- * during the initial credential candidate query stage
- * @param isSystemProviderRequired whether the request must only be fulfilled by a system
- * provider
+ * @param type the requested credential type
+ * @param credentialRetrievalData the request data
+ * @param candidateQueryData the partial request data that will be sent to the provider
+ * during the initial credential candidate query stage
+ * @param isSystemProviderRequired whether the request must only be fulfilled by a system
+ * provider
* @throws IllegalArgumentException If type is empty.
*/
- public GetCredentialOption(
+ public CredentialOption(
@NonNull String type,
@NonNull Bundle credentialRetrievalData,
@NonNull Bundle candidateQueryData,
@@ -150,7 +152,7 @@
mIsSystemProviderRequired = isSystemProviderRequired;
}
- private GetCredentialOption(@NonNull Parcel in) {
+ private CredentialOption(@NonNull Parcel in) {
String type = in.readString8();
Bundle data = in.readBundle();
Bundle candidateQueryData = in.readBundle();
@@ -165,16 +167,16 @@
mIsSystemProviderRequired = isSystemProviderRequired;
}
- public static final @NonNull Parcelable.Creator<GetCredentialOption> CREATOR =
- new Parcelable.Creator<GetCredentialOption>() {
- @Override
- public GetCredentialOption[] newArray(int size) {
- return new GetCredentialOption[size];
- }
+ @NonNull
+ public static final Parcelable.Creator<CredentialOption> CREATOR = new Parcelable.Creator<>() {
+ @Override
+ public CredentialOption[] newArray(int size) {
+ return new CredentialOption[size];
+ }
- @Override
- public GetCredentialOption createFromParcel(@NonNull Parcel in) {
- return new GetCredentialOption(in);
- }
- };
+ @Override
+ public CredentialOption createFromParcel(@NonNull Parcel in) {
+ return new CredentialOption(in);
+ }
+ };
}
diff --git a/core/java/android/credentials/GetCredentialRequest.java b/core/java/android/credentials/GetCredentialRequest.java
index 85b4468..a869c5b 100644
--- a/core/java/android/credentials/GetCredentialRequest.java
+++ b/core/java/android/credentials/GetCredentialRequest.java
@@ -39,7 +39,7 @@
* The list of credential requests.
*/
@NonNull
- private final List<GetCredentialOption> mGetCredentialOptions;
+ private final List<CredentialOption> mCredentialOptions;
/**
* The top request level data.
@@ -51,8 +51,8 @@
* Returns the list of credential options to be requested.
*/
@NonNull
- public List<GetCredentialOption> getGetCredentialOptions() {
- return mGetCredentialOptions;
+ public List<CredentialOption> getCredentialOptions() {
+ return mCredentialOptions;
}
/**
@@ -65,7 +65,7 @@
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
- dest.writeTypedList(mGetCredentialOptions, flags);
+ dest.writeTypedList(mCredentialOptions, flags);
dest.writeBundle(mData);
}
@@ -76,29 +76,29 @@
@Override
public String toString() {
- return "GetCredentialRequest {getCredentialOption=" + mGetCredentialOptions
+ return "GetCredentialRequest {credentialOption=" + mCredentialOptions
+ ", data=" + mData
+ "}";
}
- private GetCredentialRequest(@NonNull List<GetCredentialOption> getCredentialOptions,
+ private GetCredentialRequest(@NonNull List<CredentialOption> credentialOptions,
@NonNull Bundle data) {
Preconditions.checkCollectionNotEmpty(
- getCredentialOptions,
- /*valueName=*/ "getCredentialOptions");
+ credentialOptions,
+ /*valueName=*/ "credentialOptions");
Preconditions.checkCollectionElementsNotNull(
- getCredentialOptions,
- /*valueName=*/ "getCredentialOptions");
- mGetCredentialOptions = getCredentialOptions;
+ credentialOptions,
+ /*valueName=*/ "credentialOptions");
+ mCredentialOptions = credentialOptions;
mData = requireNonNull(data,
"data must not be null");
}
private GetCredentialRequest(@NonNull Parcel in) {
- List<GetCredentialOption> getCredentialOptions = new ArrayList<GetCredentialOption>();
- in.readTypedList(getCredentialOptions, GetCredentialOption.CREATOR);
- mGetCredentialOptions = getCredentialOptions;
- AnnotationValidations.validate(NonNull.class, null, mGetCredentialOptions);
+ List<CredentialOption> credentialOptions = new ArrayList<CredentialOption>();
+ in.readTypedList(credentialOptions, CredentialOption.CREATOR);
+ mCredentialOptions = credentialOptions;
+ AnnotationValidations.validate(NonNull.class, null, mCredentialOptions);
Bundle data = in.readBundle();
@@ -106,8 +106,8 @@
AnnotationValidations.validate(NonNull.class, null, mData);
}
- public static final @NonNull Parcelable.Creator<GetCredentialRequest> CREATOR =
- new Parcelable.Creator<GetCredentialRequest>() {
+ @NonNull public static final Parcelable.Creator<GetCredentialRequest> CREATOR =
+ new Parcelable.Creator<>() {
@Override
public GetCredentialRequest[] newArray(int size) {
return new GetCredentialRequest[size];
@@ -123,7 +123,7 @@
public static final class Builder {
@NonNull
- private List<GetCredentialOption> mGetCredentialOptions = new ArrayList<>();
+ private List<CredentialOption> mCredentialOptions = new ArrayList<>();
@NonNull
private final Bundle mData;
@@ -136,43 +136,42 @@
}
/**
- * Adds a specific type of {@link GetCredentialOption}.
+ * Adds a specific type of {@link CredentialOption}.
*/
@NonNull
- public Builder addGetCredentialOption(
- @NonNull GetCredentialOption getCredentialOption) {
- mGetCredentialOptions.add(requireNonNull(
- getCredentialOption, "getCredentialOption must not be null"));
+ public Builder addCredentialOption(@NonNull CredentialOption credentialOption) {
+ mCredentialOptions.add(requireNonNull(
+ credentialOption, "credentialOption must not be null"));
return this;
}
/**
- * Sets the list of {@link GetCredentialOption}.
+ * Sets the list of {@link CredentialOption}.
*/
@NonNull
- public Builder setGetCredentialOptions(
- @NonNull List<GetCredentialOption> getCredentialOptions) {
+ public Builder setCredentialOptions(
+ @NonNull List<CredentialOption> credentialOptions) {
Preconditions.checkCollectionElementsNotNull(
- getCredentialOptions,
- /*valueName=*/ "getCredentialOptions");
- mGetCredentialOptions = new ArrayList<>(getCredentialOptions);
+ credentialOptions,
+ /*valueName=*/ "credentialOptions");
+ mCredentialOptions = new ArrayList<>(credentialOptions);
return this;
}
/**
* Builds a {@link GetCredentialRequest}.
*
- * @throws IllegalArgumentException If getCredentialOptions is empty.
+ * @throws IllegalArgumentException If credentialOptions is empty.
*/
@NonNull
public GetCredentialRequest build() {
Preconditions.checkCollectionNotEmpty(
- mGetCredentialOptions,
- /*valueName=*/ "getCredentialOptions");
+ mCredentialOptions,
+ /*valueName=*/ "credentialOptions");
Preconditions.checkCollectionElementsNotNull(
- mGetCredentialOptions,
- /*valueName=*/ "getCredentialOptions");
- return new GetCredentialRequest(mGetCredentialOptions, mData);
+ mCredentialOptions,
+ /*valueName=*/ "credentialOptions");
+ return new GetCredentialRequest(mCredentialOptions, mData);
}
}
}
diff --git a/core/java/android/hardware/SyncFence.java b/core/java/android/hardware/SyncFence.java
index 1660013..d6052cd 100644
--- a/core/java/android/hardware/SyncFence.java
+++ b/core/java/android/hardware/SyncFence.java
@@ -87,8 +87,8 @@
// is well worth making.
private final Runnable mCloser;
- private SyncFence(@NonNull ParcelFileDescriptor wrapped) {
- mNativePtr = nCreate(wrapped.detachFd());
+ private SyncFence(int fileDescriptor) {
+ mNativePtr = nCreate(fileDescriptor);
mCloser = sRegistry.registerNativeAllocation(this, mNativePtr);
}
@@ -136,14 +136,26 @@
}
/**
- * Create a new SyncFence wrapped around another descriptor. By default, all method calls are
- * delegated to the wrapped descriptor.
+ * Create a new SyncFence wrapped around another {@link ParcelFileDescriptor}. By default, all
+ * method calls are delegated to the wrapped descriptor. This takes ownership of the
+ * {@link ParcelFileDescriptor}.
*
* @param wrapped The descriptor to be wrapped.
* @hide
*/
public static @NonNull SyncFence create(@NonNull ParcelFileDescriptor wrapped) {
- return new SyncFence(wrapped);
+ return new SyncFence(wrapped.detachFd());
+ }
+
+ /**
+ * Create a new SyncFence wrapped around another descriptor. The returned {@link SyncFence}
+ * instance takes ownership of the file descriptor.
+ *
+ * @param fileDescriptor The descriptor to be wrapped.
+ * @hide
+ */
+ public static @NonNull SyncFence adopt(int fileDescriptor) {
+ return new SyncFence(fileDescriptor);
}
/**
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index da847a8..19719a8 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -2387,9 +2387,8 @@
}
//TODO: Do we need to treat this as error?
- if (!mDeviceStatus.containsKey(id) || !isAvailable(mDeviceStatus.get(id))
- || !mUnavailablePhysicalDevices.containsKey(id)) {
- Log.e(TAG, String.format("Camera %s is not available. Ignore physical camera "
+ if (!mDeviceStatus.containsKey(id) || !mUnavailablePhysicalDevices.containsKey(id)) {
+ Log.e(TAG, String.format("Camera %s is not present. Ignore physical camera "
+ "status change", id));
return;
}
@@ -2414,6 +2413,12 @@
return;
}
+ if (!isAvailable(mDeviceStatus.get(id))) {
+ Log.i(TAG, String.format("Camera %s is not available. Ignore physical camera "
+ + "status change callback(s)", id));
+ return;
+ }
+
final int callbackCount = mCallbackMap.size();
for (int i = 0; i < callbackCount; i++) {
Executor executor = mCallbackMap.valueAt(i);
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index 08238ca..d49cc44 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -66,6 +66,7 @@
public final class DisplayManager {
private static final String TAG = "DisplayManager";
private static final boolean DEBUG = false;
+ private static final boolean ENABLE_VIRTUAL_DISPLAY_REFRESH_RATE = false;
private final Context mContext;
private final DisplayManagerGlobal mGlobal;
@@ -114,6 +115,23 @@
"android.hardware.display.category.PRESENTATION";
/**
+ * Display category: Rear displays.
+ * <p>
+ * This category can be used to identify complementary internal displays that are facing away
+ * from the user.
+ * Certain applications may present to this display.
+ * Similar to presentation displays.
+ * </p>
+ *
+ * @see android.app.Presentation
+ * @see Display#FLAG_PRESENTATION
+ * @see #getDisplays(String)
+ * @hide
+ */
+ public static final String DISPLAY_CATEGORY_REAR =
+ "android.hardware.display.category.REAR";
+
+ /**
* Display category: All displays, including disabled displays.
* <p>
* This returns all displays, including currently disabled and inaccessible displays.
@@ -618,11 +636,19 @@
synchronized (mLock) {
try {
if (DISPLAY_CATEGORY_PRESENTATION.equals(category)) {
- addPresentationDisplaysLocked(mTempDisplays, displayIds, Display.TYPE_WIFI);
- addPresentationDisplaysLocked(mTempDisplays, displayIds, Display.TYPE_EXTERNAL);
- addPresentationDisplaysLocked(mTempDisplays, displayIds, Display.TYPE_OVERLAY);
- addPresentationDisplaysLocked(mTempDisplays, displayIds, Display.TYPE_VIRTUAL);
- addPresentationDisplaysLocked(mTempDisplays, displayIds, Display.TYPE_INTERNAL);
+ addDisplaysLocked(mTempDisplays, displayIds, Display.TYPE_WIFI,
+ Display.FLAG_PRESENTATION);
+ addDisplaysLocked(mTempDisplays, displayIds, Display.TYPE_EXTERNAL,
+ Display.FLAG_PRESENTATION);
+ addDisplaysLocked(mTempDisplays, displayIds, Display.TYPE_OVERLAY,
+ Display.FLAG_PRESENTATION);
+ addDisplaysLocked(mTempDisplays, displayIds, Display.TYPE_VIRTUAL,
+ Display.FLAG_PRESENTATION);
+ addDisplaysLocked(mTempDisplays, displayIds, Display.TYPE_INTERNAL,
+ Display.FLAG_PRESENTATION);
+ } else if (DISPLAY_CATEGORY_REAR.equals(category)) {
+ addDisplaysLocked(mTempDisplays, displayIds, Display.TYPE_INTERNAL,
+ Display.FLAG_REAR);
} else if (category == null
|| DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED.equals(category)) {
addAllDisplaysLocked(mTempDisplays, displayIds);
@@ -643,15 +669,16 @@
}
}
- private void addPresentationDisplaysLocked(
- ArrayList<Display> displays, int[] displayIds, int matchType) {
- for (int i = 0; i < displayIds.length; i++) {
- if (displayIds[i] == DEFAULT_DISPLAY) {
+ private void addDisplaysLocked(
+ ArrayList<Display> displays, int[] displayIds, int matchType, int flagMask) {
+ for (int displayId : displayIds) {
+ if (displayId == DEFAULT_DISPLAY) {
continue;
}
- Display display = getOrCreateDisplayLocked(displayIds[i], true /*assumeValid*/);
+
+ Display display = getOrCreateDisplayLocked(displayId, /* assumeValid= */ true);
if (display != null
- && (display.getFlags() & Display.FLAG_PRESENTATION) != 0
+ && (display.getFlags() & flagMask) == flagMask
&& display.getType() == matchType) {
displays.add(display);
}
@@ -938,6 +965,24 @@
/**
* Creates a virtual display.
+ *
+ * @see #createVirtualDisplay(String, int, int, int, float, Surface, int,
+ * Handler, VirtualDisplay.Callback)
+ */
+ @Nullable
+ public VirtualDisplay createVirtualDisplay(@NonNull String name,
+ @IntRange(from = 1) int width,
+ @IntRange(from = 1) int height,
+ @IntRange(from = 1) int densityDpi,
+ float requestedRefreshRate,
+ @Nullable Surface surface,
+ @VirtualDisplayFlag int flags) {
+ return createVirtualDisplay(name, width, height, densityDpi, requestedRefreshRate,
+ surface, flags, null, null);
+ }
+
+ /**
+ * Creates a virtual display.
* <p>
* The content of a virtual display is rendered to a {@link Surface} provided
* by the application.
@@ -987,12 +1032,82 @@
@VirtualDisplayFlag int flags,
@Nullable VirtualDisplay.Callback callback,
@Nullable Handler handler) {
+ return createVirtualDisplay(name, width, height, densityDpi, 0.0f, surface,
+ flags, handler, callback);
+ }
+
+ /**
+ * Creates a virtual display.
+ * <p>
+ * The content of a virtual display is rendered to a {@link Surface} provided
+ * by the application.
+ * </p><p>
+ * The virtual display should be {@link VirtualDisplay#release released}
+ * when no longer needed. Because a virtual display renders to a surface
+ * provided by the application, it will be released automatically when the
+ * process terminates and all remaining windows on it will be forcibly removed.
+ * </p><p>
+ * The behavior of the virtual display depends on the flags that are provided
+ * to this method. By default, virtual displays are created to be private,
+ * non-presentation and unsecure. Permissions may be required to use certain flags.
+ * </p><p>
+ * As of {@link android.os.Build.VERSION_CODES#KITKAT_WATCH}, the surface may
+ * be attached or detached dynamically using {@link VirtualDisplay#setSurface}.
+ * Previously, the surface had to be non-null when {@link #createVirtualDisplay}
+ * was called and could not be changed for the lifetime of the display.
+ * </p><p>
+ * Detaching the surface that backs a virtual display has a similar effect to
+ * turning off the screen.
+ * </p>
+ *
+ * @param name The name of the virtual display, must be non-empty.
+ * @param width The width of the virtual display in pixels, must be greater than 0.
+ * @param height The height of the virtual display in pixels, must be greater than 0.
+ * @param densityDpi The density of the virtual display in dpi, must be greater than 0.
+ * @param requestedRefreshRate The requested refresh rate in frames per second.
+ * For best results, specify a divisor of the physical refresh rate, e.g., 30 or 60 on
+ * 120hz display. If an arbitrary refresh rate is specified, the rate will be rounded
+ * up or down to a divisor of the physical display. If 0 is specified, the virtual
+ * display is refreshed at the physical display refresh rate.
+ * @param surface The surface to which the content of the virtual display should
+ * be rendered, or null if there is none initially.
+ * @param flags A combination of virtual display flags:
+ * {@link #VIRTUAL_DISPLAY_FLAG_PUBLIC}, {@link #VIRTUAL_DISPLAY_FLAG_PRESENTATION},
+ * {@link #VIRTUAL_DISPLAY_FLAG_SECURE}, {@link #VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY},
+ * or {@link #VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR}.
+ * @param handler The handler on which the listener should be invoked, or null
+ * if the listener should be invoked on the calling thread's looper.
+ * @param callback Callback to call when the state of the {@link VirtualDisplay} changes
+ * @return The newly created virtual display, or null if the application could
+ * not create the virtual display.
+ *
+ * @throws SecurityException if the caller does not have permission to create
+ * a virtual display with the specified flags.
+ */
+ @Nullable
+ public VirtualDisplay createVirtualDisplay(@NonNull String name,
+ @IntRange(from = 1) int width,
+ @IntRange(from = 1) int height,
+ @IntRange(from = 1) int densityDpi,
+ float requestedRefreshRate,
+ @Nullable Surface surface,
+ @VirtualDisplayFlag int flags,
+ @Nullable Handler handler,
+ @Nullable VirtualDisplay.Callback callback) {
+ if (!ENABLE_VIRTUAL_DISPLAY_REFRESH_RATE && requestedRefreshRate != 0.0f) {
+ Slog.e(TAG, "Please turn on ENABLE_VIRTUAL_DISPLAY_REFRESH_RATE to use the new api");
+ return null;
+ }
+
final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(name, width,
height, densityDpi);
builder.setFlags(flags);
if (surface != null) {
builder.setSurface(surface);
}
+ if (requestedRefreshRate != 0.0f) {
+ builder.setRequestedRefreshRate(requestedRefreshRate);
+ }
return createVirtualDisplay(null /* projection */, builder.build(), callback, handler,
null /* windowContext */);
}
diff --git a/core/java/android/hardware/display/HdrConversionMode.java b/core/java/android/hardware/display/HdrConversionMode.java
index 1accd17..da2b016 100644
--- a/core/java/android/hardware/display/HdrConversionMode.java
+++ b/core/java/android/hardware/display/HdrConversionMode.java
@@ -18,6 +18,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;
@@ -119,4 +120,40 @@
dest.writeInt(mConversionMode);
dest.writeInt(mPreferredHdrOutputType);
}
-}
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ return o instanceof HdrConversionMode && equals((HdrConversionMode) o);
+ }
+
+ @Override
+ public int hashCode() {
+ return 0; // don't care
+ }
+
+ @Override
+ public String toString() {
+ return "HdrConversionMode{ConversionMode=" + hdrConversionModeString(getConversionMode())
+ + ", PreferredHdrOutputType="
+ + Display.HdrCapabilities.hdrTypeToString(getPreferredHdrOutputType()) + "}";
+ }
+
+ private boolean equals(HdrConversionMode other) {
+ return other != null
+ && mConversionMode == other.getConversionMode()
+ && mPreferredHdrOutputType == other.getPreferredHdrOutputType();
+ }
+
+ private static String hdrConversionModeString(int hdrConversionMode) {
+ switch (hdrConversionMode) {
+ case HDR_CONVERSION_PASSTHROUGH:
+ return "HDR_CONVERSION_PASSTHROUGH";
+ case HDR_CONVERSION_SYSTEM:
+ return "HDR_CONVERSION_SYSTEM";
+ case HDR_CONVERSION_FORCE:
+ return "HDR_CONVERSION_FORCE";
+ default:
+ return "HDR_CONVERSION_UNKNOWN";
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/java/android/hardware/display/VirtualDisplayConfig.java b/core/java/android/hardware/display/VirtualDisplayConfig.java
index abd647f..f6a2e33 100644
--- a/core/java/android/hardware/display/VirtualDisplayConfig.java
+++ b/core/java/android/hardware/display/VirtualDisplayConfig.java
@@ -109,6 +109,13 @@
@DataClass.PluralOf("displayCategory")
@NonNull private List<String> mDisplayCategories = new ArrayList<>();
+ /**
+ * The refresh rate of a virtual display in frames per second.
+ * If this value is non-zero, this is the requested refresh rate to set.
+ * If this value is zero, the system chooses a default refresh rate.
+ */
+ private float mRequestedRefreshRate = 0.0f;
+
// Code below generated by codegen v1.0.23.
@@ -135,7 +142,8 @@
@Nullable String uniqueId,
int displayIdToMirror,
boolean windowManagerMirroring,
- @NonNull List<String> displayCategories) {
+ @NonNull List<String> displayCategories,
+ float requestedRefreshRate) {
this.mName = name;
com.android.internal.util.AnnotationValidations.validate(
NonNull.class, null, mName);
@@ -161,6 +169,7 @@
this.mDisplayCategories = displayCategories;
com.android.internal.util.AnnotationValidations.validate(
NonNull.class, null, mDisplayCategories);
+ this.mRequestedRefreshRate = requestedRefreshRate;
// onConstructed(); // You can define this method to get a callback
}
@@ -256,6 +265,16 @@
return mDisplayCategories;
}
+ /**
+ * The refresh rate of a virtual display in frames per second.
+ * If this value is none zero, this is the requested refresh rate to set.
+ * If this value is zero, the system chooses a default refresh rate.
+ */
+ @DataClass.Generated.Member
+ public float getRequestedRefreshRate() {
+ return mRequestedRefreshRate;
+ }
+
@Override
@DataClass.Generated.Member
public void writeToParcel(@NonNull Parcel dest, int flags) {
@@ -276,6 +295,7 @@
if (mUniqueId != null) dest.writeString(mUniqueId);
dest.writeInt(mDisplayIdToMirror);
dest.writeStringList(mDisplayCategories);
+ dest.writeFloat(mRequestedRefreshRate);
}
@Override
@@ -301,6 +321,7 @@
int displayIdToMirror = in.readInt();
List<String> displayCategories = new ArrayList<>();
in.readStringList(displayCategories);
+ float requestedRefreshRate = in.readFloat();
this.mName = name;
com.android.internal.util.AnnotationValidations.validate(
@@ -327,6 +348,7 @@
this.mDisplayCategories = displayCategories;
com.android.internal.util.AnnotationValidations.validate(
NonNull.class, null, mDisplayCategories);
+ this.mRequestedRefreshRate = requestedRefreshRate;
// onConstructed(); // You can define this method to get a callback
}
@@ -362,6 +384,7 @@
private int mDisplayIdToMirror;
private boolean mWindowManagerMirroring;
private @NonNull List<String> mDisplayCategories;
+ private float mRequestedRefreshRate;
private long mBuilderFieldsSet = 0L;
@@ -528,10 +551,23 @@
return this;
}
+ /**
+ * The refresh rate of a virtual display in frames per second.
+ * If this value is none zero, this is the requested refresh rate to set.
+ * If this value is zero, the system chooses a default refresh rate.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setRequestedRefreshRate(float value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x400;
+ mRequestedRefreshRate = value;
+ return this;
+ }
+
/** Builds the instance. This builder should not be touched after calling this! */
public @NonNull VirtualDisplayConfig build() {
checkNotUsed();
- mBuilderFieldsSet |= 0x400; // Mark builder used
+ mBuilderFieldsSet |= 0x800; // Mark builder used
if ((mBuilderFieldsSet & 0x10) == 0) {
mFlags = 0;
@@ -551,6 +587,9 @@
if ((mBuilderFieldsSet & 0x200) == 0) {
mDisplayCategories = new ArrayList<>();
}
+ if ((mBuilderFieldsSet & 0x400) == 0) {
+ mRequestedRefreshRate = 0.0f;
+ }
VirtualDisplayConfig o = new VirtualDisplayConfig(
mName,
mWidth,
@@ -561,12 +600,13 @@
mUniqueId,
mDisplayIdToMirror,
mWindowManagerMirroring,
- mDisplayCategories);
+ mDisplayCategories,
+ mRequestedRefreshRate);
return o;
}
private void checkNotUsed() {
- if ((mBuilderFieldsSet & 0x400) != 0) {
+ if ((mBuilderFieldsSet & 0x800) != 0) {
throw new IllegalStateException(
"This Builder should not be reused. Use a new Builder instance instead");
}
@@ -574,10 +614,10 @@
}
@DataClass.Generated(
- time = 1668534501320L,
+ time = 1671047069703L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/hardware/display/VirtualDisplayConfig.java",
- inputSignatures = "private @android.annotation.NonNull java.lang.String mName\nprivate @android.annotation.IntRange int mWidth\nprivate @android.annotation.IntRange int mHeight\nprivate @android.annotation.IntRange int mDensityDpi\nprivate @android.hardware.display.DisplayManager.VirtualDisplayFlag int mFlags\nprivate @android.annotation.Nullable android.view.Surface mSurface\nprivate @android.annotation.Nullable java.lang.String mUniqueId\nprivate int mDisplayIdToMirror\nprivate boolean mWindowManagerMirroring\nprivate @com.android.internal.util.DataClass.PluralOf(\"displayCategory\") @android.annotation.NonNull java.util.List<java.lang.String> mDisplayCategories\nclass VirtualDisplayConfig extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genAidl=true, genBuilder=true)")
+ inputSignatures = "private @android.annotation.NonNull java.lang.String mName\nprivate @android.annotation.IntRange int mWidth\nprivate @android.annotation.IntRange int mHeight\nprivate @android.annotation.IntRange int mDensityDpi\nprivate @android.hardware.display.DisplayManager.VirtualDisplayFlag int mFlags\nprivate @android.annotation.Nullable android.view.Surface mSurface\nprivate @android.annotation.Nullable java.lang.String mUniqueId\nprivate int mDisplayIdToMirror\nprivate boolean mWindowManagerMirroring\nprivate @com.android.internal.util.DataClass.PluralOf(\"displayCategory\") @android.annotation.NonNull java.util.List<java.lang.String> mDisplayCategories\nprivate float mRequestedRefreshRate\nclass VirtualDisplayConfig extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genAidl=true, genBuilder=true)")
@Deprecated
private void __metadata() {}
diff --git a/core/java/android/hardware/fingerprint/IUdfpsRefreshRateRequestCallback.aidl b/core/java/android/hardware/fingerprint/IUdfpsRefreshRateRequestCallback.aidl
index 8587348..7844b40 100644
--- a/core/java/android/hardware/fingerprint/IUdfpsRefreshRateRequestCallback.aidl
+++ b/core/java/android/hardware/fingerprint/IUdfpsRefreshRateRequestCallback.aidl
@@ -39,5 +39,15 @@
* {@link android.view.Display#getDisplayId()}.
*/
void onRequestDisabled(int displayId);
+
+ /**
+ * To avoid delay in switching refresh rate when activating LHBM, allow screens to request
+ * higher refersh rate if auth is possible on particular screen
+ *
+ * @param displayId The displayId for which the refresh rate should be unset. See
+ * {@link android.view.Display#getDisplayId()}.
+ * @param isPossible If authentication is possible on particualr screen
+ */
+ void onAuthenticationPossible(int displayId, boolean isPossible);
}
diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java
index a980158..7378ac7 100644
--- a/core/java/android/nfc/NfcAdapter.java
+++ b/core/java/android/nfc/NfcAdapter.java
@@ -2510,8 +2510,18 @@
*
* <p>This returns a mapping of package names for this user id to whether we dispatch Tag
* intents to the package. {@link #ACTION_NDEF_DISCOVERED}, {@link #ACTION_TECH_DISCOVERED} or
- * {@link #ACTION_TAG_DISCOVERED} will not be dispatched to an Activity if its package is
- * disallowed.
+ * {@link #ACTION_TAG_DISCOVERED} will not be dispatched to an Activity if its package is
+ * mapped to {@code false}.
+ * <p>There are three different possible cases:
+ * <p>A package not being in the preference list.
+ * It does not contain any Tag intent filters or the user never triggers a Tag detection that
+ * matches the intent filter of the package.
+ * <p>A package being mapped to {@code true}.
+ * When a package has been launched by a tag detection for the first time, the package name is
+ * put to the map and by default mapped to {@code true}. The package will receive Tag intents as
+ * usual.
+ * <p>A package being mapped to {@code false}.
+ * The user chooses to disable this package and it will not receive any Tag intents anymore.
*
* @param userId the user to whom this preference list will belong to
* @return a map of the UserId which indicates the mapping from package name to
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index 607d73c..2bad670 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -4356,13 +4356,6 @@
}
}
- /**
- * Temporary for settings.
- */
- public final void dumpCheckinLocked(Context context, PrintWriter pw, int which, int reqUid) {
- dumpCheckinLocked(context, pw, which, reqUid, checkWifiOnly(context));
- }
-
private static final String[] CHECKIN_POWER_COMPONENT_LABELS =
new String[BatteryConsumer.POWER_COMPONENT_COUNT];
static {
@@ -7477,22 +7470,8 @@
public static final int DUMP_DEVICE_WIFI_ONLY = 1<<6;
private void dumpHistory(PrintWriter pw, int flags, long histStart, boolean checkin) {
- if (!checkin) {
- synchronized (this) {
- final long historyTotalSize = getHistoryTotalSize();
- final long historyUsedSize = getHistoryUsedSize();
- pw.print("Battery History (");
- pw.print((100 * historyUsedSize) / historyTotalSize);
- pw.print("% used, ");
- printSizeValue(pw, historyUsedSize);
- pw.print(" used of ");
- printSizeValue(pw, historyTotalSize);
- pw.print(", ");
- pw.print(getHistoryStringPoolSize());
- pw.print(" strings using ");
- printSizeValue(pw, getHistoryStringPoolBytes());
- pw.println("):");
- }
+ synchronized (this) {
+ dumpHistoryTagPoolLocked(pw, checkin);
}
final HistoryPrinter hprinter = new HistoryPrinter();
@@ -7586,6 +7565,43 @@
}
}
+ private void dumpHistoryTagPoolLocked(PrintWriter pw, boolean checkin) {
+ if (checkin) {
+ for (int i = 0; i < getHistoryStringPoolSize(); i++) {
+ pw.print(BATTERY_STATS_CHECKIN_VERSION);
+ pw.print(',');
+ pw.print(HISTORY_STRING_POOL);
+ pw.print(',');
+ pw.print(i);
+ pw.print(",");
+ pw.print(getHistoryTagPoolUid(i));
+ pw.print(",\"");
+ String str = getHistoryTagPoolString(i);
+ if (str != null) {
+ str = str.replace("\\", "\\\\");
+ str = str.replace("\"", "\\\"");
+ pw.print(str);
+ }
+ pw.print("\"");
+ pw.println();
+ }
+ } else {
+ final long historyTotalSize = getHistoryTotalSize();
+ final long historyUsedSize = getHistoryUsedSize();
+ pw.print("Battery History (");
+ pw.print((100 * historyUsedSize) / historyTotalSize);
+ pw.print("% used, ");
+ printSizeValue(pw, historyUsedSize);
+ pw.print(" used of ");
+ printSizeValue(pw, historyTotalSize);
+ pw.print(", ");
+ pw.print(getHistoryStringPoolSize());
+ pw.print(" strings using ");
+ printSizeValue(pw, getHistoryStringPoolBytes());
+ pw.println("):");
+ }
+ }
+
private void dumpDailyLevelStepSummary(PrintWriter pw, String prefix, String label,
LevelStepTracker steps, StringBuilder tmpSb, int[] tmpOutInt) {
if (steps == null) {
@@ -7804,33 +7820,17 @@
// This is called from BatteryStatsService.
@SuppressWarnings("unused")
- public void dumpCheckinLocked(Context context, PrintWriter pw,
+ public void dumpCheckin(Context context, PrintWriter pw,
List<ApplicationInfo> apps, int flags, long histStart) {
- prepareForDumpLocked();
+ synchronized (this) {
+ prepareForDumpLocked();
- dumpLine(pw, 0 /* uid */, "i" /* category */, VERSION_DATA,
- CHECKIN_VERSION, getParcelVersion(), getStartPlatformVersion(),
- getEndPlatformVersion());
+ dumpLine(pw, 0 /* uid */, "i" /* category */, VERSION_DATA,
+ CHECKIN_VERSION, getParcelVersion(), getStartPlatformVersion(),
+ getEndPlatformVersion());
+ }
if ((flags & (DUMP_INCLUDE_HISTORY | DUMP_HISTORY_ONLY)) != 0) {
- for (int i = 0; i < getHistoryStringPoolSize(); i++) {
- pw.print(BATTERY_STATS_CHECKIN_VERSION);
- pw.print(',');
- pw.print(HISTORY_STRING_POOL);
- pw.print(',');
- pw.print(i);
- pw.print(",");
- pw.print(getHistoryTagPoolUid(i));
- pw.print(",\"");
- String str = getHistoryTagPoolString(i);
- if (str != null) {
- str = str.replace("\\", "\\\\");
- str = str.replace("\"", "\\\"");
- pw.print(str);
- }
- pw.print("\"");
- pw.println();
- }
dumpHistory(pw, flags, histStart, true);
}
@@ -7838,6 +7838,13 @@
return;
}
+ synchronized (this) {
+ dumpCheckinLocked(context, pw, apps, flags);
+ }
+ }
+
+ private void dumpCheckinLocked(Context context, PrintWriter pw, List<ApplicationInfo> apps,
+ int flags) {
if (apps != null) {
SparseArray<Pair<ArrayList<String>, MutableBoolean>> uids = new SparseArray<>();
for (int i=0; i<apps.size(); i++) {
diff --git a/core/java/android/os/BugreportManager.java b/core/java/android/os/BugreportManager.java
index 222e88f..2d2b0fc 100644
--- a/core/java/android/os/BugreportManager.java
+++ b/core/java/android/os/BugreportManager.java
@@ -16,6 +16,7 @@
package android.os;
+import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.FloatRange;
import android.annotation.IntDef;
@@ -69,7 +70,10 @@
* An interface describing the callback for bugreport progress and status.
*
* <p>Callers will receive {@link #onProgress} calls as the bugreport progresses, followed by a
- * terminal call to either {@link #onFinished} or {@link #onError}.
+ * terminal call to either {@link #onFinished} or {@link #onError}. Note that
+ * {@link #onFinished(String)} will only be invoked when calling {@code startBugreport} with the
+ * {@link BugreportParams#BUGREPORT_FLAG_DEFER_CONSENT} flag set. Otherwise,
+ * {@link #onFinished()} will be invoked.
*
* <p>If an issue is encountered while starting the bugreport asynchronously, callers will
* receive an {@link #onError} call without any {@link #onProgress} callbacks.
@@ -88,7 +92,8 @@
BUGREPORT_ERROR_RUNTIME,
BUGREPORT_ERROR_USER_DENIED_CONSENT,
BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT,
- BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS
+ BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS,
+ BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE
})
public @interface BugreportErrorCode {}
@@ -115,6 +120,10 @@
public static final int BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS =
IDumpstateListener.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS;
+ /** There is no bugreport to retrieve for the caller. */
+ public static final int BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE =
+ IDumpstateListener.BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE;
+
/**
* Called when there is a progress update.
*
@@ -137,9 +146,25 @@
*/
public void onError(@BugreportErrorCode int errorCode) {}
- /** Called when taking bugreport finishes successfully. */
+ /** Called when taking bugreport finishes successfully.
+ *
+ * <p>This callback will be invoked if the
+ * {@link BugreportParams#BUGREPORT_FLAG_DEFER_CONSENT} flag is not set. Otherwise, the
+ * {@link #onFinished(String)} callback will be invoked.
+ */
public void onFinished() {}
+ /** Called when taking bugreport finishes successfully.
+ *
+ * <p>This callback will only be invoked if the
+ * {@link BugreportParams#BUGREPORT_FLAG_DEFER_CONSENT} flag is set. Otherwise, the
+ * {@link #onFinished()} callback will be invoked.
+ *
+ * @param bugreportFile the absolute path of the generated bugreport file.
+
+ */
+ public void onFinished(@NonNull String bugreportFile) {}
+
/**
* Called when it is ready for calling app to show UI, showing any extra UI before this
* callback can interfere with bugreport generation.
@@ -178,7 +203,9 @@
* updates.
*
* <p>The bugreport artifacts will be copied over to the given file descriptors only if the user
- * consents to sharing with the calling app.
+ * consents to sharing with the calling app. If
+ * {@link BugreportParams#BUGREPORT_FLAG_DEFER_CONSENT} is set, user consent will be deferred
+ * and no files will be copied to the given file descriptors.
*
* <p>{@link BugreportManager} takes ownership of {@code bugreportFd} and {@code screenshotFd}.
*
@@ -205,7 +232,9 @@
Preconditions.checkNotNull(executor);
Preconditions.checkNotNull(callback);
- boolean isScreenshotRequested = screenshotFd != null;
+ boolean deferConsent =
+ (params.getFlags() & BugreportParams.BUGREPORT_FLAG_DEFER_CONSENT) != 0;
+ boolean isScreenshotRequested = screenshotFd != null || deferConsent;
if (screenshotFd == null) {
// Binder needs a valid File Descriptor to be passed
screenshotFd =
@@ -213,7 +242,7 @@
new File("/dev/null"), ParcelFileDescriptor.MODE_READ_ONLY);
}
DumpstateListener dsListener =
- new DumpstateListener(executor, callback, isScreenshotRequested);
+ new DumpstateListener(executor, callback, isScreenshotRequested, deferConsent);
// Note: mBinder can get callingUid from the binder transaction.
mBinder.startBugreport(
-1 /* callingUid */,
@@ -238,6 +267,58 @@
}
/**
+ * Retrieves a previously generated bugreport.
+ *
+ * <p>The previously generated bugreport must have been generated by calling {@link
+ * #startBugreport(ParcelFileDescriptor, ParcelFileDescriptor, BugreportParams,
+ * Executor, BugreportCallback)} with the {@link BugreportParams#BUGREPORT_FLAG_DEFER_CONSENT}
+ * flag set. The bugreport file returned by the {@link BugreportCallback#onFinished(String)}
+ * callback for a previously generated bugreport must be passed to this method. A caller may
+ * only retrieve bugreports that they have previously requested.
+ *
+ * <p>The bugreport artifacts will be copied over to the given file descriptor only if the user
+ * consents to sharing with the calling app.
+ *
+ * <p>{@link BugreportManager} takes ownership of {@code bugreportFd}.
+ *
+ * <p>The caller may only request to retrieve a given bugreport once. Subsequent calls will fail
+ * with error code {@link BugreportCallback#BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE}.
+ *
+ * @param bugreportFile the identifier for a bugreport that was previously generated for this
+ * caller using {@code startBugreport}.
+ * @param bugreportFd file to copy over the previous bugreport. This should be opened in
+ * write-only, append mode.
+ * @param executor the executor to execute callback methods.
+ * @param callback callback for progress and status updates.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.DUMP)
+ @WorkerThread
+ public void retrieveBugreport(
+ @NonNull String bugreportFile,
+ @NonNull ParcelFileDescriptor bugreportFd,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull BugreportCallback callback
+ ) {
+ try {
+ Preconditions.checkNotNull(bugreportFile);
+ Preconditions.checkNotNull(bugreportFd);
+ Preconditions.checkNotNull(executor);
+ Preconditions.checkNotNull(callback);
+ DumpstateListener dsListener = new DumpstateListener(executor, callback, false, false);
+ mBinder.retrieveBugreport(Binder.getCallingUid(), mContext.getOpPackageName(),
+ bugreportFd.getFileDescriptor(),
+ bugreportFile,
+ dsListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } finally {
+ IoUtils.closeQuietly(bugreportFd);
+ }
+ }
+
+ /**
* Starts a connectivity bugreport.
*
* <p>The connectivity bugreport is a specialized version of bugreport that only includes
@@ -316,7 +397,7 @@
* @hide
*/
@SystemApi
- @RequiresPermission(android.Manifest.permission.DUMP)
+ @RequiresPermission(Manifest.permission.DUMP)
public void requestBugreport(
@NonNull BugreportParams params,
@Nullable CharSequence shareTitle,
@@ -335,12 +416,15 @@
private final Executor mExecutor;
private final BugreportCallback mCallback;
private final boolean mIsScreenshotRequested;
+ private final boolean mIsConsentDeferred;
DumpstateListener(
- Executor executor, BugreportCallback callback, boolean isScreenshotRequested) {
+ Executor executor, BugreportCallback callback, boolean isScreenshotRequested,
+ boolean isConsentDeferred) {
mExecutor = executor;
mCallback = callback;
mIsScreenshotRequested = isScreenshotRequested;
+ mIsConsentDeferred = isConsentDeferred;
}
@Override
@@ -364,10 +448,14 @@
}
@Override
- public void onFinished() throws RemoteException {
+ public void onFinished(String bugreportFile) throws RemoteException {
final long identity = Binder.clearCallingIdentity();
try {
- mExecutor.execute(() -> mCallback.onFinished());
+ if (mIsConsentDeferred) {
+ mExecutor.execute(() -> mCallback.onFinished(bugreportFile));
+ } else {
+ mExecutor.execute(() -> mCallback.onFinished());
+ }
} finally {
Binder.restoreCallingIdentity(identity);
}
diff --git a/core/java/android/os/BugreportParams.java b/core/java/android/os/BugreportParams.java
index 990883f..d9d14b0 100644
--- a/core/java/android/os/BugreportParams.java
+++ b/core/java/android/os/BugreportParams.java
@@ -21,6 +21,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
/**
* Parameters that specify what kind of bugreport should be taken.
@@ -125,7 +126,8 @@
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, prefix = { "BUGREPORT_FLAG_" }, value = {
- BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA
+ BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA,
+ BUGREPORT_FLAG_DEFER_CONSENT
})
public @interface BugreportFlag {}
@@ -135,4 +137,14 @@
*/
public static final int BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA =
IDumpstate.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA;
+
+ /**
+ * Flag for deferring user consent.
+ *
+ * <p>This flag should be used in cases where it may not be possible for the user to respond
+ * to a consent dialog immediately, such as when the user is driving. The generated bugreport
+ * may be retrieved at a later time using {@link BugreportManager#retrieveBugreport(
+ * String, ParcelFileDescriptor, Executor, BugreportManager.BugreportCallback)}.
+ */
+ public static final int BUGREPORT_FLAG_DEFER_CONSENT = IDumpstate.BUGREPORT_FLAG_DEFER_CONSENT;
}
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index 249f486..832f23c 100755
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -61,6 +61,17 @@
/** The name of the overall product. */
public static final String PRODUCT = getString("ro.product.name");
+ /**
+ * The product name for attestation. In non-default builds (like the AOSP build) the value of
+ * the 'PRODUCT' system property may be different to the one provisioned to KeyMint,
+ * and Keymint attestation would still attest to the product name, it's running on.
+ * @hide
+ */
+ @Nullable
+ @TestApi
+ public static final String PRODUCT_FOR_ATTESTATION =
+ getString("ro.product.name_for_attestation");
+
/** The name of the industrial design. */
public static final String DEVICE = getString("ro.product.device");
@@ -89,9 +100,31 @@
/** The consumer-visible brand with which the product/hardware will be associated, if any. */
public static final String BRAND = getString("ro.product.brand");
+ /**
+ * The product brand for attestation. In non-default builds (like the AOSP build) the value of
+ * the 'BRAND' system property may be different to the one provisioned to KeyMint,
+ * and Keymint attestation would still attest to the product brand, it's running on.
+ * @hide
+ */
+ @Nullable
+ @TestApi
+ public static final String BRAND_FOR_ATTESTATION =
+ getString("ro.product.brand_for_attestation");
+
/** The end-user-visible name for the end product. */
public static final String MODEL = getString("ro.product.model");
+ /**
+ * The product model for attestation. In non-default builds (like the AOSP build) the value of
+ * the 'MODEL' system property may be different to the one provisioned to KeyMint,
+ * and Keymint attestation would still attest to the product model, it's running on.
+ * @hide
+ */
+ @Nullable
+ @TestApi
+ public static final String MODEL_FOR_ATTESTATION =
+ getString("ro.product.model_for_attestation");
+
/** The manufacturer of the device's primary system-on-chip. */
@NonNull
public static final String SOC_MANUFACTURER = SocProperties.soc_manufacturer().orElse(UNKNOWN);
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index d63d87d..1df45d1 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -1069,11 +1069,6 @@
* Specifies that windows besides app windows should not be
* created. This will block the creation of the following types of windows.
* <li>{@link LayoutParams#TYPE_TOAST}</li>
- * <li>{@link LayoutParams#TYPE_PHONE}</li>
- * <li>{@link LayoutParams#TYPE_PRIORITY_PHONE}</li>
- * <li>{@link LayoutParams#TYPE_SYSTEM_ALERT}</li>
- * <li>{@link LayoutParams#TYPE_SYSTEM_ERROR}</li>
- * <li>{@link LayoutParams#TYPE_SYSTEM_OVERLAY}</li>
* <li>{@link LayoutParams#TYPE_APPLICATION_OVERLAY}</li>
*
* <p>This can only be set by device owners and profile owners on the primary user.
@@ -2976,6 +2971,29 @@
}
/**
+ * @hide
+ */
+ public static boolean isVisibleBackgroundUsersOnDefaultDisplayEnabled() {
+ return SystemProperties.getBoolean("fw.visible_bg_users_on_default_display",
+ Resources.getSystem()
+ .getBoolean(R.bool.config_multiuserVisibleBackgroundUsersOnDefaultDisplay));
+ }
+
+ /**
+ * Returns whether the device allows (full) users to be started in background visible in the
+ * {@link android.view.Display#DEFAULT_DISPLAY default display}.
+ *
+ * @return {@code false} for most devices, except passenger-only automotive build (i.e., when
+ * Android runs in a separate system in the back seat to manage the passenger displays).
+ *
+ * @hide
+ */
+ @TestApi
+ public boolean isVisibleBackgroundUsersOnDefaultDisplaySupported() {
+ return isVisibleBackgroundUsersOnDefaultDisplayEnabled();
+ }
+
+ /**
* Checks if the user is visible at the moment.
*
* <p>Roughly speaking, a "visible user" is a user that can present UI on at least one display.
diff --git a/core/java/android/os/storage/OWNERS b/core/java/android/os/storage/OWNERS
index c80c57c..e5b76f6 100644
--- a/core/java/android/os/storage/OWNERS
+++ b/core/java/android/os/storage/OWNERS
@@ -1,7 +1,9 @@
# Bug component: 95221
+# Please assign new bugs to android-storage-triage@, not to individual people
+
# Android Storage Team
-abkaur@google.com
+alukin@google.com
corinac@google.com
dipankarb@google.com
krishang@google.com
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index b2d89962..b29efab 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7392,6 +7392,9 @@
*
* Format like "ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0"
* where imeId is ComponentName and subtype is int32.
+ *
+ * <p>Note: This setting is not readable to the app targeting API level 34 or higher. use
+ * {@link android.view.inputmethod.InputMethodManager#getEnabledInputMethodList()} instead.
*/
@Readable(maxTargetSdk = Build.VERSION_CODES.TIRAMISU)
public static final String ENABLED_INPUT_METHODS = "enabled_input_methods";
@@ -10268,6 +10271,17 @@
"active_unlock_on_unlock_intent_when_biometric_enrolled";
/**
+ * If active unlock triggers on unlock intents, then also request active unlock on
+ * these wake-up reasons. See PowerManager.WakeReason for value mappings.
+ * WakeReasons should be separated by a pipe. For example: "0|3" or "0". If this
+ * setting should be disabled, then this should be set to an empty string. A null value
+ * will use the system default value (WAKE_REASON_UNFOLD_DEVICE).
+ * @hide
+ */
+ public static final String ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS =
+ "active_unlock_wakeups_considered_unlock_intents";
+
+ /**
* Whether the assist gesture should be enabled.
*
* @hide
@@ -11454,6 +11468,13 @@
"extra_automatic_power_save_mode";
/**
+ * Whether lockscreen weather is enabled.
+ *
+ * @hide
+ */
+ public static final String LOCK_SCREEN_WEATHER_ENABLED = "lockscreen_weather_enabled";
+
+ /**
* These entries are considered common between the personal and the managed profile,
* since the managed profile doesn't get to change them.
*/
@@ -15498,6 +15519,25 @@
public static final String AUDIO_SAFE_VOLUME_STATE = "audio_safe_volume_state";
/**
+ * Persisted safe hearding current CSD value. Values are stored as float percentages where
+ * 1.f represents 100% sound dose has been reached.
+ * @hide
+ */
+ public static final String AUDIO_SAFE_CSD_CURRENT_VALUE = "audio_safe_csd_current_value";
+
+ /**
+ * Persisted safe hearding next CSD warning value. Values are stored as float percentages.
+ * @hide
+ */
+ public static final String AUDIO_SAFE_CSD_NEXT_WARNING = "audio_safe_csd_next_warning";
+
+ /**
+ * Persisted safe hearding dose records (see {@link android.media.SoundDoseRecord})
+ * @hide
+ */
+ public static final String AUDIO_SAFE_CSD_DOSE_RECORDS = "audio_safe_csd_dose_records";
+
+ /**
* URL for tzinfo (time zone) updates
* @hide
*/
@@ -15879,6 +15919,36 @@
"user_preferred_resolution_width";
/**
+ * The HDR output mode chosen by the user. This is one of:
+ * {@link android.hardware.display.HdrConversionMode#HDR_CONVERSION_PASSTHROUGH},
+ * {@link android.hardware.display.HdrConversionMode#HDR_CONVERSION_SYSTEM},
+ * {@link android.hardware.display.HdrConversionMode#HDR_CONVERSION_FORCE}.
+ *
+ * @hide
+ */
+ @TestApi
+ @Readable
+ public static final String HDR_CONVERSION_MODE = "hdr_conversion_mode";
+
+ /**
+ * The output HDR type chosen by the user in case when {@link #HDR_CONVERSION_MODE} is
+ * {@link #HDR_CONVERSION_FORCE}. This is one of:
+ * {@link android.view.Display.HdrCapabilities#HDR_TYPE_INVALID},
+ * {@link android.view.Display.HdrCapabilities#HDR_TYPE_DOLBY_VISION},
+ * {@link android.view.Display.HdrCapabilities#HDR_TYPE_HDR10},
+ * {@link android.view.Display.HdrCapabilities#HDR_TYPE_HLG},
+ * {@link android.view.Display.HdrCapabilities#HDR_TYPE_HDR10_PLUS}
+ * <p>
+ * The value is {@link android.view.Display.HdrCapabilities#HDR_TYPE_INVALID} when user
+ * chooses SDR output type. </p>
+ *
+ * @hide
+ */
+ @TestApi
+ @Readable
+ public static final String HDR_FORCE_CONVERSION_TYPE = "hdr_force_conversion_type";
+
+ /**
* The name of the device
*/
@Readable
diff --git a/core/java/android/service/assist/OWNERS b/core/java/android/service/assist/OWNERS
new file mode 100644
index 0000000..533b1f1
--- /dev/null
+++ b/core/java/android/service/assist/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 351486
+
+include /core/java/android/view/autofill/OWNERS
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt b/core/java/android/service/assist/classification/FieldClassification.aidl
similarity index 64%
copy from packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
copy to core/java/android/service/assist/classification/FieldClassification.aidl
index 497c272..7d0c078 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
+++ b/core/java/android/service/assist/classification/FieldClassification.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,12 +14,6 @@
* limitations under the License.
*/
-package com.android.credentialmanager.jetpack.developer
+package android.service.assist.classification;
-/**
- * Internal exception used to indicate a parsing error while converting from a framework type to
- * a jetpack type.
- *
- * @hide
- */
-internal class FrameworkClassParsingException : Exception()
\ No newline at end of file
+parcelable FieldClassification;
diff --git a/core/java/android/service/assist/classification/FieldClassification.java b/core/java/android/service/assist/classification/FieldClassification.java
new file mode 100644
index 0000000..0ea8112
--- /dev/null
+++ b/core/java/android/service/assist/classification/FieldClassification.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.assist.classification;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArraySet;
+import android.view.autofill.AutofillId;
+
+
+import com.android.internal.util.DataClass;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Represents a classified field from the detection service.
+ */
+// TODO(b/266930067): Once @SystemApi is supported, use genSetters and genConstructor.
+@DataClass(
+ genToString = true,
+ genConstructor = false
+)
+public final class FieldClassification implements Parcelable {
+
+ /**
+ * Autofill id of the detected field
+ */
+ private final @NonNull AutofillId mAutofillId;
+
+ /**
+ * Detected fields types represented as autofill hints
+ *
+ * A particular field can be detected as multiple types. For eg: A sign-in field may take in a
+ * username, an email address or a phone number. In such cases, it should be detected as
+ * "username", "emailAddress" and "phoneNumber"
+ *
+ * The value of these hints are contained in androidx.autofill.HintConstants
+ */
+ private final @NonNull Set<String> mHints;
+
+
+ /**
+ * Group hints are the hints that may represent the group of related hints (including
+ * themselves). The value of these group hints are contained in androidx.autofill.HintConstants
+ *
+ * <p>
+ *
+ * "creditCardNumber" is the group hint for hints containing credit card related fields:
+ * "creditCardNumber", "creditCardExpirationDate", "creditCardExpirationDay",
+ * "creditCardExpirationMonth", "creditCardExpirationYear", "creditCardSecurityCode",
+ *
+ * <p>
+ *
+ * "postalAddress" is the group hint for hints all postal address related fields:
+ * "postalAddress", "streetAddress", "aptNumber", "dependentLocality", "extendedAddress",
+ * "postalCode", "extendedPostalCode", "addressLocality", "addressRegion", "addressCountry".
+ *
+ * <p>
+ *
+ * "phoneNumber" is the group hint for hints all phone number related fields: "phoneNumber",
+ * "phoneNumberDevice", "phoneNational", "phoneCountryCode".
+ *
+ * <p>
+ *
+ * "personName" is the group hint for hints all name related fields: "personName",
+ * "personFamilyName", "personGivenName", "personMiddleName", "personMiddleInitial",
+ * "personNamePrefix", "personNameSuffix" .
+ *
+ * <p>
+ *
+ * "birthDateFull" is the group hint for hints containing birthday related fields:
+ * "birthDateFull", "birthDateMonth", "birthDateYear",
+ *
+ * @hide
+ */
+ private final @NonNull Set<String> mGroupHints;
+
+ /**
+ * Autofill id of the detected field.
+ */
+ public @NonNull AutofillId getAutofillId() {
+ return mAutofillId;
+ }
+
+ /**
+ * Detected fields types represented as autofill hints.
+ *
+ * A particular field can be detected as multiple types. For eg: A sign-in field may take in a
+ * username, an email address or a phone number. In such cases, it should be detected as
+ * "username", "emailAddress" and "phoneNumber"
+ *
+ * The value of these hints are contained in androidx.autofill.HintConstants
+ */
+ public @NonNull Set<String> getHints() {
+ return mHints;
+ }
+
+ /**
+ * Group hints are the hints that may represent the group of related hints (including
+ * themselves). The value of these group hints are contained in androidx.autofill.HintConstants
+ *
+ * <p>
+ *
+ * "creditCardNumber" is the group hint for hints containing credit card related fields:
+ * "creditCardNumber", "creditCardExpirationDate", "creditCardExpirationDay",
+ * "creditCardExpirationMonth", "creditCardExpirationYear", "creditCardSecurityCode",
+ *
+ * <p>
+ *
+ * "postalAddress" is the group hint for hints all postal address related fields:
+ * "postalAddress", "streetAddress", "aptNumber", "dependentLocality", "extendedAddress",
+ * "postalCode", "extendedPostalCode", "addressLocality", "addressRegion", "addressCountry".
+ *
+ * <p>
+ *
+ * "phoneNumber" is the group hint for hints all phone number related fields: "phoneNumber",
+ * "phoneNumberDevice", "phoneNational", "phoneCountryCode".
+ *
+ * <p>
+ *
+ * "personName" is the group hint for hints all name related fields: "personName",
+ * "personFamilyName", "personGivenName", "personMiddleName", "personMiddleInitial",
+ * "personNamePrefix", "personNameSuffix" .
+ *
+ * <p>
+ *
+ * "birthDateFull" is the group hint for hints containing birthday related fields:
+ * "birthDateFull", "birthDateMonth", "birthDateYear",
+ *
+ * @hide
+ */
+ @SystemApi
+ public @NonNull Set<String> getGroupHints() {
+ return mGroupHints;
+ }
+
+ static Set<String> unparcelHints(Parcel in) {
+ List<String> hints = new java.util.ArrayList<>();
+ in.readStringList(hints);
+ return new ArraySet<>(hints);
+ }
+
+ void parcelHints(Parcel dest, int flags) {
+ dest.writeStringList(new ArrayList<>(mHints));
+ }
+
+ static Set<String> unparcelGroupHints(Parcel in) {
+ List<String> groupHints = new java.util.ArrayList<>();
+ in.readStringList(groupHints);
+ return new ArraySet<>(groupHints);
+ }
+
+ void parcelGroupHints(Parcel dest, int flags) {
+ dest.writeStringList(new ArrayList<>(mGroupHints));
+ }
+
+ /**
+ * Creates a new FieldClassification.
+ *
+ * @param autofillId
+ * Autofill id of the detected field
+ * @param hints
+ * Detected fields types represented as autofill hints.
+ * A particular field can be detected as multiple types. For eg: A sign-in field may take in
+ * a username, an email address or a phone number. In such cases, it should be detected as
+ * "username", "emailAddress" and "phoneNumber"
+ */
+ public FieldClassification(
+ @NonNull AutofillId autofillId,
+ @NonNull Set<String> hints) {
+ this(autofillId, hints, new ArraySet<>());
+ }
+
+ /**
+ * Creates a new FieldClassification.
+ *
+ * @param autofillId Autofill id of the detected field
+ * @param hints Detected fields types represented as autofill hints A particular field can be
+ * detected as multiple types. For eg: A sign-in field may take in a username, an email
+ * address or a phone number. In such cases, it should be detected as "username",
+ * "emailAddress" and "phoneNumber"
+ * @param groupHints Hints that may represent the group of related hints (including themselves).
+ * The value of these group hints are contained in androidx.autofill.HintConstants.
+ * See {@link #getGroupHints()} for more details
+ * @hide
+ */
+ @SystemApi
+ @DataClass.Generated.Member
+ public FieldClassification(
+ @NonNull AutofillId autofillId,
+ @NonNull Set<String> hints,
+ @NonNull Set<String> groupHints) {
+ this.mAutofillId = autofillId;
+// com.android.internal.util.AnnotationValidations.validate(
+// NonNull.class, null, mAutofillId);
+ this.mHints = hints;
+// com.android.internal.util.AnnotationValidations.validate(
+// NonNull.class, null, mHints);
+ this.mGroupHints = groupHints;
+// com.android.internal.util.AnnotationValidations.validate(
+// NonNull.class, null, mGroupHints);
+ }
+
+
+
+ // Code below generated by codegen v1.0.23.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/assist/classification/FieldClassification.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "FieldClassification { " +
+ "autofillId = " + mAutofillId + ", " +
+ "hints = " + mHints + ", " +
+ "groupHints = " + mGroupHints +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ dest.writeTypedObject(mAutofillId, flags);
+ parcelHints(dest, flags);
+ parcelGroupHints(dest, flags);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ FieldClassification(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ AutofillId autofillId = (AutofillId) in.readTypedObject(AutofillId.CREATOR);
+ Set<String> hints = unparcelHints(in);
+ Set<String> groupHints = unparcelGroupHints(in);
+
+ this.mAutofillId = autofillId;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mAutofillId);
+ this.mHints = hints;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mHints);
+ this.mGroupHints = groupHints;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mGroupHints);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<FieldClassification> CREATOR
+ = new Parcelable.Creator<FieldClassification>() {
+ @Override
+ public FieldClassification[] newArray(int size) {
+ return new FieldClassification[size];
+ }
+
+ @Override
+ public FieldClassification createFromParcel(@NonNull Parcel in) {
+ return new FieldClassification(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1675320464097L,
+ codegenVersion = "1.0.23",
+ sourceFile = "frameworks/base/core/java/android/service/assist/classification/FieldClassification.java",
+ inputSignatures = "private final @android.annotation.NonNull android.view.autofill.AutofillId mAutofillId\nprivate final @android.annotation.NonNull java.util.Set<java.lang.String> mHints\nprivate final @android.annotation.NonNull java.util.Set<java.lang.String> mGroupHints\npublic @android.annotation.NonNull android.view.autofill.AutofillId getAutofillId()\npublic @android.annotation.NonNull java.util.Set<java.lang.String> getHints()\npublic @android.annotation.SystemApi @android.annotation.NonNull java.util.Set<java.lang.String> getGroupHints()\nstatic java.util.Set<java.lang.String> unparcelHints(android.os.Parcel)\n void parcelHints(android.os.Parcel,int)\nstatic java.util.Set<java.lang.String> unparcelGroupHints(android.os.Parcel)\n void parcelGroupHints(android.os.Parcel,int)\nclass FieldClassification extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genConstructor=false)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/core/java/android/service/assist/classification/FieldClassificationRequest.aidl b/core/java/android/service/assist/classification/FieldClassificationRequest.aidl
new file mode 100644
index 0000000..740c5cb9
--- /dev/null
+++ b/core/java/android/service/assist/classification/FieldClassificationRequest.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2023, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.assist.classification;
+
+parcelable FieldClassificationRequest;
\ No newline at end of file
diff --git a/core/java/android/service/assist/classification/FieldClassificationRequest.java b/core/java/android/service/assist/classification/FieldClassificationRequest.java
new file mode 100644
index 0000000..0afcca9
--- /dev/null
+++ b/core/java/android/service/assist/classification/FieldClassificationRequest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.assist.classification;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.app.assist.AssistStructure;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.DataClass;
+
+/**
+ * Represents a request to detect fields on an activity.
+ * @hide
+ */
+@SystemApi
+@DataClass(
+ genToString = true
+)
+public final class FieldClassificationRequest implements Parcelable {
+ private final @NonNull AssistStructure mAssistStructure;
+
+
+
+ // Code below generated by codegen v1.0.23.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/assist/classification/FieldClassificationRequest.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ @DataClass.Generated.Member
+ public FieldClassificationRequest(
+ @NonNull AssistStructure assistStructure) {
+ this.mAssistStructure = assistStructure;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mAssistStructure);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public @NonNull AssistStructure getAssistStructure() {
+ return mAssistStructure;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "FieldClassificationRequest { " +
+ "assistStructure = " + mAssistStructure +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ dest.writeTypedObject(mAssistStructure, flags);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ FieldClassificationRequest(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ AssistStructure assistStructure = (AssistStructure) in.readTypedObject(AssistStructure.CREATOR);
+
+ this.mAssistStructure = assistStructure;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mAssistStructure);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<FieldClassificationRequest> CREATOR
+ = new Parcelable.Creator<FieldClassificationRequest>() {
+ @Override
+ public FieldClassificationRequest[] newArray(int size) {
+ return new FieldClassificationRequest[size];
+ }
+
+ @Override
+ public FieldClassificationRequest createFromParcel(@NonNull Parcel in) {
+ return new FieldClassificationRequest(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1675320491692L,
+ codegenVersion = "1.0.23",
+ sourceFile = "frameworks/base/core/java/android/service/assist/classification/FieldClassificationRequest.java",
+ inputSignatures = "private final @android.annotation.NonNull android.app.assist.AssistStructure mAssistStructure\nclass FieldClassificationRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt b/core/java/android/service/assist/classification/FieldClassificationResponse.aidl
similarity index 64%
copy from packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
copy to core/java/android/service/assist/classification/FieldClassificationResponse.aidl
index 497c272..1b0de85 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
+++ b/core/java/android/service/assist/classification/FieldClassificationResponse.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,12 +14,6 @@
* limitations under the License.
*/
-package com.android.credentialmanager.jetpack.developer
+package android.service.assist.classification;
-/**
- * Internal exception used to indicate a parsing error while converting from a framework type to
- * a jetpack type.
- *
- * @hide
- */
-internal class FrameworkClassParsingException : Exception()
\ No newline at end of file
+parcelable FieldClassificationResponse;
diff --git a/core/java/android/service/assist/classification/FieldClassificationResponse.java b/core/java/android/service/assist/classification/FieldClassificationResponse.java
new file mode 100644
index 0000000..faa9488
--- /dev/null
+++ b/core/java/android/service/assist/classification/FieldClassificationResponse.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.assist.classification;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArraySet;
+
+import com.android.internal.util.DataClass;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+
+/**
+ * Represents a response from detection service.
+ * @hide
+ */
+@SystemApi
+@DataClass(
+ genToString = true
+)
+public final class FieldClassificationResponse implements Parcelable {
+
+ /**
+ * List of classified fields
+ */
+ private final @NonNull Set<FieldClassification> mClassifications;
+
+ static Set<FieldClassification> unparcelClassifications(Parcel in) {
+ List<FieldClassification> detections = new java.util.ArrayList<>();
+ in.readParcelableList(
+ detections, FieldClassification.class.getClassLoader(), FieldClassification.class);
+ return new ArraySet<>(detections);
+ }
+
+ void parcelClassifications(Parcel dest, int flags) {
+ dest.writeParcelableList(new ArrayList<>(mClassifications), flags);
+ }
+
+
+
+ // Code below generated by codegen v1.0.23.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/assist/classification/FieldClassificationResponse.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /**
+ * Creates a new FieldClassificationResponse.
+ *
+ * @param classifications
+ * List of classified fields
+ */
+ @DataClass.Generated.Member
+ public FieldClassificationResponse(
+ @NonNull Set<FieldClassification> classifications) {
+ this.mClassifications = classifications;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mClassifications);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * List of classified fields
+ */
+ @DataClass.Generated.Member
+ public @NonNull Set<FieldClassification> getClassifications() {
+ return mClassifications;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "FieldClassificationResponse { " +
+ "classifications = " + mClassifications +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ parcelClassifications(dest, flags);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ FieldClassificationResponse(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ Set<FieldClassification> classifications = unparcelClassifications(in);
+
+ this.mClassifications = classifications;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mClassifications);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<FieldClassificationResponse> CREATOR
+ = new Parcelable.Creator<FieldClassificationResponse>() {
+ @Override
+ public FieldClassificationResponse[] newArray(int size) {
+ return new FieldClassificationResponse[size];
+ }
+
+ @Override
+ public FieldClassificationResponse createFromParcel(@NonNull Parcel in) {
+ return new FieldClassificationResponse(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1675320458276L,
+ codegenVersion = "1.0.23",
+ sourceFile = "frameworks/base/core/java/android/service/assist/classification/FieldClassificationResponse.java",
+ inputSignatures = "private final @android.annotation.NonNull java.util.Set<android.service.assist.classification.FieldClassification> mClassifications\nstatic java.util.Set<android.service.assist.classification.FieldClassification> unparcelClassifications(android.os.Parcel)\n void parcelClassifications(android.os.Parcel,int)\nclass FieldClassificationResponse extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/core/java/android/service/assist/classification/FieldClassificationService.java b/core/java/android/service/assist/classification/FieldClassificationService.java
new file mode 100644
index 0000000..abffdbf
--- /dev/null
+++ b/core/java/android/service/assist/classification/FieldClassificationService.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.assist.classification;
+
+import android.annotation.CallSuper;
+import android.annotation.NonNull;
+import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.BaseBundle;
+import android.os.Build;
+import android.os.CancellationSignal;
+import android.os.IBinder;
+import android.os.ICancellationSignal;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * A service using {@link android.app.assist.AssistStructure} to detect fields on the screen.
+ * Service may use classifiers to look at the un-stripped AssistStructure to make informed decision
+ * and classify the fields.
+ *
+ * Currently, it's used to detect the field types for the Autofill Framework to provide relevant
+ * autofill suggestions to the user.
+ *
+ *
+ * The methods are invoked on the binder threads.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class FieldClassificationService extends Service {
+
+ private static final String TAG = FieldClassificationService.class.getSimpleName();
+
+ static boolean sDebug = Build.IS_USER ? false : true;
+ static boolean sVerbose = false;
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ * To be supported, the service must also require the
+ * {@link android.Manifest.permission#BIND_FIELD_CLASSIFICATION_SERVICE} permission so
+ * that other applications can not abuse it.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE =
+ "android.service.assist.classification.FieldClassificationService";
+
+ // Used for metrics / debug only
+ private ComponentName mServiceComponentName;
+
+ private final class FieldClassificationServiceImpl
+ extends IFieldClassificationService.Stub {
+
+ @Override
+ public void onConnected(boolean debug, boolean verbose) {
+ handleOnConnected(debug, verbose);
+ }
+
+ @Override
+ public void onDisconnected() {
+ handleOnDisconnected();
+ }
+
+ @Override
+ public void onFieldClassificationRequest(
+ FieldClassificationRequest request, IFieldClassificationCallback callback) {
+ handleOnClassificationRequest(request, callback);
+ }
+ };
+
+ @CallSuper
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ BaseBundle.setShouldDefuse(true);
+ }
+
+ /** @hide */
+ @Override
+ public final IBinder onBind(Intent intent) {
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ mServiceComponentName = intent.getComponent();
+ return new FieldClassificationServiceImpl().asBinder();
+ }
+ Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
+ return null;
+ }
+
+ /**
+ * Called when the Android system connects to service.
+ *
+ * <p>You should generally do initialization here rather than in {@link #onCreate}.
+ */
+ public void onConnected() {
+ }
+
+ /**
+ * Requests the service to handle field classification request.
+ * @param cancellationSignal signal for observing cancellation requests. The system will use
+ * this to notify you that the detection result is no longer needed and the service should
+ * stop handling this detection request in order to save resources.
+ * @param outcomeReceiver object used to notify the result of the request. Service <b>must</b>
+ * call {@link OutcomeReceiver<>#onResult(FieldClassificationResponse)}.
+ */
+ public abstract void onClassificationRequest(
+ @NonNull FieldClassificationRequest request,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull OutcomeReceiver<FieldClassificationResponse, Exception> outcomeReceiver);
+
+ /**
+ * Called when the Android system disconnects from the service.
+ *
+ * <p> At this point this service may no longer be an active
+ * {@link FieldClassificationService}.
+ */
+ public void onDisconnected() {
+ }
+
+ private void handleOnConnected(boolean debug, boolean verbose) {
+ if (sDebug || debug) {
+ Log.d(TAG, "handleOnConnected(): debug=" + debug + ", verbose=" + verbose);
+ }
+ sDebug = debug;
+ sVerbose = verbose;
+ onConnected();
+ }
+
+ private void handleOnDisconnected() {
+ onDisconnected();
+ }
+
+ private void handleOnClassificationRequest(
+ FieldClassificationRequest request, @NonNull IFieldClassificationCallback callback) {
+
+ final ICancellationSignal transport = CancellationSignal.createTransport();
+ final CancellationSignal cancellationSignal = CancellationSignal.fromTransport(transport);
+ onClassificationRequest(
+ request,
+ cancellationSignal,
+ new OutcomeReceiver<FieldClassificationResponse, Exception>() {
+ @Override
+ public void onResult(FieldClassificationResponse result) {
+ try {
+ callback.onSuccess(result);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ @Override
+ public void onError(Exception e) {
+ try {
+ callback.onFailure();
+ } catch (RemoteException ex) {
+ ex.rethrowFromSystemServer();
+ }
+ }
+ });
+ }
+}
+
diff --git a/core/java/android/service/assist/classification/IFieldClassificationCallback.aidl b/core/java/android/service/assist/classification/IFieldClassificationCallback.aidl
new file mode 100644
index 0000000..ca9e939
--- /dev/null
+++ b/core/java/android/service/assist/classification/IFieldClassificationCallback.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.assist.classification;
+
+import android.os.Bundle;
+import android.os.ICancellationSignal;
+
+import android.service.assist.classification.FieldClassificationResponse;
+
+import java.util.List;
+
+/**
+ * Interface to receive the result of an autofill request.
+ *
+ * @hide
+ */
+interface IFieldClassificationCallback {
+
+ void onCancellable(in ICancellationSignal cancellation);
+
+ void onSuccess(in FieldClassificationResponse response);
+
+ void onFailure();
+
+ boolean isCompleted();
+
+ void cancel();
+}
diff --git a/core/java/android/service/assist/classification/IFieldClassificationService.aidl b/core/java/android/service/assist/classification/IFieldClassificationService.aidl
new file mode 100644
index 0000000..a93688d
--- /dev/null
+++ b/core/java/android/service/assist/classification/IFieldClassificationService.aidl
@@ -0,0 +1,38 @@
+
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.assist.classification;
+
+import android.content.ComponentName;
+import android.os.IBinder;
+import android.service.assist.classification.IFieldClassificationCallback;
+import android.service.assist.classification.FieldClassificationRequest;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.view.inputmethod.InlineSuggestionsRequest;
+import java.util.List;
+/**
+ * Interface from the system to an Autofill classification service.
+ *
+ * @hide
+ */
+oneway interface IFieldClassificationService {
+ void onConnected(boolean debug, boolean verbose);
+ void onDisconnected();
+ void onFieldClassificationRequest(
+ in FieldClassificationRequest request, in IFieldClassificationCallback callback);
+}
diff --git a/core/java/android/service/credentials/BeginGetCredentialRequest.java b/core/java/android/service/credentials/BeginGetCredentialRequest.java
index e375cdd..5d040db 100644
--- a/core/java/android/service/credentials/BeginGetCredentialRequest.java
+++ b/core/java/android/service/credentials/BeginGetCredentialRequest.java
@@ -19,7 +19,6 @@
import android.annotation.NonNull;
import android.app.PendingIntent;
import android.content.Intent;
-import android.credentials.GetCredentialOption;
import android.os.Parcel;
import android.os.Parcelable;
@@ -33,7 +32,7 @@
/**
* Query stage request for getting user's credentials from a given credential provider.
*
- * <p>This request contains a list of {@link GetCredentialOption} that have parameters
+ * <p>This request contains a list of {@link BeginGetCredentialOption} that have parameters
* to be used to query credentials, and return a list of {@link CredentialEntry} to be set
* on the {@link BeginGetCredentialResponse}. This list is then shown to the user on a selector.
*
diff --git a/core/java/android/service/credentials/CredentialProviderService.java b/core/java/android/service/credentials/CredentialProviderService.java
index ee386c3..f92fd3a 100644
--- a/core/java/android/service/credentials/CredentialProviderService.java
+++ b/core/java/android/service/credentials/CredentialProviderService.java
@@ -308,7 +308,7 @@
*
* <p>This API denotes a query stage request for getting user's credentials from a given
* credential provider. The request contains a list of
- * {@link android.credentials.GetCredentialOption} that have parameters to be used for
+ * {@link BeginGetCredentialOption} that have parameters to be used for
* populating candidate credentials, as a list of {@link CredentialEntry} to be set
* on the {@link BeginGetCredentialResponse}. This list is then shown to the user on a
* selector.
diff --git a/core/java/android/service/credentials/GetCredentialRequest.java b/core/java/android/service/credentials/GetCredentialRequest.java
index 4b946f0..7cdccc6 100644
--- a/core/java/android/service/credentials/GetCredentialRequest.java
+++ b/core/java/android/service/credentials/GetCredentialRequest.java
@@ -17,7 +17,7 @@
package android.service.credentials;
import android.annotation.NonNull;
-import android.credentials.GetCredentialOption;
+import android.credentials.CredentialOption;
import android.os.Parcel;
import android.os.Parcelable;
@@ -32,28 +32,30 @@
*/
public final class GetCredentialRequest implements Parcelable {
/** Calling package of the app requesting for credentials. */
- private final @NonNull CallingAppInfo mCallingAppInfo;
+ @NonNull
+ private final CallingAppInfo mCallingAppInfo;
/**
* Holds parameters to be used for retrieving a specific type of credential.
*/
- private final @NonNull GetCredentialOption mGetCredentialOption;
+ @NonNull
+ private final CredentialOption mCredentialOption;
public GetCredentialRequest(@NonNull CallingAppInfo callingAppInfo,
- @NonNull GetCredentialOption getCredentialOption) {
+ @NonNull CredentialOption credentialOption) {
this.mCallingAppInfo = callingAppInfo;
- this.mGetCredentialOption = getCredentialOption;
+ this.mCredentialOption = credentialOption;
}
private GetCredentialRequest(@NonNull Parcel in) {
mCallingAppInfo = in.readTypedObject(CallingAppInfo.CREATOR);
AnnotationValidations.validate(NonNull.class, null, mCallingAppInfo);
- mGetCredentialOption = in.readTypedObject(GetCredentialOption.CREATOR);
- AnnotationValidations.validate(NonNull.class, null, mGetCredentialOption);
+ mCredentialOption = in.readTypedObject(CredentialOption.CREATOR);
+ AnnotationValidations.validate(NonNull.class, null, mCredentialOption);
}
- public static final @NonNull Creator<GetCredentialRequest> CREATOR =
- new Creator<GetCredentialRequest>() {
+ @NonNull public static final Creator<GetCredentialRequest> CREATOR =
+ new Creator<>() {
@Override
public GetCredentialRequest createFromParcel(Parcel in) {
return new GetCredentialRequest(in);
@@ -73,20 +75,22 @@
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeTypedObject(mCallingAppInfo, flags);
- dest.writeTypedObject(mGetCredentialOption, flags);
+ dest.writeTypedObject(mCredentialOption, flags);
}
/**
* Returns info pertaining to the app requesting credentials.
*/
- public @NonNull CallingAppInfo getCallingAppInfo() {
+ @NonNull
+ public CallingAppInfo getCallingAppInfo() {
return mCallingAppInfo;
}
/**
* Returns the parameters needed to return a given type of credential.
*/
- public @NonNull GetCredentialOption getGetCredentialOption() {
- return mGetCredentialOption;
+ @NonNull
+ public CredentialOption getCredentialOption() {
+ return mCredentialOption;
}
}
diff --git a/core/java/android/service/search/ISearchUiService.aidl b/core/java/android/service/search/ISearchUiService.aidl
index aae66ca..bc6d421 100644
--- a/core/java/android/service/search/ISearchUiService.aidl
+++ b/core/java/android/service/search/ISearchUiService.aidl
@@ -37,5 +37,11 @@
void onNotifyEvent(in SearchSessionId sessionId, in Query input, in SearchTargetEvent event);
+ void onRegisterEmptyQueryResultUpdateCallback (in SearchSessionId sessionId, in ISearchCallback callback);
+
+ void onRequestEmptyQueryResultUpdate(in SearchSessionId sessionId);
+
+ void onUnregisterEmptyQueryResultUpdateCallback(in SearchSessionId sessionId, in ISearchCallback callback);
+
void onDestroy(in SearchSessionId sessionId);
}
diff --git a/core/java/android/service/search/SearchUiService.java b/core/java/android/service/search/SearchUiService.java
index 02d41ef..55a96fa 100644
--- a/core/java/android/service/search/SearchUiService.java
+++ b/core/java/android/service/search/SearchUiService.java
@@ -20,6 +20,7 @@
import android.annotation.CallSuper;
import android.annotation.MainThread;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.app.Service;
import android.app.search.ISearchCallback;
@@ -35,8 +36,10 @@
import android.os.Looper;
import android.os.RemoteException;
import android.service.search.ISearchUiService.Stub;
+import android.util.ArrayMap;
import android.util.Slog;
+import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
@@ -66,6 +69,9 @@
public static final String SERVICE_INTERFACE =
"android.service.search.SearchUiService";
+ private final ArrayMap<SearchSessionId, ArrayList<CallbackWrapper>>
+ mSessionEmptyQueryResultCallbacks = new ArrayMap<>();
+
private Handler mHandler;
private final android.service.search.ISearchUiService mInterface = new Stub() {
@@ -87,7 +93,7 @@
mHandler.sendMessage(
obtainMessage(SearchUiService::onQuery,
SearchUiService.this, sessionId, input,
- new CallbackWrapper(callback)));
+ new CallbackWrapper(callback, null)));
}
@Override
@@ -98,6 +104,28 @@
}
@Override
+ public void onRegisterEmptyQueryResultUpdateCallback(SearchSessionId sessionId,
+ ISearchCallback callback) {
+ mHandler.sendMessage(
+ obtainMessage(SearchUiService::doRegisterEmptyQueryResultUpdateCallback,
+ SearchUiService.this, sessionId, callback));
+ }
+
+ @Override
+ public void onRequestEmptyQueryResultUpdate(SearchSessionId sessionId) {
+ mHandler.sendMessage(obtainMessage(SearchUiService::doRequestEmptyQueryResultUpdate,
+ SearchUiService.this, sessionId));
+ }
+
+ @Override
+ public void onUnregisterEmptyQueryResultUpdateCallback(SearchSessionId sessionId,
+ ISearchCallback callback) {
+ mHandler.sendMessage(
+ obtainMessage(SearchUiService::doUnregisterEmptyQueryResultUpdateCallback,
+ SearchUiService.this, sessionId, callback));
+ }
+
+ @Override
public void onDestroy(SearchSessionId sessionId) {
mHandler.sendMessage(
obtainMessage(SearchUiService::doDestroy,
@@ -126,21 +154,23 @@
/**
* Creates a new search session.
*
+ * @removed
* @deprecated this is method will be removed as soon as
* {@link #onSearchSessionCreated(SearchContext, SearchSessionId)}
* is adopted by the service.
- *
- * @removed
*/
@Deprecated
public void onCreateSearchSession(@NonNull SearchContext context,
- @NonNull SearchSessionId sessionId) {}
+ @NonNull SearchSessionId sessionId) {
+ }
/**
* A new search session is created.
*/
public void onSearchSessionCreated(@NonNull SearchContext context,
- @NonNull SearchSessionId sessionId) {}
+ @NonNull SearchSessionId sessionId) {
+ mSessionEmptyQueryResultCallbacks.put(sessionId, new ArrayList<>());
+ }
/**
* Called by the client to request search results using a query string.
@@ -161,6 +191,98 @@
@NonNull Query query,
@NonNull SearchTargetEvent event);
+ private void doRegisterEmptyQueryResultUpdateCallback(@NonNull SearchSessionId sessionId,
+ @NonNull ISearchCallback callback) {
+ final ArrayList<CallbackWrapper> callbacks = mSessionEmptyQueryResultCallbacks.get(
+ sessionId);
+ if (callbacks == null) {
+ Slog.e(TAG, "Failed to register for updates for unknown session: " + sessionId);
+ return;
+ }
+
+ final CallbackWrapper wrapper = findCallbackWrapper(callbacks, callback);
+ if (wrapper == null) {
+ callbacks.add(new CallbackWrapper(callback,
+ callbackWrapper ->
+ mHandler.post(() ->
+ removeCallbackWrapper(callbacks, callbackWrapper))));
+ if (callbacks.size() == 1) {
+ onStartUpdateEmptyQueryResult();
+ }
+ }
+ }
+
+ /**
+ * Called when the first empty query result callback is registered. Service provider may make
+ * their own decision whether to generate data if no callback is registered to optimize for
+ * system health.
+ */
+ @MainThread
+ public void onStartUpdateEmptyQueryResult() {}
+
+ private void doRequestEmptyQueryResultUpdate(@NonNull SearchSessionId sessionId) {
+ // Just an optimization, if there are no callbacks, then don't bother notifying the service
+ final ArrayList<CallbackWrapper> callbacks = mSessionEmptyQueryResultCallbacks.get(
+ sessionId);
+ if (callbacks != null && !callbacks.isEmpty()) {
+ onRequestEmptyQueryResultUpdate(sessionId);
+ }
+ }
+
+ /**
+ * Called by a client to request empty query search target result for zero state. This method
+ * is only called if there are one or more empty query result update callbacks registered.
+ *
+ * @see #updateEmptyQueryResult(SearchSessionId, List)
+ */
+ @MainThread
+ public void onRequestEmptyQueryResultUpdate(@NonNull SearchSessionId sessionId) {}
+
+ private void doUnregisterEmptyQueryResultUpdateCallback(@NonNull SearchSessionId sessionId,
+ @NonNull ISearchCallback callback) {
+ final ArrayList<CallbackWrapper> callbacks = mSessionEmptyQueryResultCallbacks.get(
+ sessionId);
+ if (callbacks == null) {
+ Slog.e(TAG, "Failed to unregister for updates for unknown session: " + sessionId);
+ return;
+ }
+
+ final CallbackWrapper wrapper = findCallbackWrapper(callbacks, callback);
+ removeCallbackWrapper(callbacks, wrapper);
+ }
+
+ /**
+ * Finds the callback wrapper for the given callback.
+ */
+ private CallbackWrapper findCallbackWrapper(ArrayList<CallbackWrapper> callbacks,
+ ISearchCallback callback) {
+ for (int i = callbacks.size() - 1; i >= 0; i--) {
+ if (callbacks.get(i).isCallback(callback)) {
+ return callbacks.get(i);
+ }
+ }
+ return null;
+ }
+
+ private void removeCallbackWrapper(@Nullable ArrayList<CallbackWrapper> callbacks,
+ @Nullable CallbackWrapper wrapper) {
+ if (callbacks == null || wrapper == null) {
+ return;
+ }
+ callbacks.remove(wrapper);
+ wrapper.destroy();
+ if (callbacks.isEmpty()) {
+ onStopUpdateEmptyQueryResult();
+ }
+ }
+
+ /**
+ * Called when there are no longer any empty query result callbacks registered. Service
+ * provider can choose to stop generating data to optimize for system health.
+ */
+ @MainThread
+ public void onStopUpdateEmptyQueryResult() {}
+
private void doDestroy(@NonNull SearchSessionId sessionId) {
super.onDestroy();
onDestroy(sessionId);
@@ -172,14 +294,49 @@
@MainThread
public abstract void onDestroy(@NonNull SearchSessionId sessionId);
- private static final class CallbackWrapper implements Consumer<List<SearchTarget>> {
+ /**
+ * Used by the service provider to send back results the client app. The can be called
+ * in response to {@link #onRequestEmptyQueryResultUpdate(SearchSessionId)} or proactively as
+ * a result of changes in zero state data.
+ */
+ public final void updateEmptyQueryResult(@NonNull SearchSessionId sessionId,
+ @NonNull List<SearchTarget> targets) {
+ List<CallbackWrapper> callbacks = mSessionEmptyQueryResultCallbacks.get(sessionId);
+ if (callbacks != null) {
+ for (CallbackWrapper callback : callbacks) {
+ callback.accept(targets);
+ }
+ }
+ }
+
+ private static final class CallbackWrapper implements Consumer<List<SearchTarget>>,
+ IBinder.DeathRecipient {
private ISearchCallback mCallback;
+ private final Consumer<CallbackWrapper> mOnBinderDied;
- CallbackWrapper(ISearchCallback callback) {
+ CallbackWrapper(ISearchCallback callback,
+ @Nullable Consumer<CallbackWrapper> onBinderDied) {
mCallback = callback;
+ mOnBinderDied = onBinderDied;
+ if (mOnBinderDied != null) {
+ try {
+ mCallback.asBinder().linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to link to death:" + e);
+ }
+ }
}
+ public boolean isCallback(@NonNull ISearchCallback callback) {
+ if (mCallback == null) {
+ Slog.e(TAG, "Callback is null, likely the binder has died.");
+ return false;
+ }
+ return mCallback.asBinder().equals(callback.asBinder());
+ }
+
+
@Override
public void accept(List<SearchTarget> searchTargets) {
try {
@@ -193,5 +350,20 @@
Slog.e(TAG, "Error sending result:" + e);
}
}
+
+ public void destroy() {
+ if (mCallback != null && mOnBinderDied != null) {
+ mCallback.asBinder().unlinkToDeath(this, 0);
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ destroy();
+ mCallback = null;
+ if (mOnBinderDied != null) {
+ mOnBinderDied.accept(this);
+ }
+ }
}
}
diff --git a/core/java/android/service/voice/OWNERS b/core/java/android/service/voice/OWNERS
index 59a0c2e..ec44100 100644
--- a/core/java/android/service/voice/OWNERS
+++ b/core/java/android/service/voice/OWNERS
@@ -1,3 +1,7 @@
# Bug component: 533220
include /core/java/android/app/assist/OWNERS
+
+# The owner here should not be assist owner
+liangyuchen@google.com
+tuanng@google.com
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 44afb89..23513fad 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -1242,7 +1242,7 @@
null /* ignoringVisibilityState */, config.isScreenRound(),
false /* alwaysConsumeSystemBars */, mLayout.softInputMode,
mLayout.flags, SYSTEM_UI_FLAG_VISIBLE, mLayout.type,
- config.windowConfiguration.getWindowingMode(), null /* typeSideMap */);
+ config.windowConfiguration.getWindowingMode(), null /* idSideMap */);
if (!fixedSize) {
final Rect padding = mIWallpaperEngine.mDisplayPadding;
diff --git a/core/java/android/text/Highlights.java b/core/java/android/text/Highlights.java
index 356dfca..ac4a5f2 100644
--- a/core/java/android/text/Highlights.java
+++ b/core/java/android/text/Highlights.java
@@ -38,9 +38,9 @@
}
/**
- * Returns a number of highlight.
+ * Returns the number of highlight.
*
- * @return a number of highlight.
+ * @return the number of highlight.
*
* @see Builder#addRange(Paint, int, int)
* @see Builder#addRanges(Paint, int...)
@@ -53,7 +53,7 @@
* Returns a paint used for the i-th highlight.
*
* @param index an index of the highlight. Must be between 0 and {@link #getSize()}
- * @return a paint object
+ * @return the paint object
*
* @see Builder#addRange(Paint, int, int)
* @see Builder#addRanges(Paint, int...)
@@ -72,7 +72,7 @@
* [1, 2, 3, 4].
*
* @param index an index of the highlight. Must be between 0 and {@link #getSize()}
- * @return a paint object
+ * @return the flattened ranges.
*
* @see Builder#addRange(Paint, int, int)
* @see Builder#addRanges(Paint, int...)
@@ -90,6 +90,9 @@
/**
* Add single range highlight.
*
+ * If the given range has overlaps with the already added ranges, the previous highlights
+ * are overdrawn by this range.
+ *
* @param paint a paint object used for drawing highlight path.
* @param start an inclusive offset of the text.
* @param end an exclusive offset of the text.
@@ -111,6 +114,9 @@
/**
* Add multiple ranges highlight.
*
+ * If the given ranges have overlap with the already added ranges, the previous highlights
+ * are overdrawn by this range.
+ *
* @param paint a paint object used for drawing highlight path.
* @param ranges a flatten ranges. The {@code 2 * i}-th element is an inclusive start offset
* of the {@code i}-th character. The {@code 2 * i + 1}-th element is an
diff --git a/core/java/android/transparency/BinaryTransparencyManager.java b/core/java/android/transparency/BinaryTransparencyManager.java
index d77bbcc..c18adfc 100644
--- a/core/java/android/transparency/BinaryTransparencyManager.java
+++ b/core/java/android/transparency/BinaryTransparencyManager.java
@@ -67,24 +67,6 @@
}
/**
- * Gets binary measurements of all installed APEXs, each packed in a Bundle.
- * @return A List of {@link android.os.Bundle}s with the following keys:
- * {@link com.android.server.BinaryTransparencyService#BUNDLE_PACKAGE_INFO}
- * {@link com.android.server.BinaryTransparencyService#BUNDLE_CONTENT_DIGEST_ALGORITHM}
- * {@link com.android.server.BinaryTransparencyService#BUNDLE_CONTENT_DIGEST}
- */
- // TODO(b/259422958): Fix static constants referenced here - should be defined here
- @NonNull
- public List getApexInfo() {
- try {
- Slog.d(TAG, "Calling backend's getApexInfo()");
- return mService.getApexInfo();
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- /**
* Collects the APEX information on the device.
*
* @param includeTestOnly Whether to include test only data in the returned ApexInfo.
@@ -116,4 +98,21 @@
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Collects the silent installed MBA information on the device.
+ *
+ * @return A List containing the MBA info of silent installed.
+ * @hide
+ */
+ @NonNull
+ public List<IBinaryTransparencyService.AppInfo> collectAllSilentInstalledMbaInfo(
+ Bundle packagesToSkip) {
+ try {
+ Slog.d(TAG, "Calling backend's collectAllSilentInstalledMbaInfo()");
+ return mService.collectAllSilentInstalledMbaInfo(packagesToSkip);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/core/java/android/util/RotationUtils.java b/core/java/android/util/RotationUtils.java
index 3e7c67e..f20767b 100644
--- a/core/java/android/util/RotationUtils.java
+++ b/core/java/android/util/RotationUtils.java
@@ -27,6 +27,7 @@
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
+import android.view.Surface;
import android.view.Surface.Rotation;
import android.view.SurfaceControl;
@@ -245,4 +246,23 @@
throw new IllegalArgumentException("Unknown rotation: " + rotation);
}
}
+
+ /**
+ * Reverses the rotation direction around the Z axis. Note that this method assumes all
+ * rotations are relative to {@link Surface.ROTATION_0}.
+ *
+ * @param rotation the original rotation.
+ * @return the new rotation that should be applied.
+ */
+ @Surface.Rotation
+ public static int reverseRotationDirectionAroundZAxis(@Surface.Rotation int rotation) {
+ // Flipping 270 and 90 has the same effect as changing the direction which rotation is
+ // applied.
+ if (rotation == Surface.ROTATION_90) {
+ rotation = Surface.ROTATION_270;
+ } else if (rotation == Surface.ROTATION_270) {
+ rotation = Surface.ROTATION_90;
+ }
+ return rotation;
+ }
}
diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java
index 91febcd..3dc79cf 100644
--- a/core/java/android/view/Choreographer.java
+++ b/core/java/android/view/Choreographer.java
@@ -270,10 +270,14 @@
private static final int CALLBACK_LAST = CALLBACK_COMMIT;
private Choreographer(Looper looper, int vsyncSource) {
+ this(looper, vsyncSource, /* layerHandle */ 0L);
+ }
+
+ private Choreographer(Looper looper, int vsyncSource, long layerHandle) {
mLooper = looper;
mHandler = new FrameHandler(looper);
mDisplayEventReceiver = USE_VSYNC
- ? new FrameDisplayEventReceiver(looper, vsyncSource)
+ ? new FrameDisplayEventReceiver(looper, vsyncSource, layerHandle)
: null;
mLastFrameTimeNanos = Long.MIN_VALUE;
@@ -313,6 +317,26 @@
}
/**
+ * Gets the choreographer associated with the SurfaceControl.
+ *
+ * @param layerHandle to which the choreographer will be attached.
+ * @param looper the choreographer is attached on this looper.
+ *
+ * @return The choreographer for the looper which is attached
+ * to the sourced SurfaceControl::mNativeHandle.
+ * @throws IllegalStateException if the looper sourced is null.
+ * @hide
+ */
+ @NonNull
+ static Choreographer getInstanceForSurfaceControl(long layerHandle,
+ @NonNull Looper looper) {
+ if (looper == null) {
+ throw new IllegalStateException("The current thread must have a looper!");
+ }
+ return new Choreographer(looper, VSYNC_SOURCE_APP, layerHandle);
+ }
+
+ /**
* @return The Choreographer of the main thread, if it exists, or {@code null} otherwise.
* @hide
*/
@@ -334,6 +358,15 @@
}
/**
+ * Dispose the DisplayEventReceiver on the Choreographer.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ void invalidate() {
+ dispose();
+ }
+
+ /**
* The amount of time, in milliseconds, between each frame of the animation.
* <p>
* This is a requested time that the animation will attempt to honor, but the actual delay
@@ -1166,8 +1199,8 @@
private int mFrame;
private VsyncEventData mLastVsyncEventData = new VsyncEventData();
- public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
- super(looper, vsyncSource, 0);
+ FrameDisplayEventReceiver(Looper looper, int vsyncSource, long layerHandle) {
+ super(looper, vsyncSource, /* eventRegistration */ 0, layerHandle);
}
// TODO(b/116025192): physicalDisplayId is ignored because SF only emits VSYNC events for
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 25863a6..6b1499f 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -360,6 +360,17 @@
public static final int FLAG_STEAL_TOP_FOCUS_DISABLED = 1 << 12;
/**
+ * Display flag: Indicates that the display is a rear display.
+ * <p>
+ * This flag identifies complementary displays that are facing away from the user.
+ * </p>
+ *
+ * @hide
+ * @see #getFlags()
+ */
+ public static final int FLAG_REAR = 1 << 13;
+
+ /**
* Display flag: Indicates that the contents of the display should not be scaled
* to fit the physical screen dimensions. Used for development only to emulate
* devices with smaller physicals screens while preserving density.
@@ -2497,5 +2508,24 @@
+ ", mMaxAverageLuminance=" + mMaxAverageLuminance
+ ", mMinLuminance=" + mMinLuminance + '}';
}
+
+ /**
+ * @hide
+ */
+ @NonNull
+ public static String hdrTypeToString(int hdrType) {
+ switch (hdrType) {
+ case HDR_TYPE_DOLBY_VISION:
+ return "HDR_TYPE_DOLBY_VISION";
+ case HDR_TYPE_HDR10:
+ return "HDR_TYPE_HDR10";
+ case HDR_TYPE_HLG:
+ return "HDR_TYPE_HLG";
+ case HDR_TYPE_HDR10_PLUS:
+ return "HDR_TYPE_HDR10_PLUS";
+ default:
+ return "HDR_TYPE_INVALID";
+ }
+ }
}
}
diff --git a/core/java/android/view/DisplayEventReceiver.java b/core/java/android/view/DisplayEventReceiver.java
index ce7606a0..edce001 100644
--- a/core/java/android/view/DisplayEventReceiver.java
+++ b/core/java/android/view/DisplayEventReceiver.java
@@ -27,6 +27,8 @@
import dalvik.annotation.optimization.FastNative;
+import libcore.util.NativeAllocationRegistry;
+
import java.lang.ref.WeakReference;
/**
@@ -80,12 +82,18 @@
private MessageQueue mMessageQueue;
private static native long nativeInit(WeakReference<DisplayEventReceiver> receiver,
- MessageQueue messageQueue, int vsyncSource, int eventRegistration);
- private static native void nativeDispose(long receiverPtr);
+ MessageQueue messageQueue, int vsyncSource, int eventRegistration, long layerHandle);
+ private static native long nativeGetDisplayEventReceiverFinalizer();
@FastNative
private static native void nativeScheduleVsync(long receiverPtr);
private static native VsyncEventData nativeGetLatestVsyncEventData(long receiverPtr);
+ private static final NativeAllocationRegistry sNativeAllocationRegistry =
+ NativeAllocationRegistry.createMalloced(
+ DisplayEventReceiver.class.getClassLoader(),
+ nativeGetDisplayEventReceiverFinalizer());
+ private Runnable mFreeNativeResources;
+
/**
* Creates a display event receiver.
*
@@ -93,7 +101,11 @@
*/
@UnsupportedAppUsage
public DisplayEventReceiver(Looper looper) {
- this(looper, VSYNC_SOURCE_APP, 0);
+ this(looper, VSYNC_SOURCE_APP, /* eventRegistration */ 0, /* layerHandle */ 0L);
+ }
+
+ public DisplayEventReceiver(Looper looper, int vsyncSource, int eventRegistration) {
+ this(looper, vsyncSource, eventRegistration, /* layerHandle */ 0L);
}
/**
@@ -103,36 +115,27 @@
* @param vsyncSource The source of the vsync tick. Must be on of the VSYNC_SOURCE_* values.
* @param eventRegistration Which events to dispatch. Must be a bitfield consist of the
* EVENT_REGISTRATION_*_FLAG values.
+ * @param layerHandle Layer to which the current instance is attached to
*/
- public DisplayEventReceiver(Looper looper, int vsyncSource, int eventRegistration) {
+ public DisplayEventReceiver(Looper looper, int vsyncSource, int eventRegistration,
+ long layerHandle) {
if (looper == null) {
throw new IllegalArgumentException("looper must not be null");
}
mMessageQueue = looper.getQueue();
mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,
- vsyncSource, eventRegistration);
- }
-
- @Override
- protected void finalize() throws Throwable {
- try {
- dispose(true);
- } finally {
- super.finalize();
- }
+ vsyncSource, eventRegistration, layerHandle);
+ mFreeNativeResources = sNativeAllocationRegistry.registerNativeAllocation(this,
+ mReceiverPtr);
}
/**
* Disposes the receiver.
*/
public void dispose() {
- dispose(false);
- }
-
- private void dispose(boolean finalized) {
if (mReceiverPtr != 0) {
- nativeDispose(mReceiverPtr);
+ mFreeNativeResources.run();
mReceiverPtr = 0;
}
mMessageQueue = null;
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index c2b6bc5..3a02c48 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -337,6 +337,9 @@
@Nullable
public DisplayShape displayShape;
+ @Nullable
+ public SurfaceControl.RefreshRateRange layoutLimitedRefreshRate;
+
public static final @android.annotation.NonNull Creator<DisplayInfo> CREATOR = new Creator<DisplayInfo>() {
@Override
public DisplayInfo createFromParcel(Parcel source) {
@@ -411,7 +414,8 @@
&& brightnessDefault == other.brightnessDefault
&& Objects.equals(roundedCorners, other.roundedCorners)
&& installOrientation == other.installOrientation
- && Objects.equals(displayShape, other.displayShape);
+ && Objects.equals(displayShape, other.displayShape)
+ && Objects.equals(layoutLimitedRefreshRate, other.layoutLimitedRefreshRate);
}
@Override
@@ -466,6 +470,7 @@
roundedCorners = other.roundedCorners;
installOrientation = other.installOrientation;
displayShape = other.displayShape;
+ layoutLimitedRefreshRate = other.layoutLimitedRefreshRate;
}
public void readFromParcel(Parcel source) {
@@ -526,6 +531,7 @@
}
installOrientation = source.readInt();
displayShape = source.readTypedObject(DisplayShape.CREATOR);
+ layoutLimitedRefreshRate = source.readTypedObject(SurfaceControl.RefreshRateRange.CREATOR);
}
@Override
@@ -584,6 +590,7 @@
}
dest.writeInt(installOrientation);
dest.writeTypedObject(displayShape, flags);
+ dest.writeTypedObject(layoutLimitedRefreshRate, flags);
}
@Override
@@ -758,7 +765,7 @@
sb.append(name);
sb.append("\", displayId ");
sb.append(displayId);
- sb.append("\", displayGroupId ");
+ sb.append(", displayGroupId ");
sb.append(displayGroupId);
sb.append(flagsToString(flags));
sb.append(", real ");
@@ -843,6 +850,8 @@
sb.append(brightnessDefault);
sb.append(", installOrientation ");
sb.append(Surface.rotationToString(installOrientation));
+ sb.append(", layoutLimitedRefreshRate ");
+ sb.append(layoutLimitedRefreshRate);
sb.append("}");
return sb.toString();
}
@@ -906,6 +915,9 @@
if ((flags & Display.FLAG_TOUCH_FEEDBACK_DISABLED) != 0) {
result.append(", FLAG_TOUCH_FEEDBACK_DISABLED");
}
+ if ((flags & Display.FLAG_REAR) != 0) {
+ result.append(", FLAG_REAR_DISPLAY");
+ }
return result.toString();
}
}
diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java
index e89be47..289a5b6 100644
--- a/core/java/android/view/ImeInsetsSourceConsumer.java
+++ b/core/java/android/view/ImeInsetsSourceConsumer.java
@@ -133,7 +133,8 @@
// If we had a request before to show from IME (tracked with mImeRequestedShow), reaching
// this code here means that we now got control, so we can start the animation immediately.
// If client window is trying to control IME and IME is already visible, it is immediate.
- if (fromIme || (mState.getSource(getId()).isVisible() && getControl() != null)) {
+ if (fromIme
+ || (mState.isSourceOrDefaultVisible(getId(), getType()) && getControl() != null)) {
return ShowResult.SHOW_IMMEDIATELY;
}
diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java
index 309a94a..75f1666 100644
--- a/core/java/android/view/InsetsAnimationControlImpl.java
+++ b/core/java/android/view/InsetsAnimationControlImpl.java
@@ -33,12 +33,12 @@
import static android.view.InsetsController.DEBUG;
import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_SHOWN;
import static android.view.InsetsController.LayoutInsetsDuringAnimation;
+import static android.view.InsetsSource.ID_IME;
import static android.view.InsetsState.ISIDE_BOTTOM;
import static android.view.InsetsState.ISIDE_FLOATING;
import static android.view.InsetsState.ISIDE_LEFT;
import static android.view.InsetsState.ISIDE_RIGHT;
import static android.view.InsetsState.ISIDE_TOP;
-import static android.view.InsetsState.ITYPE_IME;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static android.view.inputmethod.ImeTracker.DEBUG_IME_VISIBILITY;
@@ -132,19 +132,19 @@
mController = controller;
mInitialInsetsState = new InsetsState(state, true /* copySources */);
if (frame != null) {
- final SparseIntArray typeSideMap = new SparseIntArray();
- mCurrentInsets = getInsetsFromState(mInitialInsetsState, frame, null /* typeSideMap */);
+ final SparseIntArray idSideMap = new SparseIntArray();
+ mCurrentInsets = getInsetsFromState(mInitialInsetsState, frame, null /* idSideMap */);
mHiddenInsets = calculateInsets(mInitialInsetsState, frame, controls, false /* shown */,
- null /* typeSideMap */);
+ null /* idSideMap */);
mShownInsets = calculateInsets(mInitialInsetsState, frame, controls, true /* shown */,
- typeSideMap);
+ idSideMap);
mHasZeroInsetsIme = mShownInsets.bottom == 0 && controlsType(WindowInsets.Type.ime());
if (mHasZeroInsetsIme) {
// IME has shownInsets of ZERO, and can't map to a side by default.
// Map zero insets IME to bottom, making it a special case of bottom insets.
- typeSideMap.put(ITYPE_IME, ISIDE_BOTTOM);
+ idSideMap.put(ID_IME, ISIDE_BOTTOM);
}
- buildSideControlsMap(typeSideMap, mSideControlsMap, controls);
+ buildSideControlsMap(idSideMap, mSideControlsMap, controls);
} else {
// Passing a null frame indicates the caller wants to play the insets animation anyway,
// no matter the source provides insets to the frame or not.
@@ -399,27 +399,27 @@
}
private Insets getInsetsFromState(InsetsState state, Rect frame,
- @Nullable @InternalInsetsSide SparseIntArray typeSideMap) {
+ @Nullable @InternalInsetsSide SparseIntArray idSideMap) {
return state.calculateInsets(frame, null /* ignoringVisibilityState */,
false /* isScreenRound */, false /* alwaysConsumeSystemBars */,
LayoutParams.SOFT_INPUT_ADJUST_RESIZE /* legacySoftInputMode*/,
0 /* legacyWindowFlags */, 0 /* legacySystemUiFlags */, TYPE_APPLICATION,
- WINDOWING_MODE_UNDEFINED, typeSideMap).getInsets(mTypes);
+ WINDOWING_MODE_UNDEFINED, idSideMap).getInsets(mTypes);
}
/** Computes the insets relative to the given frame. */
private Insets calculateInsets(InsetsState state, Rect frame,
SparseArray<InsetsSourceControl> controls, boolean shown,
- @Nullable @InternalInsetsSide SparseIntArray typeSideMap) {
+ @Nullable @InternalInsetsSide SparseIntArray idSideMap) {
for (int i = controls.size() - 1; i >= 0; i--) {
final InsetsSourceControl control = controls.valueAt(i);
if (control == null) {
// control may be null if it got revoked.
continue;
}
- state.getSource(control.getId()).setVisible(shown);
+ state.setSourceVisible(control.getId(), shown);
}
- return getInsetsFromState(state, frame, typeSideMap);
+ return getInsetsFromState(state, frame, idSideMap);
}
/** Computes the insets from the insets hints of controls. */
@@ -435,7 +435,8 @@
// control may be null if it got revoked.
continue;
}
- if (state == null || state.getSource(control.getId()).isVisible()) {
+ if (state == null
+ || state.isSourceOrDefaultVisible(control.getId(), control.getType())) {
insets = Insets.max(insets, control.getInsetsHint());
}
}
@@ -465,20 +466,23 @@
// TODO: Implement behavior when inset spans over multiple types
for (int i = controls.size() - 1; i >= 0; i--) {
final InsetsSourceControl control = controls.valueAt(i);
- final InsetsSource source = mInitialInsetsState.getSource(control.getId());
+ final InsetsSource source = mInitialInsetsState.peekSource(control.getId());
final SurfaceControl leash = control.getLeash();
mTmpMatrix.setTranslate(control.getSurfacePosition().x, control.getSurfacePosition().y);
- mTmpFrame.set(source.getFrame());
+ if (source != null) {
+ mTmpFrame.set(source.getFrame());
+ }
addTranslationToMatrix(side, offset, mTmpMatrix, mTmpFrame);
final boolean visible = mHasZeroInsetsIme && side == ISIDE_BOTTOM
? (mAnimationType == ANIMATION_TYPE_SHOW || !mFinished)
: inset != 0;
- if (outState != null) {
- outState.getSource(source.getId()).setVisible(visible);
- outState.getSource(source.getId()).setFrame(mTmpFrame);
+ if (outState != null && source != null) {
+ outState.getOrCreateSource(source.getId(), source.getType())
+ .setVisible(visible)
+ .setFrame(mTmpFrame);
}
// If the system is controlling the insets source, the leash can be null.
@@ -517,12 +521,12 @@
}
}
- private static void buildSideControlsMap(SparseIntArray typeSideMap,
+ private static void buildSideControlsMap(SparseIntArray idSideMap,
SparseSetArray<InsetsSourceControl> sideControlsMap,
SparseArray<InsetsSourceControl> controls) {
- for (int i = typeSideMap.size() - 1; i >= 0; i--) {
- final int type = typeSideMap.keyAt(i);
- final int side = typeSideMap.valueAt(i);
+ for (int i = idSideMap.size() - 1; i >= 0; i--) {
+ final int type = idSideMap.keyAt(i);
+ final int side = idSideMap.valueAt(i);
final InsetsSourceControl control = controls.get(type);
if (control == null) {
// If the types that we are controlling are less than the types that the system has,
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 8e8e28a..1c00e5f 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -19,12 +19,12 @@
import static android.os.Trace.TRACE_TAG_VIEW;
import static android.view.InsetsControllerProto.CONTROL;
import static android.view.InsetsControllerProto.STATE;
-import static android.view.InsetsState.ITYPE_CAPTION_BAR;
-import static android.view.InsetsState.ITYPE_IME;
+import static android.view.InsetsSource.ID_IME;
import static android.view.ViewRootImpl.CAPTION_ON_SHELL;
import static android.view.WindowInsets.Type.FIRST;
import static android.view.WindowInsets.Type.LAST;
import static android.view.WindowInsets.Type.all;
+import static android.view.WindowInsets.Type.captionBar;
import static android.view.WindowInsets.Type.ime;
import android.animation.AnimationHandler;
@@ -44,13 +44,12 @@
import android.os.Process;
import android.os.Trace;
import android.text.TextUtils;
-import android.util.ArraySet;
+import android.util.IntArray;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
import android.view.InsetsSourceConsumer.ShowResult;
-import android.view.InsetsState.InternalInsetsType;
import android.view.SurfaceControl.Transaction;
import android.view.WindowInsets.Type;
import android.view.WindowInsets.Type.InsetsType;
@@ -249,6 +248,9 @@
/** The amount IME will move up/down when animating in floating mode. */
private static final int FLOATING_IME_BOTTOM_INSET_DP = -80;
+ private static final int ID_CAPTION_BAR =
+ InsetsSource.createId(null /* owner */, 0 /* index */, captionBar());
+
static final boolean DEBUG = false;
static final boolean WARN = false;
@@ -621,6 +623,79 @@
private final Runnable mInvokeControllableInsetsChangedListeners =
this::invokeControllableInsetsChangedListeners;
+ private final InsetsState.OnTraverseCallbacks mRemoveGoneSources =
+ new InsetsState.OnTraverseCallbacks() {
+
+ private final IntArray mPendingRemoveIndexes = new IntArray();
+
+ @Override
+ public void onIdNotFoundInState2(int index1, InsetsSource source1) {
+ if (!CAPTION_ON_SHELL && source1.getType() == captionBar()) {
+ return;
+ }
+
+ // Don't change the indexes of the sources while traversing. Remove it later.
+ mPendingRemoveIndexes.add(index1);
+
+ // Remove the consumer as well except the IME one. IME consumer should always
+ // be there since we need to communicate with InputMethodManager no matter we
+ // have the source or not.
+ if (source1.getType() != ime()) {
+ mSourceConsumers.remove(source1.getId());
+ }
+ }
+
+ @Override
+ public void onFinish(InsetsState state1, InsetsState state2) {
+ for (int i = mPendingRemoveIndexes.size() - 1; i >= 0; i--) {
+ state1.removeSourceAt(mPendingRemoveIndexes.get(i));
+ }
+ mPendingRemoveIndexes.clear();
+ }
+ };
+
+ private final InsetsState.OnTraverseCallbacks mStartResizingAnimationIfNeeded =
+ new InsetsState.OnTraverseCallbacks() {
+
+ private @InsetsType int mTypes;
+ private InsetsState mToState;
+
+ @Override
+ public void onStart(InsetsState state1, InsetsState state2) {
+ mTypes = 0;
+ mToState = null;
+ }
+
+ @Override
+ public void onIdMatch(InsetsSource source1, InsetsSource source2) {
+ final @InsetsType int type = source1.getType();
+ if ((type & Type.systemBars()) == 0
+ || !source1.isVisible() || !source2.isVisible()
+ || source1.getFrame().equals(source2.getFrame())
+ || !(Rect.intersects(mFrame, source1.getFrame())
+ || Rect.intersects(mFrame, source2.getFrame()))) {
+ return;
+ }
+ mTypes |= type;
+ if (mToState == null) {
+ mToState = new InsetsState();
+ }
+ mToState.addSource(new InsetsSource(source2));
+ }
+
+ @Override
+ public void onFinish(InsetsState state1, InsetsState state2) {
+ if (mTypes == 0) {
+ return;
+ }
+ cancelExistingControllers(mTypes);
+ final InsetsAnimationControlRunner runner = new InsetsResizeAnimationRunner(
+ mFrame, state1, mToState, RESIZE_INTERPOLATOR,
+ ANIMATION_DURATION_RESIZE, mTypes, InsetsController.this);
+ mRunningAnimations.add(new RunningAnimation(runner, runner.getAnimationType()));
+ }
+ };
+
public InsetsController(Host host) {
this(host, (controller, source) -> {
if (source.getType() == ime()) {
@@ -671,7 +746,7 @@
WindowInsets insets = state.calculateInsets(mFrame, mState /* ignoringVisibilityState*/,
mLastInsets.isRound(), mLastInsets.shouldAlwaysConsumeSystemBars(),
mLastLegacySoftInputMode, mLastLegacyWindowFlags, mLastLegacySystemUiFlags,
- mWindowType, mLastWindowingMode, null /* typeSideMap */);
+ mWindowType, mLastWindowingMode, null /* idSideMap */);
mHost.dispatchWindowInsetsAnimationProgress(insets,
Collections.unmodifiableList(runningAnimations));
if (DEBUG) {
@@ -687,7 +762,7 @@
};
// Make mImeSourceConsumer always non-null.
- mImeSourceConsumer = getSourceConsumer(new InsetsSource(ITYPE_IME, ime()));
+ mImeSourceConsumer = getSourceConsumer(new InsetsSource(ID_IME, ime()));
}
@VisibleForTesting
@@ -739,29 +814,21 @@
true /* excludeInvisibleIme */)) {
if (DEBUG) Log.d(TAG, "onStateChanged, notifyInsetsChanged");
mHost.notifyInsetsChanged();
- startResizingAnimationIfNeeded(lastState);
+ if (lastState.getDisplayFrame().equals(mState.getDisplayFrame())) {
+ InsetsState.traverse(lastState, mState, mStartResizingAnimationIfNeeded);
+ }
}
return true;
}
private void updateState(InsetsState newState) {
mState.set(newState, 0 /* types */);
- for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
- final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
- final InsetsSource source = newState.peekSource(consumer.getId());
- if (source == null && consumer != mImeSourceConsumer) {
- // IME source consumer should always be there since we need to communicate with
- // InputMethodManager no matter we have the source or not.
- mSourceConsumers.removeAt(i);
- }
- }
@InsetsType int existingTypes = 0;
@InsetsType int visibleTypes = 0;
@InsetsType int disabledUserAnimationTypes = 0;
@InsetsType int[] cancelledUserAnimationTypes = {0};
- for (int i = 0; i < InsetsState.SIZE; i++) {
- InsetsSource source = newState.peekSource(i);
- if (source == null) continue;
+ for (int i = 0, size = newState.sourceSize(); i < size; i++) {
+ final InsetsSource source = newState.sourceAt(i);
@InsetsType int type = source.getType();
@AnimationType int animationType = getAnimationType(type);
if (!source.isUserControllable()) {
@@ -789,15 +856,7 @@
}
mVisibleTypes = visibleTypes;
}
- for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) {
- // Only update the server side insets here.
- if (!CAPTION_ON_SHELL && type == ITYPE_CAPTION_BAR) continue;
- InsetsSource source = mState.peekSource(type);
- if (source == null) continue;
- if (newState.peekSource(type) == null) {
- mState.removeSource(type);
- }
- }
+ InsetsState.traverse(mState, newState, mRemoveGoneSources);
updateDisabledUserAnimationTypes(disabledUserAnimationTypes);
@@ -825,52 +884,17 @@
if (CAPTION_ON_SHELL) {
return false;
}
- if (mState.peekSource(ITYPE_CAPTION_BAR) == null
- && mCaptionInsetsHeight == 0) {
+ final InsetsSource source = mState.peekSource(ID_CAPTION_BAR);
+ if (source == null && mCaptionInsetsHeight == 0) {
return false;
}
- if (mState.peekSource(ITYPE_CAPTION_BAR) != null
- && mCaptionInsetsHeight
- == mState.peekSource(ITYPE_CAPTION_BAR).getFrame().height()) {
+ if (source != null && mCaptionInsetsHeight == source.getFrame().height()) {
return false;
}
return true;
}
- private void startResizingAnimationIfNeeded(InsetsState fromState) {
- if (!fromState.getDisplayFrame().equals(mState.getDisplayFrame())) {
- return;
- }
- @InsetsType int types = 0;
- InsetsState toState = null;
- final ArraySet<Integer> internalTypes = InsetsState.toInternalType(Type.systemBars());
- for (int i = internalTypes.size() - 1; i >= 0; i--) {
- final @InternalInsetsType int type = internalTypes.valueAt(i);
- final InsetsSource fromSource = fromState.peekSource(type);
- final InsetsSource toSource = mState.peekSource(type);
- if (fromSource != null && toSource != null
- && fromSource.isVisible() && toSource.isVisible()
- && !fromSource.getFrame().equals(toSource.getFrame())
- && (Rect.intersects(mFrame, fromSource.getFrame())
- || Rect.intersects(mFrame, toSource.getFrame()))) {
- types |= toSource.getType();
- if (toState == null) {
- toState = new InsetsState();
- }
- toState.addSource(new InsetsSource(toSource));
- }
- }
- if (types == 0) {
- return;
- }
- cancelExistingControllers(types);
- final InsetsAnimationControlRunner runner = new InsetsResizeAnimationRunner(
- mFrame, fromState, toState, RESIZE_INTERPOLATOR, ANIMATION_DURATION_RESIZE, types,
- this);
- mRunningAnimations.add(new RunningAnimation(runner, runner.getAnimationType()));
- }
-
/**
* @see InsetsState#calculateInsets(Rect, InsetsState, boolean, boolean, int, int, int, int,
* int, SparseIntArray)
@@ -886,7 +910,7 @@
mLastLegacySystemUiFlags = legacySystemUiFlags;
mLastInsets = mState.calculateInsets(mFrame, null /* ignoringVisibilityState*/,
isScreenRound, alwaysConsumeSystemBars, legacySoftInputMode, legacyWindowFlags,
- legacySystemUiFlags, windowType, windowingMode, null /* typeSideMap */);
+ legacySystemUiFlags, windowType, windowingMode, null /* idSideMap */);
return mLastInsets;
}
@@ -1217,7 +1241,8 @@
ImeTracker.get().onFailed(statsToken,
ImeTracker.PHASE_CLIENT_DISABLED_USER_ANIMATION);
- if (fromIme && !mState.getSource(mImeSourceConsumer.getId()).isVisible()) {
+ if (fromIme
+ && !mState.isSourceOrDefaultVisible(mImeSourceConsumer.getId(), ime())) {
// We've requested IMM to show IME, but the IME is not controllable. We need to
// cancel the request.
setRequestedVisibleTypes(0 /* visibleTypes */, ime());
@@ -1757,10 +1782,10 @@
if (mCaptionInsetsHeight != height) {
mCaptionInsetsHeight = height;
if (mCaptionInsetsHeight != 0) {
- mState.getSource(ITYPE_CAPTION_BAR).setFrame(mFrame.left, mFrame.top,
- mFrame.right, mFrame.top + mCaptionInsetsHeight);
+ mState.getOrCreateSource(ID_CAPTION_BAR, captionBar()).setFrame(
+ mFrame.left, mFrame.top, mFrame.right, mFrame.top + mCaptionInsetsHeight);
} else {
- mState.removeSource(ITYPE_CAPTION_BAR);
+ mState.removeSource(ID_CAPTION_BAR);
}
mHost.notifyInsetsChanged();
}
diff --git a/core/java/android/view/InsetsResizeAnimationRunner.java b/core/java/android/view/InsetsResizeAnimationRunner.java
index cf64eedf..bffaeea 100644
--- a/core/java/android/view/InsetsResizeAnimationRunner.java
+++ b/core/java/android/view/InsetsResizeAnimationRunner.java
@@ -33,7 +33,6 @@
import android.graphics.Rect;
import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
-import android.view.InsetsState.InternalInsetsType;
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsAnimation.Bounds;
import android.view.animation.Interpolator;
@@ -142,24 +141,23 @@
return false;
}
final float fraction = mAnimation.getInterpolatedFraction();
- for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) {
- final InsetsSource fromSource = mFromState.peekSource(type);
- final InsetsSource toSource = mToState.peekSource(type);
- if (fromSource == null || toSource == null) {
- continue;
+ InsetsState.traverse(mFromState, mToState, new InsetsState.OnTraverseCallbacks() {
+ @Override
+ public void onIdMatch(InsetsSource fromSource, InsetsSource toSource) {
+ final Rect fromFrame = fromSource.getFrame();
+ final Rect toFrame = toSource.getFrame();
+ final Rect frame = new Rect(
+ (int) (fromFrame.left + fraction * (toFrame.left - fromFrame.left)),
+ (int) (fromFrame.top + fraction * (toFrame.top - fromFrame.top)),
+ (int) (fromFrame.right + fraction * (toFrame.right - fromFrame.right)),
+ (int) (fromFrame.bottom + fraction * (toFrame.bottom - fromFrame.bottom)));
+ final InsetsSource source =
+ new InsetsSource(fromSource.getId(), fromSource.getType());
+ source.setFrame(frame);
+ source.setVisible(toSource.isVisible());
+ outState.addSource(source);
}
- final Rect fromFrame = fromSource.getFrame();
- final Rect toFrame = toSource.getFrame();
- final Rect frame = new Rect(
- (int) (fromFrame.left + fraction * (toFrame.left - fromFrame.left)),
- (int) (fromFrame.top + fraction * (toFrame.top - fromFrame.top)),
- (int) (fromFrame.right + fraction * (toFrame.right - fromFrame.right)),
- (int) (fromFrame.bottom + fraction * (toFrame.bottom - fromFrame.bottom)));
- final InsetsSource source = new InsetsSource(type, fromSource.getType());
- source.setFrame(frame);
- source.setVisible(toSource.isVisible());
- outState.addSource(source);
- }
+ });
if (mFinished) {
mController.notifyFinished(this, true /* shown */);
}
diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java
index f6b063d..17ab83c 100644
--- a/core/java/android/view/InsetsSource.java
+++ b/core/java/android/view/InsetsSource.java
@@ -21,7 +21,9 @@
import static android.view.InsetsSourceProto.VISIBLE;
import static android.view.InsetsSourceProto.VISIBLE_FRAME;
import static android.view.ViewRootImpl.CAPTION_ON_SHELL;
+import static android.view.WindowInsets.Type.ime;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Insets;
@@ -40,6 +42,9 @@
*/
public class InsetsSource implements Parcelable {
+ /** The insets source ID of IME */
+ public static final int ID_IME = createId(null, 0, ime());
+
/**
* An unique integer to identify this source across processes.
*/
@@ -83,20 +88,24 @@
mInsetsRoundedCornerFrame = other.mInsetsRoundedCornerFrame;
}
- public void setFrame(int left, int top, int right, int bottom) {
+ public InsetsSource setFrame(int left, int top, int right, int bottom) {
mFrame.set(left, top, right, bottom);
+ return this;
}
- public void setFrame(Rect frame) {
+ public InsetsSource setFrame(Rect frame) {
mFrame.set(frame);
+ return this;
}
- public void setVisibleFrame(@Nullable Rect visibleFrame) {
+ public InsetsSource setVisibleFrame(@Nullable Rect visibleFrame) {
mVisibleFrame = visibleFrame != null ? new Rect(visibleFrame) : null;
+ return this;
}
- public void setVisible(boolean visible) {
+ public InsetsSource setVisible(boolean visible) {
mVisible = visible;
+ return this;
}
public int getId() {
@@ -128,8 +137,9 @@
return mInsetsRoundedCornerFrame;
}
- public void setInsetsRoundedCornerFrame(boolean insetsRoundedCornerFrame) {
+ public InsetsSource setInsetsRoundedCornerFrame(boolean insetsRoundedCornerFrame) {
mInsetsRoundedCornerFrame = insetsRoundedCornerFrame;
+ return this;
}
/**
@@ -223,6 +233,29 @@
}
/**
+ * Creates an identifier of an {@link InsetsSource}.
+ *
+ * @param owner An object owned by the owner. Only the owner can modify its own sources.
+ * @param index An owner may have multiple sources with the same type. For example, the system
+ * server might have multiple display cutout sources. This is used to identify
+ * which one is which. The value must be in a range of [0, 2047].
+ * @param type The {@link WindowInsets.Type.InsetsType type} of the source.
+ * @return a unique integer as the identifier.
+ */
+ public static int createId(Object owner, @IntRange(from = 0, to = 2047) int index,
+ @InsetsType int type) {
+ if (index < 0 || index >= 2048) {
+ throw new IllegalArgumentException();
+ }
+ // owner takes top 16 bits;
+ // index takes 11 bits since the 6th bit;
+ // type takes bottom 5 bits.
+ return (((owner != null ? owner.hashCode() : 1) % (1 << 16)) << 16)
+ + (index << 5)
+ + WindowInsets.Type.indexOf(type);
+ }
+
+ /**
* Export the state of {@link InsetsSource} into a protocol buffer output stream.
*
* @param proto Stream to write the state to
@@ -241,7 +274,7 @@
public void dump(String prefix, PrintWriter pw) {
pw.print(prefix);
- pw.print("InsetsSource id="); pw.print(mId);
+ pw.print("InsetsSource id="); pw.print(Integer.toHexString(mId));
pw.print(" type="); pw.print(WindowInsets.Type.toString(mType));
pw.print(" frame="); pw.print(mFrame.toShortString());
if (mVisibleFrame != null) {
@@ -258,7 +291,7 @@
}
/**
- * @param excludeInvisibleImeFrames If {@link InsetsState#ITYPE_IME} frames should be ignored
+ * @param excludeInvisibleImeFrames If {@link WindowInsets.Type#ime()} frames should be ignored
* when IME is not visible.
*/
public boolean equals(@Nullable Object o, boolean excludeInvisibleImeFrames) {
@@ -316,8 +349,7 @@
@Override
public String toString() {
- return "InsetsSource: {"
- + "mId=" + mId
+ return "InsetsSource: {" + Integer.toHexString(mId)
+ " mType=" + WindowInsets.Type.toString(mType)
+ " mFrame=" + mFrame.toShortString()
+ " mVisible=" + mVisible
diff --git a/core/java/android/view/InsetsSourceControl.java b/core/java/android/view/InsetsSourceControl.java
index c849cb5..7ea93f5 100644
--- a/core/java/android/view/InsetsSourceControl.java
+++ b/core/java/android/view/InsetsSourceControl.java
@@ -205,8 +205,7 @@
@Override
public String toString() {
- return "InsetsSourceControl: {"
- + "mId=" + mId
+ return "InsetsSourceControl: {" + Integer.toHexString(mId)
+ " mType=" + WindowInsets.Type.toString(mType)
+ (mInitiallyVisible ? " initiallyVisible" : "")
+ " mSurfacePosition=" + mSurfacePosition
@@ -217,7 +216,7 @@
public void dump(String prefix, PrintWriter pw) {
pw.print(prefix);
- pw.print("InsetsSourceControl mId="); pw.print(mId);
+ pw.print("InsetsSourceControl mId="); pw.print(Integer.toHexString(mId));
pw.print(" mType="); pw.print(WindowInsets.Type.toString(mType));
pw.print(" mLeash="); pw.print(mLeash);
pw.print(" mInitiallyVisible="); pw.print(mInitiallyVisible);
diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java
index 054d177..ba7d823 100644
--- a/core/java/android/view/InsetsState.java
+++ b/core/java/android/view/InsetsState.java
@@ -20,6 +20,7 @@
import static android.view.InsetsStateProto.DISPLAY_FRAME;
import static android.view.InsetsStateProto.SOURCES;
import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
+import static android.view.WindowInsets.Type.captionBar;
import static android.view.WindowInsets.Type.displayCutout;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.indexOf;
@@ -42,6 +43,7 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.util.ArraySet;
+import android.util.SparseArray;
import android.util.SparseIntArray;
import android.util.proto.ProtoOutputStream;
import android.view.WindowInsets.Type;
@@ -53,7 +55,6 @@
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.Arrays;
import java.util.Objects;
import java.util.StringJoiner;
@@ -85,11 +86,6 @@
ITYPE_TOP_TAPPABLE_ELEMENT,
ITYPE_RIGHT_TAPPABLE_ELEMENT,
ITYPE_BOTTOM_TAPPABLE_ELEMENT,
- ITYPE_LEFT_DISPLAY_CUTOUT,
- ITYPE_TOP_DISPLAY_CUTOUT,
- ITYPE_RIGHT_DISPLAY_CUTOUT,
- ITYPE_BOTTOM_DISPLAY_CUTOUT,
- ITYPE_IME,
ITYPE_CLIMATE_BAR,
ITYPE_EXTRA_NAVIGATION_BAR,
ITYPE_LEFT_GENERIC_OVERLAY,
@@ -99,15 +95,7 @@
})
public @interface InternalInsetsType {}
- /**
- * Special value to be used to by methods returning an {@link InternalInsetsType} to indicate
- * that the objects/parameters aren't associated with an {@link InternalInsetsType}
- */
- public static final int ITYPE_INVALID = -1;
-
- static final int FIRST_TYPE = 0;
-
- public static final int ITYPE_STATUS_BAR = FIRST_TYPE;
+ public static final int ITYPE_STATUS_BAR = 0;
public static final int ITYPE_NAVIGATION_BAR = 1;
public static final int ITYPE_CAPTION_BAR = 2;
@@ -121,19 +109,11 @@
public static final int ITYPE_LEFT_MANDATORY_GESTURES = 9;
public static final int ITYPE_RIGHT_MANDATORY_GESTURES = 10;
- public static final int ITYPE_LEFT_DISPLAY_CUTOUT = 11;
- public static final int ITYPE_TOP_DISPLAY_CUTOUT = 12;
- public static final int ITYPE_RIGHT_DISPLAY_CUTOUT = 13;
- public static final int ITYPE_BOTTOM_DISPLAY_CUTOUT = 14;
-
public static final int ITYPE_LEFT_TAPPABLE_ELEMENT = 15;
public static final int ITYPE_TOP_TAPPABLE_ELEMENT = 16;
public static final int ITYPE_RIGHT_TAPPABLE_ELEMENT = 17;
public static final int ITYPE_BOTTOM_TAPPABLE_ELEMENT = 18;
- /** Input method window. */
- public static final int ITYPE_IME = 19;
-
/** Additional system decorations inset type. */
public static final int ITYPE_CLIMATE_BAR = 20;
public static final int ITYPE_EXTRA_NAVIGATION_BAR = 21;
@@ -144,16 +124,8 @@
public static final int ITYPE_RIGHT_GENERIC_OVERLAY = 24;
public static final int ITYPE_BOTTOM_GENERIC_OVERLAY = 25;
- static final int LAST_TYPE = ITYPE_BOTTOM_GENERIC_OVERLAY;
- public static final int SIZE = LAST_TYPE + 1;
-
- // Derived types
-
- /** A shelf is the same as the navigation bar. */
- public static final int ITYPE_SHELF = ITYPE_NAVIGATION_BAR;
-
@Retention(RetentionPolicy.SOURCE)
- @IntDef(prefix = "IINSETS_SIDE", value = {
+ @IntDef(prefix = "ISIDE", value = {
ISIDE_LEFT,
ISIDE_TOP,
ISIDE_RIGHT,
@@ -169,7 +141,7 @@
static final int ISIDE_FLOATING = 4;
static final int ISIDE_UNKNOWN = 5;
- private final InsetsSource[] mSources = new InsetsSource[SIZE];
+ private final SparseArray<InsetsSource> mSources;
/**
* The frame of the display these sources are relative to.
@@ -201,13 +173,15 @@
private DisplayShape mDisplayShape = DisplayShape.NONE;
public InsetsState() {
+ mSources = new SparseArray<>();
}
public InsetsState(InsetsState copy) {
- set(copy);
+ this(copy, false /* copySources */);
}
public InsetsState(InsetsState copy, boolean copySources) {
+ mSources = new SparseArray<>(copy.mSources.size());
set(copy, copySources);
}
@@ -224,36 +198,29 @@
boolean isScreenRound, boolean alwaysConsumeSystemBars,
int legacySoftInputMode, int legacyWindowFlags, int legacySystemUiFlags,
int windowType, @WindowConfiguration.WindowingMode int windowingMode,
- @Nullable @InternalInsetsSide SparseIntArray typeSideMap) {
+ @Nullable @InternalInsetsSide SparseIntArray idSideMap) {
Insets[] typeInsetsMap = new Insets[Type.SIZE];
Insets[] typeMaxInsetsMap = new Insets[Type.SIZE];
boolean[] typeVisibilityMap = new boolean[Type.SIZE];
final Rect relativeFrame = new Rect(frame);
final Rect relativeFrameMax = new Rect(frame);
- for (int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
- InsetsSource source = mSources[type];
- if (source == null) {
- int index = indexOf(toPublicType(type));
- if (typeInsetsMap[index] == null) {
- typeInsetsMap[index] = Insets.NONE;
- }
- continue;
- }
+ for (int i = mSources.size() - 1; i >= 0; i--) {
+ final InsetsSource source = mSources.valueAt(i);
processSource(source, relativeFrame, false /* ignoreVisibility */, typeInsetsMap,
- typeSideMap, typeVisibilityMap);
+ idSideMap, typeVisibilityMap);
// IME won't be reported in max insets as the size depends on the EditorInfo of the IME
// target.
if (source.getType() != WindowInsets.Type.ime()) {
InsetsSource ignoringVisibilitySource = ignoringVisibilityState != null
- ? ignoringVisibilityState.getSource(type)
+ ? ignoringVisibilityState.peekSource(source.getId())
: source;
if (ignoringVisibilitySource == null) {
continue;
}
processSource(ignoringVisibilitySource, relativeFrameMax,
- true /* ignoreVisibility */, typeMaxInsetsMap, null /* typeSideMap */,
+ true /* ignoreVisibility */, typeMaxInsetsMap, null /* idSideMap */,
null /* typeVisibilityMap */);
}
}
@@ -306,8 +273,9 @@
// If mRoundedCornerFrame is set, we should calculate the new RoundedCorners based on this
// frame.
final Rect roundedCornerFrame = new Rect(mRoundedCornerFrame);
- for (InsetsSource source : mSources) {
- if (source != null && source.getInsetsRoundedCornerFrame()) {
+ for (int i = mSources.size() - 1; i >= 0; i--) {
+ final InsetsSource source = mSources.valueAt(i);
+ if (source.getInsetsRoundedCornerFrame()) {
final Insets insets = source.calculateInsets(roundedCornerFrame, false);
roundedCornerFrame.inset(insets);
}
@@ -351,13 +319,9 @@
public Insets calculateInsets(Rect frame, @InsetsType int types, boolean ignoreVisibility) {
Insets insets = Insets.NONE;
- for (int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
- InsetsSource source = mSources[type];
- if (source == null) {
- continue;
- }
- int publicType = InsetsState.toPublicType(type);
- if ((publicType & types) == 0) {
+ for (int i = mSources.size() - 1; i >= 0; i--) {
+ final InsetsSource source = mSources.valueAt(i);
+ if ((source.getType() & types) == 0) {
continue;
}
insets = Insets.max(source.calculateInsets(frame, ignoreVisibility), insets);
@@ -368,13 +332,9 @@
public Insets calculateInsets(Rect frame, @InsetsType int types,
@InsetsType int requestedVisibleTypes) {
Insets insets = Insets.NONE;
- for (int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
- InsetsSource source = mSources[type];
- if (source == null) {
- continue;
- }
- int publicType = InsetsState.toPublicType(type);
- if ((publicType & types & requestedVisibleTypes) == 0) {
+ for (int i = mSources.size() - 1; i >= 0; i--) {
+ final InsetsSource source = mSources.valueAt(i);
+ if ((source.getType() & types & requestedVisibleTypes) == 0) {
continue;
}
insets = Insets.max(source.calculateInsets(frame, true), insets);
@@ -392,13 +352,9 @@
? systemBars() | ime()
: systemBars();
Insets insets = Insets.NONE;
- for (int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
- InsetsSource source = mSources[type];
- if (source == null) {
- continue;
- }
- final int publicType = InsetsState.toPublicType(type);
- if ((publicType & visibleInsetsTypes) == 0) {
+ for (int i = mSources.size() - 1; i >= 0; i--) {
+ final InsetsSource source = mSources.valueAt(i);
+ if ((source.getType() & visibleInsetsTypes) == 0) {
continue;
}
insets = Insets.max(source.calculateVisibleInsets(frame), insets);
@@ -416,13 +372,10 @@
@InsetsType
public int calculateUncontrollableInsetsFromFrame(Rect frame) {
int blocked = 0;
- for (int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
- InsetsSource source = mSources[type];
- if (source == null) {
- continue;
- }
+ for (int i = mSources.size() - 1; i >= 0; i--) {
+ final InsetsSource source = mSources.valueAt(i);
if (!canControlSource(frame, source)) {
- blocked |= toPublicType(type);
+ blocked |= source.getType();
}
}
return blocked;
@@ -438,12 +391,12 @@
}
private void processSource(InsetsSource source, Rect relativeFrame, boolean ignoreVisibility,
- Insets[] typeInsetsMap, @Nullable @InternalInsetsSide SparseIntArray typeSideMap,
+ Insets[] typeInsetsMap, @Nullable @InternalInsetsSide SparseIntArray idSideMap,
@Nullable boolean[] typeVisibilityMap) {
Insets insets = source.calculateInsets(relativeFrame, ignoreVisibility);
final int type = source.getType();
- processSourceAsPublicType(source, typeInsetsMap, typeSideMap, typeVisibilityMap,
+ processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
insets, type);
if (type == Type.MANDATORY_SYSTEM_GESTURES) {
@@ -452,24 +405,24 @@
// Type.systemGestureInsets() as NORMAL | MANDATORY, but then we lose the
// ability to set systemGestureInsets() independently from
// mandatorySystemGestureInsets() in the Builder.
- processSourceAsPublicType(source, typeInsetsMap, typeSideMap, typeVisibilityMap,
+ processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
insets, Type.SYSTEM_GESTURES);
}
if (type == Type.CAPTION_BAR) {
// Caption should also be gesture and tappable elements. This should not be needed when
// the caption is added from the shell, as the shell can add other types at the same
// time.
- processSourceAsPublicType(source, typeInsetsMap, typeSideMap, typeVisibilityMap,
+ processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
insets, Type.SYSTEM_GESTURES);
- processSourceAsPublicType(source, typeInsetsMap, typeSideMap, typeVisibilityMap,
+ processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
insets, Type.MANDATORY_SYSTEM_GESTURES);
- processSourceAsPublicType(source, typeInsetsMap, typeSideMap, typeVisibilityMap,
+ processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
insets, Type.TAPPABLE_ELEMENT);
}
}
private void processSourceAsPublicType(InsetsSource source, Insets[] typeInsetsMap,
- @InternalInsetsSide @Nullable SparseIntArray typeSideMap,
+ @InternalInsetsSide @Nullable SparseIntArray idSideMap,
@Nullable boolean[] typeVisibilityMap, Insets insets, int type) {
int index = indexOf(type);
Insets existing = typeInsetsMap[index];
@@ -483,10 +436,10 @@
typeVisibilityMap[index] = source.isVisible();
}
- if (typeSideMap != null) {
+ if (idSideMap != null) {
@InternalInsetsSide int insetSide = getInsetSide(insets);
if (insetSide != ISIDE_UNKNOWN) {
- typeSideMap.put(source.getId(), insetSide);
+ idSideMap.put(source.getId(), insetSide);
}
}
}
@@ -514,31 +467,60 @@
return ISIDE_UNKNOWN;
}
- public InsetsSource getSource(@InternalInsetsType int type) {
- InsetsSource source = mSources[type];
+ /**
+ * Gets the source mapped from the ID, or creates one if no such mapping has been made.
+ */
+ public InsetsSource getOrCreateSource(int id, int type) {
+ InsetsSource source = mSources.get(id);
if (source != null) {
return source;
}
- source = new InsetsSource(type, toPublicType(type));
- mSources[type] = source;
+ source = new InsetsSource(id, type);
+ mSources.put(id, source);
return source;
}
- public @Nullable InsetsSource peekSource(@InternalInsetsType int type) {
- return mSources[type];
+ /**
+ * Gets the source mapped from the ID, or <code>null</code> if no such mapping has been made.
+ */
+ public @Nullable InsetsSource peekSource(int id) {
+ return mSources.get(id);
}
/**
- * Returns the source visibility or the default visibility if the source doesn't exist. This is
- * useful if when treating this object as a request.
+ * Given an index in the range <code>0...sourceSize()-1</code>, returns the source ID from the
+ * <code>index</code>th ID-source mapping that this state stores.
+ */
+ public int sourceIdAt(int index) {
+ return mSources.keyAt(index);
+ }
+
+ /**
+ * Given an index in the range <code>0...sourceSize()-1</code>, returns the source from the
+ * <code>index</code>th ID-source mapping that this state stores.
+ */
+ public InsetsSource sourceAt(int index) {
+ return mSources.valueAt(index);
+ }
+
+ /**
+ * Returns the amount of the sources.
+ */
+ public int sourceSize() {
+ return mSources.size();
+ }
+
+ /**
+ * Returns if the source is visible or the type is default visible and the source doesn't exist.
*
- * @param type The {@link InternalInsetsType} to query.
+ * @param id The ID of the source.
+ * @param type The {@link InsetsType} to see if it is default visible.
* @return {@code true} if the source is visible or the type is default visible and the source
* doesn't exist.
*/
- public boolean getSourceOrDefaultVisibility(@InternalInsetsType int type) {
- final InsetsSource source = mSources[type];
- return source != null ? source.isVisible() : getDefaultVisibility(type);
+ public boolean isSourceOrDefaultVisible(int id, @InsetsType int type) {
+ final InsetsSource source = mSources.get(id);
+ return source != null ? source.isVisible() : (type & Type.defaultVisible()) != 0;
}
public void setDisplayFrame(Rect frame) {
@@ -612,28 +594,31 @@
}
/**
- * Modifies the state of this class to exclude a certain type to make it ready for dispatching
- * to the client.
+ * Removes the source which has the ID from this state, if there was any.
*
- * @param type The {@link InternalInsetsType} of the source to remove
- * @return {@code true} if this InsetsState was modified; {@code false} otherwise.
+ * @param id The ID of the source to remove.
*/
- public boolean removeSource(@InternalInsetsType int type) {
- if (mSources[type] == null) {
- return false;
- }
- mSources[type] = null;
- return true;
+ public void removeSource(int id) {
+ mSources.delete(id);
+ }
+
+ /**
+ * Removes the source at the specified index.
+ *
+ * @param index The index of the source to remove.
+ */
+ public void removeSourceAt(int index) {
+ mSources.removeAt(index);
}
/**
* A shortcut for setting the visibility of the source.
*
- * @param type The {@link InternalInsetsType} of the source to set the visibility
+ * @param id The ID of the source to set the visibility
* @param visible {@code true} for visible
*/
- public void setSourceVisible(@InternalInsetsType int type, boolean visible) {
- InsetsSource source = mSources[type];
+ public void setSourceVisible(int id, boolean visible) {
+ final InsetsSource source = mSources.get(id);
if (source != null) {
source.setVisible(visible);
}
@@ -651,14 +636,12 @@
mRoundedCornerFrame.scale(scale);
mPrivacyIndicatorBounds = mPrivacyIndicatorBounds.scale(scale);
mDisplayShape = mDisplayShape.setScale(scale);
- for (int i = 0; i < SIZE; i++) {
- final InsetsSource source = mSources[i];
- if (source != null) {
- source.getFrame().scale(scale);
- final Rect visibleFrame = source.getVisibleFrame();
- if (visibleFrame != null) {
- visibleFrame.scale(scale);
- }
+ for (int i = mSources.size() - 1; i >= 0; i--) {
+ final InsetsSource source = mSources.valueAt(i);
+ source.getFrame().scale(scale);
+ final Rect visibleFrame = source.getVisibleFrame();
+ if (visibleFrame != null) {
+ visibleFrame.scale(scale);
}
}
}
@@ -674,15 +657,12 @@
mRoundedCornerFrame.set(other.mRoundedCornerFrame);
mPrivacyIndicatorBounds = other.getPrivacyIndicatorBounds();
mDisplayShape = other.getDisplayShape();
- if (copySources) {
- for (int i = 0; i < SIZE; i++) {
- InsetsSource source = other.mSources[i];
- mSources[i] = source != null ? new InsetsSource(source) : null;
- }
- } else {
- for (int i = 0; i < SIZE; i++) {
- mSources[i] = other.mSources[i];
- }
+ mSources.clear();
+ for (int i = 0, size = other.mSources.size(); i < size; i++) {
+ final InsetsSource otherSource = other.mSources.valueAt(i);
+ mSources.append(otherSource.getId(), copySources
+ ? new InsetsSource(otherSource)
+ : otherSource);
}
}
@@ -700,15 +680,25 @@
mRoundedCornerFrame.set(other.mRoundedCornerFrame);
mPrivacyIndicatorBounds = other.getPrivacyIndicatorBounds();
mDisplayShape = other.getDisplayShape();
- final ArraySet<Integer> t = toInternalType(types);
- for (int i = t.size() - 1; i >= 0; i--) {
- final int type = t.valueAt(i);
- mSources[type] = other.mSources[type];
+ if (types == 0) {
+ return;
+ }
+ for (int i = mSources.size() - 1; i >= 0; i--) {
+ final InsetsSource source = mSources.valueAt(i);
+ if ((source.getType() & types) != 0) {
+ mSources.removeAt(i);
+ }
+ }
+ for (int i = other.mSources.size() - 1; i >= 0; i--) {
+ final InsetsSource otherSource = other.mSources.valueAt(i);
+ if ((otherSource.getType() & types) != 0) {
+ mSources.put(otherSource.getId(), otherSource);
+ }
}
}
public void addSource(InsetsSource source) {
- mSources[source.getId()] = source;
+ mSources.put(source.getId(), source);
}
public static boolean clearsCompatInsets(int windowType, int windowFlags, int windowingMode) {
@@ -748,15 +738,6 @@
result.add(ITYPE_RIGHT_MANDATORY_GESTURES);
result.add(ITYPE_BOTTOM_MANDATORY_GESTURES);
}
- if ((types & Type.DISPLAY_CUTOUT) != 0) {
- result.add(ITYPE_LEFT_DISPLAY_CUTOUT);
- result.add(ITYPE_TOP_DISPLAY_CUTOUT);
- result.add(ITYPE_RIGHT_DISPLAY_CUTOUT);
- result.add(ITYPE_BOTTOM_DISPLAY_CUTOUT);
- }
- if ((types & Type.IME) != 0) {
- result.add(ITYPE_IME);
- }
return result;
}
@@ -780,8 +761,6 @@
return Type.SYSTEM_OVERLAYS;
case ITYPE_CAPTION_BAR:
return Type.CAPTION_BAR;
- case ITYPE_IME:
- return Type.IME;
case ITYPE_TOP_MANDATORY_GESTURES:
case ITYPE_BOTTOM_MANDATORY_GESTURES:
case ITYPE_LEFT_MANDATORY_GESTURES:
@@ -797,33 +776,11 @@
case ITYPE_RIGHT_TAPPABLE_ELEMENT:
case ITYPE_BOTTOM_TAPPABLE_ELEMENT:
return Type.TAPPABLE_ELEMENT;
- case ITYPE_LEFT_DISPLAY_CUTOUT:
- case ITYPE_TOP_DISPLAY_CUTOUT:
- case ITYPE_RIGHT_DISPLAY_CUTOUT:
- case ITYPE_BOTTOM_DISPLAY_CUTOUT:
- return Type.DISPLAY_CUTOUT;
default:
throw new IllegalArgumentException("Unknown type: " + type);
}
}
- public static boolean getDefaultVisibility(@InternalInsetsType int type) {
- return type != ITYPE_IME;
- }
-
- public static boolean containsType(@InternalInsetsType int[] types,
- @InternalInsetsType int type) {
- if (types == null) {
- return false;
- }
- for (int t : types) {
- if (t == type) {
- return true;
- }
- }
- return false;
- }
-
public void dump(String prefix, PrintWriter pw) {
final String newPrefix = prefix + " ";
pw.println(prefix + "InsetsState");
@@ -833,16 +790,14 @@
pw.println(newPrefix + "mRoundedCornerFrame=" + mRoundedCornerFrame);
pw.println(newPrefix + "mPrivacyIndicatorBounds=" + mPrivacyIndicatorBounds);
pw.println(newPrefix + "mDisplayShape=" + mDisplayShape);
- for (int i = 0; i < SIZE; i++) {
- InsetsSource source = mSources[i];
- if (source == null) continue;
- source.dump(newPrefix + " ", pw);
+ for (int i = 0, size = mSources.size(); i < size; i++) {
+ mSources.valueAt(i).dump(newPrefix + " ", pw);
}
}
void dumpDebug(ProtoOutputStream proto, long fieldId) {
final long token = proto.start(fieldId);
- InsetsSource source = mSources[ITYPE_IME];
+ final InsetsSource source = mSources.get(InsetsSource.ID_IME);
if (source != null) {
source.dumpDebug(proto, SOURCES);
}
@@ -883,16 +838,6 @@
return "ITYPE_RIGHT_TAPPABLE_ELEMENT";
case ITYPE_BOTTOM_TAPPABLE_ELEMENT:
return "ITYPE_BOTTOM_TAPPABLE_ELEMENT";
- case ITYPE_LEFT_DISPLAY_CUTOUT:
- return "ITYPE_LEFT_DISPLAY_CUTOUT";
- case ITYPE_TOP_DISPLAY_CUTOUT:
- return "ITYPE_TOP_DISPLAY_CUTOUT";
- case ITYPE_RIGHT_DISPLAY_CUTOUT:
- return "ITYPE_RIGHT_DISPLAY_CUTOUT";
- case ITYPE_BOTTOM_DISPLAY_CUTOUT:
- return "ITYPE_BOTTOM_DISPLAY_CUTOUT";
- case ITYPE_IME:
- return "ITYPE_IME";
case ITYPE_CLIMATE_BAR:
return "ITYPE_CLIMATE_BAR";
case ITYPE_EXTRA_NAVIGATION_BAR:
@@ -921,8 +866,8 @@
* excluded.
* @param excludingCaptionInsets {@code true} if we want to compare two InsetsState objects but
* ignore the caption insets source value.
- * @param excludeInvisibleImeFrames If {@link #ITYPE_IME} frames should be ignored when IME is
- * not visible.
+ * @param excludeInvisibleImeFrames If {@link WindowInsets.Type#ime()} frames should be ignored
+ * when IME is not visible.
* @return {@code true} if the two InsetsState objects are equal, {@code false} otherwise.
*/
@VisibleForTesting
@@ -941,38 +886,56 @@
|| !mDisplayShape.equals(state.mDisplayShape)) {
return false;
}
- for (int i = 0; i < SIZE; i++) {
- if (excludingCaptionInsets) {
- if (i == ITYPE_CAPTION_BAR) continue;
+
+ final SparseArray<InsetsSource> thisSources = mSources;
+ final SparseArray<InsetsSource> thatSources = state.mSources;
+ if (!excludingCaptionInsets && !excludeInvisibleImeFrames) {
+ return thisSources.contentEquals(thatSources);
+ } else {
+ final int thisSize = thisSources.size();
+ final int thatSize = thatSources.size();
+ int thisIndex = 0;
+ int thatIndex = 0;
+ while (thisIndex < thisSize && thatIndex < thatSize) {
+ // Seek to the next non-excluding source of ours.
+ InsetsSource thisSource = thisSources.valueAt(thisIndex);
+ while (thisSource != null
+ && (excludingCaptionInsets && thisSource.getType() == captionBar()
+ || excludeInvisibleImeFrames && thisSource.getType() == ime()
+ && !thisSource.isVisible())) {
+ thisIndex++;
+ thisSource = thisIndex < thisSize ? thisSources.valueAt(thisIndex) : null;
+ }
+
+ // Seek to the next non-excluding source of theirs.
+ InsetsSource thatSource = thatSources.valueAt(thatIndex);
+ while (thatSource != null
+ && (excludingCaptionInsets && thatSource.getType() == captionBar()
+ || excludeInvisibleImeFrames && thatSource.getType() == ime()
+ && !thatSource.isVisible())) {
+ thatIndex++;
+ thatSource = thatIndex < thatSize ? thatSources.valueAt(thatIndex) : null;
+ }
+
+ if (!Objects.equals(thisSource, thatSource)) {
+ return false;
+ }
+
+ thisIndex++;
+ thatIndex++;
}
- InsetsSource source = mSources[i];
- InsetsSource otherSource = state.mSources[i];
- if (source == null && otherSource == null) {
- continue;
- }
- if (excludeInvisibleImeFrames && i == ITYPE_IME
- && ((source == null && !otherSource.isVisible())
- || (otherSource == null && !source.isVisible()))) {
- continue;
- }
- if (source == null || otherSource == null) {
- return false;
- }
- if (!otherSource.equals(source, excludeInvisibleImeFrames)) {
- return false;
- }
+ return thisIndex >= thisSize && thatIndex >= thatSize;
}
- return true;
}
@Override
public int hashCode() {
- return Objects.hash(mDisplayFrame, mDisplayCutout, Arrays.hashCode(mSources),
+ return Objects.hash(mDisplayFrame, mDisplayCutout, mSources.contentHashCode(),
mRoundedCorners, mPrivacyIndicatorBounds, mRoundedCornerFrame, mDisplayShape);
}
public InsetsState(Parcel in) {
- readFromParcel(in);
+ mSources = readFromParcel(in);
}
@Override
@@ -984,14 +947,18 @@
public void writeToParcel(Parcel dest, int flags) {
mDisplayFrame.writeToParcel(dest, flags);
mDisplayCutout.writeToParcel(dest, flags);
- dest.writeTypedArray(mSources, 0 /* parcelableFlags */);
dest.writeTypedObject(mRoundedCorners, flags);
mRoundedCornerFrame.writeToParcel(dest, flags);
dest.writeTypedObject(mPrivacyIndicatorBounds, flags);
dest.writeTypedObject(mDisplayShape, flags);
+ final int size = mSources.size();
+ dest.writeInt(size);
+ for (int i = 0; i < size; i++) {
+ dest.writeTypedObject(mSources.valueAt(i), flags);
+ }
}
- public static final @NonNull Creator<InsetsState> CREATOR = new Creator<InsetsState>() {
+ public static final @NonNull Creator<InsetsState> CREATOR = new Creator<>() {
public InsetsState createFromParcel(Parcel in) {
return new InsetsState(in);
@@ -1002,24 +969,34 @@
}
};
- public void readFromParcel(Parcel in) {
+ public SparseArray<InsetsSource> readFromParcel(Parcel in) {
mDisplayFrame.readFromParcel(in);
mDisplayCutout.readFromParcel(in);
- in.readTypedArray(mSources, InsetsSource.CREATOR);
mRoundedCorners = in.readTypedObject(RoundedCorners.CREATOR);
mRoundedCornerFrame.readFromParcel(in);
mPrivacyIndicatorBounds = in.readTypedObject(PrivacyIndicatorBounds.CREATOR);
mDisplayShape = in.readTypedObject(DisplayShape.CREATOR);
+ final int size = in.readInt();
+ final SparseArray<InsetsSource> sources;
+ if (mSources == null) {
+ // We are constructing this InsetsState.
+ sources = new SparseArray<>(size);
+ } else {
+ sources = mSources;
+ sources.clear();
+ }
+ for (int i = 0; i < size; i++) {
+ final InsetsSource source = in.readTypedObject(InsetsSource.CREATOR);
+ sources.append(source.getId(), source);
+ }
+ return sources;
}
@Override
public String toString() {
- StringJoiner joiner = new StringJoiner(", ");
- for (int i = 0; i < SIZE; i++) {
- InsetsSource source = mSources[i];
- if (source != null) {
- joiner.add(source.toString());
- }
+ final StringJoiner joiner = new StringJoiner(", ");
+ for (int i = 0, size = mSources.size(); i < size; i++) {
+ joiner.add(mSources.valueAt(i).toString());
}
return "InsetsState: {"
+ "mDisplayFrame=" + mDisplayFrame
@@ -1031,5 +1008,112 @@
+ ", mSources= { " + joiner
+ " }";
}
+
+ /**
+ * Traverses sources in two {@link InsetsState}s and calls back when events defined in
+ * {@link OnTraverseCallbacks} happen. This is optimized for {@link SparseArray} that we avoid
+ * triggering the binary search while getting the key or the value.
+ *
+ * This can be used to copy attributes of sources from one InsetsState to the other one, or to
+ * remove sources existing in one InsetsState but not in the other one.
+ *
+ * @param state1 The first {@link InsetsState} to be traversed.
+ * @param state2 The second {@link InsetsState} to be traversed.
+ * @param cb The {@link OnTraverseCallbacks} to call back to the caller.
+ */
+ public static void traverse(InsetsState state1, InsetsState state2, OnTraverseCallbacks cb) {
+ cb.onStart(state1, state2);
+ final int size1 = state1.sourceSize();
+ final int size2 = state2.sourceSize();
+ int index1 = 0;
+ int index2 = 0;
+ while (index1 < size1 && index2 < size2) {
+ int id1 = state1.sourceIdAt(index1);
+ int id2 = state2.sourceIdAt(index2);
+ while (id1 != id2) {
+ if (id1 < id2) {
+ cb.onIdNotFoundInState2(index1, state1.sourceAt(index1));
+ index1++;
+ if (index1 < size1) {
+ id1 = state1.sourceIdAt(index1);
+ } else {
+ break;
+ }
+ } else {
+ cb.onIdNotFoundInState1(index2, state2.sourceAt(index2));
+ index2++;
+ if (index2 < size2) {
+ id2 = state2.sourceIdAt(index2);
+ } else {
+ break;
+ }
+ }
+ }
+ if (index1 >= size1 || index2 >= size2) {
+ break;
+ }
+ final InsetsSource source1 = state1.sourceAt(index1);
+ final InsetsSource source2 = state2.sourceAt(index2);
+ cb.onIdMatch(source1, source2);
+ index1++;
+ index2++;
+ }
+ while (index2 < size2) {
+ cb.onIdNotFoundInState1(index2, state2.sourceAt(index2));
+ index2++;
+ }
+ while (index1 < size1) {
+ cb.onIdNotFoundInState2(index1, state1.sourceAt(index1));
+ index1++;
+ }
+ cb.onFinish(state1, state2);
+ }
+
+ /**
+ * Used with {@link #traverse(InsetsState, InsetsState, OnTraverseCallbacks)} to call back when
+ * certain events happen.
+ */
+ public interface OnTraverseCallbacks {
+
+ /**
+ * Called at the beginning of the traverse.
+ *
+ * @param state1 same as the state1 supplied to {@link #traverse}
+ * @param state2 same as the state2 supplied to {@link #traverse}
+ */
+ default void onStart(InsetsState state1, InsetsState state2) { }
+
+ /**
+ * Called when finding two IDs from two InsetsStates are the same.
+ *
+ * @param source1 the source in state1.
+ * @param source2 the source in state2.
+ */
+ default void onIdMatch(InsetsSource source1, InsetsSource source2) { }
+
+ /**
+ * Called when finding an ID in state2 but not in state1.
+ *
+ * @param index2 the index of the ID in state2.
+ * @param source2 the source which has the ID in state2.
+ */
+ default void onIdNotFoundInState1(int index2, InsetsSource source2) { }
+
+ /**
+ * Called when finding an ID in state1 but not in state2.
+ *
+ * @param index1 the index of the ID in state1.
+ * @param source1 the source which has the ID in state1.
+ */
+ default void onIdNotFoundInState2(int index1, InsetsSource source1) { }
+
+ /**
+ * Called at the end of the traverse.
+ *
+ * @param state1 same as the state1 supplied to {@link #traverse}
+ * @param state2 same as the state2 supplied to {@link #traverse}
+ */
+ default void onFinish(InsetsState state1, InsetsState state2) { }
+ }
}
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 54e1a53..8663013 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -64,6 +64,7 @@
import android.opengl.EGLSync;
import android.os.Build;
import android.os.IBinder;
+import android.os.Looper;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
@@ -463,6 +464,10 @@
public long mNativeObject;
private long mNativeHandle;
+ private final Object mChoreographerLock = new Object();
+ @GuardedBy("mChoreographerLock")
+ private Choreographer mChoreographer;
+
// TODO: Move width/height to native and fix locking through out.
private final Object mLock = new Object();
@GuardedBy("mLock")
@@ -1269,6 +1274,59 @@
}
/**
+ * Returns the associated {@link Choreographer} instance with the
+ * current instance of the SurfaceControl.
+ * Must be called from a thread that already has a {@link android.os.Looper}
+ * associated with it.
+ * If there is no {@link Choreographer} associated with the SurfaceControl then a new instance
+ * of the {@link Choreographer} is created.
+ *
+ * @hide
+ */
+ @TestApi
+ public @NonNull Choreographer getChoreographer() {
+ return getChoreographer(Looper.myLooper());
+ }
+
+ /**
+ * Returns the associated {@link Choreographer} instance with the
+ * current instance of the SurfaceControl.
+ * If there is no {@link Choreographer} associated with the SurfaceControl then a new instance
+ * of the {@link Choreographer} is created.
+ *
+ * @param looper the choreographer is attached on this looper
+ *
+ * @hide
+ */
+ @TestApi
+ public @NonNull Choreographer getChoreographer(@NonNull Looper looper) {
+ checkNotReleased();
+ synchronized (mChoreographerLock) {
+ if (mChoreographer != null) {
+ return mChoreographer;
+ }
+
+ mChoreographer = Choreographer.getInstanceForSurfaceControl(mNativeHandle, looper);
+ return mChoreographer;
+ }
+ }
+
+ /**
+ * Returns true if {@link Choreographer} is present otherwise false.
+ * To check the validity use {@link #isValid} on the SurfaceControl, a valid SurfaceControl with
+ * choreographer will have the valid Choreographer.
+ *
+ * @hide
+ */
+ @TestApi
+ @UnsupportedAppUsage
+ public boolean hasChoreographer() {
+ synchronized (mChoreographerLock) {
+ return mChoreographer != null;
+ }
+ }
+
+ /**
* Write to a protocol buffer output stream. Protocol buffer message definition is at {@link
* android.view.SurfaceControlProto}.
*
@@ -1325,6 +1383,12 @@
mNativeObject = 0;
mNativeHandle = 0;
mCloseGuard.close();
+ synchronized (mChoreographerLock) {
+ if (mChoreographer != null) {
+ mChoreographer.invalidate();
+ mChoreographer = null;
+ }
+ }
}
}
@@ -1692,7 +1756,7 @@
* Information about the min and max refresh rate DM would like to set the display to.
* @hide
*/
- public static final class RefreshRateRange {
+ public static final class RefreshRateRange implements Parcelable {
public static final String TAG = "RefreshRateRange";
// The tolerance within which we consider something approximately equals.
@@ -1761,6 +1825,35 @@
this.min = other.min;
this.max = other.max;
}
+
+ /**
+ * Writes the RefreshRateRange to parce
+ *
+ * @param dest parcel to write the transaction to
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, @WriteFlags int flags) {
+ dest.writeFloat(min);
+ dest.writeFloat(max);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @NonNull Creator<RefreshRateRange> CREATOR =
+ new Creator<RefreshRateRange>() {
+ @Override
+ public RefreshRateRange createFromParcel(Parcel in) {
+ return new RefreshRateRange(in.readFloat(), in.readFloat());
+ }
+
+ @Override
+ public RefreshRateRange[] newArray(int size) {
+ return new RefreshRateRange[size];
+ }
+ };
}
/**
@@ -2411,10 +2504,10 @@
* {@link Transaction#setTrustedPresentationCallback(SurfaceControl,
* TrustedPresentationThresholds, Executor, Consumer)}
*/
- public static class TrustedPresentationThresholds {
- private float mMinAlpha;
- private float mMinFractionRendered;
- private int mStabilityRequirementMs;
+ public static final class TrustedPresentationThresholds {
+ private final float mMinAlpha;
+ private final float mMinFractionRendered;
+ private final int mStabilityRequirementMs;
/**
* Creates a TrustedPresentationThresholds that's used when calling
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index 1eb87c9..0bce710 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -20,6 +20,7 @@
import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_SUBLAYER;
import static android.view.WindowManagerPolicyConstants.APPLICATION_PANEL_SUBLAYER;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
@@ -51,6 +52,8 @@
import com.android.internal.view.SurfaceCallbackHelper;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;
@@ -117,6 +120,36 @@
* may not blend properly as a consequence of not applying alpha to the surface content directly.
*/
public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCallback {
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"SURFACE_LIFECYCLE_"},
+ value = {SURFACE_LIFECYCLE_DEFAULT,
+ SURFACE_LIFECYCLE_FOLLOWS_VISIBILITY,
+ SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT})
+ public @interface SurfaceLifecycleStrategy {}
+
+ /**
+ * Default lifecycle of the Surface owned by this SurfaceView.
+ *
+ * The default lifecycle matches {@link #SURFACE_LIFECYCLE_FOLLOWS_VISIBILITY}.
+ */
+ public static final int SURFACE_LIFECYCLE_DEFAULT = 0;
+
+ /**
+ * The Surface lifecycle is tied to SurfaceView visibility.
+ *
+ * The Surface is created when the SurfaceView becomes visible, and is destroyed when the
+ * SurfaceView is no longer visible.
+ */
+ public static final int SURFACE_LIFECYCLE_FOLLOWS_VISIBILITY = 1;
+
+ /**
+ * The Surface lifecycle is tied to SurfaceView attachment.
+ * The Surface is created when the SurfaceView first becomes attached, but is not destroyed
+ * until this SurfaceView has been detached from the current window.
+ */
+ public static final int SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT = 2;
+
private static final String TAG = "SurfaceView";
private static final boolean DEBUG = false;
private static final boolean DEBUG_POSITION = false;
@@ -151,6 +184,11 @@
SurfaceControl mBackgroundControl;
private boolean mDisableBackgroundLayer = false;
+ @SurfaceLifecycleStrategy
+ private int mRequestedSurfaceLifecycleStrategy = SURFACE_LIFECYCLE_DEFAULT;
+ @SurfaceLifecycleStrategy
+ private int mSurfaceLifecycleStrategy = SURFACE_LIFECYCLE_DEFAULT;
+
/**
* We use this lock to protect access to mSurfaceControl. Both are accessed on the UI
* thread and the render thread via RenderNode.PositionUpdateListener#positionLost.
@@ -413,6 +451,7 @@
observer.removeOnPreDrawListener(mDrawListener);
mGlobalListenersAdded = false;
}
+ if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " Detaching SV");
mRequestedVisible = false;
@@ -699,6 +738,20 @@
}
}
+ /**
+ * Controls the lifecycle of the Surface owned by this SurfaceView.
+ *
+ * <p>By default, the lifecycycle strategy employed by the SurfaceView is
+ * {@link #SURFACE_LIFECYCLE_DEFAULT}.
+ *
+ * @param lifecycleStrategy The strategy for the lifecycle of the Surface owned by this
+ * SurfaceView.
+ */
+ public void setSurfaceLifecycle(@SurfaceLifecycleStrategy int lifecycleStrategy) {
+ mRequestedSurfaceLifecycleStrategy = lifecycleStrategy;
+ updateSurface();
+ }
+
private void updateOpaqueFlag() {
if (!PixelFormat.formatHasAlpha(mRequestedFormat)) {
mSurfaceFlags |= SurfaceControl.OPAQUE;
@@ -780,7 +833,7 @@
mSurfaceLock.lock();
try {
- mDrawingStopped = !mVisible;
+ mDrawingStopped = !surfaceShouldExist();
if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+ "Cur surface: " + mSurface);
@@ -887,6 +940,20 @@
return realSizeChanged;
}
+ private boolean requiresSurfaceControlCreation(boolean formatChanged, boolean visibleChanged) {
+ if (mSurfaceLifecycleStrategy == SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT) {
+ return (mSurfaceControl == null || formatChanged) && mAttachedToWindow;
+ }
+
+ return (mSurfaceControl == null || formatChanged || visibleChanged) && mRequestedVisible;
+ }
+
+ private boolean surfaceShouldExist() {
+ final boolean respectVisibility =
+ mSurfaceLifecycleStrategy != SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT;
+ return mVisible || (!respectVisibility && mAttachedToWindow);
+ }
+
/** @hide */
protected void updateSurface() {
if (!mHaveFrame) {
@@ -921,8 +988,7 @@
final boolean formatChanged = mFormat != mRequestedFormat;
final boolean visibleChanged = mVisible != mRequestedVisible;
final boolean alphaChanged = mAlpha != alpha;
- final boolean creating = (mSurfaceControl == null || formatChanged || visibleChanged)
- && mRequestedVisible;
+ final boolean creating = requiresSurfaceControlCreation(formatChanged, visibleChanged);
final boolean sizeChanged = mSurfaceWidth != myWidth || mSurfaceHeight != myHeight;
final boolean windowVisibleChanged = mWindowVisibility != mLastWindowVisibility;
getLocationInWindow(mLocation);
@@ -933,10 +999,13 @@
final boolean hintChanged = (viewRoot.getBufferTransformHint() != mTransformHint)
&& mRequestedVisible;
final boolean relativeZChanged = mSubLayer != mRequestedSubLayer;
+ final boolean surfaceLifecycleStrategyChanged =
+ mSurfaceLifecycleStrategy != mRequestedSurfaceLifecycleStrategy;
if (creating || formatChanged || sizeChanged || visibleChanged
|| alphaChanged || windowVisibleChanged || positionChanged
- || layoutSizeChanged || hintChanged || relativeZChanged) {
+ || layoutSizeChanged || hintChanged || relativeZChanged || !mAttachedToWindow
+ || surfaceLifecycleStrategyChanged) {
if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+ "Changes: creating=" + creating
@@ -945,7 +1014,10 @@
+ " hint=" + hintChanged
+ " visible=" + visibleChanged
+ " left=" + (mWindowSpaceLeft != mLocation[0])
- + " top=" + (mWindowSpaceTop != mLocation[1]));
+ + " top=" + (mWindowSpaceTop != mLocation[1])
+ + " z=" + relativeZChanged
+ + " attached=" + mAttachedToWindow
+ + " lifecycleStrategy=" + surfaceLifecycleStrategyChanged);
try {
mVisible = mRequestedVisible;
@@ -959,6 +1031,9 @@
mTransformHint = viewRoot.getBufferTransformHint();
mSubLayer = mRequestedSubLayer;
+ final int previousSurfaceLifecycleStrategy = mSurfaceLifecycleStrategy;
+ mSurfaceLifecycleStrategy = mRequestedSurfaceLifecycleStrategy;
+
mScreenRect.left = mWindowSpaceLeft;
mScreenRect.top = mWindowSpaceTop;
mScreenRect.right = mWindowSpaceLeft + getWidth();
@@ -1000,15 +1075,27 @@
SurfaceHolder.Callback[] callbacks = null;
final boolean surfaceChanged = creating;
- if (mSurfaceCreated && (surfaceChanged || (!mVisible && visibleChanged))) {
- mSurfaceCreated = false;
- notifySurfaceDestroyed();
+ final boolean respectVisibility =
+ mSurfaceLifecycleStrategy != SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT;
+ final boolean previouslyDidNotRespectVisibility =
+ previousSurfaceLifecycleStrategy
+ == SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT;
+ final boolean lifecycleNewlyRespectsVisibility = respectVisibility
+ && previouslyDidNotRespectVisibility;
+ if (mSurfaceCreated) {
+ if (surfaceChanged || (!respectVisibility && !mAttachedToWindow)
+ || (respectVisibility && !mVisible
+ && (visibleChanged || lifecycleNewlyRespectsVisibility))) {
+ mSurfaceCreated = false;
+ notifySurfaceDestroyed();
+ }
}
copySurface(creating /* surfaceControlCreated */, sizeChanged);
- if (mVisible && mSurface.isValid()) {
- if (!mSurfaceCreated && (surfaceChanged || visibleChanged)) {
+ if (surfaceShouldExist() && mSurface.isValid()) {
+ if (!mSurfaceCreated
+ && (surfaceChanged || (respectVisibility && visibleChanged))) {
mSurfaceCreated = true;
mIsCreating = true;
if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
@@ -1019,7 +1106,7 @@
}
}
if (creating || formatChanged || sizeChanged || hintChanged
- || visibleChanged || realSizeChanged) {
+ || (respectVisibility && visibleChanged) || realSizeChanged) {
if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+ "surfaceChanged -- format=" + mFormat
+ " w=" + myWidth + " h=" + myHeight);
diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index f51d9ba..c96d298 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -27,6 +27,7 @@
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
+import android.hardware.input.InputManager;
import android.os.Build;
import android.os.Bundle;
import android.os.RemoteException;
@@ -232,6 +233,12 @@
*/
private static final int MAXIMUM_FLING_VELOCITY = 8000;
+ /** Value used as a minimum fling velocity, when fling is not supported. */
+ private static final int NO_FLING_MIN_VELOCITY = Integer.MAX_VALUE;
+
+ /** Value used as a maximum fling velocity, when fling is not supported. */
+ private static final int NO_FLING_MAX_VELOCITY = Integer.MIN_VALUE;
+
/**
* Delay before dispatching a recurring accessibility event in milliseconds.
* This delay guarantees that a recurring event will be send at most once
@@ -333,6 +340,8 @@
private final int mFadingEdgeLength;
private final int mMinimumFlingVelocity;
private final int mMaximumFlingVelocity;
+ private final int mMinimumRotaryEncoderFlingVelocity;
+ private final int mMaximumRotaryEncoderFlingVelocity;
private final int mScrollbarSize;
private final int mTouchSlop;
private final int mHandwritingSlop;
@@ -378,6 +387,8 @@
mFadingEdgeLength = FADING_EDGE_LENGTH;
mMinimumFlingVelocity = MINIMUM_FLING_VELOCITY;
mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY;
+ mMinimumRotaryEncoderFlingVelocity = MINIMUM_FLING_VELOCITY;
+ mMaximumRotaryEncoderFlingVelocity = MAXIMUM_FLING_VELOCITY;
mScrollbarSize = SCROLL_BAR_SIZE;
mTouchSlop = TOUCH_SLOP;
mHandwritingSlop = HANDWRITING_SLOP;
@@ -504,6 +515,19 @@
com.android.internal.R.dimen.config_viewMinFlingVelocity);
mMaximumFlingVelocity = res.getDimensionPixelSize(
com.android.internal.R.dimen.config_viewMaxFlingVelocity);
+
+ int configMinRotaryEncoderFlingVelocity = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_viewMinRotaryEncoderFlingVelocity);
+ int configMaxRotaryEncoderFlingVelocity = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_viewMaxRotaryEncoderFlingVelocity);
+ if (configMinRotaryEncoderFlingVelocity < 0 || configMaxRotaryEncoderFlingVelocity < 0) {
+ mMinimumRotaryEncoderFlingVelocity = NO_FLING_MIN_VELOCITY;
+ mMaximumRotaryEncoderFlingVelocity = NO_FLING_MAX_VELOCITY;
+ } else {
+ mMinimumRotaryEncoderFlingVelocity = configMinRotaryEncoderFlingVelocity;
+ mMaximumRotaryEncoderFlingVelocity = configMaxRotaryEncoderFlingVelocity;
+ }
+
mGlobalActionsKeyTimeout = res.getInteger(
com.android.internal.R.integer.config_globalActionsKeyTimeout);
@@ -1077,6 +1101,98 @@
}
/**
+ * Minimum absolute value of velocity to initiate a fling for a motion generated by an
+ * {@link InputDevice} with an id of {@code inputDeviceId}, from an input {@code source} and on
+ * a given motion event {@code axis}.
+ *
+ * <p>Before utilizing this method to get a minimum fling velocity for a motion generated by the
+ * input device, scale the velocity of the motion events generated by the input device to pixels
+ * per second.
+ *
+ * <p>For instance, if you tracked {@link MotionEvent#AXIS_SCROLL} vertical velocities generated
+ * from a {@link InputDevice#SOURCE_ROTARY_ENCODER}, the velocity returned from
+ * {@link VelocityTracker} will be in the units with which the axis values were reported in the
+ * motion event. Before comparing that velocity against the minimum fling velocity specified
+ * here, make sure that the {@link MotionEvent#AXIS_SCROLL} velocity from the tracker is
+ * calculated in "units per second" (see {@link VelocityTracker#computeCurrentVelocity(int)},
+ * {@link VelocityTracker#computeCurrentVelocity(int, float)} to adjust your velocity
+ * computations to "per second"), and use {@link #getScaledVerticalScrollFactor} to change this
+ * velocity value to "pixels/second".
+ *
+ * <p>If the provided {@code inputDeviceId} is not valid, or if the input device whose ID is
+ * provided does not support the given motion event source and/or axis, this method will return
+ * {@code Integer.MAX_VALUE}.
+ *
+ * <h3>Obtaining the correct arguments for this method call</h3>
+ * <p><b>inputDeviceId</b>: if calling this method in response to a {@link MotionEvent}, use
+ * the device ID that is reported by the event, which can be obtained using
+ * {@link MotionEvent#getDeviceId()}. Otherwise, use a valid ID that is obtained from
+ * {@link InputDevice#getId()}, or from an {@link InputManager} instance
+ * ({@link InputManager#getInputDeviceIds()} gives all the valid input device IDs).
+ *
+ * <p><b>axis</b>: a {@link MotionEvent} may report data for multiple axes, and each axis may
+ * have multiple data points for different pointers. Use the axis for which you obtained the
+ * velocity for ({@link VelocityTracker} lets you calculate velocities for a specific axis. Use
+ * the axis for which you calculated velocity). You can use
+ * {@link InputDevice#getMotionRanges()} to get all the {@link InputDevice.MotionRange}s for the
+ * {@link InputDevice}, from which you can derive all the valid axes for the device.
+ *
+ * <p><b>source</b>: use {@link MotionEvent#getSource()} if calling this method in response to a
+ * {@link MotionEvent}. Otherwise, use a valid source for the {@link InputDevice}. You can use
+ * {@link InputDevice#getMotionRanges()} to get all the {@link InputDevice.MotionRange}s for the
+ * {@link InputDevice}, from which you can derive all the valid sources for the device.
+ *
+ *
+ * <p>This method optimizes calls over multiple input device IDs, so caching the return value of
+ * the method is not necessary if you are handling multiple input devices.
+ *
+ * @param inputDeviceId the ID of the {@link InputDevice} that generated the motion triggering
+ * fling.
+ * @param axis the axis on which the motion triggering the fling happened. This axis should be
+ * a valid axis that can be reported by the provided input device from the provided
+ * input device source.
+ * @param source the input source of the motion causing fling. This source should be a valid
+ * source for the {@link InputDevice} whose ID is {@code inputDeviceId}.
+ *
+ * @return the minimum velocity, in pixels/second, to trigger fling.
+ *
+ * @see InputDevice#getMotionRange(int, int)
+ * @see InputDevice#getMotionRanges()
+ * @see VelocityTracker#getAxisVelocity(int, int)
+ * @see VelocityTracker#getAxisVelocity(int)
+ */
+ public int getScaledMinimumFlingVelocity(int inputDeviceId, int axis, int source) {
+ if (!isInputDeviceInfoValid(inputDeviceId, axis, source)) return NO_FLING_MIN_VELOCITY;
+
+ if (source == InputDevice.SOURCE_ROTARY_ENCODER) return mMinimumRotaryEncoderFlingVelocity;
+
+ return mMinimumFlingVelocity;
+ }
+
+ /**
+ * Maximum absolute value of velocity to initiate a fling for a motion generated by an
+ * {@link InputDevice} with an id of {@code inputDeviceId}, from an input {@code source} and on
+ * a given motion event {@code axis}.
+ *
+ * <p>Similar to {@link #getScaledMinimumFlingVelocity(int, int, int)}, but for maximum fling
+ * velocity, instead of minimum. Also, unlike that method which returns
+ * {@code Integer.MAX_VALUE} for bad input device ID, source and/or motion event axis inputs,
+ * this method returns {@code Integer.MIN_VALUE} for such bad inputs.
+ */
+ public int getScaledMaximumFlingVelocity(int inputDeviceId, int axis, int source) {
+ if (!isInputDeviceInfoValid(inputDeviceId, axis, source)) return NO_FLING_MAX_VELOCITY;
+
+ if (source == InputDevice.SOURCE_ROTARY_ENCODER) return mMaximumRotaryEncoderFlingVelocity;
+
+ return mMaximumFlingVelocity;
+ }
+
+ private static boolean isInputDeviceInfoValid(int id, int axis, int source) {
+ InputDevice device = InputManager.getInstance().getInputDevice(id);
+ return device != null && device.getMotionRange(axis, source) != null;
+ }
+
+ /**
* Check if shortcuts should be displayed in menus.
*
* @return {@code True} if shortcuts should be displayed in menus.
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index de946a4..d603de2 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -22,7 +22,7 @@
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.InputDevice.SOURCE_CLASS_NONE;
-import static android.view.InsetsState.ITYPE_IME;
+import static android.view.InsetsSource.ID_IME;
import static android.view.View.PFLAG_DRAW_ANIMATION;
import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
@@ -709,6 +709,7 @@
// These are accessed by multiple threads.
final Rect mWinFrame; // frame given by window manager.
+ private final Rect mLastLayoutFrame;
Rect mOverrideInsetsFrame;
final Rect mPendingBackDropFrame = new Rect();
@@ -768,7 +769,12 @@
private long mFpsPrevTime = -1;
private int mFpsNumFrames;
- private int mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED;
+ /**
+ * The resolved pointer icon type requested by this window.
+ * A null value indicates the resolved pointer icon has not yet been calculated.
+ */
+ @Nullable
+ private Integer mPointerIconType = null;
private PointerIcon mCustomPointerIcon = null;
/**
@@ -951,6 +957,7 @@
mHeight = -1;
mDirty = new Rect();
mWinFrame = new Rect();
+ mLastLayoutFrame = new Rect();
mWindow = new W(this);
mLeashToken = new Binder();
mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
@@ -1315,7 +1322,7 @@
UNSPECIFIED_LENGTH, UNSPECIFIED_LENGTH,
mInsetsController.getRequestedVisibleTypes(), 1f /* compactScale */,
mTmpFrames);
- setFrame(mTmpFrames.frame);
+ setFrame(mTmpFrames.frame, true /* withinRelayout */);
registerBackCallbackOnWindow();
if (DEBUG_LAYOUT) Log.v(mTag, "Added window " + mWindow);
if (res < WindowManagerGlobal.ADD_OKAY) {
@@ -1842,7 +1849,7 @@
onMovedToDisplay(displayId, mLastConfigurationFromResources);
}
- setFrame(frame);
+ setFrame(frame, false /* withinRelayout */);
mTmpFrames.displayFrame.set(displayFrame);
if (mTmpFrames.attachedFrame != null && attachedFrame != null) {
mTmpFrames.attachedFrame.set(attachedFrame);
@@ -5836,7 +5843,7 @@
mTmpFrames.frame.right = l + w;
mTmpFrames.frame.top = t;
mTmpFrames.frame.bottom = t + h;
- setFrame(mTmpFrames.frame);
+ setFrame(mTmpFrames.frame, false /* withinRelayout */);
maybeHandleWindowMove(mWinFrame);
}
break;
@@ -6889,13 +6896,13 @@
|| event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) {
// Other apps or the window manager may change the icon type outside of
// this app, therefore the icon type has to be reset on enter/exit event.
- mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED;
+ mPointerIconType = null;
}
if (event.getActionMasked() != MotionEvent.ACTION_HOVER_EXIT) {
if (!updatePointerIcon(event) &&
event.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE) {
- mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED;
+ mPointerIconType = null;
}
}
}
@@ -6934,7 +6941,7 @@
}
private void resetPointerIcon(MotionEvent event) {
- mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED;
+ mPointerIconType = null;
updatePointerIcon(event);
}
@@ -6964,9 +6971,9 @@
}
final int pointerType = (pointerIcon != null) ?
- pointerIcon.getType() : PointerIcon.TYPE_DEFAULT;
+ pointerIcon.getType() : PointerIcon.TYPE_NOT_SPECIFIED;
- if (mPointerIconType != pointerType) {
+ if (mPointerIconType == null || mPointerIconType != pointerType) {
mPointerIconType = pointerType;
mCustomPointerIcon = null;
if (mPointerIconType != PointerIcon.TYPE_CUSTOM) {
@@ -8343,7 +8350,7 @@
// If the position and the size of the frame are both changed, it will trigger a BLAST
// sync, and we still need to call relayout to obtain the syncSeqId. Otherwise, we just
// need to send attributes via relayoutAsync.
- final Rect oldFrame = mWinFrame;
+ final Rect oldFrame = mLastLayoutFrame;
final Rect newFrame = mTmpFrames.frame;
final boolean positionChanged =
newFrame.top != oldFrame.top || newFrame.left != oldFrame.left;
@@ -8473,7 +8480,7 @@
params.restore();
}
- setFrame(mTmpFrames.frame);
+ setFrame(mTmpFrames.frame, true /* withinRelayout */);
return relayoutResult;
}
@@ -8508,8 +8515,18 @@
mIsSurfaceOpaque = opaque;
}
- private void setFrame(Rect frame) {
+ /**
+ * Set the mWinFrame of this window.
+ * @param frame the new frame of this window.
+ * @param withinRelayout {@code true} if this setting is within the relayout, or is the initial
+ * setting. That will make sure in the relayout process, we always compare
+ * the window frame with the last processed window frame.
+ */
+ private void setFrame(Rect frame, boolean withinRelayout) {
mWinFrame.set(frame);
+ if (withinRelayout) {
+ mLastLayoutFrame.set(frame);
+ }
final WindowConfiguration winConfig = getCompatWindowConfiguration();
mPendingBackDropFrame.set(mPendingDragResizing && !winConfig.useWindowFrameForBackdrop()
@@ -8756,6 +8773,8 @@
mInsetsController.dump(prefix, writer);
+ mOnBackInvokedDispatcher.dump(prefix, writer);
+
writer.println(prefix + "View Hierarchy:");
dumpViewHierarchy(innerPrefix, writer, mView);
}
@@ -8957,7 +8976,7 @@
if (mTranslator != null) {
mTranslator.translateInsetsStateInScreenToAppWindow(insetsState);
}
- if (insetsState.getSourceOrDefaultVisibility(ITYPE_IME)) {
+ if (insetsState.isSourceOrDefaultVisible(ID_IME, Type.ime())) {
ImeTracing.getInstance().triggerClientDump("ViewRootImpl#dispatchResized",
getInsetsController().getHost().getInputMethodManager(), null /* icProto */);
}
@@ -8989,7 +9008,7 @@
mTranslator.translateInsetsStateInScreenToAppWindow(insetsState);
mTranslator.translateSourceControlsInScreenToAppWindow(activeControls);
}
- if (insetsState != null && insetsState.getSourceOrDefaultVisibility(ITYPE_IME)) {
+ if (insetsState != null && insetsState.isSourceOrDefaultVisible(ID_IME, Type.ime())) {
ImeTracing.getInstance().triggerClientDump("ViewRootImpl#dispatchInsetsControlChanged",
getInsetsController().getHost().getInputMethodManager(), null /* icProto */);
}
@@ -11428,33 +11447,25 @@
});
}
- private class VRISurfaceSyncGroup extends SurfaceSyncGroup {
- VRISurfaceSyncGroup(String name) {
- super(name);
- }
-
- @Override
- public void onSyncReady() {
- Runnable runnable = () -> {
- mNumPausedForSync--;
- if (!mIsInTraversal && mNumPausedForSync == 0) {
- scheduleTraversals();
- }
- };
-
- if (Thread.currentThread() == mThread) {
- runnable.run();
- } else {
- mHandler.post(runnable);
- }
- }
- }
-
@Override
public SurfaceSyncGroup getOrCreateSurfaceSyncGroup() {
boolean newSyncGroup = false;
if (mActiveSurfaceSyncGroup == null) {
- mActiveSurfaceSyncGroup = new VRISurfaceSyncGroup(mTag);
+ mActiveSurfaceSyncGroup = new SurfaceSyncGroup(mTag);
+ mActiveSurfaceSyncGroup.setAddedToSyncListener(() -> {
+ Runnable runnable = () -> {
+ mNumPausedForSync--;
+ if (!mIsInTraversal && mNumPausedForSync == 0) {
+ scheduleTraversals();
+ }
+ };
+
+ if (Thread.currentThread() == mThread) {
+ runnable.run();
+ } else {
+ mHandler.post(runnable);
+ }
+ });
updateSyncInProgressCount(mActiveSurfaceSyncGroup);
newSyncGroup = true;
}
diff --git a/core/java/android/view/WindowLayout.java b/core/java/android/view/WindowLayout.java
index 7077804..3b8298e 100644
--- a/core/java/android/view/WindowLayout.java
+++ b/core/java/android/view/WindowLayout.java
@@ -16,10 +16,10 @@
package android.view;
-import static android.view.InsetsState.ITYPE_IME;
-import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_STATUS_BAR;
+import static android.view.InsetsSource.ID_IME;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.systemBars;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
@@ -89,7 +89,7 @@
if (attachedWindowFrame == null) {
outParentFrame.set(outDisplayFrame);
if ((pfl & PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME) != 0) {
- final InsetsSource source = state.peekSource(ITYPE_IME);
+ final InsetsSource source = state.peekSource(ID_IME);
if (source != null) {
outParentFrame.inset(source.calculateInsets(
outParentFrame, false /* ignoreVisibility */));
@@ -109,14 +109,6 @@
// Ensure that windows with a non-ALWAYS display cutout mode are laid out in
// the cutout safe zone.
final Rect displayFrame = state.getDisplayFrame();
- final InsetsSource statusBarSource = state.peekSource(ITYPE_STATUS_BAR);
- if (statusBarSource != null && displayCutoutSafe.top > displayFrame.top) {
- // Make sure that the zone we're avoiding for the cutout is at least as tall as the
- // status bar; otherwise fullscreen apps will end up cutting halfway into the status
- // bar.
- displayCutoutSafeExceptMaybeBars.top =
- Math.max(statusBarSource.getFrame().bottom, displayCutoutSafe.top);
- }
if (cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES) {
if (displayFrame.width() < displayFrame.height()) {
displayCutoutSafeExceptMaybeBars.top = MIN_Y;
@@ -131,7 +123,7 @@
&& (cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
|| cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES)) {
final Insets systemBarsInsets = state.calculateInsets(
- displayFrame, WindowInsets.Type.systemBars(), requestedVisibleTypes);
+ displayFrame, systemBars(), requestedVisibleTypes);
if (systemBarsInsets.left > 0) {
displayCutoutSafeExceptMaybeBars.left = MIN_X;
}
@@ -145,12 +137,11 @@
displayCutoutSafeExceptMaybeBars.bottom = MAX_Y;
}
}
- if (type == TYPE_INPUT_METHOD) {
- final InsetsSource navSource = state.peekSource(ITYPE_NAVIGATION_BAR);
- if (navSource != null && navSource.calculateInsets(displayFrame, true).bottom > 0) {
- // The IME can always extend under the bottom cutout if the navbar is there.
- displayCutoutSafeExceptMaybeBars.bottom = MAX_Y;
- }
+ if (type == TYPE_INPUT_METHOD
+ && displayCutoutSafeExceptMaybeBars.bottom != MAX_Y
+ && state.calculateInsets(displayFrame, navigationBars(), true).bottom > 0) {
+ // The IME can always extend under the bottom cutout if the navbar is there.
+ displayCutoutSafeExceptMaybeBars.bottom = MAX_Y;
}
final boolean attachedInParent = attachedWindowFrame != null && !layoutInScreen;
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 145a5d3..0f68cd0 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -855,6 +855,43 @@
/**
* Application level {@link android.content.pm.PackageManager.Property PackageManager
+ * .Property} for an app to inform the system that the application can be opted-in or opted-out
+ * from the compatibility treatment that enables sending a fake focus event for unfocused
+ * resumed split screen activities. This is needed because some game engines wait to get
+ * focus before drawing the content of the app which isn't guaranteed by default in multi-window
+ * modes.
+ *
+ * <p>Device manufacturers can enable this treatment using their discretion on a per-device
+ * basis to improve display compatibility. The treatment also needs to be specifically enabled
+ * on a per-app basis afterwards. This can either be done by device manufacturers or developers.
+ *
+ * <p>With this property set to {@code true}, the system will apply the treatment only if the
+ * device manufacturer had previously enabled it on the device. A fake focus event will be sent
+ * to the app after it is resumed only if the app is in split-screen.
+ *
+ * <p>Setting this property to {@code false} informs the system that the activity must be
+ * opted-out from the compatibility treatment even if the device manufacturer has opted the app
+ * into the treatment.
+ *
+ * <p>If the property remains unset the system will apply the treatment only if it had
+ * previously been enabled both at the device and app level by the device manufacturer.
+ *
+ * <p><b>Syntax:</b>
+ * <pre>
+ * <application>
+ * <property
+ * android:name="android.window.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS"
+ * android:value="true|false"/>
+ * </application>
+ * </pre>
+ *
+ * @hide
+ */
+ // TODO(b/263984287): Make this public API.
+ String PROPERTY_COMPAT_ENABLE_FAKE_FOCUS = "android.window.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS";
+
+ /**
+ * Application level {@link android.content.pm.PackageManager.Property PackageManager
* .Property} for an app to inform the system that the app should be excluded from the
* camera compatibility force rotation treatment.
*
diff --git a/core/java/android/view/animation/AnimationUtils.java b/core/java/android/view/animation/AnimationUtils.java
index 7d1dc76..c8c910d 100644
--- a/core/java/android/view/animation/AnimationUtils.java
+++ b/core/java/android/view/animation/AnimationUtils.java
@@ -28,6 +28,7 @@
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.Xml;
+import android.view.InflateException;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -137,16 +138,9 @@
try {
parser = context.getResources().getAnimation(id);
return createAnimationFromXml(context, parser);
- } catch (XmlPullParserException ex) {
- NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
- Integer.toHexString(id));
- rnf.initCause(ex);
- throw rnf;
- } catch (IOException ex) {
- NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
- Integer.toHexString(id));
- rnf.initCause(ex);
- throw rnf;
+ } catch (XmlPullParserException | IOException | InflateException ex) {
+ throw new NotFoundException(
+ "Can't load animation resource ID #0x" + Integer.toHexString(id), ex);
} finally {
if (parser != null) parser.close();
}
@@ -159,8 +153,9 @@
}
@UnsupportedAppUsage
- private static Animation createAnimationFromXml(Context c, XmlPullParser parser,
- AnimationSet parent, AttributeSet attrs) throws XmlPullParserException, IOException {
+ private static Animation createAnimationFromXml(
+ Context c, XmlPullParser parser, AnimationSet parent, AttributeSet attrs)
+ throws XmlPullParserException, IOException, InflateException {
Animation anim = null;
@@ -168,8 +163,8 @@
int type;
int depth = parser.getDepth();
- while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
- && type != XmlPullParser.END_DOCUMENT) {
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+ && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
@@ -193,7 +188,7 @@
} else if (name.equals("extend")) {
anim = new ExtendAnimation(c, attrs);
} else {
- throw new RuntimeException("Unknown animation name: " + parser.getName());
+ throw new InflateException("Unknown animation name: " + parser.getName());
}
if (parent != null) {
@@ -220,29 +215,24 @@
try {
parser = context.getResources().getAnimation(id);
return createLayoutAnimationFromXml(context, parser);
- } catch (XmlPullParserException ex) {
- NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
- Integer.toHexString(id));
- rnf.initCause(ex);
- throw rnf;
- } catch (IOException ex) {
- NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
- Integer.toHexString(id));
- rnf.initCause(ex);
- throw rnf;
+ } catch (XmlPullParserException | IOException | InflateException ex) {
+ throw new NotFoundException(
+ "Can't load animation resource ID #0x" + Integer.toHexString(id), ex);
} finally {
if (parser != null) parser.close();
}
}
- private static LayoutAnimationController createLayoutAnimationFromXml(Context c,
- XmlPullParser parser) throws XmlPullParserException, IOException {
+ private static LayoutAnimationController createLayoutAnimationFromXml(
+ Context c, XmlPullParser parser)
+ throws XmlPullParserException, IOException, InflateException {
return createLayoutAnimationFromXml(c, parser, Xml.asAttributeSet(parser));
}
- private static LayoutAnimationController createLayoutAnimationFromXml(Context c,
- XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException {
+ private static LayoutAnimationController createLayoutAnimationFromXml(
+ Context c, XmlPullParser parser, AttributeSet attrs)
+ throws XmlPullParserException, IOException, InflateException {
LayoutAnimationController controller = null;
@@ -263,7 +253,7 @@
} else if ("gridLayoutAnimation".equals(name)) {
controller = new GridLayoutAnimationController(c, attrs);
} else {
- throw new RuntimeException("Unknown layout animation name: " + name);
+ throw new InflateException("Unknown layout animation name: " + name);
}
}
@@ -342,16 +332,9 @@
try {
parser = context.getResources().getAnimation(id);
return createInterpolatorFromXml(context.getResources(), context.getTheme(), parser);
- } catch (XmlPullParserException ex) {
- NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
- Integer.toHexString(id));
- rnf.initCause(ex);
- throw rnf;
- } catch (IOException ex) {
- NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
- Integer.toHexString(id));
- rnf.initCause(ex);
- throw rnf;
+ } catch (XmlPullParserException | IOException | InflateException ex) {
+ throw new NotFoundException(
+ "Can't load animation resource ID #0x" + Integer.toHexString(id), ex);
} finally {
if (parser != null) parser.close();
}
@@ -372,25 +355,20 @@
try {
parser = res.getAnimation(id);
return createInterpolatorFromXml(res, theme, parser);
- } catch (XmlPullParserException ex) {
- NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
- Integer.toHexString(id));
- rnf.initCause(ex);
- throw rnf;
- } catch (IOException ex) {
- NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
- Integer.toHexString(id));
- rnf.initCause(ex);
- throw rnf;
+ } catch (XmlPullParserException | IOException | InflateException ex) {
+ throw new NotFoundException(
+ "Can't load animation resource ID #0x" + Integer.toHexString(id), ex);
} finally {
- if (parser != null)
+ if (parser != null) {
parser.close();
+ }
}
}
- private static Interpolator createInterpolatorFromXml(Resources res, Theme theme, XmlPullParser parser)
- throws XmlPullParserException, IOException {
+ private static Interpolator createInterpolatorFromXml(
+ Resources res, Theme theme, XmlPullParser parser)
+ throws XmlPullParserException, IOException, InflateException {
BaseInterpolator interpolator = null;
@@ -430,7 +408,7 @@
} else if (name.equals("pathInterpolator")) {
interpolator = new PathInterpolator(res, theme, attrs);
} else {
- throw new RuntimeException("Unknown interpolator name: " + parser.getName());
+ throw new InflateException("Unknown interpolator name: " + parser.getName());
}
}
return interpolator;
diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java
index cba399f..e7c610b 100644
--- a/core/java/android/view/autofill/AutofillFeatureFlags.java
+++ b/core/java/android/view/autofill/AutofillFeatureFlags.java
@@ -145,19 +145,13 @@
* <p>For example, a list with only 1 package would be, {@code Package1:;}. A list with one
* denied activity {@code Activity1} under {@code Package1} and a full denied package
* {@code Package2} would be {@code Package1:Activity1;Package2:;}
- *
- * @hide
*/
- @TestApi
public static final String DEVICE_CONFIG_PACKAGE_DENYLIST_FOR_UNIMPORTANT_VIEW =
"package_deny_list_for_unimportant_view";
/**
* Whether the heuristics check for view is enabled
- *
- * @hide
*/
- @TestApi
public static final String DEVICE_CONFIG_TRIGGER_FILL_REQUEST_ON_UNIMPORTANT_VIEW =
"trigger_fill_request_on_unimportant_view";
@@ -169,15 +163,24 @@
*
* <p> For example, a imeAction list could be "2,3,4", corresponding to ime_action definition
* in {@link android.view.inputmethod.EditorInfo.java}</p>
- *
- * @hide
*/
- @TestApi
@SuppressLint("IntentName")
public static final String DEVICE_CONFIG_NON_AUTOFILLABLE_IME_ACTION_IDS =
"non_autofillable_ime_action_ids";
// END AUTOFILL FOR ALL APPS FLAGS //
+
+ // START AUTOFILL PCC CLASSIFICATION FLAGS
+
+ /**
+ * Sets the fill dialog feature enabled or not.
+ */
+ public static final String DEVICE_CONFIG_AUTOFILL_PCC_CLASSIFICATION_ENABLED =
+ "pcc_classification_enabled";
+
+ // END AUTOFILL PCC CLASSIFICATION FLAGS
+
+
/**
* Sets a value of delay time to show up the inline tooltip view.
*
@@ -191,6 +194,7 @@
private static final boolean DEFAULT_HAS_FILL_DIALOG_UI_FEATURE = false;
private static final String DEFAULT_FILL_DIALOG_ENABLED_HINTS = "";
+
// CREDENTIAL MANAGER DEFAULTS
// Credential manager is enabled by default so as to allow testing by app developers
private static final boolean DEFAULT_CREDENTIAL_MANAGER_ENABLED = true;
@@ -199,6 +203,13 @@
private static final boolean DEFAULT_CREDENTIAL_MANAGER_SUPPRESS_SAVE_DIALOG = false;
// END CREDENTIAL MANAGER DEFAULTS
+
+ // AUTOFILL PCC CLASSIFICATION FLAGS DEFAULTS
+ // Default for whether the pcc classification is enabled for autofill.
+ private static final boolean DEFAULT_AUTOFILL_PCC_CLASSIFICATION_ENABLED = false;
+ // END AUTOFILL PCC CLASSIFICATION FLAGS DEFAULTS
+
+
private AutofillFeatureFlags() {};
/**
@@ -302,4 +313,23 @@
DeviceConfig.NAMESPACE_AUTOFILL,
DEVICE_CONFIG_PACKAGE_DENYLIST_FOR_UNIMPORTANT_VIEW, "");
}
+
+
+ // START AUTOFILL PCC CLASSIFICATION FUNCTIONS
+
+ /**
+ * Whether Autofill PCC Detection is enabled.
+ *
+ * @hide
+ */
+ public static boolean isAutofillPccClassificationEnabled() {
+ // TODO(b/266379948): Add condition for checking whether device has PCC first
+
+ return DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_AUTOFILL,
+ DEVICE_CONFIG_AUTOFILL_PCC_CLASSIFICATION_ENABLED,
+ DEFAULT_AUTOFILL_PCC_CLASSIFICATION_ENABLED);
+ }
+
+ // END AUTOFILL PCC CLASSIFICATION FUNCTIONS
}
diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java
index 5e323fa..8f270f5 100644
--- a/core/java/android/view/inputmethod/InputConnectionWrapper.java
+++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java
@@ -361,6 +361,16 @@
* @throws NullPointerException if the target is {@code null}.
*/
@Override
+ public boolean requestCursorUpdates(@CursorUpdateMode int cursorUpdateMode,
+ @CursorUpdateFilter int cursorUpdateFilter) {
+ return mTarget.requestCursorUpdates(cursorUpdateMode, cursorUpdateFilter);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
public void requestTextBoundsInfo(
@NonNull RectF bounds, @NonNull @CallbackExecutor Executor executor,
@NonNull Consumer<TextBoundsInfoResult> consumer) {
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index d71e82f..136846a 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -187,20 +187,34 @@
// Tag used when the Editor maintains its own separate UndoManager.
private static final String UNDO_OWNER_TAG = "Editor";
- // Ordering constants used to place the Action Mode or context menu items in their menu.
- private static final int MENU_ITEM_ORDER_ASSIST = 0;
- private static final int MENU_ITEM_ORDER_UNDO = 2;
- private static final int MENU_ITEM_ORDER_REDO = 3;
- private static final int MENU_ITEM_ORDER_CUT = 4;
- private static final int MENU_ITEM_ORDER_COPY = 5;
- private static final int MENU_ITEM_ORDER_PASTE = 6;
- private static final int MENU_ITEM_ORDER_SHARE = 7;
- private static final int MENU_ITEM_ORDER_SELECT_ALL = 8;
- private static final int MENU_ITEM_ORDER_REPLACE = 9;
- private static final int MENU_ITEM_ORDER_AUTOFILL = 10;
- private static final int MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 11;
- private static final int MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START = 50;
- private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
+ // Ordering constants used to place the Action Mode items in their menu.
+ private static final int ACTION_MODE_MENU_ITEM_ORDER_ASSIST = 0;
+ private static final int ACTION_MODE_MENU_ITEM_ORDER_CUT = 4;
+ private static final int ACTION_MODE_MENU_ITEM_ORDER_COPY = 5;
+ private static final int ACTION_MODE_MENU_ITEM_ORDER_PASTE = 6;
+ private static final int ACTION_MODE_MENU_ITEM_ORDER_SHARE = 7;
+ private static final int ACTION_MODE_MENU_ITEM_ORDER_SELECT_ALL = 8;
+ private static final int ACTION_MODE_MENU_ITEM_ORDER_REPLACE = 9;
+ private static final int ACTION_MODE_MENU_ITEM_ORDER_AUTOFILL = 10;
+ private static final int ACTION_MODE_MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 11;
+ private static final int ACTION_MODE_MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START = 50;
+ private static final int ACTION_MODE_MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
+
+ // Ordering constants used to place the Context Menu items in their menu.
+ private static final int CONTEXT_MENU_ITEM_ORDER_UNDO = 2;
+ private static final int CONTEXT_MENU_ITEM_ORDER_REDO = 3;
+ private static final int CONTEXT_MENU_ITEM_ORDER_CUT = 4;
+ private static final int CONTEXT_MENU_ITEM_ORDER_COPY = 5;
+ private static final int CONTEXT_MENU_ITEM_ORDER_PASTE = 6;
+ private static final int CONTEXT_MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 7;
+ private static final int CONTEXT_MENU_ITEM_ORDER_SELECT_ALL = 8;
+ private static final int CONTEXT_MENU_ITEM_ORDER_SHARE = 9;
+ private static final int CONTEXT_MENU_ITEM_ORDER_AUTOFILL = 10;
+ private static final int CONTEXT_MENU_ITEM_ORDER_REPLACE = 11;
+
+ private static final int CONTEXT_MENU_GROUP_UNDO_REDO = Menu.FIRST;
+ private static final int CONTEXT_MENU_GROUP_CLIPBOARD = Menu.FIRST + 1;
+ private static final int CONTEXT_MENU_GROUP_MISC = Menu.FIRST + 2;
private static final int FLAG_MISSPELLED_OR_GRAMMAR_ERROR =
SuggestionSpan.FLAG_MISSPELLED | SuggestionSpan.FLAG_GRAMMAR_ERROR;
@@ -3114,8 +3128,8 @@
for (int i = 0; i < suggestionInfoArray.length; i++) {
suggestionInfoArray[i] = new SuggestionInfo();
}
- final SubMenu subMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, MENU_ITEM_ORDER_REPLACE,
- com.android.internal.R.string.replace);
+ final SubMenu subMenu = menu.addSubMenu(Menu.NONE, Menu.NONE,
+ CONTEXT_MENU_ITEM_ORDER_REPLACE, com.android.internal.R.string.replace);
final int numItems = mSuggestionHelper.getSuggestionInfo(suggestionInfoArray, null);
for (int i = 0; i < numItems; i++) {
final SuggestionInfo info = suggestionInfoArray[i];
@@ -3133,47 +3147,51 @@
final int keyboard = mTextView.getResources().getConfiguration().keyboard;
menu.setQwertyMode(keyboard == Configuration.KEYBOARD_QWERTY);
- menu.add(Menu.NONE, TextView.ID_UNDO, MENU_ITEM_ORDER_UNDO,
+ menu.setGroupDividerEnabled(true);
+
+ menu.add(CONTEXT_MENU_GROUP_UNDO_REDO, TextView.ID_UNDO, CONTEXT_MENU_ITEM_ORDER_UNDO,
com.android.internal.R.string.undo)
.setAlphabeticShortcut('z')
.setOnMenuItemClickListener(mOnContextMenuItemClickListener)
.setEnabled(mTextView.canUndo());
- menu.add(Menu.NONE, TextView.ID_REDO, MENU_ITEM_ORDER_REDO,
+ menu.add(CONTEXT_MENU_GROUP_UNDO_REDO, TextView.ID_REDO, CONTEXT_MENU_ITEM_ORDER_REDO,
com.android.internal.R.string.redo)
.setAlphabeticShortcut('z', KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON)
.setOnMenuItemClickListener(mOnContextMenuItemClickListener)
.setEnabled(mTextView.canRedo());
- menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
+ menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_CUT, CONTEXT_MENU_ITEM_ORDER_CUT,
com.android.internal.R.string.cut)
.setAlphabeticShortcut('x')
.setOnMenuItemClickListener(mOnContextMenuItemClickListener)
.setEnabled(mTextView.canCut());
- menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
+ menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_COPY, CONTEXT_MENU_ITEM_ORDER_COPY,
com.android.internal.R.string.copy)
.setAlphabeticShortcut('c')
.setOnMenuItemClickListener(mOnContextMenuItemClickListener)
.setEnabled(mTextView.canCopy());
- menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
+ menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_PASTE, CONTEXT_MENU_ITEM_ORDER_PASTE,
com.android.internal.R.string.paste)
.setAlphabeticShortcut('v')
.setEnabled(mTextView.canPaste())
.setOnMenuItemClickListener(mOnContextMenuItemClickListener);
- menu.add(Menu.NONE, TextView.ID_PASTE_AS_PLAIN_TEXT, MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
+ menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_PASTE_AS_PLAIN_TEXT,
+ CONTEXT_MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
com.android.internal.R.string.paste_as_plain_text)
.setAlphabeticShortcut('v', KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON)
.setEnabled(mTextView.canPasteAsPlainText())
.setOnMenuItemClickListener(mOnContextMenuItemClickListener);
- menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
- com.android.internal.R.string.share)
- .setEnabled(mTextView.canShare())
- .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
- menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
- com.android.internal.R.string.selectAll)
+ menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_SELECT_ALL,
+ CONTEXT_MENU_ITEM_ORDER_SELECT_ALL, com.android.internal.R.string.selectAll)
.setAlphabeticShortcut('a')
.setEnabled(mTextView.canSelectAllText())
.setOnMenuItemClickListener(mOnContextMenuItemClickListener);
- menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
+
+ menu.add(CONTEXT_MENU_GROUP_MISC, TextView.ID_SHARE, CONTEXT_MENU_ITEM_ORDER_SHARE,
+ com.android.internal.R.string.share)
+ .setEnabled(mTextView.canShare())
+ .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
+ menu.add(CONTEXT_MENU_GROUP_MISC, TextView.ID_AUTOFILL, CONTEXT_MENU_ITEM_ORDER_AUTOFILL,
android.R.string.autofill)
.setEnabled(mTextView.canRequestAutofill())
.setOnMenuItemClickListener(mOnContextMenuItemClickListener);
@@ -4355,28 +4373,28 @@
private void populateMenuWithItems(Menu menu) {
if (mTextView.canCut()) {
- menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
+ menu.add(Menu.NONE, TextView.ID_CUT, ACTION_MODE_MENU_ITEM_ORDER_CUT,
com.android.internal.R.string.cut)
.setAlphabeticShortcut('x')
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
if (mTextView.canCopy()) {
- menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
+ menu.add(Menu.NONE, TextView.ID_COPY, ACTION_MODE_MENU_ITEM_ORDER_COPY,
com.android.internal.R.string.copy)
.setAlphabeticShortcut('c')
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
if (mTextView.canPaste()) {
- menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
+ menu.add(Menu.NONE, TextView.ID_PASTE, ACTION_MODE_MENU_ITEM_ORDER_PASTE,
com.android.internal.R.string.paste)
.setAlphabeticShortcut('v')
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
if (mTextView.canShare()) {
- menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
+ menu.add(Menu.NONE, TextView.ID_SHARE, ACTION_MODE_MENU_ITEM_ORDER_SHARE,
com.android.internal.R.string.share)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
}
@@ -4384,7 +4402,7 @@
if (mTextView.canRequestAutofill()) {
final String selected = mTextView.getSelectedText();
if (selected == null || selected.isEmpty()) {
- menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
+ menu.add(Menu.NONE, TextView.ID_AUTOFILL, ACTION_MODE_MENU_ITEM_ORDER_AUTOFILL,
com.android.internal.R.string.autofill)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
}
@@ -4394,7 +4412,7 @@
menu.add(
Menu.NONE,
TextView.ID_PASTE_AS_PLAIN_TEXT,
- MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
+ ACTION_MODE_MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
com.android.internal.R.string.paste_as_plain_text)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
}
@@ -4421,7 +4439,7 @@
boolean canSelectAll = mTextView.canSelectAllText();
boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null;
if (canSelectAll && !selectAllItemExists) {
- menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
+ menu.add(Menu.NONE, TextView.ID_SELECT_ALL, ACTION_MODE_MENU_ITEM_ORDER_SELECT_ALL,
com.android.internal.R.string.selectAll)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
} else if (!canSelectAll && selectAllItemExists) {
@@ -4433,7 +4451,7 @@
boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions();
boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null;
if (canReplace && !replaceItemExists) {
- menu.add(Menu.NONE, TextView.ID_REPLACE, MENU_ITEM_ORDER_REPLACE,
+ menu.add(Menu.NONE, TextView.ID_REPLACE, ACTION_MODE_MENU_ITEM_ORDER_REPLACE,
com.android.internal.R.string.replace)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
} else if (!canReplace && replaceItemExists) {
@@ -4459,12 +4477,12 @@
// Primary assist action (Always shown).
final MenuItem item = addAssistMenuItem(menu,
textClassification.getActions().get(0), TextView.ID_ASSIST,
- MENU_ITEM_ORDER_ASSIST, MenuItem.SHOW_AS_ACTION_ALWAYS);
+ ACTION_MODE_MENU_ITEM_ORDER_ASSIST, MenuItem.SHOW_AS_ACTION_ALWAYS);
item.setIntent(textClassification.getIntent());
} else if (hasLegacyAssistItem(textClassification)) {
// Legacy primary assist action (Always shown).
final MenuItem item = menu.add(
- TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST,
+ TextView.ID_ASSIST, TextView.ID_ASSIST, ACTION_MODE_MENU_ITEM_ORDER_ASSIST,
textClassification.getLabel())
.setIcon(textClassification.getIcon())
.setIntent(textClassification.getIntent());
@@ -4478,7 +4496,7 @@
for (int i = 1; i < count; i++) {
// Secondary assist action (Never shown).
addAssistMenuItem(menu, textClassification.getActions().get(i), Menu.NONE,
- MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START + i - 1,
+ ACTION_MODE_MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START + i - 1,
MenuItem.SHOW_AS_ACTION_NEVER);
}
mPrevTextClassification = textClassification;
@@ -7923,7 +7941,7 @@
for (int i = 0; i < size; i++) {
final ResolveInfo resolveInfo = mSupportedActivities.get(i);
menu.add(Menu.NONE, Menu.NONE,
- Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
+ Editor.ACTION_MODE_MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
getLabel(resolveInfo))
.setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 089545a..be58893 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -108,6 +108,7 @@
import android.text.InputFilter;
import android.text.InputType;
import android.text.Layout;
+import android.text.NoCopySpan;
import android.text.ParcelableSpan;
import android.text.PrecomputedText;
import android.text.SegmentFinder;
@@ -905,6 +906,8 @@
private Scroller mScroller;
private TextPaint mTempTextPaint;
+ private Object mTempCursor;
+
@UnsupportedAppUsage
private BoringLayout.Metrics mBoring;
@UnsupportedAppUsage
@@ -10060,10 +10063,8 @@
}
int offset = mLayout.getOffsetForHorizontal(line, point.x);
String textToInsert = gesture.getTextToInsert();
- getEditableText().insert(offset, textToInsert);
- Selection.setSelection(getEditableText(), offset + textToInsert.length());
+ return tryInsertTextForHandwritingGesture(offset, textToInsert, gesture);
// TODO(b/243980426): Insert extra spaces if necessary.
- return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
}
/** @hide */
@@ -10163,12 +10164,11 @@
if (startOffset < endOffset) {
getEditableText().delete(startOffset, endOffset);
Selection.setSelection(getEditableText(), startOffset);
+ return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
} else {
// No whitespace found, so insert a space.
- getEditableText().insert(startOffset, " ");
- Selection.setSelection(getEditableText(), startOffset + 1);
+ return tryInsertTextForHandwritingGesture(startOffset, " ", gesture);
}
- return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
}
/** @hide */
@@ -10252,6 +10252,32 @@
area, segmentFinder, Layout.INCLUSION_STRATEGY_CONTAINS_CENTER);
}
+ private int tryInsertTextForHandwritingGesture(
+ int offset, String textToInsert, HandwritingGesture gesture) {
+ // A temporary cursor span is placed at the insertion offset. The span will be pushed
+ // forward when text is inserted, then the real cursor can be placed after the inserted
+ // text. A temporary cursor span is used in order to avoid modifying the real selection span
+ // in the case that the text is filtered out.
+ Editable editableText = getEditableText();
+ if (mTempCursor == null) {
+ mTempCursor = new NoCopySpan.Concrete();
+ }
+ editableText.setSpan(mTempCursor, offset, offset, Spanned.SPAN_POINT_POINT);
+
+ editableText.insert(offset, textToInsert);
+
+ int newOffset = editableText.getSpanStart(mTempCursor);
+ editableText.removeSpan(mTempCursor);
+ if (newOffset == offset) {
+ // The inserted text was filtered out.
+ return handleGestureFailure(gesture);
+ } else {
+ // Place the cursor after the inserted text.
+ Selection.setSelection(editableText, newOffset);
+ return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
+ }
+ }
+
private Pattern getWhitespacePattern() {
if (mWhitespacePattern == null) {
mWhitespacePattern = Pattern.compile("\\s+");
diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.java b/core/java/android/window/ImeOnBackInvokedDispatcher.java
index a0bd7f7..34b75a4 100644
--- a/core/java/android/window/ImeOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ImeOnBackInvokedDispatcher.java
@@ -211,6 +211,12 @@
IOnBackInvokedCallback getIOnBackInvokedCallback() {
return mIOnBackInvokedCallback;
}
+
+ @Override
+ public String toString() {
+ return "ImeCallback=ImeOnBackInvokedCallback@" + mId
+ + " Callback=" + mIOnBackInvokedCallback;
+ }
}
/**
diff --git a/core/java/android/window/ProxyOnBackInvokedDispatcher.java b/core/java/android/window/ProxyOnBackInvokedDispatcher.java
index 09d8b0f..56c05b2 100644
--- a/core/java/android/window/ProxyOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ProxyOnBackInvokedDispatcher.java
@@ -180,16 +180,7 @@
return;
}
clearCallbacksOnDispatcher();
- if (actualDispatcher instanceof ProxyOnBackInvokedDispatcher) {
- // We don't want to nest ProxyDispatchers, so if we are given on, we unwrap its
- // actual dispatcher.
- // This can happen when an Activity is recreated but the Window is preserved (e.g.
- // when going from split-screen back to single screen)
- mActualDispatcher =
- ((ProxyOnBackInvokedDispatcher) actualDispatcher).mActualDispatcher;
- } else {
- mActualDispatcher = actualDispatcher;
- }
+ mActualDispatcher = actualDispatcher;
transferCallbacksToDispatcher();
}
}
diff --git a/core/java/android/window/SurfaceSyncGroup.java b/core/java/android/window/SurfaceSyncGroup.java
index 2e55041..78de954 100644
--- a/core/java/android/window/SurfaceSyncGroup.java
+++ b/core/java/android/window/SurfaceSyncGroup.java
@@ -53,7 +53,7 @@
* see the <a href="https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/window/SurfaceSyncGroup.md">SurfaceSyncGroup documentation</a>
* </p>
*/
-public class SurfaceSyncGroup {
+public final class SurfaceSyncGroup {
private static final String TAG = "SurfaceSyncGroup";
private static final boolean DEBUG = false;
@@ -101,6 +101,9 @@
*/
public final ISurfaceSyncGroup mISurfaceSyncGroup = new ISurfaceSyncGroupImpl();
+ @GuardedBy("mLock")
+ private Runnable mAddedToSyncListener;
+
/**
* Token to identify this SurfaceSyncGroup. This is used to register the SurfaceSyncGroup in
* WindowManager. This token is also sent to other processes' SurfaceSyncGroup that want to be
@@ -447,12 +450,15 @@
}
/**
- * Invoked when the SurfaceSyncGroup has been added to another SurfaceSyncGroup and is ready
- * to proceed.
+ * Add a Runnable to be invoked when the SurfaceSyncGroup has been added to another
+ * SurfaceSyncGroup. This is useful to know when it's safe to proceed rendering.
*
* @hide
*/
- public void onSyncReady() {
+ public void setAddedToSyncListener(Runnable addedToSyncListener) {
+ synchronized (mLock) {
+ mAddedToSyncListener = addedToSyncListener;
+ }
}
private boolean addSyncToWm(IBinder token, boolean parentSyncGroupMerge,
@@ -525,12 +531,15 @@
if (DEBUG) {
Log.d(TAG, "setTransactionCallbackFromParent " + mName);
}
- boolean finished = false;
+
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW,
"setTransactionCallbackFromParent " + mName + " callback="
+ transactionReadyCallback.hashCode());
}
+
+ boolean finished = false;
+ Runnable addedToSyncListener = null;
synchronized (mLock) {
if (mFinished) {
finished = true;
@@ -577,6 +586,7 @@
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
};
+ addedToSyncListener = mAddedToSyncListener;
}
}
@@ -587,8 +597,8 @@
transactionReadyCallback.onTransactionReady(null);
} catch (RemoteException e) {
}
- } else {
- onSyncReady();
+ } else if (addedToSyncListener != null) {
+ addedToSyncListener.run();
}
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
diff --git a/core/java/android/window/WindowMetricsController.java b/core/java/android/window/WindowMetricsController.java
index c497c94..3478b0f 100644
--- a/core/java/android/window/WindowMetricsController.java
+++ b/core/java/android/window/WindowMetricsController.java
@@ -127,7 +127,7 @@
isScreenRound, alwaysConsumeSystemBars, SOFT_INPUT_ADJUST_NOTHING,
0 /* flags */, SYSTEM_UI_FLAG_VISIBLE,
WindowManager.LayoutParams.INVALID_WINDOW_TYPE, windowingMode,
- null /* typeSideMap */);
+ null /* idSideMap */);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index 7a5510c..d34ece9 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -29,6 +29,7 @@
import android.view.IWindow;
import android.view.IWindowSession;
+import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
@@ -232,6 +233,26 @@
return Checker.isOnBackInvokedCallbackEnabled(mChecker.getContext());
}
+ /**
+ * Dump information about this WindowOnBackInvokedDispatcher
+ * @param prefix the prefix that will be prepended to each line of the produced output
+ * @param writer the writer that will receive the resulting text
+ */
+ public void dump(String prefix, PrintWriter writer) {
+ String innerPrefix = prefix + " ";
+ writer.println(prefix + "WindowOnBackDispatcher:");
+ if (mAllCallbacks.isEmpty()) {
+ writer.println(prefix + "<None>");
+ return;
+ }
+
+ writer.println(innerPrefix + "Top Callback: " + getTopCallback());
+ writer.println(innerPrefix + "Callbacks: ");
+ mAllCallbacks.forEach((callback, priority) -> {
+ writer.println(innerPrefix + " Callback: " + callback + " Priority=" + priority);
+ });
+ }
+
static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub {
private final WeakReference<OnBackInvokedCallback> mCallback;
diff --git a/core/java/com/android/internal/accessibility/common/MagnificationConstants.java b/core/java/com/android/internal/accessibility/common/MagnificationConstants.java
new file mode 100644
index 0000000..94c230b
--- /dev/null
+++ b/core/java/com/android/internal/accessibility/common/MagnificationConstants.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.accessibility.common;
+
+/**
+ * Collection of common constants for accessibility shortcut.
+ */
+public final class MagnificationConstants {
+ private MagnificationConstants() {}
+
+ /**
+ * The min value for the magnification persisted scale. We assume if the scale is lower than
+ * the min value, there will be no obvious magnification effect.
+ */
+ public static final float PERSISTED_SCALE_MIN_VALUE = 1.3f;
+}
diff --git a/core/java/com/android/internal/app/AssistUtils.java b/core/java/com/android/internal/app/AssistUtils.java
index f843318..fc5cb4b 100644
--- a/core/java/com/android/internal/app/AssistUtils.java
+++ b/core/java/com/android/internal/app/AssistUtils.java
@@ -229,6 +229,35 @@
}
}
+ /**
+ * Enables visual detection service.
+ *
+ * @param listener to receive visual attention gained/lost events.
+ */
+ public void enableVisualQueryDetection(
+ IVisualQueryDetectionAttentionListener listener) {
+ try {
+ if (mVoiceInteractionManagerService != null) {
+ mVoiceInteractionManagerService.enableVisualQueryDetection(listener);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to register visual query detection attention listener", e);
+ }
+ }
+
+ /**
+ * Disables visual query detection.
+ */
+ public void disableVisualQueryDetection() {
+ try {
+ if (mVoiceInteractionManagerService != null) {
+ mVoiceInteractionManagerService.disableVisualQueryDetection();
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to register visual query detection attention listener", e);
+ }
+ }
+
@UnsupportedAppUsage
public ComponentName getAssistComponentForUser(int userId) {
final String setting = Settings.Secure.getStringForUser(mContext.getContentResolver(),
diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
index 681bc7a..1eecb41 100644
--- a/core/java/com/android/internal/app/ChooserListAdapter.java
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -202,12 +202,12 @@
ri.nonLocalizedLabel = li.getNonLocalizedLabel();
ri.icon = li.getIconResource();
ri.iconResourceId = ri.icon;
- ri.userHandle = mInitialIntentsUserSpace;
}
if (userManager.isManagedProfile()) {
ri.noResourceId = true;
ri.icon = 0;
}
+ ri.userHandle = mInitialIntentsUserSpace;
mCallerTargets.add(new DisplayResolveInfo(ii, ri, ii, makePresentationGetter(ri)));
if (mCallerTargets.size() == MAX_SUGGESTED_APP_TARGETS) break;
}
diff --git a/core/java/com/android/internal/app/ISoundTriggerSession.aidl b/core/java/com/android/internal/app/ISoundTriggerSession.aidl
index a1ba558..3269ffc 100644
--- a/core/java/com/android/internal/app/ISoundTriggerSession.aidl
+++ b/core/java/com/android/internal/app/ISoundTriggerSession.aidl
@@ -36,7 +36,8 @@
void deleteSoundModel(in ParcelUuid soundModelId);
- int startRecognition(in ParcelUuid soundModelId, in IRecognitionStatusCallback callback,
+ int startRecognition(in SoundTrigger.GenericSoundModel soundModel,
+ in IRecognitionStatusCallback callback,
in SoundTrigger.RecognitionConfig config, boolean runInBatterySaver);
int stopRecognition(in ParcelUuid soundModelId, in IRecognitionStatusCallback callback);
diff --git a/core/java/com/android/internal/app/IVisualQueryDetectionAttentionListener.aidl b/core/java/com/android/internal/app/IVisualQueryDetectionAttentionListener.aidl
new file mode 100644
index 0000000..3e48da7
--- /dev/null
+++ b/core/java/com/android/internal/app/IVisualQueryDetectionAttentionListener.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.app;
+
+/**
+ * Allows sysui to notify users the assistant is ready to take a query without notifying the
+ * assistant app.
+ */
+oneway interface IVisualQueryDetectionAttentionListener {
+ /**
+ * Called when attention signal is sent.
+ */
+ void onAttentionGained();
+
+ /**
+ * Called when a attention signal is lost.
+ */
+ void onAttentionLost();
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
index 88d6425..619b420 100644
--- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
+++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
@@ -39,6 +39,7 @@
import com.android.internal.app.IVoiceInteractionSessionShowCallback;
import com.android.internal.app.IVoiceInteractionSoundTriggerSession;
import com.android.internal.app.IVoiceInteractor;
+import com.android.internal.app.IVisualQueryDetectionAttentionListener;
interface IVoiceInteractionManagerService {
void showSession(in Bundle sessionArgs, int flags, String attributionTag);
@@ -300,6 +301,12 @@
*/
void shutdownHotwordDetectionService();
+ @EnforcePermission("ACCESS_VOICE_INTERACTION_SERVICE")
+ void enableVisualQueryDetection(in IVisualQueryDetectionAttentionListener Listener);
+
+ @EnforcePermission("ACCESS_VOICE_INTERACTION_SERVICE")
+ void disableVisualQueryDetection();
+
void startPerceiving(in IVisualQueryDetectionVoiceInteractionCallback callback);
void stopPerceiving();
diff --git a/core/java/com/android/internal/app/OWNERS b/core/java/com/android/internal/app/OWNERS
index 56da8e0..69660ae 100644
--- a/core/java/com/android/internal/app/OWNERS
+++ b/core/java/com/android/internal/app/OWNERS
@@ -11,6 +11,7 @@
per-file *Assist* = file:/core/java/android/service/voice/OWNERS
per-file *Hotword* = file:/core/java/android/service/voice/OWNERS
per-file *Voice* = file:/core/java/android/service/voice/OWNERS
+per-file *VisualQuery* = file:/core/java/android/service/voice/OWNERS
# System language settings
per-file *Locale* = file:platform/packages/apps/Settings:/src/com/android/settings/localepicker/OWNERS
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
index 0ea60a7..18c8eb4 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -447,13 +447,13 @@
ri.nonLocalizedLabel = li.getNonLocalizedLabel();
ri.icon = li.getIconResource();
ri.iconResourceId = ri.icon;
- ri.userHandle = mInitialIntentsUserSpace;
}
if (userManager.isManagedProfile()) {
ri.noResourceId = true;
ri.icon = 0;
}
+ ri.userHandle = mInitialIntentsUserSpace;
addResolveInfo(new DisplayResolveInfo(ii, ri,
ri.loadLabel(mPm), null, ii, makePresentationGetter(ri)));
}
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
index b9373be..1084c71 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
@@ -562,6 +562,13 @@
"task_manager_show_user_visible_jobs";
/**
+ * (boolean) Whether the task manager should tell JobScheduler it's about to ask for an
+ * app stop.
+ */
+ public static final String TASK_MANAGER_INFORM_JOB_SCHEDULER_OF_PENDING_APP_STOP =
+ "task_manager_inform_job_scheduler_of_pending_app_stop";
+
+ /**
* (boolean) Whether to show notification volume control slider separate from ring.
*/
public static final String VOLUME_SEPARATE_NOTIFICATION = "volume_separate_notification";
diff --git a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
new file mode 100644
index 0000000..f724e55
--- /dev/null
+++ b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
@@ -0,0 +1,183 @@
+/**
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.config.sysui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Build;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Provides a central definition of debug SystemUI's SystemProperties flags, and their defaults.
+ *
+ * The main feature of this class is that it encodes a system-wide default for each flag which can
+ * be updated by engineers with a single-line CL.
+ *
+ * NOTE: Because flag values returned by this class are not cached, it is important that developers
+ * understand the intricacies of changing values and how that applies to their own code.
+ * Generally, the best practice is to set the property, and then restart the device so that any
+ * processes with stale state can be updated. However, if your code has no state derived from the
+ * flag value and queries it any time behavior is relevant, then it may be safe to change the flag
+ * and not immediately reboot.
+ *
+ * To enable flags in debuggable builds, use the following commands:
+ *
+ * $ adb shell setprop persist.sysui.whatever_the_flag true
+ * $ adb reboot
+ *
+ * @hide
+ */
+public class SystemUiSystemPropertiesFlags {
+
+ /** The interface used for resolving SystemUI SystemProperties Flags to booleans. */
+ public interface FlagResolver {
+ /** Is the flag enabled? */
+ boolean isEnabled(Flag flag);
+ }
+
+ /** The primary, immutable resolver returned by getResolver() */
+ private static final FlagResolver
+ MAIN_RESOLVER =
+ Build.IS_DEBUGGABLE ? new DebugResolver() : new ProdResolver();
+
+ /**
+ * On debuggable builds, this can be set to override the resolver returned by getResolver().
+ * This can be useful to override flags when testing components that do not allow injecting the
+ * SystemUiPropertiesFlags resolver they use.
+ * Always set this to null when tests tear down.
+ */
+ @VisibleForTesting
+ public static FlagResolver TEST_RESOLVER = null;
+
+ /** Get the resolver for this device configuration. */
+ public static FlagResolver getResolver() {
+ if (Build.IS_DEBUGGABLE && TEST_RESOLVER != null) {
+ Log.i("SystemUiSystemPropertiesFlags", "Returning debug resolver " + TEST_RESOLVER);
+ return TEST_RESOLVER;
+ }
+ return MAIN_RESOLVER;
+ }
+
+ /** The teamfood flag allows multiple features to be opted into at once. */
+ public static final Flag TEAMFOOD = devFlag("persist.sysui.teamfood");
+
+ /**
+ * Flags related to notification features
+ */
+ public static final class NotificationFlags {
+
+ /**
+ * FOR DEVELOPMENT / TESTING ONLY!!!
+ * Forcibly demote *ALL* FSI notifications as if no apps have the app op permission.
+ */
+ public static final Flag FSI_FORCE_DEMOTE =
+ devFlag("persist.sysui.notification.fsi_force_demote");
+
+ /** Gating the ability for users to dismiss ongoing event notifications */
+ public static final Flag ALLOW_DISMISS_ONGOING =
+ devFlag("persist.sysui.notification.ongoing_dismissal");
+
+ /** Gating the redaction of OTP notifications on the lockscreen */
+ public static final Flag OTP_REDACTION =
+ devFlag("persist.sysui.notification.otp_redaction");
+
+ }
+
+ //// == Everything below this line is the implementation == ////
+
+ /**
+ * Creates a flag that is enabled by default in debuggable builds.
+ * It can be enabled by setting this flag's SystemProperty to 1.
+ *
+ * This flag is ALWAYS disabled in release builds.
+ */
+ @VisibleForTesting
+ public static Flag devFlag(String name) {
+ return new Flag(name, false, null);
+ }
+
+ /**
+ * Creates a flag that is disabled by default in debuggable builds.
+ * It can be enabled or force-disabled by setting this flag's SystemProperty to 1 or 0.
+ * If this flag's SystemProperty is not set, the flag can be enabled by setting the
+ * TEAMFOOD flag's SystemProperty to 1.
+ *
+ * This flag is ALWAYS disabled in release builds.
+ */
+ @VisibleForTesting
+ public static Flag teamfoodFlag(String name) {
+ return new Flag(name, false, TEAMFOOD);
+ }
+
+ /**
+ * Creates a flag that is enabled by default in debuggable builds.
+ * It can be enabled by setting this flag's SystemProperty to 0.
+ *
+ * This flag is ALWAYS enabled in release builds.
+ */
+ @VisibleForTesting
+ public static Flag releasedFlag(String name) {
+ return new Flag(name, true, null);
+ }
+
+ /** Represents a developer-switchable gate for a feature. */
+ public static final class Flag {
+ public final String mSysPropKey;
+ public final boolean mDefaultValue;
+ @Nullable
+ public final Flag mDebugDefault;
+
+ /** constructs a new flag. only visible for testing the class */
+ @VisibleForTesting
+ public Flag(@NonNull String sysPropKey, boolean defaultValue, @Nullable Flag debugDefault) {
+ mSysPropKey = sysPropKey;
+ mDefaultValue = defaultValue;
+ mDebugDefault = debugDefault;
+ }
+ }
+
+ /** Implementation of the interface used in release builds. */
+ @VisibleForTesting
+ public static final class ProdResolver implements
+ FlagResolver {
+ @Override
+ public boolean isEnabled(Flag flag) {
+ return flag.mDefaultValue;
+ }
+ }
+
+ /** Implementation of the interface used in debuggable builds. */
+ @VisibleForTesting
+ public static class DebugResolver implements FlagResolver {
+ @Override
+ public final boolean isEnabled(Flag flag) {
+ if (flag.mDebugDefault == null) {
+ return getBoolean(flag.mSysPropKey, flag.mDefaultValue);
+ }
+ return getBoolean(flag.mSysPropKey, isEnabled(flag.mDebugDefault));
+ }
+
+ /** Look up the value; overridable for tests to avoid needing to set SystemProperties */
+ @VisibleForTesting
+ public boolean getBoolean(String key, boolean defaultValue) {
+ return SystemProperties.getBoolean(key, defaultValue);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/os/IBinaryTransparencyService.aidl b/core/java/com/android/internal/os/IBinaryTransparencyService.aidl
index e782aa7..c8340ac 100644
--- a/core/java/com/android/internal/os/IBinaryTransparencyService.aidl
+++ b/core/java/com/android/internal/os/IBinaryTransparencyService.aidl
@@ -28,8 +28,6 @@
interface IBinaryTransparencyService {
String getSignedImageInfo();
- List getApexInfo();
-
void recordMeasurementsForAllPackages();
parcelable ApexInfo {
@@ -60,4 +58,5 @@
/** Test only */
List<ApexInfo> collectAllApexInfo(boolean includeTestOnly);
List<AppInfo> collectAllUpdatedPreloadInfo(in Bundle packagesToSkip);
+ List<AppInfo> collectAllSilentInstalledMbaInfo(in Bundle packagesToSkip);
}
\ No newline at end of file
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index c6aca44..b63041b 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -378,8 +378,12 @@
// window, as we'll be skipping the addView in handleResumeActivity(), and
// the token will not be updated as for a new window.
getAttributes().token = preservedWindow.getAttributes().token;
- mProxyOnBackInvokedDispatcher.setActualDispatcher(
- preservedWindow.getOnBackInvokedDispatcher());
+ final ViewRootImpl viewRoot = mDecor.getViewRootImpl();
+ if (viewRoot != null) {
+ // Clear the old callbacks and attach to the new window.
+ viewRoot.getOnBackInvokedDispatcher().clear();
+ onViewRootImplSet(viewRoot);
+ }
}
// Even though the device doesn't support picture-in-picture mode,
// an user can force using it through developer options.
diff --git a/core/java/com/android/internal/security/VerityUtils.java b/core/java/com/android/internal/security/VerityUtils.java
index 786941f..74a9d16 100644
--- a/core/java/com/android/internal/security/VerityUtils.java
+++ b/core/java/com/android/internal/security/VerityUtils.java
@@ -81,6 +81,15 @@
}
}
+ /** Enables fs-verity for an open file without signature. */
+ public static void setUpFsverity(int fd) throws IOException {
+ int errno = enableFsverityForFdNative(fd);
+ if (errno != 0) {
+ throw new IOException("Failed to enable fs-verity on FD(" + fd + "): "
+ + Os.strerror(errno));
+ }
+ }
+
/** Returns whether the file has fs-verity enabled. */
public static boolean hasFsverity(@NonNull String filePath) {
int retval = statxForFsverityNative(filePath);
@@ -211,6 +220,7 @@
}
private static native int enableFsverityNative(@NonNull String filePath);
+ private static native int enableFsverityForFdNative(int fd);
private static native int measureFsverityNative(@NonNull String filePath,
@NonNull byte[] digest);
private static native int statxForFsverityNative(@NonNull String filePath);
diff --git a/core/java/com/android/internal/statusbar/AppClipsServiceConnector.java b/core/java/com/android/internal/statusbar/AppClipsServiceConnector.java
new file mode 100644
index 0000000..287b85f
--- /dev/null
+++ b/core/java/com/android/internal/statusbar/AppClipsServiceConnector.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.statusbar;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.util.Log;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * A helper class to communicate with the App Clips service running in SystemUI.
+ */
+public class AppClipsServiceConnector {
+
+ private static final String TAG = AppClipsServiceConnector.class.getSimpleName();
+
+ private final Context mContext;
+ private final Handler mHandler;
+
+ public AppClipsServiceConnector(Context context) {
+ mContext = context;
+ HandlerThread handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ mHandler = handlerThread.getThreadHandler();
+ }
+
+ /**
+ * @return true if the task represented by {@code taskId} can launch App Clips screenshot flow,
+ * false otherwise.
+ */
+ public boolean canLaunchCaptureContentActivityForNote(int taskId) {
+ try {
+ CompletableFuture<Boolean> future = new CompletableFuture<>();
+ connectToServiceAndProcessRequest(taskId, future);
+ return future.get();
+ } catch (Exception e) {
+ Log.d(TAG, "Exception from service\n" + e);
+ }
+
+ return false;
+ }
+
+ private void connectToServiceAndProcessRequest(int taskId, CompletableFuture<Boolean> future) {
+ ServiceConnection serviceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ try {
+ future.complete(IAppClipsService.Stub.asInterface(
+ service).canLaunchCaptureContentActivityForNote(taskId));
+ } catch (Exception e) {
+ Log.d(TAG, "Exception from service\n" + e);
+ }
+ future.complete(false);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (!future.isDone()) {
+ future.complete(false);
+ }
+ }
+ };
+
+ final ComponentName serviceComponent = ComponentName.unflattenFromString(
+ mContext.getResources().getString(
+ com.android.internal.R.string.config_screenshotAppClipsServiceComponent));
+ final Intent serviceIntent = new Intent();
+ serviceIntent.setComponent(serviceComponent);
+
+ boolean bindService = mContext.bindServiceAsUser(serviceIntent, serviceConnection,
+ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, mHandler,
+ mContext.getUser());
+
+ // Complete the future early if service not bound.
+ if (!bindService) {
+ future.complete(false);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/statusbar/IAppClipsService.aidl b/core/java/com/android/internal/statusbar/IAppClipsService.aidl
new file mode 100644
index 0000000..013d0d3
--- /dev/null
+++ b/core/java/com/android/internal/statusbar/IAppClipsService.aidl
@@ -0,0 +1,26 @@
+/**
+ * Copyright (C) 2023, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.statusbar;
+
+/**
+ * A service that runs in SystemUI and helps determine if App Clips flow is supported in the
+ * current state of device. This service needs to run in SystemUI in order to communicate with the
+ * instance of app bubbles.
+ */
+interface IAppClipsService {
+ boolean canLaunchCaptureContentActivityForNote(in int taskId);
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 1c85ca2..b529a10 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -214,11 +214,11 @@
* bar and navigation bar which are temporarily visible to the user.
*
* @param displayId the ID of the display to notify.
- * @param types the internal insets types of the bars are about to show transiently.
+ * @param types the insets types of the bars are about to show transiently.
* @param isGestureOnSystemBar whether the gesture to show the transient bar was a gesture on
* one of the bars itself.
*/
- void showTransient(int displayId, in int[] types, boolean isGestureOnSystemBar);
+ void showTransient(int displayId, int types, boolean isGestureOnSystemBar);
/**
* Notifies System UI to abort the transient state of system bars, which prevents the bars being
@@ -226,9 +226,9 @@
* bars again.
*
* @param displayId the ID of the display to notify.
- * @param types the internal insets types of the bars are about to abort the transient state.
+ * @param types the insets types of the bars are about to abort the transient state.
*/
- void abortTransient(int displayId, in int[] types);
+ void abortTransient(int displayId, int types);
/**
* Show a warning that the device is about to go to sleep due to user inactivity.
diff --git a/core/java/com/android/internal/statusbar/RegisterStatusBarResult.java b/core/java/com/android/internal/statusbar/RegisterStatusBarResult.java
index 54221ce..4f827cd 100644
--- a/core/java/com/android/internal/statusbar/RegisterStatusBarResult.java
+++ b/core/java/com/android/internal/statusbar/RegisterStatusBarResult.java
@@ -16,7 +16,6 @@
package com.android.internal.statusbar;
-import android.annotation.NonNull;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
@@ -41,15 +40,14 @@
public final int mBehavior;
public final int mRequestedVisibleTypes;
public final String mPackageName;
- public final int[] mTransientBarTypes;
+ public final int mTransientBarTypes;
public final LetterboxDetails[] mLetterboxDetails;
public RegisterStatusBarResult(ArrayMap<String, StatusBarIcon> icons, int disabledFlags1,
int appearance, AppearanceRegion[] appearanceRegions, int imeWindowVis,
int imeBackDisposition, boolean showImeSwitcher, int disabledFlags2, IBinder imeToken,
boolean navbarColorManagedByIme, int behavior, int requestedVisibleTypes,
- String packageName, @NonNull int[] transientBarTypes,
- LetterboxDetails[] letterboxDetails) {
+ String packageName, int transientBarTypes, LetterboxDetails[] letterboxDetails) {
mIcons = new ArrayMap<>(icons);
mDisabledFlags1 = disabledFlags1;
mAppearance = appearance;
@@ -87,7 +85,7 @@
dest.writeInt(mBehavior);
dest.writeInt(mRequestedVisibleTypes);
dest.writeString(mPackageName);
- dest.writeIntArray(mTransientBarTypes);
+ dest.writeInt(mTransientBarTypes);
dest.writeParcelableArray(mLetterboxDetails, flags);
}
@@ -113,7 +111,7 @@
final int behavior = source.readInt();
final int requestedVisibleTypes = source.readInt();
final String packageName = source.readString();
- final int[] transientBarTypes = source.createIntArray();
+ final int transientBarTypes = source.readInt();
final LetterboxDetails[] letterboxDetails =
source.readParcelableArray(null, LetterboxDetails.class);
return new RegisterStatusBarResult(icons, disabledFlags1, appearance,
diff --git a/core/java/com/android/internal/util/ScreenshotRequest.java b/core/java/com/android/internal/util/ScreenshotRequest.java
index 1902f80..c8b7def 100644
--- a/core/java/com/android/internal/util/ScreenshotRequest.java
+++ b/core/java/com/android/internal/util/ScreenshotRequest.java
@@ -173,6 +173,9 @@
public Builder(
@WindowManager.ScreenshotType int type,
@WindowManager.ScreenshotSource int source) {
+ if (type != TAKE_SCREENSHOT_FULLSCREEN && type != TAKE_SCREENSHOT_PROVIDED_IMAGE) {
+ throw new IllegalArgumentException("Invalid screenshot type requested!");
+ }
mType = type;
mSource = source;
}
diff --git a/core/java/com/android/internal/widget/LockPatternChecker.java b/core/java/com/android/internal/widget/LockPatternChecker.java
index e56c381..5c3759f 100644
--- a/core/java/com/android/internal/widget/LockPatternChecker.java
+++ b/core/java/com/android/internal/widget/LockPatternChecker.java
@@ -1,10 +1,7 @@
package com.android.internal.widget;
-import static android.provider.DeviceConfig.NAMESPACE_AUTO_PIN_CONFIRMATION;
-
import android.annotation.NonNull;
import android.os.AsyncTask;
-import android.provider.DeviceConfig;
import com.android.internal.widget.LockPatternUtils.RequestThrottledException;
@@ -120,8 +117,7 @@
@Override
protected void onPostExecute(Boolean result) {
callback.onChecked(result, mThrottleTimeout);
- if (DeviceConfig.getBoolean(NAMESPACE_AUTO_PIN_CONFIRMATION,
- "enable_auto_pin_confirmation", false)) {
+ if (LockPatternUtils.isAutoPinConfirmFeatureAvailable()) {
utils.setPinLength(userId, credentialCopy.size());
}
credentialCopy.zeroize();
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 2f51479..4d820ac 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -681,7 +681,7 @@
* @return true, if deviceConfig flag is set to true or the flag is not propagated and
* defaultValue is true.
*/
- public boolean isAutoPinConfirmFeatureAvailable() {
+ public static boolean isAutoPinConfirmFeatureAvailable() {
return DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_AUTO_PIN_CONFIRMATION,
FLAG_ENABLE_AUTO_PIN_CONFIRMATION,
diff --git a/core/java/com/android/server/backup/ShortcutBackupHelper.java b/core/java/com/android/server/backup/ShortcutBackupHelper.java
index 0b3f2ae..3c4b622 100644
--- a/core/java/com/android/server/backup/ShortcutBackupHelper.java
+++ b/core/java/com/android/server/backup/ShortcutBackupHelper.java
@@ -19,7 +19,6 @@
import android.content.Context;
import android.content.pm.IShortcutService;
import android.os.ServiceManager;
-import android.os.UserHandle;
import android.util.Slog;
public class ShortcutBackupHelper extends BlobBackupHelper {
@@ -28,8 +27,11 @@
private static final String KEY_USER_FILE = "shortcutuser.xml";
- public ShortcutBackupHelper() {
+ private final int mUserId;
+
+ public ShortcutBackupHelper(int userId) {
super(BLOB_VERSION, KEY_USER_FILE);
+ mUserId = userId;
}
private IShortcutService getShortcutService() {
@@ -42,7 +44,7 @@
switch (key) {
case KEY_USER_FILE:
try {
- return getShortcutService().getBackupPayload(UserHandle.USER_SYSTEM);
+ return getShortcutService().getBackupPayload(mUserId);
} catch (Exception e) {
Slog.wtf(TAG, "Backup failed", e);
}
@@ -58,7 +60,7 @@
switch (key) {
case KEY_USER_FILE:
try {
- getShortcutService().applyRestore(payload, UserHandle.USER_SYSTEM);
+ getShortcutService().applyRestore(payload, mUserId);
} catch (Exception e) {
Slog.wtf(TAG, "Restore failed", e);
}
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 82f414b..b79c540 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -40,6 +40,12 @@
cppflags: ["-Wno-conversion-null"],
+ product_variables: {
+ eng: {
+ cflags: ["-DNO_RESET_STACK_PROTECTOR"],
+ },
+ },
+
cpp_std: "gnu++20",
srcs: [
diff --git a/core/jni/LayoutlibLoader.cpp b/core/jni/LayoutlibLoader.cpp
index 93ba23b..d7cbf74 100644
--- a/core/jni/LayoutlibLoader.cpp
+++ b/core/jni/LayoutlibLoader.cpp
@@ -102,6 +102,7 @@
extern int register_android_view_KeyEvent(JNIEnv* env);
extern int register_android_view_MotionEvent(JNIEnv* env);
extern int register_android_view_ThreadedRenderer(JNIEnv* env);
+extern int register_android_graphics_HardwareBufferRenderer(JNIEnv* env);
extern int register_android_view_VelocityTracker(JNIEnv* env);
extern int register_com_android_internal_util_VirtualRefBasePtr(JNIEnv *env);
diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp
index a8d8a43..b09a9c3 100644
--- a/core/jni/android_view_DisplayEventReceiver.cpp
+++ b/core/jni/android_view_DisplayEventReceiver.cpp
@@ -65,7 +65,7 @@
public:
NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak,
const sp<MessageQueue>& messageQueue, jint vsyncSource,
- jint eventRegistration);
+ jint eventRegistration, jlong layerHandle);
void dispose();
@@ -88,11 +88,15 @@
NativeDisplayEventReceiver::NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak,
const sp<MessageQueue>& messageQueue,
- jint vsyncSource, jint eventRegistration)
+ jint vsyncSource, jint eventRegistration,
+ jlong layerHandle)
: DisplayEventDispatcher(messageQueue->getLooper(),
static_cast<gui::ISurfaceComposer::VsyncSource>(vsyncSource),
static_cast<gui::ISurfaceComposer::EventRegistration>(
- eventRegistration)),
+ eventRegistration),
+ layerHandle != 0 ? sp<IBinder>::fromExisting(
+ reinterpret_cast<IBinder*>(layerHandle))
+ : nullptr),
mReceiverWeakGlobal(env->NewGlobalRef(receiverWeak)),
mMessageQueue(messageQueue) {
ALOGV("receiver %p ~ Initializing display event receiver.", this);
@@ -214,7 +218,7 @@
}
static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak, jobject messageQueueObj,
- jint vsyncSource, jint eventRegistration) {
+ jint vsyncSource, jint eventRegistration, jlong layerHandle) {
sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);
if (messageQueue == NULL) {
jniThrowRuntimeException(env, "MessageQueue is not initialized.");
@@ -223,7 +227,7 @@
sp<NativeDisplayEventReceiver> receiver =
new NativeDisplayEventReceiver(env, receiverWeak, messageQueue, vsyncSource,
- eventRegistration);
+ eventRegistration, layerHandle);
status_t status = receiver->initialize();
if (status) {
String8 message;
@@ -236,13 +240,15 @@
return reinterpret_cast<jlong>(receiver.get());
}
-static void nativeDispose(JNIEnv* env, jclass clazz, jlong receiverPtr) {
- NativeDisplayEventReceiver* receiver =
- reinterpret_cast<NativeDisplayEventReceiver*>(receiverPtr);
+static void release(NativeDisplayEventReceiver* receiver) {
receiver->dispose();
receiver->decStrong(gDisplayEventReceiverClassInfo.clazz); // drop reference held by the object
}
+static jlong nativeGetDisplayEventReceiverFinalizer(JNIEnv*, jclass) {
+ return static_cast<jlong>(reinterpret_cast<uintptr_t>(&release));
+}
+
static void nativeScheduleVsync(JNIEnv* env, jclass clazz, jlong receiverPtr) {
sp<NativeDisplayEventReceiver> receiver =
reinterpret_cast<NativeDisplayEventReceiver*>(receiverPtr);
@@ -268,9 +274,10 @@
static const JNINativeMethod gMethods[] = {
/* name, signature, funcPtr */
- {"nativeInit", "(Ljava/lang/ref/WeakReference;Landroid/os/MessageQueue;II)J",
+ {"nativeInit", "(Ljava/lang/ref/WeakReference;Landroid/os/MessageQueue;IIJ)J",
(void*)nativeInit},
- {"nativeDispose", "(J)V", (void*)nativeDispose},
+ {"nativeGetDisplayEventReceiverFinalizer", "()J",
+ (void*)nativeGetDisplayEventReceiverFinalizer},
// @FastNative
{"nativeScheduleVsync", "(J)V", (void*)nativeScheduleVsync},
{"nativeGetLatestVsyncEventData", "(J)Landroid/view/DisplayEventReceiver$VsyncEventData;",
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index 6a92581..5965e17 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -2302,7 +2302,7 @@
setpriority(PRIO_PROCESS, 0, PROCESS_PRIORITY_MIN);
}
-#if defined(__BIONIC__)
+#if defined(__BIONIC__) && !defined(NO_RESET_STACK_PROTECTOR)
// Reset the stack guard for the new process.
android_reset_stack_guards();
#endif
diff --git a/core/jni/com_android_internal_security_VerityUtils.cpp b/core/jni/com_android_internal_security_VerityUtils.cpp
index 3e5689b..4a9e2d4 100644
--- a/core/jni/com_android_internal_security_VerityUtils.cpp
+++ b/core/jni/com_android_internal_security_VerityUtils.cpp
@@ -38,13 +38,8 @@
namespace {
-int enableFsverity(JNIEnv *env, jobject /* clazz */, jstring filePath) {
- ScopedUtfChars path(env, filePath);
- if (path.c_str() == nullptr) {
- return EINVAL;
- }
- ::android::base::unique_fd rfd(open(path.c_str(), O_RDONLY | O_CLOEXEC));
- if (rfd.get() < 0) {
+int enableFsverityForFd(JNIEnv *env, jobject clazz, jint fd) {
+ if (fd < 0) {
return errno;
}
@@ -55,12 +50,21 @@
arg.salt_size = 0;
arg.salt_ptr = reinterpret_cast<uintptr_t>(nullptr);
- if (ioctl(rfd.get(), FS_IOC_ENABLE_VERITY, &arg) < 0) {
+ if (ioctl(fd, FS_IOC_ENABLE_VERITY, &arg) < 0) {
return errno;
}
return 0;
}
+int enableFsverity(JNIEnv *env, jobject clazz, jstring filePath) {
+ ScopedUtfChars path(env, filePath);
+ if (path.c_str() == nullptr) {
+ return EINVAL;
+ }
+ ::android::base::unique_fd rfd(open(path.c_str(), O_RDONLY | O_CLOEXEC));
+ return enableFsverityForFd(env, clazz, rfd.get());
+}
+
// Returns whether the file has fs-verity enabled.
// 0 if it is not present, 1 if is present, and -errno if there was an error.
int statxForFsverity(JNIEnv *env, jobject /* clazz */, jstring filePath) {
@@ -126,6 +130,7 @@
}
const JNINativeMethod sMethods[] = {
{"enableFsverityNative", "(Ljava/lang/String;)I", (void *)enableFsverity},
+ {"enableFsverityForFdNative", "(I)I", (void *)enableFsverityForFd},
{"statxForFsverityNative", "(Ljava/lang/String;)I", (void *)statxForFsverity},
{"measureFsverityNative", "(Ljava/lang/String;[B)I", (void *)measureFsverity},
};
diff --git a/core/proto/android/os/system_properties.proto b/core/proto/android/os/system_properties.proto
index 4f3eeb0..84c82e0 100644
--- a/core/proto/android/os/system_properties.proto
+++ b/core/proto/android/os/system_properties.proto
@@ -514,7 +514,10 @@
optional string gfx_driver_whitelist_0 = 45;
- // Next Tag: 46
+ optional bool egl_blobcache_multifile = 46;
+ optional int32 egl_blobcache_multifile_limit = 47;
+
+ // Next Tag: 48
}
optional Ro ro = 21;
diff --git a/core/proto/android/providers/settings/global.proto b/core/proto/android/providers/settings/global.proto
index fd0d9c5..128de8b 100644
--- a/core/proto/android/providers/settings/global.proto
+++ b/core/proto/android/providers/settings/global.proto
@@ -88,6 +88,9 @@
optional SettingProto assisted_gps_enabled = 14 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto audio_safe_volume_state = 15 [ (android.privacy).dest = DEST_AUTOMATIC ];
+ optional SettingProto audio_safe_csd_current_value = 157 [ (android.privacy).dest = DEST_AUTOMATIC ];
+ optional SettingProto audio_safe_csd_next_warning = 158 [ (android.privacy).dest = DEST_AUTOMATIC ];
+ optional SettingProto audio_safe_csd_dose_records = 159 [ (android.privacy).dest = DEST_AUTOMATIC ];
reserved 17; // Used to be autofill_compat_mode_allowed_packages
@@ -1087,5 +1090,5 @@
// Please insert fields in alphabetical order and group them into messages
// if possible (to avoid reaching the method limit).
- // Next tag = 157;
+ // Next tag = 160;
}
diff --git a/core/proto/android/service/usb.proto b/core/proto/android/service/usb.proto
index 607fd10..c98e346 100644
--- a/core/proto/android/service/usb.proto
+++ b/core/proto/android/service/usb.proto
@@ -284,7 +284,8 @@
optional int32 cards_parser = 1;
repeated UsbAlsaDeviceProto alsa_devices = 2;
- repeated UsbMidiDeviceProto midi_devices = 3;
+ reserved 3; // previously midi_devices, now unused
+ repeated UsbAlsaMidiDeviceProto alsa_midi_devices = 4;
}
message UsbAlsaDeviceProto {
@@ -299,7 +300,7 @@
optional string address = 6;
}
-message UsbMidiDeviceProto {
+message UsbAlsaMidiDeviceProto {
option (android.msg_privacy).dest = DEST_AUTOMATIC;
optional int32 card = 1;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 49f4d35..0b6b0a1 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1751,7 +1751,7 @@
<!-- Allows an application to access wrist temperature data from the watch sensors.
If you're requesting this permission, you must also request
{@link #BODY_SENSORS_WRIST_TEMPERATURE}. Requesting this permission by itself doesn't
- give you heart rate body sensors access.
+ give you wrist temperature body sensors access.
<p class="note"><strong>Note: </strong> This permission is for Wear OS only.
<p>Protection level: dangerous
@@ -2579,6 +2579,14 @@
<!-- ==================================================== -->
<eat-comment />
+ <!-- Allows an application to capture screen content to perform a screenshot using the intent
+ action {@link android.content.Intent#ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE}.
+ <p>Protection level: internal|role
+ <p>Intended for use by ROLE_NOTES only.
+ -->
+ <permission android:name="android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE"
+ android:protectionLevel="internal|role" />
+
<!-- Allows an application to get notified when a screen capture of its windows is attempted.
<p>Protection level: normal
-->
@@ -4534,6 +4542,15 @@
<permission android:name="android.permission.BIND_AUTOFILL_SERVICE"
android:protectionLevel="signature" />
+ <!-- Must be required by a
+ {@link android.service.assist.classification.FieldClassificationService},
+ to ensure that only the system can bind to it.
+ @SystemApi @hide This is not a third-party API (intended for OEMs and system apps).
+ <p>Protection level: signature
+ -->
+ <permission android:name="android.permission.BIND_FIELD_CLASSIFICATION_SERVICE"
+ android:protectionLevel="signature" />
+
<!-- Must be required by a CredentialProviderService to ensure that only the
system can bind to it.
<p>Protection level: signature
@@ -6921,8 +6938,8 @@
<!-- Allows an assistive application to perform actions on behalf of users inside of
applications.
- <p>For now, this permission is only granted to system applications fulfilling the
- ASSISTANT role.
+ <p>For now, this permission is only granted to the Assistant application selected by
+ the user.
<p>Protection level: internal|role
-->
<permission android:name="android.permission.EXECUTE_APP_ACTION"
diff --git a/core/res/res/drawable/ic_clone_badge.xml b/core/res/res/drawable/ic_clone_badge.xml
new file mode 100644
index 0000000..1682425
--- /dev/null
+++ b/core/res/res/drawable/ic_clone_badge.xml
@@ -0,0 +1,29 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M22,9.5C22,13.642 18.642,17 14.5,17C10.358,17 7,13.642 7,9.5C7,5.358 10.358,2 14.5,2C18.642,2 22,5.358 22,9.5Z"
+ android:fillColor="@android:color/system_neutral2_800"/>
+ <path
+ android:pathData="M9.5,20.333C12.722,20.333 15.333,17.722 15.333,14.5C15.333,11.278 12.722,8.667 9.5,8.667C6.278,8.667 3.667,11.278 3.667,14.5C3.667,17.722 6.278,20.333 9.5,20.333ZM9.5,22C13.642,22 17,18.642 17,14.5C17,10.358 13.642,7 9.5,7C5.358,7 2,10.358 2,14.5C2,18.642 5.358,22 9.5,22Z"
+ android:fillColor="@android:color/system_neutral2_800"
+ android:fillType="evenOdd"/>
+</vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_clone_icon_badge.xml b/core/res/res/drawable/ic_clone_icon_badge.xml
new file mode 100644
index 0000000..e4eabc8
--- /dev/null
+++ b/core/res/res/drawable/ic_clone_icon_badge.xml
@@ -0,0 +1,35 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="64dp"
+ android:height="64dp"
+ android:viewportWidth="64"
+ android:viewportHeight="64">
+<group
+ android:scaleX=".66"
+ android:scaleY=".66"
+ android:translateX="42"
+ android:translateY="42">
+ <path
+ android:pathData="M22,9.5C22,13.642 18.642,17 14.5,17C10.358,17 7,13.642 7,9.5C7,5.358 10.358,2 14.5,2C18.642,2 22,5.358 22,9.5Z"
+ android:fillColor="@android:color/system_neutral2_800"/>
+ <path
+ android:pathData="M9.5,20.333C12.722,20.333 15.333,17.722 15.333,14.5C15.333,11.278 12.722,8.667 9.5,8.667C6.278,8.667 3.667,11.278 3.667,14.5C3.667,17.722 6.278,20.333 9.5,20.333ZM9.5,22C13.642,22 17,18.642 17,14.5C17,10.358 13.642,7 9.5,7C5.358,7 2,10.358 2,14.5C2,18.642 5.358,22 9.5,22Z"
+ android:fillColor="@android:color/system_neutral2_800"
+ android:fillType="evenOdd"/>
+</group>
+</vector>
diff --git a/core/res/res/drawable/ic_dual_screen.xml b/core/res/res/drawable/ic_dual_screen.xml
new file mode 100644
index 0000000..26d97b7
--- /dev/null
+++ b/core/res/res/drawable/ic_dual_screen.xml
@@ -0,0 +1,25 @@
+<!--
+Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M11,35.9 L25,41.5Q25,41.5 25,41.5Q25,41.5 25,41.5V12.1Q25,12.1 25,12.1Q25,12.1 25,12.1L11,6.5V35.9Q11,35.9 11,35.9Q11,35.9 11,35.9ZM9.9,38.65Q9.05,38.3 8.525,37.575Q8,36.85 8,35.9V7Q8,5.75 8.875,4.875Q9.75,4 11,4L26,9.3Q26.9,9.6 27.45,10.375Q28,11.15 28,12.1V41.5Q28,43.1 26.675,43.975Q25.35,44.85 23.9,44.25ZM25,38V35H37Q37,35 37,35Q37,35 37,35V7Q37,7 37,7Q37,7 37,7H11V4H37Q38.25,4 39.125,4.875Q40,5.75 40,7V35Q40,36.25 39.125,37.125Q38.25,38 37,38ZM11,35.9Q11,35.9 11,35.9Q11,35.9 11,35.9V6.5Q11,6.5 11,6.5Q11,6.5 11,6.5V35.9Q11,35.9 11,35.9Q11,35.9 11,35.9Z"/>
+</vector>
diff --git a/core/res/res/drawable/ic_thermostat.xml b/core/res/res/drawable/ic_thermostat.xml
new file mode 100644
index 0000000..23e5792
--- /dev/null
+++ b/core/res/res/drawable/ic_thermostat.xml
@@ -0,0 +1,25 @@
+<!--
+Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M26.15,19V16H37.65V19ZM26.15,13V10H42.15V13ZM14.8,41.95Q11.05,41.95 8.375,39.275Q5.7,36.6 5.7,32.85Q5.7,30.45 6.9,28.325Q8.1,26.2 10.2,24.9V10.6Q10.2,8.7 11.55,7.35Q12.9,6 14.8,6Q16.7,6 18.05,7.35Q19.4,8.7 19.4,10.6V24.9Q21.5,26.2 22.7,28.325Q23.9,30.45 23.9,32.85Q23.9,36.6 21.225,39.275Q18.55,41.95 14.8,41.95ZM8.7,32.85H20.9Q20.9,31.1 19.95,29.4Q19,27.7 17.4,27L16.4,26.55V10.6Q16.4,9.9 15.95,9.45Q15.5,9 14.8,9Q14.1,9 13.65,9.45Q13.2,9.9 13.2,10.6V26.55L12.2,27Q10.45,27.8 9.575,29.45Q8.7,31.1 8.7,32.85Z"/>
+</vector>
diff --git a/core/res/res/values/arrays.xml b/core/res/res/values/arrays.xml
index 9e735d0..c3366cf 100644
--- a/core/res/res/values/arrays.xml
+++ b/core/res/res/values/arrays.xml
@@ -207,4 +207,25 @@
be set by OEMs for devices which use eUICCs. -->
<integer-array name="non_removable_euicc_slots"></integer-array>
+ <!-- Device state identifiers and strings for system notifications. The string arrays must have
+ the same length and order as the identifier array. -->
+ <integer-array name="device_state_notification_state_identifiers">
+ <!-- TODO(b/267231269) change to concurrent display identifier when ready -->
+ <item>-1</item>
+ </integer-array>
+ <string-array name="device_state_notification_names">
+ <item>@string/concurrent_display_notification_name</item>
+ </string-array>
+ <string-array name="device_state_notification_active_titles">
+ <item>@string/concurrent_display_notification_active_title</item>
+ </string-array>
+ <string-array name="device_state_notification_active_contents">
+ <item>@string/concurrent_display_notification_active_content</item>
+ </string-array>
+ <string-array name="device_state_notification_thermal_titles">
+ <item>@string/concurrent_display_notification_thermal_title</item>
+ </string-array>
+ <string-array name="device_state_notification_thermal_contents">
+ <item>@string/concurrent_display_notification_thermal_content</item>
+ </string-array>
</resources>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index b8ce7e3..82de7b8 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -983,6 +983,13 @@
<integer-array name="config_deviceStatesToReverseDefaultDisplayRotationAroundZAxis">
</integer-array>
+ <!-- Boolean indicating whether secondary built-in displays should have their orientation
+ match the active default display. This config assumes that the secondary display only
+ requires swapping ROTATION_90 and ROTATION_270.
+ TODO(b/265991392): This should eventually be configured and parsed in
+ display_settings.xml -->
+ <bool name="config_matchSecondaryInternalDisplaysOrientationToReverseDefaultDisplay">true</bool>
+
<!-- Indicate available ColorDisplayManager.COLOR_MODE_xxx. -->
<integer-array name="config_availableColorModes">
<!-- Example:
@@ -2186,6 +2193,9 @@
<!-- The name of the package that will hold the system financed device controller role. -->
<string name="config_systemFinancedDeviceController" translatable="false">com.android.devicelockcontroller</string>
+ <!-- The component name of the wear service class that will be started by the system server. -->
+ <string name="config_wearServiceComponent" translatable="false"></string>
+
<!-- The name of the package that will handle updating the device management role. -->
<string name="config_devicePolicyManagementUpdater" translatable="false"></string>
@@ -2674,6 +2684,14 @@
<!-- Maximum velocity to initiate a fling, as measured in dips per second. -->
<dimen name="config_viewMaxFlingVelocity">8000dp</dimen>
+ <!-- Minimum velocity (absolute value) to initiate a fling from a rotary encoder device, as
+ measured in dips per second. Setting this to -1dp disables rotary encoder fling. -->
+ <dimen name="config_viewMinRotaryEncoderFlingVelocity">-1dp</dimen>
+
+ <!-- Maximum velocity (absolute value) to initiate a fling from a rotary encoder device, as
+ measured in dips per second. Setting this to -1dp disables rotary encoder fling. -->
+ <dimen name="config_viewMaxRotaryEncoderFlingVelocity">-1dp</dimen>
+
<!-- Amount of time in ms the user needs to press the relevant key to bring up the
global actions dialog -->
<integer name="config_globalActionsKeyTimeout">500</integer>
@@ -2735,6 +2753,11 @@
Should be false for most devices, except automotive vehicle with passenger displays. -->
<bool name="config_multiuserVisibleBackgroundUsers">false</bool>
+ <!-- Whether the device allows users to start in background visible on the default display.
+ Should be false for most devices, except passenger-only automotive build (i.e., when
+ Android runs in a separate system in the back seat to manage the passenger displays) -->
+ <bool name="config_multiuserVisibleBackgroundUsersOnDefaultDisplay">false</bool>
+
<!-- Whether to automatically switch to the designated Dock User (the user chosen for
displaying dreams, etc.) after a timeout when the device is docked. -->
<bool name="config_enableTimeoutToDockUserWhenDocked">false</bool>
@@ -3220,6 +3243,12 @@
<string name="config_somnambulatorComponent" translatable="false"
>com.android.systemui/com.android.systemui.Somnambulator</string>
+ <!-- The component name of the screenshot App Clips service that communicates with SystemUI to
+ evaluate certain aspects of App Clips flow such as whether a calling activity can launch
+ capture content for note activity. -->
+ <string name="config_screenshotAppClipsServiceComponent" translatable="false"
+ >com.android.systemui/com.android.systemui.screenshot.appclips.AppClipsService</string>
+
<!-- The component name of a special dock app that merely launches a dream.
We don't want to launch this app when docked because it causes an unnecessary
activity transition. We just want to start the dream.. -->
@@ -4091,16 +4120,6 @@
-->
<string-array translatable="false" name="config_convert_to_emergency_number_map" />
- <!-- An array of packages for which notifications cannot be blocked.
- Should only be used for core device functionality that must not be
- rendered inoperative for safety reasons, like the phone dialer and
- SMS handler. -->
- <string-array translatable="false" name="config_nonBlockableNotificationPackages">
- <item>com.android.dialer</item>
- <item>com.android.messaging</item>
- <item>com.android.cellbroadcastreceiver.module</item>
- </string-array>
-
<!-- An array of packages that can make sound on the ringer stream in priority-only DND
mode -->
<string-array translatable="false" name="config_priorityOnlyDndExemptPackages">
@@ -4157,12 +4176,19 @@
<bool name="config_quickSettingsSupported">true</bool>
<!-- The component name, flattened to a string, for the default autofill service
- to enabled for an user. This service must be trusted, as it can be activated
+ to enabled for a user. This service must be trusted, as it can be activated
without explicit consent of the user. If no autofill service with the
specified name exists on the device, autofill will be disabled by default.
-->
<string name="config_defaultAutofillService" translatable="false"></string>
+ <!-- The component name, flattened to a string, for the default field classification service
+ to enabled for a user. This service must be trusted, as it can be activated
+ without explicit consent of the user. If no field classification service with the
+ specified name exists on the device, field classification will be disabled by default.
+ -->
+ <string name="config_defaultFieldClassificationService" translatable="false"></string>
+
<!-- The package name for the OEM custom system textclassifier service.
This service must be trusted, as it can be activated without explicit consent of the user.
Example: "com.android.textclassifier"
@@ -5001,6 +5027,26 @@
-->
</integer-array>
+ <!-- The properties for handling UDFPS touch detection
+ <string-array name="config_udfps_touch_detection_options">
+ <item>[detector_type],[sensor_shape],[target_size],[min_ellipse_overlap_percentage]</item>
+ </string-array>
+
+ [detector_type]: 0 for bounding box detector, 1 for ellipse detector
+ [sensor_shape]: 0 for square, 1 for circle
+ [target_size]: percentage (defined as a float of 0-1) of sensor that is considered the target, expands outward from center
+ [min_ellipse_overlap_percentage]: minimum percentage (float from 0-1) needed to be considered a valid overlap
+ [step_size]: size of each step when iterating over sensor pixel grid
+ -->
+ <string-array name="config_udfps_touch_detection_options">
+ <item>0,0,1.0,0,1</item>
+ <item>1,1,1.0,0,1</item>
+ <item>1,1,1.0,.4,1</item>
+ </string-array>
+
+ <!-- The integer index of the selected option in config_udfps_touch_detection_options -->
+ <integer name="config_selected_udfps_touch_detection">2</integer>
+
<!-- An array of arrays of side fingerprint sensor properties relative to each display.
Note: this value is temporary and is expected to be queried directly
from the HAL in the future. -->
@@ -6164,6 +6210,18 @@
different from the home screen wallpaper. -->
<bool name="config_independentLockscreenLiveWallpaper">false</bool>
+ <!-- Device state that corresponds to concurrent display mode where the default display
+ is the internal display. Public API for the feature is provided through Jetpack
+ WindowManager.
+ TODO(b/236022708) Move concurrent display state to device state config file
+ -->
+ <integer name="config_deviceStateConcurrentRearDisplay">-1</integer>
+
+ <!-- Physical display address that corresponds to the rear display in rear display mode
+ and concurrent display mode. Used to get information about the display before
+ entering the corresponding modes -->
+ <string name="config_rearDisplayPhysicalAddress" translatable="false"></string>
+
<!-- List of certificate to be used for font fs-verity integrity verification -->
<string-array translatable="false" name="config_fontManagerServiceCerts">
</string-array>
@@ -6210,4 +6268,30 @@
trusted certificate using the SHA-256 digest algorithm. -->
<string-array name="config_healthConnectMigrationKnownSigners">
</string-array>
+
+ <!-- The Universal Stylus Initiative (USI) protocol version supported by each display.
+ (@see https://universalstylus.org/).
+
+ The i-th value in this array corresponds to the supported USI version of the i-th display
+ listed in config_displayUniqueIdArray. On a single-display device, the
+ config_displayUniqueIdArray may be empty, in which case the only value in this array should
+ be the USI version for the main built-in display.
+
+ If the display does not support USI, the version value should be an empty string. If the
+ display supports USI, the version must be in the following format:
+ "<major-version>.<minor-version>"
+
+ For example, "", "1.0", and "2.0" are valid values. -->
+ <string-array name="config_displayUsiVersionArray" translatable="false">
+ <item>""</item>
+ </string-array>
+
+ <!-- Whether system apps should be scanned in the stopped state during initial boot.
+ Packages can be added by OEMs in an allowlist, to prevent them from being scanned as
+ "stopped" during initial boot of a device, or after an OTA update. Stopped state of
+ an app is not changed during subsequent reboots. -->
+ <bool name="config_stopSystemPackagesByDefault">false</bool>
+
+ <!-- Whether to show weather on the lock screen by default. -->
+ <bool name="config_lockscreenWeatherEnabledByDefault">false</bool>
</resources>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index e330ee3..da44c2e 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5067,6 +5067,13 @@
<string name="managed_profile_label_badge_2">2nd Work <xliff:g id="label" example="Email">%1$s</xliff:g></string>
<string name="managed_profile_label_badge_3">3rd Work <xliff:g id="label" example="Email">%1$s</xliff:g></string>
+ <!--
+ Used to wrap a label for content description for a Clone profile, e.g. "Clone Messenger"
+ instead of Messenger when the Messenger app is cloned.
+ [CHAR LIMIT=20]
+ -->
+ <string name="clone_profile_label_badge">Clone <xliff:g id="label" example="Messenger">%1$s</xliff:g></string>
+
<!-- DO NOT TRANSLATE -->
<string name="time_placeholder">--</string>
@@ -6234,4 +6241,17 @@
<string name="mic_access_on_toast">Microphone is available</string>
<!-- Toast message that is shown when the user mutes the microphone from the keyboard. [CHAR LIMIT=TOAST] -->
<string name="mic_access_off_toast">Microphone is blocked</string>
+
+ <!-- Name of concurrent display notifications. [CHAR LIMIT=NONE] -->
+ <string name="concurrent_display_notification_name">Dual screen</string>
+ <!-- Title of concurrent display active notification. [CHAR LIMIT=NONE] -->
+ <string name="concurrent_display_notification_active_title">Dual screen is on</string>
+ <!-- Content of concurrent display active notification. [CHAR LIMIT=NONE] -->
+ <string name="concurrent_display_notification_active_content"><xliff:g id="app_name" example="Camera app">%1$s</xliff:g> is using both displays to show content</string>
+ <!-- Title of concurrent display thermal notification. [CHAR LIMIT=NONE] -->
+ <string name="concurrent_display_notification_thermal_title">Device is too warm</string>
+ <!-- Content of concurrent display thermal notification. [CHAR LIMIT=NONE] -->
+ <string name="concurrent_display_notification_thermal_content">Dual Screen is unavailable because your phone is getting too warm</string>
+ <!-- Text of device state notification turn off button. [CHAR LIMIT=NONE] -->
+ <string name="device_state_notification_turn_off_button">Turn off</string>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 76af814..aeb46cc 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -370,6 +370,7 @@
<java-symbol type="string" name="config_controlsPackage" />
<java-symbol type="string" name="config_screenRecorderComponent" />
<java-symbol type="string" name="config_somnambulatorComponent" />
+ <java-symbol type="string" name="config_screenshotAppClipsServiceComponent" />
<java-symbol type="string" name="config_screenshotServiceComponent" />
<java-symbol type="string" name="config_screenshotErrorReceiverComponent" />
<java-symbol type="string" name="config_slicePermissionComponent" />
@@ -473,6 +474,7 @@
<java-symbol type="integer" name="config_multiuserMaxRunningUsers" />
<java-symbol type="bool" name="config_multiuserDelayUserDataLocking" />
<java-symbol type="bool" name="config_multiuserVisibleBackgroundUsers" />
+ <java-symbol type="bool" name="config_multiuserVisibleBackgroundUsersOnDefaultDisplay" />
<java-symbol type="bool" name="config_enableTimeoutToDockUserWhenDocked" />
<java-symbol type="integer" name="config_userTypePackageWhitelistMode"/>
<java-symbol type="xml" name="config_user_types" />
@@ -514,6 +516,8 @@
<java-symbol type="dimen" name="config_ambiguousGestureMultiplier" />
<java-symbol type="dimen" name="config_viewMinFlingVelocity" />
<java-symbol type="dimen" name="config_viewMaxFlingVelocity" />
+ <java-symbol type="dimen" name="config_viewMinRotaryEncoderFlingVelocity" />
+ <java-symbol type="dimen" name="config_viewMaxRotaryEncoderFlingVelocity" />
<java-symbol type="dimen" name="config_scrollbarSize" />
<java-symbol type="dimen" name="config_horizontalScrollFactor" />
<java-symbol type="dimen" name="config_verticalScrollFactor" />
@@ -1063,6 +1067,7 @@
<java-symbol type="string" name="managed_profile_label_badge" />
<java-symbol type="string" name="managed_profile_label_badge_2" />
<java-symbol type="string" name="managed_profile_label_badge_3" />
+ <java-symbol type="string" name="clone_profile_label_badge" />
<java-symbol type="string" name="mediasize_unknown_portrait" />
<java-symbol type="string" name="mediasize_unknown_landscape" />
<java-symbol type="string" name="mediasize_iso_a0" />
@@ -1386,6 +1391,8 @@
<java-symbol type="drawable" name="ic_qs_auto_rotate" />
<java-symbol type="drawable" name="ic_qs_dnd" />
<java-symbol type="drawable" name="ic_qs_one_handed_mode" />
+ <java-symbol type="drawable" name="ic_clone_icon_badge" />
+ <java-symbol type="drawable" name="ic_clone_badge" />
<java-symbol type="drawable" name="sim_light_blue" />
<java-symbol type="drawable" name="sim_light_green" />
@@ -1434,6 +1441,8 @@
<java-symbol type="drawable" name="ic_feedback_downrank" />
<java-symbol type="drawable" name="ic_account_circle" />
+ <java-symbol type="drawable" name="ic_dual_screen" />
+ <java-symbol type="drawable" name="ic_thermostat" />
<java-symbol type="color" name="user_icon_1" />
<java-symbol type="color" name="user_icon_2" />
<java-symbol type="color" name="user_icon_3" />
@@ -2494,6 +2503,8 @@
<java-symbol type="string" name="zen_mode_default_weekends_name" />
<java-symbol type="string" name="zen_mode_default_events_name" />
<java-symbol type="string" name="zen_mode_default_every_night_name" />
+ <java-symbol type="string" name="display_rotation_camera_compat_toast_after_rotation" />
+ <java-symbol type="string" name="display_rotation_camera_compat_toast_in_split_screen" />
<java-symbol type="array" name="config_system_condition_providers" />
<java-symbol type="string" name="muted_by" />
<java-symbol type="string" name="zen_mode_alarm" />
@@ -2646,6 +2657,8 @@
<java-symbol type="array" name="config_biometric_sensors" />
<java-symbol type="bool" name="allow_test_udfps" />
<java-symbol type="array" name="config_udfps_sensor_props" />
+ <java-symbol type="array" name="config_udfps_touch_detection_options" />
+ <java-symbol type="integer" name="config_selected_udfps_touch_detection" />
<java-symbol type="array" name="config_sfps_sensor_props" />
<java-symbol type="bool" name="config_is_powerbutton_fps" />
<java-symbol type="array" name="config_udfps_enroll_stage_thresholds" />
@@ -3391,6 +3404,13 @@
TODO(b/265312193): Remove this workaround when this bug is fixed.-->
<java-symbol type="array" name="config_deviceStatesToReverseDefaultDisplayRotationAroundZAxis" />
+ <!-- Boolean indicating whether secondary built-in displays should have their orientation
+ match the active default display. This config assumes that the secondary display only
+ requires swapping ROTATION_90 and ROTATION_270.
+ TODO(b/265991392): This should eventually be configured and parsed in
+ display_settings.xml -->
+ <java-symbol type="bool" name="config_matchSecondaryInternalDisplaysOrientationToReverseDefaultDisplay" />
+
<!-- Default user restrictions for the SYSTEM user -->
<java-symbol type="array" name="config_defaultFirstUserRestrictions" />
@@ -3406,7 +3426,6 @@
<java-symbol type="array" name="config_convert_to_emergency_number_map" />
- <java-symbol type="array" name="config_nonBlockableNotificationPackages" />
<java-symbol type="array" name="config_priorityOnlyDndExemptPackages" />
<!-- Screen-size-dependent modes for picker dialogs. -->
@@ -3700,6 +3719,7 @@
<java-symbol type="string" name="notification_channel_accessibility_magnification" />
<java-symbol type="string" name="notification_channel_accessibility_security_policy" />
<java-symbol type="string" name="config_defaultAutofillService" />
+ <java-symbol type="string" name="config_defaultFieldClassificationService" />
<java-symbol type="string" name="config_defaultOnDeviceSpeechRecognitionService" />
<java-symbol type="string" name="config_defaultTextClassifierPackage" />
<java-symbol type="string" name="config_defaultWellbeingPackage" />
@@ -4874,7 +4894,21 @@
<java-symbol type="array" name="config_deviceStatesAvailableForAppRequests" />
<java-symbol type="array" name="config_serviceStateLocationAllowedPackages" />
<java-symbol type="integer" name="config_deviceStateRearDisplay"/>
+ <java-symbol type="array" name="device_state_notification_state_identifiers"/>
+ <java-symbol type="array" name="device_state_notification_names"/>
+ <java-symbol type="array" name="device_state_notification_active_titles"/>
+ <java-symbol type="array" name="device_state_notification_active_contents"/>
+ <java-symbol type="array" name="device_state_notification_thermal_titles"/>
+ <java-symbol type="array" name="device_state_notification_thermal_contents"/>
+ <java-symbol type="string" name="concurrent_display_notification_name"/>
+ <java-symbol type="string" name="concurrent_display_notification_active_title"/>
+ <java-symbol type="string" name="concurrent_display_notification_active_content"/>
+ <java-symbol type="string" name="concurrent_display_notification_thermal_title"/>
+ <java-symbol type="string" name="concurrent_display_notification_thermal_content"/>
+ <java-symbol type="string" name="device_state_notification_turn_off_button"/>
<java-symbol type="bool" name="config_independentLockscreenLiveWallpaper"/>
+ <java-symbol type="integer" name="config_deviceStateConcurrentRearDisplay" />
+ <java-symbol type="string" name="config_rearDisplayPhysicalAddress" />
<!-- For app language picker -->
<java-symbol type="string" name="system_locale_title" />
@@ -4892,4 +4926,10 @@
<java-symbol type="string" name="config_mainDisplayShape"/>
<java-symbol type="string" name="config_secondaryDisplayShape"/>
<java-symbol type="array" name="config_displayShapeArray" />
+
+ <java-symbol type="bool" name="config_stopSystemPackagesByDefault"/>
+ <java-symbol type="string" name="config_wearServiceComponent" />
+
+ <!-- Whether to show weather on the lockscreen by default. -->
+ <java-symbol type="bool" name="config_lockscreenWeatherEnabledByDefault" />
</resources>
diff --git a/core/tests/benchmarks/src/android/os/ParcelableBenchmark.java b/core/tests/benchmarks/src/android/os/ParcelableBenchmark.java
index b1991c2..b3c7f88 100644
--- a/core/tests/benchmarks/src/android/os/ParcelableBenchmark.java
+++ b/core/tests/benchmarks/src/android/os/ParcelableBenchmark.java
@@ -60,8 +60,8 @@
public void timeReadWriteInsetsState(int reps) {
final InsetsState insetsState = new InsetsState();
- for (int i = 0; i < InsetsState.SIZE; i++) {
- insetsState.addSource(new InsetsSource(i, InsetsState.toPublicType(i)));
+ for (int i = 0; i < 10; i++) {
+ insetsState.addSource(new InsetsSource(i, 1 << i));
}
for (int i = 0; i < reps; i++) {
insetsState.writeToParcel(mParcel, 0);
diff --git a/core/tests/coretests/src/android/app/admin/PackagePolicyTest.java b/core/tests/coretests/src/android/app/admin/PackagePolicyTest.java
new file mode 100644
index 0000000..d8298fd
--- /dev/null
+++ b/core/tests/coretests/src/android/app/admin/PackagePolicyTest.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.admin;
+
+import static android.app.admin.PackagePolicy.PACKAGE_POLICY_ALLOWLIST;
+import static android.app.admin.PackagePolicy.PACKAGE_POLICY_ALLOWLIST_AND_SYSTEM;
+import static android.app.admin.PackagePolicy.PACKAGE_POLICY_BLOCKLIST;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
+import android.util.ArraySet;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class PackagePolicyTest {
+
+ private static final String TEST_PACKAGE_NAME = "com.example";
+
+ private static final String TEST_PACKAGE_NAME_2 = "com.example.2";
+
+ private static final String TEST_SYSTEM_PACKAGE_NAME = "com.system";
+
+
+ @Test
+ public void testParceling() {
+ final int policyType = PACKAGE_POLICY_BLOCKLIST;
+ final Set<String> packageNames = new ArraySet<>();
+ packageNames.add(TEST_PACKAGE_NAME);
+
+ final Parcel parcel = Parcel.obtain();
+ PackagePolicy packagePolicy = new PackagePolicy(policyType, packageNames);
+ try {
+ packagePolicy.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ packagePolicy = PackagePolicy.CREATOR.createFromParcel(parcel);
+ } finally {
+ parcel.recycle();
+ }
+
+ assertEquals(policyType, packagePolicy.getPolicyType());
+ assertNotNull(packagePolicy.getPackageNames());
+ assertEquals(1, packagePolicy.getPackageNames().size());
+ assertTrue(packagePolicy.getPackageNames().contains(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ public void testEmptyBlocklistCreation() {
+ PackagePolicy policy = new PackagePolicy(PACKAGE_POLICY_BLOCKLIST);
+ assertEquals(PACKAGE_POLICY_BLOCKLIST, policy.getPolicyType());
+ assertNotNull(policy.getPackageNames());
+ assertTrue(policy.getPackageNames().isEmpty());
+ }
+
+ @Test
+ public void testEmptyAllowlistCreation() {
+ PackagePolicy policy = new PackagePolicy(PACKAGE_POLICY_ALLOWLIST);
+ assertEquals(PACKAGE_POLICY_ALLOWLIST, policy.getPolicyType());
+ assertNotNull(policy.getPackageNames());
+ assertTrue(policy.getPackageNames().isEmpty());
+ }
+
+ @Test
+ public void testEmptyAllowlistAndSystemCreation() {
+ PackagePolicy policy = new PackagePolicy(PACKAGE_POLICY_ALLOWLIST_AND_SYSTEM);
+ assertEquals(PACKAGE_POLICY_ALLOWLIST_AND_SYSTEM, policy.getPolicyType());
+ assertNotNull(policy.getPackageNames());
+ assertTrue(policy.getPackageNames().isEmpty());
+ }
+
+ @Test
+ public void testSuppliedBlocklistCreation() {
+ final Set<String> packageNames = new ArraySet<>();
+ packageNames.add(TEST_PACKAGE_NAME);
+ PackagePolicy policy = new PackagePolicy(PACKAGE_POLICY_BLOCKLIST, packageNames);
+ assertEquals(PACKAGE_POLICY_BLOCKLIST, policy.getPolicyType());
+ assertNotNull(policy.getPackageNames());
+ assertEquals(1, policy.getPackageNames().size());
+ assertTrue(policy.getPackageNames().contains(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ public void testSuppliedAllowlistCreation() {
+ final Set<String> packageNames = new ArraySet<>();
+ packageNames.add(TEST_PACKAGE_NAME);
+ PackagePolicy policy = new PackagePolicy(PACKAGE_POLICY_ALLOWLIST, packageNames);
+ assertEquals(PACKAGE_POLICY_ALLOWLIST, policy.getPolicyType());
+ assertNotNull(policy.getPackageNames());
+ assertEquals(1, policy.getPackageNames().size());
+ assertTrue(policy.getPackageNames().contains(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ public void testSuppliedAllowlistAndSystemCreation() {
+ final Set<String> packageNames = new ArraySet<>();
+ packageNames.add(TEST_PACKAGE_NAME);
+ PackagePolicy policy = new PackagePolicy(PACKAGE_POLICY_ALLOWLIST_AND_SYSTEM, packageNames);
+ assertEquals(PACKAGE_POLICY_ALLOWLIST_AND_SYSTEM, policy.getPolicyType());
+ assertNotNull(policy.getPackageNames());
+ assertEquals(1, policy.getPackageNames().size());
+ assertTrue(policy.getPackageNames().contains(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ public void testBlocklist_isPackageAllowed() {
+ final Set<String> packageNames = new ArraySet<>();
+ packageNames.add(TEST_PACKAGE_NAME);
+ final Set<String> systemPackages = new ArraySet<>();
+ systemPackages.add(TEST_SYSTEM_PACKAGE_NAME);
+
+ PackagePolicy policy = new PackagePolicy(PACKAGE_POLICY_BLOCKLIST, packageNames);
+
+ assertFalse(policy.isPackageAllowed(TEST_PACKAGE_NAME, Collections.emptySet()));
+ assertFalse(policy.isPackageAllowed(TEST_PACKAGE_NAME, systemPackages));
+ assertTrue(policy.isPackageAllowed(TEST_PACKAGE_NAME_2, Collections.emptySet()));
+ assertTrue(policy.isPackageAllowed(TEST_PACKAGE_NAME_2, systemPackages));
+ assertTrue(policy.isPackageAllowed(TEST_SYSTEM_PACKAGE_NAME, Collections.emptySet()));
+ assertTrue(policy.isPackageAllowed(TEST_SYSTEM_PACKAGE_NAME, systemPackages));
+ }
+
+ @Test
+ public void testAllowlist_isPackageAllowed() {
+ final Set<String> packageNames = new ArraySet<>();
+ packageNames.add(TEST_PACKAGE_NAME);
+ final Set<String> systemPackages = new ArraySet<>();
+ systemPackages.add(TEST_SYSTEM_PACKAGE_NAME);
+ PackagePolicy policy = new PackagePolicy(PACKAGE_POLICY_ALLOWLIST, packageNames);
+
+ assertTrue(policy.isPackageAllowed(TEST_PACKAGE_NAME, Collections.emptySet()));
+ assertTrue(policy.isPackageAllowed(TEST_PACKAGE_NAME, systemPackages));
+ assertFalse(policy.isPackageAllowed(TEST_PACKAGE_NAME_2, Collections.emptySet()));
+ assertFalse(policy.isPackageAllowed(TEST_PACKAGE_NAME_2, systemPackages));
+ assertFalse(policy.isPackageAllowed(TEST_SYSTEM_PACKAGE_NAME, Collections.emptySet()));
+ assertFalse(policy.isPackageAllowed(TEST_SYSTEM_PACKAGE_NAME, systemPackages));
+ }
+
+ @Test
+ public void testAllowlistAndSystem_isPackageAllowed() {
+ final Set<String> packageNames = new ArraySet<>();
+ packageNames.add(TEST_PACKAGE_NAME);
+ final Set<String> systemPackages = new ArraySet<>();
+ systemPackages.add(TEST_SYSTEM_PACKAGE_NAME);
+ PackagePolicy policy = new PackagePolicy(PACKAGE_POLICY_ALLOWLIST_AND_SYSTEM, packageNames);
+
+ assertTrue(policy.isPackageAllowed(TEST_PACKAGE_NAME, Collections.emptySet()));
+ assertTrue(policy.isPackageAllowed(TEST_PACKAGE_NAME, systemPackages));
+ assertFalse(policy.isPackageAllowed(TEST_PACKAGE_NAME_2, Collections.emptySet()));
+ assertFalse(policy.isPackageAllowed(TEST_PACKAGE_NAME_2, systemPackages));
+ assertFalse(policy.isPackageAllowed(TEST_SYSTEM_PACKAGE_NAME, Collections.emptySet()));
+ assertTrue(policy.isPackageAllowed(TEST_SYSTEM_PACKAGE_NAME, systemPackages));
+ }
+
+}
diff --git a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
index 958fdc6..a3eda8d 100644
--- a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
@@ -17,7 +17,7 @@
package android.view;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.view.InsetsState.ITYPE_IME;
+import static android.view.InsetsSource.ID_IME;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
@@ -92,7 +92,7 @@
@Test
public void testImeVisibility() {
- final InsetsSourceControl ime = new InsetsSourceControl(ITYPE_IME, WindowInsets.Type.ime(),
+ final InsetsSourceControl ime = new InsetsSourceControl(ID_IME, WindowInsets.Type.ime(),
mLeash, false, new Point(), Insets.NONE);
mController.onControlsChanged(new InsetsSourceControl[] { ime });
@@ -121,7 +121,7 @@
mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
// set control and verify visibility is applied.
- InsetsSourceControl control = new InsetsSourceControl(ITYPE_IME,
+ InsetsSourceControl control = new InsetsSourceControl(ID_IME,
WindowInsets.Type.ime(), mLeash, false, new Point(), Insets.NONE);
mController.onControlsChanged(new InsetsSourceControl[] { control });
// IME show animation should be triggered when control becomes available.
@@ -161,7 +161,7 @@
}
// set control and verify visibility is applied.
- InsetsSourceControl control = Mockito.spy(new InsetsSourceControl(ITYPE_IME,
+ InsetsSourceControl control = Mockito.spy(new InsetsSourceControl(ID_IME,
WindowInsets.Type.ime(), mLeash, false, new Point(), Insets.NONE));
// Simulate IME source control set this flag when the target has starting window.
control.setSkipAnimationOnce(true);
diff --git a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
index cc5f7f8..1682135 100644
--- a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
+++ b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
@@ -16,8 +16,8 @@
package android.view;
-import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_STATUS_BAR;
+import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.statusBars;
import static android.view.WindowInsets.Type.systemBars;
import static org.junit.Assert.assertEquals;
@@ -66,6 +66,11 @@
@RunWith(AndroidJUnit4.class)
public class InsetsAnimationControlImplTest {
+ private static final int ID_STATUS_BAR = InsetsSource.createId(
+ null /* owner */, 0 /* index */, statusBars());
+ private static final int ID_NAVIGATION_BAR = InsetsSource.createId(
+ null /* owner */, 0 /* index */, navigationBars());
+
private InsetsAnimationControlImpl mController;
private SurfaceSession mSession = new SurfaceSession();
@@ -87,29 +92,31 @@
.setName("testSurface")
.build();
mInsetsState = new InsetsState();
- mInsetsState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 500, 100));
- mInsetsState.getSource(ITYPE_NAVIGATION_BAR).setFrame(new Rect(400, 0, 500, 500));
- InsetsSourceConsumer topConsumer = new InsetsSourceConsumer(ITYPE_STATUS_BAR,
+ mInsetsState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 500, 100));
+ mInsetsState.getOrCreateSource(ID_NAVIGATION_BAR, navigationBars())
+ .setFrame(new Rect(400, 0, 500, 500));
+ InsetsSourceConsumer topConsumer = new InsetsSourceConsumer(ID_STATUS_BAR,
WindowInsets.Type.statusBars(), mInsetsState,
() -> mMockTransaction, mMockController);
topConsumer.setControl(
- new InsetsSourceControl(ITYPE_STATUS_BAR, WindowInsets.Type.statusBars(),
+ new InsetsSourceControl(ID_STATUS_BAR, WindowInsets.Type.statusBars(),
mStatusLeash, true, new Point(0, 0), Insets.of(0, 100, 0, 0)),
new int[1], new int[1]);
- InsetsSourceConsumer navConsumer = new InsetsSourceConsumer(ITYPE_NAVIGATION_BAR,
+ InsetsSourceConsumer navConsumer = new InsetsSourceConsumer(ID_NAVIGATION_BAR,
WindowInsets.Type.navigationBars(), mInsetsState,
() -> mMockTransaction, mMockController);
navConsumer.setControl(
- new InsetsSourceControl(ITYPE_NAVIGATION_BAR, WindowInsets.Type.navigationBars(),
+ new InsetsSourceControl(ID_NAVIGATION_BAR, WindowInsets.Type.navigationBars(),
mNavLeash, true, new Point(400, 0), Insets.of(0, 0, 100, 0)),
new int[1], new int[1]);
mMockController.setRequestedVisibleTypes(0, WindowInsets.Type.navigationBars());
navConsumer.applyLocalVisibilityOverride();
SparseArray<InsetsSourceControl> controls = new SparseArray<>();
- controls.put(ITYPE_STATUS_BAR, topConsumer.getControl());
- controls.put(ITYPE_NAVIGATION_BAR, navConsumer.getControl());
+ controls.put(ID_STATUS_BAR, topConsumer.getControl());
+ controls.put(ID_NAVIGATION_BAR, navConsumer.getControl());
mController = new InsetsAnimationControlImpl(controls,
new Rect(0, 0, 500, 500), mInsetsState, mMockListener, systemBars(),
mMockController, 10 /* durationMs */, new LinearInterpolator(),
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index 5ec93e5..ca1367a 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -22,16 +22,13 @@
import static android.view.InsetsController.ANIMATION_TYPE_RESIZE;
import static android.view.InsetsController.ANIMATION_TYPE_SHOW;
import static android.view.InsetsController.AnimationType;
+import static android.view.InsetsSource.ID_IME;
import static android.view.InsetsSourceConsumer.ShowResult.IME_SHOW_DELAYED;
import static android.view.InsetsSourceConsumer.ShowResult.SHOW_IMMEDIATELY;
-import static android.view.InsetsState.FIRST_TYPE;
-import static android.view.InsetsState.ITYPE_CAPTION_BAR;
-import static android.view.InsetsState.ITYPE_IME;
-import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_STATUS_BAR;
-import static android.view.InsetsState.LAST_TYPE;
import static android.view.ViewRootImpl.CAPTION_ON_SHELL;
+import static android.view.WindowInsets.Type.SIZE;
import static android.view.WindowInsets.Type.all;
+import static android.view.WindowInsets.Type.captionBar;
import static android.view.WindowInsets.Type.defaultVisible;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.navigationBars;
@@ -64,7 +61,6 @@
import android.graphics.Rect;
import android.os.CancellationSignal;
import android.platform.test.annotations.Presubmit;
-import android.view.InsetsState.InternalInsetsType;
import android.view.SurfaceControl.Transaction;
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsController.OnControllableInsetsChangedListener;
@@ -101,6 +97,12 @@
@Presubmit
@RunWith(AndroidJUnit4.class)
public class InsetsControllerTest {
+
+ private static final int ID_STATUS_BAR = InsetsSource.createId(
+ null /* owner */, 0 /* index */, statusBars());
+ private static final int ID_NAVIGATION_BAR = InsetsSource.createId(
+ null /* owner */, 0 /* index */, navigationBars());
+
private InsetsSource mStatusSource;
private InsetsSource mNavSource;
private InsetsSource mImeSource;
@@ -153,11 +155,11 @@
}
}, mTestHandler);
final Rect rect = new Rect(5, 5, 5, 5);
- mStatusSource = new InsetsSource(ITYPE_STATUS_BAR, statusBars());
+ mStatusSource = new InsetsSource(ID_STATUS_BAR, statusBars());
mStatusSource.setFrame(new Rect(0, 0, 100, 10));
- mNavSource = new InsetsSource(ITYPE_NAVIGATION_BAR, navigationBars());
+ mNavSource = new InsetsSource(ID_NAVIGATION_BAR, navigationBars());
mNavSource.setFrame(new Rect(0, 90, 100, 100));
- mImeSource = new InsetsSource(ITYPE_IME, ime());
+ mImeSource = new InsetsSource(ID_IME, ime());
mImeSource.setFrame(new Rect(0, 0, 100, 10));
InsetsState state = new InsetsState();
state.addSource(mStatusSource);
@@ -179,7 +181,7 @@
@Test
public void testControlsChanged() {
- mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR, statusBars()));
+ mController.onControlsChanged(createSingletonControl(ID_STATUS_BAR, statusBars()));
assertNotNull(mController.getSourceConsumer(mStatusSource).getControl().getLeash());
mController.addOnControllableInsetsChangedListener(
((controller, typeMask) -> assertEquals(statusBars(), typeMask)));
@@ -190,7 +192,7 @@
OnControllableInsetsChangedListener listener
= mock(OnControllableInsetsChangedListener.class);
mController.addOnControllableInsetsChangedListener(listener);
- mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR, statusBars()));
+ mController.onControlsChanged(createSingletonControl(ID_STATUS_BAR, statusBars()));
mController.onControlsChanged(new InsetsSourceControl[0]);
assertNull(mController.getSourceConsumer(mStatusSource).getControl());
InOrder inOrder = Mockito.inOrder(listener);
@@ -202,7 +204,7 @@
@Test
public void testControlsRevoked_duringAnim() {
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR, statusBars()));
+ mController.onControlsChanged(createSingletonControl(ID_STATUS_BAR, statusBars()));
ArgumentCaptor<WindowInsetsAnimationController> animationController =
ArgumentCaptor.forClass(WindowInsetsAnimationController.class);
@@ -231,7 +233,7 @@
InsetsSourceControl control =
new InsetsSourceControl(
- ITYPE_STATUS_BAR, statusBars(), mLeash, true, new Point(),
+ ID_STATUS_BAR, statusBars(), mLeash, true, new Point(),
Insets.of(0, 10, 0, 0));
mController.onControlsChanged(new InsetsSourceControl[]{control});
mController.controlWindowInsetsAnimation(0, 0 /* durationMs */,
@@ -284,7 +286,7 @@
@Test
public void testApplyImeVisibility() {
- InsetsSourceControl ime = createControl(ITYPE_IME, ime());
+ InsetsSourceControl ime = createControl(ID_IME, ime());
mController.onControlsChanged(new InsetsSourceControl[] { ime });
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
mController.getSourceConsumer(mImeSource).onWindowFocusGained(true);
@@ -423,28 +425,28 @@
@Test
public void testRestoreStartsAnimation() {
- mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR, statusBars()));
+ mController.onControlsChanged(createSingletonControl(ID_STATUS_BAR, statusBars()));
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
mController.hide(statusBars());
mController.cancelExistingAnimations();
assertFalse(isRequestedVisible(mController, statusBars()));
- assertFalse(mController.getState().getSource(ITYPE_STATUS_BAR).isVisible());
+ assertFalse(mController.getState().peekSource(ID_STATUS_BAR).isVisible());
// Loosing control
InsetsState state = new InsetsState(mController.getState());
- state.setSourceVisible(ITYPE_STATUS_BAR, true);
+ state.setSourceVisible(ID_STATUS_BAR, true);
mController.onStateChanged(state);
mController.onControlsChanged(new InsetsSourceControl[0]);
assertFalse(isRequestedVisible(mController, statusBars()));
- assertTrue(mController.getState().getSource(ITYPE_STATUS_BAR).isVisible());
+ assertTrue(mController.getState().peekSource(ID_STATUS_BAR).isVisible());
// Gaining control
- mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR, statusBars()));
+ mController.onControlsChanged(createSingletonControl(ID_STATUS_BAR, statusBars()));
assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(statusBars()));
mController.cancelExistingAnimations();
assertFalse(isRequestedVisible(mController, statusBars()));
- assertFalse(mController.getState().getSource(ITYPE_STATUS_BAR).isVisible());
+ assertFalse(mController.getState().peekSource(ID_STATUS_BAR).isVisible());
});
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
@@ -455,18 +457,18 @@
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
mController.show(ime());
- assertFalse(mController.getState().getSource(ITYPE_IME).isVisible());
+ assertFalse(mController.getState().peekSource(ID_IME).isVisible());
// Pretend IME is calling
mController.show(ime(), true /* fromIme */, null /* statsToken */);
// Gaining control shortly after
- mController.onControlsChanged(createSingletonControl(ITYPE_IME, ime()));
+ mController.onControlsChanged(createSingletonControl(ID_IME, ime()));
assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ime()));
mController.cancelExistingAnimations();
assertTrue(isRequestedVisible(mController, ime()));
- assertTrue(mController.getState().getSource(ITYPE_IME).isVisible());
+ assertTrue(mController.getState().peekSource(ID_IME).isVisible());
});
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
@@ -476,10 +478,10 @@
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
mController.show(ime());
- assertFalse(mController.getState().getSource(ITYPE_IME).isVisible());
+ assertFalse(mController.getState().peekSource(ID_IME).isVisible());
// Gaining control shortly after
- mController.onControlsChanged(createSingletonControl(ITYPE_IME, ime()));
+ mController.onControlsChanged(createSingletonControl(ID_IME, ime()));
// Pretend IME is calling
mController.show(ime(), true /* fromIme */, null /* statsToken */);
@@ -487,14 +489,14 @@
assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ime()));
mController.cancelExistingAnimations();
assertTrue(isRequestedVisible(mController, ime()));
- assertTrue(mController.getState().getSource(ITYPE_IME).isVisible());
+ assertTrue(mController.getState().peekSource(ID_IME).isVisible());
});
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
@Test
public void testAnimationEndState_controller() throws Exception {
- mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR, statusBars()));
+ mController.onControlsChanged(createSingletonControl(ID_STATUS_BAR, statusBars()));
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
WindowInsetsAnimationControlListener mockListener =
@@ -520,7 +522,7 @@
@Test
public void testCancellation_afterGainingControl() throws Exception {
- mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR, statusBars()));
+ mController.onControlsChanged(createSingletonControl(ID_STATUS_BAR, statusBars()));
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
WindowInsetsAnimationControlListener mockListener =
@@ -641,61 +643,60 @@
public void testFrameUpdateDuringAnimation() {
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- mController.onControlsChanged(createSingletonControl(ITYPE_IME, ime()));
+ mController.onControlsChanged(createSingletonControl(ID_IME, ime()));
// Pretend IME is calling
mController.show(ime(), true /* fromIme */, null /* statsToken */);
InsetsState copy = new InsetsState(mController.getState(), true /* copySources */);
- copy.getSource(ITYPE_IME).setFrame(0, 1, 2, 3);
- copy.getSource(ITYPE_IME).setVisibleFrame(new Rect(4, 5, 6, 7));
+ copy.peekSource(ID_IME).setFrame(0, 1, 2, 3);
+ copy.peekSource(ID_IME).setVisibleFrame(new Rect(4, 5, 6, 7));
mController.onStateChanged(copy);
assertNotEquals(new Rect(0, 1, 2, 3),
- mController.getState().getSource(ITYPE_IME).getFrame());
+ mController.getState().peekSource(ID_IME).getFrame());
assertNotEquals(new Rect(4, 5, 6, 7),
- mController.getState().getSource(ITYPE_IME).getVisibleFrame());
+ mController.getState().peekSource(ID_IME).getVisibleFrame());
mController.cancelExistingAnimations();
assertEquals(new Rect(0, 1, 2, 3),
- mController.getState().getSource(ITYPE_IME).getFrame());
+ mController.getState().peekSource(ID_IME).getFrame());
assertEquals(new Rect(4, 5, 6, 7),
- mController.getState().getSource(ITYPE_IME).getVisibleFrame());
+ mController.getState().peekSource(ID_IME).getVisibleFrame());
});
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
@Test
public void testResizeAnimation_insetsTypes() {
- for (@InternalInsetsType int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
- final @AnimationType int expectedAnimationType =
- (InsetsState.toPublicType(type) & systemBars()) != 0
+ for (int i = 0; i < SIZE; i++) {
+ final @InsetsType int type = 1 << i;
+ final @AnimationType int expectedAnimationType = (type & systemBars()) != 0
? ANIMATION_TYPE_RESIZE
: ANIMATION_TYPE_NONE;
doTestResizeAnimation_insetsTypes(type, expectedAnimationType);
}
}
- private void doTestResizeAnimation_insetsTypes(@InternalInsetsType int type,
+ private void doTestResizeAnimation_insetsTypes(@InsetsType int type,
@AnimationType int expectedAnimationType) {
- final @InsetsType int publicType = InsetsState.toPublicType(type);
+ final int id = type;
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
final InsetsState state1 = new InsetsState();
- state1.getSource(type).setVisible(true);
- state1.getSource(type).setFrame(0, 0, 500, 50);
+ state1.getOrCreateSource(id, type).setVisible(true).setFrame(0, 0, 500, 50);
final InsetsState state2 = new InsetsState(state1, true /* copySources */);
- state2.getSource(type).setFrame(0, 0, 500, 60);
- final String message = "Animation type of " + InsetsState.typeToString(type) + ":";
+ state2.peekSource(id).setFrame(0, 0, 500, 60);
+ final String message = "Animation type of " + WindowInsets.Type.toString(type) + ":";
// New insets source won't cause the resize animation.
mController.onStateChanged(state1);
- assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(publicType));
+ assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
// Changing frame might cause the resize animation. This depends on the insets type.
mController.onStateChanged(state2);
- assertEquals(message, expectedAnimationType, mController.getAnimationType(publicType));
+ assertEquals(message, expectedAnimationType, mController.getAnimationType(type));
// Cancel the existing animations for the next iteration.
mController.cancelExistingAnimations();
- assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(publicType));
+ assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
});
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
@@ -703,23 +704,23 @@
@Test
public void testResizeAnimation_displayFrame() {
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- final @InternalInsetsType int type = ITYPE_STATUS_BAR;
- final @InsetsType int publicType = statusBars();
+ final int id = ID_STATUS_BAR;
+ final @InsetsType int type = statusBars();
final InsetsState state1 = new InsetsState();
state1.setDisplayFrame(new Rect(0, 0, 500, 1000));
- state1.getSource(type).setFrame(0, 0, 500, 50);
+ state1.getOrCreateSource(id, type).setFrame(0, 0, 500, 50);
final InsetsState state2 = new InsetsState(state1, true /* copySources */);
state2.setDisplayFrame(new Rect(0, 0, 500, 1010));
- state2.getSource(type).setFrame(0, 0, 500, 60);
+ state2.peekSource(id).setFrame(0, 0, 500, 60);
final String message = "There must not be resize animation.";
// New insets source won't cause the resize animation.
mController.onStateChanged(state1);
- assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(publicType));
+ assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
// Changing frame won't cause the resize animation if the display frame is also changed.
mController.onStateChanged(state2);
- assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(publicType));
+ assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
});
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
@@ -727,32 +728,29 @@
@Test
public void testResizeAnimation_visibility() {
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- final @InternalInsetsType int type = ITYPE_STATUS_BAR;
- final @InsetsType int publicType = statusBars();
+ final int id = ID_STATUS_BAR;
+ final @InsetsType int type = statusBars();
final InsetsState state1 = new InsetsState();
- state1.getSource(type).setVisible(true);
- state1.getSource(type).setFrame(0, 0, 500, 50);
+ state1.getOrCreateSource(id, type).setVisible(true).setFrame(0, 0, 500, 50);
final InsetsState state2 = new InsetsState(state1, true /* copySources */);
- state2.getSource(type).setVisible(false);
- state2.getSource(type).setFrame(0, 0, 500, 60);
+ state2.peekSource(id).setVisible(false).setFrame(0, 0, 500, 60);
final InsetsState state3 = new InsetsState(state2, true /* copySources */);
- state3.getSource(type).setVisible(true);
- state3.getSource(type).setFrame(0, 0, 500, 70);
+ state3.peekSource(id).setVisible(true).setFrame(0, 0, 500, 70);
final String message = "There must not be resize animation.";
// New insets source won't cause the resize animation.
mController.onStateChanged(state1);
- assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(publicType));
+ assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
// Changing source visibility (visible --> invisible) won't cause the resize animation.
// The previous source and the current one must be both visible.
mController.onStateChanged(state2);
- assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(publicType));
+ assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
// Changing source visibility (invisible --> visible) won't cause the resize animation.
// The previous source and the current one must be both visible.
mController.onStateChanged(state3);
- assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(publicType));
+ assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
});
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
@@ -765,22 +763,20 @@
return;
}
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- mController.onFrameChanged(new Rect(0, 0, 100, 300));
- final InsetsState state = new InsetsState(mController.getState(), true);
- final Rect captionFrame = new Rect(0, 0, 100, 100);
- mController.setCaptionInsetsHeight(100);
- mController.onStateChanged(state);
- final InsetsState currentState = new InsetsState(mController.getState());
- // The caption bar source should be synced with the info in mAttachInfo.
- assertEquals(captionFrame, currentState.peekSource(ITYPE_CAPTION_BAR).getFrame());
- assertTrue(currentState.equals(state, true /* excludingCaptionInsets*/,
- true /* excludeInvisibleIme */));
+ final Rect frame = new Rect(0, 0, 100, 300);
+ final int captionBarHeight = 100;
+ final InsetsState state = mController.getState();
+ mController.onFrameChanged(frame);
+ mController.setCaptionInsetsHeight(captionBarHeight);
+ // The caption bar insets height should be the same as the caption bar height.
+ assertEquals(captionBarHeight, state.calculateInsets(frame, captionBar(), false).top);
// Test update to remove the caption bar
mController.setCaptionInsetsHeight(0);
- mController.onStateChanged(state);
// The caption bar source should not be there at all, because we don't add empty
// caption to the state from the server.
- assertNull(mController.getState().peekSource(ITYPE_CAPTION_BAR));
+ for (int i = state.sourceSize() - 1; i >= 0; i--) {
+ assertNotEquals(captionBar(), state.sourceAt(i).getType());
+ }
});
}
@@ -792,7 +788,6 @@
return;
}
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- final InsetsState state = new InsetsState(mController.getState(), true);
reset(mTestHost);
mController.setCaptionInsetsHeight(100);
verify(mTestHost).notifyInsetsChanged();
@@ -881,28 +876,28 @@
// Changing status bar frame should cause notifyInsetsChanged.
clearInvocations(mTestHost);
InsetsState newState = new InsetsState(localState, true /* copySources */);
- newState.getSource(ITYPE_STATUS_BAR).getFrame().bottom++;
+ newState.peekSource(ID_STATUS_BAR).getFrame().bottom++;
mController.onStateChanged(newState);
verify(mTestHost, times(1)).notifyInsetsChanged();
// Changing status bar visibility should cause notifyInsetsChanged.
clearInvocations(mTestHost);
newState = new InsetsState(localState, true /* copySources */);
- newState.getSource(ITYPE_STATUS_BAR).setVisible(false);
+ newState.peekSource(ID_STATUS_BAR).setVisible(false);
mController.onStateChanged(newState);
verify(mTestHost, times(1)).notifyInsetsChanged();
// Changing invisible IME frame should not cause notifyInsetsChanged.
clearInvocations(mTestHost);
newState = new InsetsState(localState, true /* copySources */);
- newState.getSource(ITYPE_IME).getFrame().top--;
+ newState.peekSource(ID_IME).getFrame().top--;
mController.onStateChanged(newState);
verify(mTestHost, never()).notifyInsetsChanged();
// Changing IME visibility should cause notifyInsetsChanged.
clearInvocations(mTestHost);
newState = new InsetsState(localState, true /* copySources */);
- newState.getSource(ITYPE_IME).setVisible(true);
+ newState.peekSource(ID_IME).setVisible(true);
mController.onStateChanged(newState);
verify(mTestHost, times(1)).notifyInsetsChanged();
});
@@ -947,9 +942,9 @@
}
private InsetsSourceControl[] prepareControls() {
- final InsetsSourceControl navBar = createControl(ITYPE_NAVIGATION_BAR, navigationBars());
- final InsetsSourceControl statusBar = createControl(ITYPE_STATUS_BAR, statusBars());
- final InsetsSourceControl ime = createControl(ITYPE_IME, ime());
+ final InsetsSourceControl navBar = createControl(ID_NAVIGATION_BAR, navigationBars());
+ final InsetsSourceControl statusBar = createControl(ID_STATUS_BAR, statusBars());
+ final InsetsSourceControl ime = createControl(ID_IME, ime());
InsetsSourceControl[] controls = new InsetsSourceControl[3];
controls[0] = navBar;
diff --git a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
index 0486e3c..988e690 100644
--- a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
@@ -18,9 +18,8 @@
import static android.view.InsetsController.ANIMATION_TYPE_NONE;
import static android.view.InsetsController.ANIMATION_TYPE_USER;
+import static android.view.InsetsSource.ID_IME;
import static android.view.InsetsSourceConsumer.ShowResult.SHOW_IMMEDIATELY;
-import static android.view.InsetsState.ITYPE_IME;
-import static android.view.InsetsState.ITYPE_STATUS_BAR;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.statusBars;
@@ -69,6 +68,9 @@
@RunWith(AndroidJUnit4.class)
public class InsetsSourceConsumerTest {
+ private static final int ID_STATUS_BAR = InsetsSource.createId(
+ null /* owner */, 0 /* index */, statusBars());
+
private InsetsSourceConsumer mConsumer;
private SurfaceSession mSession = new SurfaceSession();
@@ -97,11 +99,11 @@
// activity isn't running, lets ignore BadTokenException.
}
mState = new InsetsState();
- mSpyInsetsSource = Mockito.spy(new InsetsSource(ITYPE_STATUS_BAR, statusBars()));
+ mSpyInsetsSource = Mockito.spy(new InsetsSource(ID_STATUS_BAR, statusBars()));
mState.addSource(mSpyInsetsSource);
mController = new InsetsController(new ViewRootInsetsControllerHost(mViewRoot));
- mConsumer = new InsetsSourceConsumer(ITYPE_STATUS_BAR, statusBars(), mState,
+ mConsumer = new InsetsSourceConsumer(ID_STATUS_BAR, statusBars(), mState,
() -> mMockTransaction, mController) {
@Override
public void removeSurface() {
@@ -113,7 +115,7 @@
instrumentation.waitForIdleSync();
mConsumer.setControl(
- new InsetsSourceControl(ITYPE_STATUS_BAR, statusBars(), mLeash,
+ new InsetsSourceControl(ID_STATUS_BAR, statusBars(), mLeash,
true /* initialVisible */, new Point(), Insets.NONE),
new int[1], new int[1]);
}
@@ -147,25 +149,25 @@
InsetsController controller = new InsetsController(new ViewRootInsetsControllerHost(
mViewRoot));
InsetsSourceConsumer consumer = new InsetsSourceConsumer(
- ITYPE_IME, ime(), state, null, controller);
+ ID_IME, ime(), state, null, controller);
- InsetsSource source = new InsetsSource(ITYPE_IME, ime());
+ InsetsSource source = new InsetsSource(ID_IME, ime());
source.setFrame(0, 1, 2, 3);
consumer.updateSource(new InsetsSource(source), ANIMATION_TYPE_NONE);
// While we're animating, updates are delayed
source.setFrame(4, 5, 6, 7);
consumer.updateSource(new InsetsSource(source), ANIMATION_TYPE_USER);
- assertEquals(new Rect(0, 1, 2, 3), state.peekSource(ITYPE_IME).getFrame());
+ assertEquals(new Rect(0, 1, 2, 3), state.peekSource(ID_IME).getFrame());
// Finish the animation, now the pending frame should be applied
assertTrue(consumer.onAnimationStateChanged(false /* running */));
- assertEquals(new Rect(4, 5, 6, 7), state.peekSource(ITYPE_IME).getFrame());
+ assertEquals(new Rect(4, 5, 6, 7), state.peekSource(ID_IME).getFrame());
// Animating again, updates are delayed
source.setFrame(8, 9, 10, 11);
consumer.updateSource(new InsetsSource(source), ANIMATION_TYPE_USER);
- assertEquals(new Rect(4, 5, 6, 7), state.peekSource(ITYPE_IME).getFrame());
+ assertEquals(new Rect(4, 5, 6, 7), state.peekSource(ID_IME).getFrame());
// Updating with the current frame triggers a different code path, verify this clears
// the pending 8, 9, 10, 11 frame:
@@ -173,7 +175,7 @@
consumer.updateSource(new InsetsSource(source), ANIMATION_TYPE_USER);
assertFalse(consumer.onAnimationStateChanged(false /* running */));
- assertEquals(new Rect(4, 5, 6, 7), state.peekSource(ITYPE_IME).getFrame());
+ assertEquals(new Rect(4, 5, 6, 7), state.peekSource(ID_IME).getFrame());
}
@Test
@@ -185,7 +187,7 @@
verifyZeroInteractions(mMockTransaction);
int[] hideTypes = new int[1];
mConsumer.setControl(
- new InsetsSourceControl(ITYPE_STATUS_BAR, statusBars(), mLeash,
+ new InsetsSourceControl(ID_STATUS_BAR, statusBars(), mLeash,
true /* initialVisible */, new Point(), Insets.NONE),
new int[1], hideTypes);
assertEquals(statusBars(), hideTypes[0]);
@@ -203,7 +205,7 @@
mRemoveSurfaceCalled = false;
int[] hideTypes = new int[1];
mConsumer.setControl(
- new InsetsSourceControl(ITYPE_STATUS_BAR, statusBars(), mLeash,
+ new InsetsSourceControl(ID_STATUS_BAR, statusBars(), mLeash,
false /* initialVisible */, new Point(), Insets.NONE),
new int[1], hideTypes);
assertTrue(mRemoveSurfaceCalled);
@@ -219,7 +221,7 @@
ViewRootInsetsControllerHost host = new ViewRootInsetsControllerHost(mViewRoot);
InsetsController insetsController = new InsetsController(host, (controller, source) -> {
if (source.getType() == ime()) {
- return new InsetsSourceConsumer(ITYPE_IME, ime(), state,
+ return new InsetsSourceConsumer(ID_IME, ime(), state,
() -> mMockTransaction, controller) {
@Override
public int requestShow(boolean fromController,
@@ -231,11 +233,11 @@
return new InsetsSourceConsumer(source.getId(), source.getType(),
controller.getState(), Transaction::new, controller);
}, host.getHandler());
- InsetsSource imeSource = new InsetsSource(ITYPE_IME, ime());
+ InsetsSource imeSource = new InsetsSource(ID_IME, ime());
InsetsSourceConsumer imeConsumer = insetsController.getSourceConsumer(imeSource);
// Initial IME insets source control with its leash.
- imeConsumer.setControl(new InsetsSourceControl(ITYPE_IME, ime(), mLeash,
+ imeConsumer.setControl(new InsetsSourceControl(ID_IME, ime(), mLeash,
false /* initialVisible */, new Point(), Insets.NONE), new int[1], new int[1]);
reset(mMockTransaction);
@@ -244,7 +246,7 @@
insetsController.controlWindowInsetsAnimation(ime(), 0L,
null /* interpolator */, null /* cancellationSignal */, null /* listener */);
assertEquals(ANIMATION_TYPE_USER, insetsController.getAnimationType(ime()));
- imeConsumer.setControl(new InsetsSourceControl(ITYPE_IME, ime(), mLeash,
+ imeConsumer.setControl(new InsetsSourceControl(ID_IME, ime(), mLeash,
true /* initialVisible */, new Point(), Insets.NONE), new int[1], new int[1]);
verify(mMockTransaction, never()).show(mLeash);
});
diff --git a/core/tests/coretests/src/android/view/InsetsSourceTest.java b/core/tests/coretests/src/android/view/InsetsSourceTest.java
index e01440c..6fa8f11 100644
--- a/core/tests/coretests/src/android/view/InsetsSourceTest.java
+++ b/core/tests/coretests/src/android/view/InsetsSourceTest.java
@@ -16,15 +16,20 @@
package android.view;
+import static android.view.WindowInsets.Type.FIRST;
+import static android.view.WindowInsets.Type.LAST;
+import static android.view.WindowInsets.Type.SIZE;
import static android.view.WindowInsets.Type.captionBar;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.navigationBars;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
import android.graphics.Insets;
import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
+import android.util.SparseArray;
import androidx.test.runner.AndroidJUnit4;
@@ -206,6 +211,21 @@
assertEquals(Insets.of(0, 0, 0, 0), insets);
}
+ @Test
+ public void testCreateId() {
+ final int numSourcePerType = 2048;
+ final int numTotalSources = SIZE * numSourcePerType;
+ final SparseArray<InsetsSource> sources = new SparseArray<>(numTotalSources);
+ final Object owner = new Object();
+ for (int index = 0; index < numSourcePerType; index++) {
+ for (int type = FIRST; type <= LAST; type = type << 1) {
+ final int id = InsetsSource.createId(owner, index, type);
+ assertNull("Must not create the same ID.", sources.get(id));
+ sources.append(id, new InsetsSource(id, type));
+ }
+ }
+ assertEquals(numTotalSources, sources.size());
+ }
// Parcel and equals already tested via InsetsStateTest
}
diff --git a/core/tests/coretests/src/android/view/InsetsStateTest.java b/core/tests/coretests/src/android/view/InsetsStateTest.java
index 6a96f28..fde1a6d 100644
--- a/core/tests/coretests/src/android/view/InsetsStateTest.java
+++ b/core/tests/coretests/src/android/view/InsetsStateTest.java
@@ -18,24 +18,20 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.view.InsetsSource.ID_IME;
import static android.view.InsetsState.ISIDE_BOTTOM;
import static android.view.InsetsState.ISIDE_TOP;
-import static android.view.InsetsState.ITYPE_BOTTOM_GESTURES;
-import static android.view.InsetsState.ITYPE_CAPTION_BAR;
-import static android.view.InsetsState.ITYPE_CLIMATE_BAR;
-import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_IME;
-import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_STATUS_BAR;
import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT;
import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT;
import static android.view.RoundedCorner.POSITION_TOP_LEFT;
import static android.view.RoundedCorner.POSITION_TOP_RIGHT;
import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
+import static android.view.WindowInsets.Type.captionBar;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.navigationBars;
import static android.view.WindowInsets.Type.statusBars;
import static android.view.WindowInsets.Type.systemBars;
+import static android.view.WindowInsets.Type.systemGestures;
import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
@@ -47,8 +43,10 @@
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import android.graphics.Insets;
@@ -76,33 +74,50 @@
@RunWith(AndroidJUnit4.class)
public class InsetsStateTest {
- private InsetsState mState = new InsetsState();
- private InsetsState mState2 = new InsetsState();
+ private static final int ID_STATUS_BAR = InsetsSource.createId(
+ null /* owner */, 0 /* index */, statusBars());
+ private static final int ID_NAVIGATION_BAR = InsetsSource.createId(
+ null /* owner */, 0 /* index */, navigationBars());
+ private static final int ID_CAPTION_BAR = InsetsSource.createId(
+ null /* owner */, 0 /* index */, captionBar());
+ private static final int ID_CLIMATE_BAR = InsetsSource.createId(
+ null /* owner */, 1 /* index */, statusBars());
+ private static final int ID_EXTRA_NAVIGATION_BAR = InsetsSource.createId(
+ null /* owner */, 1 /* index */, navigationBars());
+ private static final int ID_BOTTOM_GESTURES = InsetsSource.createId(
+ null /* owner */, 0 /* index */, systemGestures());
+
+ private final InsetsState mState = new InsetsState();
+ private final InsetsState mState2 = new InsetsState();
@Test
public void testCalculateInsets() {
- mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_STATUS_BAR).setVisible(true);
- mState.getSource(ITYPE_IME).setFrame(new Rect(0, 200, 100, 300));
- mState.getSource(ITYPE_IME).setVisible(true);
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setVisible(true);
+ mState.getOrCreateSource(ID_IME, ime())
+ .setFrame(new Rect(0, 200, 100, 300))
+ .setVisible(true);
SparseIntArray typeSideMap = new SparseIntArray();
WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), null, false,
false, SOFT_INPUT_ADJUST_RESIZE, 0, 0, TYPE_APPLICATION, WINDOWING_MODE_UNDEFINED,
typeSideMap);
assertEquals(Insets.of(0, 100, 0, 100), insets.getSystemWindowInsets());
assertEquals(Insets.of(0, 100, 0, 100), insets.getInsets(Type.all()));
- assertEquals(ISIDE_TOP, typeSideMap.get(ITYPE_STATUS_BAR));
- assertEquals(ISIDE_BOTTOM, typeSideMap.get(ITYPE_IME));
+ assertEquals(ISIDE_TOP, typeSideMap.get(ID_STATUS_BAR));
+ assertEquals(ISIDE_BOTTOM, typeSideMap.get(ID_IME));
assertEquals(Insets.of(0, 100, 0, 0), insets.getInsets(statusBars()));
assertEquals(Insets.of(0, 0, 0, 100), insets.getInsets(ime()));
}
@Test
public void testCalculateInsets_imeAndNav() {
- mState.getSource(ITYPE_NAVIGATION_BAR).setFrame(new Rect(0, 200, 100, 300));
- mState.getSource(ITYPE_NAVIGATION_BAR).setVisible(true);
- mState.getSource(ITYPE_IME).setFrame(new Rect(0, 100, 100, 300));
- mState.getSource(ITYPE_IME).setVisible(true);
+ mState.getOrCreateSource(ID_NAVIGATION_BAR, navigationBars())
+ .setFrame(new Rect(0, 200, 100, 300))
+ .setVisible(true);
+ mState.getOrCreateSource(ID_IME, ime())
+ .setFrame(new Rect(0, 100, 100, 300))
+ .setVisible(true);
WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), null, false,
false, SOFT_INPUT_ADJUST_RESIZE, 0, 0, TYPE_APPLICATION, WINDOWING_MODE_UNDEFINED,
null);
@@ -116,10 +131,12 @@
@Test
public void testCalculateInsets_navRightStatusTop() {
- mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_STATUS_BAR).setVisible(true);
- mState.getSource(ITYPE_NAVIGATION_BAR).setFrame(new Rect(80, 0, 100, 300));
- mState.getSource(ITYPE_NAVIGATION_BAR).setVisible(true);
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setVisible(true);
+ mState.getOrCreateSource(ID_NAVIGATION_BAR, navigationBars())
+ .setFrame(new Rect(80, 0, 100, 300))
+ .setVisible(true);
WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), null, false,
false, 0, 0, 0, TYPE_APPLICATION, WINDOWING_MODE_UNDEFINED, null);
assertEquals(Insets.of(0, 100, 20, 0), insets.getSystemWindowInsets());
@@ -129,14 +146,14 @@
@Test
public void testCalculateInsets_extraNavRightClimateTop() throws Exception {
- mState.getSource(ITYPE_CLIMATE_BAR).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_CLIMATE_BAR).setVisible(true);
- mState.getSource(ITYPE_EXTRA_NAVIGATION_BAR).setFrame(new Rect(80, 0, 100, 300));
- mState.getSource(ITYPE_EXTRA_NAVIGATION_BAR).setVisible(true);
+ mState.getOrCreateSource(ID_CLIMATE_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setVisible(true);
+ mState.getOrCreateSource(ID_EXTRA_NAVIGATION_BAR, navigationBars())
+ .setFrame(new Rect(80, 0, 100, 300))
+ .setVisible(true);
WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), null, false,
false, 0, 0, 0, TYPE_APPLICATION, WINDOWING_MODE_UNDEFINED, null);
- // ITYPE_CLIMATE_BAR is a type of status bar and ITYPE_EXTRA_NAVIGATION_BAR is a type
- // of navigation bar.
assertEquals(Insets.of(0, 100, 20, 0), insets.getSystemWindowInsets());
assertEquals(Insets.of(0, 100, 0, 0), insets.getInsets(Type.statusBars()));
assertEquals(Insets.of(0, 0, 20, 0), insets.getInsets(Type.navigationBars()));
@@ -144,10 +161,12 @@
@Test
public void testCalculateInsets_imeIgnoredWithoutAdjustResize() {
- mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_STATUS_BAR).setVisible(true);
- mState.getSource(ITYPE_IME).setFrame(new Rect(0, 200, 100, 300));
- mState.getSource(ITYPE_IME).setVisible(true);
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setVisible(true);
+ mState.getOrCreateSource(ID_IME, ime())
+ .setFrame(new Rect(0, 200, 100, 300))
+ .setVisible(true);
WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), null, false,
false, SOFT_INPUT_ADJUST_NOTHING, 0, 0, TYPE_APPLICATION, WINDOWING_MODE_UNDEFINED,
null);
@@ -158,10 +177,12 @@
@Test
public void testCalculateInsets_systemUiFlagLayoutStable() {
- mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_STATUS_BAR).setVisible(false);
- mState.getSource(ITYPE_IME).setFrame(new Rect(0, 200, 100, 300));
- mState.getSource(ITYPE_IME).setVisible(true);
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setVisible(false);
+ mState.getOrCreateSource(ID_IME, ime())
+ .setFrame(new Rect(0, 200, 100, 300))
+ .setVisible(true);
WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), null, false,
false, SOFT_INPUT_ADJUST_NOTHING, 0, SYSTEM_UI_FLAG_LAYOUT_STABLE, TYPE_APPLICATION,
WINDOWING_MODE_UNDEFINED, null);
@@ -174,8 +195,9 @@
@Test
public void testCalculateInsets_systemUiFlagLayoutStable_windowFlagFullscreen() {
- mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_STATUS_BAR).setVisible(false);
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setVisible(false);
WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), null, false,
false, SOFT_INPUT_ADJUST_NOTHING, FLAG_FULLSCREEN, SYSTEM_UI_FLAG_LAYOUT_STABLE,
TYPE_APPLICATION, WINDOWING_MODE_UNDEFINED, null);
@@ -188,8 +210,9 @@
@Test
public void testCalculateInsets_flagLayoutNoLimits() {
- mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_STATUS_BAR).setVisible(true);
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setVisible(true);
WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), null, false,
false, SOFT_INPUT_ADJUST_NOTHING, FLAG_LAYOUT_NO_LIMITS,
0 /* legacySystemUiFlags */, TYPE_APPLICATION, WINDOWING_MODE_UNDEFINED, null);
@@ -211,10 +234,12 @@
@Test
public void testCalculateInsets_captionStatusBarOverlap() {
- mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_STATUS_BAR).setVisible(true);
- mState.getSource(ITYPE_CAPTION_BAR).setFrame(new Rect(0, 0, 100, 300));
- mState.getSource(ITYPE_CAPTION_BAR).setVisible(true);
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setVisible(true);
+ mState.getOrCreateSource(ID_CAPTION_BAR, captionBar())
+ .setFrame(new Rect(0, 0, 100, 300))
+ .setVisible(true);
Insets visibleInsets = mState.calculateVisibleInsets(
new Rect(0, 0, 100, 400), TYPE_APPLICATION, WINDOWING_MODE_UNDEFINED,
@@ -224,8 +249,9 @@
@Test
public void testCalculateInsets_captionBarOffset() {
- mState.getSource(ITYPE_CAPTION_BAR).setFrame(new Rect(0, 0, 100, 300));
- mState.getSource(ITYPE_CAPTION_BAR).setVisible(true);
+ mState.getOrCreateSource(ID_CAPTION_BAR, captionBar())
+ .setFrame(new Rect(0, 0, 100, 300))
+ .setVisible(true);
Insets visibleInsets = mState.calculateVisibleInsets(
new Rect(0, 0, 150, 400), TYPE_APPLICATION, WINDOWING_MODE_UNDEFINED,
@@ -235,10 +261,12 @@
@Test
public void testCalculateInsets_extraNavRightStatusTop() {
- mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_STATUS_BAR).setVisible(true);
- mState.getSource(ITYPE_EXTRA_NAVIGATION_BAR).setFrame(new Rect(80, 0, 100, 300));
- mState.getSource(ITYPE_EXTRA_NAVIGATION_BAR).setVisible(true);
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setVisible(true);
+ mState.getOrCreateSource(ID_EXTRA_NAVIGATION_BAR, navigationBars())
+ .setFrame(new Rect(80, 0, 100, 300))
+ .setVisible(true);
WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), null, false,
false, 0, 0, 0, TYPE_APPLICATION, WINDOWING_MODE_UNDEFINED, null);
assertEquals(Insets.of(0, 100, 20, 0), insets.getSystemWindowInsets());
@@ -248,10 +276,12 @@
@Test
public void testCalculateInsets_navigationRightClimateTop() {
- mState.getSource(ITYPE_CLIMATE_BAR).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_CLIMATE_BAR).setVisible(true);
- mState.getSource(ITYPE_NAVIGATION_BAR).setFrame(new Rect(80, 0, 100, 300));
- mState.getSource(ITYPE_NAVIGATION_BAR).setVisible(true);
+ mState.getOrCreateSource(ID_CLIMATE_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setVisible(true);
+ mState.getOrCreateSource(ID_NAVIGATION_BAR, navigationBars())
+ .setFrame(new Rect(80, 0, 100, 300))
+ .setVisible(true);
WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), null, false,
false, 0, 0, 0, TYPE_APPLICATION, WINDOWING_MODE_UNDEFINED, null);
assertEquals(Insets.of(0, 100, 20, 0), insets.getSystemWindowInsets());
@@ -261,11 +291,13 @@
@Test
public void testStripForDispatch() {
- mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_STATUS_BAR).setVisible(true);
- mState.getSource(ITYPE_IME).setFrame(new Rect(0, 200, 100, 300));
- mState.getSource(ITYPE_IME).setVisible(true);
- mState.removeSource(ITYPE_IME);
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setVisible(true);
+ mState.getOrCreateSource(ID_IME, ime())
+ .setFrame(new Rect(0, 200, 100, 300))
+ .setVisible(true);
+ mState.removeSource(ID_IME);
WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), null, false, false,
SOFT_INPUT_ADJUST_RESIZE, 0, 0, TYPE_APPLICATION, WINDOWING_MODE_UNDEFINED, null);
assertEquals(0, insets.getSystemWindowInsetBottom());
@@ -273,32 +305,42 @@
@Test
public void testEquals_differentRect() {
- mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
- mState2.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 10, 10));
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100));
+ mState2.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 10, 10));
assertNotEqualsAndHashCode();
}
@Test
public void testEquals_differentSource() {
- mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
- mState2.getSource(ITYPE_IME).setFrame(new Rect(0, 0, 100, 100));
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100));
+ mState2.getOrCreateSource(ID_IME, ime())
+ .setFrame(new Rect(0, 0, 100, 100));
assertNotEqualsAndHashCode();
}
@Test
public void testEquals_sameButDifferentInsertOrder() {
- mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_IME).setFrame(new Rect(0, 0, 100, 100));
- mState2.getSource(ITYPE_IME).setFrame(new Rect(0, 0, 100, 100));
- mState2.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100));
+ mState.getOrCreateSource(ID_IME, ime())
+ .setFrame(new Rect(0, 0, 100, 100));
+ mState2.getOrCreateSource(ID_IME, ime())
+ .setFrame(new Rect(0, 0, 100, 100));
+ mState2.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100));
assertEqualsAndHashCode();
}
@Test
public void testEquals_visibility() {
- mState.getSource(ITYPE_IME).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_IME).setVisible(true);
- mState2.getSource(ITYPE_IME).setFrame(new Rect(0, 0, 100, 100));
+ mState.getOrCreateSource(ID_IME, ime())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setVisible(true);
+ mState2.getOrCreateSource(ID_IME, ime())
+ .setFrame(new Rect(0, 0, 100, 100));
assertNotEqualsAndHashCode();
}
@@ -363,19 +405,23 @@
@Test
public void testEquals_excludeInvisibleIme() {
- mState.getSource(ITYPE_IME).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_IME).setVisible(false);
- mState2.getSource(ITYPE_IME).setFrame(new Rect(0, 0, 100, 200));
- mState2.getSource(ITYPE_IME).setVisible(false);
+ mState.getOrCreateSource(ID_IME, ime())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setVisible(false);
+ mState2.getOrCreateSource(ID_IME, ime())
+ .setFrame(new Rect(0, 0, 100, 200))
+ .setVisible(false);
assertTrue(mState2.equals(mState, true, true /* excludeInvisibleIme */));
}
@Test
public void testParcelUnparcel() {
- mState.getSource(ITYPE_IME).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_IME).setVisibleFrame(new Rect(0, 0, 50, 10));
- mState.getSource(ITYPE_IME).setVisible(true);
- mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
+ mState.getOrCreateSource(ID_IME, ime())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setVisibleFrame(new Rect(0, 0, 50, 10))
+ .setVisible(true);
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100));
Parcel p = Parcel.obtain();
mState.writeToParcel(p, 0 /* flags */);
p.setDataPosition(0);
@@ -386,35 +432,31 @@
@Test
public void testCopy() {
- mState.getSource(ITYPE_IME).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_IME).setVisibleFrame(new Rect(0, 0, 50, 10));
- mState.getSource(ITYPE_IME).setVisible(true);
- mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
- mState2.getSource(ITYPE_NAVIGATION_BAR).setFrame(new Rect(0, 0, 100, 100));
+ mState.getOrCreateSource(ID_IME, ime())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setVisibleFrame(new Rect(0, 0, 50, 10))
+ .setVisible(true);
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100));
+ mState2.getOrCreateSource(ID_NAVIGATION_BAR, navigationBars())
+ .setFrame(new Rect(0, 0, 100, 100));
mState2.set(mState, true);
assertEquals(mState, mState2);
}
@Test
- public void testGetDefaultVisibility() {
- assertTrue(InsetsState.getDefaultVisibility(ITYPE_STATUS_BAR));
- assertTrue(InsetsState.getDefaultVisibility(ITYPE_NAVIGATION_BAR));
- assertTrue(InsetsState.getDefaultVisibility(ITYPE_CLIMATE_BAR));
- assertTrue(InsetsState.getDefaultVisibility(ITYPE_EXTRA_NAVIGATION_BAR));
- assertTrue(InsetsState.getDefaultVisibility(ITYPE_CAPTION_BAR));
- assertFalse(InsetsState.getDefaultVisibility(ITYPE_IME));
- }
-
- @Test
public void testCalculateVisibleInsets() {
- mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_STATUS_BAR).setVisible(true);
- mState.getSource(ITYPE_IME).setFrame(new Rect(0, 200, 100, 300));
- mState.getSource(ITYPE_IME).setVisible(true);
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setVisible(true);
+ mState.getOrCreateSource(ID_IME, ime())
+ .setFrame(new Rect(0, 200, 100, 300))
+ .setVisible(true);
// Make sure bottom gestures are ignored
- mState.getSource(ITYPE_BOTTOM_GESTURES).setFrame(new Rect(0, 100, 100, 300));
- mState.getSource(ITYPE_BOTTOM_GESTURES).setVisible(true);
+ mState.getOrCreateSource(ID_BOTTOM_GESTURES, systemGestures())
+ .setFrame(new Rect(0, 100, 100, 300))
+ .setVisible(true);
Insets visibleInsets = mState.calculateVisibleInsets(
new Rect(0, 0, 100, 300), TYPE_APPLICATION, WINDOWING_MODE_UNDEFINED,
SOFT_INPUT_ADJUST_PAN, 0 /* windowFlags */);
@@ -423,14 +465,17 @@
@Test
public void testCalculateVisibleInsets_adjustNothing() {
- mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_STATUS_BAR).setVisible(true);
- mState.getSource(ITYPE_IME).setFrame(new Rect(0, 200, 100, 300));
- mState.getSource(ITYPE_IME).setVisible(true);
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setVisible(true);
+ mState.getOrCreateSource(ID_IME, ime())
+ .setFrame(new Rect(0, 200, 100, 300))
+ .setVisible(true);
// Make sure bottom gestures are ignored
- mState.getSource(ITYPE_BOTTOM_GESTURES).setFrame(new Rect(0, 100, 100, 300));
- mState.getSource(ITYPE_BOTTOM_GESTURES).setVisible(true);
+ mState.getOrCreateSource(ID_BOTTOM_GESTURES, systemGestures())
+ .setFrame(new Rect(0, 100, 100, 300))
+ .setVisible(true);
Insets visibleInsets = mState.calculateVisibleInsets(
new Rect(0, 0, 100, 300), TYPE_APPLICATION, WINDOWING_MODE_UNDEFINED,
SOFT_INPUT_ADJUST_NOTHING, 0 /* windowFlags */);
@@ -439,14 +484,17 @@
@Test
public void testCalculateVisibleInsets_layoutNoLimits() {
- mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
- mState.getSource(ITYPE_STATUS_BAR).setVisible(true);
- mState.getSource(ITYPE_IME).setFrame(new Rect(0, 200, 100, 300));
- mState.getSource(ITYPE_IME).setVisible(true);
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setVisible(true);
+ mState.getOrCreateSource(ID_IME, ime())
+ .setFrame(new Rect(0, 200, 100, 300))
+ .setVisible(true);
// Make sure bottom gestures are ignored
- mState.getSource(ITYPE_BOTTOM_GESTURES).setFrame(new Rect(0, 100, 100, 300));
- mState.getSource(ITYPE_BOTTOM_GESTURES).setVisible(true);
+ mState.getOrCreateSource(ID_BOTTOM_GESTURES, systemGestures())
+ .setFrame(new Rect(0, 100, 100, 300))
+ .setVisible(true);
Insets visibleInsets = mState.calculateVisibleInsets(
new Rect(0, 0, 100, 300), TYPE_APPLICATION, WINDOWING_MODE_UNDEFINED,
SOFT_INPUT_ADJUST_PAN, FLAG_LAYOUT_NO_LIMITS);
@@ -455,12 +503,15 @@
@Test
public void testCalculateUncontrollableInsets() {
- mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 200, 100));
- mState.getSource(ITYPE_STATUS_BAR).setVisible(true);
- mState.getSource(ITYPE_IME).setFrame(new Rect(0, 200, 200, 300));
- mState.getSource(ITYPE_IME).setVisible(true);
- mState.getSource(ITYPE_NAVIGATION_BAR).setFrame(new Rect(100, 0, 200, 300));
- mState.getSource(ITYPE_NAVIGATION_BAR).setVisible(true);
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 200, 100))
+ .setVisible(true);
+ mState.getOrCreateSource(ID_IME, ime())
+ .setFrame(new Rect(0, 200, 200, 300))
+ .setVisible(true);
+ mState.getOrCreateSource(ID_NAVIGATION_BAR, navigationBars())
+ .setFrame(new Rect(100, 0, 200, 300))
+ .setVisible(true);
mState.setDisplayFrame(new Rect(0, 0, 200, 300));
assertEquals(0,
@@ -539,4 +590,77 @@
assertNotEquals(mState, mState2);
assertNotEquals(mState.hashCode(), mState2.hashCode());
}
+
+ @Test
+ public void testTraverse() {
+ // The type doesn't matter in this test.
+ final int type = statusBars();
+
+ final InsetsState insetsState1 = new InsetsState();
+ insetsState1.getOrCreateSource(2000, type);
+ insetsState1.getOrCreateSource(1000, type);
+ insetsState1.getOrCreateSource(3000, type);
+
+ final InsetsState insetsState2 = new InsetsState();
+ insetsState2.getOrCreateSource(3000, type);
+ insetsState2.getOrCreateSource(4000, type);
+ insetsState2.getOrCreateSource(2000, type);
+ insetsState2.getOrCreateSource(5000, type);
+
+ final int[] onStartCalled = {0};
+ final int[] onIdMatchCalled = {0};
+ final int[] onIdNotFoundInState1Called = {0};
+ final int[] onIdNotFoundInState2Called = {0};
+ final int[] onFinishCalled = {0};
+
+ InsetsState.traverse(insetsState1, insetsState2, new InsetsState.OnTraverseCallbacks() {
+ @Override
+ public void onStart(InsetsState state1, InsetsState state2) {
+ assertSame("state1 must be the same as insetsState1", state1, insetsState1);
+ assertSame("state2 must be the same as insetsState2", state2, insetsState2);
+ onStartCalled[0]++;
+ }
+
+ @Override
+ public void onIdMatch(InsetsSource source1, InsetsSource source2) {
+ assertNotNull("source1 must not be null.", source1);
+ assertNotNull("source2 must not be null.", source2);
+ assertEquals("Source IDs must match.", source1.getId(), source2.getId());
+ onIdMatchCalled[0]++;
+ }
+
+ @Override
+ public void onIdNotFoundInState1(int index2, InsetsSource source2) {
+ assertNotNull("source2 must not be null.", source2);
+ assertSame(source2 + " must be placed at " + index2 + " of insetsState2",
+ source2, insetsState2.sourceAt(index2));
+ assertNull("state1 must not have " + source2,
+ insetsState1.peekSource(source2.getId()));
+ onIdNotFoundInState1Called[0]++;
+ }
+
+ @Override
+ public void onIdNotFoundInState2(int index1, InsetsSource source1) {
+ assertNotNull("source1 must not be null.", source1);
+ assertSame(source1 + " must be placed at " + index1 + " of insetsState1",
+ source1, insetsState1.sourceAt(index1));
+ assertNull("state2 must not have " + source1,
+ insetsState2.peekSource(source1.getId()));
+ onIdNotFoundInState2Called[0]++;
+ }
+
+ @Override
+ public void onFinish(InsetsState state1, InsetsState state2) {
+ assertSame("state1 must be the same as insetsState1", state1, insetsState1);
+ assertSame("state2 must be the same as insetsState2", state2, insetsState2);
+ onFinishCalled[0]++;
+ }
+ });
+
+ assertEquals(1, onStartCalled[0]);
+ assertEquals(2, onIdMatchCalled[0]); // 2000 and 3000.
+ assertEquals(2, onIdNotFoundInState1Called[0]); // 4000 and 5000.
+ assertEquals(1, onIdNotFoundInState2Called[0]); // 1000.
+ assertEquals(1, onFinishCalled[0]);
+ }
}
diff --git a/core/tests/coretests/src/android/view/SurfaceControlViewHostInsetsTest.java b/core/tests/coretests/src/android/view/SurfaceControlViewHostInsetsTest.java
index 4731e81..4a00b00 100644
--- a/core/tests/coretests/src/android/view/SurfaceControlViewHostInsetsTest.java
+++ b/core/tests/coretests/src/android/view/SurfaceControlViewHostInsetsTest.java
@@ -16,35 +16,26 @@
package android.view;
+import static android.view.WindowInsets.Type.statusBars;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-import static android.view.InsetsState.ITYPE_STATUS_BAR;
-
import android.app.Instrumentation;
import android.content.Context;
import android.graphics.Insets;
-import android.graphics.Point;
import android.graphics.Rect;
import android.os.Binder;
import android.platform.test.annotations.Presubmit;
-import android.view.InsetsState.InternalInsetsType;
-import android.view.SurfaceControlViewHost;
-import android.view.View;
-import android.view.WindowInsets;
-import android.view.WindowInsets.Type;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
@Presubmit
@RunWith(AndroidJUnit4.class)
public class SurfaceControlViewHostInsetsTest {
@@ -78,9 +69,12 @@
private InsetsState statusBarState(boolean visible) {
final InsetsState insetsState = new InsetsState();
+ final int id = InsetsSource.createId(null /* owner */, 0 /* index */, statusBars());
insetsState.setDisplayFrame(new Rect(0, 0, 1000, 1000));
- insetsState.getSource(ITYPE_STATUS_BAR).setVisible(visible);
- insetsState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 10));
+ insetsState.getOrCreateSource(
+ InsetsSource.createId(null /* owner */, 0 /* index */, statusBars()), statusBars())
+ .setVisible(visible)
+ .setFrame(new Rect(0, 0, 100, 10));
return insetsState;
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java b/core/tests/coretests/src/android/view/WindowLayoutTests.java
similarity index 83%
rename from services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java
rename to core/tests/coretests/src/android/view/WindowLayoutTests.java
index 56c59cc..5cac98d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java
+++ b/core/tests/coretests/src/android/view/WindowLayoutTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,14 +14,13 @@
* limitations under the License.
*/
-package com.android.server.wm;
+package android.view;
-import static android.view.InsetsState.ITYPE_BOTTOM_DISPLAY_CUTOUT;
-import static android.view.InsetsState.ITYPE_LEFT_DISPLAY_CUTOUT;
-import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_RIGHT_DISPLAY_CUTOUT;
-import static android.view.InsetsState.ITYPE_STATUS_BAR;
-import static android.view.InsetsState.ITYPE_TOP_DISPLAY_CUTOUT;
+import static android.view.InsetsSource.ID_IME;
+import static android.view.WindowInsets.Type.displayCutout;
+import static android.view.WindowInsets.Type.ime;
+import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.statusBars;
import static android.view.WindowLayout.UNSPECIFIED_LENGTH;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
@@ -38,12 +37,6 @@
import android.graphics.Insets;
import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
-import android.view.DisplayCutout;
-import android.view.Gravity;
-import android.view.InsetsState;
-import android.view.WindowInsets;
-import android.view.WindowLayout;
-import android.view.WindowManager;
import android.window.ClientWindowFrames;
import androidx.test.filters.SmallTest;
@@ -70,6 +63,11 @@
new Rect(DISPLAY_WIDTH / 4, 0, DISPLAY_WIDTH * 3 / 4, DISPLAY_CUTOUT_HEIGHT);
private static final Insets WATERFALL_INSETS = Insets.of(6, 0, 12, 0);
+ private static final int ID_STATUS_BAR = InsetsSource.createId(
+ null /* owner */, 0 /* index */, statusBars());
+ private static final int ID_NAVIGATION_BAR = InsetsSource.createId(
+ null /* owner */, 0 /* index */, navigationBars());
+
private final WindowLayout mWindowLayout = new WindowLayout();
private final ClientWindowFrames mFrames = new ClientWindowFrames();
@@ -88,9 +86,9 @@
mAttrs = new WindowManager.LayoutParams();
mState = new InsetsState();
mState.setDisplayFrame(new Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT));
- mState.getSource(ITYPE_STATUS_BAR).setFrame(
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars()).setFrame(
0, 0, DISPLAY_WIDTH, STATUS_BAR_HEIGHT);
- mState.getSource(ITYPE_NAVIGATION_BAR).setFrame(
+ mState.getOrCreateSource(ID_NAVIGATION_BAR, navigationBars()).setFrame(
0, DISPLAY_HEIGHT - NAVIGATION_BAR_HEIGHT, DISPLAY_WIDTH, DISPLAY_HEIGHT);
mState.getDisplayCutoutSafe(mDisplayCutoutSafe);
mWindowBounds = new Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
@@ -117,14 +115,14 @@
new Rect(),
WATERFALL_INSETS));
mState.getDisplayCutoutSafe(mDisplayCutoutSafe);
- mState.getSource(ITYPE_LEFT_DISPLAY_CUTOUT).setFrame(
- 0, 0, mDisplayCutoutSafe.left, DISPLAY_HEIGHT);
- mState.getSource(ITYPE_TOP_DISPLAY_CUTOUT).setFrame(
- 0, 0, DISPLAY_WIDTH, mDisplayCutoutSafe.top);
- mState.getSource(ITYPE_RIGHT_DISPLAY_CUTOUT).setFrame(
- mDisplayCutoutSafe.right, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
- mState.getSource(ITYPE_BOTTOM_DISPLAY_CUTOUT).setFrame(
- 0, mDisplayCutoutSafe.bottom, DISPLAY_WIDTH, DISPLAY_HEIGHT);
+ mState.getOrCreateSource(InsetsSource.createId(null, 0, displayCutout()), displayCutout())
+ .setFrame(0, 0, mDisplayCutoutSafe.left, DISPLAY_HEIGHT);
+ mState.getOrCreateSource(InsetsSource.createId(null, 1, displayCutout()), displayCutout())
+ .setFrame(0, 0, DISPLAY_WIDTH, mDisplayCutoutSafe.top);
+ mState.getOrCreateSource(InsetsSource.createId(null, 2, displayCutout()), displayCutout())
+ .setFrame(mDisplayCutoutSafe.right, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
+ mState.getOrCreateSource(InsetsSource.createId(null, 3, displayCutout()), displayCutout())
+ .setFrame(0, mDisplayCutoutSafe.bottom, DISPLAY_WIDTH, DISPLAY_HEIGHT);
}
private static void assertInsetByTopBottom(int top, int bottom, Rect actual) {
@@ -266,8 +264,8 @@
@Test
public void fitInvisibleInsets() {
- mState.getSource(ITYPE_STATUS_BAR).setVisible(false);
- mState.getSource(ITYPE_NAVIGATION_BAR).setVisible(false);
+ mState.setSourceVisible(ID_STATUS_BAR, false);
+ mState.setSourceVisible(ID_NAVIGATION_BAR, false);
computeFrames();
assertInsetByTopBottom(0, 0, mFrames.displayFrame);
@@ -277,8 +275,8 @@
@Test
public void fitInvisibleInsetsIgnoringVisibility() {
- mState.getSource(ITYPE_STATUS_BAR).setVisible(false);
- mState.getSource(ITYPE_NAVIGATION_BAR).setVisible(false);
+ mState.setSourceVisible(ID_STATUS_BAR, false);
+ mState.setSourceVisible(ID_NAVIGATION_BAR, false);
mAttrs.setFitInsetsIgnoringVisibility(true);
computeFrames();
@@ -289,9 +287,9 @@
@Test
public void insetParentFrameByIme() {
- mState.getSource(InsetsState.ITYPE_IME).setVisible(true);
- mState.getSource(InsetsState.ITYPE_IME).setFrame(
- 0, DISPLAY_HEIGHT - IME_HEIGHT, DISPLAY_WIDTH, DISPLAY_HEIGHT);
+ mState.getOrCreateSource(ID_IME, ime())
+ .setVisible(true)
+ .setFrame(0, DISPLAY_HEIGHT - IME_HEIGHT, DISPLAY_WIDTH, DISPLAY_HEIGHT);
mAttrs.privateFlags |= PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME;
computeFrames();
@@ -322,11 +320,11 @@
mAttrs.setFitInsetsTypes(0);
computeFrames();
- assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
+ assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
mFrames.displayFrame);
- assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
+ assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
mFrames.parentFrame);
- assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
+ assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
mFrames.frame);
}
@@ -363,17 +361,17 @@
@Test
public void layoutInDisplayCutoutModeDefaultWithInvisibleSystemBars() {
addDisplayCutout();
- mState.getSource(ITYPE_STATUS_BAR).setVisible(false);
- mState.getSource(ITYPE_NAVIGATION_BAR).setVisible(false);
+ mState.setSourceVisible(ID_STATUS_BAR, false);
+ mState.setSourceVisible(ID_NAVIGATION_BAR, false);
mAttrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
mAttrs.setFitInsetsTypes(0);
computeFrames();
- assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
+ assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
mFrames.displayFrame);
- assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
+ assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
mFrames.parentFrame);
- assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
+ assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
mFrames.frame);
}
@@ -396,11 +394,11 @@
mAttrs.setFitInsetsTypes(0);
computeFrames();
- assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
+ assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
mFrames.displayFrame);
- assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
+ assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
mFrames.parentFrame);
- assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
+ assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
mFrames.frame);
}
diff --git a/core/tests/coretests/src/com/android/internal/config/sysui/OWNERS b/core/tests/coretests/src/com/android/internal/config/sysui/OWNERS
new file mode 100644
index 0000000..2e96c97
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/config/sysui/OWNERS
@@ -0,0 +1 @@
+include /packages/SystemUI/OWNERS
diff --git a/core/tests/coretests/src/com/android/internal/config/sysui/SystemUiSystemPropertiesFlagsTest.java b/core/tests/coretests/src/com/android/internal/config/sysui/SystemUiSystemPropertiesFlagsTest.java
new file mode 100644
index 0000000..6b9d39c
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/config/sysui/SystemUiSystemPropertiesFlagsTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.config.sysui;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.Flag;
+import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.FlagResolver;
+
+import junit.framework.TestCase;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@SmallTest
+public class SystemUiSystemPropertiesFlagsTest extends TestCase {
+
+ public class TestableDebugResolver extends SystemUiSystemPropertiesFlags.DebugResolver {
+ final Map<String, Boolean> mTestData = new HashMap<>();
+
+ @Override
+ public boolean getBoolean(String key, boolean defaultValue) {
+ Boolean testValue = mTestData.get(key);
+ return testValue == null ? defaultValue : testValue;
+ }
+
+ public void set(Flag flag, Boolean value) {
+ mTestData.put(flag.mSysPropKey, value);
+ }
+ }
+
+ private FlagResolver mProdResolver;
+ private TestableDebugResolver mDebugResolver;
+
+ private Flag mReleasedFlag;
+ private Flag mTeamfoodFlag;
+ private Flag mDevFlag;
+
+ public void setUp() {
+ mProdResolver = new SystemUiSystemPropertiesFlags.ProdResolver();
+ mDebugResolver = new TestableDebugResolver();
+ mReleasedFlag = SystemUiSystemPropertiesFlags.releasedFlag("mReleasedFlag");
+ mTeamfoodFlag = SystemUiSystemPropertiesFlags.teamfoodFlag("mTeamfoodFlag");
+ mDevFlag = SystemUiSystemPropertiesFlags.devFlag("mDevFlag");
+ }
+
+ public void tearDown() {
+ SystemUiSystemPropertiesFlags.TEST_RESOLVER = null;
+ }
+
+ public void testProdResolverReturnsDefault() {
+ assertThat(mProdResolver.isEnabled(mReleasedFlag)).isTrue();
+ assertThat(mProdResolver.isEnabled(mTeamfoodFlag)).isFalse();
+ assertThat(mProdResolver.isEnabled(mDevFlag)).isFalse();
+ }
+
+ public void testDebugResolverAndReleasedFlag() {
+ assertThat(mDebugResolver.isEnabled(mReleasedFlag)).isTrue();
+
+ mDebugResolver.set(mReleasedFlag, false);
+ assertThat(mDebugResolver.isEnabled(mReleasedFlag)).isFalse();
+
+ mDebugResolver.set(mReleasedFlag, true);
+ assertThat(mDebugResolver.isEnabled(mReleasedFlag)).isTrue();
+ }
+
+ private void assertTeamfoodFlag(Boolean flagValue, Boolean teamfood, boolean expected) {
+ mDebugResolver.set(mTeamfoodFlag, flagValue);
+ mDebugResolver.set(SystemUiSystemPropertiesFlags.TEAMFOOD, teamfood);
+ assertThat(mDebugResolver.isEnabled(mTeamfoodFlag)).isEqualTo(expected);
+ }
+
+ public void testDebugResolverAndTeamfoodFlag() {
+ assertTeamfoodFlag(null, null, false);
+ assertTeamfoodFlag(true, null, true);
+ assertTeamfoodFlag(false, null, false);
+ assertTeamfoodFlag(null, true, true);
+ assertTeamfoodFlag(true, true, true);
+ assertTeamfoodFlag(false, true, false);
+ assertTeamfoodFlag(null, false, false);
+ assertTeamfoodFlag(true, false, true);
+ assertTeamfoodFlag(false, false, false);
+ }
+
+ public void testDebugResolverAndDevFlag() {
+ assertThat(mDebugResolver.isEnabled(mDevFlag)).isFalse();
+
+ mDebugResolver.set(mDevFlag, true);
+ assertThat(mDebugResolver.isEnabled(mDevFlag)).isTrue();
+
+ mDebugResolver.set(mDevFlag, false);
+ assertThat(mDebugResolver.isEnabled(mDevFlag)).isFalse();
+ }
+}
diff --git a/core/tests/coretests/src/com/android/internal/statusbar/RegisterStatusBarResultTest.java b/core/tests/coretests/src/com/android/internal/statusbar/RegisterStatusBarResultTest.java
index 048c48b..f79ba28 100644
--- a/core/tests/coretests/src/com/android/internal/statusbar/RegisterStatusBarResultTest.java
+++ b/core/tests/coretests/src/com/android/internal/statusbar/RegisterStatusBarResultTest.java
@@ -67,7 +67,7 @@
BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE,
WindowInsets.Type.defaultVisible(),
"test" /* packageName */,
- new int[0] /* transientBarTypes */,
+ 0 /* transientBarTypes */,
new LetterboxDetails[] {letterboxDetails});
final RegisterStatusBarResult copy = clone(original);
diff --git a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
index 30540a5..89acbc7 100644
--- a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
+++ b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
@@ -131,6 +131,12 @@
assertEquals(Insets.NONE, out.getInsets());
}
+ @Test
+ public void testInvalidType() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new ScreenshotRequest.Builder(5, 2).build());
+ }
+
private Bitmap makeHardwareBitmap(int width, int height) {
HardwareBuffer buffer = HardwareBuffer.create(
width, height, HardwareBuffer.RGBA_8888, 1, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
diff --git a/data/etc/Android.bp b/data/etc/Android.bp
index d0c3e5f..f233c6e 100644
--- a/data/etc/Android.bp
+++ b/data/etc/Android.bp
@@ -36,6 +36,12 @@
}
prebuilt_etc {
+ name: "initial-package-stopped-states.xml",
+ sub_dir: "sysconfig",
+ src: "initial-package-stopped-states.xml",
+}
+
+prebuilt_etc {
name: "preinstalled-packages-platform-overlays.xml",
product_specific: true,
sub_dir: "sysconfig",
diff --git a/data/etc/initial-package-stopped-states.xml b/data/etc/initial-package-stopped-states.xml
new file mode 100644
index 0000000..6bda2c0
--- /dev/null
+++ b/data/etc/initial-package-stopped-states.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<!--
+This XML defines an allowlist for packages that should not be scanned in a "stopped" state.
+When this feature is turned on (indicated by the config config_stopSystemPackagesByDefault in
+core/res/res/values/config.xml) packages on the system partition that are encountered by
+the PackageManagerService for the first time are scanned in the "stopped" state. This allowlist
+is also considered while creating new users on the device. Stopped state is not set during
+subsequent reboots.
+
+Example usage
+ 1. <initial-package-state package="com.example.app" stopped="false"/>
+ Indicates that a system package - com.example.app's initial stopped state should not be set
+ by the Package Manager. By default, system apps are marked as stopped.
+ 2. <initial-package-state package="com.example.app" stopped="true"/>
+ Indicates that a system package - com.example.app's initial state should be set by the
+ Package Manager to "stopped=true". It will have the same effect on the
+ package's stopped state even if this package was not included in the allow list.
+ 3. <initial-package-state package="com.example.app"/>
+ Invalid usage.
+-->
+
+<config></config>
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index bccf283..53f4747 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -61,6 +61,12 @@
"group": "WM_DEBUG_APP_TRANSITIONS",
"at": "com\/android\/server\/wm\/WindowToken.java"
},
+ "-2088209279": {
+ "message": "Notified TransitionController that the display is ready.",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+ "at": "com\/android\/server\/wm\/WindowManagerService.java"
+ },
"-2072089308": {
"message": "Attempted to add window with token that is a sub-window: %s. Aborting.",
"level": "WARN",
@@ -1771,6 +1777,12 @@
"group": "WM_DEBUG_KEEP_SCREEN_ON",
"at": "com\/android\/server\/wm\/RootWindowContainer.java"
},
+ "-464564167": {
+ "message": "Current transition prevents automatic focus change",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_FOCUS",
+ "at": "com\/android\/server\/wm\/DisplayContent.java"
+ },
"-463348344": {
"message": "Removing and adding activity %s to root task at top callers=%s",
"level": "INFO",
@@ -2065,12 +2077,6 @@
"group": "WM_DEBUG_ANIM",
"at": "com\/android\/server\/wm\/SurfaceAnimator.java"
},
- "-206549078": {
- "message": "Not moving display (displayId=%d) to top. Top focused displayId=%d. Reason: config_perDisplayFocusEnabled",
- "level": "INFO",
- "group": "WM_DEBUG_FOCUS_LIGHT",
- "at": "com\/android\/server\/wm\/WindowManagerService.java"
- },
"-203358733": {
"message": "commitFinishDrawingLocked: mDrawState=READY_TO_SHOW %s",
"level": "INFO",
@@ -2767,6 +2773,12 @@
"group": "WM_DEBUG_STATES",
"at": "com\/android\/server\/wm\/TaskFragment.java"
},
+ "378890013": {
+ "message": "Apply and finish immediately because player is disabled for transition #%d .",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+ "at": "com\/android\/server\/wm\/Transition.java"
+ },
"385237117": {
"message": "moveFocusableActivityToTop: already on top and focused, activity=%s",
"level": "DEBUG",
@@ -3649,6 +3661,12 @@
"group": "WM_DEBUG_STATES",
"at": "com\/android\/server\/wm\/ActivityRecord.java"
},
+ "1282992082": {
+ "message": "Disabling player for transition #%d because display isn't enabled yet",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+ "at": "com\/android\/server\/wm\/TransitionController.java"
+ },
"1284122013": {
"message": "TaskFragment appeared name=%s",
"level": "VERBOSE",
diff --git a/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java b/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java
index fa173072..d39d4b4 100644
--- a/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java
+++ b/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java
@@ -412,11 +412,11 @@
private static ParsedRequiresPermission parseRequiresPermissionRecursively(
MethodInvocationTree tree, VisitorState state) {
- if (ENFORCE_VIA_CONTEXT.matches(tree, state)) {
+ if (ENFORCE_VIA_CONTEXT.matches(tree, state) && tree.getArguments().size() > 0) {
final ParsedRequiresPermission res = new ParsedRequiresPermission();
res.allOf.add(String.valueOf(ASTHelpers.constValue(tree.getArguments().get(0))));
return res;
- } else if (ENFORCE_VIA_CHECKER.matches(tree, state)) {
+ } else if (ENFORCE_VIA_CHECKER.matches(tree, state) && tree.getArguments().size() > 1) {
final ParsedRequiresPermission res = new ParsedRequiresPermission();
res.allOf.add(String.valueOf(ASTHelpers.constValue(tree.getArguments().get(1))));
return res;
diff --git a/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java b/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java
index 388988e..38831b1 100644
--- a/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java
+++ b/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java
@@ -415,4 +415,27 @@
"}")
.doTest();
}
+
+ @Test
+ public void testInvalidFunctions() {
+ compilationHelper
+ .addSourceFile("/android/annotation/RequiresPermission.java")
+ .addSourceFile("/android/annotation/SuppressLint.java")
+ .addSourceFile("/android/content/Context.java")
+ .addSourceLines("Example.java",
+ "import android.annotation.RequiresPermission;",
+ "import android.annotation.SuppressLint;",
+ "import android.content.Context;",
+ "class Foo extends Context {",
+ " private static final String RED = \"red\";",
+ " public void checkPermission() {",
+ " }",
+ " @RequiresPermission(RED)",
+ " // BUG: Diagnostic contains:",
+ " public void exampleScoped(Context context) {",
+ " checkPermission();",
+ " }",
+ "}")
+ .doTest();
+ }
}
diff --git a/graphics/java/android/graphics/BaseCanvas.java b/graphics/java/android/graphics/BaseCanvas.java
index 00ffd09..8cd8ddf 100644
--- a/graphics/java/android/graphics/BaseCanvas.java
+++ b/graphics/java/android/graphics/BaseCanvas.java
@@ -670,8 +670,12 @@
/**
* Draws a mesh object to the screen.
*
+ * <p>Note: antialiasing is not supported, therefore {@link Paint#ANTI_ALIAS_FLAG} is
+ * ignored.</p>
+ *
* @param mesh {@link Mesh} object that will be drawn to the screen
- * @param blendMode {@link BlendMode} used to blend mesh primitives with the Paint color/shader
+ * @param blendMode {@link BlendMode} used to blend mesh primitives as the destination color
+ * with the Paint color/shader as the source color.
* @param paint {@link Paint} used to provide a color/shader/blend mode.
*/
public void drawMesh(@NonNull Mesh mesh, BlendMode blendMode, @NonNull Paint paint) {
diff --git a/graphics/java/android/graphics/Canvas.java b/graphics/java/android/graphics/Canvas.java
index 42c892a..e7814cb 100644
--- a/graphics/java/android/graphics/Canvas.java
+++ b/graphics/java/android/graphics/Canvas.java
@@ -241,7 +241,7 @@
/**
* Return true if the device that the current layer draws into is opaque
- * (i.e. does not support per-pixel alpha).
+ * (that is, it does not support per-pixel alpha).
*
* @return true if the device that the current layer draws into is opaque
*/
diff --git a/graphics/java/android/graphics/ColorSpace.java b/graphics/java/android/graphics/ColorSpace.java
index 2427dec..4c669b8 100644
--- a/graphics/java/android/graphics/ColorSpace.java
+++ b/graphics/java/android/graphics/ColorSpace.java
@@ -212,9 +212,9 @@
new Rgb.TransferParameters(1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4);
private static final Rgb.TransferParameters BT2020_HLG_TRANSFER_PARAMETERS =
new Rgb.TransferParameters(2.0f, 2.0f, 1 / 0.17883277f,
- 0.28466892f, 0.5599107f, 0.0f, -3.0f, true);
+ 0.28466892f, 0.5599107f, -11 / 12.0f, -3.0f, true);
private static final Rgb.TransferParameters BT2020_PQ_TRANSFER_PARAMETERS =
- new Rgb.TransferParameters(107 / 128.0f, 1.0f, 32 / 2523.0f,
+ new Rgb.TransferParameters(-107 / 128.0f, 1.0f, 32 / 2523.0f,
2413 / 128.0f, -2392 / 128.0f, 8192 / 1305.0f, -2.0f, true);
// See static initialization block next to #get(Named)
diff --git a/graphics/java/android/graphics/HardwareBufferRenderer.java b/graphics/java/android/graphics/HardwareBufferRenderer.java
new file mode 100644
index 0000000..361dc59
--- /dev/null
+++ b/graphics/java/android/graphics/HardwareBufferRenderer.java
@@ -0,0 +1,390 @@
+/*
+ * 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 android.graphics;
+
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.ColorSpace.Named;
+import android.hardware.HardwareBuffer;
+import android.hardware.SyncFence;
+import android.view.SurfaceControl;
+
+import libcore.util.NativeAllocationRegistry;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * <p>Creates an instance of a hardware-accelerated renderer. This is used to render a scene built
+ * from {@link RenderNode}s to an output {@link HardwareBuffer}. There can be as many
+ * HardwareBufferRenderer instances as desired.</p>
+ *
+ * <h3>Resources & lifecycle</h3>
+ *
+ * <p>All HardwareBufferRenderer and {@link HardwareRenderer} instances share a common render
+ * thread. Therefore HardwareBufferRenderer will share common resources and GPU utilization with
+ * hardware accelerated rendering initiated by the UI thread of an application.
+ * The render thread contains the GPU context & resources necessary to do GPU-accelerated
+ * rendering. As such, the first HardwareBufferRenderer created comes with the cost of also creating
+ * the associated GPU contexts, however each incremental HardwareBufferRenderer thereafter is fairly
+ * cheap. The expected usage is to have a HardwareBufferRenderer instance for every active {@link
+ * HardwareBuffer}.</p>
+ *
+ * This is useful in situations where a scene built with {@link RenderNode}s can be consumed
+ * directly by the system compositor through
+ * {@link SurfaceControl.Transaction#setBuffer(SurfaceControl, HardwareBuffer)}.
+ *
+ * HardwareBufferRenderer will never clear contents before each draw invocation so previous contents
+ * in the {@link HardwareBuffer} target will be preserved across renders.
+ */
+public class HardwareBufferRenderer implements AutoCloseable {
+
+ private static final ColorSpace DEFAULT_COLORSPACE = ColorSpace.get(Named.SRGB);
+
+ private static class HardwareBufferRendererHolder {
+ public static final NativeAllocationRegistry REGISTRY =
+ NativeAllocationRegistry.createMalloced(
+ HardwareBufferRenderer.class.getClassLoader(), nGetFinalizer());
+ }
+
+ private final HardwareBuffer mHardwareBuffer;
+ private final RenderRequest mRenderRequest;
+ private final RenderNode mRootNode;
+ private final Runnable mCleaner;
+
+ private long mProxy;
+
+ /**
+ * Creates a new instance of {@link HardwareBufferRenderer} with the provided {@link
+ * HardwareBuffer} as the output of the rendered scene.
+ */
+ public HardwareBufferRenderer(@NonNull HardwareBuffer buffer) {
+ RenderNode rootNode = RenderNode.adopt(nCreateRootRenderNode());
+ rootNode.setClipToBounds(false);
+ mProxy = nCreateHardwareBufferRenderer(buffer, rootNode.mNativeRenderNode);
+ mCleaner = HardwareBufferRendererHolder.REGISTRY.registerNativeAllocation(this, mProxy);
+ mRenderRequest = new RenderRequest();
+ mRootNode = rootNode;
+ mHardwareBuffer = buffer;
+ }
+
+ /**
+ * Sets the content root to render. It is not necessary to call this whenever the content
+ * recording changes. Any mutations to the RenderNode content, or any of the RenderNodes
+ * contained within the content node, will be applied whenever a new {@link RenderRequest} is
+ * issued via {@link #obtainRenderRequest()} and {@link RenderRequest#draw(Executor,
+ * Consumer)}.
+ *
+ * @param content The content to set as the root RenderNode. If null the content root is removed
+ * and the renderer will draw nothing.
+ */
+ public void setContentRoot(@Nullable RenderNode content) {
+ RecordingCanvas canvas = mRootNode.beginRecording();
+ if (content != null) {
+ canvas.drawRenderNode(content);
+ }
+ mRootNode.endRecording();
+ }
+
+ /**
+ * Returns a {@link RenderRequest} that can be used to render into the provided {@link
+ * HardwareBuffer}. This is used to synchronize the RenderNode content provided by {@link
+ * #setContentRoot(RenderNode)}.
+ *
+ * @return An instance of {@link RenderRequest}. The instance may be reused for every frame, so
+ * the caller should not hold onto it for longer than a single render request.
+ */
+ @NonNull
+ public RenderRequest obtainRenderRequest() {
+ mRenderRequest.reset();
+ return mRenderRequest;
+ }
+
+ /**
+ * Returns if the {@link HardwareBufferRenderer} has already been closed. That is
+ * {@link HardwareBufferRenderer#close()} has been invoked.
+ * @return True if the {@link HardwareBufferRenderer} has been closed, false otherwise.
+ */
+ public boolean isClosed() {
+ return mProxy == 0L;
+ }
+
+ /**
+ * Releases the resources associated with this {@link HardwareBufferRenderer} instance. **Note**
+ * this does not call {@link HardwareBuffer#close()} on the provided {@link HardwareBuffer}
+ * instance
+ */
+ @Override
+ public void close() {
+ // Note we explicitly call this only here to clean-up potential animator state
+ // This is not done as part of the NativeAllocationRegistry as it would invoke animator
+ // callbacks on the wrong thread
+ nDestroyRootRenderNode(mRootNode.mNativeRenderNode);
+ if (mProxy != 0L) {
+ mCleaner.run();
+ mProxy = 0L;
+ }
+ }
+
+ /**
+ * Sets the center of the light source. The light source point controls the directionality and
+ * shape of shadows rendered by RenderNode Z & elevation.
+ *
+ * <p>The light source should be setup both as part of initial configuration, and whenever
+ * the window moves to ensure the light source stays anchored in display space instead of in
+ * window space.
+ *
+ * <p>This must be set at least once along with {@link #setLightSourceAlpha(float, float)}
+ * before shadows will work.
+ *
+ * @param lightX The X position of the light source. If unsure, a reasonable default
+ * is 'displayWidth / 2f - windowLeft'.
+ * @param lightY The Y position of the light source. If unsure, a reasonable default
+ * is '0 - windowTop'
+ * @param lightZ The Z position of the light source. Must be >= 0. If unsure, a reasonable
+ * default is 600dp.
+ * @param lightRadius The radius of the light source. Smaller radius will have sharper edges,
+ * larger radius will have softer shadows. If unsure, a reasonable default is 800 dp.
+ */
+ public void setLightSourceGeometry(
+ float lightX,
+ float lightY,
+ @FloatRange(from = 0f) float lightZ,
+ @FloatRange(from = 0f) float lightRadius
+ ) {
+ validateFinite(lightX, "lightX");
+ validateFinite(lightY, "lightY");
+ validatePositive(lightZ, "lightZ");
+ validatePositive(lightRadius, "lightRadius");
+ nSetLightGeometry(mProxy, lightX, lightY, lightZ, lightRadius);
+ }
+
+ /**
+ * Configures the ambient & spot shadow alphas. This is the alpha used when the shadow has max
+ * alpha, and ramps down from the values provided to zero.
+ *
+ * <p>These values are typically provided by the current theme, see
+ * {@link android.R.attr#spotShadowAlpha} and {@link android.R.attr#ambientShadowAlpha}.
+ *
+ * <p>This must be set at least once along with
+ * {@link #setLightSourceGeometry(float, float, float, float)} before shadows will work.
+ *
+ * @param ambientShadowAlpha The alpha for the ambient shadow. If unsure, a reasonable default
+ * is 0.039f.
+ * @param spotShadowAlpha The alpha for the spot shadow. If unsure, a reasonable default is
+ * 0.19f.
+ */
+ public void setLightSourceAlpha(@FloatRange(from = 0.0f, to = 1.0f) float ambientShadowAlpha,
+ @FloatRange(from = 0.0f, to = 1.0f) float spotShadowAlpha) {
+ validateAlpha(ambientShadowAlpha, "ambientShadowAlpha");
+ validateAlpha(spotShadowAlpha, "spotShadowAlpha");
+ nSetLightAlpha(mProxy, ambientShadowAlpha, spotShadowAlpha);
+ }
+
+ /**
+ * Class that contains data regarding the result of the render request.
+ * Consumers are to wait on the provided {@link SyncFence} before consuming the HardwareBuffer
+ * provided to {@link HardwareBufferRenderer} as well as verify that the status returned by
+ * {@link RenderResult#getStatus()} returns {@link RenderResult#SUCCESS}.
+ */
+ public static final class RenderResult {
+
+ /**
+ * Render request was completed successfully
+ */
+ public static final int SUCCESS = 0;
+
+ /**
+ * Render request failed with an unknown error
+ */
+ public static final int ERROR_UNKNOWN = 1;
+
+ /** @hide **/
+ @IntDef(value = {SUCCESS, ERROR_UNKNOWN})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RenderResultStatus{}
+
+ private final SyncFence mFence;
+ private final int mResultStatus;
+
+ private RenderResult(@NonNull SyncFence fence, @RenderResultStatus int resultStatus) {
+ mFence = fence;
+ mResultStatus = resultStatus;
+ }
+
+ @NonNull
+ public SyncFence getFence() {
+ return mFence;
+ }
+
+ @RenderResultStatus
+ public int getStatus() {
+ return mResultStatus;
+ }
+ }
+
+ /**
+ * Sets the parameters that can be used to control a render request for a {@link
+ * HardwareBufferRenderer}. This is not thread-safe and must not be held on to for longer than a
+ * single request.
+ */
+ public final class RenderRequest {
+
+ private ColorSpace mColorSpace = DEFAULT_COLORSPACE;
+ private int mTransform = SurfaceControl.BUFFER_TRANSFORM_IDENTITY;
+
+ private RenderRequest() { }
+
+ /**
+ * Syncs the RenderNode tree to the render thread and requests content to be drawn. This
+ * {@link RenderRequest} instance should no longer be used after calling this method. The
+ * system internally may reuse instances of {@link RenderRequest} to reduce allocation
+ * churn.
+ *
+ * @param executor Executor used to deliver callbacks
+ * @param renderCallback Callback invoked when rendering is complete. This includes a
+ * {@link RenderResult} that provides a {@link SyncFence} that should be waited upon for
+ * completion before consuming the rendered output in the provided {@link HardwareBuffer}
+ * instance.
+ *
+ * @throws IllegalStateException if attempt to draw is made when
+ * {@link HardwareBufferRenderer#isClosed()} returns true
+ */
+ public void draw(
+ @NonNull Executor executor,
+ @NonNull Consumer<RenderResult> renderCallback
+ ) {
+ Consumer<RenderResult> wrapped = consumable -> executor.execute(
+ () -> renderCallback.accept(consumable));
+ if (!isClosed()) {
+ nRender(
+ mProxy,
+ mTransform,
+ mHardwareBuffer.getWidth(),
+ mHardwareBuffer.getHeight(),
+ mColorSpace.getNativeInstance(),
+ wrapped);
+ } else {
+ throw new IllegalStateException("Attempt to draw with a HardwareBufferRenderer "
+ + "instance that has already been closed");
+ }
+ }
+
+ private void reset() {
+ mColorSpace = DEFAULT_COLORSPACE;
+ mTransform = SurfaceControl.BUFFER_TRANSFORM_IDENTITY;
+ }
+
+ /**
+ * Configures the color space which the content should be rendered in. This affects
+ * how the framework will interpret the color at each pixel. The color space provided here
+ * must be non-null, RGB based and leverage an ICC parametric curve. The min/max values
+ * of the components should not reduce the numerical range compared to the previously
+ * assigned color space. If left unspecified, the default color space of SRGB will be used.
+ *
+ * @param colorSpace The color space the content should be rendered in. If null is provided
+ * the default of SRGB will be used.
+ */
+ @NonNull
+ public RenderRequest setColorSpace(@Nullable ColorSpace colorSpace) {
+ if (colorSpace == null) {
+ mColorSpace = DEFAULT_COLORSPACE;
+ } else {
+ mColorSpace = colorSpace;
+ }
+ return this;
+ }
+
+ /**
+ * Specifies a transform to be applied before content is rendered. This is useful
+ * for pre-rotating content for the current display orientation to increase performance
+ * of displaying the associated buffer. This transformation will also adjust the light
+ * source position for the specified rotation.
+ * @see SurfaceControl.Transaction#setBufferTransform(SurfaceControl, int)
+ */
+ @NonNull
+ public RenderRequest setBufferTransform(
+ @SurfaceControl.BufferTransform int bufferTransform) {
+ boolean validTransform = bufferTransform == SurfaceControl.BUFFER_TRANSFORM_IDENTITY
+ || bufferTransform == SurfaceControl.BUFFER_TRANSFORM_ROTATE_90
+ || bufferTransform == SurfaceControl.BUFFER_TRANSFORM_ROTATE_180
+ || bufferTransform == SurfaceControl.BUFFER_TRANSFORM_ROTATE_270;
+ if (validTransform) {
+ mTransform = bufferTransform;
+ } else {
+ throw new IllegalArgumentException("Invalid transform provided, must be one of"
+ + "the SurfaceControl.BufferTransform values");
+ }
+ return this;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ /* package */
+ static native int nRender(long renderer, int transform, int width, int height, long colorSpace,
+ Consumer<RenderResult> callback);
+
+ private static native long nCreateRootRenderNode();
+
+ private static native void nDestroyRootRenderNode(long rootRenderNode);
+
+ private static native long nCreateHardwareBufferRenderer(HardwareBuffer buffer,
+ long rootRenderNode);
+
+ private static native void nSetLightGeometry(long bufferRenderer, float lightX, float lightY,
+ float lightZ, float radius);
+
+ private static native void nSetLightAlpha(long nativeProxy, float ambientShadowAlpha,
+ float spotShadowAlpha);
+
+ private static native long nGetFinalizer();
+
+ // Called by native
+ private static void invokeRenderCallback(
+ @NonNull Consumer<RenderResult> callback,
+ int fd,
+ int status
+ ) {
+ callback.accept(new RenderResult(SyncFence.adopt(fd), status));
+ }
+
+ private static void validateAlpha(float alpha, String argumentName) {
+ if (!(alpha >= 0.0f && alpha <= 1.0f)) {
+ throw new IllegalArgumentException(argumentName + " must be a valid alpha, "
+ + alpha + " is not in the range of 0.0f to 1.0f");
+ }
+ }
+
+ private static void validateFinite(float f, String argumentName) {
+ if (!Float.isFinite(f)) {
+ throw new IllegalArgumentException(argumentName + " must be finite, given=" + f);
+ }
+ }
+
+ private static void validatePositive(float f, String argumentName) {
+ if (!(Float.isFinite(f) && f >= 0.0f)) {
+ throw new IllegalArgumentException(argumentName
+ + " must be a finite positive, given=" + f);
+ }
+ }
+}
diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java
index c3eb7aa..0488b9d 100644
--- a/graphics/java/android/graphics/HardwareRenderer.java
+++ b/graphics/java/android/graphics/HardwareRenderer.java
@@ -668,7 +668,7 @@
/** @hide */
public void setTargetSdrHdrRatio(float ratio) {
- if (ratio < 1.f || Float.isNaN(ratio) || Float.isInfinite(ratio)) ratio = 1.f;
+ if (ratio < 1.f || !Float.isFinite(ratio)) ratio = 1.f;
nSetTargetSdrHdrRatio(mNativeProxy, ratio);
}
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
index 2830d7eff..4715045 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
@@ -801,25 +801,32 @@
));
if (mSpec.isDevicePropertiesAttestationIncluded()) {
+ final String platformReportedBrand = TextUtils.isEmpty(Build.BRAND_FOR_ATTESTATION)
+ ? Build.BRAND : Build.BRAND_FOR_ATTESTATION;
params.add(KeyStore2ParameterUtils.makeBytes(
KeymasterDefs.KM_TAG_ATTESTATION_ID_BRAND,
- Build.BRAND.getBytes(StandardCharsets.UTF_8)
+ platformReportedBrand.getBytes(StandardCharsets.UTF_8)
));
params.add(KeyStore2ParameterUtils.makeBytes(
KeymasterDefs.KM_TAG_ATTESTATION_ID_DEVICE,
Build.DEVICE.getBytes(StandardCharsets.UTF_8)
));
+ final String platformReportedProduct =
+ TextUtils.isEmpty(Build.PRODUCT_FOR_ATTESTATION) ? Build.PRODUCT :
+ Build.PRODUCT_FOR_ATTESTATION;
params.add(KeyStore2ParameterUtils.makeBytes(
KeymasterDefs.KM_TAG_ATTESTATION_ID_PRODUCT,
- Build.PRODUCT.getBytes(StandardCharsets.UTF_8)
+ platformReportedProduct.getBytes(StandardCharsets.UTF_8)
));
params.add(KeyStore2ParameterUtils.makeBytes(
KeymasterDefs.KM_TAG_ATTESTATION_ID_MANUFACTURER,
Build.MANUFACTURER.getBytes(StandardCharsets.UTF_8)
));
+ final String platformReportedModel = TextUtils.isEmpty(Build.MODEL_FOR_ATTESTATION)
+ ? Build.MODEL : Build.MODEL_FOR_ATTESTATION;
params.add(KeyStore2ParameterUtils.makeBytes(
KeymasterDefs.KM_TAG_ATTESTATION_ID_MODEL,
- Build.MODEL.getBytes(StandardCharsets.UTF_8)
+ platformReportedModel.getBytes(StandardCharsets.UTF_8)
));
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index 666b472..76e0e1e 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -48,7 +48,7 @@
// TODO(b/241126279) Introduce constants to better version functionality
@Override
public int getVendorApiLevel() {
- return 2;
+ return 3;
}
@NonNull
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentation.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentation.java
new file mode 100644
index 0000000..1ff1694
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentation.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions.area;
+
+import android.app.Presentation;
+import android.content.Context;
+import android.view.Display;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.window.extensions.core.util.function.Consumer;
+
+/**
+ * {@link Presentation} object that is used to present extra content
+ * on the rear facing display when in a rear display presentation feature.
+ */
+class RearDisplayPresentation extends Presentation implements ExtensionWindowAreaPresentation {
+
+ @NonNull
+ private final Consumer<@WindowAreaComponent.WindowAreaSessionState Integer> mStateConsumer;
+
+ RearDisplayPresentation(@NonNull Context outerContext, @NonNull Display display,
+ @NonNull Consumer<@WindowAreaComponent.WindowAreaSessionState Integer> stateConsumer) {
+ super(outerContext, display);
+ mStateConsumer = stateConsumer;
+ }
+
+ /**
+ * {@code mStateConsumer} is notified that their content is now visible when the
+ * {@link Presentation} object is started. There is no comparable callback for
+ * {@link WindowAreaComponent#SESSION_STATE_INVISIBLE} in {@link #onStop()} due to the
+ * timing of when a {@link android.hardware.devicestate.DeviceStateRequest} is cancelled
+ * ending rear display presentation mode happening before the {@link Presentation} is stopped.
+ */
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mStateConsumer.accept(WindowAreaComponent.SESSION_STATE_VISIBLE);
+ }
+
+ @NonNull
+ @Override
+ public Context getPresentationContext() {
+ return getContext();
+ }
+
+ @Override
+ public void setPresentationView(View view) {
+ setContentView(view);
+ if (!isShowing()) {
+ show();
+ }
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationController.java
new file mode 100644
index 0000000..141a6ad
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationController.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions.area;
+
+import static androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_ACTIVE;
+import static androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_INACTIVE;
+
+import android.content.Context;
+import android.hardware.devicestate.DeviceStateRequest;
+import android.hardware.display.DisplayManager;
+import android.util.Log;
+import android.view.Display;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.window.extensions.core.util.function.Consumer;
+
+import java.util.Objects;
+
+/**
+ * Controller class that keeps track of the status of the device state request
+ * to enable the rear display presentation feature. This controller notifies the session callback
+ * when the state request is active, and notifies the callback when the request is canceled.
+ *
+ * Clients are notified via {@link Consumer} provided with
+ * {@link androidx.window.extensions.area.WindowAreaComponent.WindowAreaStatus} values to signify
+ * when the request becomes active and cancelled.
+ */
+class RearDisplayPresentationController implements DeviceStateRequest.Callback {
+
+ private static final String TAG = "RearDisplayPresentationController";
+
+ // Original context that requested to enable rear display presentation mode
+ @NonNull
+ private final Context mContext;
+ @NonNull
+ private final Consumer<@WindowAreaComponent.WindowAreaSessionState Integer> mStateConsumer;
+ @Nullable
+ private ExtensionWindowAreaPresentation mExtensionWindowAreaPresentation;
+ @NonNull
+ private final DisplayManager mDisplayManager;
+
+ /**
+ * Creates the RearDisplayPresentationController
+ * @param context Originating {@link android.content.Context} that is initiating the rear
+ * display presentation session.
+ * @param stateConsumer {@link Consumer} that will be notified that the session is active when
+ * the device state request is active and the session has been created. If the device
+ * state request is cancelled, the callback will be notified that the session has been
+ * ended. This could occur through a call to cancel the feature or if the device is
+ * manipulated in a way that cancels any device state override.
+ */
+ RearDisplayPresentationController(@NonNull Context context,
+ @NonNull Consumer<@WindowAreaComponent.WindowAreaSessionState Integer> stateConsumer) {
+ Objects.requireNonNull(context);
+ Objects.requireNonNull(stateConsumer);
+
+ mContext = context;
+ mStateConsumer = stateConsumer;
+ mDisplayManager = context.getSystemService(DisplayManager.class);
+ }
+
+ @Override
+ public void onRequestActivated(@NonNull DeviceStateRequest request) {
+ Display[] rearDisplays = mDisplayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_REAR);
+ if (rearDisplays.length == 0) {
+ mStateConsumer.accept(SESSION_STATE_INACTIVE);
+ Log.e(TAG, "Rear display list should not be empty");
+ return;
+ }
+
+ mExtensionWindowAreaPresentation =
+ new RearDisplayPresentation(mContext, rearDisplays[0], mStateConsumer);
+ mStateConsumer.accept(SESSION_STATE_ACTIVE);
+ }
+
+ @Override
+ public void onRequestCanceled(@NonNull DeviceStateRequest request) {
+ mStateConsumer.accept(SESSION_STATE_INACTIVE);
+ }
+
+ @Nullable
+ public ExtensionWindowAreaPresentation getWindowAreaPresentation() {
+ return mExtensionWindowAreaPresentation;
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationStatus.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationStatus.java
new file mode 100644
index 0000000..0b1423a
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/RearDisplayPresentationStatus.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions.area;
+
+import android.util.DisplayMetrics;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Class that provides information around the current status of a window area feature. Contains
+ * the current {@link WindowAreaComponent.WindowAreaStatus} value corresponding to the
+ * rear display presentation feature, as well as the {@link DisplayMetrics} for the rear facing
+ * display.
+ */
+class RearDisplayPresentationStatus implements ExtensionWindowAreaStatus {
+
+ @WindowAreaComponent.WindowAreaStatus
+ private final int mWindowAreaStatus;
+
+ @NonNull
+ private final DisplayMetrics mDisplayMetrics;
+
+ RearDisplayPresentationStatus(@WindowAreaComponent.WindowAreaStatus int status,
+ @NonNull DisplayMetrics displayMetrics) {
+ mWindowAreaStatus = status;
+ mDisplayMetrics = displayMetrics;
+ }
+
+ /**
+ * Returns the {@link androidx.window.extensions.area.WindowAreaComponent.WindowAreaStatus}
+ * value that relates to the current status of a feature.
+ */
+ @Override
+ @WindowAreaComponent.WindowAreaStatus
+ public int getWindowAreaStatus() {
+ return mWindowAreaStatus;
+ }
+
+ /**
+ * Returns the {@link DisplayMetrics} that corresponds to the window area that a feature
+ * interacts with. This is converted to size class information provided to developers.
+ */
+ @Override
+ @NonNull
+ public DisplayMetrics getWindowAreaDisplayMetrics() {
+ return mDisplayMetrics;
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
index 3adae70..274dcae 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
@@ -22,15 +22,22 @@
import android.content.Context;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateRequest;
+import android.hardware.display.DisplayManager;
import android.util.ArraySet;
+import android.util.DisplayMetrics;
+import android.util.Pair;
+import android.view.Display;
+import android.view.DisplayAddress;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.window.extensions.core.util.function.Consumer;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ArrayUtils;
import java.util.concurrent.Executor;
-import java.util.function.Consumer;
/**
* Reference implementation of androidx.window.extensions.area OEM interface for use with
@@ -46,61 +53,102 @@
private final Object mLock = new Object();
+ @NonNull
private final DeviceStateManager mDeviceStateManager;
+ @NonNull
+ private final DisplayManager mDisplayManager;
+ @NonNull
private final Executor mExecutor;
@GuardedBy("mLock")
private final ArraySet<Consumer<Integer>> mRearDisplayStatusListeners = new ArraySet<>();
+ @GuardedBy("mLock")
+ private final ArraySet<Consumer<ExtensionWindowAreaStatus>>
+ mRearDisplayPresentationStatusListeners = new ArraySet<>();
private final int mRearDisplayState;
+ private final int mConcurrentDisplayState;
+ @NonNull
+ private final int[] mFoldedDeviceStates;
+ @NonNull
+ private long mRearDisplayAddress = 0;
@WindowAreaSessionState
private int mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE;
@GuardedBy("mLock")
private int mCurrentDeviceState = INVALID_DEVICE_STATE;
@GuardedBy("mLock")
- private int mCurrentDeviceBaseState = INVALID_DEVICE_STATE;
+ private int[] mCurrentSupportedDeviceStates;
+
@GuardedBy("mLock")
- private DeviceStateRequest mDeviceStateRequest;
+ private DeviceStateRequest mRearDisplayStateRequest;
+ @GuardedBy("mLock")
+ private RearDisplayPresentationController mRearDisplayPresentationController;
+
+ @Nullable
+ @GuardedBy("mLock")
+ private DisplayMetrics mRearDisplayMetrics;
+
+ @WindowAreaSessionState
+ @GuardedBy("mLock")
+ private int mLastReportedRearDisplayPresentationStatus;
public WindowAreaComponentImpl(@NonNull Context context) {
mDeviceStateManager = context.getSystemService(DeviceStateManager.class);
+ mDisplayManager = context.getSystemService(DisplayManager.class);
mExecutor = context.getMainExecutor();
+ mCurrentSupportedDeviceStates = mDeviceStateManager.getSupportedStates();
+ mFoldedDeviceStates = context.getResources().getIntArray(
+ R.array.config_foldedDeviceStates);
+
// TODO(b/236022708) Move rear display state to device state config file
mRearDisplayState = context.getResources().getInteger(
R.integer.config_deviceStateRearDisplay);
+ mConcurrentDisplayState = context.getResources().getInteger(
+ R.integer.config_deviceStateConcurrentRearDisplay);
+
mDeviceStateManager.registerCallback(mExecutor, this);
+ if (mConcurrentDisplayState != INVALID_DEVICE_STATE) {
+ mRearDisplayAddress = Long.parseLong(context.getResources().getString(
+ R.string.config_rearDisplayPhysicalAddress));
+ }
}
/**
* Adds a listener interested in receiving updates on the RearDisplayStatus
* of the device. Because this is being called from the OEM provided
- * extensions, we will post the result of the listener on the executor
+ * extensions, the result of the listener will be posted on the executor
* provided by the developer at the initial call site.
*
- * Depending on the initial state of the device, we will return either
+ * Rear display mode moves the calling application to the display on the device that is
+ * facing the same direction as the rear cameras. This would be the cover display on a fold-in
+ * style device when the device is opened.
+ *
+ * Depending on the initial state of the device, the {@link Consumer} will receive either
* {@link WindowAreaComponent#STATUS_AVAILABLE} or
* {@link WindowAreaComponent#STATUS_UNAVAILABLE} if the feature is supported or not in that
- * state respectively. When the rear display feature is triggered, we update the status to be
- * {@link WindowAreaComponent#STATUS_UNAVAILABLE}. TODO(b/240727590) Prefix with AREA_
+ * state respectively. When the rear display feature is triggered, the status is updated to be
+ * {@link WindowAreaComponent#STATUS_UNAVAILABLE}.
+ * TODO(b/240727590): Prefix with AREA_
*
- * TODO(b/239833099) Add a STATUS_ACTIVE option to let apps know if a feature is currently
- * enabled.
+ * TODO(b/239833099): Add a STATUS_ACTIVE option to let apps know if a feature is currently
+ * enabled.
*
* @param consumer {@link Consumer} interested in receiving updates to the status of
* rear display mode.
*/
+ @Override
public void addRearDisplayStatusListener(
@NonNull Consumer<@WindowAreaStatus Integer> consumer) {
synchronized (mLock) {
mRearDisplayStatusListeners.add(consumer);
- // If current device state is still invalid, we haven't gotten our initial value yet
+ // If current device state is still invalid, the initial value has not been provided.
if (mCurrentDeviceState == INVALID_DEVICE_STATE) {
return;
}
- consumer.accept(getCurrentStatus());
+ consumer.accept(getCurrentRearDisplayModeStatus());
}
}
@@ -108,6 +156,7 @@
* Removes a listener no longer interested in receiving updates.
* @param consumer no longer interested in receiving updates to RearDisplayStatus
*/
+ @Override
public void removeRearDisplayStatusListener(
@NonNull Consumer<@WindowAreaStatus Integer> consumer) {
synchronized (mLock) {
@@ -118,13 +167,17 @@
/**
* Creates and starts a rear display session and provides updates to the
* callback provided. Because this is being called from the OEM provided
- * extensions, we will post the result of the listener on the executor
+ * extensions, the result of the listener will be posted on the executor
* provided by the developer at the initial call site.
*
- * When we enable rear display mode, we submit a request to {@link DeviceStateManager}
+ * Rear display mode moves the calling application to the display on the device that is
+ * facing the same direction as the rear cameras. This would be the cover display on a fold-in
+ * style device when the device is opened.
+ *
+ * When rear display mode is enabled, a request is made to {@link DeviceStateManager}
* to override the device state to the state that corresponds to RearDisplay
- * mode. When the {@link DeviceStateRequest} is activated, we let the
- * consumer know that the session is active by sending
+ * mode. When the {@link DeviceStateRequest} is activated, the provided {@link Consumer} is
+ * notified that the session is active by receiving
* {@link WindowAreaComponent#SESSION_STATE_ACTIVE}.
*
* @param activity to provide updates to the client on
@@ -132,19 +185,20 @@
* @param rearDisplaySessionCallback to provide updates to the client on
* the status of the Session
*/
+ @Override
public void startRearDisplaySession(@NonNull Activity activity,
@NonNull Consumer<@WindowAreaSessionState Integer> rearDisplaySessionCallback) {
synchronized (mLock) {
- if (mDeviceStateRequest != null) {
+ if (mRearDisplayStateRequest != null) {
// Rear display session is already active
throw new IllegalStateException(
"Unable to start new rear display session as one is already active");
}
- mDeviceStateRequest = DeviceStateRequest.newBuilder(mRearDisplayState).build();
+ mRearDisplayStateRequest = DeviceStateRequest.newBuilder(mRearDisplayState).build();
mDeviceStateManager.requestState(
- mDeviceStateRequest,
+ mRearDisplayStateRequest,
mExecutor,
- new DeviceStateRequestCallbackAdapter(rearDisplaySessionCallback)
+ new RearDisplayStateRequestCallbackAdapter(rearDisplaySessionCallback)
);
}
}
@@ -152,13 +206,14 @@
/**
* Ends the current rear display session and provides updates to the
* callback provided. Because this is being called from the OEM provided
- * extensions, we will post the result of the listener on the executor
- * provided by the developer.
+ * extensions, the result of the listener will be posted on the executor
+ * provided by the developer at the initial call site.
*/
+ @Override
public void endRearDisplaySession() {
synchronized (mLock) {
- if (mDeviceStateRequest != null || isRearDisplayActive()) {
- mDeviceStateRequest = null;
+ if (mRearDisplayStateRequest != null || isRearDisplayActive()) {
+ mRearDisplayStateRequest = null;
mDeviceStateManager.cancelStateRequest();
} else {
throw new IllegalStateException(
@@ -167,13 +222,176 @@
}
}
+ /**
+ * Adds a listener interested in receiving updates on the RearDisplayPresentationStatus
+ * of the device. Because this is being called from the OEM provided
+ * extensions, the result of the listener will be posted on the executor
+ * provided by the developer at the initial call site.
+ *
+ * Rear display presentation mode is a feature where an {@link Activity} can present
+ * additional content on a device with a second display that is facing the same direction
+ * as the rear camera (i.e. the cover display on a fold-in style device). The calling
+ * {@link Activity} does not move, whereas in rear display mode it does.
+ *
+ * This listener receives a {@link Pair} with the first item being the
+ * {@link WindowAreaComponent.WindowAreaStatus} that corresponds to the current status of the
+ * feature, and the second being the {@link DisplayMetrics} of the display that would be
+ * presented to when the feature is active.
+ *
+ * Depending on the initial state of the device, the {@link Consumer} will receive either
+ * {@link WindowAreaComponent#STATUS_AVAILABLE} or
+ * {@link WindowAreaComponent#STATUS_UNAVAILABLE} for the status value of the {@link Pair} if
+ * the feature is supported or not in that state respectively. Rear display presentation mode is
+ * currently not supported when the device is folded. When the rear display presentation feature
+ * is triggered, the status is updated to be {@link WindowAreaComponent#STATUS_UNAVAILABLE}.
+ * TODO(b/240727590): Prefix with AREA_
+ *
+ * TODO(b/239833099): Add a STATUS_ACTIVE option to let apps know if a feature is currently
+ * enabled.
+ *
+ * @param consumer {@link Consumer} interested in receiving updates to the status of
+ * rear display presentation mode.
+ */
@Override
- public void onBaseStateChanged(int state) {
+ public void addRearDisplayPresentationStatusListener(
+ @NonNull Consumer<ExtensionWindowAreaStatus> consumer) {
synchronized (mLock) {
- mCurrentDeviceBaseState = state;
- if (state == mCurrentDeviceState) {
- updateStatusConsumers(getCurrentStatus());
+ mRearDisplayPresentationStatusListeners.add(consumer);
+
+ // If current device state is still invalid, the initial value has not been provided
+ if (mCurrentDeviceState == INVALID_DEVICE_STATE) {
+ return;
}
+ @WindowAreaStatus int currentStatus = getCurrentRearDisplayPresentationModeStatus();
+ consumer.accept(
+ new RearDisplayPresentationStatus(currentStatus, getRearDisplayMetrics()));
+ }
+ }
+
+ /**
+ * Removes a listener no longer interested in receiving updates.
+ * @param consumer no longer interested in receiving updates to RearDisplayPresentationStatus
+ */
+ @Override
+ public void removeRearDisplayPresentationStatusListener(
+ @NonNull Consumer<ExtensionWindowAreaStatus> consumer) {
+ synchronized (mLock) {
+ mRearDisplayPresentationStatusListeners.remove(consumer);
+ }
+ }
+
+ /**
+ * Creates and starts a rear display presentation session and sends state updates to the
+ * consumer provided. This consumer will receive a constant represented by
+ * {@link WindowAreaSessionState} to represent the state of the current rear display
+ * session. It will be translated to a more friendly interface in the library.
+ *
+ * Because this is being called from the OEM provided extensions, the library
+ * will post the result of the listener on the executor provided by the developer.
+ *
+ * Rear display presentation mode refers to a feature where an {@link Activity} can present
+ * additional content on a device with a second display that is facing the same direction
+ * as the rear camera (i.e. the cover display on a fold-in style device). The calling
+ * {@link Activity} stays on the user-facing display.
+ *
+ * @param activity that the OEM implementation will use as a base
+ * context and to identify the source display area of the request.
+ * The reference to the activity instance must not be stored in the OEM
+ * implementation to prevent memory leaks.
+ * @param consumer to provide updates to the client on the status of the session
+ * @throws UnsupportedOperationException if this method is called when rear display presentation
+ * mode is not available. This could be to an incompatible device state or when
+ * another process is currently in this mode.
+ */
+ @Override
+ public void startRearDisplayPresentationSession(@NonNull Activity activity,
+ @NonNull Consumer<@WindowAreaSessionState Integer> consumer) {
+ synchronized (mLock) {
+ if (mRearDisplayPresentationController != null) {
+ // Rear display presentation session is already active
+ throw new IllegalStateException(
+ "Unable to start new rear display presentation session as one is already "
+ + "active");
+ }
+ if (getCurrentRearDisplayPresentationModeStatus()
+ != WindowAreaComponent.STATUS_AVAILABLE) {
+ throw new IllegalStateException(
+ "Unable to start new rear display presentation session as the feature is "
+ + "is not currently available");
+ }
+
+ mRearDisplayPresentationController = new RearDisplayPresentationController(activity,
+ stateStatus -> {
+ synchronized (mLock) {
+ if (stateStatus == SESSION_STATE_INACTIVE) {
+ // If the last reported session status was VISIBLE
+ // then the INVISIBLE state should be dispatched before INACTIVE
+ // due to not having a good mechanism to know when
+ // the content is no longer visible before it's fully removed
+ if (getLastReportedRearDisplayPresentationStatus()
+ == SESSION_STATE_VISIBLE) {
+ consumer.accept(SESSION_STATE_INVISIBLE);
+ }
+ mRearDisplayPresentationController = null;
+ }
+ mLastReportedRearDisplayPresentationStatus = stateStatus;
+ consumer.accept(stateStatus);
+ }
+ });
+
+ DeviceStateRequest concurrentDisplayStateRequest = DeviceStateRequest.newBuilder(
+ mConcurrentDisplayState).build();
+ mDeviceStateManager.requestState(
+ concurrentDisplayStateRequest,
+ mExecutor,
+ mRearDisplayPresentationController
+ );
+ }
+ }
+
+ /**
+ * Ends the current rear display presentation session and provides updates to the
+ * callback provided. When this is ended, the presented content from the calling
+ * {@link Activity} will also be removed from the rear facing display.
+ * Because this is being called from the OEM provided extensions, the result of the listener
+ * will be posted on the executor provided by the developer at the initial call site.
+ *
+ * Cancelling the {@link DeviceStateRequest} and exiting the rear display presentation state,
+ * will remove the presentation window from the cover display as the cover display is no longer
+ * enabled.
+ */
+ @Override
+ public void endRearDisplayPresentationSession() {
+ synchronized (mLock) {
+ if (mRearDisplayPresentationController != null) {
+ mDeviceStateManager.cancelStateRequest();
+ } else {
+ throw new IllegalStateException(
+ "Unable to cancel a rear display presentation session as there is no "
+ + "active session");
+ }
+ }
+ }
+
+ @Nullable
+ @Override
+ public ExtensionWindowAreaPresentation getRearDisplayPresentation() {
+ synchronized (mLock) {
+ ExtensionWindowAreaPresentation presentation = null;
+ if (mRearDisplayPresentationController != null) {
+ presentation = mRearDisplayPresentationController.getWindowAreaPresentation();
+ }
+ return presentation;
+ }
+ }
+
+ @Override
+ public void onSupportedStatesChanged(int[] supportedStates) {
+ synchronized (mLock) {
+ mCurrentSupportedDeviceStates = supportedStates;
+ updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus());
+ updateRearDisplayPresentationStatusListeners(
+ getCurrentRearDisplayPresentationModeStatus());
}
}
@@ -181,13 +399,17 @@
public void onStateChanged(int state) {
synchronized (mLock) {
mCurrentDeviceState = state;
- updateStatusConsumers(getCurrentStatus());
+ updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus());
+ updateRearDisplayPresentationStatusListeners(
+ getCurrentRearDisplayPresentationModeStatus());
}
}
+
@GuardedBy("mLock")
- private int getCurrentStatus() {
+ private int getCurrentRearDisplayModeStatus() {
if (mRearDisplaySessionStatus == WindowAreaComponent.SESSION_STATE_ACTIVE
+ || !ArrayUtils.contains(mCurrentSupportedDeviceStates, mRearDisplayState)
|| isRearDisplayActive()) {
return WindowAreaComponent.STATUS_UNAVAILABLE;
}
@@ -196,19 +418,20 @@
/**
* Helper method to determine if a rear display session is currently active by checking
- * if the current device configuration matches that of rear display. This would be true
- * if there is a device override currently active (base state != current state) and the current
- * state is that which corresponds to {@code mRearDisplayState}
- * @return {@code true} if the device is in rear display mode and {@code false} if not
+ * if the current device state is that which corresponds to {@code mRearDisplayState}.
+ *
+ * @return {@code true} if the device is in rear display state {@code false} if not
*/
@GuardedBy("mLock")
private boolean isRearDisplayActive() {
- return (mCurrentDeviceState != mCurrentDeviceBaseState) && (mCurrentDeviceState
- == mRearDisplayState);
+ return mCurrentDeviceState == mRearDisplayState;
}
@GuardedBy("mLock")
- private void updateStatusConsumers(@WindowAreaStatus int windowAreaStatus) {
+ private void updateRearDisplayStatusListeners(@WindowAreaStatus int windowAreaStatus) {
+ if (mRearDisplayState == INVALID_DEVICE_STATE) {
+ return;
+ }
synchronized (mLock) {
for (int i = 0; i < mRearDisplayStatusListeners.size(); i++) {
mRearDisplayStatusListeners.valueAt(i).accept(windowAreaStatus);
@@ -216,26 +439,95 @@
}
}
+ @GuardedBy("mLock")
+ private int getCurrentRearDisplayPresentationModeStatus() {
+ if (mCurrentDeviceState == mConcurrentDisplayState
+ || !ArrayUtils.contains(mCurrentSupportedDeviceStates, mConcurrentDisplayState)
+ || isDeviceFolded()) {
+ return WindowAreaComponent.STATUS_UNAVAILABLE;
+ }
+ return WindowAreaComponent.STATUS_AVAILABLE;
+ }
+
+ @GuardedBy("mLock")
+ private boolean isDeviceFolded() {
+ return ArrayUtils.contains(mFoldedDeviceStates, mCurrentDeviceState);
+ }
+
+ @GuardedBy("mLock")
+ private void updateRearDisplayPresentationStatusListeners(
+ @WindowAreaStatus int windowAreaStatus) {
+ if (mConcurrentDisplayState == INVALID_DEVICE_STATE) {
+ return;
+ }
+ RearDisplayPresentationStatus consumerValue = new RearDisplayPresentationStatus(
+ windowAreaStatus, getRearDisplayMetrics());
+ synchronized (mLock) {
+ for (int i = 0; i < mRearDisplayPresentationStatusListeners.size(); i++) {
+ mRearDisplayPresentationStatusListeners.valueAt(i).accept(consumerValue);
+ }
+ }
+ }
+
+ /**
+ * Returns the{@link DisplayMetrics} associated with the rear facing display. If the rear facing
+ * display was not found in the display list, but we have already computed the
+ * {@link DisplayMetrics} for that display, we return the cached value.
+ *
+ * TODO(b/267563768): Update with guidance from Display team for missing displays.
+ *
+ * @throws IllegalArgumentException if the display is not found and there is no cached
+ * {@link DisplayMetrics} for this display.
+ */
+ @GuardedBy("mLock")
+ private DisplayMetrics getRearDisplayMetrics() {
+ Display[] displays = mDisplayManager.getDisplays(
+ DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED);
+ for (int i = 0; i < displays.length; i++) {
+ DisplayAddress.Physical address =
+ (DisplayAddress.Physical) displays[i].getAddress();
+ if (mRearDisplayAddress == address.getPhysicalDisplayId()) {
+ if (mRearDisplayMetrics == null) {
+ mRearDisplayMetrics = new DisplayMetrics();
+ }
+ displays[i].getRealMetrics(mRearDisplayMetrics);
+ return mRearDisplayMetrics;
+ }
+ }
+ if (mRearDisplayMetrics != null) {
+ return mRearDisplayMetrics;
+ } else {
+ throw new IllegalArgumentException(
+ "No display found with the provided display address");
+ }
+ }
+
+ @GuardedBy("mLock")
+ @WindowAreaSessionState
+ private int getLastReportedRearDisplayPresentationStatus() {
+ return mLastReportedRearDisplayPresentationStatus;
+ }
+
/**
* Callback for the {@link DeviceStateRequest} to be notified of when the request has been
* activated or cancelled. This callback provides information to the client library
* on the status of the RearDisplay session through {@code mRearDisplaySessionCallback}
*/
- private class DeviceStateRequestCallbackAdapter implements DeviceStateRequest.Callback {
+ private class RearDisplayStateRequestCallbackAdapter implements DeviceStateRequest.Callback {
private final Consumer<Integer> mRearDisplaySessionCallback;
- DeviceStateRequestCallbackAdapter(@NonNull Consumer<Integer> callback) {
+ RearDisplayStateRequestCallbackAdapter(@NonNull Consumer<Integer> callback) {
mRearDisplaySessionCallback = callback;
}
@Override
public void onRequestActivated(@NonNull DeviceStateRequest request) {
synchronized (mLock) {
- if (request.equals(mDeviceStateRequest)) {
+ if (request.equals(mRearDisplayStateRequest)) {
mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_ACTIVE;
mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus);
- updateStatusConsumers(getCurrentStatus());
+ updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus());
}
}
}
@@ -243,12 +535,12 @@
@Override
public void onRequestCanceled(DeviceStateRequest request) {
synchronized (mLock) {
- if (request.equals(mDeviceStateRequest)) {
- mDeviceStateRequest = null;
+ if (request.equals(mRearDisplayStateRequest)) {
+ mRearDisplayStateRequest = null;
}
mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE;
mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus);
- updateStatusConsumers(getCurrentStatus());
+ updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus());
}
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index 67963a3..d94e8e4 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -154,7 +154,7 @@
activityOptions);
// Set adjacent to each other so that the containers below will be invisible.
- setAdjacentTaskFragments(wct, launchingFragmentToken, secondaryFragmentToken, rule);
+ setAdjacentTaskFragmentsWithRule(wct, launchingFragmentToken, secondaryFragmentToken, rule);
setCompanionTaskFragment(wct, launchingFragmentToken, secondaryFragmentToken, rule,
false /* isStacked */);
}
@@ -167,7 +167,7 @@
void expandTaskFragment(@NonNull WindowContainerTransaction wct,
@NonNull IBinder fragmentToken) {
resizeTaskFragment(wct, fragmentToken, new Rect());
- setAdjacentTaskFragments(wct, fragmentToken, null /* secondary */, null /* splitRule */);
+ clearAdjacentTaskFragments(wct, fragmentToken);
updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED);
updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT);
}
@@ -238,26 +238,37 @@
wct.reparentActivityToTaskFragment(fragmentToken, reparentActivityToken);
}
- void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
- @NonNull IBinder primary, @Nullable IBinder secondary, @Nullable SplitRule splitRule) {
- if (secondary == null) {
- wct.clearAdjacentTaskFragments(primary);
- return;
- }
-
+ /**
+ * Sets the two given TaskFragments as adjacent to each other with respecting the given
+ * {@link SplitRule} for {@link WindowContainerTransaction.TaskFragmentAdjacentParams}.
+ */
+ void setAdjacentTaskFragmentsWithRule(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder primary, @NonNull IBinder secondary, @NonNull SplitRule splitRule) {
WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams = null;
final boolean finishSecondaryWithPrimary =
- splitRule != null && SplitContainer.shouldFinishSecondaryWithPrimary(splitRule);
+ SplitContainer.shouldFinishSecondaryWithPrimary(splitRule);
final boolean finishPrimaryWithSecondary =
- splitRule != null && SplitContainer.shouldFinishPrimaryWithSecondary(splitRule);
+ SplitContainer.shouldFinishPrimaryWithSecondary(splitRule);
if (finishSecondaryWithPrimary || finishPrimaryWithSecondary) {
adjacentParams = new WindowContainerTransaction.TaskFragmentAdjacentParams();
adjacentParams.setShouldDelayPrimaryLastActivityRemoval(finishSecondaryWithPrimary);
adjacentParams.setShouldDelaySecondaryLastActivityRemoval(finishPrimaryWithSecondary);
}
+ setAdjacentTaskFragments(wct, primary, secondary, adjacentParams);
+ }
+
+ void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder primary, @NonNull IBinder secondary,
+ @Nullable WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams) {
wct.setAdjacentTaskFragments(primary, secondary, adjacentParams);
}
+ void clearAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder fragmentToken) {
+ // Clear primary will also clear secondary.
+ wct.clearAdjacentTaskFragments(fragmentToken);
+ }
+
void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct,
@NonNull IBinder primary, @NonNull IBinder secondary, @NonNull SplitRule splitRule,
boolean isStacked) {
@@ -268,7 +279,7 @@
} else {
finishPrimaryWithSecondary = shouldFinishPrimaryWithSecondary(splitRule);
}
- wct.setCompanionTaskFragment(primary, finishPrimaryWithSecondary ? secondary : null);
+ setCompanionTaskFragment(wct, primary, finishPrimaryWithSecondary ? secondary : null);
final boolean finishSecondaryWithPrimary;
if (isStacked) {
@@ -277,7 +288,12 @@
} else {
finishSecondaryWithPrimary = shouldFinishSecondaryWithPrimary(splitRule);
}
- wct.setCompanionTaskFragment(secondary, finishSecondaryWithPrimary ? primary : null);
+ setCompanionTaskFragment(wct, secondary, finishSecondaryWithPrimary ? primary : null);
+ }
+
+ void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder primary,
+ @Nullable IBinder secondary) {
+ wct.setCompanionTaskFragment(primary, secondary);
}
void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
index 77284c41..18497ad 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
@@ -17,12 +17,15 @@
package androidx.window.extensions.embedding;
import android.app.Activity;
-import android.content.res.Configuration;
+import android.os.Binder;
+import android.os.IBinder;
import android.util.Pair;
import android.util.Size;
+import android.window.TaskFragmentParentInfo;
import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
+import androidx.window.extensions.core.util.function.Function;
/**
* Client-side descriptor of a split that holds two containers.
@@ -34,8 +37,14 @@
private final TaskFragmentContainer mSecondaryContainer;
@NonNull
private final SplitRule mSplitRule;
+ /** @see SplitContainer#getCurrentSplitAttributes() */
@NonNull
- private SplitAttributes mSplitAttributes;
+ private SplitAttributes mCurrentSplitAttributes;
+ /** @see SplitContainer#getDefaultSplitAttributes() */
+ @NonNull
+ private SplitAttributes mDefaultSplitAttributes;
+ @NonNull
+ private final IBinder mToken;
SplitContainer(@NonNull TaskFragmentContainer primaryContainer,
@NonNull Activity primaryActivity,
@@ -45,7 +54,9 @@
mPrimaryContainer = primaryContainer;
mSecondaryContainer = secondaryContainer;
mSplitRule = splitRule;
- mSplitAttributes = splitAttributes;
+ mDefaultSplitAttributes = splitRule.getDefaultSplitAttributes();
+ mCurrentSplitAttributes = splitAttributes;
+ mToken = new Binder("SplitContainer");
if (shouldFinishPrimaryWithSecondary(splitRule)) {
if (mPrimaryContainer.getRunningActivityCount() == 1
@@ -78,19 +89,60 @@
return mSplitRule;
}
+ /**
+ * Returns the current {@link SplitAttributes} this {@code SplitContainer} is showing.
+ * <p>
+ * If the {@code SplitAttributes} calculator function is not set by
+ * {@link SplitController#setSplitAttributesCalculator(Function)}, the current
+ * {@code SplitAttributes} is either to expand the containers if the size constraints of
+ * {@link #getSplitRule()} are not satisfied,
+ * or the {@link #getDefaultSplitAttributes()}, otherwise.
+ * </p><p>
+ * If the {@code SplitAttributes} calculator function is set, the current
+ * {@code SplitAttributes} will be customized by the function, which can be any
+ * {@code SplitAttributes}.
+ * </p>
+ *
+ * @see SplitAttributes.SplitType.ExpandContainersSplitType
+ */
@NonNull
- SplitAttributes getSplitAttributes() {
- return mSplitAttributes;
+ SplitAttributes getCurrentSplitAttributes() {
+ return mCurrentSplitAttributes;
+ }
+
+ /**
+ * Returns the default {@link SplitAttributes} when the parent task container bounds satisfy
+ * {@link #getSplitRule()} constraints.
+ * <p>
+ * The value is usually from {@link SplitRule#getDefaultSplitAttributes} unless it is overridden
+ * by {@link SplitController#updateSplitAttributes(IBinder, SplitAttributes)}.
+ */
+ @NonNull
+ SplitAttributes getDefaultSplitAttributes() {
+ return mDefaultSplitAttributes;
+ }
+
+ @NonNull
+ IBinder getToken() {
+ return mToken;
}
/**
* Updates the {@link SplitAttributes} to this container.
* It is usually used when there's a folding state change or
- * {@link SplitController#onTaskFragmentParentInfoChanged(WindowContainerTransaction, int,
- * Configuration)}.
+ * {@link SplitController#onTaskFragmentParentInfoChanged(WindowContainerTransaction,
+ * int, TaskFragmentParentInfo)}.
*/
- void setSplitAttributes(@NonNull SplitAttributes splitAttributes) {
- mSplitAttributes = splitAttributes;
+ void updateCurrentSplitAttributes(@NonNull SplitAttributes splitAttributes) {
+ mCurrentSplitAttributes = splitAttributes;
+ }
+
+ /**
+ * Overrides the default {@link SplitAttributes} to this container, which may be different
+ * from {@link SplitRule#getDefaultSplitAttributes}.
+ */
+ void updateDefaultSplitAttributes(@NonNull SplitAttributes splitAttributes) {
+ mDefaultSplitAttributes = splitAttributes;
}
@NonNull
@@ -112,7 +164,7 @@
@NonNull
SplitInfo toSplitInfo() {
return new SplitInfo(mPrimaryContainer.toActivityStack(),
- mSecondaryContainer.toActivityStack(), mSplitAttributes);
+ mSecondaryContainer.toActivityStack(), mCurrentSplitAttributes, mToken);
}
static boolean shouldFinishPrimaryWithSecondary(@NonNull SplitRule splitRule) {
@@ -171,9 +223,10 @@
public String toString() {
return "SplitContainer{"
+ " primaryContainer=" + mPrimaryContainer
- + " secondaryContainer=" + mSecondaryContainer
- + " splitRule=" + mSplitRule
- + " splitAttributes" + mSplitAttributes
+ + ", secondaryContainer=" + mSecondaryContainer
+ + ", splitRule=" + mSplitRule
+ + ", currentSplitAttributes" + mCurrentSplitAttributes
+ + ", defaultSplitAttributes" + mDefaultSplitAttributes
+ "}";
}
}
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 57ba6bb..2c1ddf7 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -87,6 +87,7 @@
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -230,6 +231,14 @@
return mSplitAttributesCalculator;
}
+ @Override
+ @NonNull
+ public ActivityOptions setLaunchingActivityStack(@NonNull ActivityOptions options,
+ @NonNull IBinder token) {
+ options.setLaunchTaskFragmentToken(token);
+ return options;
+ }
+
@NonNull
@GuardedBy("mLock")
@VisibleForTesting
@@ -271,6 +280,98 @@
}
}
+ @Override
+ public void finishActivityStacks(@NonNull Set<IBinder> activityStackTokens) {
+ if (activityStackTokens.isEmpty()) {
+ return;
+ }
+ synchronized (mLock) {
+ // Translate ActivityStack to TaskFragmentContainer.
+ final List<TaskFragmentContainer> pendingFinishingContainers =
+ activityStackTokens.stream()
+ .map(token -> {
+ synchronized (mLock) {
+ return getContainer(token);
+ }
+ }).filter(Objects::nonNull)
+ .toList();
+
+ if (pendingFinishingContainers.isEmpty()) {
+ return;
+ }
+ // Start transaction with close transit type.
+ final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
+ transactionRecord.setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE);
+ final WindowContainerTransaction wct = transactionRecord.getTransaction();
+
+ forAllTaskContainers(taskContainer -> {
+ synchronized (mLock) {
+ final List<TaskFragmentContainer> containers = taskContainer.mContainers;
+ // Clean up the TaskFragmentContainers by the z-order from the lowest.
+ for (int i = 0; i < containers.size() - 1; i++) {
+ final TaskFragmentContainer container = containers.get(i);
+ if (pendingFinishingContainers.contains(container)) {
+ // Don't update records here to prevent double invocation.
+ container.finish(false /* shouldFinishDependant */, mPresenter,
+ wct, this, false /* shouldRemoveRecord */);
+ }
+ }
+ // Remove container records.
+ removeContainers(taskContainer, pendingFinishingContainers);
+ // Update the change to the client side.
+ updateContainersInTaskIfVisible(wct, taskContainer.getTaskId());
+ }
+ });
+
+ // Apply the transaction.
+ transactionRecord.apply(false /* shouldApplyIndependently */);
+ }
+ }
+
+ @Override
+ public void invalidateTopVisibleSplitAttributes() {
+ synchronized (mLock) {
+ WindowContainerTransaction wct = mTransactionManager.startNewTransaction()
+ .getTransaction();
+ forAllTaskContainers(taskContainer -> {
+ synchronized (mLock) {
+ updateContainersInTaskIfVisible(wct, taskContainer.getTaskId());
+ }
+ });
+ mTransactionManager.getCurrentTransactionRecord()
+ .apply(false /* shouldApplyIndependently */);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void forAllTaskContainers(@NonNull Consumer<TaskContainer> callback) {
+ for (int i = mTaskContainers.size() - 1; i >= 0; --i) {
+ callback.accept(mTaskContainers.valueAt(i));
+ }
+ }
+
+ @Override
+ public void updateSplitAttributes(@NonNull IBinder splitInfoToken,
+ @NonNull SplitAttributes splitAttributes) {
+ synchronized (mLock) {
+ final SplitContainer splitContainer = getSplitContainer(splitInfoToken);
+ if (splitContainer == null) {
+ Log.w(TAG, "Cannot find SplitContainer for token:" + splitInfoToken);
+ return;
+ }
+ WindowContainerTransaction wct = mTransactionManager.startNewTransaction()
+ .getTransaction();
+ if (updateSplitContainerIfNeeded(splitContainer, wct, splitAttributes)) {
+ splitContainer.updateDefaultSplitAttributes(splitAttributes);
+ mTransactionManager.getCurrentTransactionRecord()
+ .apply(false /* shouldApplyIndependently */);
+ } else {
+ // Abort if the SplitContainer wasn't updated.
+ mTransactionManager.getCurrentTransactionRecord().abort();
+ }
+ }
+ }
+
/**
* Called when the transaction is ready so that the organizer can update the TaskFragments based
* on the changes in transaction.
@@ -424,6 +525,9 @@
// All overrides will be cleanup.
container.setLastRequestedBounds(null /* bounds */);
container.setLastRequestedWindowingMode(WINDOWING_MODE_UNDEFINED);
+ container.clearLastAdjacentTaskFragment();
+ container.setLastCompanionTaskFragment(null /* fragmentToken */);
+ container.setLastRequestAnimationParams(TaskFragmentAnimationParams.DEFAULT);
cleanupForEnterPip(wct, container);
} else if (wasInPip) {
// Exit PIP.
@@ -640,35 +744,6 @@
}
}
- /** Returns whether the given {@link TaskContainer} may show in split. */
- // Suppress GuardedBy warning because lint asks to mark this method as
- // @GuardedBy(mPresenter.mController.mLock), which is mLock itself
- @SuppressWarnings("GuardedBy")
- @GuardedBy("mLock")
- private boolean mayShowSplit(@NonNull TaskContainer taskContainer) {
- // No split inside PIP.
- if (taskContainer.isInPictureInPicture()) {
- return false;
- }
- // Always assume the TaskContainer if SplitAttributesCalculator is set
- if (mSplitAttributesCalculator != null) {
- return true;
- }
- // Check if the parent container bounds can support any split rule.
- for (EmbeddingRule rule : mSplitRules) {
- if (!(rule instanceof SplitRule)) {
- continue;
- }
- final SplitRule splitRule = (SplitRule) rule;
- final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes(
- taskContainer.getTaskProperties(), splitRule, null /* minDimensionsPair */);
- if (shouldShowSplit(splitAttributes)) {
- return true;
- }
- }
- return false;
- }
-
@VisibleForTesting
@GuardedBy("mLock")
void onActivityCreated(@NonNull WindowContainerTransaction wct,
@@ -1352,20 +1427,33 @@
* Removes the container from bookkeeping records.
*/
void removeContainer(@NonNull TaskFragmentContainer container) {
+ removeContainers(container.getTaskContainer(), Collections.singletonList(container));
+ }
+
+ /**
+ * Removes containers from bookkeeping records.
+ */
+ void removeContainers(@NonNull TaskContainer taskContainer,
+ @NonNull List<TaskFragmentContainer> containers) {
// Remove all split containers that included this one
- final TaskContainer taskContainer = container.getTaskContainer();
- taskContainer.mContainers.remove(container);
+ taskContainer.mContainers.removeAll(containers);
// Marked as a pending removal which will be removed after it is actually removed on the
// server side (#onTaskFragmentVanished).
// In this way, we can keep track of the Task bounds until we no longer have any
// TaskFragment there.
- taskContainer.mFinishedContainer.add(container.getTaskFragmentToken());
+ taskContainer.mFinishedContainer.addAll(containers.stream().map(
+ TaskFragmentContainer::getTaskFragmentToken).toList());
// Cleanup any split references.
final List<SplitContainer> containersToRemove = new ArrayList<>();
for (SplitContainer splitContainer : taskContainer.mSplitContainers) {
- if (container.equals(splitContainer.getSecondaryContainer())
- || container.equals(splitContainer.getPrimaryContainer())) {
+ if (containersToRemove.contains(splitContainer)) {
+ // Don't need to check because it has been in the remove list.
+ continue;
+ }
+ if (containers.stream().anyMatch(container ->
+ splitContainer.getPrimaryContainer().equals(container)
+ || splitContainer.getSecondaryContainer().equals(container))) {
containersToRemove.add(splitContainer);
}
}
@@ -1373,7 +1461,7 @@
// Cleanup any dependent references.
for (TaskFragmentContainer containerToUpdate : taskContainer.mContainers) {
- containerToUpdate.removeContainerToFinishOnExit(container);
+ containerToUpdate.removeContainersToFinishOnExit(containers);
}
}
@@ -1453,26 +1541,53 @@
if (splitContainer == null) {
return;
}
+
+ updateSplitContainerIfNeeded(splitContainer, wct, null /* splitAttributes */);
+ }
+
+ /**
+ * Updates {@link SplitContainer} with the given {@link SplitAttributes} if the
+ * {@link SplitContainer} is the top most and not finished. If passed {@link SplitAttributes}
+ * are {@code null}, the {@link SplitAttributes} will be calculated with
+ * {@link SplitPresenter#computeSplitAttributes(TaskContainer.TaskProperties, SplitRule, Pair)}.
+ *
+ * @param splitContainer The {@link SplitContainer} to update
+ * @param splitAttributes Update with this {@code splitAttributes} if it is not {@code null}.
+ * Otherwise, use the value calculated by
+ * {@link SplitPresenter#computeSplitAttributes(
+ * TaskContainer.TaskProperties, SplitRule, Pair)}
+ *
+ * @return {@code true} if the update succeed. Otherwise, returns {@code false}.
+ */
+ @GuardedBy("mLock")
+ private boolean updateSplitContainerIfNeeded(@NonNull SplitContainer splitContainer,
+ @NonNull WindowContainerTransaction wct, @Nullable SplitAttributes splitAttributes) {
if (!isTopMostSplit(splitContainer)) {
// Skip position update - it isn't the topmost split.
- return;
+ return false;
}
if (splitContainer.getPrimaryContainer().isFinished()
|| splitContainer.getSecondaryContainer().isFinished()) {
// Skip position update - one or both containers are finished.
- return;
+ return false;
}
- final TaskContainer taskContainer = splitContainer.getTaskContainer();
- final SplitRule splitRule = splitContainer.getSplitRule();
- final Pair<Size, Size> minDimensionsPair = splitContainer.getMinDimensionsPair();
- final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes(
- taskContainer.getTaskProperties(), splitRule, minDimensionsPair);
- splitContainer.setSplitAttributes(splitAttributes);
+ if (splitAttributes == null) {
+ final TaskContainer.TaskProperties taskProperties = splitContainer.getTaskContainer()
+ .getTaskProperties();
+ final SplitRule splitRule = splitContainer.getSplitRule();
+ final SplitAttributes defaultSplitAttributes = splitContainer
+ .getDefaultSplitAttributes();
+ final Pair<Size, Size> minDimensionsPair = splitContainer.getMinDimensionsPair();
+ splitAttributes = mPresenter.computeSplitAttributes(taskProperties, splitRule,
+ defaultSplitAttributes, minDimensionsPair);
+ }
+ splitContainer.updateCurrentSplitAttributes(splitAttributes);
if (dismissPlaceholderIfNecessary(wct, splitContainer)) {
// Placeholder was finished, the positions will be updated when its container is emptied
- return;
+ return true;
}
- mPresenter.updateSplitContainer(splitContainer, container, wct);
+ mPresenter.updateSplitContainer(splitContainer, wct);
+ return true;
}
/** Whether the given split is the topmost split in the Task. */
@@ -1568,7 +1683,7 @@
final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(activity,
placeholderRule.getPlaceholderIntent());
final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes(taskProperties,
- placeholderRule, minDimensionsPair);
+ placeholderRule, placeholderRule.getDefaultSplitAttributes(), minDimensionsPair);
if (!SplitPresenter.shouldShowSplit(splitAttributes)) {
return false;
}
@@ -1647,7 +1762,7 @@
// The placeholder should remain after it was first shown.
return false;
}
- final SplitAttributes splitAttributes = splitContainer.getSplitAttributes();
+ final SplitAttributes splitAttributes = splitContainer.getCurrentSplitAttributes();
if (SplitPresenter.shouldShowSplit(splitAttributes)) {
return false;
}
@@ -1791,6 +1906,20 @@
@Nullable
@GuardedBy("mLock")
+ SplitContainer getSplitContainer(@NonNull IBinder token) {
+ for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
+ final List<SplitContainer> containers = mTaskContainers.valueAt(i).mSplitContainers;
+ for (SplitContainer container : containers) {
+ if (container.getToken().equals(token)) {
+ return container;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ @GuardedBy("mLock")
TaskContainer getTaskContainer(int taskId) {
return mTaskContainers.get(taskId);
}
@@ -2064,6 +2193,7 @@
transactionRecord.apply(false /* shouldApplyIndependently */);
// Amend the request to let the WM know that the activity should be placed in
// the dedicated container.
+ // TODO(b/229680885): skip override launching TaskFragment token by split-rule
options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
launchedInTaskFragment.getTaskFragmentToken());
mCurrentIntent = intent;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 2b93682..0408511 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -179,7 +179,7 @@
final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(
primaryActivity, secondaryIntent);
final SplitAttributes splitAttributes = computeSplitAttributes(taskProperties, rule,
- minDimensionsPair);
+ rule.getDefaultSplitAttributes(), minDimensionsPair);
final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties,
splitAttributes);
final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
@@ -225,7 +225,7 @@
final Pair<Size, Size> minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity,
secondaryActivity);
final SplitAttributes splitAttributes = computeSplitAttributes(taskProperties, rule,
- minDimensionsPair);
+ rule.getDefaultSplitAttributes(), minDimensionsPair);
final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties,
splitAttributes);
final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
@@ -334,11 +334,9 @@
/**
* Updates the positions of containers in an existing split.
* @param splitContainer The split container to be updated.
- * @param updatedContainer The task fragment that was updated and caused this split update.
* @param wct WindowContainerTransaction that this update should be performed with.
*/
void updateSplitContainer(@NonNull SplitContainer splitContainer,
- @NonNull TaskFragmentContainer updatedContainer,
@NonNull WindowContainerTransaction wct) {
// Getting the parent configuration using the updated container - it will have the recent
// value.
@@ -348,8 +346,9 @@
if (activity == null) {
return;
}
- final TaskProperties taskProperties = getTaskProperties(updatedContainer);
- final SplitAttributes splitAttributes = splitContainer.getSplitAttributes();
+ final TaskContainer taskContainer = splitContainer.getTaskContainer();
+ final TaskProperties taskProperties = taskContainer.getTaskProperties();
+ final SplitAttributes splitAttributes = splitContainer.getCurrentSplitAttributes();
final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties,
splitAttributes);
final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties,
@@ -370,7 +369,6 @@
// When placeholder is shown in split, we should keep the focus on the primary.
wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken());
}
- final TaskContainer taskContainer = updatedContainer.getTaskContainer();
final int windowingMode = taskContainer.getWindowingModeForSplitTaskFragment(
primaryRelBounds);
updateTaskFragmentWindowingModeIfRegistered(wct, primaryContainer, windowingMode);
@@ -387,10 +385,9 @@
// secondaryContainer could not be finished.
boolean isStacked = !shouldShowSplit(splitAttributes);
if (isStacked) {
- setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
- null /* secondary */, null /* splitRule */);
+ clearAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken());
} else {
- setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
+ setAdjacentTaskFragmentsWithRule(wct, primaryContainer.getTaskFragmentToken(),
secondaryContainer.getTaskFragmentToken(), splitRule);
}
setCompanionTaskFragment(wct, primaryContainer.getTaskFragmentToken(),
@@ -427,7 +424,7 @@
fragmentOptions.getFragmentToken());
if (container == null) {
throw new IllegalStateException(
- "Creating a task fragment that is not registered with controller.");
+ "Creating a TaskFragment that is not registered with controller.");
}
container.setLastRequestedBounds(fragmentOptions.getInitialRelativeBounds());
@@ -441,7 +438,7 @@
TaskFragmentContainer container = mController.getContainer(fragmentToken);
if (container == null) {
throw new IllegalStateException(
- "Resizing a task fragment that is not registered with controller.");
+ "Resizing a TaskFragment that is not registered with controller.");
}
if (container.areLastRequestedBoundsEqual(relBounds)) {
@@ -458,7 +455,7 @@
@NonNull IBinder fragmentToken, @WindowingMode int windowingMode) {
final TaskFragmentContainer container = mController.getContainer(fragmentToken);
if (container == null) {
- throw new IllegalStateException("Setting windowing mode for a task fragment that is"
+ throw new IllegalStateException("Setting windowing mode for a TaskFragment that is"
+ " not registered with controller.");
}
@@ -476,7 +473,7 @@
@NonNull IBinder fragmentToken, @NonNull TaskFragmentAnimationParams animationParams) {
final TaskFragmentContainer container = mController.getContainer(fragmentToken);
if (container == null) {
- throw new IllegalStateException("Setting animation params for a task fragment that is"
+ throw new IllegalStateException("Setting animation params for a TaskFragment that is"
+ " not registered with controller.");
}
@@ -489,6 +486,64 @@
super.updateAnimationParams(wct, fragmentToken, animationParams);
}
+ @Override
+ void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder primary, @NonNull IBinder secondary,
+ @Nullable WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams) {
+ final TaskFragmentContainer primaryContainer = mController.getContainer(primary);
+ final TaskFragmentContainer secondaryContainer = mController.getContainer(secondary);
+ if (primaryContainer == null || secondaryContainer == null) {
+ throw new IllegalStateException("setAdjacentTaskFragments on TaskFragment that is"
+ + " not registered with controller.");
+ }
+
+ if (primaryContainer.isLastAdjacentTaskFragmentEqual(secondary, adjacentParams)
+ && secondaryContainer.isLastAdjacentTaskFragmentEqual(primary, adjacentParams)) {
+ // Return early if the same adjacent TaskFragments were already requested
+ return;
+ }
+
+ primaryContainer.setLastAdjacentTaskFragment(secondary, adjacentParams);
+ secondaryContainer.setLastAdjacentTaskFragment(primary, adjacentParams);
+ super.setAdjacentTaskFragments(wct, primary, secondary, adjacentParams);
+ }
+
+ @Override
+ void clearAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder fragmentToken) {
+ final TaskFragmentContainer container = mController.getContainer(fragmentToken);
+ if (container == null) {
+ throw new IllegalStateException("clearAdjacentTaskFragments on TaskFragment that is"
+ + " not registered with controller.");
+ }
+
+ if (container.isLastAdjacentTaskFragmentEqual(null /* fragmentToken*/, null /* params */)) {
+ // Return early if no adjacent TaskFragment was yet requested
+ return;
+ }
+
+ container.clearLastAdjacentTaskFragment();
+ super.clearAdjacentTaskFragments(wct, fragmentToken);
+ }
+
+ @Override
+ void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder primary,
+ @Nullable IBinder secondary) {
+ final TaskFragmentContainer container = mController.getContainer(primary);
+ if (container == null) {
+ throw new IllegalStateException("setCompanionTaskFragment on TaskFragment that is"
+ + " not registered with controller.");
+ }
+
+ if (container.isLastCompanionTaskFragmentEqual(secondary)) {
+ // Return early if the same companion TaskFragment was already requested
+ return;
+ }
+
+ container.setLastCompanionTaskFragment(secondary);
+ super.setCompanionTaskFragment(wct, primary, secondary);
+ }
+
/**
* Expands the split container if the current split bounds are smaller than the Activity or
* Intent that is added to the container.
@@ -515,9 +570,9 @@
// Expand the splitContainer if minimum dimensions are not satisfied.
final TaskContainer taskContainer = splitContainer.getTaskContainer();
final SplitAttributes splitAttributes = sanitizeSplitAttributes(
- taskContainer.getTaskProperties(), splitContainer.getSplitAttributes(),
+ taskContainer.getTaskProperties(), splitContainer.getCurrentSplitAttributes(),
minDimensionsPair);
- splitContainer.setSplitAttributes(splitAttributes);
+ splitContainer.updateCurrentSplitAttributes(splitAttributes);
if (!shouldShowSplit(splitAttributes)) {
// If the client side hasn't received TaskFragmentInfo yet, we can't change TaskFragment
// bounds. Return failure to create a new SplitContainer which fills task bounds.
@@ -540,7 +595,7 @@
}
static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) {
- return shouldShowSplit(splitContainer.getSplitAttributes());
+ return shouldShowSplit(splitContainer.getCurrentSplitAttributes());
}
static boolean shouldShowSplit(@NonNull SplitAttributes splitAttributes) {
@@ -549,12 +604,12 @@
@NonNull
SplitAttributes computeSplitAttributes(@NonNull TaskProperties taskProperties,
- @NonNull SplitRule rule, @Nullable Pair<Size, Size> minDimensionsPair) {
+ @NonNull SplitRule rule, @NonNull SplitAttributes defaultSplitAttributes,
+ @Nullable Pair<Size, Size> minDimensionsPair) {
final Configuration taskConfiguration = taskProperties.getConfiguration();
final WindowMetrics taskWindowMetrics = getTaskWindowMetrics(taskConfiguration);
final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator =
mController.getSplitAttributesCalculator();
- final SplitAttributes defaultSplitAttributes = rule.getDefaultSplitAttributes();
final boolean areDefaultConstraintsSatisfied = rule.checkParentMetrics(taskWindowMetrics);
if (calculator == null) {
if (!areDefaultConstraintsSatisfied) {
@@ -957,11 +1012,6 @@
}
@NonNull
- static TaskProperties getTaskProperties(@NonNull TaskFragmentContainer container) {
- return container.getTaskContainer().getTaskProperties();
- }
-
- @NonNull
TaskProperties getTaskProperties(@NonNull Activity activity) {
final TaskContainer taskContainer = mController.getTaskContainer(
mController.getTaskId(activity));
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index f41295b..4b15bb1 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -108,12 +108,6 @@
}
@NonNull
- Configuration getConfiguration() {
- // Make a copy in case the config is updated unexpectedly.
- return new Configuration(mConfiguration);
- }
-
- @NonNull
TaskProperties getTaskProperties() {
return new TaskProperties(mDisplayId, mConfiguration);
}
@@ -157,7 +151,7 @@
@WindowingMode
private int getWindowingMode() {
- return getConfiguration().windowConfiguration.getWindowingMode();
+ return mConfiguration.windowConfiguration.getWindowingMode();
}
/** Whether there is any {@link TaskFragmentContainer} below this Task. */
@@ -220,10 +214,7 @@
}
}
- /**
- * A wrapper class which contains the display ID and {@link Configuration} of a
- * {@link TaskContainer}
- */
+ /** A wrapper class which contains the information of {@link TaskContainer} */
static final class TaskProperties {
private final int mDisplayId;
@NonNull
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 861cb49..b38f824 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -37,8 +37,10 @@
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Iterator;
import java.util.List;
+import java.util.Objects;
/**
* Client-side container for a stack of activities. Corresponds to an instance of TaskFragment
@@ -116,6 +118,27 @@
private TaskFragmentAnimationParams mLastAnimationParams = TaskFragmentAnimationParams.DEFAULT;
/**
+ * TaskFragment token that was requested last via
+ * {@link android.window.TaskFragmentOperation#OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS}.
+ */
+ @Nullable
+ private IBinder mLastAdjacentTaskFragment;
+
+ /**
+ * {@link WindowContainerTransaction.TaskFragmentAdjacentParams} token that was requested last
+ * via {@link android.window.TaskFragmentOperation#OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS}.
+ */
+ @Nullable
+ private WindowContainerTransaction.TaskFragmentAdjacentParams mLastAdjacentParams;
+
+ /**
+ * TaskFragment token that was requested last via
+ * {@link android.window.TaskFragmentOperation#OP_TYPE_SET_COMPANION_TASK_FRAGMENT}.
+ */
+ @Nullable
+ private IBinder mLastCompanionTaskFragment;
+
+ /**
* When the TaskFragment has appeared in server, but is empty, we should remove the TaskFragment
* if it is still empty after the timeout.
*/
@@ -243,7 +266,7 @@
@NonNull
ActivityStack toActivityStack() {
- return new ActivityStack(collectNonFinishingActivities(), isEmpty());
+ return new ActivityStack(collectNonFinishingActivities(), isEmpty(), mToken);
}
/** Adds the activity that will be reparented to this container. */
@@ -436,10 +459,17 @@
* Removes a container that should be finished when this container is finished.
*/
void removeContainerToFinishOnExit(@NonNull TaskFragmentContainer containerToRemove) {
+ removeContainersToFinishOnExit(Collections.singletonList(containerToRemove));
+ }
+
+ /**
+ * Removes container list that should be finished when this container is finished.
+ */
+ void removeContainersToFinishOnExit(@NonNull List<TaskFragmentContainer> containersToRemove) {
if (mIsFinished) {
return;
}
- mContainersToFinishOnExit.remove(containerToRemove);
+ mContainersToFinishOnExit.removeAll(containersToRemove);
}
/**
@@ -478,6 +508,16 @@
@GuardedBy("mController.mLock")
void finish(boolean shouldFinishDependent, @NonNull SplitPresenter presenter,
@NonNull WindowContainerTransaction wct, @NonNull SplitController controller) {
+ finish(shouldFinishDependent, presenter, wct, controller, true /* shouldRemoveRecord */);
+ }
+
+ /**
+ * Removes all activities that belong to this process and finishes other containers/activities
+ * configured to finish together.
+ */
+ void finish(boolean shouldFinishDependent, @NonNull SplitPresenter presenter,
+ @NonNull WindowContainerTransaction wct, @NonNull SplitController controller,
+ boolean shouldRemoveRecord) {
if (!mIsFinished) {
mIsFinished = true;
if (mAppearEmptyTimeout != null) {
@@ -494,8 +534,10 @@
// Cleanup the visuals
presenter.deleteTaskFragment(wct, getTaskFragmentToken());
- // Cleanup the records
- controller.removeContainer(this);
+ if (shouldRemoveRecord) {
+ // Cleanup the records
+ controller.removeContainer(this);
+ }
// Clean up task fragment information
mInfo = null;
}
@@ -571,6 +613,7 @@
/**
* Checks if last requested bounds are equal to the provided value.
* The requested bounds are relative bounds in parent coordinate.
+ * @see WindowContainerTransaction#setRelativeBounds
*/
boolean areLastRequestedBoundsEqual(@Nullable Rect relBounds) {
return (relBounds == null && mLastRequestedBounds.isEmpty())
@@ -580,6 +623,7 @@
/**
* Updates the last requested bounds.
* The requested bounds are relative bounds in parent coordinate.
+ * @see WindowContainerTransaction#setRelativeBounds
*/
void setLastRequestedBounds(@Nullable Rect relBounds) {
if (relBounds == null) {
@@ -589,13 +633,9 @@
}
}
- @NonNull
- Rect getLastRequestedBounds() {
- return mLastRequestedBounds;
- }
-
/**
* Checks if last requested windowing mode is equal to the provided value.
+ * @see WindowContainerTransaction#setWindowingMode
*/
boolean isLastRequestedWindowingModeEqual(@WindowingMode int windowingMode) {
return mLastRequestedWindowingMode == windowingMode;
@@ -603,6 +643,7 @@
/**
* Updates the last requested windowing mode.
+ * @see WindowContainerTransaction#setWindowingMode
*/
void setLastRequestedWindowingMode(@WindowingMode int windowingModes) {
mLastRequestedWindowingMode = windowingModes;
@@ -610,6 +651,7 @@
/**
* Checks if last requested {@link TaskFragmentAnimationParams} are equal to the provided value.
+ * @see android.window.TaskFragmentOperation#OP_TYPE_SET_ANIMATION_PARAMS
*/
boolean areLastRequestedAnimationParamsEqual(
@NonNull TaskFragmentAnimationParams animationParams) {
@@ -618,11 +660,66 @@
/**
* Updates the last requested {@link TaskFragmentAnimationParams}.
+ * @see android.window.TaskFragmentOperation#OP_TYPE_SET_ANIMATION_PARAMS
*/
void setLastRequestAnimationParams(@NonNull TaskFragmentAnimationParams animationParams) {
mLastAnimationParams = animationParams;
}
+ /**
+ * Checks if last requested adjacent TaskFragment token and params are equal to the provided
+ * values.
+ * @see android.window.TaskFragmentOperation#OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS
+ * @see android.window.TaskFragmentOperation#OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS
+ */
+ boolean isLastAdjacentTaskFragmentEqual(@Nullable IBinder fragmentToken,
+ @Nullable WindowContainerTransaction.TaskFragmentAdjacentParams params) {
+ return Objects.equals(mLastAdjacentTaskFragment, fragmentToken)
+ && Objects.equals(mLastAdjacentParams, params);
+ }
+
+ /**
+ * Updates the last requested adjacent TaskFragment token and params.
+ * @see android.window.TaskFragmentOperation#OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS
+ */
+ void setLastAdjacentTaskFragment(@NonNull IBinder fragmentToken,
+ @NonNull WindowContainerTransaction.TaskFragmentAdjacentParams params) {
+ mLastAdjacentTaskFragment = fragmentToken;
+ mLastAdjacentParams = params;
+ }
+
+ /**
+ * Clears the last requested adjacent TaskFragment token and params.
+ * @see android.window.TaskFragmentOperation#OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS
+ */
+ void clearLastAdjacentTaskFragment() {
+ final TaskFragmentContainer lastAdjacentTaskFragment = mLastAdjacentTaskFragment != null
+ ? mController.getContainer(mLastAdjacentTaskFragment)
+ : null;
+ mLastAdjacentTaskFragment = null;
+ mLastAdjacentParams = null;
+ if (lastAdjacentTaskFragment != null) {
+ // Clear the previous adjacent TaskFragment as well.
+ lastAdjacentTaskFragment.clearLastAdjacentTaskFragment();
+ }
+ }
+
+ /**
+ * Checks if last requested companion TaskFragment token is equal to the provided value.
+ * @see android.window.TaskFragmentOperation#OP_TYPE_SET_COMPANION_TASK_FRAGMENT
+ */
+ boolean isLastCompanionTaskFragmentEqual(@Nullable IBinder fragmentToken) {
+ return Objects.equals(mLastCompanionTaskFragment, fragmentToken);
+ }
+
+ /**
+ * Updates the last requested companion TaskFragment token.
+ * @see android.window.TaskFragmentOperation#OP_TYPE_SET_COMPANION_TASK_FRAGMENT
+ */
+ void setLastCompanionTaskFragment(@Nullable IBinder fragmentToken) {
+ mLastCompanionTaskFragment = fragmentToken;
+ }
+
/** Gets the parent leaf Task id. */
int getTaskId() {
return mTaskContainer.getTaskId();
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index a26311e..17909d4 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -275,7 +275,8 @@
assertNotNull(tf);
assertNotNull(taskContainer);
- assertEquals(TASK_BOUNDS, taskContainer.getConfiguration().windowConfiguration.getBounds());
+ assertEquals(TASK_BOUNDS, taskContainer.getTaskProperties().getConfiguration()
+ .windowConfiguration.getBounds());
}
@Test
@@ -288,7 +289,7 @@
doReturn(true).when(tf).isEmpty();
doReturn(true).when(mSplitController).launchPlaceholderIfNecessary(mTransaction,
mActivity, false /* isOnCreated */);
- doNothing().when(mSplitPresenter).updateSplitContainer(any(), any(), any());
+ doNothing().when(mSplitPresenter).updateSplitContainer(any(), any());
mSplitController.updateContainer(mTransaction, tf);
@@ -341,7 +342,7 @@
mSplitController.updateContainer(mTransaction, tf);
- verify(mSplitPresenter, never()).updateSplitContainer(any(), any(), any());
+ verify(mSplitPresenter, never()).updateSplitContainer(any(), any());
// Verify if the top active split is updated if both of its containers are not finished.
doReturn(false).when(mSplitController)
@@ -349,7 +350,7 @@
mSplitController.updateContainer(mTransaction, tf);
- verify(mSplitPresenter).updateSplitContainer(splitContainer, tf, mTransaction);
+ verify(mSplitPresenter).updateSplitContainer(splitContainer, mTransaction);
}
@Test
@@ -366,14 +367,14 @@
doReturn(false).when(taskContainer).isVisible();
mSplitController.updateContainer(mTransaction, taskFragmentContainer);
- verify(mSplitPresenter, never()).updateSplitContainer(any(), any(), any());
+ verify(mSplitPresenter, never()).updateSplitContainer(any(), any());
// Update the split when the Task is visible.
doReturn(true).when(taskContainer).isVisible();
mSplitController.updateContainer(mTransaction, taskFragmentContainer);
verify(mSplitPresenter).updateSplitContainer(taskContainer.mSplitContainers.get(0),
- taskFragmentContainer, mTransaction);
+ mTransaction);
}
@Test
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
index a41e63f..8330156 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
@@ -177,6 +177,64 @@
}
@Test
+ public void testSetAdjacentTaskFragments() {
+ final TaskFragmentContainer container0 = mController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer container1 = mController.newContainer(mActivity, TASK_ID);
+
+ mPresenter.setAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken(),
+ container1.getTaskFragmentToken(), null /* adjacentParams */);
+ verify(mTransaction).setAdjacentTaskFragments(container0.getTaskFragmentToken(),
+ container1.getTaskFragmentToken(), null /* adjacentParams */);
+
+ // No request to set the same adjacent TaskFragments.
+ clearInvocations(mTransaction);
+ mPresenter.setAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken(),
+ container1.getTaskFragmentToken(), null /* adjacentParams */);
+
+ verify(mTransaction, never()).setAdjacentTaskFragments(any(), any(), any());
+ }
+
+ @Test
+ public void testClearAdjacentTaskFragments() {
+ final TaskFragmentContainer container0 = mController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer container1 = mController.newContainer(mActivity, TASK_ID);
+
+ // No request to clear as it is not set by default.
+ mPresenter.clearAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken());
+ verify(mTransaction, never()).clearAdjacentTaskFragments(any());
+
+ mPresenter.setAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken(),
+ container1.getTaskFragmentToken(), null /* adjacentParams */);
+ mPresenter.clearAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken());
+ verify(mTransaction).clearAdjacentTaskFragments(container0.getTaskFragmentToken());
+
+ // No request to clear on either of the previous cleared TasKFragments.
+ clearInvocations(mTransaction);
+ mPresenter.clearAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken());
+ mPresenter.clearAdjacentTaskFragments(mTransaction, container1.getTaskFragmentToken());
+
+ verify(mTransaction, never()).clearAdjacentTaskFragments(any());
+ }
+
+ @Test
+ public void testSetCompanionTaskFragment() {
+ final TaskFragmentContainer container0 = mController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer container1 = mController.newContainer(mActivity, TASK_ID);
+
+ mPresenter.setCompanionTaskFragment(mTransaction, container0.getTaskFragmentToken(),
+ container1.getTaskFragmentToken());
+ verify(mTransaction).setCompanionTaskFragment(container0.getTaskFragmentToken(),
+ container1.getTaskFragmentToken());
+
+ // No request to set the same adjacent TaskFragments.
+ clearInvocations(mTransaction);
+ mPresenter.setCompanionTaskFragment(mTransaction, container0.getTaskFragmentToken(),
+ container1.getTaskFragmentToken());
+
+ verify(mTransaction, never()).setCompanionTaskFragment(any(), any());
+ }
+
+ @Test
public void testUpdateAnimationParams() {
final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID);
@@ -228,7 +286,7 @@
@Test
public void testGetRelBoundsForPosition_expandContainers() {
- final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties();
final SplitAttributes splitAttributes = new SplitAttributes.Builder()
.setSplitType(new SplitAttributes.SplitType.ExpandContainersSplitType())
.build();
@@ -248,7 +306,7 @@
@Test
public void testGetRelBoundsForPosition_expandContainers_isRelativeToParent() {
- final TaskContainer.TaskProperties taskProperties = getTaskProperty(
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties(
new Rect(100, 100, 500, 1000));
final SplitAttributes splitAttributes = new SplitAttributes.Builder()
.setSplitType(new SplitAttributes.SplitType.ExpandContainersSplitType())
@@ -273,7 +331,7 @@
false /* splitHorizontally */);
final Rect secondaryBounds = getSplitBounds(false /* isPrimary */,
false /* splitHorizontally */);
- final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties();
SplitAttributes splitAttributes = new SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
.setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
@@ -339,7 +397,7 @@
// Offset TaskBounds to 100, 100. The returned rel bounds shouldn't be affected.
final Rect taskBounds = new Rect(TASK_BOUNDS);
taskBounds.offset(100, 100);
- final TaskContainer.TaskProperties taskProperties = getTaskProperty(taskBounds);
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties(taskBounds);
SplitAttributes splitAttributes = new SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
.setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
@@ -400,7 +458,7 @@
true /* splitHorizontally */);
final Rect secondaryBounds = getSplitBounds(false /* isPrimary */,
true /* splitHorizontally */);
- final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties();
SplitAttributes splitAttributes = new SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
.setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
@@ -442,7 +500,7 @@
false /* splitHorizontally */);
final Rect secondaryBounds = getSplitBounds(false /* isPrimary */,
false /* splitHorizontally */);
- final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties();
final SplitAttributes splitAttributes = new SplitAttributes.Builder()
.setSplitType(new SplitAttributes.SplitType.HingeSplitType(
SplitAttributes.SplitType.RatioSplitType.splitEqually()
@@ -506,7 +564,7 @@
@Test
public void testGetRelBoundsForPosition_fallbackToExpandContainers() {
- final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties();
final SplitAttributes splitAttributes = new SplitAttributes.Builder()
.setSplitType(new SplitAttributes.SplitType.HingeSplitType(
new SplitAttributes.SplitType.ExpandContainersSplitType()
@@ -528,7 +586,7 @@
@Test
public void testGetRelBoundsForPosition_useHingeSplitType() {
- final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties();
final SplitAttributes splitAttributes = new SplitAttributes.Builder()
.setSplitType(new SplitAttributes.SplitType.HingeSplitType(
new SplitAttributes.SplitType.ExpandContainersSplitType()
@@ -581,13 +639,13 @@
splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */));
verify(mPresenter, never()).expandTaskFragment(any(), any());
- splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES);
+ splitContainer.updateCurrentSplitAttributes(SPLIT_ATTRIBUTES);
doReturn(createActivityInfoWithMinDimensions()).when(secondaryActivity).getActivityInfo();
assertEquals(RESULT_EXPAND_FAILED_NO_TF_INFO, mPresenter.expandSplitContainerIfNeeded(
mTransaction, splitContainer, mActivity, secondaryActivity,
null /* secondaryIntent */));
- splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES);
+ splitContainer.updateCurrentSplitAttributes(SPLIT_ATTRIBUTES);
primaryTf.setInfo(mTransaction, createMockTaskFragmentInfo(primaryTf, mActivity));
secondaryTf.setInfo(mTransaction,
createMockTaskFragmentInfo(secondaryTf, secondaryActivity));
@@ -597,7 +655,7 @@
verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken());
verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken());
- splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES);
+ splitContainer.updateCurrentSplitAttributes(SPLIT_ATTRIBUTES);
clearInvocations(mPresenter);
assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction,
@@ -639,37 +697,35 @@
.setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY)
.setDefaultSplitAttributes(SPLIT_ATTRIBUTES)
.build();
- final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+ final TaskContainer.TaskProperties taskProperties = getTaskProperties();
assertEquals(SPLIT_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties,
- splitPairRule, null /* minDimensionsPair */));
+ splitPairRule, SPLIT_ATTRIBUTES, null /* minDimensionsPair */));
final Pair<Size, Size> minDimensionsPair = new Pair<>(
new Size(TASK_BOUNDS.width(), TASK_BOUNDS.height()), null);
assertEquals(EXPAND_CONTAINERS_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties,
- splitPairRule, minDimensionsPair));
+ splitPairRule, SPLIT_ATTRIBUTES, minDimensionsPair));
taskProperties.getConfiguration().windowConfiguration.setBounds(new Rect(
TASK_BOUNDS.left + 1, TASK_BOUNDS.top + 1, TASK_BOUNDS.right + 1,
TASK_BOUNDS.bottom + 1));
assertEquals(EXPAND_CONTAINERS_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties,
- splitPairRule, null /* minDimensionsPair */));
+ splitPairRule, SPLIT_ATTRIBUTES, null /* minDimensionsPair */));
final SplitAttributes splitAttributes = new SplitAttributes.Builder()
- .setSplitType(
- new SplitAttributes.SplitType.HingeSplitType(
- SplitAttributes.SplitType.RatioSplitType.splitEqually()
- )
- ).build();
+ .setSplitType(new SplitAttributes.SplitType.HingeSplitType(
+ SplitAttributes.SplitType.RatioSplitType.splitEqually()))
+ .build();
final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator =
params -> splitAttributes;
mController.setSplitAttributesCalculator(calculator);
assertEquals(splitAttributes, mPresenter.computeSplitAttributes(taskProperties,
- splitPairRule, null /* minDimensionsPair */));
+ splitPairRule, SPLIT_ATTRIBUTES, null /* minDimensionsPair */));
}
@Test
@@ -696,14 +752,15 @@
doReturn(activityConfig).when(mActivityResources).getConfiguration();
doReturn(new ActivityInfo()).when(activity).getActivityInfo();
doReturn(mock(IBinder.class)).when(activity).getActivityToken();
+ doReturn(TASK_ID).when(activity).getTaskId();
return activity;
}
- private static TaskContainer.TaskProperties getTaskProperty() {
- return getTaskProperty(TASK_BOUNDS);
+ private static TaskContainer.TaskProperties getTaskProperties() {
+ return getTaskProperties(TASK_BOUNDS);
}
- private static TaskContainer.TaskProperties getTaskProperty(@NonNull Rect taskBounds) {
+ private static TaskContainer.TaskProperties getTaskProperties(@NonNull Rect taskBounds) {
final Configuration configuration = new Configuration();
configuration.windowConfiguration.setBounds(taskBounds);
return new TaskContainer.TaskProperties(DEFAULT_DISPLAY, configuration);
diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar
index cddbf469..7cd5dd6 100644
--- a/libs/WindowManager/Jetpack/window-extensions-release.aar
+++ b/libs/WindowManager/Jetpack/window-extensions-release.aar
Binary files differ
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 360bfe7..36c0cb6 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
@@ -68,10 +68,14 @@
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
+import android.view.IWindowManager;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
+import android.window.ScreenCapture;
+import android.window.ScreenCapture.ScreenCaptureListener;
+import android.window.ScreenCapture.ScreenshotSync;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
@@ -143,6 +147,7 @@
private final SyncTransactionQueue mSyncQueue;
private final ShellController mShellController;
private final ShellCommandHandler mShellCommandHandler;
+ private final IWindowManager mWmService;
// Used to post to main UI thread
private final ShellExecutor mMainExecutor;
@@ -237,7 +242,8 @@
@ShellMainThread Handler mainHandler,
@ShellBackgroundThread ShellExecutor bgExecutor,
TaskViewTransitions taskViewTransitions,
- SyncTransactionQueue syncQueue) {
+ SyncTransactionQueue syncQueue,
+ IWindowManager wmService) {
mContext = context;
mShellCommandHandler = shellCommandHandler;
mShellController = shellController;
@@ -269,6 +275,7 @@
mOneHandedOptional = oneHandedOptional;
mDragAndDropController = dragAndDropController;
mSyncQueue = syncQueue;
+ mWmService = wmService;
shellInit.addInitCallback(this::onInit, this);
}
@@ -1037,6 +1044,26 @@
}
/**
+ * Performs a screenshot that may exclude the bubble layer, if one is present. The screenshot
+ * can be access via the supplied {@link ScreenshotSync#get()} asynchronously.
+ *
+ * TODO(b/267324693): Implement the exclude layer functionality in screenshot.
+ */
+ public void getScreenshotExcludingBubble(int displayId,
+ Pair<ScreenCaptureListener, ScreenshotSync> screenCaptureListener) {
+ try {
+ mWmService.captureDisplay(displayId, null, screenCaptureListener.first);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to capture screenshot");
+ }
+ }
+
+ /** Sets the app bubble's taskId which is cached for SysUI. */
+ public void setAppBubbleTaskId(int taskId) {
+ mImpl.mCachedState.setAppBubbleTaskId(taskId);
+ }
+
+ /**
* Fills the overflow bubbles by loading them from disk.
*/
void loadOverflowBubblesFromDisk() {
@@ -1614,6 +1641,7 @@
private HashSet<String> mSuppressedBubbleKeys = new HashSet<>();
private HashMap<String, String> mSuppressedGroupToNotifKeys = new HashMap<>();
private HashMap<String, Bubble> mShortcutIdToBubble = new HashMap<>();
+ private int mAppBubbleTaskId = INVALID_TASK_ID;
private ArrayList<Bubble> mTmpBubbles = new ArrayList<>();
@@ -1645,12 +1673,22 @@
mSuppressedBubbleKeys.clear();
mShortcutIdToBubble.clear();
+ mAppBubbleTaskId = INVALID_TASK_ID;
for (Bubble b : mTmpBubbles) {
mShortcutIdToBubble.put(b.getShortcutId(), b);
updateBubbleSuppressedState(b);
+
+ if (KEY_APP_BUBBLE.equals(b.getKey())) {
+ mAppBubbleTaskId = b.getTaskId();
+ }
}
}
+ /** Sets the app bubble's taskId which is cached for SysUI. */
+ synchronized void setAppBubbleTaskId(int taskId) {
+ mAppBubbleTaskId = taskId;
+ }
+
/**
* Updates a specific bubble suppressed state. This is used mainly because notification
* suppression changes don't go through the same BubbleData update mechanism.
@@ -1700,6 +1738,8 @@
for (String key : mSuppressedGroupToNotifKeys.keySet()) {
pw.println(" suppressing: " + key);
}
+
+ pw.print("mAppBubbleTaskId: " + mAppBubbleTaskId);
}
}
@@ -1750,6 +1790,24 @@
}
@Override
+ public boolean isAppBubbleTaskId(int taskId) {
+ return mCachedState.mAppBubbleTaskId == taskId;
+ }
+
+ @Override
+ @Nullable
+ public ScreenshotSync getScreenshotExcludingBubble(int displayId) {
+ Pair<ScreenCaptureListener, ScreenshotSync> screenCaptureListener =
+ ScreenCapture.createSyncCaptureListener();
+
+ mMainExecutor.execute(
+ () -> BubbleController.this.getScreenshotExcludingBubble(displayId,
+ screenCaptureListener));
+
+ return screenCaptureListener.second;
+ }
+
+ @Override
public boolean handleDismissalInterception(BubbleEntry entry,
@Nullable List<BubbleEntry> children, IntConsumer removeCallback,
Executor callbackExecutor) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 1feff18..57c7731 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -278,6 +278,11 @@
// The taskId is saved to use for removeTask, preventing appearance in recent tasks.
mTaskId = taskId;
+ if (Bubble.KEY_APP_BUBBLE.equals(getBubbleKey())) {
+ // Let the controller know sooner what the taskId is.
+ mController.setAppBubbleTaskId(mTaskId);
+ }
+
// With the task org, the taskAppeared callback will only happen once the task has
// already drawn
setContentVisibility(true);
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 df43257..1753cda 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
@@ -16,6 +16,8 @@
package com.android.wm.shell.bubbles;
+import static android.window.ScreenCapture.ScreenshotSync;
+
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
import static java.lang.annotation.ElementType.PARAMETER;
@@ -24,11 +26,13 @@
import android.app.NotificationChannel;
import android.content.Intent;
import android.content.pm.UserInfo;
+import android.hardware.HardwareBuffer;
import android.os.UserHandle;
import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationListenerService.RankingMap;
import android.util.Pair;
import android.util.SparseArray;
+import android.window.ScreenCapture.ScreenshotHardwareBuffer;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
@@ -132,6 +136,18 @@
*/
void showOrHideAppBubble(Intent intent);
+ /** @return true if the specified {@code taskId} corresponds to app bubble's taskId. */
+ boolean isAppBubbleTaskId(int taskId);
+
+ /**
+ * @return a {@link ScreenshotSync} after performing a screenshot that may exclude the bubble
+ * layer, if one is present. The underlying {@link ScreenshotHardwareBuffer} can be access via
+ * {@link ScreenshotSync#get()} asynchronously and care should be taken to
+ * {@link HardwareBuffer#close()} the associated
+ * {@link ScreenshotHardwareBuffer#getHardwareBuffer()} when no longer required.
+ */
+ ScreenshotSync getScreenshotExcludingBubble(int displayId);
+
/**
* @return a bubble that matches the provided shortcutId, if one exists.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index d0aef20..a775db9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -141,7 +141,7 @@
if (pd == null) {
return false;
}
- final InsetsSource imeSource = pd.mInsetsState.getSource(InsetsState.ITYPE_IME);
+ final InsetsSource imeSource = pd.mInsetsState.peekSource(InsetsSource.ID_IME);
return imeSource != null && pd.mImeSourceControl != null && imeSource.isVisible();
}
@@ -245,14 +245,17 @@
return;
}
- updateImeVisibility(insetsState.getSourceOrDefaultVisibility(InsetsState.ITYPE_IME));
+ updateImeVisibility(insetsState.isSourceOrDefaultVisible(InsetsSource.ID_IME,
+ WindowInsets.Type.ime()));
- final InsetsSource newSource = insetsState.getSource(InsetsState.ITYPE_IME);
- final Rect newFrame = newSource.getFrame();
- final Rect oldFrame = mInsetsState.getSource(InsetsState.ITYPE_IME).getFrame();
+ final InsetsSource newSource = insetsState.peekSource(InsetsSource.ID_IME);
+ final Rect newFrame = newSource != null ? newSource.getFrame() : null;
+ final boolean newSourceVisible = newSource != null && newSource.isVisible();
+ final InsetsSource oldSource = mInsetsState.peekSource(InsetsSource.ID_IME);
+ final Rect oldFrame = oldSource != null ? oldSource.getFrame() : null;
mInsetsState.set(insetsState, true /* copySources */);
- if (mImeShowing && !newFrame.equals(oldFrame) && newSource.isVisible()) {
+ if (mImeShowing && !Objects.equals(oldFrame, newFrame) && newSourceVisible) {
if (DEBUG) Slog.d(TAG, "insetsChanged when IME showing, restart animation");
startAnimation(mImeShowing, true /* forceRestart */, null /* statsToken */);
}
@@ -351,7 +354,7 @@
* Sends the local visibility state back to window manager. Needed for legacy adjustForIme.
*/
private void setVisibleDirectly(boolean visible) {
- mInsetsState.getSource(InsetsState.ITYPE_IME).setVisible(visible);
+ mInsetsState.setSourceVisible(InsetsSource.ID_IME, visible);
mRequestedVisibleTypes = visible
? mRequestedVisibleTypes | WindowInsets.Type.ime()
: mRequestedVisibleTypes & ~WindowInsets.Type.ime();
@@ -382,7 +385,7 @@
private void startAnimation(final boolean show, final boolean forceRestart,
@Nullable ImeTracker.Token statsToken) {
- final InsetsSource imeSource = mInsetsState.getSource(InsetsState.ITYPE_IME);
+ final InsetsSource imeSource = mInsetsState.peekSource(InsetsSource.ID_IME);
if (imeSource == null || mImeSourceControl == null) {
ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE);
return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
index 8484013..b8e8363 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
@@ -372,7 +372,7 @@
// Only navigation bar
if (hasNavigationBar) {
- final InsetsSource extraNavBar = insetsState.getSource(ITYPE_EXTRA_NAVIGATION_BAR);
+ final InsetsSource extraNavBar = insetsState.peekSource(ITYPE_EXTRA_NAVIGATION_BAR);
final boolean hasExtraNav = extraNavBar != null && extraNavBar.isVisible();
int position = navigationBarPosition(res, displayWidth, displayHeight, displayRotation);
int navBarSize =
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
index c634198..5933ac2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
@@ -37,6 +37,7 @@
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
+import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
@@ -216,13 +217,14 @@
void onInsetsChanged(InsetsState insetsState, boolean animate) {
mSplitLayout.getDividerBounds(mTempRect);
final InsetsSource taskBarInsetsSource =
- insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+ insetsState.peekSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
// Only insets the divider bar with task bar when it's expanded so that the rounded corners
// will be drawn against task bar.
// But there is no need to do it when IME showing because there are no rounded corners at
// the bottom. This also avoids the problem of task bar height not changing when IME
// floating.
- if (!insetsState.getSourceOrDefaultVisibility(InsetsState.ITYPE_IME)
+ if (!insetsState.isSourceOrDefaultVisible(InsetsSource.ID_IME, WindowInsets.Type.ime())
+ && taskBarInsetsSource != null
&& taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
mTempRect.inset(taskBarInsetsSource.calculateVisibleInsets(mTempRect));
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index 45b234a..f616e6f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -699,19 +699,6 @@
return bounds.width() > bounds.height();
}
- /** Reverse the split position. */
- @SplitPosition
- public static int reversePosition(@SplitPosition int position) {
- switch (position) {
- case SPLIT_POSITION_TOP_OR_LEFT:
- return SPLIT_POSITION_BOTTOM_OR_RIGHT;
- case SPLIT_POSITION_BOTTOM_OR_RIGHT:
- return SPLIT_POSITION_TOP_OR_LEFT;
- default:
- return SPLIT_POSITION_UNDEFINED;
- }
- }
-
/**
* Return if this layout is landscape.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java
new file mode 100644
index 0000000..042721c9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common.split;
+
+import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
+import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.PendingIntent;
+import android.content.Intent;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.wm.shell.ShellTaskOrganizer;
+
+/** Helper utility class for split screen components to use. */
+public class SplitScreenUtils {
+ /** Reverse the split position. */
+ @SplitScreenConstants.SplitPosition
+ public static int reverseSplitPosition(@SplitScreenConstants.SplitPosition int position) {
+ switch (position) {
+ case SPLIT_POSITION_TOP_OR_LEFT:
+ return SPLIT_POSITION_BOTTOM_OR_RIGHT;
+ case SPLIT_POSITION_BOTTOM_OR_RIGHT:
+ return SPLIT_POSITION_TOP_OR_LEFT;
+ case SPLIT_POSITION_UNDEFINED:
+ default:
+ return SPLIT_POSITION_UNDEFINED;
+ }
+ }
+
+ /** Returns true if the task is valid for split screen. */
+ public static boolean isValidToSplit(ActivityManager.RunningTaskInfo taskInfo) {
+ return taskInfo != null && taskInfo.supportsMultiWindow
+ && ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, taskInfo.getActivityType())
+ && ArrayUtils.contains(CONTROLLED_WINDOWING_MODES, taskInfo.getWindowingMode());
+ }
+
+ /** Retrieve package name from an intent */
+ @Nullable
+ public static String getPackageName(Intent intent) {
+ if (intent == null || intent.getComponent() == null) {
+ return null;
+ }
+ return intent.getComponent().getPackageName();
+ }
+
+ /** Retrieve package name from a PendingIntent */
+ @Nullable
+ public static String getPackageName(PendingIntent pendingIntent) {
+ if (pendingIntent == null || pendingIntent.getIntent() == null) {
+ return null;
+ }
+ return getPackageName(pendingIntent.getIntent());
+ }
+
+ /** Retrieve package name from a taskId */
+ @Nullable
+ public static String getPackageName(int taskId, ShellTaskOrganizer taskOrganizer) {
+ final ActivityManager.RunningTaskInfo taskInfo = taskOrganizer.getRunningTaskInfo(taskId);
+ return taskInfo != null ? getPackageName(taskInfo.baseIntent) : null;
+ }
+
+ /** Returns true if they are the same package. */
+ public static boolean samePackage(String packageName1, String packageName2) {
+ return packageName1 != null && packageName1.equals(packageName2);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 4879d86..d83f1eb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -21,6 +21,7 @@
import android.os.Handler;
import android.os.UserManager;
import android.view.Choreographer;
+import android.view.IWindowManager;
import android.view.WindowManager;
import com.android.internal.jank.InteractionJankMonitor;
@@ -170,14 +171,15 @@
@ShellMainThread Handler mainHandler,
@ShellBackgroundThread ShellExecutor bgExecutor,
TaskViewTransitions taskViewTransitions,
- SyncTransactionQueue syncQueue) {
+ SyncTransactionQueue syncQueue,
+ IWindowManager wmService) {
return new BubbleController(context, shellInit, shellCommandHandler, shellController, data,
null /* synchronizer */, floatingContentCoordinator,
new BubbleDataRepository(context, launcherApps, mainExecutor),
statusBarService, windowManager, windowManagerShellWrapper, userManager,
launcherApps, logger, taskStackListener, organizer, positioner, displayController,
oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, bgExecutor,
- taskViewTransitions, syncQueue);
+ taskViewTransitions, syncQueue, wmService);
}
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
index 6728c00..d9ac76e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
@@ -28,6 +28,7 @@
import android.annotation.NonNull;
import android.app.TaskInfo;
import android.content.Context;
+import android.content.pm.ActivityInfo;
import android.graphics.Rect;
import android.view.Surface;
import android.view.SurfaceControl;
@@ -361,22 +362,26 @@
}
void setColorContentOverlay(Context context) {
- final SurfaceControl.Transaction tx =
- mSurfaceControlTransactionFactory.getTransaction();
- if (mContentOverlay != null) {
- mContentOverlay.detach(tx);
- }
- mContentOverlay = new PipContentOverlay.PipColorOverlay(context);
- mContentOverlay.attach(tx, mLeash);
+ reattachContentOverlay(new PipContentOverlay.PipColorOverlay(context));
}
void setSnapshotContentOverlay(TaskSnapshot snapshot, Rect sourceRectHint) {
+ reattachContentOverlay(
+ new PipContentOverlay.PipSnapshotOverlay(snapshot, sourceRectHint));
+ }
+
+ void setAppIconContentOverlay(Context context, Rect bounds, ActivityInfo activityInfo) {
+ reattachContentOverlay(
+ new PipContentOverlay.PipAppIconOverlay(context, bounds, activityInfo));
+ }
+
+ private void reattachContentOverlay(PipContentOverlay overlay) {
final SurfaceControl.Transaction tx =
mSurfaceControlTransactionFactory.getTransaction();
if (mContentOverlay != null) {
mContentOverlay.detach(tx);
}
- mContentOverlay = new PipContentOverlay.PipSnapshotOverlay(snapshot, sourceRectHint);
+ mContentOverlay = overlay;
mContentOverlay.attach(tx, mLeash);
}
@@ -570,8 +575,9 @@
final Rect base = getBaseValue();
final Rect start = getStartValue();
final Rect end = getEndValue();
+ Rect bounds = mRectEvaluator.evaluate(fraction, start, end);
if (mContentOverlay != null) {
- mContentOverlay.onAnimationUpdate(tx, fraction);
+ mContentOverlay.onAnimationUpdate(tx, bounds, fraction);
}
if (rotatedEndRect != null) {
// Animate the bounds in a different orientation. It only happens when
@@ -579,7 +585,6 @@
applyRotation(tx, leash, fraction, start, end);
return;
}
- Rect bounds = mRectEvaluator.evaluate(fraction, start, end);
float angle = (1.0f - fraction) * startingAngle;
setCurrentValue(bounds);
if (inScaleTransition() || sourceHintRect == null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
index 283b1ec..480bf93 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
@@ -16,11 +16,21 @@
package com.android.wm.shell.pip;
+import static android.util.TypedValue.COMPLEX_UNIT_DIP;
+
import android.annotation.Nullable;
import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
import android.graphics.Color;
+import android.graphics.Matrix;
import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.window.TaskSnapshot;
@@ -51,9 +61,11 @@
* Animates the internal {@link #mLeash} by a given fraction.
* @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly
* call apply on this transaction, it should be applied on the caller side.
+ * @param currentBounds {@link Rect} of the current animation bounds.
* @param fraction progress of the animation ranged from 0f to 1f.
*/
- public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction);
+ public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+ Rect currentBounds, float fraction);
/**
* Callback when reaches the end of animation on the internal {@link #mLeash}.
@@ -66,13 +78,15 @@
/** A {@link PipContentOverlay} uses solid color. */
public static final class PipColorOverlay extends PipContentOverlay {
+ private static final String TAG = PipColorOverlay.class.getSimpleName();
+
private final Context mContext;
public PipColorOverlay(Context context) {
mContext = context;
mLeash = new SurfaceControl.Builder(new SurfaceSession())
- .setCallsite("PipAnimation")
- .setName(PipColorOverlay.class.getSimpleName())
+ .setCallsite(TAG)
+ .setName(TAG)
.setColorLayer()
.build();
}
@@ -88,7 +102,8 @@
}
@Override
- public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction) {
+ public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+ Rect currentBounds, float fraction) {
atomicTx.setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
}
@@ -114,6 +129,8 @@
/** A {@link PipContentOverlay} uses {@link TaskSnapshot}. */
public static final class PipSnapshotOverlay extends PipContentOverlay {
+ private static final String TAG = PipSnapshotOverlay.class.getSimpleName();
+
private final TaskSnapshot mSnapshot;
private final Rect mSourceRectHint;
@@ -121,8 +138,8 @@
mSnapshot = snapshot;
mSourceRectHint = new Rect(sourceRectHint);
mLeash = new SurfaceControl.Builder(new SurfaceSession())
- .setCallsite("PipAnimation")
- .setName(PipSnapshotOverlay.class.getSimpleName())
+ .setCallsite(TAG)
+ .setName(TAG)
.build();
}
@@ -143,7 +160,8 @@
}
@Override
- public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction) {
+ public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+ Rect currentBounds, float fraction) {
// Do nothing. Keep the snapshot till animation ends.
}
@@ -152,4 +170,113 @@
atomicTx.remove(mLeash);
}
}
+
+ /** A {@link PipContentOverlay} shows app icon on solid color background. */
+ public static final class PipAppIconOverlay extends PipContentOverlay {
+ private static final String TAG = PipAppIconOverlay.class.getSimpleName();
+ private static final int APP_ICON_SIZE_DP = 48;
+
+ private final Context mContext;
+ private final int mAppIconSizePx;
+ private final Rect mAppBounds;
+ private final Matrix mTmpTransform = new Matrix();
+ private final float[] mTmpFloat9 = new float[9];
+
+ private Bitmap mBitmap;
+
+ public PipAppIconOverlay(Context context, Rect appBounds, ActivityInfo activityInfo) {
+ mContext = context;
+ mAppIconSizePx = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, APP_ICON_SIZE_DP,
+ context.getResources().getDisplayMetrics());
+ mAppBounds = new Rect(appBounds);
+ mBitmap = Bitmap.createBitmap(appBounds.width(), appBounds.height(),
+ Bitmap.Config.ARGB_8888);
+ prepareAppIconOverlay(activityInfo);
+ mLeash = new SurfaceControl.Builder(new SurfaceSession())
+ .setCallsite(TAG)
+ .setName(TAG)
+ .build();
+ }
+
+ @Override
+ public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) {
+ tx.show(mLeash);
+ tx.setLayer(mLeash, Integer.MAX_VALUE);
+ tx.setBuffer(mLeash, mBitmap.getHardwareBuffer());
+ tx.reparent(mLeash, parentLeash);
+ tx.apply();
+ }
+
+ @Override
+ public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+ Rect currentBounds, float fraction) {
+ mTmpTransform.reset();
+ // Scale back the bitmap with the pivot point at center.
+ mTmpTransform.postScale(
+ (float) mAppBounds.width() / currentBounds.width(),
+ (float) mAppBounds.height() / currentBounds.height(),
+ mAppBounds.centerX(),
+ mAppBounds.centerY());
+ atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9)
+ .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
+ }
+
+ @Override
+ public void onAnimationEnd(SurfaceControl.Transaction atomicTx, Rect destinationBounds) {
+ atomicTx.remove(mLeash);
+ }
+
+ @Override
+ public void detach(SurfaceControl.Transaction tx) {
+ super.detach(tx);
+ if (mBitmap != null && !mBitmap.isRecycled()) {
+ mBitmap.recycle();
+ }
+ }
+
+ private void prepareAppIconOverlay(ActivityInfo activityInfo) {
+ final Canvas canvas = new Canvas();
+ canvas.setBitmap(mBitmap);
+ final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
+ android.R.attr.colorBackground });
+ try {
+ int colorAccent = ta.getColor(0, 0);
+ canvas.drawRGB(
+ Color.red(colorAccent),
+ Color.green(colorAccent),
+ Color.blue(colorAccent));
+ } finally {
+ ta.recycle();
+ }
+ final Drawable appIcon = loadActivityInfoIcon(activityInfo,
+ mContext.getResources().getConfiguration().densityDpi);
+ final Rect appIconBounds = new Rect(
+ mAppBounds.centerX() - mAppIconSizePx / 2,
+ mAppBounds.centerY() - mAppIconSizePx / 2,
+ mAppBounds.centerX() + mAppIconSizePx / 2,
+ mAppBounds.centerY() + mAppIconSizePx / 2);
+ appIcon.setBounds(appIconBounds);
+ appIcon.draw(canvas);
+ mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */);
+ }
+
+ // Copied from com.android.launcher3.icons.IconProvider#loadActivityInfoIcon
+ private Drawable loadActivityInfoIcon(ActivityInfo ai, int density) {
+ final int iconRes = ai.getIconResource();
+ Drawable icon = null;
+ // Get the preferred density icon from the app's resources
+ if (density != 0 && iconRes != 0) {
+ try {
+ final Resources resources = mContext.getPackageManager()
+ .getResourcesForApplication(ai.applicationInfo);
+ icon = resources.getDrawableForDensity(iconRes, density);
+ } catch (PackageManager.NameNotFoundException | Resources.NotFoundException exc) { }
+ }
+ // Get the default density icon
+ if (icon == null) {
+ icon = ai.loadIcon(mContext.getPackageManager());
+ }
+ return icon;
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index 8ba2583..aad27b9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -62,6 +62,7 @@
import android.graphics.Rect;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.os.SystemProperties;
import android.util.Log;
import android.view.Choreographer;
import android.view.Display;
@@ -1568,7 +1569,13 @@
// Similar to auto-enter-pip transition, we use content overlay when there is no
// source rect hint to enter PiP use bounds animation.
if (sourceHintRect == null) {
- animator.setColorContentOverlay(mContext);
+ if (SystemProperties.getBoolean(
+ "persist.wm.debug.enable_pip_app_icon_overlay", false)) {
+ animator.setAppIconContentOverlay(
+ mContext, currentBounds, mTaskInfo.topActivityInfo);
+ } else {
+ animator.setColorContentOverlay(mContext);
+ }
} else {
final TaskSnapshot snapshot = PipUtils.getTaskSnapshot(
mTaskInfo.launchIntoPipHostTaskId, false /* isLowResolution */);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 83158ff..d9d1009 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -50,6 +50,7 @@
import android.graphics.Point;
import android.graphics.Rect;
import android.os.IBinder;
+import android.os.SystemProperties;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowManager;
@@ -792,7 +793,13 @@
if (sourceHintRect == null) {
// We use content overlay when there is no source rect hint to enter PiP use bounds
// animation.
- animator.setColorContentOverlay(mContext);
+ if (SystemProperties.getBoolean(
+ "persist.wm.debug.enable_pip_app_icon_overlay", false)) {
+ animator.setAppIconContentOverlay(
+ mContext, currentBounds, taskInfo.topActivityInfo);
+ } else {
+ animator.setColorContentOverlay(mContext);
+ }
}
} else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
animator = mPipAnimationController.getAnimator(taskInfo, leash, destinationBounds,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java
index a906522..e787ed9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java
@@ -45,8 +45,9 @@
@VisibleForTesting
final SizeSpecSource mSizeSpecSourceImpl;
- /** The preferred minimum (and default) size specified by apps. */
+ /** The preferred minimum (and default minimum) size specified by apps. */
@Nullable private Size mOverrideMinSize;
+ private int mOverridableMinSize;
/** Used to store values obtained from resource files. */
private Point mScreenEdgeInsets;
@@ -386,6 +387,8 @@
mDefaultMinSize = res.getDimensionPixelSize(
R.dimen.default_minimal_size_pip_resizable_task);
+ mOverridableMinSize = res.getDimensionPixelSize(
+ R.dimen.overridable_minimal_size_pip_resizable_task);
final String screenEdgeInsetsDpString = res.getString(
R.string.config_defaultPictureInPictureScreenEdgeInsets);
@@ -443,13 +446,19 @@
/** Returns the preferred minimal size specified by the activity in PIP. */
@Nullable
public Size getOverrideMinSize() {
+ if (mOverrideMinSize != null
+ && (mOverrideMinSize.getWidth() < mOverridableMinSize
+ || mOverrideMinSize.getHeight() < mOverridableMinSize)) {
+ return new Size(mOverridableMinSize, mOverridableMinSize);
+ }
+
return mOverrideMinSize;
}
/** Returns the minimum edge size of the override minimum size, or 0 if not set. */
public int getOverrideMinEdgeSize() {
if (mOverrideMinSize == null) return 0;
- return Math.min(mOverrideMinSize.getWidth(), mOverrideMinSize.getHeight());
+ return Math.min(getOverrideMinSize().getWidth(), getOverrideMinSize().getHeight());
}
public int getMinEdgeSize() {
@@ -505,7 +514,7 @@
if (mOverrideMinSize == null) {
return null;
}
- final Size size = mOverrideMinSize;
+ final Size size = getOverrideMinSize();
final float sizeAspectRatio = size.getWidth() / (float) size.getHeight();
if (sizeAspectRatio > aspectRatio) {
// Size is wider, fix the width and increase the height
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 21eeaa2..7cb5cf2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -28,6 +28,9 @@
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.common.split.SplitScreenUtils.isValidToSplit;
+import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
+import static com.android.wm.shell.common.split.SplitScreenUtils.samePackage;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN;
@@ -39,11 +42,8 @@
import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
import android.app.PendingIntent;
-import android.content.ActivityNotFoundException;
-import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
import android.graphics.Rect;
import android.os.Bundle;
@@ -82,8 +82,8 @@
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.annotations.ExternalThread;
-import com.android.wm.shell.common.split.SplitLayout;
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
+import com.android.wm.shell.common.split.SplitScreenUtils;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.draganddrop.DragAndDropPolicy;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
@@ -318,10 +318,6 @@
return mStageCoordinator;
}
- public boolean isValidToEnterSplitScreen(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
- return mStageCoordinator.isValidToEnterSplitScreen(taskInfo);
- }
-
@Nullable
public ActivityManager.RunningTaskInfo getTaskInfo(@SplitPosition int splitPosition) {
if (!isSplitScreenVisible() || splitPosition == SPLIT_POSITION_UNDEFINED) {
@@ -480,39 +476,54 @@
@Override
public void startShortcut(String packageName, String shortcutId, @SplitPosition int position,
@Nullable Bundle options, UserHandle user) {
- IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
- @Override
- public void onAnimationStart(@WindowManager.TransitionOldType int transit,
- RemoteAnimationTarget[] apps,
- RemoteAnimationTarget[] wallpapers,
- RemoteAnimationTarget[] nonApps,
- final IRemoteAnimationFinishedCallback finishedCallback) {
- try {
- finishedCallback.onAnimationFinished();
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to invoke onAnimationFinished", e);
- }
- final WindowContainerTransaction evictWct = new WindowContainerTransaction();
- mStageCoordinator.prepareEvictNonOpeningChildTasks(position, apps, evictWct);
- mSyncQueue.queue(evictWct);
+ if (options == null) options = new Bundle();
+ final ActivityOptions activityOptions = ActivityOptions.fromBundle(options);
+
+ if (samePackage(packageName, getPackageName(reverseSplitPosition(position)))) {
+ if (supportMultiInstancesSplit(packageName)) {
+ activityOptions.setApplyMultipleTaskFlagForShortcut(true);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
+ } else if (isSplitScreenVisible()) {
+ mStageCoordinator.switchSplitPosition("startShortcut");
+ return;
+ } else {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+ "Cancel entering split as not supporting multi-instances");
+ Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
+ Toast.LENGTH_SHORT).show();
+ return;
}
- @Override
- public void onAnimationCancelled(boolean isKeyguardOccluded) {
- }
- };
- options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options,
- null /* wct */);
- RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(wrapper,
- 0 /* duration */, 0 /* statusBarTransitionDelay */);
- ActivityOptions activityOptions = ActivityOptions.fromBundle(options);
- activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
- try {
- LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
- launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */,
- activityOptions.toBundle(), user);
- } catch (ActivityNotFoundException e) {
- Slog.e(TAG, "Failed to launch shortcut", e);
}
+
+ mStageCoordinator.startShortcut(packageName, shortcutId, position,
+ activityOptions.toBundle(), user);
+ }
+
+ void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo,
+ @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
+ @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
+ InstanceId instanceId) {
+ if (options1 == null) options1 = new Bundle();
+ final ActivityOptions activityOptions = ActivityOptions.fromBundle(options1);
+
+ final String packageName1 = shortcutInfo.getPackage();
+ final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer);
+ if (samePackage(packageName1, packageName2)) {
+ if (supportMultiInstancesSplit(shortcutInfo.getPackage())) {
+ activityOptions.setApplyMultipleTaskFlagForShortcut(true);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
+ } else {
+ taskId = INVALID_TASK_ID;
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+ "Cancel entering split as not supporting multi-instances");
+ Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ mStageCoordinator.startShortcutAndTaskWithLegacyTransition(shortcutInfo,
+ activityOptions.toBundle(), taskId, options2, splitPosition, splitRatio, adapter,
+ instanceId);
}
/**
@@ -530,8 +541,10 @@
@SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
InstanceId instanceId) {
Intent fillInIntent = null;
- if (launchSameAppAdjacently(pendingIntent, taskId)) {
- if (supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
+ final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent);
+ final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer);
+ if (samePackage(packageName1, packageName2)) {
+ if (supportMultiInstancesSplit(packageName1)) {
fillInIntent = new Intent();
fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
@@ -551,8 +564,10 @@
int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
Intent fillInIntent = null;
- if (launchSameAppAdjacently(pendingIntent, taskId)) {
- if (supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
+ final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent);
+ final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer);
+ if (samePackage(packageName1, packageName2)) {
+ if (supportMultiInstancesSplit(packageName1)) {
fillInIntent = new Intent();
fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
@@ -573,8 +588,10 @@
float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) {
Intent fillInIntent1 = null;
Intent fillInIntent2 = null;
- if (launchSameAppAdjacently(pendingIntent1, pendingIntent2)) {
- if (supportMultiInstancesSplit(pendingIntent1.getIntent().getComponent())) {
+ final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent1);
+ final String packageName2 = SplitScreenUtils.getPackageName(pendingIntent2);
+ if (samePackage(packageName1, packageName2)) {
+ if (supportMultiInstancesSplit(packageName1)) {
fillInIntent1 = new Intent();
fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
fillInIntent2 = new Intent();
@@ -602,13 +619,15 @@
if (fillInIntent == null) fillInIntent = new Intent();
fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);
- if (launchSameAppAdjacently(position, intent)) {
- final ComponentName launching = intent.getIntent().getComponent();
- if (supportMultiInstancesSplit(launching)) {
+ final String packageName1 = SplitScreenUtils.getPackageName(intent);
+ final String packageName2 = getPackageName(reverseSplitPosition(position));
+ if (SplitScreenUtils.samePackage(packageName1, packageName2)) {
+ if (supportMultiInstancesSplit(packageName1)) {
// To prevent accumulating large number of instances in the background, reuse task
// in the background with priority.
final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional
- .map(recentTasks -> recentTasks.findTaskInBackground(launching))
+ .map(recentTasks -> recentTasks.findTaskInBackground(
+ intent.getIntent().getComponent()))
.orElse(null);
if (taskInfo != null) {
startTask(taskInfo.taskId, position, options);
@@ -636,63 +655,32 @@
mStageCoordinator.startIntent(intent, fillInIntent, position, options);
}
+ /** Retrieve package name of a specific split position if split screen is activated, otherwise
+ * returns the package name of the top running task. */
@Nullable
- private String getPackageName(Intent intent) {
- if (intent == null || intent.getComponent() == null) {
- return null;
- }
- return intent.getComponent().getPackageName();
- }
-
- private boolean launchSameAppAdjacently(@SplitPosition int position,
- PendingIntent pendingIntent) {
- ActivityManager.RunningTaskInfo adjacentTaskInfo = null;
+ private String getPackageName(@SplitPosition int position) {
+ ActivityManager.RunningTaskInfo taskInfo;
if (isSplitScreenVisible()) {
- adjacentTaskInfo = getTaskInfo(SplitLayout.reversePosition(position));
+ taskInfo = getTaskInfo(position);
} else {
- adjacentTaskInfo = mRecentTasksOptional
- .map(recentTasks -> recentTasks.getTopRunningTask()).orElse(null);
- if (!isValidToEnterSplitScreen(adjacentTaskInfo)) {
- return false;
+ taskInfo = mRecentTasksOptional
+ .map(recentTasks -> recentTasks.getTopRunningTask())
+ .orElse(null);
+ if (!isValidToSplit(taskInfo)) {
+ return null;
}
}
- if (adjacentTaskInfo == null) {
- return false;
- }
-
- final String targetPackageName = getPackageName(pendingIntent.getIntent());
- final String adjacentPackageName = getPackageName(adjacentTaskInfo.baseIntent);
- return targetPackageName != null && targetPackageName.equals(adjacentPackageName);
- }
-
- private boolean launchSameAppAdjacently(PendingIntent pendingIntent, int taskId) {
- final ActivityManager.RunningTaskInfo adjacentTaskInfo =
- mTaskOrganizer.getRunningTaskInfo(taskId);
- if (adjacentTaskInfo == null) {
- return false;
- }
- final String targetPackageName = getPackageName(pendingIntent.getIntent());
- final String adjacentPackageName = getPackageName(adjacentTaskInfo.baseIntent);
- return targetPackageName != null && targetPackageName.equals(adjacentPackageName);
- }
-
- private boolean launchSameAppAdjacently(PendingIntent pendingIntent1,
- PendingIntent pendingIntent2) {
- final String targetPackageName = getPackageName(pendingIntent1.getIntent());
- final String adjacentPackageName = getPackageName(pendingIntent2.getIntent());
- return targetPackageName != null && targetPackageName.equals(adjacentPackageName);
+ return taskInfo != null ? SplitScreenUtils.getPackageName(taskInfo.baseIntent) : null;
}
@VisibleForTesting
- /** Returns {@code true} if the component supports multi-instances split. */
- boolean supportMultiInstancesSplit(@Nullable ComponentName launching) {
- if (launching == null) return false;
-
- final String packageName = launching.getPackageName();
- for (int i = 0; i < mAppsSupportMultiInstances.length; i++) {
- if (mAppsSupportMultiInstances[i].equals(packageName)) {
- return true;
+ boolean supportMultiInstancesSplit(String packageName) {
+ if (packageName != null) {
+ for (int i = 0; i < mAppsSupportMultiInstances.length; i++) {
+ if (mAppsSupportMultiInstances[i].equals(packageName)) {
+ return true;
+ }
}
}
@@ -1011,7 +999,7 @@
InstanceId instanceId) {
executeRemoteCallWithTaskPermission(mController,
"startShortcutAndTaskWithLegacyTransition", (controller) ->
- controller.mStageCoordinator.startShortcutAndTaskWithLegacyTransition(
+ controller.startShortcutAndTaskWithLegacyTransition(
shortcutInfo, options1, taskId, options2, splitPosition,
splitRatio, adapter, instanceId));
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 39cf5f1..5a9170b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -36,12 +36,11 @@
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;
import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER;
-import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
-import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
@@ -76,8 +75,10 @@
import android.app.IActivityTaskManager;
import android.app.PendingIntent;
import android.app.WindowConfiguration;
+import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
@@ -87,6 +88,7 @@
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.os.UserHandle;
import android.util.Log;
import android.util.Slog;
import android.view.Choreographer;
@@ -370,7 +372,7 @@
int sideStagePosition;
if (stageType == STAGE_TYPE_MAIN) {
targetStage = mMainStage;
- sideStagePosition = SplitLayout.reversePosition(stagePosition);
+ sideStagePosition = reverseSplitPosition(stagePosition);
} else if (stageType == STAGE_TYPE_SIDE) {
targetStage = mSideStage;
sideStagePosition = stagePosition;
@@ -428,6 +430,72 @@
return mLogger;
}
+ void startShortcut(String packageName, String shortcutId, @SplitPosition int position,
+ Bundle options, UserHandle user) {
+ final boolean isEnteringSplit = !isSplitActive();
+
+ IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
+ @Override
+ public void onAnimationStart(@WindowManager.TransitionOldType int transit,
+ RemoteAnimationTarget[] apps,
+ RemoteAnimationTarget[] wallpapers,
+ RemoteAnimationTarget[] nonApps,
+ final IRemoteAnimationFinishedCallback finishedCallback) {
+ boolean openingToSide = false;
+ if (apps != null) {
+ for (int i = 0; i < apps.length; ++i) {
+ if (apps[i].mode == MODE_OPENING
+ && mSideStage.containsTask(apps[i].taskId)) {
+ openingToSide = true;
+ break;
+ }
+ }
+ }
+
+ if (isEnteringSplit && !openingToSide) {
+ mMainExecutor.execute(() -> exitSplitScreen(
+ mSideStage.getChildCount() == 0 ? mMainStage : mSideStage,
+ EXIT_REASON_UNKNOWN));
+ }
+
+ if (finishedCallback != null) {
+ try {
+ finishedCallback.onAnimationFinished();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error finishing legacy transition: ", e);
+ }
+ }
+
+ if (!isEnteringSplit && openingToSide) {
+ final WindowContainerTransaction evictWct = new WindowContainerTransaction();
+ prepareEvictNonOpeningChildTasks(position, apps, evictWct);
+ mSyncQueue.queue(evictWct);
+ }
+ }
+ @Override
+ public void onAnimationCancelled(boolean isKeyguardOccluded) {
+ if (isEnteringSplit) {
+ mMainExecutor.execute(() -> exitSplitScreen(
+ mSideStage.getChildCount() == 0 ? mMainStage : mSideStage,
+ EXIT_REASON_UNKNOWN));
+ }
+ }
+ };
+ options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options,
+ null /* wct */);
+ RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(wrapper,
+ 0 /* duration */, 0 /* statusBarTransitionDelay */);
+ ActivityOptions activityOptions = ActivityOptions.fromBundle(options);
+ activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
+ try {
+ LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
+ launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */,
+ activityOptions.toBundle(), user);
+ } catch (ActivityNotFoundException e) {
+ Slog.e(TAG, "Failed to launch shortcut", e);
+ }
+ }
+
/** Launches an activity into split. */
void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position,
@Nullable Bundle options) {
@@ -899,7 +967,7 @@
case STAGE_TYPE_MAIN: {
if (position != SPLIT_POSITION_UNDEFINED) {
// Set the side stage opposite of what we want to the main stage.
- setSideStagePosition(SplitLayout.reversePosition(position), wct);
+ setSideStagePosition(reverseSplitPosition(position), wct);
} else {
position = getMainStagePosition();
}
@@ -923,7 +991,7 @@
@SplitPosition
int getMainStagePosition() {
- return SplitLayout.reversePosition(mSideStagePosition);
+ return reverseSplitPosition(mSideStagePosition);
}
int getTaskId(@SplitPosition int splitPosition) {
@@ -950,7 +1018,7 @@
mSplitLayout.splitSwitching(t, topLeftStage.mRootLeash, bottomRightStage.mRootLeash,
insets -> {
WindowContainerTransaction wct = new WindowContainerTransaction();
- setSideStagePosition(SplitLayout.reversePosition(mSideStagePosition), wct);
+ setSideStagePosition(reverseSplitPosition(mSideStagePosition), wct);
mSyncQueue.queue(wct);
mSyncQueue.runInSync(st -> {
updateSurfaceBounds(mSplitLayout, st, false /* applyResizingOffset */);
@@ -1694,12 +1762,6 @@
}
}
- boolean isValidToEnterSplitScreen(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
- return taskInfo.supportsMultiWindow
- && ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, taskInfo.getActivityType())
- && ArrayUtils.contains(CONTROLLED_WINDOWING_MODES, taskInfo.getWindowingMode());
- }
-
@Override
public void onSnappedToDismiss(boolean bottomOrRight, int reason) {
final boolean mainStageToTop =
@@ -2108,7 +2170,7 @@
// Use normal animations.
return false;
- } else if (mMixedHandler != null && hasDisplayChange(info)) {
+ } else if (mMixedHandler != null && Transitions.hasDisplayChange(info)) {
// A display-change has been un-expectedly inserted into the transition. Redirect
// handling to the mixed-handler to deal with splitting it up.
if (mMixedHandler.animatePendingSplitWithDisplayChange(transition, info,
@@ -2151,15 +2213,6 @@
return true;
}
- private boolean hasDisplayChange(TransitionInfo info) {
- boolean has = false;
- for (int iC = 0; iC < info.getChanges().size() && !has; ++iC) {
- final TransitionInfo.Change change = info.getChanges().get(iC);
- has = change.getMode() == TRANSIT_CHANGE && (change.getFlags() & FLAG_IS_DISPLAY) != 0;
- }
- return has;
- }
-
/** Called to clean-up state and do house-keeping after the animation is done. */
public void onTransitionAnimationComplete() {
// If still playing, let it finish.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
index b4e0584..02f19eb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
@@ -93,6 +93,11 @@
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
+ if (!Transitions.SHELL_TRANSITIONS_ROTATION && Transitions.hasDisplayChange(info)) {
+ // Note that if the remote doesn't have permission ACCESS_SURFACE_FLINGER, some
+ // operations of the start transaction may be ignored.
+ return false;
+ }
RemoteTransition pendingRemote = mRequestedRemotes.get(transition);
if (pendingRemote == null) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s doesn't have "
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 44d6a0d..b2f61c2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -24,6 +24,7 @@
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.view.WindowManager.fixScale;
+import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
import static android.window.TransitionInfo.FLAG_IS_OCCLUDED;
import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
import static android.window.TransitionInfo.FLAG_NO_ANIMATION;
@@ -328,6 +329,17 @@
return type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK;
}
+ /** Returns {@code true} if the transition has a display change. */
+ public static boolean hasDisplayChange(@NonNull TransitionInfo info) {
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (change.getMode() == TRANSIT_CHANGE && change.hasFlags(FLAG_IS_DISPLAY)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Sets up visibility/alpha/transforms to resemble the starting state of an animation.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java
index eab82f0..87e447b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java
@@ -85,7 +85,7 @@
@Override
public void insetsChanged(InsetsState insetsState) {
- mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+ mTaskbarInsetsSource = insetsState.peekSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
AnimationContext context = mAnimationContextByTaskId.valueAt(i);
context.update(mTaskbarInsetsSource, context.mTaskInfo);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java
index 6e10ebe..8a0fbe3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java
@@ -126,7 +126,7 @@
@Override
public void insetsChanged(InsetsState insetsState) {
- mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+ mTaskbarInsetsSource = insetsState.peekSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
updateContexts();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index 476a7ec..2981f5e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -36,6 +36,7 @@
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.transition.Transitions;
/**
* View model for the window decoration with a caption and shadows. Works with
@@ -65,6 +66,9 @@
mTaskOrganizer = taskOrganizer;
mDisplayController = displayController;
mSyncQueue = syncQueue;
+ if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mTaskOperations = new TaskOperations(null, mContext, mSyncQueue);
+ }
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
index 907977c..3734487 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
@@ -29,7 +29,8 @@
*/
public interface WindowDecorViewModel {
/**
- * Sets the transition starter that starts freeform task transitions.
+ * Sets the transition starter that starts freeform task transitions. Only called when
+ * {@link com.android.wm.shell.transition.Transitions#ENABLE_SHELL_TRANSITIONS} is {@code true}.
*
* @param transitionStarter the transition starter that starts freeform task transitions
*/
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt
similarity index 96%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt
index b3a2ad3..7358da3 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt
@@ -45,7 +45,7 @@
@RequiresDevice
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-open class MultiBubblesScreen(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
+open class ChangeActiveActivityFromBubbleTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
@Before
open fun before() {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTestCfArm.kt
similarity index 88%
copy from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenCfArm.kt
copy to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTestCfArm.kt
index 69eb06f..1a0fbe4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTestCfArm.kt
@@ -23,4 +23,5 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-open class MultiBubblesScreenCfArm(flicker: FlickerTest) : MultiBubblesScreen(flicker)
+open class ChangeActiveActivityFromBubbleTestCfArm(flicker: FlickerTest) :
+ ChangeActiveActivityFromBubbleTest(flicker)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTestShellTransit.kt
similarity index 91%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenShellTransit.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTestShellTransit.kt
index 191f4fa..cf696c8 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenShellTransit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTestShellTransit.kt
@@ -30,7 +30,8 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FlakyTest(bugId = 217777115)
-class MultiBubblesScreenShellTransit(flicker: FlickerTest) : MultiBubblesScreen(flicker) {
+class ChangeActiveActivityFromBubbleTestShellTransit(flicker: FlickerTest) :
+ ChangeActiveActivityFromBubbleTest(flicker) {
@Before
override fun before() {
Assume.assumeTrue(isShellTransitionsEnabled)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreenCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreenCfArm.kt
deleted file mode 100644
index 315e021..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreenCfArm.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.bubble
-
-import com.android.server.wm.flicker.FlickerTest
-import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-class DismissBubbleScreenCfArm(flicker: FlickerTest) : DismissBubbleScreen(flicker)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt
similarity index 96%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt
index 7a86c25..9367a8a 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt
@@ -44,7 +44,7 @@
@RequiresDevice
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-open class DismissBubbleScreen(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
+open class DragToDismissBubbleScreenTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
private val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
private val displaySize = DisplayMetrics()
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTestCfArm.kt
similarity index 89%
copy from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenCfArm.kt
copy to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTestCfArm.kt
index 69eb06f..85a534c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTestCfArm.kt
@@ -23,4 +23,5 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-open class MultiBubblesScreenCfArm(flicker: FlickerTest) : MultiBubblesScreen(flicker)
+class DragToDismissBubbleScreenTestCfArm(flicker: FlickerTest) :
+ DragToDismissBubbleScreenTest(flicker)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreenCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreenCfArm.kt
deleted file mode 100644
index 26e6273..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreenCfArm.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.bubble
-
-import com.android.server.wm.flicker.FlickerTest
-import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-class ExpandBubbleScreenCfArm(flicker: FlickerTest) : ExpandBubbleScreen(flicker)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreenCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreenCfArm.kt
deleted file mode 100644
index ec6a9c1..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreenCfArm.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.bubble
-
-import com.android.server.wm.flicker.FlickerTest
-import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-open class LaunchBubbleScreenCfArm(flicker: FlickerTest) : LaunchBubbleScreen(flicker)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLockreenTest.kt
similarity index 97%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLockreenTest.kt
index 379d5e9..0b1382b 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLockreenTest.kt
@@ -46,7 +46,7 @@
@RequiresDevice
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-class LaunchBubbleFromLockScreen(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
+class OpenActivityFromBubbleOnLockreenTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
/** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt
similarity index 95%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt
index 0cda626..50507bf 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt
@@ -42,7 +42,7 @@
@RequiresDevice
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-open class ExpandBubbleScreen(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
+open class OpenActivityFromBubbleTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
/** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTestCfArm.kt
similarity index 90%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenCfArm.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTestCfArm.kt
index 69eb06f..94147e8 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTestCfArm.kt
@@ -23,4 +23,4 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-open class MultiBubblesScreenCfArm(flicker: FlickerTest) : MultiBubblesScreen(flicker)
+class OpenActivityFromBubbleTestCfArm(flicker: FlickerTest) : OpenActivityFromBubbleTest(flicker)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt
similarity index 95%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt
index 5c0f854..4be4dcd 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt
@@ -41,7 +41,7 @@
@RequiresDevice
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-open class LaunchBubbleScreen(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
+open class SendBubbleNotificationTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
/** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTestCfArm.kt
similarity index 89%
copy from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenCfArm.kt
copy to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTestCfArm.kt
index 69eb06f..7efbcdb 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTestCfArm.kt
@@ -23,4 +23,5 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-open class MultiBubblesScreenCfArm(flicker: FlickerTest) : MultiBubblesScreen(flicker)
+open class SendBubbleNotificationTestCfArm(flicker: FlickerTest) :
+ SendBubbleNotificationTest(flicker)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
index 7aa40e7..82617dd 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
@@ -82,13 +82,33 @@
@Presubmit
@Test
- override fun pipAppLayerAlwaysVisible() {
- if (!flicker.scenario.isGesturalNavigation) super.pipAppLayerAlwaysVisible()
- else {
- // pip layer in gesture nav will disappear during transition
- flicker.assertLayers {
- this.isVisible(pipApp).then().isInvisible(pipApp).then().isVisible(pipApp)
- }
+ override fun pipAppLayerOrOverlayAlwaysVisible() {
+ // pip layer in gesture nav will disappear during transition
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+ super.pipAppLayerOrOverlayAlwaysVisible()
+ }
+
+ @Presubmit
+ @Test
+ fun pipAppWindowVisibleChanges() {
+ // pip layer in gesture nav will disappear during transition
+ Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
+ flicker.assertWm {
+ this.isAppWindowVisible(pipApp)
+ .then()
+ .isAppWindowInvisible(pipApp, isOptional = true)
+ .then()
+ .isAppWindowVisible(pipApp, isOptional = true)
+ }
+ }
+
+ @Presubmit
+ @Test
+ fun pipAppLayerVisibleChanges() {
+ Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
+ // pip layer in gesture nav will disappear during transition
+ flicker.assertLayers {
+ this.isVisible(pipApp).then().isInvisible(pipApp).then().isVisible(pipApp)
}
}
@@ -116,12 +136,19 @@
@Presubmit
@Test
- override fun pipLayerRemainInsideVisibleBounds() {
- if (!flicker.scenario.isGesturalNavigation) super.pipLayerRemainInsideVisibleBounds()
- else {
- // pip layer in gesture nav will disappear during transition
- flicker.assertLayersStart { this.visibleRegion(pipApp).coversAtMost(displayBounds) }
- flicker.assertLayersEnd { this.visibleRegion(pipApp).coversAtMost(displayBounds) }
- }
+ override fun pipLayerOrOverlayRemainInsideVisibleBounds() {
+ // pip layer in gesture nav will disappear during transition
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+ super.pipLayerOrOverlayRemainInsideVisibleBounds()
+ }
+
+ @Presubmit
+ @Test
+ fun pipLayerRemainInsideVisibleBounds() {
+ // pip layer in gesture nav will disappear during transition
+ Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
+ // pip layer in gesture nav will disappear during transition
+ flicker.assertLayersStart { this.visibleRegion(pipApp).coversAtMost(displayBounds) }
+ flicker.assertLayersEnd { this.visibleRegion(pipApp).coversAtMost(displayBounds) }
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
index 5d7003f..33a6405 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
@@ -74,8 +74,14 @@
/** Checks [pipApp] layer remains visible throughout the animation */
@Presubmit
@Test
- open fun pipAppLayerAlwaysVisible() {
- flicker.assertLayers { this.isVisible(pipApp) }
+ open fun pipAppLayerOrOverlayAlwaysVisible() {
+ flicker.assertLayers {
+ this.isVisible(pipApp)
+ .then()
+ .isVisible(ComponentNameMatcher.PIP_CONTENT_OVERLAY)
+ .then()
+ .isVisible(pipApp)
+ }
}
/**
@@ -94,8 +100,10 @@
*/
@Presubmit
@Test
- open fun pipLayerRemainInsideVisibleBounds() {
- flicker.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) }
+ open fun pipLayerOrOverlayRemainInsideVisibleBounds() {
+ flicker.assertLayersVisibleRegion(pipApp.or(ComponentNameMatcher.PIP_CONTENT_OVERLAY) ) {
+ coversAtMost(displayBounds)
+ }
}
/** Checks that the visible region of [pipApp] always reduces during the animation */
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
index 02d50f4..afc4106 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
@@ -17,6 +17,7 @@
package com.android.wm.shell.flicker.pip
import android.app.Activity
+import android.platform.test.annotations.FlakyTest
import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import androidx.test.filters.RequiresDevice
@@ -197,7 +198,7 @@
}
/** {@inheritDoc} */
- @Presubmit
+ @FlakyTest(bugId = 267424412)
@Test
override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
super.visibleLayersShownMoreThanOneConsecutiveEntry()
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt
index 7466916..691b087 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt
@@ -19,6 +19,7 @@
import android.platform.test.annotations.Presubmit
import com.android.server.wm.flicker.FlickerTest
import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.server.wm.traces.common.ComponentNameMatcher
import org.junit.Test
/** Base class for pip expand tests */
@@ -32,7 +33,9 @@
@Presubmit
@Test
open fun pipAppWindowRemainInsideVisibleBounds() {
- flicker.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) }
+ flicker.assertWmVisibleRegion(pipApp.or(ComponentNameMatcher.TRANSITION_SNAPSHOT)) {
+ coversAtMost(displayBounds)
+ }
}
/**
@@ -42,7 +45,9 @@
@Presubmit
@Test
open fun pipAppLayerRemainInsideVisibleBounds() {
- flicker.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) }
+ flicker.assertLayersVisibleRegion(pipApp.or(ComponentNameMatcher.TRANSITION_SNAPSHOT)) {
+ coversAtMost(displayBounds)
+ }
}
/**
@@ -72,7 +77,10 @@
@Test
open fun showBothAppLayersThenHidePip() {
flicker.assertLayers {
- isVisible(testApp).isVisible(pipApp).then().isInvisible(testApp).isVisible(pipApp)
+ isVisible(testApp)
+ .isVisible(pipApp.or(ComponentNameMatcher.TRANSITION_SNAPSHOT))
+ .then()
+ .isInvisible(testApp).isVisible(pipApp)
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt
index 54ad991..1b48965 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt
@@ -76,7 +76,9 @@
?.height
?: error("Couldn't find Nav or Task bar layer")
// The dismiss button doesn't appear at the complete bottom of the screen,
- val displayY = device.displayHeight - barLayerHeight
+ // it appears above the hot seat but `hotseatBarSize` is not available outside
+ // the platform
+ val displayY = (device.displayHeight * 0.9).toInt() - barLayerHeight
device.swipe(pipCenterX, pipCenterY, displayCenterX, displayY, 50)
// Wait until the other app is no longer visible
wmHelper
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
index ffb1a4d..01e2f98 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
@@ -17,7 +17,7 @@
package com.android.wm.shell.common;
import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.InsetsState.ITYPE_IME;
+import static android.view.InsetsSource.ID_IME;
import static android.view.Surface.ROTATION_0;
import static android.view.WindowInsets.Type.ime;
@@ -138,15 +138,15 @@
private InsetsSourceControl[] insetsSourceControl() {
return new InsetsSourceControl[]{
new InsetsSourceControl(
- ITYPE_IME, ime(), mock(SurfaceControl.class), false, new Point(0, 0),
+ ID_IME, ime(), mock(SurfaceControl.class), false, new Point(0, 0),
Insets.NONE)
};
}
private InsetsState insetsStateWithIme(boolean visible) {
InsetsState state = new InsetsState();
- state.addSource(new InsetsSource(ITYPE_IME, ime()));
- state.setSourceVisible(ITYPE_IME, visible);
+ state.addSource(new InsetsSource(ID_IME, ime()));
+ state.setSourceVisible(ID_IME, visible);
return state;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
index 3d8cd2e..ec264a6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
@@ -300,9 +300,9 @@
(MAX_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2
};
final Size[] minimalSizes = new Size[] {
- new Size((int) (100 * aspectRatios[0]), 100),
- new Size((int) (100 * aspectRatios[1]), 100),
- new Size((int) (100 * aspectRatios[2]), 100)
+ new Size((int) (200 * aspectRatios[0]), 200),
+ new Size((int) (200 * aspectRatios[1]), 200),
+ new Size((int) (200 * aspectRatios[2]), 200)
};
for (int i = 0; i < aspectRatios.length; i++) {
final float aspectRatio = aspectRatios[i];
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java
index ede5bde..341a451 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java
@@ -162,10 +162,10 @@
@Test
public void testSetOverrideMinSize_notChanged_callbackNotInvoked() {
final Runnable callback = mock(Runnable.class);
- mPipBoundsState.setOverrideMinSize(new Size(5, 5));
+ mPipBoundsState.setOverrideMinSize(new Size(100, 150));
mPipBoundsState.setOnMinimalSizeChangeCallback(callback);
- mPipBoundsState.setOverrideMinSize(new Size(5, 5));
+ mPipBoundsState.setOverrideMinSize(new Size(100, 150));
verify(callback, never()).run();
}
@@ -175,11 +175,11 @@
mPipBoundsState.setOverrideMinSize(null);
assertEquals(0, mPipBoundsState.getOverrideMinEdgeSize());
- mPipBoundsState.setOverrideMinSize(new Size(5, 10));
- assertEquals(5, mPipBoundsState.getOverrideMinEdgeSize());
+ mPipBoundsState.setOverrideMinSize(new Size(100, 110));
+ assertEquals(100, mPipBoundsState.getOverrideMinEdgeSize());
- mPipBoundsState.setOverrideMinSize(new Size(15, 10));
- assertEquals(10, mPipBoundsState.getOverrideMinEdgeSize());
+ mPipBoundsState.setOverrideMinSize(new Size(150, 200));
+ assertEquals(150, mPipBoundsState.getOverrideMinEdgeSize());
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index ea3af9d..d0e2601 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -27,8 +27,6 @@
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -201,7 +199,6 @@
ActivityManager.RunningTaskInfo topRunningTask =
createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask();
- doReturn(true).when(mStageCoordinator).isValidToEnterSplitScreen(any());
mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
@@ -222,7 +219,6 @@
ActivityManager.RunningTaskInfo topRunningTask =
createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask();
- doReturn(true).when(mStageCoordinator).isValidToEnterSplitScreen(any());
// Put the same component into a task in the background
ActivityManager.RecentTaskInfo sameTaskInfo = new ActivityManager.RecentTaskInfo();
doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any());
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index d116ed8..eeed226 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -338,6 +338,7 @@
"jni/android_util_PathParser.cpp",
"jni/Bitmap.cpp",
+ "jni/HardwareBufferHelpers.cpp",
"jni/BitmapFactory.cpp",
"jni/ByteBufferStreamAdaptor.cpp",
"jni/Camera.cpp",
@@ -414,6 +415,7 @@
"jni/AnimatedImageDrawable.cpp",
"jni/android_graphics_TextureLayer.cpp",
"jni/android_graphics_HardwareRenderer.cpp",
+ "jni/android_graphics_HardwareBufferRenderer.cpp",
"jni/BitmapRegionDecoder.cpp",
"jni/GIFMovie.cpp",
"jni/GraphicsStatsService.cpp",
@@ -551,6 +553,7 @@
"RootRenderNode.cpp",
"SkiaCanvas.cpp",
"SkiaInterpolator.cpp",
+ "Tonemapper.cpp",
"VectorDrawable.cpp",
],
@@ -609,7 +612,6 @@
"ProfileData.cpp",
"ProfileDataContainer.cpp",
"Readback.cpp",
- "Tonemapper.cpp",
"TreeInfo.cpp",
"WebViewFunctorManager.cpp",
"protos/graphicsstats.proto",
diff --git a/libs/hwui/CanvasTransform.h b/libs/hwui/CanvasTransform.h
index c46a2d3..291f4cf 100644
--- a/libs/hwui/CanvasTransform.h
+++ b/libs/hwui/CanvasTransform.h
@@ -45,4 +45,4 @@
SkColor transformColor(ColorTransform transform, SkColor color);
SkColor transformColorInverse(ColorTransform transform, SkColor color);
-} // namespace android::uirenderer;
\ No newline at end of file
+} // namespace android::uirenderer
diff --git a/libs/hwui/ColorMode.h b/libs/hwui/ColorMode.h
index e45db01..959cf74 100644
--- a/libs/hwui/ColorMode.h
+++ b/libs/hwui/ColorMode.h
@@ -27,6 +27,9 @@
WideColorGamut = 1,
// Extended range Display P3
Hdr = 2,
+ // Extended range Display P3 10-bit
+ // for test purposes only, not shippable due to insuffient alpha
+ Hdr10 = 3,
// Alpha 8
A8 = 4,
};
diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp
index 3f7c4f0..430e69e 100644
--- a/libs/hwui/RecordingCanvas.cpp
+++ b/libs/hwui/RecordingCanvas.cpp
@@ -43,6 +43,7 @@
#include "SkRegion.h"
#include "SkTextBlob.h"
#include "SkVertices.h"
+#include "Tonemapper.h"
#include "VectorDrawable.h"
#include "include/gpu/GpuTypes.h" // from Skia
#include "include/gpu/GrDirectContext.h"
@@ -344,7 +345,9 @@
SkPaint paint;
BitmapPalette palette;
void draw(SkCanvas* c, const SkMatrix&) const {
- c->drawImage(image.get(), x, y, sampling, &paint);
+ SkPaint newPaint = paint;
+ tonemapPaint(image->imageInfo(), c->imageInfo(), -1, newPaint);
+ c->drawImage(image.get(), x, y, sampling, &newPaint);
}
};
struct DrawImageRect final : Op {
@@ -366,7 +369,9 @@
SkCanvas::SrcRectConstraint constraint;
BitmapPalette palette;
void draw(SkCanvas* c, const SkMatrix&) const {
- c->drawImageRect(image.get(), src, dst, sampling, &paint, constraint);
+ SkPaint newPaint = paint;
+ tonemapPaint(image->imageInfo(), c->imageInfo(), -1, newPaint);
+ c->drawImageRect(image.get(), src, dst, sampling, &newPaint, constraint);
}
};
struct DrawImageLattice final : Op {
@@ -399,8 +404,10 @@
auto flags =
(0 == fs) ? nullptr : pod<SkCanvas::Lattice::RectType>(
this, (xs + ys) * sizeof(int) + fs * sizeof(SkColor));
- c->drawImageLattice(image.get(), {xdivs, ydivs, flags, xs, ys, &src, colors}, dst,
- filter, &paint);
+ SkPaint newPaint = paint;
+ tonemapPaint(image->imageInfo(), c->imageInfo(), -1, newPaint);
+ c->drawImageLattice(image.get(), {xdivs, ydivs, flags, xs, ys, &src, colors}, dst, filter,
+ &newPaint);
}
};
diff --git a/libs/hwui/Tonemapper.cpp b/libs/hwui/Tonemapper.cpp
index a7e76b6..0d39f0e 100644
--- a/libs/hwui/Tonemapper.cpp
+++ b/libs/hwui/Tonemapper.cpp
@@ -18,7 +18,10 @@
#include <SkRuntimeEffect.h>
#include <log/log.h>
+// libshaders only exists on Android devices
+#ifdef __ANDROID__
#include <shaders/shaders.h>
+#endif
#include "utils/Color.h"
@@ -26,6 +29,8 @@
namespace {
+// custom tonemapping only exists on Android devices
+#ifdef __ANDROID__
class ColorFilterRuntimeEffectBuilder : public SkRuntimeEffectBuilder {
public:
explicit ColorFilterRuntimeEffectBuilder(sk_sp<SkRuntimeEffect> effect)
@@ -59,20 +64,21 @@
return effectBuilder.makeColorFilter();
}
-static bool extractTransfer(ui::Dataspace dataspace) {
- return dataspace & HAL_DATASPACE_TRANSFER_MASK;
+static ui::Dataspace extractTransfer(ui::Dataspace dataspace) {
+ return static_cast<ui::Dataspace>(dataspace & HAL_DATASPACE_TRANSFER_MASK);
}
static bool isHdrDataspace(ui::Dataspace dataspace) {
const auto transfer = extractTransfer(dataspace);
- return transfer == HAL_DATASPACE_TRANSFER_ST2084 || transfer == HAL_DATASPACE_TRANSFER_HLG;
+ return transfer == ui::Dataspace::TRANSFER_ST2084 || transfer == ui::Dataspace::TRANSFER_HLG;
}
static ui::Dataspace getDataspace(const SkImageInfo& image) {
return static_cast<ui::Dataspace>(
ColorSpaceToADataSpace(image.colorSpace(), image.colorType()));
}
+#endif
} // namespace
@@ -80,6 +86,8 @@
// shader and tag it on the supplied paint.
void tonemapPaint(const SkImageInfo& source, const SkImageInfo& destination, float maxLuminanceNits,
SkPaint& paint) {
+// custom tonemapping only exists on Android devices
+#ifdef __ANDROID__
const auto sourceDataspace = getDataspace(source);
const auto destinationDataspace = getDataspace(destination);
@@ -102,6 +110,9 @@
paint.setColorFilter(colorFilter);
}
}
+#else
+ return;
+#endif
}
} // namespace android::uirenderer
diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp
index d07fc5d..c509ed4 100644
--- a/libs/hwui/apex/jni_runtime.cpp
+++ b/libs/hwui/apex/jni_runtime.cpp
@@ -84,6 +84,7 @@
extern int register_android_view_DisplayListCanvas(JNIEnv* env);
extern int register_android_view_RenderNode(JNIEnv* env);
extern int register_android_view_ThreadedRenderer(JNIEnv* env);
+extern int register_android_graphics_HardwareBufferRenderer(JNIEnv* env);
#ifdef NDEBUG
#define REG_JNI(name) { name }
@@ -153,6 +154,8 @@
REG_JNI(register_android_util_PathParser),
REG_JNI(register_android_view_RenderNode),
REG_JNI(register_android_view_DisplayListCanvas),
+ REG_JNI(register_android_graphics_HardwareBufferRenderer),
+
REG_JNI(register_android_view_ThreadedRenderer),
};
diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp
index 0a755f0..cbebae9 100644
--- a/libs/hwui/hwui/Bitmap.cpp
+++ b/libs/hwui/hwui/Bitmap.cpp
@@ -41,6 +41,7 @@
#include <SkHighContrastFilter.h>
#include <SkImageEncoder.h>
#include <SkImagePriv.h>
+#include <SkJpegGainmapEncoder.h>
#include <SkPixmap.h>
#include <SkRect.h>
#include <SkStream.h>
@@ -458,6 +459,16 @@
}
bool Bitmap::compress(JavaCompressFormat format, int32_t quality, SkWStream* stream) {
+#ifdef __ANDROID__ // TODO: This isn't built for host for some reason?
+ if (hasGainmap() && format == JavaCompressFormat::Jpeg) {
+ SkBitmap baseBitmap = getSkBitmap();
+ SkBitmap gainmapBitmap = gainmap()->bitmap->getSkBitmap();
+ SkJpegEncoder::Options options{.fQuality = quality};
+ return SkJpegGainmapEncoder::EncodeJpegR(stream, baseBitmap.pixmap(), options,
+ gainmapBitmap.pixmap(), options, gainmap()->info);
+ }
+#endif
+
SkBitmap skbitmap;
getSkBitmap(&skbitmap);
return compress(skbitmap, format, quality, stream);
diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp
index 1f5a1d3..10c287d 100644
--- a/libs/hwui/jni/Bitmap.cpp
+++ b/libs/hwui/jni/Bitmap.cpp
@@ -2,49 +2,42 @@
#define LOG_TAG "Bitmap"
#include "Bitmap.h"
+#include <hwui/Bitmap.h>
+#include <hwui/Paint.h>
+
+#include "CreateJavaOutputStreamAdaptor.h"
#include "Gainmap.h"
#include "GraphicsJNI.h"
+#include "HardwareBufferHelpers.h"
#include "SkBitmap.h"
#include "SkBlendMode.h"
#include "SkCanvas.h"
#include "SkColor.h"
#include "SkColorSpace.h"
#include "SkData.h"
-#include "SkImageEncoder.h"
#include "SkImageInfo.h"
#include "SkPaint.h"
-#include "SkPixelRef.h"
#include "SkPixmap.h"
#include "SkPoint.h"
#include "SkRefCnt.h"
#include "SkStream.h"
#include "SkTypes.h"
-#include "SkWebpEncoder.h"
-
-
#include "android_nio_utils.h"
-#include "CreateJavaOutputStreamAdaptor.h"
-#include <hwui/Paint.h>
-#include <hwui/Bitmap.h>
-#include <utils/Color.h>
#ifdef __ANDROID__ // Layoutlib does not support graphic buffer, parcel or render thread
#include <android-base/unique_fd.h>
#include <android/binder_parcel.h>
#include <android/binder_parcel_jni.h>
#include <android/binder_parcel_platform.h>
-#include <android/binder_parcel_utils.h>
-#include <private/android/AHardwareBufferHelpers.h>
#include <cutils/ashmem.h>
-#include <dlfcn.h>
#include <renderthread/RenderProxy.h>
#include <sys/mman.h>
#endif
#include <inttypes.h>
#include <string.h>
+
#include <memory>
-#include <string>
#define DEBUG_PARCEL 0
@@ -1205,18 +1198,11 @@
return createBitmap(env, bitmap.release(), getPremulBitmapCreateFlags(false));
}
-#ifdef __ANDROID__ // Layoutlib does not support graphic buffer
-typedef AHardwareBuffer* (*AHB_from_HB)(JNIEnv*, jobject);
-AHB_from_HB AHardwareBuffer_fromHardwareBuffer;
-
-typedef jobject (*AHB_to_HB)(JNIEnv*, AHardwareBuffer*);
-AHB_to_HB AHardwareBuffer_toHardwareBuffer;
-#endif
-
static jobject Bitmap_wrapHardwareBufferBitmap(JNIEnv* env, jobject, jobject hardwareBuffer,
jlong colorSpacePtr) {
#ifdef __ANDROID__ // Layoutlib does not support graphic buffer
- AHardwareBuffer* buffer = AHardwareBuffer_fromHardwareBuffer(env, hardwareBuffer);
+ AHardwareBuffer* buffer = uirenderer::HardwareBufferHelpers::AHardwareBuffer_fromHardwareBuffer(
+ env, hardwareBuffer);
sk_sp<Bitmap> bitmap = Bitmap::createFrom(buffer,
GraphicsJNI::getNativeColorSpace(colorSpacePtr));
if (!bitmap.get()) {
@@ -1239,7 +1225,8 @@
}
Bitmap& bitmap = bitmapHandle->bitmap();
- return AHardwareBuffer_toHardwareBuffer(env, bitmap.hardwareBuffer());
+ return uirenderer::HardwareBufferHelpers::AHardwareBuffer_toHardwareBuffer(
+ env, bitmap.hardwareBuffer());
#else
return nullptr;
#endif
@@ -1347,18 +1334,7 @@
gBitmap_nativePtr = GetFieldIDOrDie(env, gBitmap_class, "mNativePtr", "J");
gBitmap_constructorMethodID = GetMethodIDOrDie(env, gBitmap_class, "<init>", "(JIIIZ[BLandroid/graphics/NinePatch$InsetStruct;Z)V");
gBitmap_reinitMethodID = GetMethodIDOrDie(env, gBitmap_class, "reinit", "(IIZ)V");
-
-#ifdef __ANDROID__ // Layoutlib does not support graphic buffer or parcel
- void* handle_ = dlopen("libandroid.so", RTLD_NOW | RTLD_NODELETE);
- AHardwareBuffer_fromHardwareBuffer =
- (AHB_from_HB)dlsym(handle_, "AHardwareBuffer_fromHardwareBuffer");
- LOG_ALWAYS_FATAL_IF(AHardwareBuffer_fromHardwareBuffer == nullptr,
- "Failed to find required symbol AHardwareBuffer_fromHardwareBuffer!");
-
- AHardwareBuffer_toHardwareBuffer = (AHB_to_HB)dlsym(handle_, "AHardwareBuffer_toHardwareBuffer");
- LOG_ALWAYS_FATAL_IF(AHardwareBuffer_toHardwareBuffer == nullptr,
- " Failed to find required symbol AHardwareBuffer_toHardwareBuffer!");
-#endif
+ uirenderer::HardwareBufferHelpers::init();
return android::RegisterMethodsOrDie(env, "android/graphics/Bitmap", gBitmapMethods,
NELEM(gBitmapMethods));
}
diff --git a/libs/hwui/jni/GraphicsJNI.h b/libs/hwui/jni/GraphicsJNI.h
index 2c556af..6b983c1 100644
--- a/libs/hwui/jni/GraphicsJNI.h
+++ b/libs/hwui/jni/GraphicsJNI.h
@@ -2,19 +2,18 @@
#define _ANDROID_GRAPHICS_GRAPHICS_JNI_H_
#include <cutils/compiler.h>
+#include <hwui/Bitmap.h>
+#include <hwui/Canvas.h>
-#include "Bitmap.h"
#include "BRDAllocator.h"
+#include "Bitmap.h"
#include "SkBitmap.h"
#include "SkCodec.h"
-#include "SkPixelRef.h"
+#include "SkColorSpace.h"
#include "SkMallocPixelRef.h"
+#include "SkPixelRef.h"
#include "SkPoint.h"
#include "SkRect.h"
-#include "SkColorSpace.h"
-#include <hwui/Canvas.h>
-#include <hwui/Bitmap.h>
-
#include "graphics_jni_helpers.h"
class SkCanvas;
@@ -333,6 +332,26 @@
int fLen;
};
+class JGlobalRefHolder {
+public:
+ JGlobalRefHolder(JavaVM* vm, jobject object) : mVm(vm), mObject(object) {}
+
+ virtual ~JGlobalRefHolder() {
+ GraphicsJNI::getJNIEnv()->DeleteGlobalRef(mObject);
+ mObject = nullptr;
+ }
+
+ jobject object() { return mObject; }
+ JavaVM* vm() { return mVm; }
+
+private:
+ JGlobalRefHolder(const JGlobalRefHolder&) = delete;
+ void operator=(const JGlobalRefHolder&) = delete;
+
+ JavaVM* mVm;
+ jobject mObject;
+};
+
void doThrowNPE(JNIEnv* env);
void doThrowAIOOBE(JNIEnv* env); // Array Index Out Of Bounds Exception
void doThrowIAE(JNIEnv* env, const char* msg = NULL); // Illegal Argument
diff --git a/libs/hwui/jni/HardwareBufferHelpers.cpp b/libs/hwui/jni/HardwareBufferHelpers.cpp
new file mode 100644
index 0000000..7e3f771
--- /dev/null
+++ b/libs/hwui/jni/HardwareBufferHelpers.cpp
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+#include "HardwareBufferHelpers.h"
+
+#include <dlfcn.h>
+#include <log/log.h>
+
+#ifdef __ANDROID__
+typedef AHardwareBuffer* (*AHB_from_HB)(JNIEnv*, jobject);
+typedef jobject (*AHB_to_HB)(JNIEnv*, AHardwareBuffer*);
+static AHB_from_HB fromHardwareBuffer = nullptr;
+static AHB_to_HB toHardwareBuffer = nullptr;
+#endif
+
+void android::uirenderer::HardwareBufferHelpers::init() {
+#ifdef __ANDROID__ // Layoutlib does not support graphic buffer or parcel
+ void* handle_ = dlopen("libandroid.so", RTLD_NOW | RTLD_NODELETE);
+ fromHardwareBuffer = (AHB_from_HB)dlsym(handle_, "AHardwareBuffer_fromHardwareBuffer");
+ LOG_ALWAYS_FATAL_IF(fromHardwareBuffer == nullptr,
+ "Failed to find required symbol AHardwareBuffer_fromHardwareBuffer!");
+
+ toHardwareBuffer = (AHB_to_HB)dlsym(handle_, "AHardwareBuffer_toHardwareBuffer");
+ LOG_ALWAYS_FATAL_IF(toHardwareBuffer == nullptr,
+ " Failed to find required symbol AHardwareBuffer_toHardwareBuffer!");
+#endif
+}
+
+AHardwareBuffer* android::uirenderer::HardwareBufferHelpers::AHardwareBuffer_fromHardwareBuffer(
+ JNIEnv* env, jobject hardwarebuffer) {
+#ifdef __ANDROID__
+ LOG_ALWAYS_FATAL_IF(fromHardwareBuffer == nullptr,
+ "Failed to find symbol AHardwareBuffer_fromHardwareBuffer, did you forget "
+ "to call HardwareBufferHelpers::init?");
+ return fromHardwareBuffer(env, hardwarebuffer);
+#else
+ ALOGE("ERROR attempting to invoke AHardwareBuffer_fromHardwareBuffer on non Android "
+ "configuration");
+ return nullptr;
+#endif
+}
+
+jobject android::uirenderer::HardwareBufferHelpers::AHardwareBuffer_toHardwareBuffer(
+ JNIEnv* env, AHardwareBuffer* ahardwarebuffer) {
+#ifdef __ANDROID__
+ LOG_ALWAYS_FATAL_IF(toHardwareBuffer == nullptr,
+ "Failed to find symbol AHardwareBuffer_toHardwareBuffer, did you forget to "
+ "call HardwareBufferHelpers::init?");
+ return toHardwareBuffer(env, ahardwarebuffer);
+#else
+ ALOGE("ERROR attempting to invoke AHardwareBuffer_toHardwareBuffer on non Android "
+ "configuration");
+ return nullptr;
+#endif
+}
\ No newline at end of file
diff --git a/libs/hwui/jni/HardwareBufferHelpers.h b/libs/hwui/jni/HardwareBufferHelpers.h
new file mode 100644
index 0000000..326babf
--- /dev/null
+++ b/libs/hwui/jni/HardwareBufferHelpers.h
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+#ifndef HARDWAREBUFFER_JNI_HELPERS_H
+#define HARDWAREBUFFER_JNI_HELPERS_H
+
+#include <android/bitmap.h>
+#include <jni.h>
+
+namespace android {
+namespace uirenderer {
+
+class HardwareBufferHelpers {
+public:
+ static void init();
+ static AHardwareBuffer* AHardwareBuffer_fromHardwareBuffer(JNIEnv*, jobject);
+ static jobject AHardwareBuffer_toHardwareBuffer(JNIEnv*, AHardwareBuffer*);
+
+private:
+ HardwareBufferHelpers() = default; // not to be instantiated
+};
+
+} // namespace uirenderer
+} // namespace android
+
+#endif // HARDWAREBUFFER_JNI_HELPERS_H
\ No newline at end of file
diff --git a/libs/hwui/jni/JvmErrorReporter.h b/libs/hwui/jni/JvmErrorReporter.h
new file mode 100644
index 0000000..5e10b9d
--- /dev/null
+++ b/libs/hwui/jni/JvmErrorReporter.h
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+#ifndef JVMERRORREPORTER_H
+#define JVMERRORREPORTER_H
+
+#include <TreeInfo.h>
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+
+#include "GraphicsJNI.h"
+
+namespace android {
+namespace uirenderer {
+
+class JvmErrorReporter : public android::uirenderer::ErrorHandler {
+public:
+ JvmErrorReporter(JNIEnv* env) { env->GetJavaVM(&mVm); }
+
+ virtual void onError(const std::string& message) override {
+ JNIEnv* env = GraphicsJNI::getJNIEnv();
+ jniThrowException(env, "java/lang/IllegalStateException", message.c_str());
+ }
+
+private:
+ JavaVM* mVm;
+};
+
+} // namespace uirenderer
+} // namespace android
+
+#endif // JVMERRORREPORTER_H
diff --git a/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp
new file mode 100644
index 0000000..3e453e6
--- /dev/null
+++ b/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+#undef LOG_TAG
+#define LOG_TAG "HardwareBufferRenderer"
+#define ATRACE_TAG ATRACE_TAG_VIEW
+
+#include <GraphicsJNI.h>
+#include <RootRenderNode.h>
+#include <TreeInfo.h>
+#include <android-base/unique_fd.h>
+#include <android/native_window.h>
+#include <nativehelper/JNIPlatformHelp.h>
+#include <renderthread/CanvasContext.h>
+#include <renderthread/RenderProxy.h>
+#include <renderthread/RenderThread.h>
+
+#include "HardwareBufferHelpers.h"
+#include "JvmErrorReporter.h"
+
+namespace android {
+
+using namespace android::uirenderer;
+using namespace android::uirenderer::renderthread;
+
+struct {
+ jclass clazz;
+ jmethodID invokeRenderCallback;
+} gHardwareBufferRendererClassInfo;
+
+static RenderCallback createRenderCallback(JNIEnv* env, jobject releaseCallback) {
+ if (releaseCallback == nullptr) return nullptr;
+
+ JavaVM* vm = nullptr;
+ LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&vm) != JNI_OK, "Unable to get Java VM");
+ auto globalCallbackRef =
+ std::make_shared<JGlobalRefHolder>(vm, env->NewGlobalRef(releaseCallback));
+ return [globalCallbackRef](android::base::unique_fd&& fd, int status) {
+ GraphicsJNI::getJNIEnv()->CallStaticVoidMethod(
+ gHardwareBufferRendererClassInfo.clazz,
+ gHardwareBufferRendererClassInfo.invokeRenderCallback, globalCallbackRef->object(),
+ reinterpret_cast<jint>(fd.release()), reinterpret_cast<jint>(status));
+ };
+}
+
+static long android_graphics_HardwareBufferRenderer_createRootNode(JNIEnv* env, jobject) {
+ auto* node = new RootRenderNode(std::make_unique<JvmErrorReporter>(env));
+ node->incStrong(nullptr);
+ node->setName("RootRenderNode");
+ return reinterpret_cast<jlong>(node);
+}
+
+static void android_graphics_hardwareBufferRenderer_destroyRootNode(JNIEnv*, jobject,
+ jlong renderNodePtr) {
+ auto* node = reinterpret_cast<RootRenderNode*>(renderNodePtr);
+ node->destroy();
+}
+
+static long android_graphics_HardwareBufferRenderer_create(JNIEnv* env, jobject, jobject buffer,
+ jlong renderNodePtr) {
+ auto* hardwareBuffer = HardwareBufferHelpers::AHardwareBuffer_fromHardwareBuffer(env, buffer);
+ auto* rootRenderNode = reinterpret_cast<RootRenderNode*>(renderNodePtr);
+ ContextFactoryImpl factory(rootRenderNode);
+ auto* proxy = new RenderProxy(true, rootRenderNode, &factory);
+ proxy->setHardwareBuffer(hardwareBuffer);
+ return (jlong)proxy;
+}
+
+static void HardwareBufferRenderer_destroy(jlong renderProxy) {
+ auto* proxy = reinterpret_cast<RenderProxy*>(renderProxy);
+ delete proxy;
+}
+
+static SkMatrix createMatrixFromBufferTransform(SkScalar width, SkScalar height, int transform) {
+ auto matrix = SkMatrix();
+ switch (transform) {
+ case ANATIVEWINDOW_TRANSFORM_ROTATE_90:
+ matrix.setRotate(90);
+ matrix.postTranslate(width, 0);
+ break;
+ case ANATIVEWINDOW_TRANSFORM_ROTATE_180:
+ matrix.setRotate(180);
+ matrix.postTranslate(width, height);
+ break;
+ case ANATIVEWINDOW_TRANSFORM_ROTATE_270:
+ matrix.setRotate(270);
+ matrix.postTranslate(0, width);
+ break;
+ default:
+ ALOGE("Invalid transform provided. Transform should be validated from"
+ "the java side. Leveraging identity transform as a fallback");
+ [[fallthrough]];
+ case ANATIVEWINDOW_TRANSFORM_IDENTITY:
+ break;
+ }
+ return matrix;
+}
+
+static int android_graphics_HardwareBufferRenderer_render(JNIEnv* env, jobject, jlong renderProxy,
+ jint transform, jint width, jint height,
+ jlong colorspacePtr, jobject consumer) {
+ auto* proxy = reinterpret_cast<RenderProxy*>(renderProxy);
+ auto skWidth = static_cast<SkScalar>(width);
+ auto skHeight = static_cast<SkScalar>(height);
+ auto matrix = createMatrixFromBufferTransform(skWidth, skHeight, transform);
+ auto colorSpace = GraphicsJNI::getNativeColorSpace(colorspacePtr);
+ proxy->setHardwareBufferRenderParams(
+ HardwareBufferRenderParams(matrix, colorSpace, createRenderCallback(env, consumer)));
+ nsecs_t vsync = systemTime(SYSTEM_TIME_MONOTONIC);
+ UiFrameInfoBuilder(proxy->frameInfo())
+ .setVsync(vsync, vsync, UiFrameInfoBuilder::INVALID_VSYNC_ID,
+ UiFrameInfoBuilder::UNKNOWN_DEADLINE,
+ UiFrameInfoBuilder::UNKNOWN_FRAME_INTERVAL)
+ .addFlag(FrameInfoFlags::SurfaceCanvas);
+ return proxy->syncAndDrawFrame();
+}
+
+static void android_graphics_HardwareBufferRenderer_setLightGeometry(JNIEnv*, jobject,
+ jlong renderProxyPtr,
+ jfloat lightX, jfloat lightY,
+ jfloat lightZ,
+ jfloat lightRadius) {
+ auto* proxy = reinterpret_cast<RenderProxy*>(renderProxyPtr);
+ proxy->setLightGeometry((Vector3){lightX, lightY, lightZ}, lightRadius);
+}
+
+static void android_graphics_HardwareBufferRenderer_setLightAlpha(JNIEnv* env, jobject,
+ jlong renderProxyPtr,
+ jfloat ambientShadowAlpha,
+ jfloat spotShadowAlpha) {
+ auto* proxy = reinterpret_cast<RenderProxy*>(renderProxyPtr);
+ proxy->setLightAlpha((uint8_t)(255 * ambientShadowAlpha), (uint8_t)(255 * spotShadowAlpha));
+}
+
+static jlong android_graphics_HardwareBufferRenderer_getFinalizer() {
+ return static_cast<jlong>(reinterpret_cast<uintptr_t>(&HardwareBufferRenderer_destroy));
+}
+
+// ----------------------------------------------------------------------------
+// JNI Glue
+// ----------------------------------------------------------------------------
+
+const char* const kClassPathName = "android/graphics/HardwareBufferRenderer";
+
+static const JNINativeMethod gMethods[] = {
+ {"nCreateHardwareBufferRenderer", "(Landroid/hardware/HardwareBuffer;J)J",
+ (void*)android_graphics_HardwareBufferRenderer_create},
+ {"nRender", "(JIIIJLjava/util/function/Consumer;)I",
+ (void*)android_graphics_HardwareBufferRenderer_render},
+ {"nCreateRootRenderNode", "()J",
+ (void*)android_graphics_HardwareBufferRenderer_createRootNode},
+ {"nSetLightGeometry", "(JFFFF)V",
+ (void*)android_graphics_HardwareBufferRenderer_setLightGeometry},
+ {"nSetLightAlpha", "(JFF)V", (void*)android_graphics_HardwareBufferRenderer_setLightAlpha},
+ {"nGetFinalizer", "()J", (void*)android_graphics_HardwareBufferRenderer_getFinalizer},
+ {"nDestroyRootRenderNode", "(J)V",
+ (void*)android_graphics_hardwareBufferRenderer_destroyRootNode}};
+
+int register_android_graphics_HardwareBufferRenderer(JNIEnv* env) {
+ jclass hardwareBufferRendererClazz =
+ FindClassOrDie(env, "android/graphics/HardwareBufferRenderer");
+ gHardwareBufferRendererClassInfo.clazz = hardwareBufferRendererClazz;
+ gHardwareBufferRendererClassInfo.invokeRenderCallback =
+ GetStaticMethodIDOrDie(env, hardwareBufferRendererClazz, "invokeRenderCallback",
+ "(Ljava/util/function/Consumer;II)V");
+ HardwareBufferHelpers::init();
+ return RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods));
+}
+
+} // namespace android
\ No newline at end of file
diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
index a30fc49..d6aad7d 100644
--- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
+++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
@@ -56,6 +56,7 @@
#include <atomic>
#include <vector>
+#include "JvmErrorReporter.h"
#include "android_graphics_HardwareRendererObserver.h"
namespace android {
@@ -93,35 +94,12 @@
jmethodID getDestinationBitmap;
} gCopyRequest;
-static JNIEnv* getenv(JavaVM* vm) {
- JNIEnv* env;
- if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
- LOG_ALWAYS_FATAL("Failed to get JNIEnv for JavaVM: %p", vm);
- }
- return env;
-}
-
typedef ANativeWindow* (*ANW_fromSurface)(JNIEnv* env, jobject surface);
ANW_fromSurface fromSurface;
-class JvmErrorReporter : public ErrorHandler {
-public:
- JvmErrorReporter(JNIEnv* env) {
- env->GetJavaVM(&mVm);
- }
-
- virtual void onError(const std::string& message) override {
- JNIEnv* env = getenv(mVm);
- jniThrowException(env, "java/lang/IllegalStateException", message.c_str());
- }
-private:
- JavaVM* mVm;
-};
-
class FrameCommitWrapper : public LightRefBase<FrameCommitWrapper> {
public:
explicit FrameCommitWrapper(JNIEnv* env, jobject jobject) {
- env->GetJavaVM(&mVm);
mObject = env->NewGlobalRef(jobject);
LOG_ALWAYS_FATAL_IF(!mObject, "Failed to make global ref");
}
@@ -131,19 +109,18 @@
void onFrameCommit(bool didProduceBuffer) {
if (mObject) {
ATRACE_FORMAT("frameCommit success=%d", didProduceBuffer);
- getenv(mVm)->CallVoidMethod(mObject, gFrameCommitCallback.onFrameCommit,
- didProduceBuffer);
+ GraphicsJNI::getJNIEnv()->CallVoidMethod(mObject, gFrameCommitCallback.onFrameCommit,
+ didProduceBuffer);
releaseObject();
}
}
private:
- JavaVM* mVm;
jobject mObject;
void releaseObject() {
if (mObject) {
- getenv(mVm)->DeleteGlobalRef(mObject);
+ GraphicsJNI::getJNIEnv()->DeleteGlobalRef(mObject);
mObject = nullptr;
}
}
@@ -449,26 +426,6 @@
proxy->forceDrawNextFrame();
}
-class JGlobalRefHolder {
-public:
- JGlobalRefHolder(JavaVM* vm, jobject object) : mVm(vm), mObject(object) {}
-
- virtual ~JGlobalRefHolder() {
- getenv(mVm)->DeleteGlobalRef(mObject);
- mObject = nullptr;
- }
-
- jobject object() { return mObject; }
- JavaVM* vm() { return mVm; }
-
-private:
- JGlobalRefHolder(const JGlobalRefHolder&) = delete;
- void operator=(const JGlobalRefHolder&) = delete;
-
- JavaVM* mVm;
- jobject mObject;
-};
-
using TextureMap = std::unordered_map<uint32_t, sk_sp<SkImage>>;
struct PictureCaptureState {
@@ -584,7 +541,7 @@
auto pictureState = std::make_shared<PictureCaptureState>();
proxy->setPictureCapturedCallback([globalCallbackRef,
pictureState](sk_sp<SkPicture>&& picture) {
- JNIEnv* env = getenv(globalCallbackRef->vm());
+ JNIEnv* env = GraphicsJNI::getJNIEnv();
Picture* wrapper = new PictureWrapper{std::move(picture), pictureState};
env->CallStaticVoidMethod(gHardwareRenderer.clazz,
gHardwareRenderer.invokePictureCapturedCallback,
@@ -606,7 +563,7 @@
vm, env->NewGlobalRef(aSurfaceTransactionCallback));
proxy->setASurfaceTransactionCallback(
[globalCallbackRef](int64_t transObj, int64_t scObj, int64_t frameNr) -> bool {
- JNIEnv* env = getenv(globalCallbackRef->vm());
+ JNIEnv* env = GraphicsJNI::getJNIEnv();
jboolean ret = env->CallBooleanMethod(
globalCallbackRef->object(),
gASurfaceTransactionCallback.onMergeTransaction,
@@ -628,7 +585,7 @@
auto globalCallbackRef =
std::make_shared<JGlobalRefHolder>(vm, env->NewGlobalRef(callback));
proxy->setPrepareSurfaceControlForWebviewCallback([globalCallbackRef]() {
- JNIEnv* env = getenv(globalCallbackRef->vm());
+ JNIEnv* env = GraphicsJNI::getJNIEnv();
env->CallVoidMethod(globalCallbackRef->object(),
gPrepareSurfaceControlForWebviewCallback.prepare);
});
@@ -647,7 +604,7 @@
env->NewGlobalRef(frameCallback));
proxy->setFrameCallback([globalCallbackRef](int32_t syncResult,
int64_t frameNr) -> std::function<void(bool)> {
- JNIEnv* env = getenv(globalCallbackRef->vm());
+ JNIEnv* env = GraphicsJNI::getJNIEnv();
ScopedLocalRef<jobject> frameCommitCallback(
env, env->CallObjectMethod(
globalCallbackRef->object(), gFrameDrawingCallback.onFrameDraw,
@@ -686,7 +643,7 @@
auto globalCallbackRef =
std::make_shared<JGlobalRefHolder>(vm, env->NewGlobalRef(callback));
proxy->setFrameCompleteCallback([globalCallbackRef]() {
- JNIEnv* env = getenv(globalCallbackRef->vm());
+ JNIEnv* env = GraphicsJNI::getJNIEnv();
env->CallVoidMethod(globalCallbackRef->object(),
gFrameCompleteCallback.onFrameComplete);
});
@@ -699,7 +656,7 @@
: CopyRequest(srcRect), mRefHolder(vm, jCopyRequest) {}
virtual SkBitmap getDestinationBitmap(int srcWidth, int srcHeight) override {
- JNIEnv* env = getenv(mRefHolder.vm());
+ JNIEnv* env = GraphicsJNI::getJNIEnv();
jlong bitmapPtr = env->CallLongMethod(
mRefHolder.object(), gCopyRequest.getDestinationBitmap, srcWidth, srcHeight);
SkBitmap bitmap;
@@ -708,7 +665,7 @@
}
virtual void onCopyFinished(CopyResult result) override {
- JNIEnv* env = getenv(mRefHolder.vm());
+ JNIEnv* env = GraphicsJNI::getJNIEnv();
env->CallVoidMethod(mRefHolder.object(), gCopyRequest.onCopyFinished,
static_cast<jint>(result));
}
diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
index 19cd7bd..202a62c 100644
--- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
@@ -55,7 +55,9 @@
MakeCurrentResult SkiaOpenGLPipeline::makeCurrent() {
// In case the surface was destroyed (e.g. a previous trimMemory call) we
// need to recreate it here.
- if (!isSurfaceReady() && mNativeWindow) {
+ if (mHardwareBuffer) {
+ mRenderThread.requireGlContext();
+ } else if (!isSurfaceReady() && mNativeWindow) {
setSurface(mNativeWindow.get(), mSwapBehavior);
}
@@ -67,17 +69,24 @@
}
Frame SkiaOpenGLPipeline::getFrame() {
- LOG_ALWAYS_FATAL_IF(mEglSurface == EGL_NO_SURFACE,
- "drawRenderNode called on a context with no surface!");
- return mEglManager.beginFrame(mEglSurface);
+ if (mHardwareBuffer) {
+ AHardwareBuffer_Desc description;
+ AHardwareBuffer_describe(mHardwareBuffer, &description);
+ return Frame(description.width, description.height, 0);
+ } else {
+ LOG_ALWAYS_FATAL_IF(mEglSurface == EGL_NO_SURFACE,
+ "drawRenderNode called on a context with no surface!");
+ return mEglManager.beginFrame(mEglSurface);
+ }
}
IRenderPipeline::DrawResult SkiaOpenGLPipeline::draw(
const Frame& frame, const SkRect& screenDirty, const SkRect& dirty,
const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
- const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler) {
- if (!isCapturingSkp()) {
+ const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler,
+ const HardwareBufferRenderParams& bufferParams) {
+ if (!isCapturingSkp() && !mHardwareBuffer) {
mEglManager.damageFrame(frame, dirty);
}
@@ -104,13 +113,25 @@
SkSurfaceProps props(0, kUnknown_SkPixelGeometry);
SkASSERT(mRenderThread.getGrContext() != nullptr);
- sk_sp<SkSurface> surface(SkSurface::MakeFromBackendRenderTarget(
- mRenderThread.getGrContext(), backendRT, this->getSurfaceOrigin(), colorType,
- mSurfaceColorSpace, &props));
+ sk_sp<SkSurface> surface;
+ SkMatrix preTransform;
+ if (mHardwareBuffer) {
+ surface = getBufferSkSurface(bufferParams);
+ preTransform = bufferParams.getTransform();
+ } else {
+ surface = SkSurface::MakeFromBackendRenderTarget(mRenderThread.getGrContext(), backendRT,
+ getSurfaceOrigin(), colorType,
+ mSurfaceColorSpace, &props);
+ preTransform = SkMatrix::I();
+ }
- LightingInfo::updateLighting(lightGeometry, lightInfo);
+ SkPoint lightCenter = preTransform.mapXY(lightGeometry.center.x, lightGeometry.center.y);
+ LightGeometry localGeometry = lightGeometry;
+ localGeometry.center.x = lightCenter.fX;
+ localGeometry.center.y = lightCenter.fY;
+ LightingInfo::updateLighting(localGeometry, lightInfo);
renderFrame(*layerUpdateQueue, dirty, renderNodes, opaque, contentDrawBounds, surface,
- SkMatrix::I());
+ preTransform);
// Draw visual debugging features
if (CC_UNLIKELY(Properties::showDirtyRegions ||
@@ -142,6 +163,10 @@
// metrics the frame was swapped at this point
currentFrameInfo->markSwapBuffers();
+ if (mHardwareBuffer) {
+ return false;
+ }
+
*requireSwap = drew || mEglManager.damageRequiresSwap();
if (*requireSwap && (CC_UNLIKELY(!mEglManager.swapBuffers(frame, screenDirty)))) {
@@ -197,6 +222,26 @@
return false;
}
+[[nodiscard]] android::base::unique_fd SkiaOpenGLPipeline::flush() {
+ int fence = -1;
+ EGLSyncKHR sync = EGL_NO_SYNC_KHR;
+ mEglManager.createReleaseFence(true, &sync, &fence);
+ // If a sync object is returned here then the device does not support native
+ // fences, we block on the returned sync and return -1 as a file descriptor
+ if (sync != EGL_NO_SYNC_KHR) {
+ EGLDisplay display = mEglManager.eglDisplay();
+ EGLint result = eglClientWaitSyncKHR(display, sync, 0, 1000000000);
+ if (result == EGL_FALSE) {
+ ALOGE("EglManager::createReleaseFence: error waiting for previous fence: %#x",
+ eglGetError());
+ } else if (result == EGL_TIMEOUT_EXPIRED_KHR) {
+ ALOGE("EglManager::createReleaseFence: timeout waiting for previous fence");
+ }
+ eglDestroySyncKHR(display, sync);
+ }
+ return android::base::unique_fd(fence);
+}
+
bool SkiaOpenGLPipeline::isSurfaceReady() {
return CC_UNLIKELY(mEglSurface != EGL_NO_SURFACE);
}
diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h
index a80c613..940d6bf 100644
--- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h
+++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h
@@ -21,6 +21,7 @@
#include "SkiaPipeline.h"
#include "renderstate/RenderState.h"
+#include "renderthread/HardwareBufferRenderParams.h"
namespace android {
@@ -36,19 +37,18 @@
renderthread::MakeCurrentResult makeCurrent() override;
renderthread::Frame getFrame() override;
- renderthread::IRenderPipeline::DrawResult draw(const renderthread::Frame& frame,
- const SkRect& screenDirty, const SkRect& dirty,
- const LightGeometry& lightGeometry,
- LayerUpdateQueue* layerUpdateQueue,
- const Rect& contentDrawBounds, bool opaque,
- const LightInfo& lightInfo,
- const std::vector<sp<RenderNode> >& renderNodes,
- FrameInfoVisualizer* profiler) override;
+ renderthread::IRenderPipeline::DrawResult draw(
+ const renderthread::Frame& frame, const SkRect& screenDirty, const SkRect& dirty,
+ const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
+ const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
+ const std::vector<sp<RenderNode> >& renderNodes, FrameInfoVisualizer* profiler,
+ const renderthread::HardwareBufferRenderParams& bufferParams) override;
GrSurfaceOrigin getSurfaceOrigin() override { return kBottomLeft_GrSurfaceOrigin; }
bool swapBuffers(const renderthread::Frame& frame, bool drew, const SkRect& screenDirty,
FrameInfo* currentFrameInfo, bool* requireSwap) override;
DeferredLayerUpdater* createTextureLayer() override;
bool setSurface(ANativeWindow* surface, renderthread::SwapBehavior swapBehavior) override;
+ [[nodiscard]] android::base::unique_fd flush() override;
void onStop() override;
bool isSurfaceReady() override;
bool isContextReady() override;
diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
index 09c3120..6628463 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
@@ -605,6 +605,31 @@
ALOGD("%s", log.c_str());
}
+void SkiaPipeline::setHardwareBuffer(AHardwareBuffer* buffer) {
+ if (mHardwareBuffer) {
+ AHardwareBuffer_release(mHardwareBuffer);
+ mHardwareBuffer = nullptr;
+ }
+
+ if (buffer) {
+ AHardwareBuffer_acquire(buffer);
+ mHardwareBuffer = buffer;
+ }
+}
+
+sk_sp<SkSurface> SkiaPipeline::getBufferSkSurface(
+ const renderthread::HardwareBufferRenderParams& bufferParams) {
+ auto bufferColorSpace = bufferParams.getColorSpace();
+ if (mBufferSurface == nullptr || mBufferColorSpace == nullptr ||
+ !SkColorSpace::Equals(mBufferColorSpace.get(), bufferColorSpace.get())) {
+ mBufferSurface = SkSurface::MakeFromAHardwareBuffer(
+ mRenderThread.getGrContext(), mHardwareBuffer, kTopLeft_GrSurfaceOrigin,
+ bufferColorSpace, nullptr, true);
+ mBufferColorSpace = bufferColorSpace;
+ }
+ return mBufferSurface;
+}
+
void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) {
mColorMode = colorMode;
switch (colorMode) {
@@ -621,6 +646,11 @@
mSurfaceColorSpace = SkColorSpace::MakeRGB(
GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3);
break;
+ case ColorMode::Hdr10:
+ mSurfaceColorType = SkColorType::kRGBA_1010102_SkColorType;
+ mSurfaceColorSpace = SkColorSpace::MakeRGB(
+ GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3);
+ break;
case ColorMode::A8:
mSurfaceColorType = SkColorType::kAlpha_8_SkColorType;
mSurfaceColorSpace = nullptr;
@@ -629,7 +659,7 @@
}
void SkiaPipeline::setTargetSdrHdrRatio(float ratio) {
- if (mColorMode == ColorMode::Hdr) {
+ if (mColorMode == ColorMode::Hdr || mColorMode == ColorMode::Hdr10) {
mTargetSdrHdrRatio = ratio;
mSurfaceColorSpace = SkColorSpace::MakeRGB(GetExtendedTransferFunction(mTargetSdrHdrRatio),
SkNamedGamut::kDisplayP3);
diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h
index b7d799f..befee89 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.h
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.h
@@ -20,9 +20,11 @@
#include <SkDocument.h>
#include <SkMultiPictureDocument.h>
#include <SkSurface.h>
+
#include "Lighting.h"
#include "hwui/AnimatedImageDrawable.h"
#include "renderthread/CanvasContext.h"
+#include "renderthread/HardwareBufferRenderParams.h"
#include "renderthread/IRenderPipeline.h"
class SkFILEWStream;
@@ -73,14 +75,22 @@
mCaptureMode = callback ? CaptureMode::CallbackAPI : CaptureMode::None;
}
+ virtual void setHardwareBuffer(AHardwareBuffer* buffer) override;
+ bool hasHardwareBuffer() override { return mHardwareBuffer != nullptr; }
void setTargetSdrHdrRatio(float ratio) override;
protected:
+ sk_sp<SkSurface> getBufferSkSurface(
+ const renderthread::HardwareBufferRenderParams& bufferParams);
void dumpResourceCacheUsage() const;
renderthread::RenderThread& mRenderThread;
+ AHardwareBuffer* mHardwareBuffer = nullptr;
+ sk_sp<SkSurface> mBufferSurface = nullptr;
+ sk_sp<SkColorSpace> mBufferColorSpace = nullptr;
+
ColorMode mColorMode = ColorMode::Default;
SkColorType mSurfaceColorType;
sk_sp<SkColorSpace> mSurfaceColorSpace;
diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
index a8c752f..99298bc 100644
--- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
@@ -57,37 +57,55 @@
MakeCurrentResult SkiaVulkanPipeline::makeCurrent() {
// In case the surface was destroyed (e.g. a previous trimMemory call) we
// need to recreate it here.
- if (!isSurfaceReady() && mNativeWindow) {
+ if (mHardwareBuffer) {
+ mRenderThread.requireVkContext();
+ } else if (!isSurfaceReady() && mNativeWindow) {
setSurface(mNativeWindow.get(), SwapBehavior::kSwap_default);
}
return isContextReady() ? MakeCurrentResult::AlreadyCurrent : MakeCurrentResult::Failed;
}
Frame SkiaVulkanPipeline::getFrame() {
- LOG_ALWAYS_FATAL_IF(mVkSurface == nullptr, "getFrame() called on a context with no surface!");
- return vulkanManager().dequeueNextBuffer(mVkSurface);
+ if (mHardwareBuffer) {
+ AHardwareBuffer_Desc description;
+ AHardwareBuffer_describe(mHardwareBuffer, &description);
+ return Frame(description.width, description.height, 0);
+ } else {
+ LOG_ALWAYS_FATAL_IF(mVkSurface == nullptr,
+ "getFrame() called on a context with no surface!");
+ return vulkanManager().dequeueNextBuffer(mVkSurface);
+ }
}
IRenderPipeline::DrawResult SkiaVulkanPipeline::draw(
const Frame& frame, const SkRect& screenDirty, const SkRect& dirty,
const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
- const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler) {
- sk_sp<SkSurface> backBuffer = mVkSurface->getCurrentSkSurface();
+ const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler,
+ const HardwareBufferRenderParams& bufferParams) {
+ sk_sp<SkSurface> backBuffer;
+ SkMatrix preTransform;
+ if (mHardwareBuffer) {
+ backBuffer = getBufferSkSurface(bufferParams);
+ preTransform = bufferParams.getTransform();
+ } else {
+ backBuffer = mVkSurface->getCurrentSkSurface();
+ preTransform = mVkSurface->getCurrentPreTransform();
+ }
+
if (backBuffer.get() == nullptr) {
return {false, -1};
}
// update the coordinates of the global light position based on surface rotation
- SkPoint lightCenter = mVkSurface->getCurrentPreTransform().mapXY(lightGeometry.center.x,
- lightGeometry.center.y);
+ SkPoint lightCenter = preTransform.mapXY(lightGeometry.center.x, lightGeometry.center.y);
LightGeometry localGeometry = lightGeometry;
localGeometry.center.x = lightCenter.fX;
localGeometry.center.y = lightCenter.fY;
LightingInfo::updateLighting(localGeometry, lightInfo);
renderFrame(*layerUpdateQueue, dirty, renderNodes, opaque, contentDrawBounds, backBuffer,
- mVkSurface->getCurrentPreTransform());
+ preTransform);
// Draw visual debugging features
if (CC_UNLIKELY(Properties::showDirtyRegions ||
@@ -116,12 +134,16 @@
bool SkiaVulkanPipeline::swapBuffers(const Frame& frame, bool drew, const SkRect& screenDirty,
FrameInfo* currentFrameInfo, bool* requireSwap) {
- *requireSwap = drew;
-
// Even if we decided to cancel the frame, from the perspective of jank
// metrics the frame was swapped at this point
currentFrameInfo->markSwapBuffers();
+ if (mHardwareBuffer) {
+ return false;
+ }
+
+ *requireSwap = drew;
+
if (*requireSwap) {
vulkanManager().swapBuffers(mVkSurface, screenDirty);
}
@@ -137,6 +159,12 @@
void SkiaVulkanPipeline::onStop() {}
+[[nodiscard]] android::base::unique_fd SkiaVulkanPipeline::flush() {
+ int fence = -1;
+ vulkanManager().createReleaseFence(&fence, mRenderThread.getGrContext());
+ return android::base::unique_fd(fence);
+}
+
// We can safely ignore the swap behavior because VkManager will always operate
// in a mode equivalent to EGLManager::SwapBehavior::kBufferAge
bool SkiaVulkanPipeline::setSurface(ANativeWindow* surface, SwapBehavior /*swapBehavior*/) {
diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h
index e0884a8..d921ddb 100644
--- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h
+++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h
@@ -16,14 +16,13 @@
#pragma once
+#include "SkRefCnt.h"
#include "SkiaPipeline.h"
+#include "renderstate/RenderState.h"
+#include "renderthread/HardwareBufferRenderParams.h"
#include "renderthread/VulkanManager.h"
#include "renderthread/VulkanSurface.h"
-#include "renderstate/RenderState.h"
-
-#include "SkRefCnt.h"
-
class SkBitmap;
struct SkRect;
@@ -38,18 +37,18 @@
renderthread::MakeCurrentResult makeCurrent() override;
renderthread::Frame getFrame() override;
- renderthread::IRenderPipeline::DrawResult draw(const renderthread::Frame& frame,
- const SkRect& screenDirty, const SkRect& dirty,
- const LightGeometry& lightGeometry,
- LayerUpdateQueue* layerUpdateQueue,
- const Rect& contentDrawBounds, bool opaque,
- const LightInfo& lightInfo,
- const std::vector<sp<RenderNode> >& renderNodes,
- FrameInfoVisualizer* profiler) override;
+ renderthread::IRenderPipeline::DrawResult draw(
+ const renderthread::Frame& frame, const SkRect& screenDirty, const SkRect& dirty,
+ const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
+ const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
+ const std::vector<sp<RenderNode> >& renderNodes, FrameInfoVisualizer* profiler,
+ const renderthread::HardwareBufferRenderParams& bufferParams) override;
GrSurfaceOrigin getSurfaceOrigin() override { return kTopLeft_GrSurfaceOrigin; }
bool swapBuffers(const renderthread::Frame& frame, bool drew, const SkRect& screenDirty,
FrameInfo* currentFrameInfo, bool* requireSwap) override;
DeferredLayerUpdater* createTextureLayer() override;
+ [[nodiscard]] android::base::unique_fd flush() override;
+
bool setSurface(ANativeWindow* surface, renderthread::SwapBehavior swapBehavior) override;
void onStop() override;
bool isSurfaceReady() override;
@@ -66,7 +65,6 @@
private:
renderthread::VulkanManager& vulkanManager();
-
renderthread::VulkanSurface* mVkSurface = nullptr;
sp<ANativeWindow> mNativeWindow;
};
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index c0f3086..f10b2b2 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -153,6 +153,7 @@
void CanvasContext::destroy() {
stopDrawing();
+ setHardwareBuffer(nullptr);
setSurface(nullptr);
setSurfaceControl(nullptr);
freePrefetchedLayers();
@@ -176,12 +177,23 @@
native_window_set_buffer_count(window, bufferCount);
}
+void CanvasContext::setHardwareBuffer(AHardwareBuffer* buffer) {
+ if (mHardwareBuffer) {
+ AHardwareBuffer_release(mHardwareBuffer);
+ mHardwareBuffer = nullptr;
+ }
+
+ if (buffer) {
+ AHardwareBuffer_acquire(buffer);
+ mHardwareBuffer = buffer;
+ }
+ mRenderPipeline->setHardwareBuffer(mHardwareBuffer);
+}
+
void CanvasContext::setSurface(ANativeWindow* window, bool enableTimeout) {
ATRACE_CALL();
if (window) {
- // Ensure the hint session is running here, away from any critical paths
- mHintSessionWrapper.init();
mNativeSurface = std::make_unique<ReliableSurface>(window);
mNativeSurface->init();
if (enableTimeout) {
@@ -261,7 +273,7 @@
mRenderThread.removeFrameCallback(this);
mRenderPipeline->onStop();
mRenderThread.cacheManager().onContextStopped(this);
- } else if (mIsDirty && hasSurface()) {
+ } else if (mIsDirty && hasOutputTarget()) {
mRenderThread.postFrameCallback(this);
}
}
@@ -289,7 +301,8 @@
float CanvasContext::setColorMode(ColorMode mode) {
if (mode != mColorMode) {
- if (mode == ColorMode::Hdr && !mRenderPipeline->supportsExtendedRangeHdr()) {
+ const bool isHdr = mode == ColorMode::Hdr || mode == ColorMode::Hdr10;
+ if (isHdr && !mRenderPipeline->supportsExtendedRangeHdr()) {
mode = ColorMode::WideColorGamut;
}
mColorMode = mode;
@@ -299,13 +312,15 @@
switch (mColorMode) {
case ColorMode::Hdr:
return 3.f; // TODO: Refine this number
+ case ColorMode::Hdr10:
+ return 10.f;
default:
return 1.f;
}
}
float CanvasContext::targetSdrHdrRatio() const {
- if (mColorMode == ColorMode::Hdr) {
+ if (mColorMode == ColorMode::Hdr || mColorMode == ColorMode::Hdr10) {
return mTargetSdrHdrRatio;
} else {
return 1.f;
@@ -425,7 +440,7 @@
mIsDirty = true;
- if (CC_UNLIKELY(!hasSurface())) {
+ if (CC_UNLIKELY(!hasOutputTarget())) {
mCurrentFrameInfo->addFlag(FrameInfoFlags::SkippedFrame);
info.out.canDrawThisFrame = false;
return;
@@ -570,7 +585,7 @@
std::scoped_lock lock(mFrameMetricsReporterMutex);
drawResult = mRenderPipeline->draw(frame, windowDirty, dirty, mLightGeometry,
&mLayerUpdateQueue, mContentDrawBounds, mOpaque,
- mLightInfo, mRenderNodes, &(profiler()));
+ mLightInfo, mRenderNodes, &(profiler()), mBufferParams);
}
uint64_t frameCompleteNr = getFrameNumber();
@@ -1050,6 +1065,10 @@
mSyncDelayDuration = duration;
}
+void CanvasContext::startHintSession() {
+ mHintSessionWrapper.init();
+}
+
} /* namespace renderthread */
} /* namespace uirenderer */
} /* namespace android */
diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h
index 0f6b736..b26c018 100644
--- a/libs/hwui/renderthread/CanvasContext.h
+++ b/libs/hwui/renderthread/CanvasContext.h
@@ -125,12 +125,13 @@
// Won't take effect until next EGLSurface creation
void setSwapBehavior(SwapBehavior swapBehavior);
+ void setHardwareBuffer(AHardwareBuffer* buffer);
void setSurface(ANativeWindow* window, bool enableTimeout = true);
void setSurfaceControl(ASurfaceControl* surfaceControl);
bool pauseSurface();
void setStopped(bool stopped);
- bool isStopped() { return mStopped || !hasSurface(); }
- bool hasSurface() const { return mNativeSurface.get(); }
+ bool isStopped() { return mStopped || !hasOutputTarget(); }
+ bool hasOutputTarget() const { return mNativeSurface.get() || mHardwareBuffer; }
void allocateBuffers();
void setLightAlpha(uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha);
@@ -208,6 +209,10 @@
mASurfaceTransactionCallback = callback;
}
+ void setHardwareBufferRenderParams(const HardwareBufferRenderParams& params) {
+ mBufferParams = params;
+ }
+
bool mergeTransaction(ASurfaceTransaction* transaction, ASurfaceControl* control);
void setPrepareSurfaceControlForWebviewCallback(const std::function<void()>& callback) {
@@ -224,6 +229,8 @@
void setSyncDelayDuration(nsecs_t duration);
+ void startHintSession();
+
private:
CanvasContext(RenderThread& thread, bool translucent, RenderNode* rootRenderNode,
IContextFactory* contextFactory, std::unique_ptr<IRenderPipeline> renderPipeline,
@@ -262,6 +269,9 @@
int32_t mLastFrameHeight = 0;
RenderThread& mRenderThread;
+
+ AHardwareBuffer* mHardwareBuffer = nullptr;
+ HardwareBufferRenderParams mBufferParams;
std::unique_ptr<ReliableSurface> mNativeSurface;
// The SurfaceControl reference is passed from ViewRootImpl, can be set to
// NULL to remove the reference
diff --git a/libs/hwui/renderthread/DrawFrameTask.cpp b/libs/hwui/renderthread/DrawFrameTask.cpp
index ccdf715..fab2f46 100644
--- a/libs/hwui/renderthread/DrawFrameTask.cpp
+++ b/libs/hwui/renderthread/DrawFrameTask.cpp
@@ -26,6 +26,7 @@
#include "../Properties.h"
#include "../RenderNode.h"
#include "CanvasContext.h"
+#include "HardwareBufferRenderParams.h"
#include "RenderThread.h"
namespace android {
@@ -92,6 +93,9 @@
mContext->setSyncDelayDuration(systemTime(SYSTEM_TIME_MONOTONIC) - mSyncQueued);
mContext->setTargetSdrHdrRatio(mRenderSdrHdrRatio);
+ auto hardwareBufferParams = mHardwareBufferParams;
+ mContext->setHardwareBufferRenderParams(hardwareBufferParams);
+ IRenderPipeline* pipeline = mContext->getRenderPipeline();
bool canUnblockUiThread;
bool canDrawThisFrame;
{
@@ -151,6 +155,11 @@
if (!canUnblockUiThread) {
unblockUiThread();
}
+
+ if (pipeline->hasHardwareBuffer()) {
+ auto fence = pipeline->flush();
+ hardwareBufferParams.invokeRenderCallback(std::move(fence), 0);
+ }
}
bool DrawFrameTask::syncFrameState(TreeInfo& info) {
@@ -176,8 +185,9 @@
// This is after the prepareTree so that any pending operations
// (RenderNode tree state, prefetched layers, etc...) will be flushed.
- if (CC_UNLIKELY(!mContext->hasSurface() || !canDraw)) {
- if (!mContext->hasSurface()) {
+ bool hasTarget = mContext->hasOutputTarget();
+ if (CC_UNLIKELY(!hasTarget || !canDraw)) {
+ if (!hasTarget) {
mSyncResult |= SyncResult::LostSurfaceRewardIfFound;
} else {
// If we have a surface but can't draw we must be stopped
diff --git a/libs/hwui/renderthread/DrawFrameTask.h b/libs/hwui/renderthread/DrawFrameTask.h
index 4be8f6b..4130d4a 100644
--- a/libs/hwui/renderthread/DrawFrameTask.h
+++ b/libs/hwui/renderthread/DrawFrameTask.h
@@ -27,8 +27,16 @@
#include "../Rect.h"
#include "../TreeInfo.h"
#include "RenderTask.h"
+#include "SkColorSpace.h"
+#include "SwapBehavior.h"
+#include "utils/TimeUtils.h"
+#ifdef __ANDROID__ // Layoutlib does not support hardware acceleration
+#include <android/hardware_buffer.h>
+#endif
+#include "HardwareBufferRenderParams.h"
namespace android {
+
namespace uirenderer {
class DeferredLayerUpdater;
@@ -88,6 +96,10 @@
void forceDrawNextFrame() { mForceDrawFrame = true; }
+ void setHardwareBufferRenderParams(const HardwareBufferRenderParams& params) {
+ mHardwareBufferParams = params;
+ }
+
void setRenderSdrHdrRatio(float ratio) { mRenderSdrHdrRatio = ratio; }
private:
@@ -114,6 +126,7 @@
int64_t mFrameInfo[UI_THREAD_FRAME_INFO_SIZE];
+ HardwareBufferRenderParams mHardwareBufferParams;
std::function<std::function<void(bool)>(int32_t, int64_t)> mFrameCallback;
std::function<void(bool)> mFrameCommitCallback;
std::function<void()> mFrameCompleteCallback;
diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp
index 5b7cf75..4fb114b 100644
--- a/libs/hwui/renderthread/EglManager.cpp
+++ b/libs/hwui/renderthread/EglManager.cpp
@@ -455,6 +455,7 @@
// composer3 support, just treat HDR as equivalent to wide color gamut if
// the GLES path is still being hit
case ColorMode::Hdr:
+ case ColorMode::Hdr10:
case ColorMode::WideColorGamut: {
skcms_Matrix3x3 colorGamut;
LOG_ALWAYS_FATAL_IF(!colorSpace->toXYZD50(&colorGamut),
diff --git a/libs/hwui/renderthread/HardwareBufferRenderParams.h b/libs/hwui/renderthread/HardwareBufferRenderParams.h
new file mode 100644
index 0000000..91fe3f6
--- /dev/null
+++ b/libs/hwui/renderthread/HardwareBufferRenderParams.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+#ifndef HARDWAREBUFFERRENDERER_H_
+#define HARDWAREBUFFERRENDERER_H_
+
+#include <android-base/unique_fd.h>
+#include <android/hardware_buffer.h>
+
+#include "SkColorSpace.h"
+#include "SkMatrix.h"
+#include "SkSurface.h"
+
+namespace android {
+namespace uirenderer {
+namespace renderthread {
+
+using namespace android::uirenderer::renderthread;
+
+using RenderCallback = std::function<void(android::base::unique_fd&&, int)>;
+
+class RenderProxy;
+
+class HardwareBufferRenderParams {
+public:
+ HardwareBufferRenderParams() = default;
+ HardwareBufferRenderParams(const SkMatrix& transform, const sk_sp<SkColorSpace>& colorSpace,
+ RenderCallback&& callback)
+ : mTransform(transform)
+ , mColorSpace(colorSpace)
+ , mRenderCallback(std::move(callback)) {}
+ const SkMatrix& getTransform() const { return mTransform; }
+ sk_sp<SkColorSpace> getColorSpace() const { return mColorSpace; }
+
+ void invokeRenderCallback(android::base::unique_fd&& fenceFd, int status) {
+ if (mRenderCallback) {
+ std::invoke(mRenderCallback, std::move(fenceFd), status);
+ }
+ }
+
+private:
+ SkMatrix mTransform = SkMatrix::I();
+ sk_sp<SkColorSpace> mColorSpace = SkColorSpace::MakeSRGB();
+ RenderCallback mRenderCallback = nullptr;
+};
+
+} // namespace renderthread
+} // namespace uirenderer
+} // namespace android
+#endif // HARDWAREBUFFERRENDERER_H_
diff --git a/libs/hwui/renderthread/IRenderPipeline.h b/libs/hwui/renderthread/IRenderPipeline.h
index 18cad31..c68fcdf 100644
--- a/libs/hwui/renderthread/IRenderPipeline.h
+++ b/libs/hwui/renderthread/IRenderPipeline.h
@@ -16,17 +16,19 @@
#pragma once
+#include <SkColorSpace.h>
+#include <SkRect.h>
+#include <android-base/unique_fd.h>
+#include <utils/RefBase.h>
+
+#include "ColorMode.h"
#include "DamageAccumulator.h"
#include "FrameInfoVisualizer.h"
+#include "HardwareBufferRenderParams.h"
#include "LayerUpdateQueue.h"
#include "Lighting.h"
#include "SwapBehavior.h"
#include "hwui/Bitmap.h"
-#include "ColorMode.h"
-
-#include <SkColorSpace.h>
-#include <SkRect.h>
-#include <utils/RefBase.h>
class GrDirectContext;
@@ -64,10 +66,14 @@
const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
const std::vector<sp<RenderNode>>& renderNodes,
- FrameInfoVisualizer* profiler) = 0;
+ FrameInfoVisualizer* profiler,
+ const HardwareBufferRenderParams& bufferParams) = 0;
virtual bool swapBuffers(const Frame& frame, bool drew, const SkRect& screenDirty,
FrameInfo* currentFrameInfo, bool* requireSwap) = 0;
virtual DeferredLayerUpdater* createTextureLayer() = 0;
+ [[nodiscard]] virtual android::base::unique_fd flush() = 0;
+ virtual void setHardwareBuffer(AHardwareBuffer* hardwareBuffer) = 0;
+ virtual bool hasHardwareBuffer() = 0;
virtual bool setSurface(ANativeWindow* window, SwapBehavior swapBehavior) = 0;
virtual void onStop() = 0;
virtual bool isSurfaceReady() = 0;
diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp
index f8e2dee..31b4b20 100644
--- a/libs/hwui/renderthread/RenderProxy.cpp
+++ b/libs/hwui/renderthread/RenderProxy.cpp
@@ -45,8 +45,12 @@
pid_t uiThreadId = pthread_gettid_np(pthread_self());
pid_t renderThreadId = getRenderThreadTid();
mContext = mRenderThread.queue().runSync([=, this]() -> CanvasContext* {
- return CanvasContext::create(mRenderThread, translucent, rootRenderNode, contextFactory,
- uiThreadId, renderThreadId);
+ CanvasContext* context = CanvasContext::create(mRenderThread, translucent, rootRenderNode,
+ contextFactory, uiThreadId, renderThreadId);
+ if (context != nullptr) {
+ mRenderThread.queue().post([=] { context->startHintSession(); });
+ }
+ return context;
});
mDrawFrameTask.setContext(&mRenderThread, mContext, rootRenderNode);
}
@@ -85,6 +89,18 @@
mRenderThread.queue().runSync([this, name]() { mContext->setName(std::string(name)); });
}
+void RenderProxy::setHardwareBuffer(AHardwareBuffer* buffer) {
+ if (buffer) {
+ AHardwareBuffer_acquire(buffer);
+ }
+ mRenderThread.queue().post([this, hardwareBuffer = buffer]() mutable {
+ mContext->setHardwareBuffer(hardwareBuffer);
+ if (hardwareBuffer) {
+ AHardwareBuffer_release(hardwareBuffer);
+ }
+ });
+}
+
void RenderProxy::setSurface(ANativeWindow* window, bool enableTimeout) {
if (window) { ANativeWindow_acquire(window); }
mRenderThread.queue().post([this, win = window, enableTimeout]() mutable {
@@ -134,7 +150,7 @@
float RenderProxy::setColorMode(ColorMode mode) {
// We only need to figure out what the renderer supports for HDR, otherwise this can stay
// an async call since we already know the return value
- if (mode == ColorMode::Hdr) {
+ if (mode == ColorMode::Hdr || mode == ColorMode::Hdr10) {
return mRenderThread.queue().runSync(
[=]() -> float { return mContext->setColorMode(mode); });
} else {
@@ -340,6 +356,10 @@
mDrawFrameTask.setContentDrawBounds(left, top, right, bottom);
}
+void RenderProxy::setHardwareBufferRenderParams(const HardwareBufferRenderParams& params) {
+ mDrawFrameTask.setHardwareBufferRenderParams(params);
+}
+
void RenderProxy::setPictureCapturedCallback(
const std::function<void(sk_sp<SkPicture>&&)>& callback) {
mRenderThread.queue().post(
diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h
index 5dd65a0..82072a6 100644
--- a/libs/hwui/renderthread/RenderProxy.h
+++ b/libs/hwui/renderthread/RenderProxy.h
@@ -18,6 +18,7 @@
#define RENDERPROXY_H_
#include <SkRefCnt.h>
+#include <android/hardware_buffer.h>
#include <android/native_window.h>
#include <android/surface_control.h>
#include <cutils/compiler.h>
@@ -76,7 +77,7 @@
void setSwapBehavior(SwapBehavior swapBehavior);
bool loadSystemProperties();
void setName(const char* name);
-
+ void setHardwareBuffer(AHardwareBuffer* buffer);
void setSurface(ANativeWindow* window, bool enableTimeout = true);
void setSurfaceControl(ASurfaceControl* surfaceControl);
void allocateBuffers();
@@ -84,6 +85,7 @@
void setStopped(bool stopped);
void setLightAlpha(uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha);
void setLightGeometry(const Vector3& lightCenter, float lightRadius);
+ void setHardwareBufferRenderParams(const HardwareBufferRenderParams& params);
void setOpaque(bool opaque);
float setColorMode(ColorMode mode);
void setRenderSdrHdrRatio(float ratio);
diff --git a/libs/hwui/renderthread/VulkanSurface.cpp b/libs/hwui/renderthread/VulkanSurface.cpp
index 2efa5d6..21b6c44 100644
--- a/libs/hwui/renderthread/VulkanSurface.cpp
+++ b/libs/hwui/renderthread/VulkanSurface.cpp
@@ -201,7 +201,7 @@
outWindowInfo->colorspace = colorSpace;
outWindowInfo->colorMode = colorMode;
- if (colorMode == ColorMode::Hdr) {
+ if (colorMode == ColorMode::Hdr || colorMode == ColorMode::Hdr10) {
outWindowInfo->dataspace =
static_cast<android_dataspace>(STANDARD_DCI_P3 | TRANSFER_SRGB | RANGE_EXTENDED);
} else {
@@ -509,7 +509,7 @@
mNativeBuffers[i].skSurface.reset();
}
- if (mWindowInfo.colorMode == ColorMode::Hdr) {
+ if (mWindowInfo.colorMode == ColorMode::Hdr || mWindowInfo.colorMode == ColorMode::Hdr10) {
mWindowInfo.dataspace =
static_cast<android_dataspace>(STANDARD_DCI_P3 | TRANSFER_SRGB | RANGE_EXTENDED);
} else {
@@ -521,7 +521,7 @@
"Unsupported colorspace");
if (mNativeWindow) {
- int err = native_window_set_buffers_data_space(mNativeWindow.get(), mWindowInfo.dataspace);
+ int err = ANativeWindow_setBuffersDataSpace(mNativeWindow.get(), mWindowInfo.dataspace);
if (err != 0) {
ALOGE("VulkanSurface::setColorSpace() native_window_set_buffers_data_space(%d) "
"failed: %s (%d)",
diff --git a/libs/hwui/tests/unit/CanvasContextTests.cpp b/libs/hwui/tests/unit/CanvasContextTests.cpp
index 88420a5..9e376e3 100644
--- a/libs/hwui/tests/unit/CanvasContextTests.cpp
+++ b/libs/hwui/tests/unit/CanvasContextTests.cpp
@@ -38,7 +38,7 @@
std::unique_ptr<CanvasContext> canvasContext(
CanvasContext::create(renderThread, false, rootNode.get(), &contextFactory, 0, 0));
- ASSERT_FALSE(canvasContext->hasSurface());
+ ASSERT_FALSE(canvasContext->hasOutputTarget());
canvasContext->destroy();
}
diff --git a/libs/input/MouseCursorController.cpp b/libs/input/MouseCursorController.cpp
index a835167..24cfc9d 100644
--- a/libs/input/MouseCursorController.cpp
+++ b/libs/input/MouseCursorController.cpp
@@ -38,6 +38,8 @@
: mContext(context) {
std::scoped_lock lock(mLock);
+ mLocked.stylusHoverMode = false;
+
mLocked.animationFrameIndex = 0;
mLocked.lastFrameUpdatedTime = 0;
@@ -47,7 +49,8 @@
mLocked.pointerAlpha = 0.0f; // pointer is initially faded
mLocked.pointerSprite = mContext.getSpriteController()->createSprite();
mLocked.updatePointerIcon = false;
- mLocked.requestedPointerType = mContext.getPolicy()->getDefaultPointerIconId();
+ mLocked.requestedPointerType = PointerIconStyle::TYPE_NOT_SPECIFIED;
+ mLocked.resolvedPointerType = PointerIconStyle::TYPE_NOT_SPECIFIED;
mLocked.resourcesLoaded = false;
@@ -184,6 +187,15 @@
}
}
+void MouseCursorController::setStylusHoverMode(bool stylusHoverMode) {
+ std::scoped_lock lock(mLock);
+
+ if (mLocked.stylusHoverMode != stylusHoverMode) {
+ mLocked.stylusHoverMode = stylusHoverMode;
+ mLocked.updatePointerIcon = true;
+ }
+}
+
void MouseCursorController::reloadPointerResources(bool getAdditionalMouseResources) {
std::scoped_lock lock(mLock);
@@ -339,7 +351,7 @@
bool MouseCursorController::doBitmapAnimationLocked(nsecs_t timestamp) REQUIRES(mLock) {
std::map<PointerIconStyle, PointerAnimation>::const_iterator iter =
- mLocked.animationResources.find(mLocked.requestedPointerType);
+ mLocked.animationResources.find(mLocked.resolvedPointerType);
if (iter == mLocked.animationResources.end()) {
return false;
}
@@ -381,14 +393,23 @@
}
if (mLocked.updatePointerIcon) {
- if (mLocked.requestedPointerType == mContext.getPolicy()->getDefaultPointerIconId()) {
+ mLocked.resolvedPointerType = mLocked.requestedPointerType;
+ const PointerIconStyle defaultPointerIconId =
+ mContext.getPolicy()->getDefaultPointerIconId();
+ if (mLocked.resolvedPointerType == PointerIconStyle::TYPE_NOT_SPECIFIED) {
+ mLocked.resolvedPointerType = mLocked.stylusHoverMode
+ ? mContext.getPolicy()->getDefaultStylusIconId()
+ : defaultPointerIconId;
+ }
+
+ if (mLocked.resolvedPointerType == defaultPointerIconId) {
mLocked.pointerSprite->setIcon(mLocked.pointerIcon);
} else {
std::map<PointerIconStyle, SpriteIcon>::const_iterator iter =
- mLocked.additionalMouseResources.find(mLocked.requestedPointerType);
+ mLocked.additionalMouseResources.find(mLocked.resolvedPointerType);
if (iter != mLocked.additionalMouseResources.end()) {
std::map<PointerIconStyle, PointerAnimation>::const_iterator anim_iter =
- mLocked.animationResources.find(mLocked.requestedPointerType);
+ mLocked.animationResources.find(mLocked.resolvedPointerType);
if (anim_iter != mLocked.animationResources.end()) {
mLocked.animationFrameIndex = 0;
mLocked.lastFrameUpdatedTime = systemTime(SYSTEM_TIME_MONOTONIC);
@@ -396,7 +417,7 @@
}
mLocked.pointerSprite->setIcon(iter->second);
} else {
- ALOGW("Can't find the resource for icon id %d", mLocked.requestedPointerType);
+ ALOGW("Can't find the resource for icon id %d", mLocked.resolvedPointerType);
mLocked.pointerSprite->setIcon(mLocked.pointerIcon);
}
}
diff --git a/libs/input/MouseCursorController.h b/libs/input/MouseCursorController.h
index 208d33d..db0ab56 100644
--- a/libs/input/MouseCursorController.h
+++ b/libs/input/MouseCursorController.h
@@ -53,6 +53,7 @@
void fade(PointerControllerInterface::Transition transition);
void unfade(PointerControllerInterface::Transition transition);
void setDisplayViewport(const DisplayViewport& viewport, bool getAdditionalMouseResources);
+ void setStylusHoverMode(bool stylusHoverMode);
void updatePointerIcon(PointerIconStyle iconId);
void setCustomPointerIcon(const SpriteIcon& icon);
@@ -74,6 +75,7 @@
struct Locked {
DisplayViewport viewport;
+ bool stylusHoverMode;
size_t animationFrameIndex;
nsecs_t lastFrameUpdatedTime;
@@ -92,6 +94,7 @@
std::map<PointerIconStyle, PointerAnimation> animationResources;
PointerIconStyle requestedPointerType;
+ PointerIconStyle resolvedPointerType;
int32_t buttonState;
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index 099efd3..fedf58d 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -195,7 +195,11 @@
return;
}
- if (presentation == Presentation::POINTER) {
+ if (presentation == Presentation::POINTER || presentation == Presentation::STYLUS_HOVER) {
+ // For now, we support stylus hover using the mouse cursor implementation.
+ // TODO: Add proper support for stylus hover icons.
+ mCursorController.setStylusHoverMode(presentation == Presentation::STYLUS_HOVER);
+
mCursorController.getAdditionalMouseResources();
clearSpotsLocked();
}
@@ -249,7 +253,8 @@
if (mCursorController.resourcesLoaded()) {
bool getAdditionalMouseResources = false;
- if (mLocked.presentation == PointerController::Presentation::POINTER) {
+ if (mLocked.presentation == PointerController::Presentation::POINTER ||
+ mLocked.presentation == PointerController::Presentation::STYLUS_HOVER) {
getAdditionalMouseResources = true;
}
mCursorController.reloadPointerResources(getAdditionalMouseResources);
@@ -260,7 +265,8 @@
std::scoped_lock lock(getLock());
bool getAdditionalMouseResources = false;
- if (mLocked.presentation == PointerController::Presentation::POINTER) {
+ if (mLocked.presentation == PointerController::Presentation::POINTER ||
+ mLocked.presentation == PointerController::Presentation::STYLUS_HOVER) {
getAdditionalMouseResources = true;
}
mCursorController.setDisplayViewport(viewport, getAdditionalMouseResources);
diff --git a/libs/input/PointerControllerContext.h b/libs/input/PointerControllerContext.h
index 1797428..96d83a5 100644
--- a/libs/input/PointerControllerContext.h
+++ b/libs/input/PointerControllerContext.h
@@ -79,6 +79,7 @@
std::map<PointerIconStyle, PointerAnimation>* outAnimationResources,
int32_t displayId) = 0;
virtual PointerIconStyle getDefaultPointerIconId() = 0;
+ virtual PointerIconStyle getDefaultStylusIconId() = 0;
virtual PointerIconStyle getCustomPointerIconId() = 0;
virtual void onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos) = 0;
};
diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp
index a6a4115..c820d00 100644
--- a/libs/input/tests/PointerController_test.cpp
+++ b/libs/input/tests/PointerController_test.cpp
@@ -35,6 +35,7 @@
CURSOR_TYPE_ANCHOR,
CURSOR_TYPE_ADDITIONAL,
CURSOR_TYPE_ADDITIONAL_ANIM,
+ CURSOR_TYPE_STYLUS,
CURSOR_TYPE_CUSTOM = -1,
};
@@ -57,6 +58,7 @@
std::map<PointerIconStyle, PointerAnimation>* outAnimationResources,
int32_t displayId) override;
virtual PointerIconStyle getDefaultPointerIconId() override;
+ virtual PointerIconStyle getDefaultStylusIconId() override;
virtual PointerIconStyle getCustomPointerIconId() override;
virtual void onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos) override;
@@ -105,6 +107,11 @@
(*outResources)[static_cast<PointerIconStyle>(cursorType)] = icon;
(*outAnimationResources)[static_cast<PointerIconStyle>(cursorType)] = anim;
+ // CURSOR_TYPE_STYLUS doesn't have animation resource.
+ cursorType = CURSOR_TYPE_STYLUS;
+ loadPointerIconForType(&icon, cursorType);
+ (*outResources)[static_cast<PointerIconStyle>(cursorType)] = icon;
+
additionalMouseResourcesLoaded = true;
}
@@ -112,6 +119,10 @@
return static_cast<PointerIconStyle>(CURSOR_TYPE_DEFAULT);
}
+PointerIconStyle MockPointerControllerPolicyInterface::getDefaultStylusIconId() {
+ return static_cast<PointerIconStyle>(CURSOR_TYPE_STYLUS);
+}
+
PointerIconStyle MockPointerControllerPolicyInterface::getCustomPointerIconId() {
return static_cast<PointerIconStyle>(CURSOR_TYPE_CUSTOM);
}
@@ -214,6 +225,21 @@
mPointerController->reloadPointerResources();
}
+TEST_F(PointerControllerTest, useStylusTypeForStylusHover) {
+ ensureDisplayViewportIsSet();
+ mPointerController->setPresentation(PointerController::Presentation::STYLUS_HOVER);
+ mPointerController->unfade(PointerController::Transition::IMMEDIATE);
+ std::pair<float, float> hotspot = getHotSpotCoordinatesForType(CURSOR_TYPE_STYLUS);
+ EXPECT_CALL(*mPointerSprite, setVisible(true));
+ EXPECT_CALL(*mPointerSprite, setAlpha(1.0f));
+ EXPECT_CALL(*mPointerSprite,
+ setIcon(AllOf(Field(&SpriteIcon::style,
+ static_cast<PointerIconStyle>(CURSOR_TYPE_STYLUS)),
+ Field(&SpriteIcon::hotSpotX, hotspot.first),
+ Field(&SpriteIcon::hotSpotY, hotspot.second))));
+ mPointerController->reloadPointerResources();
+}
+
TEST_F(PointerControllerTest, updatePointerIcon) {
ensureDisplayViewportIsSet();
mPointerController->setPresentation(PointerController::Presentation::POINTER);
diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java
index 28496f1..c5202dc 100644
--- a/media/java/android/media/MediaRoute2Info.java
+++ b/media/java/android/media/MediaRoute2Info.java
@@ -108,133 +108,148 @@
public static final int PLAYBACK_VOLUME_VARIABLE = 1;
/** @hide */
- @IntDef({
- TYPE_UNKNOWN, TYPE_BUILTIN_SPEAKER, TYPE_WIRED_HEADSET,
- TYPE_WIRED_HEADPHONES, TYPE_BLUETOOTH_A2DP, TYPE_HDMI, TYPE_USB_DEVICE,
- TYPE_USB_ACCESSORY, TYPE_DOCK, TYPE_USB_HEADSET, TYPE_HEARING_AID, TYPE_BLE_HEADSET,
- TYPE_REMOTE_TV, TYPE_REMOTE_SPEAKER, TYPE_GROUP})
+ @IntDef(
+ prefix = {"TYPE_"},
+ value = {
+ TYPE_UNKNOWN,
+ TYPE_BUILTIN_SPEAKER,
+ TYPE_WIRED_HEADSET,
+ TYPE_WIRED_HEADPHONES,
+ TYPE_BLUETOOTH_A2DP,
+ TYPE_HDMI,
+ TYPE_USB_DEVICE,
+ TYPE_USB_ACCESSORY,
+ TYPE_DOCK,
+ TYPE_USB_HEADSET,
+ TYPE_HEARING_AID,
+ TYPE_BLE_HEADSET,
+ TYPE_REMOTE_TV,
+ TYPE_REMOTE_SPEAKER,
+ TYPE_REMOTE_AUDIO_VIDEO_RECEIVER,
+ TYPE_GROUP
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface Type {}
/**
- * The default route type indicating the type is unknown.
+ * Indicates the route's type is unknown or undefined.
*
* @see #getType
- * @hide
*/
public static final int TYPE_UNKNOWN = 0;
/**
- * A route type describing the speaker system (i.e. a mono speaker or stereo speakers) built
- * in a device.
+ * Indicates the route is the speaker system (i.e. a mono speaker or stereo speakers) built into
+ * the device.
*
* @see #getType
- * @hide
*/
public static final int TYPE_BUILTIN_SPEAKER = AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
/**
- * A route type describing a headset, which is the combination of a headphones and microphone.
+ * Indicates the route is a headset, which is the combination of a headphones and a microphone.
*
* @see #getType
- * @hide
*/
public static final int TYPE_WIRED_HEADSET = AudioDeviceInfo.TYPE_WIRED_HEADSET;
/**
- * A route type describing a pair of wired headphones.
+ * Indicates the route is a pair of wired headphones.
*
* @see #getType
- * @hide
*/
public static final int TYPE_WIRED_HEADPHONES = AudioDeviceInfo.TYPE_WIRED_HEADPHONES;
/**
- * A route type indicating the presentation of the media is happening
- * on a bluetooth device such as a bluetooth speaker.
+ * Indicates the route is a bluetooth device, such as a bluetooth speaker or headphones.
*
* @see #getType
- * @hide
*/
public static final int TYPE_BLUETOOTH_A2DP = AudioDeviceInfo.TYPE_BLUETOOTH_A2DP;
/**
- * A route type describing an HDMI connection.
+ * Indicates the route is an HDMI connection.
*
* @see #getType
- * @hide
*/
public static final int TYPE_HDMI = AudioDeviceInfo.TYPE_HDMI;
/**
- * A route type describing a USB audio device.
+ * Indicates the route is a USB audio device.
*
* @see #getType
- * @hide
*/
public static final int TYPE_USB_DEVICE = AudioDeviceInfo.TYPE_USB_DEVICE;
/**
- * A route type describing a USB audio device in accessory mode.
+ * Indicates the route is a USB audio device in accessory mode.
*
* @see #getType
- * @hide
*/
public static final int TYPE_USB_ACCESSORY = AudioDeviceInfo.TYPE_USB_ACCESSORY;
/**
- * A route type describing the audio device associated with a dock.
+ * Indicates the route is the audio device associated with a dock.
*
* @see #getType
- * @hide
*/
public static final int TYPE_DOCK = AudioDeviceInfo.TYPE_DOCK;
/**
- * A device type describing a USB audio headset.
+ * Indicates the route is a USB audio headset.
*
* @see #getType
- * @hide
*/
public static final int TYPE_USB_HEADSET = AudioDeviceInfo.TYPE_USB_HEADSET;
/**
- * A route type describing a Hearing Aid.
+ * Indicates the route is a hearing aid.
*
* @see #getType
- * @hide
*/
public static final int TYPE_HEARING_AID = AudioDeviceInfo.TYPE_HEARING_AID;
/**
- * A route type describing a BLE HEADSET.
+ * Indicates the route is a Bluetooth Low Energy (BLE) HEADSET.
*
* @see #getType
- * @hide
*/
public static final int TYPE_BLE_HEADSET = AudioDeviceInfo.TYPE_BLE_HEADSET;
/**
- * A route type indicating the presentation of the media is happening on a TV.
+ * Indicates the route is a remote TV.
+ *
+ * <p>A remote device uses a routing protocol managed by the application, as opposed to the
+ * routing being done by the system.
*
* @see #getType
- * @hide
*/
public static final int TYPE_REMOTE_TV = 1001;
/**
- * A route type indicating the presentation of the media is happening on a speaker.
+ * Indicates the route is a remote speaker.
+ *
+ * <p>A remote device uses a routing protocol managed by the application, as opposed to the
+ * routing being done by the system.
*
* @see #getType
- * @hide
*/
public static final int TYPE_REMOTE_SPEAKER = 1002;
/**
- * A route type indicating the presentation of the media is happening on multiple devices.
+ * Indicates the route is a remote Audio/Video Receiver (AVR).
+ *
+ * <p>A remote device uses a routing protocol managed by the application, as opposed to the
+ * routing being done by the system.
*
* @see #getType
- * @hide
+ */
+ public static final int TYPE_REMOTE_AUDIO_VIDEO_RECEIVER = 1003;
+
+ /**
+ * Indicates the route is a group of devices.
+ *
+ * @see #getType
*/
public static final int TYPE_GROUP = 2000;
@@ -436,16 +451,23 @@
}
/**
- * Gets the type of this route.
+ * Returns the type of this route.
*
- * @return The type of this route:
- * {@link #TYPE_UNKNOWN},
- * {@link #TYPE_BUILTIN_SPEAKER}, {@link #TYPE_WIRED_HEADSET}, {@link #TYPE_WIRED_HEADPHONES},
- * {@link #TYPE_BLUETOOTH_A2DP}, {@link #TYPE_HDMI}, {@link #TYPE_DOCK},
- * {@Link #TYPE_USB_DEVICE}, {@link #TYPE_USB_ACCESSORY}, {@link #TYPE_USB_HEADSET}
- * {@link #TYPE_HEARING_AID},
- * {@link #TYPE_REMOTE_TV}, {@link #TYPE_REMOTE_SPEAKER}, {@link #TYPE_GROUP}.
- * @hide
+ * @see #TYPE_UNKNOWN
+ * @see #TYPE_BUILTIN_SPEAKER
+ * @see #TYPE_WIRED_HEADSET
+ * @see #TYPE_WIRED_HEADPHONES
+ * @see #TYPE_BLUETOOTH_A2DP
+ * @see #TYPE_HDMI
+ * @see #TYPE_DOCK
+ * @see #TYPE_USB_DEVICE
+ * @see #TYPE_USB_ACCESSORY
+ * @see #TYPE_USB_HEADSET
+ * @see #TYPE_HEARING_AID
+ * @see #TYPE_REMOTE_TV
+ * @see #TYPE_REMOTE_SPEAKER
+ * @see #TYPE_REMOTE_AUDIO_VIDEO_RECEIVER
+ * @see #TYPE_GROUP
*/
@Type
public int getType() {
@@ -657,6 +679,7 @@
pw.println(indent + "mId=" + mId);
pw.println(indent + "mName=" + mName);
pw.println(indent + "mFeatures=" + mFeatures);
+ pw.println(indent + "mType=" + getDeviceTypeString(mType));
pw.println(indent + "mIsSystem=" + mIsSystem);
pw.println(indent + "mIconUri=" + mIconUri);
pw.println(indent + "mDescription=" + mDescription);
@@ -787,6 +810,42 @@
dest.writeString8Array(mAllowedPackages.toArray(new String[0]));
}
+ private static String getDeviceTypeString(@Type int deviceType) {
+ switch (deviceType) {
+ case TYPE_BUILTIN_SPEAKER:
+ return "BUILTIN_SPEAKER";
+ case TYPE_WIRED_HEADSET:
+ return "WIRED_HEADSET";
+ case TYPE_WIRED_HEADPHONES:
+ return "WIRED_HEADPHONES";
+ case TYPE_BLUETOOTH_A2DP:
+ return "BLUETOOTH_A2DP";
+ case TYPE_HDMI:
+ return "HDMI";
+ case TYPE_DOCK:
+ return "DOCK";
+ case TYPE_USB_DEVICE:
+ return "USB_DEVICE";
+ case TYPE_USB_ACCESSORY:
+ return "USB_ACCESSORY";
+ case TYPE_USB_HEADSET:
+ return "USB_HEADSET";
+ case TYPE_HEARING_AID:
+ return "HEARING_AID";
+ case TYPE_REMOTE_TV:
+ return "REMOTE_TV";
+ case TYPE_REMOTE_SPEAKER:
+ return "REMOTE_SPEAKER";
+ case TYPE_REMOTE_AUDIO_VIDEO_RECEIVER:
+ return "REMOTE_AUDIO_VIDEO_RECEIVER";
+ case TYPE_GROUP:
+ return "GROUP";
+ case TYPE_UNKNOWN:
+ default:
+ return TextUtils.formatSimple("UNKNOWN(%d)", deviceType);
+ }
+ }
+
/**
* Builder for {@link MediaRoute2Info media route info}.
*/
@@ -932,7 +991,8 @@
/**
* Sets the route's type.
- * @hide
+ *
+ * @see MediaRoute2Info#getType()
*/
@NonNull
public Builder setType(@Type int type) {
diff --git a/media/java/android/media/RouteListingPreference.java b/media/java/android/media/RouteListingPreference.java
index ff0a492..6caedda 100644
--- a/media/java/android/media/RouteListingPreference.java
+++ b/media/java/android/media/RouteListingPreference.java
@@ -17,7 +17,6 @@
package android.media;
import android.annotation.IntDef;
-import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
@@ -45,14 +44,24 @@
public final class RouteListingPreference implements Parcelable {
/**
- * {@link Intent} action for apps to take the user to a screen for transferring media playback
- * to the route with the id provided by the extra with key {@link #EXTRA_ROUTE_ID}.
+ * {@link Intent} action that the system uses to take the user the app when the user selects an
+ * {@link Item} whose {@link Item#getSelectionBehavior() selection behavior} is {@link
+ * Item#SELECTION_BEHAVIOR_GO_TO_APP}.
+ *
+ * <p>The launched intent will identify the selected item using the extra identified by {@link
+ * #EXTRA_ROUTE_ID}.
+ *
+ * @see #getLinkedItemComponentName()
+ * @see Item#SELECTION_BEHAVIOR_GO_TO_APP
*/
public static final String ACTION_TRANSFER_MEDIA = "android.media.action.TRANSFER_MEDIA";
/**
* {@link Intent} string extra key that contains the {@link Item#getRouteId() id} of the route
* to transfer to, as part of an {@link #ACTION_TRANSFER_MEDIA} intent.
+ *
+ * @see #getLinkedItemComponentName()
+ * @see Item#SELECTION_BEHAVIOR_GO_TO_APP
*/
public static final String EXTRA_ROUTE_ID = "android.media.extra.ROUTE_ID";
@@ -72,12 +81,12 @@
@NonNull private final List<Item> mItems;
private final boolean mUseSystemOrdering;
- @Nullable private final ComponentName mInAppOnlyItemRoutingReceiver;
+ @Nullable private final ComponentName mLinkedItemComponentName;
private RouteListingPreference(Builder builder) {
mItems = builder.mItems;
mUseSystemOrdering = builder.mUseSystemOrdering;
- mInAppOnlyItemRoutingReceiver = builder.mInAppOnlyItemRoutingReceiver;
+ mLinkedItemComponentName = builder.mLinkedItemComponentName;
}
private RouteListingPreference(Parcel in) {
@@ -85,7 +94,7 @@
in.readParcelableList(new ArrayList<>(), Item.class.getClassLoader(), Item.class);
mItems = List.copyOf(items);
mUseSystemOrdering = in.readBoolean();
- mInAppOnlyItemRoutingReceiver = ComponentName.readFromParcel(in);
+ mLinkedItemComponentName = ComponentName.readFromParcel(in);
}
/**
@@ -110,18 +119,20 @@
}
/**
- * Returns a {@link ComponentName} for handling routes disabled via {@link
- * Item#DISABLE_REASON_IN_APP_ONLY}, or null if the user needs to manually navigate to the app
- * in order to route to select the corresponding routes.
+ * Returns a {@link ComponentName} for navigating to the application.
*
- * <p>If the user selects an {@link Item} disabled via {@link Item#DISABLE_REASON_IN_APP_ONLY},
- * and this method returns a non-null {@link ComponentName}, the system takes the user back to
- * the app by launching an intent to the returned {@link ComponentName}, using action {@link
- * #ACTION_TRANSFER_MEDIA}, with the extra {@link #EXTRA_ROUTE_ID}.
+ * <p>Must not be null if any of the {@link #getItems() items} of this route listing preference
+ * has {@link Item#getSelectionBehavior() selection behavior} {@link
+ * Item#SELECTION_BEHAVIOR_GO_TO_APP}.
+ *
+ * <p>The system navigates to the application when the user selects {@link Item} with {@link
+ * Item#SELECTION_BEHAVIOR_GO_TO_APP} by launching an intent to the returned {@link
+ * ComponentName}, using action {@link #ACTION_TRANSFER_MEDIA}, with the extra {@link
+ * #EXTRA_ROUTE_ID}.
*/
@Nullable
- public ComponentName getInAppOnlyItemRoutingReceiver() {
- return mInAppOnlyItemRoutingReceiver;
+ public ComponentName getLinkedItemComponentName() {
+ return mLinkedItemComponentName;
}
// RouteListingPreference Parcelable implementation.
@@ -135,7 +146,7 @@
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeParcelableList(mItems, flags);
dest.writeBoolean(mUseSystemOrdering);
- ComponentName.writeToParcel(mInAppOnlyItemRoutingReceiver, dest);
+ ComponentName.writeToParcel(mLinkedItemComponentName, dest);
}
// Equals and hashCode.
@@ -151,13 +162,12 @@
RouteListingPreference that = (RouteListingPreference) other;
return mItems.equals(that.mItems)
&& mUseSystemOrdering == that.mUseSystemOrdering
- && Objects.equals(
- mInAppOnlyItemRoutingReceiver, that.mInAppOnlyItemRoutingReceiver);
+ && Objects.equals(mLinkedItemComponentName, that.mLinkedItemComponentName);
}
@Override
public int hashCode() {
- return Objects.hash(mItems, mUseSystemOrdering, mInAppOnlyItemRoutingReceiver);
+ return Objects.hash(mItems, mUseSystemOrdering, mLinkedItemComponentName);
}
/** Builder for {@link RouteListingPreference}. */
@@ -165,7 +175,7 @@
private List<Item> mItems;
private boolean mUseSystemOrdering;
- private ComponentName mInAppOnlyItemRoutingReceiver;
+ private ComponentName mLinkedItemComponentName;
/** Creates a new instance with default values (documented in the setters). */
public Builder() {
@@ -198,14 +208,13 @@
}
/**
- * See {@link #getInAppOnlyItemRoutingReceiver()}.
+ * See {@link #getLinkedItemComponentName()}.
*
* <p>The default value is {@code null}.
*/
@NonNull
- public Builder setInAppOnlyItemRoutingReceiver(
- @Nullable ComponentName inAppOnlyItemRoutingReceiver) {
- mInAppOnlyItemRoutingReceiver = inAppOnlyItemRoutingReceiver;
+ public Builder setLinkedItemComponentName(@Nullable ComponentName linkedItemComponentName) {
+ mLinkedItemComponentName = linkedItemComponentName;
return this;
}
@@ -225,9 +234,32 @@
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(
+ prefix = {"SELECTION_BEHAVIOR_"},
+ value = {
+ SELECTION_BEHAVIOR_NONE,
+ SELECTION_BEHAVIOR_TRANSFER,
+ SELECTION_BEHAVIOR_GO_TO_APP
+ })
+ public @interface SelectionBehavior {}
+
+ /** The corresponding route is not selectable by the user. */
+ public static final int SELECTION_BEHAVIOR_NONE = 0;
+ /** If the user selects the corresponding route, the media transfers to the said route. */
+ public static final int SELECTION_BEHAVIOR_TRANSFER = 1;
+ /**
+ * If the user selects the corresponding route, the system takes the user to the
+ * application.
+ *
+ * <p>The system uses {@link #getLinkedItemComponentName()} in order to navigate to the app.
+ */
+ public static final int SELECTION_BEHAVIOR_GO_TO_APP = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
flag = true,
prefix = {"FLAG_"},
- value = {FLAG_ONGOING_SESSION, FLAG_SUGGESTED_ROUTE})
+ value = {FLAG_ONGOING_SESSION, FLAG_SUGGESTED, FLAG_ONGOING_SESSION_MANAGED})
public @interface Flags {}
/**
@@ -237,6 +269,22 @@
public static final int FLAG_ONGOING_SESSION = 1;
/**
+ * Signals that the ongoing session on the corresponding route is managed by the current
+ * user of the app.
+ *
+ * <p>The system can use this flag to provide visual indication that the route is not only
+ * hosting a session, but also that the user has ownership over said session.
+ *
+ * <p>This flag is ignored if {@link #FLAG_ONGOING_SESSION} is not set, or if the
+ * corresponding route is not currently selected.
+ *
+ * <p>This flag does not affect volume adjustment (see {@link VolumeProvider}, and {@link
+ * MediaRoute2Info#getVolumeHandling()}), or any aspect other than the visual representation
+ * of the corresponding item.
+ */
+ public static final int FLAG_ONGOING_SESSION_MANAGED = 1 << 1;
+
+ /**
* The corresponding route is specially likely to be selected by the user.
*
* <p>A UI reflecting this preference may reserve a specific space for suggested routes,
@@ -244,53 +292,47 @@
* number supported by the UI, the routes listed first in {@link
* RouteListingPreference#getItems()} will take priority.
*/
- public static final int FLAG_SUGGESTED_ROUTE = 1 << 1;
+ public static final int FLAG_SUGGESTED = 1 << 2;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(
- prefix = {"DISABLE_REASON_"},
+ prefix = {"SUBTEXT_"},
value = {
- DISABLE_REASON_NONE,
- DISABLE_REASON_SUBSCRIPTION_REQUIRED,
- DISABLE_REASON_DOWNLOADED_CONTENT,
- DISABLE_REASON_AD,
- DISABLE_REASON_IN_APP_ONLY,
- DISABLE_REASON_CUSTOM
+ SUBTEXT_NONE,
+ SUBTEXT_SUBSCRIPTION_REQUIRED,
+ SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED,
+ SUBTEXT_AD_ROUTING_DISALLOWED,
+ SUBTEXT_CUSTOM
})
- public @interface DisableReason {}
+ public @interface SubText {}
- /** The corresponding route is available for routing. */
- public static final int DISABLE_REASON_NONE = 0;
+ /** The corresponding route has no associated subtext. */
+ public static final int SUBTEXT_NONE = 0;
/**
- * The corresponding route requires a special subscription in order to be available for
- * routing.
+ * The corresponding route's subtext must indicate that it requires a special subscription
+ * in order to be available for routing.
*/
- public static final int DISABLE_REASON_SUBSCRIPTION_REQUIRED = 1;
+ public static final int SUBTEXT_SUBSCRIPTION_REQUIRED = 1;
/**
- * The corresponding route is not available because downloaded content cannot be routed to
- * it.
+ * The corresponding route's subtext must indicate that downloaded content cannot be routed
+ * to it.
*/
- public static final int DISABLE_REASON_DOWNLOADED_CONTENT = 2;
- /** The corresponding route is not available because an ad is in progress. */
- public static final int DISABLE_REASON_AD = 3;
+ public static final int SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED = 2;
/**
- * The corresponding route is only available for routing from within the app.
- *
- * <p>The user may still select the corresponding route if the app provides an {@link
- * #getInAppOnlyItemRoutingReceiver() in-app routing receiver}, in which case the system
- * will take the user to the app.
+ * The corresponding route's subtext must indicate that it is not available because an ad is
+ * in progress.
*/
- public static final int DISABLE_REASON_IN_APP_ONLY = 4;
+ public static final int SUBTEXT_AD_ROUTING_DISALLOWED = 3;
/**
- * The corresponding route is not available because of the reason described by {@link
- * #getCustomDisableReasonMessage()}.
+ * The corresponding route's subtext must be obtained from {@link
+ * #getCustomSubtextMessage()}.
*
* <p>Applications should strongly prefer one of the other disable reasons (for the full
- * list, see {@link #getDisableReason()}) in order to guarantee correct localization and
- * rendering across all form factors.
+ * list, see {@link #getSubText()}) in order to guarantee correct localization and rendering
+ * across all form factors.
*/
- public static final int DISABLE_REASON_CUSTOM = 5;
+ public static final int SUBTEXT_CUSTOM = 10000;
@NonNull
public static final Creator<Item> CREATOR =
@@ -307,29 +349,29 @@
};
@NonNull private final String mRouteId;
+ @SelectionBehavior private final int mSelectionBehavior;
@Flags private final int mFlags;
- @DisableReason private final int mDisableReason;
- private final int mSessionParticipantCount;
- @Nullable private final CharSequence mCustomDisableReasonMessage;
+ @SubText private final int mSubText;
+
+ @Nullable private final CharSequence mCustomSubtextMessage;
private Item(@NonNull Builder builder) {
mRouteId = builder.mRouteId;
+ mSelectionBehavior = builder.mSelectionBehavior;
mFlags = builder.mFlags;
- mDisableReason = builder.mDisableReason;
- mSessionParticipantCount = builder.mSessionParticipantCount;
- mCustomDisableReasonMessage = builder.mCustomDisableReasonMessage;
- validateCustomDisableReasonMessage();
+ mSubText = builder.mSubText;
+ mCustomSubtextMessage = builder.mCustomSubtextMessage;
+ validateCustomMessageSubtext();
}
private Item(Parcel in) {
mRouteId = in.readString();
Preconditions.checkArgument(!TextUtils.isEmpty(mRouteId));
+ mSelectionBehavior = in.readInt();
mFlags = in.readInt();
- mDisableReason = in.readInt();
- mSessionParticipantCount = in.readInt();
- Preconditions.checkArgument(mSessionParticipantCount >= 0);
- mCustomDisableReasonMessage = in.readCharSequence();
- validateCustomDisableReasonMessage();
+ mSubText = in.readInt();
+ mCustomSubtextMessage = in.readCharSequence();
+ validateCustomMessageSubtext();
}
/**
@@ -343,10 +385,21 @@
}
/**
+ * Returns the behavior that the corresponding route has if the user selects it.
+ *
+ * @see #SELECTION_BEHAVIOR_NONE
+ * @see #SELECTION_BEHAVIOR_TRANSFER
+ * @see #SELECTION_BEHAVIOR_GO_TO_APP
+ */
+ public int getSelectionBehavior() {
+ return mSelectionBehavior;
+ }
+
+ /**
* Returns the flags associated to the route that corresponds to this item.
*
* @see #FLAG_ONGOING_SESSION
- * @see #FLAG_SUGGESTED_ROUTE
+ * @see #FLAG_SUGGESTED
*/
@Flags
public int getFlags() {
@@ -354,49 +407,42 @@
}
/**
- * Returns the reason for the corresponding route to be disabled, or {@link
- * #DISABLE_REASON_NONE} if the route is not disabled.
+ * Returns the type of subtext associated to this route.
*
- * @see #DISABLE_REASON_NONE
- * @see #DISABLE_REASON_SUBSCRIPTION_REQUIRED
- * @see #DISABLE_REASON_DOWNLOADED_CONTENT
- * @see #DISABLE_REASON_AD
- * @see #DISABLE_REASON_IN_APP_ONLY
- * @see #DISABLE_REASON_CUSTOM
+ * <p>Subtext types other than {@link #SUBTEXT_NONE} and {@link #SUBTEXT_CUSTOM} must not
+ * have {@link #SELECTION_BEHAVIOR_TRANSFER}.
+ *
+ * <p>If this method returns {@link #SUBTEXT_CUSTOM}, then the subtext is obtained form
+ * {@link #getCustomSubtextMessage()}.
+ *
+ * @see #SUBTEXT_NONE
+ * @see #SUBTEXT_SUBSCRIPTION_REQUIRED
+ * @see #SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED
+ * @see #SUBTEXT_AD_ROUTING_DISALLOWED
+ * @see #SUBTEXT_CUSTOM
*/
- @DisableReason
- public int getDisableReason() {
- return mDisableReason;
+ @SubText
+ public int getSubText() {
+ return mSubText;
}
/**
- * Returns a non-negative number of participants in the ongoing session (if any) on the
- * corresponding route.
+ * Returns a human-readable {@link CharSequence} providing the subtext for the corresponding
+ * route.
*
- * <p>The system ignores this value if zero, or if {@link #getFlags()} does not include
- * {@link #FLAG_ONGOING_SESSION}.
- */
- public int getSessionParticipantCount() {
- return mSessionParticipantCount;
- }
-
- /**
- * Returns a human-readable {@link CharSequence} describing the reason for this route to be
- * disabled. May be null if {@link #getDisableReason()} is not {@link
- * #DISABLE_REASON_CUSTOM}.
- *
- * <p>This value is ignored if the {@link #getDisableReason() disable reason} for this item
- * is not {@link #DISABLE_REASON_CUSTOM}.
+ * <p>This value is ignored if the {@link #getSubText() subtext} for this item is not {@link
+ * #SUBTEXT_CUSTOM}..
*
* <p>Applications must provide a localized message that matches the system's locale. See
* {@link Locale#getDefault()}.
*
- * <p>This message is a hint for the system. Applications should strongly prefer one of the
- * other disable reasons listed in {@link #getDisableReason()}.
+ * <p>Applications should avoid using custom messages (and instead use one of non-custom
+ * subtexts listed in {@link #getSubText()} in order to guarantee correct visual
+ * representation and localization on all form factors.
*/
@Nullable
- public CharSequence getCustomDisableReasonMessage() {
- return mCustomDisableReasonMessage;
+ public CharSequence getCustomSubtextMessage() {
+ return mCustomSubtextMessage;
}
// Item Parcelable implementation.
@@ -409,10 +455,10 @@
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString(mRouteId);
+ dest.writeInt(mSelectionBehavior);
dest.writeInt(mFlags);
- dest.writeInt(mDisableReason);
- dest.writeInt(mSessionParticipantCount);
- dest.writeCharSequence(mCustomDisableReasonMessage);
+ dest.writeInt(mSubText);
+ dest.writeCharSequence(mCustomSubtextMessage);
}
// Equals and hashCode.
@@ -427,40 +473,36 @@
}
Item item = (Item) other;
return mRouteId.equals(item.mRouteId)
+ && mSelectionBehavior == item.mSelectionBehavior
&& mFlags == item.mFlags
- && mDisableReason == item.mDisableReason
- && mSessionParticipantCount == item.mSessionParticipantCount
- && TextUtils.equals(
- mCustomDisableReasonMessage, item.mCustomDisableReasonMessage);
+ && mSubText == item.mSubText
+ && TextUtils.equals(mCustomSubtextMessage, item.mCustomSubtextMessage);
}
@Override
public int hashCode() {
return Objects.hash(
- mRouteId,
- mFlags,
- mDisableReason,
- mSessionParticipantCount,
- mCustomDisableReasonMessage);
+ mRouteId, mSelectionBehavior, mFlags, mSubText, mCustomSubtextMessage);
}
- private void validateCustomDisableReasonMessage() {
- if (mDisableReason == DISABLE_REASON_CUSTOM) {
- Preconditions.checkArgument(
- !TextUtils.isEmpty(mCustomDisableReasonMessage),
- "customDisableReasonMessage must not be null or empty if disable reason is"
- + " DISABLE_REASON_CUSTOM.");
- }
+ // Internal methods.
+
+ private void validateCustomMessageSubtext() {
+ Preconditions.checkArgument(
+ mSubText != SUBTEXT_CUSTOM || mCustomSubtextMessage != null,
+ "The custom subtext message cannot be null if subtext is SUBTEXT_CUSTOM.");
}
+ // Internal classes.
+
/** Builder for {@link Item}. */
public static final class Builder {
private final String mRouteId;
+ private int mSelectionBehavior;
private int mFlags;
- private int mDisableReason;
- private int mSessionParticipantCount;
- private CharSequence mCustomDisableReasonMessage;
+ private int mSubText;
+ private CharSequence mCustomSubtextMessage;
/**
* Constructor.
@@ -470,39 +512,51 @@
public Builder(@NonNull String routeId) {
Preconditions.checkArgument(!TextUtils.isEmpty(routeId));
mRouteId = routeId;
- mDisableReason = DISABLE_REASON_NONE;
+ mSelectionBehavior = SELECTION_BEHAVIOR_TRANSFER;
+ mSubText = SUBTEXT_NONE;
}
- /** See {@link Item#getFlags()}. */
+ /**
+ * See {@link Item#getSelectionBehavior()}.
+ *
+ * <p>The default value is {@link #ACTION_TRANSFER_MEDIA}.
+ */
+ @NonNull
+ public Builder setSelectionBehavior(int selectionBehavior) {
+ mSelectionBehavior = selectionBehavior;
+ return this;
+ }
+
+ /**
+ * See {@link Item#getFlags()}.
+ *
+ * <p>The default value is zero (no flags).
+ */
@NonNull
public Builder setFlags(int flags) {
mFlags = flags;
return this;
}
- /** See {@link Item#getDisableReason()}. */
+ /**
+ * See {@link Item#getSubText()}.
+ *
+ * <p>The default value is {@link #SUBTEXT_NONE}.
+ */
@NonNull
- public Builder setDisableReason(int disableReason) {
- mDisableReason = disableReason;
+ public Builder setSubText(int subText) {
+ mSubText = subText;
return this;
}
- /** See {@link Item#getSessionParticipantCount()}. */
+ /**
+ * See {@link Item#getCustomSubtextMessage()}.
+ *
+ * <p>The default value is {@code null}.
+ */
@NonNull
- public Builder setSessionParticipantCount(
- @IntRange(from = 0) int sessionParticipantCount) {
- Preconditions.checkArgument(
- sessionParticipantCount >= 0,
- "sessionParticipantCount must be non-negative.");
- mSessionParticipantCount = sessionParticipantCount;
- return this;
- }
-
- /** See {@link Item#getCustomDisableReasonMessage()}. */
- @NonNull
- public Builder setCustomDisableReasonMessage(
- @Nullable CharSequence customDisableReasonMessage) {
- mCustomDisableReasonMessage = customDisableReasonMessage;
+ public Builder setCustomSubtextMessage(@Nullable CharSequence customSubtextMessage) {
+ mCustomSubtextMessage = customSubtextMessage;
return this;
}
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index 985ac3c..9e9012e 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -253,23 +253,19 @@
public void onStop() { }
/**
- * Indicates the width and height of the captured region in pixels. Called immediately after
- * capture begins to provide the app with accurate sizing for the stream. Also called
- * when the region captured in this MediaProjection session is resized.
+ * Invoked immediately after capture begins or when the size of the captured region changes,
+ * providing the accurate sizing for the streamed capture.
* <p>
* The given width and height, in pixels, corresponds to the same width and height that
- * would be returned from {@link android.view.WindowMetrics#getBounds()}
+ * would be returned from {@link android.view.WindowMetrics#getBounds()} of the captured
+ * region.
* </p>
* <p>
- * Without the application resizing the {@link VirtualDisplay} (returned from
- * {@code MediaProjection#createVirtualDisplay}) and output {@link Surface} (provided
- * to {@code MediaProjection#createVirtualDisplay}), the captured stream will have
- * letterboxing (black bars) around the recorded content to make up for the
- * difference in aspect ratio.
- * </p>
- * <p>
- * The application can prevent the letterboxing by overriding this method, and
- * updating the size of both the {@link VirtualDisplay} and output {@link Surface}:
+ * If the recorded content has a different aspect ratio from either the
+ * {@link VirtualDisplay} or output {@link Surface}, the captured stream has letterboxing
+ * (black bars) around the recorded content. The application can avoid the letterboxing
+ * around the recorded content by updating the size of both the {@link VirtualDisplay} and
+ * output {@link Surface}:
* </p>
*
* <pre>
@@ -293,27 +289,29 @@
public void onCapturedContentResize(int width, int height) { }
/**
- * Indicates the visibility of the captured region has changed. Called immediately after
- * capture begins with the initial visibility state, and when visibility changes. Provides
- * the app with accurate state for presenting its own UI. The application can take advantage
- * of this by showing or hiding the captured content, based on if the captured region is
- * currently visible to the user.
+ * Invoked immediately after capture begins or when the visibility of the captured region
+ * changes, providing the current visibility of the captured region.
+ * <p>
+ * Applications can take advantage of this callback by showing or hiding the captured
+ * content from the output {@link Surface}, based on if the captured region is currently
+ * visible to the user.
+ * </p>
* <p>
* For example, if the user elected to capture a single app (from the activity shown from
- * {@link MediaProjectionManager#createScreenCaptureIntent()}), the callback may be
- * triggered for the following reasons:
+ * {@link MediaProjectionManager#createScreenCaptureIntent()}), the following scenarios
+ * trigger the callback:
* <ul>
* <li>
- * The captured region may become visible ({@code isVisible} with value
- * {@code true}), because the captured app is at least partially visible. This may
- * happen if the captured app was previously covered by another app. The other app
- * moves to show at least some portion of the captured app.
+ * The captured region is visible ({@code isVisible} with value {@code true}),
+ * because the captured app is at least partially visible. This may happen if the
+ * user moves the covering app to show at least some portion of the captured app
+ * (e.g. the user has multiple apps visible in a multi-window mode such as split
+ * screen).
* </li>
* <li>
- * The captured region may become invisible ({@code isVisible} with value
- * {@code false}) if it is entirely hidden. This may happen if the captured app is
- * entirely covered by another app, or the user navigates away from the captured
- * app.
+ * The captured region is invisible ({@code isVisible} with value {@code false}) if
+ * it is entirely hidden. This may happen if another app entirely covers the
+ * captured app, or the user navigates away from the captured app.
* </li>
* </ul>
* </p>
diff --git a/media/java/android/media/soundtrigger/SoundTriggerDetector.java b/media/java/android/media/soundtrigger/SoundTriggerDetector.java
index 59e19ed..1a3e54d 100644
--- a/media/java/android/media/soundtrigger/SoundTriggerDetector.java
+++ b/media/java/android/media/soundtrigger/SoundTriggerDetector.java
@@ -25,6 +25,7 @@
import android.compat.annotation.UnsupportedAppUsage;
import android.hardware.soundtrigger.IRecognitionStatusCallback;
import android.hardware.soundtrigger.SoundTrigger;
+import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
import android.media.AudioFormat;
@@ -50,6 +51,7 @@
* VoiceInteractionService} instead. Access to this class is protected by a permission
* granted only to system or privileged apps.
* @deprecated use {@link SoundTriggerManager} directly
+ *
* @hide
*/
@Deprecated
@@ -67,7 +69,7 @@
private final Object mLock = new Object();
private final ISoundTriggerSession mSoundTriggerSession;
- private final UUID mSoundModelId;
+ private final GenericSoundModel mSoundModel;
private final Callback mCallback;
private final Handler mHandler;
private final RecognitionCallback mRecognitionCallback;
@@ -277,10 +279,11 @@
* This class should be constructed by the {@link SoundTriggerManager}.
* @hide
*/
- SoundTriggerDetector(ISoundTriggerSession soundTriggerSession, UUID soundModelId,
+ SoundTriggerDetector(ISoundTriggerSession soundTriggerSession,
+ @NonNull GenericSoundModel soundModel,
@NonNull Callback callback, @Nullable Handler handler) {
mSoundTriggerSession = soundTriggerSession;
- mSoundModelId = soundModelId;
+ mSoundModel = soundModel;
mCallback = callback;
if (handler == null) {
mHandler = new MyHandler();
@@ -320,7 +323,7 @@
int status;
try {
- status = mSoundTriggerSession.startRecognition(new ParcelUuid(mSoundModelId),
+ status = mSoundTriggerSession.startRecognition(mSoundModel,
mRecognitionCallback, new RecognitionConfig(captureTriggerAudio,
allowMultipleTriggers, null, null, audioCapabilities),
runInBatterySaver);
@@ -339,7 +342,7 @@
public boolean stopRecognition() {
int status = STATUS_OK;
try {
- status = mSoundTriggerSession.stopRecognition(new ParcelUuid(mSoundModelId),
+ status = mSoundTriggerSession.stopRecognition(new ParcelUuid(mSoundModel.getUuid()),
mRecognitionCallback);
} catch (RemoteException e) {
return false;
diff --git a/media/java/android/media/soundtrigger/SoundTriggerManager.java b/media/java/android/media/soundtrigger/SoundTriggerManager.java
index 8dc708a..c473a01 100644
--- a/media/java/android/media/soundtrigger/SoundTriggerManager.java
+++ b/media/java/android/media/soundtrigger/SoundTriggerManager.java
@@ -184,11 +184,16 @@
if (oldInstance != null) {
// Shutdown old instance.
}
- SoundTriggerDetector newInstance = new SoundTriggerDetector(mSoundTriggerSession,
- soundModelId, callback, handler);
- mReceiverInstanceMap.put(soundModelId, newInstance);
- return newInstance;
- }
+ try {
+ SoundTriggerDetector newInstance = new SoundTriggerDetector(mSoundTriggerSession,
+ mSoundTriggerSession.getSoundModel(new ParcelUuid(soundModelId)),
+ callback, handler);
+ mReceiverInstanceMap.put(soundModelId, newInstance);
+ return newInstance;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
/**
* Class captures the data and fields that represent a non-keyphrase sound model. Use the
diff --git a/media/java/android/media/tv/AdBuffer.java b/media/java/android/media/tv/AdBuffer.java
index ed44508..230d763 100644
--- a/media/java/android/media/tv/AdBuffer.java
+++ b/media/java/android/media/tv/AdBuffer.java
@@ -24,9 +24,8 @@
/**
* Buffer for advertisement data.
- * @hide
*/
-public class AdBuffer implements Parcelable {
+public final class AdBuffer implements Parcelable {
private final int mId;
@NonNull
private final String mMimeType;
@@ -60,6 +59,8 @@
/**
* Gets corresponding AD request ID.
+ *
+ * @return The ID of the ad request
*/
public int getId() {
return mId;
@@ -67,6 +68,8 @@
/**
* Gets the mime type of the data.
+ *
+ * @return The mime type of the data.
*/
@NonNull
public String getMimeType() {
@@ -74,7 +77,17 @@
}
/**
- * Gets the shared memory which stores the data.
+ * Gets the {@link SharedMemory} which stores the data.
+ *
+ * <p> Information on how the data in this buffer is formatted can be found using
+ * {@link AdRequest#getMetadata()}
+ * <p> This data lives in a {@link SharedMemory} instance because of the
+ * potentially large amount of data needed to store the ad. This optimizes the
+ * data communication between the ad data source and the service responsible for
+ * its display.
+ *
+ * @see SharedMemory#create(String, int)
+ * @return The {@link SharedMemory} that stores the data for this ad buffer.
*/
@NonNull
public SharedMemory getSharedMemory() {
@@ -82,28 +95,38 @@
}
/**
- * Gets the offset of the buffer.
+ * Gets the offset into the shared memory to begin mapping.
+ *
+ * @see SharedMemory#map(int, int, int)
+ * @return The offset of this ad buffer in the shared memory in bytes.
*/
public int getOffset() {
return mOffset;
}
/**
- * Gets the data length.
+ * Gets the data length of this ad buffer.
+ *
+ * @return The data length of this ad buffer in bytes.
*/
public int getLength() {
return mLength;
}
/**
- * Gets the presentation time in microseconds.
+ * Gets the presentation time.
+ *
+ * @return The presentation time in microseconds.
*/
public long getPresentationTimeUs() {
return mPresentationTimeUs;
}
/**
- * Gets the flags.
+ * Gets the buffer flags for this ad buffer.
+ *
+ * @see android.media.MediaCodec
+ * @return The buffer flags for this ad buffer.
*/
@BufferFlag
public int getFlags() {
diff --git a/media/java/android/media/tv/AdRequest.java b/media/java/android/media/tv/AdRequest.java
index 60dfc5e..d8cddfc 100644
--- a/media/java/android/media/tv/AdRequest.java
+++ b/media/java/android/media/tv/AdRequest.java
@@ -79,7 +79,6 @@
mediaFileType, metadata);
}
- /** @hide */
public AdRequest(int id, @RequestType int requestType, @Nullable Uri uri, long startTime,
long stopTime, long echoInterval, @NonNull Bundle metadata) {
this(id, requestType, null, uri, startTime, stopTime, echoInterval, null, metadata);
@@ -153,7 +152,6 @@
*
* @return The URI of the AD media. Can be {@code null} for {@link #REQUEST_TYPE_STOP} or a file
* descriptor is used.
- * @hide
*/
@Nullable
public Uri getUri() {
diff --git a/media/java/android/media/tv/AdResponse.java b/media/java/android/media/tv/AdResponse.java
index a15e8c1..7ec4eb2 100644
--- a/media/java/android/media/tv/AdResponse.java
+++ b/media/java/android/media/tv/AdResponse.java
@@ -43,7 +43,6 @@
public static final int RESPONSE_TYPE_FINISHED = 2;
public static final int RESPONSE_TYPE_STOPPED = 3;
public static final int RESPONSE_TYPE_ERROR = 4;
- /** @hide */
public static final int RESPONSE_TYPE_BUFFERING = 5;
public static final @NonNull Parcelable.Creator<AdResponse> CREATOR =
diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java
index 7a4d988d..8166114 100644
--- a/media/java/android/media/tv/TvInputService.java
+++ b/media/java/android/media/tv/TvInputService.java
@@ -1005,9 +1005,10 @@
/**
* Notifies the advertisement buffer is consumed.
- * @hide
+ *
+ * @param buffer the {@link AdBuffer} that was consumed.
*/
- public void notifyAdBufferConsumed(AdBuffer buffer) {
+ public void notifyAdBufferConsumed(@NonNull AdBuffer buffer) {
executeOrPostRunnableOnMainThread(new Runnable() {
@MainThread
@Override
@@ -1320,10 +1321,11 @@
}
/**
- * Called when advertisement buffer is ready.
- * @hide
+ * Called when an advertisement buffer is ready for playback.
+ *
+ * @param buffer The {@link AdBuffer} that became ready for playback.
*/
- public void onAdBuffer(AdBuffer buffer) {
+ public void onAdBuffer(@NonNull AdBuffer buffer) {
}
/**
diff --git a/media/java/android/media/tv/TvRecordingClient.java b/media/java/android/media/tv/TvRecordingClient.java
index b1356f5..cdeef2b 100644
--- a/media/java/android/media/tv/TvRecordingClient.java
+++ b/media/java/android/media/tv/TvRecordingClient.java
@@ -575,7 +575,7 @@
Log.w(TAG, "onError - session not created");
return;
}
- if (mCallback == null) {
+ if (mCallback != null) {
mCallback.onError(error);
}
if (mTvIAppView != null) {
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 360073d..8b85fa1 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -606,22 +606,22 @@
}
/**
- * Receives requested recording info.
+ * Receives the requested {@link android.media.tv.TvRecordingInfo}.
*
- * @param recordingInfo The requested recording info. Null if recording not found.
- * @hide
+ * @see #requestTvRecordingInfo(String)
+ * @param recordingInfo The requested recording info. {@code null} if no recording found.
*/
public void onTvRecordingInfo(@Nullable TvRecordingInfo recordingInfo) {
}
/**
- * Receives requested recording info.
+ * Receives requested recording info list.
*
- * @param recordingInfoList The requested recording info list. Null if recording not found.
- * @hide
+ * @see #requestTvRecordingInfoList(int)
+ * @param recordingInfoList The list of recording info requested. Returns an empty list if
+ * no matching recording info found.
*/
- public void onTvRecordingInfoList(@Nullable List<TvRecordingInfo> recordingInfoList) {
- }
+ public void onTvRecordingInfoList(@NonNull List<TvRecordingInfo> recordingInfoList) {}
/**
* Receives started recording's ID.
@@ -896,10 +896,10 @@
/**
* Called when an advertisement buffer is consumed.
- * @hide
+ *
+ * @param buffer The {@link AdBuffer} that was consumed.
*/
- public void onAdBufferConsumed(AdBuffer buffer) {
-
+ public void onAdBufferConsumed(@NonNull AdBuffer buffer) {
}
/**
@@ -1919,10 +1919,11 @@
/**
* Notifies when the advertisement buffer is filled and ready to be read.
- * @hide
+ *
+ * @param buffer The {@link AdBuffer} to be received
*/
@CallSuper
- public void notifyAdBuffer(AdBuffer buffer) {
+ public void notifyAdBuffer(@NonNull AdBuffer buffer) {
executeOrPostRunnableOnMainThread(new Runnable() {
@MainThread
@Override
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
index 78c6bcf..0b44a89 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
@@ -635,8 +635,8 @@
/**
* Sends the requested {@link android.media.tv.TvRecordingInfo}.
*
- * @param recordingInfo The recording info requested {@code null} if no recording found.
- * @hide
+ * @see TvInteractiveAppService.Session#requestTvRecordingInfo(String)
+ * @param recordingInfo The recording info requested. {@code null} if no recording found.
*/
public void sendTvRecordingInfo(@Nullable TvRecordingInfo recordingInfo) {
if (DEBUG) {
@@ -650,10 +650,11 @@
/**
* Sends the requested {@link android.media.tv.TvRecordingInfo}.
*
- * @param recordingInfoList The list of recording info requested.
- * @hide
+ * @see TvInteractiveAppService.Session#requestTvRecordingInfoList(int)
+ * @param recordingInfoList The list of recording info requested. Returns an empty list if no
+ * matching recording info found.
*/
- public void sendTvRecordingInfoList(@Nullable List<TvRecordingInfo> recordingInfoList) {
+ public void sendTvRecordingInfoList(@NonNull List<TvRecordingInfo> recordingInfoList) {
if (DEBUG) {
Log.d(TAG, "sendTvRecordingInfoList");
}
diff --git a/media/java/android/media/tv/tuner/filter/MediaEvent.java b/media/java/android/media/tv/tuner/filter/MediaEvent.java
index d958db1..8f2e7b5 100644
--- a/media/java/android/media/tv/tuner/filter/MediaEvent.java
+++ b/media/java/android/media/tv/tuner/filter/MediaEvent.java
@@ -18,10 +18,15 @@
import android.annotation.BytesLong;
import android.annotation.IntRange;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.media.AudioPresentation;
import android.media.MediaCodec.LinearBlock;
+import java.util.Collections;
+import java.util.List;
+
/**
* Filter event sent from {@link Filter} objects with media type.
*
@@ -51,12 +56,13 @@
private final boolean mIsPrivateData;
private final int mScIndexMask;
private final AudioDescriptor mExtraMetaData;
+ private final List<AudioPresentation> mAudioPresentations;
// This constructor is used by JNI code only
private MediaEvent(int streamId, boolean isPtsPresent, long pts, boolean isDtsPresent, long dts,
long dataLength, long offset, LinearBlock buffer, boolean isSecureMemory, long dataId,
int mpuSequenceNumber, boolean isPrivateData, int scIndexMask,
- AudioDescriptor extraMetaData) {
+ AudioDescriptor extraMetaData, List<AudioPresentation> audioPresentations) {
mStreamId = streamId;
mIsPtsPresent = isPtsPresent;
mPts = pts;
@@ -71,6 +77,7 @@
mIsPrivateData = isPrivateData;
mScIndexMask = scIndexMask;
mExtraMetaData = extraMetaData;
+ mAudioPresentations = audioPresentations;
}
/**
@@ -215,6 +222,13 @@
return mExtraMetaData;
}
+ /**
+ * Gets audio presentations.
+ */
+ @NonNull
+ public List<AudioPresentation> getAudioPresentations() {
+ return mAudioPresentations == null ? Collections.emptyList() : mAudioPresentations;
+ }
/**
* Finalize the MediaEvent object.
diff --git a/media/java/android/media/tv/tuner/frontend/IptvFrontendSettings.java b/media/java/android/media/tv/tuner/frontend/IptvFrontendSettings.java
index fb11ea9..ba75102 100644
--- a/media/java/android/media/tv/tuner/frontend/IptvFrontendSettings.java
+++ b/media/java/android/media/tv/tuner/frontend/IptvFrontendSettings.java
@@ -92,7 +92,7 @@
private final long mBitrate;
private final String mContentUrl;
- public IptvFrontendSettings(@NonNull byte[] srcIpAddress, @NonNull byte[] dstIpAddress,
+ private IptvFrontendSettings(@NonNull byte[] srcIpAddress, @NonNull byte[] dstIpAddress,
int srcPort, int dstPort, @NonNull IptvFrontendSettingsFec fec, int protocol, int igmp,
long bitrate, @NonNull String contentUrl) {
super(0);
diff --git a/media/java/android/media/tv/tuner/frontend/IptvFrontendSettingsFec.java b/media/java/android/media/tv/tuner/frontend/IptvFrontendSettingsFec.java
index 699d615..a70af17 100644
--- a/media/java/android/media/tv/tuner/frontend/IptvFrontendSettingsFec.java
+++ b/media/java/android/media/tv/tuner/frontend/IptvFrontendSettingsFec.java
@@ -62,7 +62,7 @@
private final int mFecRowNum;
private final int mFecColNum;
- public IptvFrontendSettingsFec(@FecType int fecType, int fecRowNum, int fecColNum) {
+ private IptvFrontendSettingsFec(@FecType int fecType, int fecRowNum, int fecColNum) {
mFecType = fecType;
mFecRowNum = fecRowNum;
mFecColNum = fecColNum;
diff --git a/media/jni/Android.bp b/media/jni/Android.bp
index a0304bb..ed1072c 100644
--- a/media/jni/Android.bp
+++ b/media/jni/Android.bp
@@ -88,6 +88,7 @@
"android.hidl.memory@1.0",
"android.hidl.token@1.0-utils",
"android.hardware.drm-V1-ndk",
+ "android.hardware.tv.tuner-V2-ndk",
],
header_libs: [
diff --git a/media/jni/android_media_AudioPresentation.h b/media/jni/android_media_AudioPresentation.h
index a3adddd..0c83ddf 100644
--- a/media/jni/android_media_AudioPresentation.h
+++ b/media/jni/android_media_AudioPresentation.h
@@ -19,10 +19,14 @@
#include "jni.h"
+#include <aidl/android/hardware/tv/tuner/AudioPresentation.h>
#include <media/stagefright/foundation/ADebug.h> // CHECK
#include <media/stagefright/foundation/AudioPresentationInfo.h>
#include <nativehelper/ScopedLocalRef.h>
+using ::aidl::android::hardware::tv::tuner::AudioPreselectionRenderingIndicationType;
+using TunerAudioPresentation = ::aidl::android::hardware::tv::tuner::AudioPresentation;
+
namespace android {
struct JAudioPresentationInfo {
@@ -97,6 +101,38 @@
}
static void addPresentations(JNIEnv *env, const fields_t& fields,
+ const std::vector<TunerAudioPresentation>& tunerAudioPresentations,
+ jobject presentationsJObj) {
+ AudioPresentationCollection apc = {};
+ static const std::map<AudioPreselectionRenderingIndicationType, MasteringIndication> mMap {
+ { AudioPreselectionRenderingIndicationType::NOT_INDICATED, MASTERING_NOT_INDICATED },
+ { AudioPreselectionRenderingIndicationType::STEREO, MASTERED_FOR_STEREO },
+ { AudioPreselectionRenderingIndicationType::TWO_DIMENSIONAL, MASTERED_FOR_SURROUND },
+ { AudioPreselectionRenderingIndicationType::THREE_DIMENSIONAL, MASTERED_FOR_3D },
+ { AudioPreselectionRenderingIndicationType::HEADPHONE, MASTERED_FOR_HEADPHONE },
+ };
+ for (const auto& tap : tunerAudioPresentations) {
+ AudioPresentationV1 ap;
+ ap.mPresentationId = tap.preselection.preselectionId;
+ ap.mProgramId = tap.ac4ShortProgramId;
+ for (const auto& md : tap.preselection.labels) {
+ ap.mLabels.insert(std::pair(md.language, md.text));
+ }
+ ap.mLanguage = tap.preselection.language;
+ ap.mMasteringIndication = MASTERING_NOT_INDICATED;
+ auto masteringSearch = mMap.find(tap.preselection.renderingIndication);
+ if (masteringSearch != mMap.end()) {
+ ap.mMasteringIndication = masteringSearch->second;
+ }
+ ap.mAudioDescriptionAvailable = tap.preselection.hasAudioDescription;
+ ap.mSpokenSubtitlesAvailable = tap.preselection.hasSpokenSubtitles;
+ ap.mDialogueEnhancementAvailable = tap.preselection.hasDialogueEnhancement;
+ apc.push_back(ap);
+ }
+ addPresentations(env, fields, apc, presentationsJObj);
+ }
+
+ static void addPresentations(JNIEnv *env, const fields_t& fields,
const AudioPresentationCollection& presentations, jobject presentationsJObj) {
for (const auto& ap : presentations) {
ScopedLocalRef<jobject> jLabelObject = convertLabelsToMap(env, fields, ap.mLabels);
diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp
index 625e842..f56e236 100644
--- a/media/jni/android_media_tv_Tuner.cpp
+++ b/media/jni/android_media_tv_Tuner.cpp
@@ -20,6 +20,7 @@
#include "android_media_tv_Tuner.h"
#include <aidl/android/hardware/tv/tuner/AudioExtraMetaData.h>
+#include <aidl/android/hardware/tv/tuner/AudioPresentation.h>
#include <aidl/android/hardware/tv/tuner/AudioStreamType.h>
#include <aidl/android/hardware/tv/tuner/AvStreamType.h>
#include <aidl/android/hardware/tv/tuner/Constant.h>
@@ -161,12 +162,14 @@
#include <nativehelper/ScopedLocalRef.h>
#include <utils/Log.h>
+#include "android_media_AudioPresentation.h"
#include "android_media_MediaCodecLinearBlock.h"
#include "android_runtime/AndroidRuntime.h"
#pragma GCC diagnostic ignored "-Wunused-function"
using ::aidl::android::hardware::tv::tuner::AudioExtraMetaData;
+using ::aidl::android::hardware::tv::tuner::AudioPreselection;
using ::aidl::android::hardware::tv::tuner::AudioStreamType;
using ::aidl::android::hardware::tv::tuner::AvStreamType;
using ::aidl::android::hardware::tv::tuner::Constant;
@@ -362,6 +365,9 @@
}
namespace android {
+
+static JAudioPresentationInfo::fields_t gAudioPresentationFields;
+
/////////////// LnbClientCallbackImpl ///////////////////////
void LnbClientCallbackImpl::onEvent(const LnbEventType lnbEventType) {
ALOGV("LnbClientCallbackImpl::onEvent, type=%d", lnbEventType);
@@ -634,27 +640,45 @@
eventClazz,
"<init>",
"(IZJZJJJLandroid/media/MediaCodec$LinearBlock;"
- "ZJIZILandroid/media/tv/tuner/filter/AudioDescriptor;)V");
+ "ZJIZILandroid/media/tv/tuner/filter/AudioDescriptor;"
+ "Ljava/util/List;)V");
jfieldID eventContext = env->GetFieldID(eventClazz, "mNativeContext", "J");
const DemuxFilterMediaEvent &mediaEvent = event.get<DemuxFilterEvent::Tag::media>();
jobject audioDescriptor = nullptr;
- if (mediaEvent.extraMetaData.getTag() == DemuxFilterMediaEventExtraMetaData::Tag::audio) {
- jclass adClazz = env->FindClass("android/media/tv/tuner/filter/AudioDescriptor");
- jmethodID adInit = env->GetMethodID(adClazz, "<init>", "(BBCBBB)V");
+ gAudioPresentationFields.init(env);
+ jobject presentationsJObj = JAudioPresentationInfo::asJobject(env, gAudioPresentationFields);
+ switch (mediaEvent.extraMetaData.getTag()) {
+ case DemuxFilterMediaEventExtraMetaData::Tag::audio: {
+ jclass adClazz = env->FindClass("android/media/tv/tuner/filter/AudioDescriptor");
+ jmethodID adInit = env->GetMethodID(adClazz, "<init>", "(BBCBBB)V");
- const AudioExtraMetaData &ad =
- mediaEvent.extraMetaData.get<DemuxFilterMediaEventExtraMetaData::Tag::audio>();
- jbyte adFade = ad.adFade;
- jbyte adPan = ad.adPan;
- jchar versionTextTag = ad.versionTextTag;
- jbyte adGainCenter = ad.adGainCenter;
- jbyte adGainFront = ad.adGainFront;
- jbyte adGainSurround = ad.adGainSurround;
+ const AudioExtraMetaData &ad =
+ mediaEvent.extraMetaData.get<DemuxFilterMediaEventExtraMetaData::Tag::audio>();
+ jbyte adFade = ad.adFade;
+ jbyte adPan = ad.adPan;
+ jchar versionTextTag = ad.versionTextTag;
+ jbyte adGainCenter = ad.adGainCenter;
+ jbyte adGainFront = ad.adGainFront;
+ jbyte adGainSurround = ad.adGainSurround;
- audioDescriptor = env->NewObject(adClazz, adInit, adFade, adPan, versionTextTag,
- adGainCenter, adGainFront, adGainSurround);
- env->DeleteLocalRef(adClazz);
+ audioDescriptor = env->NewObject(adClazz, adInit, adFade, adPan, versionTextTag,
+ adGainCenter, adGainFront, adGainSurround);
+ env->DeleteLocalRef(adClazz);
+ break;
+ }
+ case DemuxFilterMediaEventExtraMetaData::Tag::audioPresentations: {
+ JAudioPresentationInfo::addPresentations(
+ env, gAudioPresentationFields,
+ mediaEvent.extraMetaData
+ .get<DemuxFilterMediaEventExtraMetaData::Tag::audioPresentations>(),
+ presentationsJObj);
+ break;
+ }
+ default: {
+ ALOGE("FilterClientCallbackImpl::getMediaEvent: unknown extraMetaData");
+ break;
+ }
}
jlong dataLength = mediaEvent.dataLength;
@@ -683,7 +707,8 @@
jobject obj = env->NewObject(eventClazz, eventInit, streamId, isPtsPresent, pts, isDtsPresent,
dts, dataLength, offset, nullptr, isSecureMemory, avDataId,
- mpuSequenceNumber, isPesPrivateData, sc, audioDescriptor);
+ mpuSequenceNumber, isPesPrivateData, sc, audioDescriptor,
+ presentationsJObj);
uint64_t avSharedMemSize = mFilterClient->getAvSharedHandleInfo().size;
if (mediaEvent.avMemory.fds.size() > 0 || mediaEvent.avDataId != 0 ||
@@ -702,6 +727,7 @@
}
env->DeleteLocalRef(obj);
env->DeleteLocalRef(eventClazz);
+ env->DeleteLocalRef(presentationsJObj);
}
void FilterClientCallbackImpl::getPesEvent(jobjectArray &arr, const int size,
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index 27666caa..b3628fa 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -69,7 +69,7 @@
int updateTargetWorkDuration(int64_t targetDurationNanos);
int reportActualWorkDuration(int64_t actualDurationNanos);
- int sendHint(int32_t hint);
+ int sendHint(SessionHint hint);
int setThreads(const int32_t* threadIds, size_t size);
int getThreadIds(int32_t* const threadIds, size_t* size);
@@ -243,7 +243,7 @@
return 0;
}
-int APerformanceHintSession::sendHint(int32_t hint) {
+int APerformanceHintSession::sendHint(SessionHint hint) {
if (hint < 0 || hint >= static_cast<int32_t>(mLastHintSentTimestamp.size())) {
ALOGE("%s: invalid session hint %d", __FUNCTION__, hint);
return EINVAL;
@@ -335,7 +335,7 @@
delete session;
}
-int APerformanceHint_sendHint(void* session, int32_t hint) {
+int APerformanceHint_sendHint(void* session, SessionHint hint) {
return reinterpret_cast<APerformanceHintSession*>(session)->sendHint(hint);
}
diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
index 321a7dd..791adfd 100644
--- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
+++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
@@ -127,7 +127,7 @@
result = APerformanceHint_reportActualWorkDuration(session, -1L);
EXPECT_EQ(EINVAL, result);
- int hintId = 2;
+ SessionHint hintId = SessionHint::CPU_LOAD_RESET;
EXPECT_CALL(*iSession, sendHint(Eq(hintId))).Times(Exactly(1));
result = APerformanceHint_sendHint(session, hintId);
EXPECT_EQ(0, result);
@@ -140,7 +140,7 @@
result = APerformanceHint_sendHint(session, hintId);
EXPECT_EQ(0, result);
- result = APerformanceHint_sendHint(session, -1);
+ result = APerformanceHint_sendHint(session, static_cast<SessionHint>(-1));
EXPECT_EQ(EINVAL, result);
EXPECT_CALL(*iSession, close()).Times(Exactly(1));
diff --git a/packages/CompanionDeviceManager/res/layout/list_item_permission.xml b/packages/CompanionDeviceManager/res/layout/list_item_permission.xml
index ab2d815..6c463e1 100644
--- a/packages/CompanionDeviceManager/res/layout/list_item_permission.xml
+++ b/packages/CompanionDeviceManager/res/layout/list_item_permission.xml
@@ -22,7 +22,7 @@
android:orientation="horizontal"
android:paddingStart="32dp"
android:paddingEnd="32dp"
- android:paddingBottom="14dp">
+ android:paddingTop="12dp">
<ImageView
android:id="@+id/permission_icon"
diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml
index 7397688..b842761 100644
--- a/packages/CompanionDeviceManager/res/values/strings.xml
+++ b/packages/CompanionDeviceManager/res/values/strings.xml
@@ -20,7 +20,7 @@
<string name="app_label">Companion Device Manager</string>
<!-- Title of the device association confirmation dialog. -->
- <string name="confirmation_title">Allow <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong> to access your <strong><xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g></strong></string>
+ <string name="confirmation_title">Allow <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong> to access <strong><xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g></strong></string>
<!-- ================= DEVICE_PROFILE_WATCH and null profile ================= -->
@@ -31,10 +31,10 @@
<string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to be managed by <strong><xliff:g id="app_name" example="Android Wear">%2$s</xliff:g></strong></string>
<!-- Description of the privileges the application will get if associated with the companion device of WATCH profile (type) [CHAR LIMIT=NONE] -->
- <string name="summary_watch">The app is needed to manage your <xliff:g id="device_name" example="My Watch">%1$s</xliff:g>. <xliff:g id="app_name" example="Android Wear">%2$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts, Calendar, Call logs and Nearby devices permissions.</string>
+ <string name="summary_watch">The app is needed to manage your <xliff:g id="device_name" example="My Watch">%1$s</xliff:g>. <xliff:g id="app_name" example="Android Wear">%2$s</xliff:g> will be allowed to sync info, like the name of someone calling, interact with your notifications and access your Phone, SMS, Contacts, Calendar, Call logs and Nearby devices permissions.</string>
<!-- Description of the privileges the application will get if associated with the companion device of WATCH profile for singleDevice(type) [CHAR LIMIT=NONE] -->
- <string name="summary_watch_single_device">The app is needed to manage your <xliff:g id="device_name" example="My Watch">%1$s</xliff:g>. <xliff:g id="app_name" example="Android Wear">%2$s</xliff:g> will be allowed to interact with these permissions:</string>
+ <string name="summary_watch_single_device">The app is needed to manage your <xliff:g id="device_name" example="My Watch">%1$s</xliff:g>. <xliff:g id="app_name" example="Android Wear">%2$s</xliff:g> will be allowed to sync info, like the name of someone calling, and access these permissions:</string>
<!-- TODO(b/256140614) To replace all glasses related strings with final versions -->
<!-- ================= DEVICE_PROFILE_GLASSES ================= -->
@@ -43,10 +43,10 @@
<string name="profile_name_glasses">glasses</string>
<!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile (type) [CHAR LIMIT=NONE] -->
- <string name="summary_glasses">This app is needed to manage your <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts, Microphone and Nearby devices permissions.</string>
+ <string name="summary_glasses">This app is needed to manage <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts, Microphone and Nearby devices permissions.</string>
<!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile for singleDevice(type) [CHAR LIMIT=NONE] -->
- <string name="summary_glasses_single_device">The app is needed to manage your <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to interact with these permissions:</string>
+ <string name="summary_glasses_single_device">The app is needed to manage <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to interact with these permissions:</string>
<!-- ================= DEVICE_PROFILE_APP_STREAMING ================= -->
@@ -99,7 +99,10 @@
<string name="profile_name_generic">device</string>
<!-- Description of the privileges the application will get if associated with the companion device of unspecified profile (type) [CHAR LIMIT=NONE] -->
- <string name="summary_generic"></string>
+ <string name="summary_generic_single_device">This app will be able to sync info, like the name of someone calling, between your phone and <xliff:g id="device_name" example="My Watch">%1$s</xliff:g>.</string>
+
+ <!-- Description of the privileges the application will get if associated with the companion device of unspecified profile (type) [CHAR LIMIT=NONE] -->
+ <string name="summary_generic">This app will be able to sync info, like the name of someone calling, between your phone and the chosen device.</string>
<!-- ================= Buttons ================= -->
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
index c5ed5c9..918f9c6 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
@@ -546,17 +546,16 @@
}
if (deviceProfile == null) {
- // Summary is not needed for null profile.
- mSummary.setVisibility(View.GONE);
+ summary = getHtmlFromResources(this, SUMMARIES.get(null), deviceName);
mConstraintList.setVisibility(View.GONE);
} else {
+ summary = getHtmlFromResources(this, SUMMARIES.get(deviceProfile),
+ getString(PROFILES_NAME.get(deviceProfile)), appLabel);
mPermissionTypes.addAll(PERMISSION_TYPES.get(deviceProfile));
setupPermissionList();
}
title = getHtmlFromResources(this, TITLES.get(deviceProfile), appLabel, deviceName);
- summary = getHtmlFromResources(this, SUMMARIES.get(deviceProfile),
- getString(PROFILES_NAME.get(deviceProfile)), appLabel);
profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile));
mTitle.setText(title);
@@ -586,7 +585,6 @@
if (deviceProfile == null) {
summary = getHtmlFromResources(this, summaryResourceId);
- mSummary.setVisibility(View.GONE);
} else {
summary = getHtmlFromResources(this, summaryResourceId, profileName, appLabel);
}
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java
index 6f5f4fe..e3fd354 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java
@@ -88,7 +88,7 @@
final Map<String, Integer> map = new ArrayMap<>();
map.put(DEVICE_PROFILE_WATCH, R.string.summary_watch_single_device);
map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses_single_device);
- map.put(null, R.string.summary_generic);
+ map.put(null, R.string.summary_generic_single_device);
SUMMARIES = unmodifiableMap(map);
}
diff --git a/packages/CredentialManager/Android.bp b/packages/CredentialManager/Android.bp
index 2cb3468..00d42bd 100644
--- a/packages/CredentialManager/Android.bp
+++ b/packages/CredentialManager/Android.bp
@@ -31,6 +31,7 @@
"androidx.compose.ui_ui",
"androidx.compose.ui_ui-tooling",
"androidx.core_core-ktx",
+ "androidx.credentials_credentials",
"androidx.lifecycle_lifecycle-extensions",
"androidx.lifecycle_lifecycle-livedata",
"androidx.lifecycle_lifecycle-runtime-ktx",
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index d6909719..49ac482 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -13,6 +13,10 @@
<string name="string_more_options">More options</string>
<!-- This is a label for a button that links to additional information about passkeys. [CHAR LIMIT=20] -->
<string name="string_learn_more">Learn more</string>
+ <!-- This is a label for content description for show password icon button. -->
+ <string name="content_description_show_password">Show password</string>
+ <!-- This is a label for content description for hide password icon button. -->
+ <string name="content_description_hide_password">Hide password</string>
<!-- This string introduces passkeys to the users for the first time they use this method. Tip: to avoid gendered language patterns, this header could be translated as if the original string were "More safety with passkeys". [CHAR LIMIT=200] -->
<string name="passkey_creation_intro_title">Safer with passkeys</string>
<!-- This string highlight passkey benefits related with the password. [CHAR LIMIT=200] -->
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index a48cd2b..6ea1d8d 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -16,15 +16,13 @@
package com.android.credentialmanager
-import android.app.PendingIntent
import android.app.slice.Slice
import android.app.slice.SliceSpec
import android.content.Context
import android.content.Intent
-import android.content.pm.SigningInfo
import android.credentials.CreateCredentialRequest
import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
-import android.credentials.GetCredentialOption
+import android.credentials.CredentialOption
import android.credentials.GetCredentialRequest
import android.credentials.ui.Constants
import android.credentials.ui.Entry
@@ -40,18 +38,15 @@
import android.os.Binder
import android.os.Bundle
import android.os.ResultReceiver
-import android.service.credentials.CredentialProviderService
import com.android.credentialmanager.createflow.DisabledProviderInfo
import com.android.credentialmanager.createflow.EnabledProviderInfo
import com.android.credentialmanager.createflow.RequestDisplayInfo
import com.android.credentialmanager.getflow.GetCredentialUiState
-import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest.Companion.toCredentialDataBundle
-import com.android.credentialmanager.jetpack.developer.CreatePublicKeyCredentialRequest
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
-import com.android.credentialmanager.jetpack.provider.Action
-import com.android.credentialmanager.jetpack.provider.CreateEntry
-import com.android.credentialmanager.jetpack.provider.CredentialCountInformation
-import com.android.credentialmanager.jetpack.provider.CredentialEntry
+import androidx.credentials.CreateCredentialRequest.DisplayInfo
+import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.CreatePasswordRequest
+
+import java.time.Instant
// Consider repo per screen, similar to view model?
class CredentialManagerRepo(
@@ -69,7 +64,7 @@
requestInfo = intent.extras?.getParcelable(
RequestInfo.EXTRA_REQUEST_INFO,
RequestInfo::class.java
- ) ?: testCreatePasskeyRequestInfo()
+ ) ?: testCreatePasswordRequestInfo()
providerEnabledList = when (requestInfo.type) {
RequestInfo.TYPE_CREATE ->
@@ -180,14 +175,14 @@
.Builder("io.enpass.app")
.setSaveEntries(
listOf<Entry>(
- newCreateEntry(
+ CreateTestUtils.newCreateEntry(context,
"key1", "subkey-1", "elisa.beckett@gmail.com",
- 20, 7, 27, 10L,
- "Optional footer description"
+ 20, 7, 27, Instant.ofEpochSecond(10L),
+ "Legal note"
),
- newCreateEntry(
+ CreateTestUtils.newCreateEntry(context,
"key1", "subkey-2", "elisa.work@google.com",
- 20, 7, 27, 12L,
+ 20, 7, 27, Instant.ofEpochSecond(12L),
null
),
)
@@ -200,14 +195,14 @@
.Builder("com.dashlane")
.setSaveEntries(
listOf<Entry>(
- newCreateEntry(
+ CreateTestUtils.newCreateEntry(context,
"key1", "subkey-3", "elisa.beckett@dashlane.com",
- 20, 7, 27, 11L,
+ 20, 7, 27, Instant.ofEpochSecond(11L),
null
),
- newCreateEntry(
+ CreateTestUtils.newCreateEntry(context,
"key1", "subkey-4", "elisa.work@dashlane.com",
- 20, 7, 27, 14L,
+ 20, 7, 27, Instant.ofEpochSecond(14L),
null
),
)
@@ -228,33 +223,33 @@
GetCredentialProviderData.Builder("io.enpass.app")
.setCredentialEntries(
listOf<Entry>(
- newGetEntry(
- "key1", "subkey-1", TYPE_PASSWORD_CREDENTIAL, "Password",
- "elisa.family@outlook.com", null, 3L
+ GetTestUtils.newPasswordEntry(
+ context, "key1", "subkey-1", "elisa.family@outlook.com", null,
+ Instant.ofEpochSecond(8000L)
),
- newGetEntry(
- "key1", "subkey-1", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey",
- "elisa.bakery@gmail.com", "Elisa Beckett", 0L
+ GetTestUtils.newPasskeyEntry(
+ context, "key1", "subkey-1", "elisa.bakery@gmail.com", "Elisa Beckett",
+ null
),
- newGetEntry(
- "key1", "subkey-2", TYPE_PASSWORD_CREDENTIAL, "Password",
- "elisa.bakery@gmail.com", null, 10L
+ GetTestUtils.newPasswordEntry(
+ context, "key1", "subkey-2", "elisa.bakery@gmail.com", null,
+ Instant.ofEpochSecond(10000L)
),
- newGetEntry(
- "key1", "subkey-3", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey",
- "elisa.family@outlook.com", "Elisa Beckett", 1L
+ GetTestUtils.newPasskeyEntry(
+ context, "key1", "subkey-3", "elisa.family@outlook.com",
+ "Elisa Beckett", Instant.ofEpochSecond(500L)
),
)
).setAuthenticationEntry(
- newAuthenticationEntry("key2", "subkey-1", TYPE_PASSWORD_CREDENTIAL)
+ GetTestUtils.newAuthenticationEntry(context, "key2", "subkey-1")
).setActionChips(
listOf(
- newActionEntry(
- "key3", "subkey-1", TYPE_PASSWORD_CREDENTIAL,
+ GetTestUtils.newActionEntry(
+ context, "key3", "subkey-1",
"Open Google Password Manager", "elisa.beckett@gmail.com"
),
- newActionEntry(
- "key3", "subkey-2", TYPE_PASSWORD_CREDENTIAL,
+ GetTestUtils.newActionEntry(
+ context, "key3", "subkey-2",
"Open Google Password Manager", "beckett-family@gmail.com"
),
)
@@ -264,137 +259,29 @@
GetCredentialProviderData.Builder("com.dashlane")
.setCredentialEntries(
listOf<Entry>(
- newGetEntry(
- "key1", "subkey-2", TYPE_PASSWORD_CREDENTIAL, "Password",
- "elisa.family@outlook.com", null, 4L
+ GetTestUtils.newPasswordEntry(
+ context, "key1", "subkey-2", "elisa.family@outlook.com", null,
+ Instant.ofEpochSecond(9000L)
),
- newGetEntry(
- "key1", "subkey-3", TYPE_PASSWORD_CREDENTIAL, "Password",
- "elisa.work@outlook.com", null, 11L
+ GetTestUtils.newPasswordEntry(
+ context, "key1", "subkey-3", "elisa.work@outlook.com", null,
+ Instant.ofEpochSecond(11000L)
),
)
).setAuthenticationEntry(
- newAuthenticationEntry("key2", "subkey-1", TYPE_PASSWORD_CREDENTIAL)
+ GetTestUtils.newAuthenticationEntry(context, "key2", "subkey-1")
).setActionChips(
listOf(
- newActionEntry(
- "key3", "subkey-1", TYPE_PASSWORD_CREDENTIAL,
- "Open Enpass"
+ GetTestUtils.newActionEntry(
+ context, "key3", "subkey-1", "Open Enpass",
+ "Manage passwords"
),
)
).build(),
)
}
- private fun newActionEntry(
- key: String,
- subkey: String,
- credentialType: String,
- text: String,
- subtext: String? = null,
- ): Entry {
- val action = Action(text, subtext, null)
- return Entry(
- key,
- subkey,
- Action.toSlice(action)
- )
- }
-
- private fun newAuthenticationEntry(
- key: String,
- subkey: String,
- credentialType: String,
- ): Entry {
- val slice = Slice.Builder(
- Uri.EMPTY, SliceSpec(credentialType, 1)
- )
- return Entry(
- key,
- subkey,
- slice.build()
- )
- }
-
- private fun newGetEntry(
- key: String,
- subkey: String,
- credentialType: String,
- credentialTypeDisplayName: String,
- userName: String,
- userDisplayName: String?,
- lastUsedTimeMillis: Long?,
- ): Entry {
- val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD")
- .setPackage("com.androidauth.androidvault")
- intent.putExtra("provider_extra_sample", "testprovider")
-
- val pendingIntent = PendingIntent.getActivity(
- context, 1,
- intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
- or PendingIntent.FLAG_ONE_SHOT)
- )
-
- val credentialEntry = CredentialEntry(
- credentialType, credentialTypeDisplayName, userName,
- userDisplayName, pendingIntent, lastUsedTimeMillis
- ?: 0L, null, false
- )
-
- return Entry(
- key,
- subkey,
- CredentialEntry.toSlice(credentialEntry),
- Intent()
- )
- }
-
- private fun newCreateEntry(
- key: String,
- subkey: String,
- providerDisplayName: String,
- passwordCount: Int,
- passkeyCount: Int,
- totalCredentialCount: Int,
- lastUsedTimeMillis: Long,
- footerDescription: String?,
- ): Entry {
- val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD")
- .setPackage("com.androidauth.androidvault")
- intent.putExtra("provider_extra_sample", "testprovider")
- val pendingIntent = PendingIntent.getActivity(
- context, 1,
- intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
- or PendingIntent.FLAG_ONE_SHOT)
- )
- val createPasswordRequest = android.service.credentials.CreateCredentialRequest(
- android.service.credentials.CallingAppInfo(
- context.applicationInfo.packageName, SigningInfo()
- ),
- TYPE_PASSWORD_CREDENTIAL,
- toCredentialDataBundle("beckett-bakert@gmail.com", "password123")
- )
- val fillInIntent = Intent().putExtra(
- CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST,
- createPasswordRequest
- )
-
- val createEntry = CreateEntry(
- providerDisplayName, pendingIntent,
- null, lastUsedTimeMillis,
- listOf(
- CredentialCountInformation.createPasswordCountInformation(passwordCount),
- CredentialCountInformation.createPublicKeyCountInformation(passkeyCount),
- ), footerDescription
- )
- return Entry(
- key,
- subkey,
- CreateEntry.toSlice(createEntry),
- fillInIntent
- )
- }
private fun newRemoteEntry(
key: String,
@@ -451,7 +338,7 @@
return RequestInfo.newCreateRequestInfo(
Binder(),
CreateCredentialRequest(
- TYPE_PUBLIC_KEY_CREDENTIAL,
+ "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL",
credentialData,
// TODO: populate with actual data
/*candidateQueryData=*/ Bundle(),
@@ -462,14 +349,13 @@
}
private fun testCreatePasswordRequestInfo(): RequestInfo {
- val data = toCredentialDataBundle("beckett-bakert@gmail.com", "password123")
+ val request = CreatePasswordRequest("beckett-bakert@gmail.com", "password123")
return RequestInfo.newCreateRequestInfo(
Binder(),
CreateCredentialRequest(
TYPE_PASSWORD_CREDENTIAL,
- data,
- // TODO: populate with actual data
- /*candidateQueryData=*/ Bundle(),
+ request.credentialData,
+ request.candidateQueryData,
/*isSystemProviderRequired=*/ false
),
"com.google.android.youtube"
@@ -478,6 +364,10 @@
private fun testCreateOtherCredentialRequestInfo(): RequestInfo {
val data = Bundle()
+ val displayInfo = DisplayInfo("my-username00", "Joe")
+ data.putBundle(
+ "androidx.credentials.BUNDLE_KEY_REQUEST_DISPLAY_INFO",
+ displayInfo.toBundle())
return RequestInfo.newCreateRequestInfo(
Binder(),
CreateCredentialRequest(
@@ -496,9 +386,9 @@
GetCredentialRequest.Builder(
Bundle()
)
- .addGetCredentialOption(
- GetCredentialOption(
- TYPE_PUBLIC_KEY_CREDENTIAL,
+ .addCredentialOption(
+ CredentialOption(
+ "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL",
Bundle(),
Bundle(), /*isSystemProviderRequired=*/
false
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index 3f705d6..15acd8c 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -16,18 +16,22 @@
package com.android.credentialmanager
+import android.app.slice.Slice
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
+import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
import android.credentials.ui.CreateCredentialProviderData
import android.credentials.ui.DisabledProviderData
import android.credentials.ui.Entry
import android.credentials.ui.GetCredentialProviderData
import android.credentials.ui.RequestInfo
import android.graphics.drawable.Drawable
+import android.service.credentials.CredentialEntry
import android.text.TextUtils
import android.util.Log
import com.android.credentialmanager.common.Constants
+import com.android.credentialmanager.common.CredentialType
import com.android.credentialmanager.createflow.ActiveEntry
import com.android.credentialmanager.createflow.CreateCredentialUiState
import com.android.credentialmanager.createflow.CreateOptionInfo
@@ -41,17 +45,22 @@
import com.android.credentialmanager.getflow.CredentialEntryInfo
import com.android.credentialmanager.getflow.ProviderInfo
import com.android.credentialmanager.getflow.RemoteEntryInfo
-import com.android.credentialmanager.jetpack.developer.CreateCredentialRequest
-import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest
-import com.android.credentialmanager.jetpack.developer.CreatePublicKeyCredentialRequest
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
-import com.android.credentialmanager.jetpack.provider.Action
-import com.android.credentialmanager.jetpack.provider.AuthenticationAction
-import com.android.credentialmanager.jetpack.provider.CreateEntry
-import com.android.credentialmanager.jetpack.provider.CredentialCountInformation
-import com.android.credentialmanager.jetpack.provider.CredentialEntry
+import androidx.credentials.CreateCredentialRequest
+import androidx.credentials.CreateCustomCredentialRequest
+import androidx.credentials.CreatePasswordRequest
+import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
+import androidx.credentials.provider.Action
+import androidx.credentials.provider.AuthenticationAction
+import androidx.credentials.provider.CreateEntry
+import androidx.credentials.provider.CustomCredentialEntry
+import androidx.credentials.provider.PasswordCredentialEntry
+import androidx.credentials.provider.PublicKeyCredentialEntry
+import androidx.credentials.provider.RemoteCreateEntry
+import androidx.credentials.provider.RemoteCredentialEntry
import org.json.JSONObject
+// TODO: remove all !! checks
private fun getAppLabel(
pm: PackageManager,
appPackageName: String
@@ -174,25 +183,76 @@
credentialEntries: List<Entry>,
context: Context,
): List<CredentialEntryInfo> {
- return credentialEntries.map {
- // TODO: handle NPE gracefully
- val credentialEntry = CredentialEntry.fromSlice(it.slice)!!
+ val result: MutableList<CredentialEntryInfo> = mutableListOf()
+ credentialEntries.forEach {
+ val credentialEntry = parseCredentialEntryFromSlice(it.slice)
+ when (credentialEntry) {
+ is PasswordCredentialEntry -> {
+ result.add(CredentialEntryInfo(
+ providerId = providerId,
+ entryKey = it.key,
+ entrySubkey = it.subkey,
+ pendingIntent = credentialEntry.pendingIntent,
+ fillInIntent = it.frameworkExtrasIntent,
+ credentialType = CredentialType.PASSWORD,
+ credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(),
+ userName = credentialEntry.username.toString(),
+ displayName = credentialEntry.displayName?.toString(),
+ icon = credentialEntry.icon?.loadDrawable(context),
+ lastUsedTimeMillis = credentialEntry.lastUsedTime,
+ ))
+ }
+ is PublicKeyCredentialEntry -> {
+ result.add(CredentialEntryInfo(
+ providerId = providerId,
+ entryKey = it.key,
+ entrySubkey = it.subkey,
+ pendingIntent = credentialEntry.pendingIntent,
+ fillInIntent = it.frameworkExtrasIntent,
+ credentialType = CredentialType.PASSKEY,
+ credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(),
+ userName = credentialEntry.username.toString(),
+ displayName = credentialEntry.displayName?.toString(),
+ icon = credentialEntry.icon?.loadDrawable(context),
+ lastUsedTimeMillis = credentialEntry.lastUsedTime,
+ ))
+ }
+ is CustomCredentialEntry -> {
+ result.add(CredentialEntryInfo(
+ providerId = providerId,
+ entryKey = it.key,
+ entrySubkey = it.subkey,
+ pendingIntent = credentialEntry.pendingIntent,
+ fillInIntent = it.frameworkExtrasIntent,
+ credentialType = CredentialType.UNKNOWN,
+ credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(),
+ userName = credentialEntry.title.toString(),
+ displayName = credentialEntry.subtitle?.toString(),
+ icon = credentialEntry.icon?.loadDrawable(context),
+ lastUsedTimeMillis = credentialEntry.lastUsedTime,
+ ))
+ }
+ else -> Log.d(
+ Constants.LOG_TAG,
+ "Encountered unrecognized credential entry ${it.slice.spec?.type}"
+ )
+ }
+ }
+ // TODO: handle empty list due to parsing error.
+ return result
+ }
- // Consider directly move the UI object into the class.
- return@map CredentialEntryInfo(
- providerId = providerId,
- entryKey = it.key,
- entrySubkey = it.subkey,
- pendingIntent = credentialEntry.pendingIntent,
- fillInIntent = it.frameworkExtrasIntent,
- credentialType = credentialEntry.type,
- credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(),
- userName = credentialEntry.username.toString(),
- displayName = credentialEntry.displayName?.toString(),
- // TODO: proper fallback
- icon = credentialEntry.icon?.loadDrawable(context),
- lastUsedTimeMillis = credentialEntry.lastUsedTimeMillis,
- )
+ private fun parseCredentialEntryFromSlice(slice: Slice): CredentialEntry? {
+ try {
+ when (slice.spec?.type) {
+ TYPE_PASSWORD_CREDENTIAL -> return PasswordCredentialEntry.fromSlice(slice)!!
+ TYPE_PUBLIC_KEY_CREDENTIAL -> return PublicKeyCredentialEntry.fromSlice(slice)!!
+ else -> return CustomCredentialEntry.fromSlice(slice)!!
+ }
+ } catch (e: Exception) {
+ // Try CustomCredentialEntry.fromSlice one last time in case the cause was a failed
+ // password / passkey parsing attempt.
+ return CustomCredentialEntry.fromSlice(slice)
}
}
@@ -205,18 +265,13 @@
if (authEntry == null) {
return null
}
- val authStructuredEntry = AuthenticationAction.fromSlice(
- authEntry!!.slice
- )
- if (authStructuredEntry == null) {
- return null
- }
-
+ val structuredAuthEntry =
+ AuthenticationAction.fromSlice(authEntry.slice) ?: return null
return AuthenticationEntryInfo(
providerId = providerId,
entryKey = authEntry.key,
entrySubkey = authEntry.subkey,
- pendingIntent = authStructuredEntry.pendingIntent,
+ pendingIntent = structuredAuthEntry.pendingIntent,
fillInIntent = authEntry.frameworkExtrasIntent,
title = providerDisplayName,
icon = providerIcon,
@@ -228,11 +283,13 @@
if (remoteEntry == null) {
return null
}
+ val structuredRemoteEntry = RemoteCredentialEntry.fromSlice(remoteEntry.slice)
+ ?: return null
return RemoteEntryInfo(
providerId = providerId,
entryKey = remoteEntry.key,
entrySubkey = remoteEntry.subkey,
- pendingIntent = remoteEntry.pendingIntent,
+ pendingIntent = structuredRemoteEntry.pendingIntent,
fillInIntent = remoteEntry.frameworkExtrasIntent,
)
}
@@ -242,22 +299,22 @@
actionEntries: List<Entry>,
providerIcon: Drawable,
): List<ActionEntryInfo> {
- return actionEntries.map {
- // TODO: handle NPE gracefully
- val actionEntryUi = Action.fromSlice(it.slice)!!
-
- return@map ActionEntryInfo(
+ val result: MutableList<ActionEntryInfo> = mutableListOf()
+ actionEntries.forEach {
+ val actionEntryUi = Action.fromSlice(it.slice) ?: return@forEach
+ result.add(ActionEntryInfo(
providerId = providerId,
entryKey = it.key,
entrySubkey = it.subkey,
pendingIntent = actionEntryUi.pendingIntent,
fillInIntent = it.frameworkExtrasIntent,
title = actionEntryUi.title.toString(),
- // TODO: gracefully fail
icon = providerIcon,
- subTitle = actionEntryUi.subTitle?.toString(),
- )
+ subTitle = actionEntryUi.subtitle?.toString(),
+ ))
}
+ // TODO: handle empty list
+ return result
}
}
}
@@ -316,22 +373,21 @@
): RequestDisplayInfo? {
val appLabel = getAppLabel(context.packageManager, requestInfo.appPackageName)
?: return null
- val createCredentialRequest = requestInfo.createCredentialRequest
- val createCredentialRequestJetpack = createCredentialRequest?.let {
- CreateCredentialRequest.createFrom(
- it.type, it.credentialData, it.candidateQueryData, it.isSystemProviderRequired
+ val createCredentialRequest = requestInfo.createCredentialRequest ?: return null
+ val createCredentialRequestJetpack = CreateCredentialRequest.createFrom(
+ createCredentialRequest.type,
+ createCredentialRequest.credentialData,
+ createCredentialRequest.candidateQueryData,
+ createCredentialRequest.isSystemProviderRequired
+ )
+ return when (createCredentialRequestJetpack) {
+ is CreatePasswordRequest -> RequestDisplayInfo(
+ createCredentialRequestJetpack.id,
+ createCredentialRequestJetpack.password,
+ CredentialType.PASSWORD,
+ appLabel,
+ context.getDrawable(R.drawable.ic_password)!!
)
- }
- when (createCredentialRequestJetpack) {
- is CreatePasswordRequest -> {
- return RequestDisplayInfo(
- createCredentialRequestJetpack.id,
- createCredentialRequestJetpack.password,
- createCredentialRequestJetpack.type,
- appLabel,
- context.getDrawable(R.drawable.ic_password)!!
- )
- }
is CreatePublicKeyCredentialRequest -> {
val requestJson = createCredentialRequestJetpack.requestJson
val json = JSONObject(requestJson)
@@ -342,24 +398,29 @@
name = user.getString("name")
displayName = user.getString("displayName")
}
- return RequestDisplayInfo(
+ RequestDisplayInfo(
name,
displayName,
- createCredentialRequestJetpack.type,
+ CredentialType.PASSKEY,
appLabel,
context.getDrawable(R.drawable.ic_passkey)!!
)
}
- // TODO: correctly parsing for other sign-ins
- else -> {
- return RequestDisplayInfo(
- "beckett-bakert@gmail.com",
- "Elisa Beckett",
- "other-sign-ins",
- appLabel.toString(),
- context.getDrawable(R.drawable.ic_other_sign_in)!!
+ is CreateCustomCredentialRequest -> {
+ // TODO: directly use the display info once made public
+ val displayInfo = CreateCredentialRequest.DisplayInfo
+ .parseFromCredentialDataBundle(createCredentialRequest.credentialData)
+ ?: return null
+ RequestDisplayInfo(
+ title = displayInfo.userId,
+ subtitle = displayInfo.userDisplayName,
+ type = CredentialType.UNKNOWN,
+ appName = appLabel,
+ typeIcon = displayInfo.credentialTypeIcon?.loadDrawable(context)
+ ?: context.getDrawable(R.drawable.ic_other_sign_in)!!
)
}
+ else -> null
}
}
@@ -408,7 +469,7 @@
currentScreenState = initialScreenState,
requestDisplayInfo = requestDisplayInfo,
sortedCreateOptionsPairs = createOptionsPairs.sortedWith(
- compareByDescending { it.first.lastUsedTimeMillis }
+ compareByDescending { it.first.lastUsedTime }
),
hasDefaultProvider = defaultProvider != null,
activeEntry = toActiveEntry(
@@ -430,7 +491,7 @@
isPasskeyFirstUse: Boolean,
): CreateScreenState? {
return if (isPasskeyFirstUse && requestDisplayInfo.type ==
- TYPE_PUBLIC_KEY_CREDENTIAL && !isOnPasskeyIntroStateAlready) {
+ CredentialType.PASSKEY && !isOnPasskeyIntroStateAlready) {
CreateScreenState.PASSKEY_INTRO
} else if ((defaultProvider == null || defaultProvider.createOptions.isEmpty()) &&
createOptionSize > 1) {
@@ -479,12 +540,10 @@
creationEntries: List<Entry>,
context: Context,
): List<CreateOptionInfo> {
- return creationEntries.map {
- // TODO: handle NPE gracefully
- val createEntry = CreateEntry.fromSlice(it.slice)!!
-
- return@map CreateOptionInfo(
- // TODO: remove fallbacks
+ val result: MutableList<CreateOptionInfo> = mutableListOf()
+ creationEntries.forEach {
+ val createEntry = CreateEntry.fromSlice(it.slice) ?: return@forEach
+ result.add(CreateOptionInfo(
providerId = providerId,
entryKey = it.key,
entrySubkey = it.subkey,
@@ -492,32 +551,28 @@
fillInIntent = it.frameworkExtrasIntent,
userProviderDisplayName = createEntry.accountName.toString(),
profileIcon = createEntry.icon?.loadDrawable(context),
- passwordCount = CredentialCountInformation.getPasswordCount(
- createEntry.credentialCountInformationList
- ) ?: 0,
- passkeyCount = CredentialCountInformation.getPasskeyCount(
- createEntry.credentialCountInformationList
- ) ?: 0,
- totalCredentialCount = CredentialCountInformation.getTotalCount(
- createEntry.credentialCountInformationList
- ) ?: 0,
- lastUsedTimeMillis = createEntry.lastUsedTimeMillis ?: 0,
- footerDescription = createEntry.footerDescription?.toString()
- )
+ passwordCount = createEntry.getPasswordCredentialCount(),
+ passkeyCount = createEntry.getPublicKeyCredentialCount(),
+ totalCredentialCount = createEntry.getTotalCredentialCount(),
+ lastUsedTime = createEntry.lastUsedTime,
+ footerDescription = createEntry.description?.toString()
+ ))
}
+ return result
}
private fun toRemoteInfo(
providerId: String,
remoteEntry: Entry?,
): RemoteInfo? {
- // TODO: should also call fromSlice after getting the official jetpack code.
return if (remoteEntry != null) {
+ val structuredRemoteEntry = RemoteCreateEntry.fromSlice(remoteEntry.slice)
+ ?: return null
RemoteInfo(
providerId = providerId,
entryKey = remoteEntry.key,
entrySubkey = remoteEntry.subkey,
- pendingIntent = remoteEntry.pendingIntent,
+ pendingIntent = structuredRemoteEntry.pendingIntent,
fillInIntent = remoteEntry.frameworkExtrasIntent,
)
} else null
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt b/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt
new file mode 100644
index 0000000..e3bbaeb
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.credentialmanager
+
+import android.app.PendingIntent
+import android.app.slice.Slice
+import android.app.slice.SliceSpec
+import android.content.Context
+import android.content.Intent
+import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
+import android.credentials.ui.Entry
+import android.graphics.drawable.Icon
+import android.net.Uri
+import android.provider.Settings
+import androidx.credentials.provider.CreateEntry
+
+import java.time.Instant
+
+// TODO: remove once testing is complete
+class GetTestUtils {
+ companion object {
+ internal fun newAuthenticationEntry(
+ context: Context,
+ key: String,
+ subkey: String,
+ ): Entry {
+ val slice = Slice.Builder(
+ Uri.EMPTY, SliceSpec("AuthenticationAction", 0)
+ )
+ val intent = Intent(Settings.ACTION_SYNC_SETTINGS)
+ val pendingIntent =
+ PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+ slice.addAction(
+ pendingIntent,
+ Slice.Builder(slice)
+ .addHints(listOf("androidx.credentials.provider.authenticationAction" +
+ ".SLICE_HINT_PENDING_INTENT"))
+ .build(),
+ /*subType=*/null
+ )
+ return Entry(
+ key,
+ subkey,
+ slice.build()
+ )
+ }
+
+ internal fun newActionEntry(
+ context: Context,
+ key: String,
+ subkey: String,
+ text: String,
+ subtext: String? = null,
+ ): Entry {
+ val intent = Intent(Settings.ACTION_SYNC_SETTINGS)
+ val pendingIntent =
+ PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+ val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("Action", 0))
+ .addText(
+ text, /*subType=*/null,
+ listOf("androidx.credentials.provider.action.HINT_ACTION_TITLE")
+ )
+ .addText(
+ subtext, /*subType=*/null,
+ listOf("androidx.credentials.provider.action.HINT_ACTION_SUBTEXT")
+ )
+ sliceBuilder.addAction(
+ pendingIntent,
+ Slice.Builder(sliceBuilder)
+ .addHints(
+ listOf("androidx.credentials.provider.action.SLICE_HINT_PENDING_INTENT")
+ )
+ .build(),
+ /*subType=*/null
+ )
+ return Entry(
+ key,
+ subkey,
+ sliceBuilder.build()
+ )
+ }
+
+ private const val SLICE_HINT_TYPE_DISPLAY_NAME =
+ "androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
+ private const val SLICE_HINT_TITLE =
+ "androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_USER_NAME"
+ private const val SLICE_HINT_SUBTITLE =
+ "androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
+ private const val SLICE_HINT_LAST_USED_TIME_MILLIS =
+ "androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
+ private const val SLICE_HINT_ICON =
+ "androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_PROFILE_ICON"
+ private const val SLICE_HINT_PENDING_INTENT =
+ "androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_PENDING_INTENT"
+ private const val SLICE_HINT_AUTO_ALLOWED =
+ "androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_AUTO_ALLOWED"
+ private const val AUTO_SELECT_TRUE_STRING = "true"
+ private const val AUTO_SELECT_FALSE_STRING = "false"
+
+ internal fun newPasswordEntry(
+ context: Context,
+ key: String,
+ subkey: String,
+ userName: String,
+ userDisplayName: String?,
+ lastUsedTime: Instant?,
+ ): Entry {
+ val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD")
+ .setPackage("com.androidauth.androidvault")
+ intent.putExtra("provider_extra_sample", "testprovider")
+ val pendingIntent = PendingIntent.getActivity(
+ context, 1,
+ intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ or PendingIntent.FLAG_ONE_SHOT)
+ )
+ return Entry(
+ key,
+ subkey,
+ toPasswordSlice(userName, userDisplayName, pendingIntent, lastUsedTime),
+ Intent()
+ )
+ }
+
+ private fun toPasswordSlice(
+ title: CharSequence,
+ subTitle: CharSequence?,
+ pendingIntent: PendingIntent,
+ lastUsedTime: Instant?,
+ icon: Icon? = null,
+ isAutoSelectAllowed: Boolean = true
+ ): Slice {
+ val type = TYPE_PASSWORD_CREDENTIAL
+ val autoSelectAllowed = if (isAutoSelectAllowed) {
+ AUTO_SELECT_TRUE_STRING
+ } else {
+ AUTO_SELECT_FALSE_STRING
+ }
+ val sliceBuilder = Slice.Builder(
+ Uri.EMPTY, SliceSpec(
+ type, 1
+ )
+ )
+ .addText(
+ "Password", /*subType=*/null,
+ listOf(SLICE_HINT_TYPE_DISPLAY_NAME)
+ )
+ .addText(
+ title, /*subType=*/null,
+ listOf(SLICE_HINT_TITLE)
+ )
+ .addText(
+ subTitle, /*subType=*/null,
+ listOf(SLICE_HINT_SUBTITLE)
+ )
+ .addText(
+ autoSelectAllowed, /*subType=*/null,
+ listOf(SLICE_HINT_AUTO_ALLOWED)
+ )
+ if (lastUsedTime != null) {
+ sliceBuilder.addLong(
+ lastUsedTime.toEpochMilli(),
+ /*subType=*/null,
+ listOf(SLICE_HINT_LAST_USED_TIME_MILLIS)
+ )
+ }
+ if (icon != null) {
+ sliceBuilder.addIcon(
+ icon, /*subType=*/null,
+ listOf(SLICE_HINT_ICON)
+ )
+ }
+ sliceBuilder.addAction(
+ pendingIntent,
+ Slice.Builder(sliceBuilder)
+ .addHints(listOf(SLICE_HINT_PENDING_INTENT))
+ .build(),
+ /*subType=*/null
+ )
+ return sliceBuilder.build()
+ }
+
+
+ private const val PASSKEY_SLICE_HINT_TYPE_DISPLAY_NAME =
+ "androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
+ private const val PASSKEY_SLICE_HINT_TITLE =
+ "androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_USER_NAME"
+ private const val PASSKEY_SLICE_HINT_SUBTITLE =
+ "androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
+ private const val PASSKEY_SLICE_HINT_LAST_USED_TIME_MILLIS =
+ "androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
+ private const val PASSKEY_SLICE_HINT_ICON =
+ "androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_PROFILE_ICON"
+ private const val PASSKEY_SLICE_HINT_PENDING_INTENT =
+ "androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_PENDING_INTENT"
+ private const val PASSKEY_SLICE_HINT_AUTO_ALLOWED =
+ "androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_AUTO_ALLOWED"
+
+ internal fun newPasskeyEntry(
+ context: Context,
+ key: String,
+ subkey: String,
+ userName: String,
+ userDisplayName: String?,
+ lastUsedTime: Instant?,
+ ): Entry {
+ val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD")
+ .setPackage("com.androidauth.androidvault")
+ intent.putExtra("provider_extra_sample", "testprovider")
+ val pendingIntent = PendingIntent.getActivity(
+ context, 1,
+ intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ or PendingIntent.FLAG_ONE_SHOT)
+ )
+ return Entry(
+ key, subkey, toPasskeySlice(
+ userName, userDisplayName, pendingIntent, lastUsedTime
+ ),
+ Intent()
+ )
+ }
+
+ private fun toPasskeySlice(
+ title: CharSequence,
+ subTitle: CharSequence?,
+ pendingIntent: PendingIntent,
+ lastUsedTime: Instant?,
+ icon: Icon? = null,
+ isAutoSelectAllowed: Boolean = true
+ ): Slice {
+ val type = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL"
+ val autoSelectAllowed = if (isAutoSelectAllowed) {
+ AUTO_SELECT_TRUE_STRING
+ } else {
+ AUTO_SELECT_FALSE_STRING
+ }
+ val sliceBuilder = Slice.Builder(
+ Uri.EMPTY, SliceSpec(
+ type, 1
+ )
+ )
+ .addText(
+ "Passkey", /*subType=*/null,
+ listOf(PASSKEY_SLICE_HINT_TYPE_DISPLAY_NAME)
+ )
+ .addText(
+ title, /*subType=*/null,
+ listOf(PASSKEY_SLICE_HINT_TITLE)
+ )
+ .addText(
+ subTitle, /*subType=*/null,
+ listOf(PASSKEY_SLICE_HINT_SUBTITLE)
+ )
+ .addText(
+ autoSelectAllowed, /*subType=*/null,
+ listOf(PASSKEY_SLICE_HINT_AUTO_ALLOWED)
+ )
+ if (lastUsedTime != null) {
+ sliceBuilder.addLong(
+ lastUsedTime.toEpochMilli(),
+ /*subType=*/null,
+ listOf(PASSKEY_SLICE_HINT_LAST_USED_TIME_MILLIS)
+ )
+ }
+ if (icon != null) {
+ sliceBuilder.addIcon(
+ icon, /*subType=*/null,
+ listOf(PASSKEY_SLICE_HINT_ICON)
+ )
+ }
+ sliceBuilder.addAction(
+ pendingIntent,
+ Slice.Builder(sliceBuilder)
+ .addHints(listOf(PASSKEY_SLICE_HINT_PENDING_INTENT))
+ .build(),
+ /*subType=*/null
+ )
+ return sliceBuilder.build()
+ }
+ }
+}
+
+class CreateTestUtils {
+ companion object {
+ private const val TYPE_TOTAL_CREDENTIAL = "TOTAL_CREDENTIAL_COUNT_TYPE"
+ private const val SLICE_HINT_ACCOUNT_NAME =
+ "androidx.credentials.provider.createEntry.SLICE_HINT_USER_PROVIDER_ACCOUNT_NAME"
+ private const val SLICE_HINT_NOTE =
+ "androidx.credentials.provider.createEntry.SLICE_HINT_NOTE"
+ private const val SLICE_HINT_ICON =
+ "androidx.credentials.provider.createEntry.SLICE_HINT_PROFILE_ICON"
+ private const val SLICE_HINT_CREDENTIAL_COUNT_INFORMATION =
+ "androidx.credentials.provider.createEntry.SLICE_HINT_CREDENTIAL_COUNT_INFORMATION"
+ private const val SLICE_HINT_LAST_USED_TIME_MILLIS =
+ "androidx.credentials.provider.createEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
+ private const val SLICE_HINT_PENDING_INTENT =
+ "androidx.credentials.provider.createEntry.SLICE_HINT_PENDING_INTENT"
+
+ internal fun newCreateEntry(
+ context: Context,
+ key: String,
+ subkey: String,
+ providerUserDisplayName: String,
+ passwordCount: Int?,
+ passkeyCount: Int?,
+ totalCredentialCount: Int?,
+ lastUsedTime: Instant?,
+ footerDescription: String?,
+ ): Entry {
+ val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD")
+ .setPackage("com.androidauth.androidvault")
+ intent.putExtra("provider_extra_sample", "testprovider")
+ val pendingIntent = PendingIntent.getActivity(
+ context, 1,
+ intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ or PendingIntent.FLAG_ONE_SHOT)
+ )
+ val credCountMap = mutableMapOf<String, Int>()
+ passwordCount?.let { credCountMap.put(TYPE_PASSWORD_CREDENTIAL, it) }
+ passkeyCount?.let {
+ credCountMap.put("androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL", it)
+ }
+ totalCredentialCount?.let { credCountMap.put(TYPE_TOTAL_CREDENTIAL, it) }
+ return Entry(
+ key,
+ subkey,
+ CreateEntry.toSlice(
+ providerUserDisplayName,
+ null,
+ footerDescription,
+ lastUsedTime,
+ credCountMap,
+ pendingIntent
+ ),
+ Intent()
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/CredentialType.kt
similarity index 64%
copy from packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
copy to packages/CredentialManager/src/com/android/credentialmanager/common/CredentialType.kt
index 497c272..cc92f60 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/CredentialType.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,12 +14,8 @@
* limitations under the License.
*/
-package com.android.credentialmanager.jetpack.developer
+package com.android.credentialmanager.common
-/**
- * Internal exception used to indicate a parsing error while converting from a framework type to
- * a jetpack type.
- *
- * @hide
- */
-internal class FrameworkClassParsingException : Exception()
\ No newline at end of file
+enum class CredentialType {
+ UNKNOWN, PASSKEY, PASSWORD,
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ActionButton.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ActionButton.kt
index d0271ab..984057a 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ActionButton.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ActionButton.kt
@@ -16,11 +16,23 @@
package com.android.credentialmanager.common.ui
+import com.android.credentialmanager.R
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Visibility
+import androidx.compose.material.icons.outlined.VisibilityOff
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
@Composable
fun ActionButton(text: String, onClick: () -> Unit) {
@@ -32,4 +44,27 @@
) {
Text(text = text)
}
+}
+
+@Composable
+fun ToggleVisibilityButton(modifier: Modifier = Modifier, onToggle: (Boolean) -> Unit) {
+ // default state is visibility off
+ val toggleState: MutableState<Boolean> = remember { mutableStateOf(false) }
+
+ IconButton(
+ modifier = modifier,
+ onClick = {
+ toggleState.value = !toggleState.value
+ onToggle(toggleState.value)
+ }
+ ) {
+ Icon(
+ imageVector = if (toggleState.value)
+ Icons.Outlined.Visibility else Icons.Outlined.VisibilityOff,
+ contentDescription = if (toggleState.value)
+ stringResource(R.string.content_description_show_password) else
+ stringResource(R.string.content_description_hide_password),
+ tint = LocalAndroidColorScheme.current.colorAccentPrimaryVariant
+ )
+ }
}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
index 5e432b9..0b9e578 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
@@ -2,7 +2,6 @@
package com.android.credentialmanager.createflow
-import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.result.ActivityResult
import androidx.activity.result.IntentSenderRequest
@@ -11,6 +10,8 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@@ -27,16 +28,22 @@
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
import com.android.credentialmanager.R
+import com.android.credentialmanager.common.CredentialType
import com.android.credentialmanager.common.ProviderActivityState
import com.android.credentialmanager.common.material.ModalBottomSheetLayout
import com.android.credentialmanager.common.material.ModalBottomSheetValue
@@ -48,9 +55,9 @@
import com.android.credentialmanager.common.ui.TextSecondary
import com.android.credentialmanager.common.ui.TextOnSurfaceVariant
import com.android.credentialmanager.common.ui.ContainerCard
+import com.android.credentialmanager.common.ui.ToggleVisibilityButton
import com.android.credentialmanager.ui.theme.EntryShape
import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -288,11 +295,11 @@
text = stringResource(
R.string.choose_provider_title,
when (requestDisplayInfo.type) {
- TYPE_PUBLIC_KEY_CREDENTIAL ->
+ CredentialType.PASSKEY ->
stringResource(R.string.passkeys)
- TYPE_PASSWORD_CREDENTIAL ->
+ CredentialType.PASSWORD ->
stringResource(R.string.passwords)
- else -> stringResource(R.string.sign_in_info)
+ CredentialType.UNKNOWN -> stringResource(R.string.sign_in_info)
}),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(horizontal = 24.dp)
@@ -398,11 +405,11 @@
stringResource(
R.string.save_credential_to_title,
when (requestDisplayInfo.type) {
- TYPE_PUBLIC_KEY_CREDENTIAL ->
+ CredentialType.PASSKEY ->
stringResource(R.string.passkey)
- TYPE_PASSWORD_CREDENTIAL ->
+ CredentialType.PASSWORD ->
stringResource(R.string.password)
- else -> stringResource(R.string.sign_in_info)
+ CredentialType.UNKNOWN -> stringResource(R.string.sign_in_info)
}),
style = MaterialTheme.typography.titleMedium,
)
@@ -571,15 +578,15 @@
)
TextOnSurface(
text = when (requestDisplayInfo.type) {
- TYPE_PUBLIC_KEY_CREDENTIAL -> stringResource(
+ CredentialType.PASSKEY -> stringResource(
R.string.choose_create_option_passkey_title,
requestDisplayInfo.appName
)
- TYPE_PASSWORD_CREDENTIAL -> stringResource(
+ CredentialType.PASSWORD -> stringResource(
R.string.choose_create_option_password_title,
requestDisplayInfo.appName
)
- else -> stringResource(
+ CredentialType.UNKNOWN -> stringResource(
R.string.choose_create_option_sign_in_title,
requestDisplayInfo.appName
)
@@ -828,7 +835,7 @@
Column() {
// TODO: Add the function to hide/view password when the type is create password
when (requestDisplayInfo.type) {
- TYPE_PUBLIC_KEY_CREDENTIAL -> {
+ CredentialType.PASSKEY -> {
TextOnSurfaceVariant(
text = requestDisplayInfo.title,
style = MaterialTheme.typography.titleLarge,
@@ -846,20 +853,46 @@
modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
)
}
- TYPE_PASSWORD_CREDENTIAL -> {
+ CredentialType.PASSWORD -> {
TextOnSurfaceVariant(
text = requestDisplayInfo.title,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(top = 16.dp, start = 5.dp),
)
- TextSecondary(
+ Row(modifier = Modifier.fillMaxWidth().padding(top = 4.dp, bottom = 16.dp,
+ start = 5.dp),
+ verticalAlignment = Alignment.CenterVertically) {
+ val visualTransformation = remember { PasswordVisualTransformation() }
// This subtitle would never be null for create password
- text = requestDisplayInfo.subtitle ?: "",
- style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
- )
+ val originalPassword by remember {
+ mutableStateOf(requestDisplayInfo.subtitle ?: "")
+ }
+ val displayedPassword = remember {
+ mutableStateOf(
+ visualTransformation.filter(
+ AnnotatedString(originalPassword)
+ ).text.text
+ )
+ }
+ TextSecondary(
+ text = displayedPassword.value,
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(top = 4.dp, bottom = 4.dp),
+ )
+
+ ToggleVisibilityButton(modifier = Modifier.padding(start = 4.dp)
+ .height(24.dp).width(24.dp), onToggle = {
+ if (it) {
+ displayedPassword.value = originalPassword
+ } else {
+ displayedPassword.value = visualTransformation.filter(
+ AnnotatedString(originalPassword)
+ ).text.text
+ }
+ })
+ }
}
- else -> {
+ CredentialType.UNKNOWN -> {
if (requestDisplayInfo.subtitle != null) {
TextOnSurfaceVariant(
text = requestDisplayInfo.title,
@@ -917,8 +950,8 @@
modifier = Modifier.padding(start = 5.dp),
)
}
- if (requestDisplayInfo.type == TYPE_PUBLIC_KEY_CREDENTIAL ||
- requestDisplayInfo.type == TYPE_PASSWORD_CREDENTIAL) {
+ if (requestDisplayInfo.type == CredentialType.PASSKEY ||
+ requestDisplayInfo.type == CredentialType.PASSWORD) {
if (createOptionInfo.passwordCount != null &&
createOptionInfo.passkeyCount != null
) {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
index 12a5085..05be0a6 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
@@ -21,6 +21,8 @@
import android.graphics.drawable.Drawable
import com.android.credentialmanager.common.DialogState
import com.android.credentialmanager.common.ProviderActivityState
+import com.android.credentialmanager.common.CredentialType
+import java.time.Instant
data class CreateCredentialUiState(
val enabledProviders: List<EnabledProviderInfo>,
@@ -68,18 +70,18 @@
)
class CreateOptionInfo(
- providerId: String,
- entryKey: String,
- entrySubkey: String,
- pendingIntent: PendingIntent?,
- fillInIntent: Intent?,
- val userProviderDisplayName: String?,
- val profileIcon: Drawable?,
- val passwordCount: Int?,
- val passkeyCount: Int?,
- val totalCredentialCount: Int?,
- val lastUsedTimeMillis: Long?,
- val footerDescription: String?,
+ providerId: String,
+ entryKey: String,
+ entrySubkey: String,
+ pendingIntent: PendingIntent?,
+ fillInIntent: Intent?,
+ val userProviderDisplayName: String?,
+ val profileIcon: Drawable?,
+ val passwordCount: Int?,
+ val passkeyCount: Int?,
+ val totalCredentialCount: Int?,
+ val lastUsedTime: Instant?,
+ val footerDescription: String?,
) : EntryInfo(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent)
class RemoteInfo(
@@ -93,7 +95,7 @@
data class RequestDisplayInfo(
val title: String,
val subtitle: String?,
- val type: String,
+ val type: CredentialType,
val appName: String,
val typeIcon: Drawable,
)
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index b30d1ec..a5e19b6 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -16,7 +16,6 @@
package com.android.credentialmanager.getflow
-import android.credentials.Credential
import android.text.TextUtils
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.result.ActivityResult
@@ -59,6 +58,7 @@
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
import com.android.credentialmanager.R
+import com.android.credentialmanager.common.CredentialType
import com.android.credentialmanager.common.ProviderActivityState
import com.android.credentialmanager.common.material.ModalBottomSheetLayout
import com.android.credentialmanager.common.material.ModalBottomSheetValue
@@ -71,7 +71,6 @@
import com.android.credentialmanager.common.ui.TextOnSurfaceVariant
import com.android.credentialmanager.common.ui.ContainerCard
import com.android.credentialmanager.common.ui.TransparentBackgroundEntry
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential
import com.android.credentialmanager.ui.theme.EntryShape
import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
@@ -171,7 +170,7 @@
) {
if (sortedUserNameToCredentialEntryList.first()
.sortedCredentialEntryList.first().credentialType
- == PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL
+ == CredentialType.PASSKEY
) R.string.get_dialog_title_use_passkey_for
else R.string.get_dialog_title_use_sign_in_for
} else if (
@@ -534,7 +533,7 @@
)
TextSecondary(
text = if (
- credentialEntryInfo.credentialType == Credential.TYPE_PASSWORD_CREDENTIAL) {
+ credentialEntryInfo.credentialType == CredentialType.PASSWORD) {
"••••••••••••"
} else {
if (TextUtils.isEmpty(credentialEntryInfo.displayName))
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
index 7d2f0da..5741f36 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
@@ -26,11 +26,11 @@
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.android.credentialmanager.CredentialManagerRepo
+import com.android.credentialmanager.common.CredentialType
import com.android.credentialmanager.common.Constants
import com.android.credentialmanager.common.DialogState
import com.android.credentialmanager.common.ProviderActivityResult
import com.android.credentialmanager.common.ProviderActivityState
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential
import com.android.internal.util.Preconditions
data class GetCredentialUiState(
@@ -258,9 +258,9 @@
override fun compare(p0: CredentialEntryInfo, p1: CredentialEntryInfo): Int {
// First prefer passkey type for its security benefits
if (p0.credentialType != p1.credentialType) {
- if (PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL == p0.credentialType) {
+ if (CredentialType.PASSKEY == p0.credentialType) {
return -1
- } else if (PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL == p1.credentialType) {
+ } else if (CredentialType.PASSKEY == p1.credentialType) {
return 1
}
}
@@ -272,9 +272,9 @@
} else if (p0.lastUsedTimeMillis > p1.lastUsedTimeMillis) {
return -1
}
- } else if (p0.lastUsedTimeMillis != null && p0.lastUsedTimeMillis > 0) {
+ } else if (p0.lastUsedTimeMillis != null) {
return -1
- } else if (p1.lastUsedTimeMillis != null && p1.lastUsedTimeMillis > 0) {
+ } else if (p1.lastUsedTimeMillis != null) {
return 1
}
return 0
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
index 8ce31bd..4c05dea 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
@@ -19,6 +19,9 @@
import android.app.PendingIntent
import android.content.Intent
import android.graphics.drawable.Drawable
+import com.android.credentialmanager.common.CredentialType
+
+import java.time.Instant
data class ProviderInfo(
/**
@@ -62,13 +65,13 @@
pendingIntent: PendingIntent?,
fillInIntent: Intent?,
/** Type of this credential used for sorting. Not localized so must not be directly displayed. */
- val credentialType: String,
+ val credentialType: CredentialType,
/** Localized type value of this credential used for display purpose. */
val credentialTypeDisplayName: String,
val userName: String,
val displayName: String?,
val icon: Drawable?,
- val lastUsedTimeMillis: Long?,
+ val lastUsedTimeMillis: Instant?,
) : EntryInfo(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent)
class AuthenticationEntryInfo(
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCredentialRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCredentialRequest.kt
deleted file mode 100644
index eaa2ad4..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCredentialRequest.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.credentialmanager.jetpack.developer
-
-import android.credentials.Credential
-import android.os.Bundle
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.BUNDLE_KEY_SUBTYPE
-
-/**
- * Base request class for registering a credential.
- *
- * @property type the credential type
- * @property data the request data in the [Bundle] format
- * @property requireSystemProvider true if must only be fulfilled by a system provider and false
- * otherwise
- */
-open class CreateCredentialRequest(
- open val type: String,
- open val credentialData: Bundle,
- open val candidateQueryData: Bundle,
- open val requireSystemProvider: Boolean
-) {
- companion object {
- @JvmStatic
- fun createFrom(
- type: String,
- credentialData: Bundle,
- candidateQueryData: Bundle,
- requireSystemProvider: Boolean
- ): CreateCredentialRequest {
- return try {
- when (type) {
- Credential.TYPE_PASSWORD_CREDENTIAL ->
- CreatePasswordRequest.createFrom(credentialData)
- PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
- when (credentialData.getString(BUNDLE_KEY_SUBTYPE)) {
- CreatePublicKeyCredentialRequest
- .BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST ->
- CreatePublicKeyCredentialRequest.createFrom(credentialData)
- CreatePublicKeyCredentialRequestPrivileged
- .BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV ->
- CreatePublicKeyCredentialRequestPrivileged
- .createFrom(credentialData)
- else -> throw FrameworkClassParsingException()
- }
- else -> throw FrameworkClassParsingException()
- }
- } catch (e: FrameworkClassParsingException) {
- // Parsing failed but don't crash the process. Instead just output a request with
- // the raw framework values.
- CreateCustomCredentialRequest(
- type,
- credentialData,
- candidateQueryData,
- requireSystemProvider
- )
- }
- }
- }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCustomCredentialRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCustomCredentialRequest.kt
deleted file mode 100644
index 50da9a1..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCustomCredentialRequest.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.developer
-
-import android.os.Bundle
-
-/**
- * Base custom create request class for registering a credential.
- *
- * An application can construct a subtype custom request and call
- * [CredentialManager.executeCreateCredential] to launch framework UI flows to collect consent and
- * any other metadata needed from the user to register a new user credential.
- *
- * @property type the credential type determined by the credential-type-specific subclass for custom
- * use cases
- * @property credentialData the full credential creation request data in the [Bundle] format for
- * custom use cases
- * @property candidateQueryData the partial request data in the [Bundle] format that will be sent to
- * the provider during the initial candidate query stage, which should not contain sensitive user
- * credential information
- * @property requireSystemProvider true if must only be fulfilled by a system provider and false
- * otherwise
- * @throws IllegalArgumentException If [type] is empty
- * @throws NullPointerException If [type] or [credentialData] are null
- */
-open class CreateCustomCredentialRequest(
- final override val type: String,
- final override val credentialData: Bundle,
- final override val candidateQueryData: Bundle,
- @get:JvmName("requireSystemProvider")
- final override val requireSystemProvider: Boolean
-) : CreateCredentialRequest(type, credentialData, candidateQueryData, requireSystemProvider) {
- init {
- require(type.isNotEmpty()) { "type should not be empty" }
- }
-}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePasswordRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePasswordRequest.kt
deleted file mode 100644
index bf0aa8a..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePasswordRequest.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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.credentialmanager.jetpack.developer
-
-import android.credentials.Credential
-import android.os.Bundle
-
-/**
- * A request to save the user password credential with their password provider.
- *
- * @property id the user id associated with the password
- * @property password the password
- * @throws NullPointerException If [id] is null
- * @throws NullPointerException If [password] is null
- * @throws IllegalArgumentException If [password] is empty
- */
-class CreatePasswordRequest constructor(
- val id: String,
- val password: String,
-) : CreateCredentialRequest(
- type = Credential.TYPE_PASSWORD_CREDENTIAL,
- credentialData = toCredentialDataBundle(id, password),
- // No credential data should be sent during the query phase.
- candidateQueryData = Bundle(),
- requireSystemProvider = false,
-) {
-
- init {
- require(password.isNotEmpty()) { "password should not be empty" }
- }
-
- companion object {
- const val BUNDLE_KEY_ID = "androidx.credentials.BUNDLE_KEY_ID"
- const val BUNDLE_KEY_PASSWORD = "androidx.credentials.BUNDLE_KEY_PASSWORD"
-
- @JvmStatic
- internal fun toCredentialDataBundle(id: String, password: String): Bundle {
- val bundle = Bundle()
- bundle.putString(BUNDLE_KEY_ID, id)
- bundle.putString(BUNDLE_KEY_PASSWORD, password)
- return bundle
- }
-
- @JvmStatic
- internal fun toCandidateDataBundle(id: String): Bundle {
- val bundle = Bundle()
- bundle.putString(BUNDLE_KEY_ID, id)
- return bundle
- }
-
- @JvmStatic
- internal fun createFrom(data: Bundle): CreatePasswordRequest {
- try {
- val id = data.getString(BUNDLE_KEY_ID)
- val password = data.getString(BUNDLE_KEY_PASSWORD)
- return CreatePasswordRequest(id!!, password!!)
- } catch (e: Exception) {
- throw FrameworkClassParsingException()
- }
- }
- }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequest.kt
deleted file mode 100644
index f3d402a..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequest.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * 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.credentialmanager.jetpack.developer
-
-import android.os.Bundle
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.BUNDLE_KEY_SUBTYPE
-
-/**
-* A request to register a passkey from the user's public key credential provider.
-*
-* @property requestJson the privileged request in JSON format in the standard webauthn web json
-* shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
-* @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
-* immediately when there is no available passkey registration offering instead of falling back to
-* discovering remote options, and false (default) otherwise
-* @throws NullPointerException If [requestJson] is null
-* @throws IllegalArgumentException If [requestJson] is empty
-*/
-class CreatePublicKeyCredentialRequest @JvmOverloads constructor(
- val requestJson: String,
- @get:JvmName("preferImmediatelyAvailableCredentials")
- val preferImmediatelyAvailableCredentials: Boolean = false
-) : CreateCredentialRequest(
- type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
- credentialData = toCredentialDataBundle(requestJson, preferImmediatelyAvailableCredentials),
- // The whole request data should be passed during the query phase.
- candidateQueryData = toCredentialDataBundle(requestJson, preferImmediatelyAvailableCredentials),
- requireSystemProvider = false,
-) {
-
- init {
- require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
- }
-
- /** @hide */
- companion object {
- const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
- "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
- internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
- internal const val BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST =
- "androidx.credentials.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST"
-
- @JvmStatic
- internal fun toCredentialDataBundle(
- requestJson: String,
- preferImmediatelyAvailableCredentials: Boolean
- ): Bundle {
- val bundle = Bundle()
- bundle.putString(BUNDLE_KEY_SUBTYPE,
- BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST)
- bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
- bundle.putBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
- preferImmediatelyAvailableCredentials)
- return bundle
- }
-
- @JvmStatic
- internal fun toCandidateDataBundle(
- requestJson: String,
- preferImmediatelyAvailableCredentials: Boolean
- ): Bundle {
- val bundle = Bundle()
- bundle.putString(BUNDLE_KEY_SUBTYPE,
- BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST)
- bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
- bundle.putBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
- preferImmediatelyAvailableCredentials)
- return bundle
- }
-
- @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
- // boolean value from being returned.
- @JvmStatic
- internal fun createFrom(data: Bundle): CreatePublicKeyCredentialRequest {
- try {
- val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
- val preferImmediatelyAvailableCredentials =
- data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
- return CreatePublicKeyCredentialRequest(requestJson!!,
- (preferImmediatelyAvailableCredentials!!) as Boolean)
- } catch (e: Exception) {
- throw FrameworkClassParsingException()
- }
- }
- }
-}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequestPrivileged.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequestPrivileged.kt
deleted file mode 100644
index 85ab2d2..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequestPrivileged.kt
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * 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.credentialmanager.jetpack.developer
-
-import android.os.Bundle
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.BUNDLE_KEY_SUBTYPE
-
-/**
- * A privileged request to register a passkey from the user’s public key credential provider, where
- * the caller can modify the rp. Only callers with privileged permission, e.g. user’s default
- * brower, caBLE, can use this. These permissions will be introduced in an upcoming release.
- * TODO("Add specific permission info/annotation")
- *
- * @property requestJson the privileged request in JSON format in the standard webauthn web json
- * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
- * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
- * immediately when there is no available passkey registration offering instead of falling back to
- * discovering remote options, and false (default) otherwise
- * @property relyingParty the expected true RP ID which will override the one in the [requestJson],
- * where rp is defined [here](https://w3c.github.io/webauthn/#rp-id)
- * @property clientDataHash a hash that is used to verify the [relyingParty] Identity
- * @throws NullPointerException If any of [requestJson], [relyingParty], or [clientDataHash] is
- * null
- * @throws IllegalArgumentException If any of [requestJson], [relyingParty], or [clientDataHash] is
- * empty
- */
-class CreatePublicKeyCredentialRequestPrivileged @JvmOverloads constructor(
- val requestJson: String,
- val relyingParty: String,
- val clientDataHash: String,
- @get:JvmName("preferImmediatelyAvailableCredentials")
- val preferImmediatelyAvailableCredentials: Boolean = false
-) : CreateCredentialRequest(
- type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
- credentialData = toCredentialDataBundle(
- requestJson,
- relyingParty,
- clientDataHash,
- preferImmediatelyAvailableCredentials
- ),
- // The whole request data should be passed during the query phase.
- candidateQueryData = toCredentialDataBundle(
- requestJson, relyingParty, clientDataHash, preferImmediatelyAvailableCredentials
- ),
- requireSystemProvider = false,
-) {
-
- init {
- require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
- require(relyingParty.isNotEmpty()) { "rp must not be empty" }
- require(clientDataHash.isNotEmpty()) { "clientDataHash must not be empty" }
- }
-
- /** A builder for [CreatePublicKeyCredentialRequestPrivileged]. */
- class Builder(
- private var requestJson: String,
- private var relyingParty: String,
- private var clientDataHash: String
- ) {
-
- private var preferImmediatelyAvailableCredentials: Boolean = false
-
- /**
- * Sets the privileged request in JSON format.
- */
- fun setRequestJson(requestJson: String): Builder {
- this.requestJson = requestJson
- return this
- }
-
- /**
- * Sets to true if you prefer the operation to return immediately when there is no available
- * passkey registration offering instead of falling back to discovering remote options, and
- * false otherwise.
- *
- * The default value is false.
- */
- @Suppress("MissingGetterMatchingBuilder")
- fun setPreferImmediatelyAvailableCredentials(
- preferImmediatelyAvailableCredentials: Boolean
- ): Builder {
- this.preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
- return this
- }
-
- /**
- * Sets the expected true RP ID which will override the one in the [requestJson].
- */
- fun setRelyingParty(relyingParty: String): Builder {
- this.relyingParty = relyingParty
- return this
- }
-
- /**
- * Sets a hash that is used to verify the [relyingParty] Identity.
- */
- fun setClientDataHash(clientDataHash: String): Builder {
- this.clientDataHash = clientDataHash
- return this
- }
-
- /** Builds a [CreatePublicKeyCredentialRequestPrivileged]. */
- fun build(): CreatePublicKeyCredentialRequestPrivileged {
- return CreatePublicKeyCredentialRequestPrivileged(
- this.requestJson,
- this.relyingParty, this.clientDataHash, this.preferImmediatelyAvailableCredentials
- )
- }
- }
-
- /** @hide */
- companion object {
- internal const val BUNDLE_KEY_RELYING_PARTY =
- "androidx.credentials.BUNDLE_KEY_RELYING_PARTY"
- internal const val BUNDLE_KEY_CLIENT_DATA_HASH =
- "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
- internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
- "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
-
- internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
-
- internal const val BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV =
- "androidx.credentials.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_" +
- "PRIVILEGED"
-
- @JvmStatic
- internal fun toCredentialDataBundle(
- requestJson: String,
- relyingParty: String,
- clientDataHash: String,
- preferImmediatelyAvailableCredentials: Boolean
- ): Bundle {
- val bundle = Bundle()
- bundle.putString(
- PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
- BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV
- )
- bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
- bundle.putString(BUNDLE_KEY_RELYING_PARTY, relyingParty)
- bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
- bundle.putBoolean(
- BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
- preferImmediatelyAvailableCredentials
- )
- return bundle
- }
-
- @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
- // boolean value from being returned.
- @JvmStatic
- internal fun createFrom(data: Bundle): CreatePublicKeyCredentialRequestPrivileged {
- try {
- val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
- val rp = data.getString(BUNDLE_KEY_RELYING_PARTY)
- val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH)
- val preferImmediatelyAvailableCredentials =
- data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
- return CreatePublicKeyCredentialRequestPrivileged(
- requestJson!!,
- rp!!,
- clientDataHash!!,
- (preferImmediatelyAvailableCredentials!!) as Boolean,
- )
- } catch (e: Exception) {
- throw FrameworkClassParsingException()
- }
- }
- }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/Credential.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/Credential.kt
deleted file mode 100644
index ee08e9e..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/Credential.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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.credentialmanager.jetpack.developer
-
-import android.os.Bundle
-
-/**
- * Base class for a credential with which the user consented to authenticate to the app.
- *
- * @property type the credential type
- * @property data the credential data in the [Bundle] format.
- */
-open class Credential(val type: String, val data: Bundle)
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialOption.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialOption.kt
deleted file mode 100644
index fc7b7de..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialOption.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * 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.credentialmanager.jetpack.developer
-
-import android.credentials.Credential
-import android.os.Bundle
-
-/**
- * Base request class for getting a registered credential.
- *
- * @property type the credential type
- * @property data the request data in the [Bundle] format
- * @property requireSystemProvider true if must only be fulfilled by a system provider and false
- * otherwise
- */
-open class GetCredentialOption(
- open val type: String,
- open val requestData: Bundle,
- open val candidateQueryData: Bundle,
- open val requireSystemProvider: Boolean,
-) {
- companion object {
- @JvmStatic
- fun createFrom(
- type: String,
- requestData: Bundle,
- candidateQueryData: Bundle,
- requireSystemProvider: Boolean
- ): GetCredentialOption {
- return try {
- when (type) {
- Credential.TYPE_PASSWORD_CREDENTIAL ->
- GetPasswordOption.createFrom(requestData)
- PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
- when (requestData.getString(PublicKeyCredential.BUNDLE_KEY_SUBTYPE)) {
- GetPublicKeyCredentialOption
- .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION ->
- GetPublicKeyCredentialOption.createFrom(requestData)
- GetPublicKeyCredentialOptionPrivileged
- .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED ->
- GetPublicKeyCredentialOptionPrivileged.createFrom(requestData)
- else -> throw FrameworkClassParsingException()
- }
- else -> throw FrameworkClassParsingException()
- }
- } catch (e: FrameworkClassParsingException) {
- // Parsing failed but don't crash the process. Instead just output a request with
- // the raw framework values.
- GetCustomCredentialOption(
- type, requestData, candidateQueryData, requireSystemProvider)
- }
- }
- }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialRequest.kt
deleted file mode 100644
index 5cb8d3b..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialRequest.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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.credentialmanager.jetpack.developer
-
-/**
- * Encapsulates a request to get a user credential.
- *
- * @property getCredentialOptions the list of [GetCredentialOption] from which the user can choose
- * one to authenticate to the app
- * @throws IllegalArgumentException If [getCredentialOptions] is empty
- */
-class GetCredentialRequest constructor(
- val getCredentialOptions: List<GetCredentialOption>,
-) {
-
- init {
- require(getCredentialOptions.isNotEmpty()) { "credentialRequests should not be empty" }
- }
-
- /** A builder for [GetCredentialRequest]. */
- class Builder {
- private var getCredentialOptions: MutableList<GetCredentialOption> = mutableListOf()
-
- /** Adds a specific type of [GetCredentialOption]. */
- fun addGetCredentialOption(getCredentialOption: GetCredentialOption): Builder {
- getCredentialOptions.add(getCredentialOption)
- return this
- }
-
- /** Sets the list of [GetCredentialOption]. */
- fun setGetCredentialOptions(getCredentialOptions: List<GetCredentialOption>): Builder {
- this.getCredentialOptions = getCredentialOptions.toMutableList()
- return this
- }
-
- /**
- * Builds a [GetCredentialRequest].
- *
- * @throws IllegalArgumentException If [getCredentialOptions] is empty
- */
- fun build(): GetCredentialRequest {
- return GetCredentialRequest(getCredentialOptions.toList())
- }
- }
-
- companion object {
- @JvmStatic
- fun createFrom(from: android.credentials.GetCredentialRequest): GetCredentialRequest {
- return GetCredentialRequest(
- from.getCredentialOptions.map {
- GetCredentialOption.createFrom(
- it.type,
- it.credentialRetrievalData,
- it.candidateQueryData,
- it.isSystemProviderRequired()
- )
- }
- )
- }
- }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCustomCredentialOption.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCustomCredentialOption.kt
deleted file mode 100644
index 803885c..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCustomCredentialOption.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.developer
-
-import android.os.Bundle
-
-/**
- * Allows extending custom versions of GetCredentialOptions for unique use cases.
- *
- * @property type the credential type determined by the credential-type-specific subclass
- * generated for custom use cases
- * @property requestData the request data in the [Bundle] format, generated for custom use cases
- * @property candidateQueryData the partial request data in the [Bundle] format that will be sent to
- * the provider during the initial candidate query stage, which should not contain sensitive user
- * information
- * @property requireSystemProvider true if must only be fulfilled by a system provider and false
- * otherwise
- * @throws IllegalArgumentException If [type] is empty
- * @throws NullPointerException If [requestData] or [type] is null
- */
-open class GetCustomCredentialOption(
- final override val type: String,
- final override val requestData: Bundle,
- final override val candidateQueryData: Bundle,
- @get:JvmName("requireSystemProvider")
- final override val requireSystemProvider: Boolean
-) : GetCredentialOption(
- type,
- requestData,
- candidateQueryData,
- requireSystemProvider
-) {
- init {
- require(type.isNotEmpty()) { "type should not be empty" }
- }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPasswordOption.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPasswordOption.kt
deleted file mode 100644
index 2b9cfa3..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPasswordOption.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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.credentialmanager.jetpack.developer
-
-import android.credentials.Credential
-import android.os.Bundle
-
-/** A request to retrieve the user's saved application password from their password provider. */
-class GetPasswordOption : GetCredentialOption(
- Credential.TYPE_PASSWORD_CREDENTIAL,
- Bundle(),
- Bundle(),
- false,
-) {
- companion object {
- @JvmStatic
- fun createFrom(data: Bundle): GetPasswordOption {
- return GetPasswordOption()
- }
- }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOption.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOption.kt
deleted file mode 100644
index 2f9b249..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOption.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * 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.credentialmanager.jetpack.developer
-
-import android.os.Bundle
-
-/**
- * A request to get passkeys from the user's public key credential provider.
- *
- * @property requestJson the privileged request in JSON format in the standard webauthn web json
- * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
- * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
- * immediately when there is no available credential instead of falling back to discovering remote
- * credentials, and false (default) otherwise
- * @throws NullPointerException If [requestJson] is null
- * @throws IllegalArgumentException If [requestJson] is empty
- */
-class GetPublicKeyCredentialOption @JvmOverloads constructor(
- val requestJson: String,
- @get:JvmName("preferImmediatelyAvailableCredentials")
- val preferImmediatelyAvailableCredentials: Boolean = false,
-) : GetCredentialOption(
- type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
- requestData = toRequestDataBundle(requestJson, preferImmediatelyAvailableCredentials),
- candidateQueryData = toRequestDataBundle(requestJson, preferImmediatelyAvailableCredentials),
- requireSystemProvider = false
-) {
- init {
- require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
- }
-
- /** @hide */
- companion object {
- internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
- "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
- internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
- internal const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION =
- "androidx.credentials.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION"
-
- @JvmStatic
- internal fun toRequestDataBundle(
- requestJson: String,
- preferImmediatelyAvailableCredentials: Boolean
- ): Bundle {
- val bundle = Bundle()
- bundle.putString(
- PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
- BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION
- )
- bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
- bundle.putBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
- preferImmediatelyAvailableCredentials)
- return bundle
- }
-
- @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
- // boolean value from being returned.
- @JvmStatic
- internal fun createFrom(data: Bundle): GetPublicKeyCredentialOption {
- try {
- val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
- val preferImmediatelyAvailableCredentials =
- data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
- return GetPublicKeyCredentialOption(requestJson!!,
- (preferImmediatelyAvailableCredentials!!) as Boolean)
- } catch (e: Exception) {
- throw FrameworkClassParsingException()
- }
- }
- }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOptionPrivileged.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOptionPrivileged.kt
deleted file mode 100644
index 6f4782a..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOptionPrivileged.kt
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * 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.credentialmanager.jetpack.developer
-
-import android.os.Bundle
-
-/**
- * A privileged request to get passkeys from the user's public key credential provider. The caller
- * can modify the RP. Only callers with privileged permission (e.g. user's public browser or caBLE)
- * can use this. These permissions will be introduced in an upcoming release.
- * TODO("Add specific permission info/annotation")
- *
- * @property requestJson the privileged request in JSON format in the standard webauthn web json
- * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
- * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
- * immediately when there is no available credential instead of falling back to discovering remote
- * credentials, and false (default) otherwise
- * @property relyingParty the expected true RP ID which will override the one in the [requestJson],
- * where relyingParty is defined [here](https://w3c.github.io/webauthn/#rp-id) in more detail
- * @property clientDataHash a hash that is used to verify the [relyingParty] Identity
- * @throws NullPointerException If any of [requestJson], [relyingParty], or [clientDataHash]
- * is null
- * @throws IllegalArgumentException If any of [requestJson], [relyingParty], or [clientDataHash] is
- * empty
- */
-class GetPublicKeyCredentialOptionPrivileged @JvmOverloads constructor(
- val requestJson: String,
- val relyingParty: String,
- val clientDataHash: String,
- @get:JvmName("preferImmediatelyAvailableCredentials")
- val preferImmediatelyAvailableCredentials: Boolean = false
-) : GetCredentialOption(
- type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
- requestData = toBundle(
- requestJson,
- relyingParty,
- clientDataHash,
- preferImmediatelyAvailableCredentials
- ),
- candidateQueryData = toBundle(
- requestJson,
- relyingParty,
- clientDataHash,
- preferImmediatelyAvailableCredentials
- ),
- requireSystemProvider = false,
-) {
-
- init {
- require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
- require(relyingParty.isNotEmpty()) { "rp must not be empty" }
- require(clientDataHash.isNotEmpty()) { "clientDataHash must not be empty" }
- }
-
- /** A builder for [GetPublicKeyCredentialOptionPrivileged]. */
- class Builder(
- private var requestJson: String,
- private var relyingParty: String,
- private var clientDataHash: String
- ) {
-
- private var preferImmediatelyAvailableCredentials: Boolean = false
-
- /**
- * Sets the privileged request in JSON format.
- */
- fun setRequestJson(requestJson: String): Builder {
- this.requestJson = requestJson
- return this
- }
-
- /**
- * Sets to true if you prefer the operation to return immediately when there is no available
- * credential instead of falling back to discovering remote credentials, and false
- * otherwise.
- *
- * The default value is false.
- */
- @Suppress("MissingGetterMatchingBuilder")
- fun setPreferImmediatelyAvailableCredentials(
- preferImmediatelyAvailableCredentials: Boolean
- ): Builder {
- this.preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
- return this
- }
-
- /**
- * Sets the expected true RP ID which will override the one in the [requestJson].
- */
- fun setRelyingParty(relyingParty: String): Builder {
- this.relyingParty = relyingParty
- return this
- }
-
- /**
- * Sets a hash that is used to verify the [relyingParty] Identity.
- */
- fun setClientDataHash(clientDataHash: String): Builder {
- this.clientDataHash = clientDataHash
- return this
- }
-
- /** Builds a [GetPublicKeyCredentialOptionPrivileged]. */
- fun build(): GetPublicKeyCredentialOptionPrivileged {
- return GetPublicKeyCredentialOptionPrivileged(
- this.requestJson,
- this.relyingParty, this.clientDataHash, this.preferImmediatelyAvailableCredentials
- )
- }
- }
-
- /** @hide */
- companion object {
- internal const val BUNDLE_KEY_RELYING_PARTY =
- "androidx.credentials.BUNDLE_KEY_RELYING_PARTY"
- internal const val BUNDLE_KEY_CLIENT_DATA_HASH =
- "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
- internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
- "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
- internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
- internal const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED =
- "androidx.credentials.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION" +
- "_PRIVILEGED"
-
- @JvmStatic
- internal fun toBundle(
- requestJson: String,
- relyingParty: String,
- clientDataHash: String,
- preferImmediatelyAvailableCredentials: Boolean
- ): Bundle {
- val bundle = Bundle()
- bundle.putString(
- PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
- BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED
- )
- bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
- bundle.putString(BUNDLE_KEY_RELYING_PARTY, relyingParty)
- bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
- bundle.putBoolean(
- BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
- preferImmediatelyAvailableCredentials
- )
- return bundle
- }
-
- @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
- // boolean value from being returned.
- @JvmStatic
- internal fun createFrom(data: Bundle): GetPublicKeyCredentialOptionPrivileged {
- try {
- val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
- val rp = data.getString(BUNDLE_KEY_RELYING_PARTY)
- val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH)
- val preferImmediatelyAvailableCredentials =
- data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
- return GetPublicKeyCredentialOptionPrivileged(
- requestJson!!,
- rp!!,
- clientDataHash!!,
- (preferImmediatelyAvailableCredentials!!) as Boolean,
- )
- } catch (e: Exception) {
- throw FrameworkClassParsingException()
- }
- }
- }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PasswordCredential.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PasswordCredential.kt
deleted file mode 100644
index 1658858..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PasswordCredential.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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.credentialmanager.jetpack.developer
-
-import android.os.Bundle
-
-class PasswordCredential constructor(
- val id: String,
- val password: String,
-) : Credential(android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL, toBundle(id, password)) {
-
- init {
- require(password.isNotEmpty()) { "password should not be empty" }
- }
-
- /** @hide */
- companion object {
-
- const val BUNDLE_KEY_ID = "androidx.credentials.BUNDLE_KEY_ID"
- const val BUNDLE_KEY_PASSWORD = "androidx.credentials.BUNDLE_KEY_PASSWORD"
-
- @JvmStatic
- internal fun toBundle(id: String, password: String): Bundle {
- val bundle = Bundle()
- bundle.putString(BUNDLE_KEY_ID, id)
- bundle.putString(BUNDLE_KEY_PASSWORD, password)
- return bundle
- }
-
- @JvmStatic
- internal fun createFrom(data: Bundle): PasswordCredential {
- try {
- val id = data.getString(BUNDLE_KEY_ID)
- val password = data.getString(BUNDLE_KEY_PASSWORD)
- return PasswordCredential(id!!, password!!)
- } catch (e: Exception) {
- throw FrameworkClassParsingException()
- }
- }
- }
-}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PublicKeyCredential.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PublicKeyCredential.kt
deleted file mode 100644
index 6a81167..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PublicKeyCredential.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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.credentialmanager.jetpack.developer
-
-import android.os.Bundle
-
-/**
- * Represents the user's passkey credential granted by the user for app sign-in.
- *
- * @property authenticationResponseJson the public key credential authentication response in
- * JSON format that follows the standard webauthn json format shown at
- * [this w3c link](https://w3c.github.io/webauthn/#dictdef-authenticationresponsejson)
- * @throws NullPointerException If [authenticationResponseJson] is null. This is handled by the
- * kotlin runtime
- * @throws IllegalArgumentException If [authenticationResponseJson] is empty
- *
- * @hide
- */
-class PublicKeyCredential constructor(
- val authenticationResponseJson: String
-) : Credential(
- TYPE_PUBLIC_KEY_CREDENTIAL,
- toBundle(authenticationResponseJson)
-) {
-
- init {
- require(authenticationResponseJson.isNotEmpty()) {
- "authentication response JSON must not be empty" }
- }
- companion object {
- /** The type value for public key credential related operations. */
- const val TYPE_PUBLIC_KEY_CREDENTIAL: String =
- "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL"
- /** The Bundle key value for the public key credential subtype (privileged or regular). */
- internal const val BUNDLE_KEY_SUBTYPE = "androidx.credentials.BUNDLE_KEY_SUBTYPE"
- const val BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON =
- "androidx.credentials.BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON"
-
- @JvmStatic
- internal fun toBundle(authenticationResponseJson: String): Bundle {
- val bundle = Bundle()
- bundle.putString(BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON, authenticationResponseJson)
- return bundle
- }
- }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/Action.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/Action.kt
deleted file mode 100644
index 1abf911..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/Action.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * 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.credentialmanager.jetpack.provider
-
-import android.annotation.SuppressLint
-import android.app.PendingIntent
-import android.app.slice.Slice
-import android.app.slice.SliceSpec
-import android.net.Uri
-import android.util.Log
-import java.util.Collections
-
-/**
- * UI representation for a credential entry used during the get credential flow.
- *
- * TODO: move to jetpack.
- */
-class Action constructor(
- val title: CharSequence,
- val subTitle: CharSequence?,
- val pendingIntent: PendingIntent?,
-) {
-
- init {
- require(title.isNotEmpty()) { "title must not be empty" }
- }
-
- companion object {
- private const val TAG = "Action"
- internal const val SLICE_HINT_TITLE =
- "androidx.credentials.provider.action.HINT_ACTION_TITLE"
- internal const val SLICE_HINT_SUBTITLE =
- "androidx.credentials.provider.action.HINT_ACTION_SUBTEXT"
- internal const val SLICE_HINT_PENDING_INTENT =
- "androidx.credentials.provider.action.SLICE_HINT_PENDING_INTENT"
-
- @JvmStatic
- fun toSlice(action: Action): Slice {
- // TODO("Put the right spec and version value")
- val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))
- .addText(action.title, /*subType=*/null,
- listOf(SLICE_HINT_TITLE))
- .addText(action.subTitle, /*subType=*/null,
- listOf(SLICE_HINT_SUBTITLE))
- if (action.pendingIntent != null) {
- sliceBuilder.addAction(action.pendingIntent,
- Slice.Builder(sliceBuilder)
- .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
- .build(),
- /*subType=*/null)
- }
- return sliceBuilder.build()
- }
-
- /**
- * Returns an instance of [Action] derived from a [Slice] object.
- *
- * @param slice the [Slice] object constructed through [toSlice]
- */
- @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
- @JvmStatic
- fun fromSlice(slice: Slice): Action? {
- // TODO("Put the right spec and version value")
- var title: CharSequence = ""
- var subTitle: CharSequence? = null
- var pendingIntent: PendingIntent? = null
-
- slice.items.forEach {
- if (it.hasHint(SLICE_HINT_TITLE)) {
- title = it.text
- } else if (it.hasHint(SLICE_HINT_SUBTITLE)) {
- subTitle = it.text
- } else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
- pendingIntent = it.action
- }
- }
-
- return try {
- Action(title, subTitle, pendingIntent)
- } catch (e: Exception) {
- Log.i(TAG, "fromSlice failed with: " + e.message)
- null
- }
- }
- }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/AuthenticationAction.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/AuthenticationAction.kt
deleted file mode 100644
index 283c7ba..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/AuthenticationAction.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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.credentialmanager.jetpack.provider
-
-import android.app.PendingIntent
-import android.app.slice.Slice
-import android.util.Log
-import androidx.annotation.VisibleForTesting
-
-/**
- * UI representation for a credential entry used during the get credential flow.
- *
- * TODO: move to jetpack.
- */
-class AuthenticationAction constructor(
- val pendingIntent: PendingIntent
-) {
-
-
- companion object {
- private const val TAG = "AuthenticationAction"
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- internal const val SLICE_HINT_PENDING_INTENT =
- "androidx.credentials.provider.authenticationAction.SLICE_HINT_PENDING_INTENT"
-
- @JvmStatic
- fun fromSlice(slice: Slice): AuthenticationAction? {
- slice.items.forEach {
- if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
- return try {
- AuthenticationAction(it.action)
- } catch (e: Exception) {
- Log.i(TAG, "fromSlice failed with: " + e.message)
- null
- }
- }
- }
- return null
- }
- }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CreateEntry.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CreateEntry.kt
deleted file mode 100644
index 0ec91d6..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CreateEntry.kt
+++ /dev/null
@@ -1,243 +0,0 @@
-/*
- * 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.credentialmanager.jetpack.provider
-
-import android.annotation.SuppressLint
-import android.app.PendingIntent
-import android.app.slice.Slice
-import android.app.slice.SliceSpec
-import android.graphics.drawable.Icon
-import android.net.Uri
-import android.os.Bundle
-import android.util.Log
-import java.util.Collections
-
-/**
- * UI representation for a save entry used during the create credential flow.
- *
- * TODO: move to jetpack.
- */
-class CreateEntry internal constructor(
- val accountName: CharSequence,
- val pendingIntent: PendingIntent?,
- val icon: Icon?,
- val lastUsedTimeMillis: Long,
- val credentialCountInformationList: List<CredentialCountInformation>,
- val footerDescription: CharSequence?,
-) {
-
- init {
- require(accountName.isNotEmpty()) { "accountName must not be empty" }
- }
-
- /**
- * A builder for [CreateEntry]
- *
- * @property accountName the name of the account where the credential will be registered
- * @property pendingIntent the [PendingIntent] that will be fired when the user selects
- * this entry
- *
- * @hide
- */
- class Builder constructor(
- private val accountName: CharSequence,
- private val pendingIntent: PendingIntent? = null
- ) {
-
- private var credentialCountInformationList: MutableList<CredentialCountInformation> =
- mutableListOf()
- private var icon: Icon? = null
- private var lastUsedTimeMillis: Long = 0
- private var footerDescription: CharSequence? = null
-
- /** Adds a [CredentialCountInformation] denoting a given credential
- * type and the count of credentials that the provider has stored for that
- * credential type.
- *
- * This information will be displayed on the [CreateEntry] to help the user
- * make a choice.
- */
- @Suppress("MissingGetterMatchingBuilder")
- fun addCredentialCountInformation(info: CredentialCountInformation): Builder {
- credentialCountInformationList.add(info)
- return this
- }
-
- /** Sets a list of [CredentialCountInformation]. Each item in the list denotes a given
- * credential type and the count of credentials that the provider has stored of that
- * credential type.
- *
- * This information will be displayed on the [CreateEntry] to help the user
- * make a choice.
- */
- fun setCredentialCountInformationList(infoList: List<CredentialCountInformation>): Builder {
- credentialCountInformationList = infoList as MutableList<CredentialCountInformation>
- return this
- }
-
- /** Sets an icon to be displayed with the entry on the UI */
- fun setIcon(icon: Icon?): Builder {
- this.icon = icon
- return this
- }
-
- /** Sets the last time this account was used */
- fun setLastUsedTimeMillis(lastUsedTimeMillis: Long): Builder {
- this.lastUsedTimeMillis = lastUsedTimeMillis
- return this
- }
-
- /** Sets the footer description of this */
- fun setFooterDescription(footerDescription: CharSequence): Builder {
- this.footerDescription = footerDescription
- return this
- }
-
- /**
- * Builds an instance of [CreateEntry]
- *
- * @throws IllegalArgumentException If [accountName] is empty
- */
- fun build(): CreateEntry {
- return CreateEntry(accountName, pendingIntent, icon, lastUsedTimeMillis,
- credentialCountInformationList, footerDescription)
- }
- }
-
- companion object {
- private const val TAG = "CreateEntry"
- internal const val SLICE_HINT_ACCOUNT_NAME =
- "androidx.credentials.provider.createEntry.SLICE_HINT_USER_PROVIDER_ACCOUNT_NAME"
- internal const val SLICE_HINT_ICON =
- "androidx.credentials.provider.createEntry.SLICE_HINT_PROFILE_ICON"
- internal const val SLICE_HINT_CREDENTIAL_COUNT_INFORMATION =
- "androidx.credentials.provider.createEntry.SLICE_HINT_CREDENTIAL_COUNT_INFORMATION"
- internal const val SLICE_HINT_LAST_USED_TIME_MILLIS =
- "androidx.credentials.provider.createEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
- internal const val SLICE_HINT_PENDING_INTENT =
- "androidx.credentials.provider.createEntry.SLICE_HINT_PENDING_INTENT"
- internal const val SLICE_HINT_FOOTER_DESCRIPTION =
- "androidx.credentials.provider.createEntry.SLICE_HINT_FOOTER_DESCRIPTION"
-
- @JvmStatic
- fun toSlice(createEntry: CreateEntry): Slice {
- // TODO("Use the right type and revision")
- val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))
- sliceBuilder.addText(createEntry.accountName, /*subType=*/null,
- listOf(SLICE_HINT_ACCOUNT_NAME))
- .addLong(createEntry.lastUsedTimeMillis, /*subType=*/null, listOf(
- SLICE_HINT_LAST_USED_TIME_MILLIS))
- if (createEntry.icon != null) {
- sliceBuilder.addIcon(createEntry.icon, /*subType=*/null,
- listOf(SLICE_HINT_ICON))
- }
-
- val credentialCountBundle = convertCredentialCountInfoToBundle(
- createEntry.credentialCountInformationList)
- if (credentialCountBundle != null) {
- sliceBuilder.addBundle(convertCredentialCountInfoToBundle(
- createEntry.credentialCountInformationList), null, listOf(
- SLICE_HINT_CREDENTIAL_COUNT_INFORMATION))
- }
- if (createEntry.pendingIntent != null) {
- sliceBuilder.addAction(createEntry.pendingIntent,
- Slice.Builder(sliceBuilder)
- .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
- .build(),
- /*subType=*/null)
- }
- if (createEntry.footerDescription != null) {
- sliceBuilder.addText(createEntry.footerDescription, /*subType=*/null,
- listOf(SLICE_HINT_FOOTER_DESCRIPTION))
- }
- return sliceBuilder.build()
- }
-
- /**
- * Returns an instance of [CreateEntry] derived from a [Slice] object.
- *
- * @param slice the [Slice] object constructed through [toSlice]
- */
- @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
- @JvmStatic
- fun fromSlice(slice: Slice): CreateEntry? {
- // TODO("Put the right spec and version value")
- var accountName: CharSequence = ""
- var icon: Icon? = null
- var pendingIntent: PendingIntent? = null
- var credentialCountInfo: List<CredentialCountInformation> = listOf()
- var lastUsedTimeMillis: Long = 0
- var footerDescription: CharSequence? = null
-
- slice.items.forEach {
- if (it.hasHint(SLICE_HINT_ACCOUNT_NAME)) {
- accountName = it.text
- } else if (it.hasHint(SLICE_HINT_ICON)) {
- icon = it.icon
- } else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
- pendingIntent = it.action
- } else if (it.hasHint(SLICE_HINT_CREDENTIAL_COUNT_INFORMATION)) {
- credentialCountInfo = convertBundleToCredentialCountInfo(it.bundle)
- } else if (it.hasHint(SLICE_HINT_LAST_USED_TIME_MILLIS)) {
- lastUsedTimeMillis = it.long
- } else if (it.hasHint(SLICE_HINT_FOOTER_DESCRIPTION)) {
- footerDescription = it.text
- }
- }
-
- return try {
- CreateEntry(accountName, pendingIntent, icon,
- lastUsedTimeMillis, credentialCountInfo, footerDescription)
- } catch (e: Exception) {
- Log.i(TAG, "fromSlice failed with: " + e.message)
- null
- }
- }
-
- @JvmStatic
- internal fun convertBundleToCredentialCountInfo(bundle: Bundle?):
- List<CredentialCountInformation> {
- val credentialCountList = ArrayList<CredentialCountInformation>()
- if (bundle == null) {
- return credentialCountList
- }
- bundle.keySet().forEach {
- try {
- credentialCountList.add(
- CredentialCountInformation(it, bundle.getInt(it)))
- } catch (e: Exception) {
- Log.i(TAG, "Issue unpacking credential count info bundle: " + e.message)
- }
- }
- return credentialCountList
- }
-
- @JvmStatic
- internal fun convertCredentialCountInfoToBundle(
- credentialCountInformationList: List<CredentialCountInformation>
- ): Bundle? {
- if (credentialCountInformationList.isEmpty()) {
- return null
- }
- val bundle = Bundle()
- credentialCountInformationList.forEach {
- bundle.putInt(it.type, it.count)
- }
- return bundle
- }
- }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialCountInformation.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialCountInformation.kt
deleted file mode 100644
index aa77b74..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialCountInformation.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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.credentialmanager.jetpack.provider
-
-import android.credentials.Credential
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential
-
-class CredentialCountInformation constructor(
- val type: String,
- val count: Int
-) {
- companion object {
- @JvmStatic
- fun createPasswordCountInformation(count: Int): CredentialCountInformation {
- return CredentialCountInformation(Credential.TYPE_PASSWORD_CREDENTIAL, count)
- }
-
- @JvmStatic
- fun getPasswordCount(infos: List<CredentialCountInformation>): Int? {
- return getCountForType(infos, Credential.TYPE_PASSWORD_CREDENTIAL)
- }
-
- @JvmStatic
- fun createPublicKeyCountInformation(count: Int): CredentialCountInformation {
- return CredentialCountInformation(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL, count)
- }
-
- @JvmStatic
- fun getPasskeyCount(infos: List<CredentialCountInformation>): Int? {
- return getCountForType(infos, PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
- }
-
- @JvmStatic
- fun createTotalCountInformation(count: Int): CredentialCountInformation {
- return CredentialCountInformation("TOTAL_COUNT", count)
- }
-
- @JvmStatic
- fun getTotalCount(infos: List<CredentialCountInformation>): Int? {
- return getCountForType(infos, "TOTAL_COUNT")
- }
-
- private fun getCountForType(infos: List<CredentialCountInformation>, type: String): Int? {
- return infos.firstOrNull { info -> info.type == type }?.count
- }
- }
-}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntry.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntry.kt
deleted file mode 100644
index 61a104b..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntry.kt
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * 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.credentialmanager.jetpack.provider
-
-import android.annotation.SuppressLint
-import android.app.PendingIntent
-import android.app.slice.Slice
-import android.app.slice.SliceSpec
-import android.graphics.drawable.Icon
-import android.net.Uri
-import android.util.Log
-import java.util.Collections
-
-/**
- * UI representation for a credential entry used during the get credential flow.
- *
- * TODO: move to jetpack.
- */
-open class CredentialEntry constructor(
- // TODO("Add credential type display name for both CredentialEntry & CreateEntry")
- val type: String,
- val typeDisplayName: CharSequence,
- val username: CharSequence,
- val displayName: CharSequence?,
- val pendingIntent: PendingIntent?,
- // TODO("Consider using Instant or other strongly typed time data type")
- val lastUsedTimeMillis: Long,
- val icon: Icon?,
- var autoSelectAllowed: Boolean
-) {
- init {
- require(type.isNotEmpty()) { "type must not be empty" }
- require(username.isNotEmpty()) { "type must not be empty" }
- }
-
- companion object {
- private const val TAG = "CredentialEntry"
- internal const val SLICE_HINT_TYPE_DISPLAY_NAME =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
- internal const val SLICE_HINT_USERNAME =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_USER_NAME"
- internal const val SLICE_HINT_DISPLAYNAME =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_CREDENTIAL_TYPE_DISPLAY_NAME"
- internal const val SLICE_HINT_LAST_USED_TIME_MILLIS =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
- internal const val SLICE_HINT_ICON =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_PROFILE_ICON"
- internal const val SLICE_HINT_PENDING_INTENT =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_PENDING_INTENT"
- internal const val SLICE_HINT_AUTO_ALLOWED =
- "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_ALLOWED"
- internal const val AUTO_SELECT_TRUE_STRING = "true"
- internal const val AUTO_SELECT_FALSE_STRING = "false"
-
- @JvmStatic
- internal fun toSlice(credentialEntry: CredentialEntry): Slice {
- // TODO("Put the right revision value")
- val autoSelectAllowed = if (credentialEntry.autoSelectAllowed) {
- AUTO_SELECT_TRUE_STRING
- } else {
- AUTO_SELECT_FALSE_STRING
- }
- val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(
- credentialEntry.type, 1))
- .addText(credentialEntry.typeDisplayName, /*subType=*/null,
- listOf(SLICE_HINT_TYPE_DISPLAY_NAME))
- .addText(credentialEntry.username, /*subType=*/null,
- listOf(SLICE_HINT_USERNAME))
- .addText(credentialEntry.displayName, /*subType=*/null,
- listOf(SLICE_HINT_DISPLAYNAME))
- .addLong(credentialEntry.lastUsedTimeMillis, /*subType=*/null,
- listOf(SLICE_HINT_LAST_USED_TIME_MILLIS))
- .addText(autoSelectAllowed, /*subType=*/null,
- listOf(SLICE_HINT_AUTO_ALLOWED))
- if (credentialEntry.icon != null) {
- sliceBuilder.addIcon(credentialEntry.icon, /*subType=*/null,
- listOf(SLICE_HINT_ICON))
- }
- if (credentialEntry.pendingIntent != null) {
- sliceBuilder.addAction(credentialEntry.pendingIntent,
- Slice.Builder(sliceBuilder)
- .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
- .build(),
- /*subType=*/null)
- }
- return sliceBuilder.build()
- }
-
- /**
- * Returns an instance of [CredentialEntry] derived from a [Slice] object.
- *
- * @param slice the [Slice] object constructed through [toSlice]
- */
- @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
- @JvmStatic
- fun fromSlice(slice: Slice): CredentialEntry? {
- var typeDisplayName: CharSequence? = null
- var username: CharSequence? = null
- var displayName: CharSequence? = null
- var icon: Icon? = null
- var pendingIntent: PendingIntent? = null
- var lastUsedTimeMillis: Long = 0
- var autoSelectAllowed = false
-
- slice.items.forEach {
- if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
- typeDisplayName = it.text
- } else if (it.hasHint(SLICE_HINT_USERNAME)) {
- username = it.text
- } else if (it.hasHint(SLICE_HINT_DISPLAYNAME)) {
- displayName = it.text
- } else if (it.hasHint(SLICE_HINT_ICON)) {
- icon = it.icon
- } else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
- pendingIntent = it.action
- } else if (it.hasHint(SLICE_HINT_LAST_USED_TIME_MILLIS)) {
- lastUsedTimeMillis = it.long
- } else if (it.hasHint(SLICE_HINT_AUTO_ALLOWED)) {
- val autoSelectValue = it.text
- if (autoSelectValue == AUTO_SELECT_TRUE_STRING) {
- autoSelectAllowed = true
- }
- }
- }
-
- return try {
- CredentialEntry(slice.spec!!.type, typeDisplayName!!, username!!,
- displayName, pendingIntent,
- lastUsedTimeMillis, icon, autoSelectAllowed)
- } catch (e: Exception) {
- Log.i(TAG, "fromSlice failed with: " + e.message)
- null
- }
- }
- }
-}
diff --git a/packages/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml
index ae6f71c..ab680b0 100644
--- a/packages/PackageInstaller/res/values/strings.xml
+++ b/packages/PackageInstaller/res/values/strings.xml
@@ -38,10 +38,11 @@
<!-- Message for updating an existing app [CHAR LIMIT=NONE] -->
<string name="install_confirm_question_update">Do you want to update this app?</string>
<!-- TODO(b/244413073) Revise the description after getting UX input and UXR on this. -->
- <!-- Message for updating an existing app when updating owner changed [CHAR LIMIT=NONE] -->
+ <!-- Message for updating an existing app when updating owner changed [DO NOT TRANSLATE][CHAR LIMIT=NONE] -->
<string name="install_confirm_question_update_owner_changed">Updates to this app are currently managed by <xliff:g id="existing_update_owner">%1$s</xliff:g>.\n\nBy updating, you\'ll get future updates from <xliff:g id="new_update_owner">%2$s</xliff:g> instead.</string>
- <!-- Message for updating an existing app with update owner reminder [CHAR LIMIT=NONE] -->
- <string name="install_confirm_question_update_owner_reminder">Updates to this app are currently managed by <xliff:g id="existing_update_owner">%1$s</xliff:g>.\n\nDo you want to install this update from <xliff:g id="new_update_owner">%2$s</xliff:g>.</string>
+ <!-- TODO(b/244413073) Revise the description after getting UX input and UXR on this. -->
+ <!-- Message for updating an existing app with update owner reminder [DO NOT TRANSLATE][CHAR LIMIT=NONE] -->
+ <string name="install_confirm_question_update_owner_reminder">Updates to this app are currently managed by <xliff:g id="existing_update_owner">%1$s</xliff:g>.\n\nDo you want to install this update from <xliff:g id="new_update_owner">%2$s</xliff:g>?</string>
<!-- [CHAR LIMIT=100] -->
<string name="install_failed">App not installed.</string>
<!-- Reason displayed when installation fails because the package was blocked
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java
old mode 100755
new mode 100644
index 93387e2..c6217ec
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java
@@ -141,7 +141,7 @@
File file = new File(mPackageURI.getPath());
try {
final InstallInfo result = getPackageManager().getPackageInstaller()
- .getInstallInfo(file, 0);
+ .readInstallInfo(file, 0);
params.setAppPackageName(result.getPackageName());
params.setInstallLocation(result.getInstallLocation());
try {
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index 49c9188..3ec81aa 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -392,7 +392,7 @@
final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID,
-1 /* defaultValue */);
final SessionInfo info = mInstaller.getSessionInfo(sessionId);
- if (info == null || !info.getIsPreApprovalRequested()) {
+ if (info == null || !info.isPreApprovalRequested()) {
Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
finish();
return;
diff --git a/packages/SettingsLib/ActivityEmbedding/Android.bp b/packages/SettingsLib/ActivityEmbedding/Android.bp
index c35fb3b..4b4cfb7 100644
--- a/packages/SettingsLib/ActivityEmbedding/Android.bp
+++ b/packages/SettingsLib/ActivityEmbedding/Android.bp
@@ -30,5 +30,6 @@
apex_available: [
"//apex_available:platform",
"com.android.permission",
+ "com.android.healthconnect",
],
}
diff --git a/packages/SettingsLib/ActivityEmbedding/OWNERS b/packages/SettingsLib/ActivityEmbedding/OWNERS
new file mode 100644
index 0000000..7022402
--- /dev/null
+++ b/packages/SettingsLib/ActivityEmbedding/OWNERS
@@ -0,0 +1,5 @@
+# Default reviewers for this and subdirectories.
+arcwang@google.com
+chiujason@google.com
+
+# Emergency approvers in case the above are not available
diff --git a/packages/SettingsLib/ActivityEmbedding/src/com/android/settingslib/activityembedding/ActivityEmbeddingUtils.java b/packages/SettingsLib/ActivityEmbedding/src/com/android/settingslib/activityembedding/ActivityEmbeddingUtils.java
index 1407725..f89be9f 100644
--- a/packages/SettingsLib/ActivityEmbedding/src/com/android/settingslib/activityembedding/ActivityEmbeddingUtils.java
+++ b/packages/SettingsLib/ActivityEmbedding/src/com/android/settingslib/activityembedding/ActivityEmbeddingUtils.java
@@ -23,7 +23,7 @@
import android.util.Log;
import androidx.core.os.BuildCompat;
-import androidx.window.embedding.SplitController;
+import androidx.window.embedding.ActivityEmbeddingController;
import com.android.settingslib.utils.BuildCompatUtils;
@@ -84,7 +84,7 @@
* @param activity Activity that needs the check
*/
public static boolean isActivityEmbedded(Activity activity) {
- return SplitController.getInstance().isActivityEmbedded(activity);
+ return ActivityEmbeddingController.getInstance(activity).isActivityEmbedded(activity);
}
/**
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index f170ead..af20a4b 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -42,7 +42,7 @@
"SettingsLibAdaptiveIcon",
"SettingsLibRadioButtonPreference",
"SettingsLibSelectorWithWidgetPreference",
- "SettingsLibDisplayDensityUtils",
+ "SettingsLibDisplayUtils",
"SettingsLibUtils",
"SettingsLibEmergencyNumber",
"SettingsLibTopIntroPreference",
diff --git a/packages/SettingsLib/DisplayDensityUtils/Android.bp b/packages/SettingsLib/DisplayUtils/Android.bp
similarity index 90%
rename from packages/SettingsLib/DisplayDensityUtils/Android.bp
rename to packages/SettingsLib/DisplayUtils/Android.bp
index b7bfb5f..136f883 100644
--- a/packages/SettingsLib/DisplayDensityUtils/Android.bp
+++ b/packages/SettingsLib/DisplayUtils/Android.bp
@@ -8,7 +8,7 @@
}
android_library {
- name: "SettingsLibDisplayDensityUtils",
+ name: "SettingsLibDisplayUtils",
srcs: ["src/**/*.java"],
diff --git a/packages/SettingsLib/DisplayUtils/AndroidManifest.xml b/packages/SettingsLib/DisplayUtils/AndroidManifest.xml
new file mode 100644
index 0000000..e064cab
--- /dev/null
+++ b/packages/SettingsLib/DisplayUtils/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.display">
+
+</manifest>
diff --git a/packages/SettingsLib/src/com/android/settingslib/display/BrightnessUtils.java b/packages/SettingsLib/DisplayUtils/src/com/android/settingslib/display/BrightnessUtils.java
similarity index 98%
rename from packages/SettingsLib/src/com/android/settingslib/display/BrightnessUtils.java
rename to packages/SettingsLib/DisplayUtils/src/com/android/settingslib/display/BrightnessUtils.java
index 4f86afaa9..66ed1043 100644
--- a/packages/SettingsLib/src/com/android/settingslib/display/BrightnessUtils.java
+++ b/packages/SettingsLib/DisplayUtils/src/com/android/settingslib/display/BrightnessUtils.java
@@ -18,6 +18,7 @@
import android.util.MathUtils;
+/** Utility methods for calculating the display brightness. */
public class BrightnessUtils {
public static final int GAMMA_SPACE_MIN = 0;
diff --git a/packages/SettingsLib/DisplayDensityUtils/src/com/android/settingslib/display/DisplayDensityConfiguration.java b/packages/SettingsLib/DisplayUtils/src/com/android/settingslib/display/DisplayDensityConfiguration.java
similarity index 100%
rename from packages/SettingsLib/DisplayDensityUtils/src/com/android/settingslib/display/DisplayDensityConfiguration.java
rename to packages/SettingsLib/DisplayUtils/src/com/android/settingslib/display/DisplayDensityConfiguration.java
diff --git a/packages/SettingsLib/IllustrationPreference/res/values/attrs.xml b/packages/SettingsLib/IllustrationPreference/res/values/attrs.xml
new file mode 100644
index 0000000..141886c
--- /dev/null
+++ b/packages/SettingsLib/IllustrationPreference/res/values/attrs.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <declare-styleable name="IllustrationPreference">
+ <attr name="dynamicColor" format="boolean" />
+ </declare-styleable>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
index 1592094..3b90275 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
@@ -61,6 +61,8 @@
private View mMiddleGroundView;
private OnBindListener mOnBindListener;
+ private boolean mLottieDynamicColor;
+
/**
* Interface to listen in on when {@link #onBindViewHolder(PreferenceViewHolder)} occurs.
*/
@@ -146,6 +148,10 @@
ColorUtils.applyDynamicColors(getContext(), illustrationView);
}
+ if (mLottieDynamicColor) {
+ LottieColorUtils.applyDynamicColors(getContext(), illustrationView);
+ }
+
if (mOnBindListener != null) {
mOnBindListener.onBind(illustrationView);
}
@@ -262,6 +268,21 @@
}
}
+ /**
+ * Sets the lottie illustration apply dynamic color.
+ */
+ public void applyDynamicColor() {
+ mLottieDynamicColor = true;
+ notifyChanged();
+ }
+
+ /**
+ * Return if the lottie illustration apply dynamic color or not.
+ */
+ public boolean isApplyDynamicColor() {
+ return mLottieDynamicColor;
+ }
+
private void resetImageResourceCache() {
mImageDrawable = null;
mImageUri = null;
@@ -403,9 +424,15 @@
mIsAutoScale = false;
if (attrs != null) {
- final TypedArray a = context.obtainStyledAttributes(attrs,
+ TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.LottieAnimationView, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
mImageResId = a.getResourceId(R.styleable.LottieAnimationView_lottie_rawRes, 0);
+
+ a = context.obtainStyledAttributes(attrs,
+ R.styleable.IllustrationPreference, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
+ mLottieDynamicColor = a.getBoolean(R.styleable.IllustrationPreference_dynamicColor,
+ false);
+
a.recycle();
}
}
diff --git a/packages/SettingsLib/OWNERS b/packages/SettingsLib/OWNERS
index 36315b5..1b3020a 100644
--- a/packages/SettingsLib/OWNERS
+++ b/packages/SettingsLib/OWNERS
@@ -1,12 +1,11 @@
# People who can approve changes for submission
dsandler@android.com
edgarwang@google.com
-emilychuang@google.com
evanlaird@google.com
-hanxu@google.com
juliacr@google.com
+lijun@google.com
+songchenxi@google.com
yantingyang@google.com
-ykhung@google.com
# Exempt resource files (because they are in a flat directory and too hard to manage via OWNERS)
per-file *.xml=*
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index e54e276..c702f59 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -24,6 +24,9 @@
import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
import com.android.settingslib.spa.gallery.dialog.AlterDialogPageProvider
import com.android.settingslib.spa.gallery.home.HomePageProvider
+import com.android.settingslib.spa.gallery.itemList.ItemListPageProvider
+import com.android.settingslib.spa.gallery.itemList.ItemOperatePageProvider
+import com.android.settingslib.spa.gallery.itemList.OperateListPageProvider
import com.android.settingslib.spa.gallery.page.ArgumentPageProvider
import com.android.settingslib.spa.gallery.page.ChartPageProvider
import com.android.settingslib.spa.gallery.page.FooterPageProvider
@@ -50,6 +53,8 @@
HOME("home"),
PREFERENCE("preference"),
ARGUMENT("argument"),
+ ITEM_LIST("itemList"),
+ ITEM_OP_PAGE("itemOp"),
// Add your SPPs
}
@@ -76,6 +81,9 @@
LoadingBarPageProvider,
ChartPageProvider,
AlterDialogPageProvider,
+ ItemListPageProvider,
+ ItemOperatePageProvider,
+ OperateListPageProvider,
),
rootPages = listOf(
HomePageProvider.createSettingsPage(),
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonsPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonsPage.kt
index decc292..df1d7d1 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonsPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonsPage.kt
@@ -24,8 +24,8 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.widget.button.ActionButton
@@ -39,13 +39,24 @@
object ActionButtonPageProvider : SettingsPageProvider {
override val name = "ActionButton"
+ override fun getTitle(arguments: Bundle?): String {
+ return TITLE
+ }
+
@Composable
override fun Page(arguments: Bundle?) {
- ActionButtonPage()
+ RegularScaffold(title = TITLE) {
+ val actionButtons = listOf(
+ ActionButton(text = "Open", imageVector = Icons.Outlined.Launch) {},
+ ActionButton(text = "Uninstall", imageVector = Icons.Outlined.Delete) {},
+ ActionButton(text = "Force stop", imageVector = Icons.Outlined.WarningAmber) {},
+ )
+ ActionButtons(actionButtons)
+ }
}
fun buildInjectEntry(): SettingsEntryBuilder {
- return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+ return SettingsEntryBuilder.createInject(owner = createSettingsPage())
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = TITLE
@@ -55,22 +66,10 @@
}
}
-@Composable
-fun ActionButtonPage() {
- RegularScaffold(title = TITLE) {
- val actionButtons = listOf(
- ActionButton(text = "Open", imageVector = Icons.Outlined.Launch) {},
- ActionButton(text = "Uninstall", imageVector = Icons.Outlined.Delete) {},
- ActionButton(text = "Force stop", imageVector = Icons.Outlined.WarningAmber) {},
- )
- ActionButtons(actionButtons)
- }
-}
-
@Preview(showBackground = true)
@Composable
private fun ActionButtonPagePreview() {
SettingsTheme {
- ActionButtonPage()
+ ActionButtonPageProvider.Page(null)
}
}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
index 5d26b34..0878fc0 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
@@ -18,6 +18,7 @@
import android.os.Bundle
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsPageProvider
@@ -28,6 +29,7 @@
import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
import com.android.settingslib.spa.gallery.dialog.AlterDialogPageProvider
+import com.android.settingslib.spa.gallery.itemList.OperateListPageProvider
import com.android.settingslib.spa.gallery.page.ArgumentPageModel
import com.android.settingslib.spa.gallery.page.ArgumentPageProvider
import com.android.settingslib.spa.gallery.page.ChartPageProvider
@@ -50,6 +52,7 @@
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
return listOf(
PreferenceMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
+ OperateListPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
ArgumentPageProvider.buildInjectEntry("foo")!!.setLink(fromPage = owner).build(),
SliderPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
SpinnerPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
@@ -71,8 +74,10 @@
@Composable
override fun Page(arguments: Bundle?) {
- HomeScaffold(title = getTitle(arguments)) {
- for (entry in buildEntry(arguments)) {
+ val title = remember { getTitle(arguments) }
+ val entries = remember { buildEntry(arguments) }
+ HomeScaffold(title) {
+ for (entry in entries) {
if (entry.owner.isCreateBy(SettingsPageProviderEnum.ARGUMENT.name)) {
entry.UiLayout(ArgumentPageModel.buildArgument(intParam = 0))
} else {
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemListPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemListPage.kt
new file mode 100644
index 0000000..08e6452
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemListPage.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.gallery.itemList
+
+import android.os.Bundle
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.core.os.bundleOf
+import androidx.navigation.NavType
+import androidx.navigation.navArgument
+import com.android.settingslib.spa.framework.common.EntrySearchData
+import com.android.settingslib.spa.framework.common.SettingsEntry
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.util.getStringArg
+import com.android.settingslib.spa.framework.util.navLink
+import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+
+private const val OPERATOR_PARAM_NAME = "opParam"
+
+object ItemListPageProvider : SettingsPageProvider {
+ override val name = SettingsPageProviderEnum.ITEM_LIST.name
+ override val displayName = SettingsPageProviderEnum.ITEM_LIST.displayName
+ override val parameter = listOf(
+ navArgument(OPERATOR_PARAM_NAME) { type = NavType.StringType },
+ )
+
+ override fun getTitle(arguments: Bundle?): String {
+ val operation = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments) ?: "NULL"
+ return "Operation: $operation"
+ }
+
+ override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
+ if (!ItemOperatePageProvider.isValidArgs(arguments)) return emptyList()
+ val operation = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments)!!
+ val owner = createSettingsPage(arguments)
+ return listOf(
+ ItemOperatePageProvider.buildInjectEntry(operation)!!.setLink(fromPage = owner).build(),
+ )
+ }
+
+ fun buildInjectEntry(opParam: String): SettingsEntryBuilder? {
+ val arguments = bundleOf(OPERATOR_PARAM_NAME to opParam)
+ if (!ItemOperatePageProvider.isValidArgs(arguments)) return null
+
+ return SettingsEntryBuilder.createInject(
+ owner = createSettingsPage(arguments),
+ displayName = "ItemList_$opParam",
+ ).setUiLayoutFn {
+ Preference(
+ object : PreferenceModel {
+ override val title = opParam
+ override val onClick = navigator(
+ SettingsPageProviderEnum.ITEM_LIST.name + parameter.navLink(it)
+ )
+ }
+ )
+ }.setSearchDataFn {
+ EntrySearchData(title = "Operation: $opParam")
+ }
+ }
+
+ @Composable
+ override fun Page(arguments: Bundle?) {
+ val title = remember { getTitle(arguments) }
+ val entries = remember { buildEntry(arguments) }
+ val itemList = remember {
+ // Add logic to get item List during runtime.
+ listOf("itemFoo", "itemBar", "itemToy")
+ }
+ RegularScaffold(title) {
+ for (item in itemList) {
+ val rtArgs = ItemOperatePageProvider.genRuntimeArguments(item)
+ for (entry in entries) {
+ entry.UiLayout(rtArgs)
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemOperatePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemOperatePage.kt
new file mode 100644
index 0000000..8179356
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemOperatePage.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.gallery.itemList
+
+import android.os.Bundle
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.core.os.bundleOf
+import androidx.navigation.NavType
+import androidx.navigation.navArgument
+import com.android.settingslib.spa.framework.common.SettingsEntry
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.util.getStringArg
+import com.android.settingslib.spa.framework.util.navLink
+import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.preference.SwitchPreference
+import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
+
+private const val OPERATOR_PARAM_NAME = "opParam"
+private const val ITEM_NAME_PARAM_NAME = "rt_nameParam"
+private val ALLOWED_OPERATOR_LIST = listOf("opDnD", "opPiP", "opInstall", "opConnect")
+
+object ItemOperatePageProvider : SettingsPageProvider {
+ override val name = SettingsPageProviderEnum.ITEM_OP_PAGE.name
+ override val displayName = SettingsPageProviderEnum.ITEM_OP_PAGE.displayName
+ override val parameter = listOf(
+ navArgument(OPERATOR_PARAM_NAME) { type = NavType.StringType },
+ navArgument(ITEM_NAME_PARAM_NAME) { type = NavType.StringType },
+ )
+
+ override fun getTitle(arguments: Bundle?): String {
+ // Operation name is not a runtime parameter, which should always available
+ val operation = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments) ?: "opInValid"
+ // Item name is a runtime parameter, which could be missing
+ val itemName = parameter.getStringArg(ITEM_NAME_PARAM_NAME, arguments) ?: "[unset]"
+ return "$operation on $itemName"
+ }
+
+ override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
+ if (!isValidArgs(arguments)) return emptyList()
+
+ val owner = createSettingsPage(arguments)
+ val entryList = mutableListOf<SettingsEntry>()
+ entryList.add(
+ SettingsEntryBuilder.create("ItemName", owner)
+ .setUiLayoutFn {
+ // Item name is a runtime parameter, which needs to be read inside UiLayoutFn
+ val itemName = parameter.getStringArg(ITEM_NAME_PARAM_NAME, it) ?: "NULL"
+ Preference(
+ object : PreferenceModel {
+ override val title = "Item $itemName"
+ }
+ )
+ }.build()
+ )
+
+ // Operation name is not a runtime parameter, which can be read outside.
+ val opName = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments)!!
+ entryList.add(
+ SettingsEntryBuilder.create("ItemOp", owner)
+ .setUiLayoutFn {
+ val checked = rememberSaveable { mutableStateOf(false) }
+ SwitchPreference(remember {
+ object : SwitchPreferenceModel {
+ override val title = "Item operation: $opName"
+ override val checked = checked
+ override val onCheckedChange =
+ { newChecked: Boolean -> checked.value = newChecked }
+ }
+ })
+ }.build(),
+ )
+ return entryList
+ }
+
+ fun buildInjectEntry(opParam: String): SettingsEntryBuilder? {
+ val arguments = bundleOf(OPERATOR_PARAM_NAME to opParam)
+ if (!isValidArgs(arguments)) return null
+
+ return SettingsEntryBuilder.createInject(
+ owner = createSettingsPage(arguments),
+ displayName = "ItemOp_$opParam",
+ ).setUiLayoutFn {
+ // Item name is a runtime parameter, which needs to be read inside UiLayoutFn
+ val itemName = parameter.getStringArg(ITEM_NAME_PARAM_NAME, it) ?: "NULL"
+ Preference(
+ object : PreferenceModel {
+ override val title = "item: $itemName"
+ override val onClick = navigator(
+ SettingsPageProviderEnum.ITEM_OP_PAGE.name + parameter.navLink(it)
+ )
+ }
+ )
+ }
+ }
+
+ fun isValidArgs(arguments: Bundle?): Boolean {
+ val opParam = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments)
+ return (opParam != null && ALLOWED_OPERATOR_LIST.contains(opParam))
+ }
+
+ fun genRuntimeArguments(itemName: String): Bundle {
+ return bundleOf(ITEM_NAME_PARAM_NAME to itemName)
+ }
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/OperateListPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/OperateListPage.kt
new file mode 100644
index 0000000..e0baf86
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/OperateListPage.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.gallery.itemList
+
+import android.os.Bundle
+import com.android.settingslib.spa.framework.common.SettingsEntry
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+
+private const val TITLE = "Operate List Main"
+
+object OperateListPageProvider : SettingsPageProvider {
+ override val name = "OpList"
+ private val owner = createSettingsPage()
+
+ override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
+ return listOf(
+ ItemListPageProvider.buildInjectEntry("opPiP")!!.setLink(fromPage = owner).build(),
+ ItemListPageProvider.buildInjectEntry("opInstall")!!.setLink(fromPage = owner).build(),
+ ItemListPageProvider.buildInjectEntry("opDnD")!!.setLink(fromPage = owner).build(),
+ ItemListPageProvider.buildInjectEntry("opConnect")!!.setLink(fromPage = owner).build(),
+ )
+ }
+
+ override fun getTitle(arguments: Bundle?): String {
+ return TITLE
+ }
+
+ fun buildInjectEntry(): SettingsEntryBuilder {
+ return SettingsEntryBuilder.createInject(owner = owner)
+ .setUiLayoutFn {
+ Preference(object : PreferenceModel {
+ override val title = TITLE
+ override val onClick = navigator(name)
+ })
+ }
+ }
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt
index 42ac1ac..eca47b6 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt
@@ -18,6 +18,7 @@
import android.os.Bundle
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
@@ -101,10 +102,13 @@
@Composable
override fun Page(arguments: Bundle?) {
- RegularScaffold(title = getTitle(arguments)) {
- for (entry in buildEntry(arguments)) {
+ val title = remember { getTitle(arguments) }
+ val entries = remember { buildEntry(arguments) }
+ val rtArgNext = remember { ArgumentPageModel.buildNextArgument(arguments) }
+ RegularScaffold(title) {
+ for (entry in entries) {
if (entry.toPage != null) {
- entry.UiLayout(ArgumentPageModel.buildNextArgument(arguments))
+ entry.UiLayout(rtArgNext)
} else {
entry.UiLayout()
}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt
index 5f15865..5d6aa03 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt
@@ -132,7 +132,7 @@
override val title = PAGE_TITLE
override val summary = stateOf(summaryArray.joinToString(", "))
override val onClick = navigator(
- SettingsPageProviderEnum.ARGUMENT.displayName + parameter.navLink(arguments)
+ SettingsPageProviderEnum.ARGUMENT.name + parameter.navLink(arguments)
)
}
}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ChartPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ChartPage.kt
index 7f21a4d..69c4705 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ChartPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ChartPage.kt
@@ -21,8 +21,8 @@
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.widget.chart.BarChart
@@ -36,7 +36,6 @@
import com.android.settingslib.spa.widget.chart.PieChartModel
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
-import com.android.settingslib.spa.widget.scaffold.RegularScaffold
import com.github.mikephil.charting.formatter.IAxisValueFormatter
import java.text.NumberFormat
@@ -47,9 +46,13 @@
object ChartPageProvider : SettingsPageProvider {
override val name = "Chart"
+ private val owner = createSettingsPage()
+
+ override fun getTitle(arguments: Bundle?): String {
+ return TITLE
+ }
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
- val owner = SettingsPage.create(name)
val entryList = mutableListOf<SettingsEntry>()
entryList.add(
SettingsEntryBuilder.create("Line Chart", owner)
@@ -133,7 +136,7 @@
}
fun buildInjectEntry(): SettingsEntryBuilder {
- return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+ return SettingsEntryBuilder.createInject(owner)
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = TITLE
@@ -141,15 +144,6 @@
})
}
}
-
- @Composable
- override fun Page(arguments: Bundle?) {
- RegularScaffold(title = TITLE) {
- for (entry in buildEntry(arguments)) {
- entry.UiLayout(arguments)
- }
- }
- }
}
@Preview(showBackground = true)
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt
index 9f24ea9..9c7e0ce 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt
@@ -23,8 +23,8 @@
import com.android.settingslib.spa.framework.common.EntrySearchData
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.framework.theme.SettingsTheme
@@ -37,9 +37,9 @@
object FooterPageProvider : SettingsPageProvider {
override val name = "Footer"
+ private val owner = createSettingsPage()
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
- val owner = SettingsPage.create(name)
val entryList = mutableListOf<SettingsEntry>()
entryList.add(
SettingsEntryBuilder.create( "Some Preference", owner)
@@ -58,7 +58,7 @@
}
fun buildInjectEntry(): SettingsEntryBuilder {
- return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+ return SettingsEntryBuilder.createInject(owner)
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = TITLE
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt
index 44f0343..ee22b96 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt
@@ -21,8 +21,8 @@
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.gallery.R
@@ -36,9 +36,9 @@
object IllustrationPageProvider : SettingsPageProvider {
override val name = "Illustration"
+ private val owner = createSettingsPage()
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
- val owner = SettingsPage.create(name)
val entryList = mutableListOf<SettingsEntry>()
entryList.add(
SettingsEntryBuilder.create( "Lottie Illustration", owner)
@@ -71,7 +71,7 @@
}
fun buildInjectEntry(): SettingsEntryBuilder {
- return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+ return SettingsEntryBuilder.createInject(owner)
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = TITLE
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPage.kt
index 4332a81..247990c 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPage.kt
@@ -29,8 +29,8 @@
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.widget.preference.Preference
@@ -45,7 +45,7 @@
override val name = "LoadingBar"
fun buildInjectEntry(): SettingsEntryBuilder {
- return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+ return SettingsEntryBuilder.createInject(owner = createSettingsPage())
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = TITLE
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt
index 20d90dd..9026a24 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt
@@ -28,8 +28,8 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.widget.preference.Preference
@@ -47,7 +47,7 @@
override val name = "ProgressBar"
fun buildInjectEntry(): SettingsEntryBuilder {
- return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+ return SettingsEntryBuilder.createInject(owner = createSettingsPage())
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = TITLE
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
index c0d0abc..dc45e6d 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
@@ -23,8 +23,8 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.widget.preference.Preference
@@ -39,7 +39,7 @@
override val name = "SettingsPager"
fun buildInjectEntry(): SettingsEntryBuilder {
- return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+ return SettingsEntryBuilder.createInject(owner = createSettingsPage())
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = TITLE
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt
index a62ec7b..1051549 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt
@@ -29,8 +29,8 @@
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.widget.preference.SliderPreference
@@ -42,9 +42,9 @@
object SliderPageProvider : SettingsPageProvider {
override val name = "Slider"
+ private val owner = createSettingsPage()
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
- val owner = SettingsPage.create(name)
val entryList = mutableListOf<SettingsEntry>()
entryList.add(
SettingsEntryBuilder.create("Simple Slider", owner)
@@ -104,7 +104,7 @@
}
fun buildInjectEntry(): SettingsEntryBuilder {
- return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+ return SettingsEntryBuilder.createInject(owner)
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = TITLE
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt
index 67e35dc..442ace8 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt
@@ -24,8 +24,8 @@
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.framework.theme.SettingsTheme
@@ -38,9 +38,9 @@
object MainSwitchPreferencePageProvider : SettingsPageProvider {
override val name = "MainSwitchPreference"
+ private val owner = createSettingsPage()
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
- val owner = SettingsPage.create(name)
val entryList = mutableListOf<SettingsEntry>()
entryList.add(
SettingsEntryBuilder.create( "MainSwitchPreference", owner)
@@ -59,7 +59,7 @@
}
fun buildInjectEntry(): SettingsEntryBuilder {
- return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+ return SettingsEntryBuilder.createInject(owner)
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = TITLE
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt
index 067911c..b67e066 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt
@@ -27,8 +27,8 @@
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.framework.theme.SettingsTheme
@@ -43,9 +43,9 @@
object SwitchPreferencePageProvider : SettingsPageProvider {
override val name = "SwitchPreference"
+ private val owner = createSettingsPage()
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
- val owner = SettingsPage.create(name)
val entryList = mutableListOf<SettingsEntry>()
entryList.add(
SettingsEntryBuilder.create( "SwitchPreference", owner)
@@ -82,7 +82,7 @@
}
fun buildInjectEntry(): SettingsEntryBuilder {
- return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+ return SettingsEntryBuilder.createInject(owner)
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = TITLE
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt
index 33e5e8d..a2cd283 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt
@@ -25,8 +25,8 @@
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.framework.theme.SettingsTheme
@@ -40,9 +40,9 @@
object TwoTargetSwitchPreferencePageProvider : SettingsPageProvider {
override val name = "TwoTargetSwitchPreference"
+ private val owner = createSettingsPage()
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
- val owner = SettingsPage.create(name)
val entryList = mutableListOf<SettingsEntry>()
entryList.add(
SettingsEntryBuilder.create( "TwoTargetSwitchPreference", owner)
@@ -73,7 +73,7 @@
}
fun buildInjectEntry(): SettingsEntryBuilder {
- return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+ return SettingsEntryBuilder.createInject(owner)
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = TITLE
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt
index cb58abf6..7a1fad0 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt
@@ -18,16 +18,17 @@
import android.os.Bundle
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.common.EntrySearchData
+import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
-import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.preference.SimplePreferenceMacro
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
import com.android.settingslib.spa.widget.ui.Category
import com.android.settingslib.spa.widget.ui.CategoryTitle
@@ -36,50 +37,60 @@
object CategoryPageProvider : SettingsPageProvider {
override val name = "Category"
+ private val owner = createSettingsPage()
fun buildInjectEntry(): SettingsEntryBuilder {
- return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+ return SettingsEntryBuilder.createInject(owner)
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = TITLE
override val onClick = navigator(name)
})
}
+ .setSearchDataFn { EntrySearchData(title = TITLE) }
}
override fun getTitle(arguments: Bundle?): String {
return TITLE
}
+ override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
+ val entryList = mutableListOf<SettingsEntry>()
+ entryList.add(
+ SettingsEntryBuilder.create("Preference 1", owner)
+ .setMacro { SimplePreferenceMacro(title = "Preference 1", summary = "Summary 1") }
+ .build()
+ )
+ entryList.add(
+ SettingsEntryBuilder.create("Preference 2", owner)
+ .setMacro { SimplePreferenceMacro(title = "Preference 2", summary = "Summary 2") }
+ .build()
+ )
+ entryList.add(
+ SettingsEntryBuilder.create("Preference 3", owner)
+ .setMacro { SimplePreferenceMacro(title = "Preference 2", summary = "Summary 3") }
+ .build()
+
+ )
+ entryList.add(
+ SettingsEntryBuilder.create("Preference 4", owner)
+ .setMacro { SimplePreferenceMacro(title = "Preference 4", summary = "Summary 4") }
+ .build()
+ )
+ return entryList
+ }
+
@Composable
override fun Page(arguments: Bundle?) {
+ val entries = buildEntry(arguments)
RegularScaffold(title = getTitle(arguments)) {
CategoryTitle("Category A")
- Preference(remember {
- object : PreferenceModel {
- override val title = "Preference 1"
- override val summary = stateOf("Summary 1")
- }
- })
- Preference(remember {
- object : PreferenceModel {
- override val title = "Preference 2"
- override val summary = stateOf("Summary 2")
- }
- })
+ entries[0].UiLayout()
+ entries[1].UiLayout()
+
Category("Category B") {
- Preference(remember {
- object : PreferenceModel {
- override val title = "Preference 3"
- override val summary = stateOf("Summary 3")
- }
- })
- Preference(remember {
- object : PreferenceModel {
- override val title = "Preference 4"
- override val summary = stateOf("Summary 4")
- }
- })
+ entries[2].UiLayout()
+ entries[3].UiLayout()
}
}
}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt
index 8fdc22f..aeba6ea 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt
@@ -26,8 +26,8 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.widget.preference.Preference
@@ -42,7 +42,7 @@
override val name = "Spinner"
fun buildInjectEntry(): SettingsEntryBuilder {
- return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+ return SettingsEntryBuilder.createInject(owner = createSettingsPage())
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = TITLE
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugProvider.kt
index 838c0cf..494e3cc 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugProvider.kt
@@ -161,7 +161,7 @@
.add(ColumnEnum.PAGE_ROUTE.id, page.buildRoute())
.add(ColumnEnum.PAGE_INTENT_URI.id, intent.toUri(URI_INTENT_SCHEME))
.add(ColumnEnum.PAGE_ENTRY_COUNT.id, pageWithEntry.entries.size)
- .add(ColumnEnum.HAS_RUNTIME_PARAM.id, if (page.hasRuntimeParam()) 1 else 0)
+ .add(ColumnEnum.PAGE_BROWSABLE.id, if (page.isBrowsable()) 1 else 0)
}
return cursor
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/ProviderColumn.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/ProviderColumn.kt
index bb9a134..fc6160e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/ProviderColumn.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/ProviderColumn.kt
@@ -28,7 +28,7 @@
PAGE_ROUTE("pageRoute"),
PAGE_INTENT_URI("pageIntent"),
PAGE_ENTRY_COUNT("entryCount"),
- HAS_RUNTIME_PARAM("hasRuntimeParam"),
+ PAGE_BROWSABLE("pageBrowsable"),
PAGE_START_ADB("pageStartAdb"),
// Columns related to entry
@@ -67,7 +67,7 @@
ColumnEnum.PAGE_ROUTE,
ColumnEnum.PAGE_INTENT_URI,
ColumnEnum.PAGE_ENTRY_COUNT,
- ColumnEnum.HAS_RUNTIME_PARAM,
+ ColumnEnum.PAGE_BROWSABLE,
)
),
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
index a8432d6..f1b1abd 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
@@ -39,7 +39,7 @@
import androidx.navigation.NavGraph.Companion.findStartDestination
import com.android.settingslib.spa.R
import com.android.settingslib.spa.framework.common.LogCategory
-import com.android.settingslib.spa.framework.common.SettingsPage
+import com.android.settingslib.spa.framework.common.NullPageProvider
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.common.SettingsPageProviderRepository
import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
@@ -106,14 +106,13 @@
@Composable
private fun NavControllerWrapperImpl.NavContent(allProvider: Collection<SettingsPageProvider>) {
- val nullPage = SettingsPage.createNull()
AnimatedNavHost(
navController = navController,
- startDestination = nullPage.sppName,
+ startDestination = NullPageProvider.name,
) {
val slideEffect = tween<IntOffset>(durationMillis = 300)
val fadeEffect = tween<Float>(durationMillis = 300)
- composable(nullPage.sppName) {}
+ composable(NullPageProvider.name) {}
for (spp in allProvider) {
composable(
route = spp.name + spp.parameter.navRoute(),
@@ -139,9 +138,7 @@
) + fadeOut(animationSpec = fadeEffect)
},
) { navBackStackEntry ->
- val page = remember(navBackStackEntry.arguments) {
- spp.createSettingsPage(navBackStackEntry.arguments)
- }
+ val page = remember { spp.createSettingsPage(navBackStackEntry.arguments) }
page.PageWithEvent()
}
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
index 44714ab..b92729d 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
@@ -25,9 +25,6 @@
import androidx.compose.runtime.remember
import com.android.settingslib.spa.framework.compose.LocalNavController
-private const val INJECT_ENTRY_NAME = "INJECT"
-private const val ROOT_ENTRY_NAME = "ROOT"
-
interface EntryData {
val pageId: String?
get() = null
@@ -163,148 +160,3 @@
}
}
}
-
-/**
- * The helper to build a Settings Entry instance.
- */
-class SettingsEntryBuilder(private val name: String, private val owner: SettingsPage) {
- private var displayName = name
- private var fromPage: SettingsPage? = null
- private var toPage: SettingsPage? = null
-
- // Attributes
- private var isAllowSearch: Boolean = false
- private var isSearchDataDynamic: Boolean = false
- private var hasMutableStatus: Boolean = false
- private var hasSliceSupport: Boolean = false
-
- // Functions
- private var uiLayoutFn: UiLayerRenderer = { }
- private var statusDataFn: StatusDataGetter = { null }
- private var searchDataFn: SearchDataGetter = { null }
- private var sliceDataFn: SliceDataGetter = { _: Uri, _: Bundle? -> null }
-
- fun build(): SettingsEntry {
- val page = fromPage ?: owner
- val isEnabled = page.isEnabled()
- return SettingsEntry(
- id = id(),
- name = name,
- owner = owner,
- displayName = displayName,
-
- // linking data
- fromPage = fromPage,
- toPage = toPage,
-
- // attributes
- // TODO: set isEnabled & (isAllowSearch, hasSliceSupport) separately
- isAllowSearch = isEnabled && isAllowSearch,
- isSearchDataDynamic = isSearchDataDynamic,
- hasMutableStatus = hasMutableStatus,
- hasSliceSupport = isEnabled && hasSliceSupport,
-
- // functions
- statusDataImpl = statusDataFn,
- searchDataImpl = searchDataFn,
- sliceDataImpl = sliceDataFn,
- uiLayoutImpl = uiLayoutFn,
- )
- }
-
- fun setDisplayName(displayName: String): SettingsEntryBuilder {
- this.displayName = displayName
- return this
- }
-
- fun setLink(
- fromPage: SettingsPage? = null,
- toPage: SettingsPage? = null
- ): SettingsEntryBuilder {
- if (fromPage != null) this.fromPage = fromPage
- if (toPage != null) this.toPage = toPage
- return this
- }
-
- fun setIsSearchDataDynamic(isDynamic: Boolean): SettingsEntryBuilder {
- this.isSearchDataDynamic = isDynamic
- return this
- }
-
- fun setHasMutableStatus(hasMutableStatus: Boolean): SettingsEntryBuilder {
- this.hasMutableStatus = hasMutableStatus
- return this
- }
-
- fun setMacro(fn: (arguments: Bundle?) -> EntryMacro): SettingsEntryBuilder {
- setStatusDataFn { fn(it).getStatusData() }
- setSearchDataFn { fn(it).getSearchData() }
- setUiLayoutFn {
- val macro = remember { fn(it) }
- macro.UiLayout()
- }
- return this
- }
-
- fun setStatusDataFn(fn: StatusDataGetter): SettingsEntryBuilder {
- this.statusDataFn = fn
- return this
- }
-
- fun setSearchDataFn(fn: SearchDataGetter): SettingsEntryBuilder {
- this.searchDataFn = fn
- this.isAllowSearch = true
- return this
- }
-
- fun clearSearchDataFn(): SettingsEntryBuilder {
- this.searchDataFn = { null }
- this.isAllowSearch = false
- return this
- }
-
- fun setSliceDataFn(fn: SliceDataGetter): SettingsEntryBuilder {
- this.sliceDataFn = fn
- this.hasSliceSupport = true
- return this
- }
-
- fun setUiLayoutFn(fn: UiLayerRenderer): SettingsEntryBuilder {
- this.uiLayoutFn = fn
- return this
- }
-
- // The unique id of this entry, which is computed by name + owner + fromPage + toPage.
- private fun id(): String {
- return "$name:${owner.id}(${fromPage?.id}-${toPage?.id})".toHashId()
- }
-
- companion object {
- fun create(entryName: String, owner: SettingsPage): SettingsEntryBuilder {
- return SettingsEntryBuilder(entryName, owner)
- }
-
- fun createLinkFrom(entryName: String, owner: SettingsPage): SettingsEntryBuilder {
- return create(entryName, owner).setLink(fromPage = owner)
- }
-
- fun createLinkTo(entryName: String, owner: SettingsPage): SettingsEntryBuilder {
- return create(entryName, owner).setLink(toPage = owner)
- }
-
- fun create(owner: SettingsPage, entryName: String, displayName: String? = null):
- SettingsEntryBuilder {
- return SettingsEntryBuilder(entryName, owner).setDisplayName(displayName ?: entryName)
- }
-
- fun createInject(owner: SettingsPage, displayName: String? = null): SettingsEntryBuilder {
- val name = displayName ?: "${INJECT_ENTRY_NAME}_${owner.displayName}"
- return createLinkTo(INJECT_ENTRY_NAME, owner).setDisplayName(name)
- }
-
- fun createRoot(owner: SettingsPage, displayName: String? = null): SettingsEntryBuilder {
- val name = displayName ?: "${ROOT_ENTRY_NAME}_${owner.displayName}"
- return createLinkTo(ROOT_ENTRY_NAME, owner).setDisplayName(name)
- }
- }
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryBuilder.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryBuilder.kt
new file mode 100644
index 0000000..67f9ea5
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryBuilder.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.common
+
+import android.net.Uri
+import android.os.Bundle
+import androidx.compose.runtime.remember
+import com.android.settingslib.spa.framework.util.genEntryId
+
+private const val INJECT_ENTRY_NAME = "INJECT"
+private const val ROOT_ENTRY_NAME = "ROOT"
+
+/**
+ * The helper to build a Settings Entry instance.
+ */
+class SettingsEntryBuilder(private val name: String, private val owner: SettingsPage) {
+ private var displayName = name
+ private var fromPage: SettingsPage? = null
+ private var toPage: SettingsPage? = null
+
+ // Attributes
+ private var isAllowSearch: Boolean = false
+ private var isSearchDataDynamic: Boolean = false
+ private var hasMutableStatus: Boolean = false
+ private var hasSliceSupport: Boolean = false
+
+ // Functions
+ private var uiLayoutFn: UiLayerRenderer = { }
+ private var statusDataFn: StatusDataGetter = { null }
+ private var searchDataFn: SearchDataGetter = { null }
+ private var sliceDataFn: SliceDataGetter = { _: Uri, _: Bundle? -> null }
+
+ fun build(): SettingsEntry {
+ val page = fromPage ?: owner
+ val isEnabled = page.isEnabled()
+ return SettingsEntry(
+ id = genEntryId(name, owner, fromPage, toPage),
+ name = name,
+ owner = owner,
+ displayName = displayName,
+
+ // linking data
+ fromPage = fromPage,
+ toPage = toPage,
+
+ // attributes
+ // TODO: set isEnabled & (isAllowSearch, hasSliceSupport) separately
+ isAllowSearch = isEnabled && isAllowSearch,
+ isSearchDataDynamic = isSearchDataDynamic,
+ hasMutableStatus = hasMutableStatus,
+ hasSliceSupport = isEnabled && hasSliceSupport,
+
+ // functions
+ statusDataImpl = statusDataFn,
+ searchDataImpl = searchDataFn,
+ sliceDataImpl = sliceDataFn,
+ uiLayoutImpl = uiLayoutFn,
+ )
+ }
+
+ fun setDisplayName(displayName: String): SettingsEntryBuilder {
+ this.displayName = displayName
+ return this
+ }
+
+ fun setLink(
+ fromPage: SettingsPage? = null,
+ toPage: SettingsPage? = null
+ ): SettingsEntryBuilder {
+ if (fromPage != null) this.fromPage = fromPage
+ if (toPage != null) this.toPage = toPage
+ return this
+ }
+
+ fun setIsSearchDataDynamic(isDynamic: Boolean): SettingsEntryBuilder {
+ this.isSearchDataDynamic = isDynamic
+ return this
+ }
+
+ fun setHasMutableStatus(hasMutableStatus: Boolean): SettingsEntryBuilder {
+ this.hasMutableStatus = hasMutableStatus
+ return this
+ }
+
+ fun setMacro(fn: (arguments: Bundle?) -> EntryMacro): SettingsEntryBuilder {
+ setStatusDataFn { fn(it).getStatusData() }
+ setSearchDataFn { fn(it).getSearchData() }
+ setUiLayoutFn {
+ val macro = remember { fn(it) }
+ macro.UiLayout()
+ }
+ return this
+ }
+
+ fun setStatusDataFn(fn: StatusDataGetter): SettingsEntryBuilder {
+ this.statusDataFn = fn
+ return this
+ }
+
+ fun setSearchDataFn(fn: SearchDataGetter): SettingsEntryBuilder {
+ this.searchDataFn = fn
+ this.isAllowSearch = true
+ return this
+ }
+
+ fun clearSearchDataFn(): SettingsEntryBuilder {
+ this.searchDataFn = { null }
+ this.isAllowSearch = false
+ return this
+ }
+
+ fun setSliceDataFn(fn: SliceDataGetter): SettingsEntryBuilder {
+ this.sliceDataFn = fn
+ this.hasSliceSupport = true
+ return this
+ }
+
+ fun setUiLayoutFn(fn: UiLayerRenderer): SettingsEntryBuilder {
+ this.uiLayoutFn = fn
+ return this
+ }
+
+ companion object {
+ fun create(entryName: String, owner: SettingsPage): SettingsEntryBuilder {
+ return SettingsEntryBuilder(entryName, owner)
+ }
+
+ fun createLinkFrom(entryName: String, owner: SettingsPage): SettingsEntryBuilder {
+ return create(entryName, owner).setLink(fromPage = owner)
+ }
+
+ fun createLinkTo(entryName: String, owner: SettingsPage): SettingsEntryBuilder {
+ return create(entryName, owner).setLink(toPage = owner)
+ }
+
+ fun create(owner: SettingsPage, entryName: String, displayName: String? = null):
+ SettingsEntryBuilder {
+ return SettingsEntryBuilder(entryName, owner).setDisplayName(displayName ?: entryName)
+ }
+
+ fun createInject(owner: SettingsPage, displayName: String? = null): SettingsEntryBuilder {
+ val name = displayName ?: "${INJECT_ENTRY_NAME}_${owner.displayName}"
+ return createLinkTo(INJECT_ENTRY_NAME, owner).setDisplayName(name)
+ }
+
+ fun createRoot(owner: SettingsPage, displayName: String? = null): SettingsEntryBuilder {
+ val name = displayName ?: "${ROOT_ENTRY_NAME}_${owner.displayName}"
+ return createLinkTo(ROOT_ENTRY_NAME, owner).setDisplayName(name)
+ }
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt
index 14b1629..429f97b 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt
@@ -30,14 +30,10 @@
val injectEntry: SettingsEntry,
)
-private fun SettingsPage.getTitle(sppRepository: SettingsPageProviderRepository): String {
- return sppRepository.getProviderOrNull(sppName)!!.getTitle(arguments)
-}
-
/**
* The repository to maintain all Settings entries
*/
-class SettingsEntryRepository(private val sppRepository: SettingsPageProviderRepository) {
+class SettingsEntryRepository(sppRepository: SettingsPageProviderRepository) {
// Map of entry unique Id to entry
private val entryMap: Map<String, SettingsEntry>
@@ -49,7 +45,7 @@
entryMap = mutableMapOf()
pageWithEntryMap = mutableMapOf()
- val nullPage = SettingsPage.createNull()
+ val nullPage = NullPageProvider.createSettingsPage()
val entryQueue = LinkedList<SettingsEntry>()
for (page in sppRepository.getAllRootPages()) {
val rootEntry =
@@ -66,6 +62,9 @@
if (page == null || pageWithEntryMap.containsKey(page.id)) continue
val spp = sppRepository.getProviderOrNull(page.sppName) ?: continue
val newEntries = spp.buildEntry(page.arguments)
+ // The page id could be existed already, if there are 2+ pages go to the same one.
+ // For now, override the previous ones, which means only the last from-page is kept.
+ // TODO: support multiple from-pages if necessary.
pageWithEntryMap[page.id] = SettingsPageWithEntry(
page = page,
entries = newEntries,
@@ -123,7 +122,7 @@
if (it.toPage == null)
defaultTitle
else {
- it.toPage.getTitle(sppRepository)
+ it.toPage.getTitle()
}
}
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
index 8bbeb62..724588f 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
@@ -19,11 +19,9 @@
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.navigation.NamedNavArgument
+import com.android.settingslib.spa.framework.util.genPageId
import com.android.settingslib.spa.framework.util.isRuntimeParam
import com.android.settingslib.spa.framework.util.navLink
-import com.android.settingslib.spa.framework.util.normalize
-
-private const val NULL_PAGE_NAME = "NULL"
/**
* Defines data to identify a Settings page.
@@ -45,10 +43,7 @@
val arguments: Bundle? = null,
) {
companion object {
- fun createNull(): SettingsPage {
- return create(NULL_PAGE_NAME)
- }
-
+ // TODO: cleanup it once all its usage in Settings are switched to Spp.createSettingsPage
fun create(
name: String,
displayName: String? = null,
@@ -56,23 +51,13 @@
arguments: Bundle? = null
): SettingsPage {
return SettingsPage(
- id = id(name, parameter, arguments),
+ id = genPageId(name, parameter, arguments),
sppName = name,
displayName = displayName ?: name,
parameter = parameter,
arguments = arguments
)
}
-
- // The unique id of this page, which is computed by name + normalized(arguments)
- private fun id(
- name: String,
- parameter: List<NamedNavArgument> = emptyList(),
- arguments: Bundle? = null
- ): String {
- val normArguments = parameter.normalize(arguments, eraseRuntimeValues = true)
- return "$name:${normArguments?.toString()}".toHashId()
- }
}
// Returns if this Settings Page is created by the given Spp.
@@ -84,42 +69,24 @@
return sppName + parameter.navLink(arguments)
}
- fun hasRuntimeParam(): Boolean {
- for (navArg in parameter) {
- if (navArg.isRuntimeParam()) return true
- }
- return false
- }
-
fun isBrowsable(): Boolean {
- return !isCreateBy(NULL_PAGE_NAME) && !hasRuntimeParam()
- }
-
- private fun getProvider(): SettingsPageProvider? {
- if (!SpaEnvironmentFactory.isReady()) return null
- val pageProviderRepository by SpaEnvironmentFactory.instance.pageProviderRepository
- return pageProviderRepository.getProviderOrNull(sppName)
+ if (sppName == NullPageProvider.name) return false
+ for (navArg in parameter) {
+ if (navArg.isRuntimeParam()) return false
+ }
+ return true
}
fun isEnabled(): Boolean {
- return getProvider()?.isEnabled(arguments) ?: false
+ return getPageProvider(sppName)?.isEnabled(arguments) ?: false
+ }
+
+ fun getTitle(): String {
+ return getPageProvider(sppName)?.getTitle(arguments) ?: ""
}
@Composable
fun UiLayout() {
- getProvider()?.Page(arguments)
+ getPageProvider(sppName)?.Page(arguments)
}
}
-
-fun SettingsPageProvider.createSettingsPage(arguments: Bundle? = null): SettingsPage {
- return SettingsPage.create(
- name = name,
- displayName = displayName,
- parameter = parameter,
- arguments = arguments
- )
-}
-
-fun String.toHashId(): String {
- return this.hashCode().toUInt().toString(36)
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
index 42e5f7e..18f964e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
@@ -18,9 +18,14 @@
import android.os.Bundle
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.navigation.NamedNavArgument
+import com.android.settingslib.spa.framework.util.genPageId
+import com.android.settingslib.spa.framework.util.normalizeArgList
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+private const val NULL_PAGE_NAME = "NULL"
+
/**
* An SettingsPageProvider which is used to create Settings page instances.
*/
@@ -52,10 +57,33 @@
/** The [Composable] used to render this page. */
@Composable
fun Page(arguments: Bundle?) {
- RegularScaffold(title = getTitle(arguments)) {
- for (entry in buildEntry(arguments)) {
+ val title = remember { getTitle(arguments) }
+ val entries = remember { buildEntry(arguments) }
+ RegularScaffold(title) {
+ for (entry in entries) {
entry.UiLayout()
}
}
}
}
+
+fun SettingsPageProvider.createSettingsPage(arguments: Bundle? = null): SettingsPage {
+ return SettingsPage(
+ id = genPageId(name, parameter, arguments),
+ sppName = name,
+ displayName = displayName + parameter.normalizeArgList(arguments, eraseRuntimeValues = true)
+ .joinToString("") { arg -> "/$arg" },
+ parameter = parameter,
+ arguments = arguments,
+ )
+}
+
+object NullPageProvider : SettingsPageProvider {
+ override val name = NULL_PAGE_NAME
+}
+
+fun getPageProvider(sppName: String): SettingsPageProvider? {
+ if (!SpaEnvironmentFactory.isReady()) return null
+ val pageProviderRepository by SpaEnvironmentFactory.instance.pageProviderRepository
+ return pageProviderRepository.getProviderOrNull(sppName)
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Parameter.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Parameter.kt
index be303f0..f0eeb13 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Parameter.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Parameter.kt
@@ -29,8 +29,16 @@
}
fun List<NamedNavArgument>.navLink(arguments: Bundle? = null): String {
+ return normalizeArgList(arguments).joinToString("") { arg -> "/$arg" }
+}
+
+fun List<NamedNavArgument>.normalizeArgList(
+ arguments: Bundle? = null,
+ eraseRuntimeValues: Boolean = false
+): List<String> {
val argsArray = mutableListOf<String>()
for (navArg in this) {
+ if (eraseRuntimeValues && navArg.isRuntimeParam()) continue
if (arguments == null || !arguments.containsKey(navArg.name)) {
argsArray.add(UNSET_PARAM_VALUE)
continue
@@ -44,7 +52,7 @@
}
}
}
- return argsArray.joinToString("") { arg -> "/$arg" }
+ return argsArray
}
fun List<NamedNavArgument>.normalize(
@@ -55,7 +63,7 @@
val normArgs = Bundle()
for (navArg in this) {
// Erase value of runtime parameters.
- if (navArg.isRuntimeParam() && eraseRuntimeValues) {
+ if (eraseRuntimeValues && navArg.isRuntimeParam()) {
normArgs.putString(navArg.name, null)
continue
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/UniqueId.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/UniqueId.kt
new file mode 100644
index 0000000..3b0ff7d
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/UniqueId.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.util
+
+import android.os.Bundle
+import androidx.navigation.NamedNavArgument
+import com.android.settingslib.spa.framework.common.SettingsPage
+
+fun genPageId(
+ sppName: String,
+ parameter: List<NamedNavArgument> = emptyList(),
+ arguments: Bundle? = null
+): String {
+ val normArguments = parameter.normalize(arguments, eraseRuntimeValues = true)
+ return "$sppName:${normArguments?.toString()}".toHashId()
+}
+
+fun genEntryId(
+ name: String,
+ owner: SettingsPage,
+ fromPage: SettingsPage? = null,
+ toPage: SettingsPage? = null
+): String {
+ return "$name:${owner.id}(${fromPage?.id}-${toPage?.id})".toHashId()
+}
+
+// TODO: implement a better hash function
+private fun String.toHashId(): String {
+ return this.hashCode().toUInt().toString(36)
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
index d6f5328..207c174 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
@@ -30,9 +30,9 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
@@ -74,7 +74,6 @@
return alertDialogPresenter
}
-@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun AlertDialogPresenter.SettingsAlertDialog(
confirmButton: AlertDialogButton?,
@@ -82,15 +81,9 @@
title: String?,
text: @Composable (() -> Unit)?,
) {
- val configuration = LocalConfiguration.current
AlertDialog(
onDismissRequest = ::close,
- modifier = when (configuration.orientation) {
- Configuration.ORIENTATION_LANDSCAPE -> {
- Modifier.width(configuration.screenWidthDp.dp * 0.6f)
- }
- else -> Modifier
- },
+ modifier = Modifier.width(getDialogWidth()),
confirmButton = { confirmButton?.let { Button(it) } },
dismissButton = dismissButton?.let { { Button(it) } },
title = title?.let { { Text(it) } },
@@ -106,6 +99,15 @@
}
@Composable
+private fun getDialogWidth(): Dp {
+ val configuration = LocalConfiguration.current
+ return configuration.screenWidthDp.dp * when (configuration.orientation) {
+ Configuration.ORIENTATION_LANDSCAPE -> 0.6f
+ else -> 0.8f
+ }
+}
+
+@Composable
private fun AlertDialogPresenter.Button(button: AlertDialogButton) {
TextButton(
onClick = {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
index 32b283e..62189dc 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
@@ -24,7 +24,12 @@
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.LayoutDirection
import com.android.settingslib.spa.framework.compose.LocalNavController
/** Action that navigates back to last page. */
@@ -50,6 +55,7 @@
Icon(
imageVector = Icons.Outlined.ArrowBack,
contentDescription = contentDescription,
+ modifier = Modifier.autoMirrored(),
)
}
}
@@ -75,3 +81,10 @@
)
}
}
+
+private fun Modifier.autoMirrored() = composed {
+ when (LocalLayoutDirection.current) {
+ LayoutDirection.Rtl -> scale(scaleX = -1f, scaleY = 1f)
+ else -> this
+ }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt
index c0b7464..379b9a7 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt
@@ -19,12 +19,12 @@
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.util.genEntryId
+import com.android.settingslib.spa.framework.util.genPageId
import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
import com.android.settingslib.spa.tests.testutils.SppHome
import com.android.settingslib.spa.tests.testutils.SppLayer1
import com.android.settingslib.spa.tests.testutils.SppLayer2
-import com.android.settingslib.spa.tests.testutils.getUniqueEntryId
-import com.android.settingslib.spa.tests.testutils.getUniquePageId
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -41,18 +41,18 @@
val pageWithEntry = entryRepository.getAllPageWithEntry()
assertThat(pageWithEntry.size).isEqualTo(3)
assertThat(
- entryRepository.getPageWithEntry(getUniquePageId("SppHome"))
+ entryRepository.getPageWithEntry(genPageId("SppHome"))
?.entries?.size
).isEqualTo(1)
assertThat(
- entryRepository.getPageWithEntry(getUniquePageId("SppLayer1"))
+ entryRepository.getPageWithEntry(genPageId("SppLayer1"))
?.entries?.size
).isEqualTo(3)
assertThat(
- entryRepository.getPageWithEntry(getUniquePageId("SppLayer2"))
+ entryRepository.getPageWithEntry(genPageId("SppLayer2"))
?.entries?.size
).isEqualTo(2)
- assertThat(entryRepository.getPageWithEntry(getUniquePageId("SppWithParam"))).isNull()
+ assertThat(entryRepository.getPageWithEntry(genPageId("SppWithParam"))).isNull()
}
@Test
@@ -61,17 +61,17 @@
assertThat(entry.size).isEqualTo(7)
assertThat(
entryRepository.getEntry(
- getUniqueEntryId(
+ genEntryId(
"ROOT",
SppHome.createSettingsPage(),
- SettingsPage.createNull(),
+ NullPageProvider.createSettingsPage(),
SppHome.createSettingsPage(),
)
)
).isNotNull()
assertThat(
entryRepository.getEntry(
- getUniqueEntryId(
+ genEntryId(
"INJECT",
SppLayer1.createSettingsPage(),
SppHome.createSettingsPage(),
@@ -81,7 +81,7 @@
).isNotNull()
assertThat(
entryRepository.getEntry(
- getUniqueEntryId(
+ genEntryId(
"INJECT",
SppLayer2.createSettingsPage(),
SppLayer1.createSettingsPage(),
@@ -91,45 +91,46 @@
).isNotNull()
assertThat(
entryRepository.getEntry(
- getUniqueEntryId("Layer1Entry1", SppLayer1.createSettingsPage())
+ genEntryId("Layer1Entry1", SppLayer1.createSettingsPage())
)
).isNotNull()
assertThat(
entryRepository.getEntry(
- getUniqueEntryId("Layer1Entry2", SppLayer1.createSettingsPage())
+ genEntryId("Layer1Entry2", SppLayer1.createSettingsPage())
)
).isNotNull()
assertThat(
entryRepository.getEntry(
- getUniqueEntryId("Layer2Entry1", SppLayer2.createSettingsPage())
+ genEntryId("Layer2Entry1", SppLayer2.createSettingsPage())
)
).isNotNull()
assertThat(
entryRepository.getEntry(
- getUniqueEntryId("Layer2Entry2", SppLayer2.createSettingsPage())
+ genEntryId("Layer2Entry2", SppLayer2.createSettingsPage())
)
).isNotNull()
}
@Test
fun testGetEntryPath() {
+ SpaEnvironmentFactory.reset(spaEnvironment)
assertThat(
entryRepository.getEntryPathWithDisplayName(
- getUniqueEntryId("Layer2Entry1", SppLayer2.createSettingsPage())
+ genEntryId("Layer2Entry1", SppLayer2.createSettingsPage())
)
).containsExactly("Layer2Entry1", "INJECT_SppLayer2", "INJECT_SppLayer1", "ROOT_SppHome")
.inOrder()
assertThat(
entryRepository.getEntryPathWithTitle(
- getUniqueEntryId("Layer2Entry2", SppLayer2.createSettingsPage()),
+ genEntryId("Layer2Entry2", SppLayer2.createSettingsPage()),
"entryTitle"
)
).containsExactly("entryTitle", "SppLayer2", "TitleLayer1", "TitleHome").inOrder()
assertThat(
entryRepository.getEntryPathWithDisplayName(
- getUniqueEntryId(
+ genEntryId(
"INJECT",
SppLayer1.createSettingsPage(),
SppHome.createSettingsPage(),
@@ -140,7 +141,7 @@
assertThat(
entryRepository.getEntryPathWithTitle(
- getUniqueEntryId(
+ genEntryId(
"INJECT",
SppLayer2.createSettingsPage(),
SppLayer1.createSettingsPage(),
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
index 6de1ae5..5754c9b 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
@@ -23,11 +23,12 @@
import androidx.core.os.bundleOf
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.util.genEntryId
+import com.android.settingslib.spa.framework.util.genPageId
import com.android.settingslib.spa.slice.appendSpaParams
import com.android.settingslib.spa.slice.getEntryId
import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
-import com.android.settingslib.spa.tests.testutils.getUniqueEntryId
-import com.android.settingslib.spa.tests.testutils.getUniquePageId
+import com.android.settingslib.spa.tests.testutils.createSettingsPage
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
@@ -64,9 +65,9 @@
@Test
fun testBuildBasic() {
- val owner = SettingsPage.create("mySpp")
+ val owner = createSettingsPage("mySpp")
val entry = SettingsEntryBuilder.create(owner, "myEntry").build()
- assertThat(entry.id).isEqualTo(getUniqueEntryId("myEntry", owner))
+ assertThat(entry.id).isEqualTo(genEntryId("myEntry", owner))
assertThat(entry.displayName).isEqualTo("myEntry")
assertThat(entry.owner.sppName).isEqualTo("mySpp")
assertThat(entry.owner.displayName).isEqualTo("mySpp")
@@ -80,19 +81,19 @@
@Test
fun testBuildWithLink() {
- val owner = SettingsPage.create("mySpp")
- val fromPage = SettingsPage.create("fromSpp")
- val toPage = SettingsPage.create("toSpp")
+ val owner = createSettingsPage("mySpp")
+ val fromPage = createSettingsPage("fromSpp")
+ val toPage = createSettingsPage("toSpp")
val entryFrom =
SettingsEntryBuilder.createLinkFrom("myEntry", owner).setLink(toPage = toPage).build()
- assertThat(entryFrom.id).isEqualTo(getUniqueEntryId("myEntry", owner, owner, toPage))
+ assertThat(entryFrom.id).isEqualTo(genEntryId("myEntry", owner, owner, toPage))
assertThat(entryFrom.displayName).isEqualTo("myEntry")
assertThat(entryFrom.fromPage!!.sppName).isEqualTo("mySpp")
assertThat(entryFrom.toPage!!.sppName).isEqualTo("toSpp")
val entryTo =
SettingsEntryBuilder.createLinkTo("myEntry", owner).setLink(fromPage = fromPage).build()
- assertThat(entryTo.id).isEqualTo(getUniqueEntryId("myEntry", owner, fromPage, owner))
+ assertThat(entryTo.id).isEqualTo(genEntryId("myEntry", owner, fromPage, owner))
assertThat(entryTo.displayName).isEqualTo("myEntry")
assertThat(entryTo.fromPage!!.sppName).isEqualTo("fromSpp")
assertThat(entryTo.toPage!!.sppName).isEqualTo("mySpp")
@@ -100,10 +101,10 @@
@Test
fun testBuildInject() {
- val owner = SettingsPage.create("mySpp")
+ val owner = createSettingsPage("mySpp")
val entryInject = SettingsEntryBuilder.createInject(owner).build()
assertThat(entryInject.id).isEqualTo(
- getUniqueEntryId(
+ genEntryId(
INJECT_ENTRY_NAME_TEST, owner, toPage = owner
)
)
@@ -114,10 +115,10 @@
@Test
fun testBuildRoot() {
- val owner = SettingsPage.create("mySpp")
+ val owner = createSettingsPage("mySpp")
val entryInject = SettingsEntryBuilder.createRoot(owner, "myRootEntry").build()
assertThat(entryInject.id).isEqualTo(
- getUniqueEntryId(
+ genEntryId(
ROOT_ENTRY_NAME_TEST, owner, toPage = owner
)
)
@@ -129,7 +130,7 @@
@Test
fun testSetAttributes() {
SpaEnvironmentFactory.reset(spaEnvironment)
- val owner = SettingsPage.create("SppHome")
+ val owner = createSettingsPage("SppHome")
val entryBuilder =
SettingsEntryBuilder.create(owner, "myEntry")
.setDisplayName("myEntryDisplay")
@@ -138,7 +139,7 @@
.setSearchDataFn { null }
.setSliceDataFn { _, _ -> null }
val entry = entryBuilder.build()
- assertThat(entry.id).isEqualTo(getUniqueEntryId("myEntry", owner))
+ assertThat(entry.id).isEqualTo(genEntryId("myEntry", owner))
assertThat(entry.displayName).isEqualTo("myEntryDisplay")
assertThat(entry.fromPage).isNull()
assertThat(entry.toPage).isNull()
@@ -148,7 +149,7 @@
assertThat(entry.hasSliceSupport).isTrue()
// Test disabled Spp
- val ownerDisabled = SettingsPage.create("SppDisabled")
+ val ownerDisabled = createSettingsPage("SppDisabled")
val entryBuilderDisabled =
SettingsEntryBuilder.create(ownerDisabled, "myEntry")
.setDisplayName("myEntryDisplay")
@@ -157,7 +158,7 @@
.setSearchDataFn { null }
.setSliceDataFn { _, _ -> null }
val entryDisabled = entryBuilderDisabled.build()
- assertThat(entryDisabled.id).isEqualTo(getUniqueEntryId("myEntry", ownerDisabled))
+ assertThat(entryDisabled.id).isEqualTo(genEntryId("myEntry", ownerDisabled))
assertThat(entryDisabled.displayName).isEqualTo("myEntryDisplay")
assertThat(entryDisabled.fromPage).isNull()
assertThat(entryDisabled.toPage).isNull()
@@ -173,7 +174,7 @@
// Clear SppHome in spa environment
SpaEnvironmentFactory.reset()
val entry3 = entryBuilder.build()
- assertThat(entry3.id).isEqualTo(getUniqueEntryId("myEntry", owner))
+ assertThat(entry3.id).isEqualTo(genEntryId("myEntry", owner))
assertThat(entry3.displayName).isEqualTo("myEntryDisplay")
assertThat(entry3.fromPage).isNull()
assertThat(entry3.toPage).isNull()
@@ -186,12 +187,12 @@
@Test
fun testSetMarco() {
SpaEnvironmentFactory.reset(spaEnvironment)
- val owner = SettingsPage.create("SppHome", arguments = bundleOf("param" to "v1"))
+ val owner = createSettingsPage("SppHome", arguments = bundleOf("param" to "v1"))
val entry = SettingsEntryBuilder.create(owner, "myEntry").setMacro {
assertThat(it?.getString("param")).isEqualTo("v1")
assertThat(it?.getString("rtParam")).isEqualTo("v2")
assertThat(it?.getString("unknown")).isNull()
- MacroForTest(getUniquePageId("SppHome"), getUniqueEntryId("myEntry", owner))
+ MacroForTest(genPageId("SppHome"), genEntryId("myEntry", owner))
}.build()
val rtArguments = bundleOf("rtParam" to "v2")
@@ -211,8 +212,8 @@
@Test
fun testSetSliceDataFn() {
SpaEnvironmentFactory.reset(spaEnvironment)
- val owner = SettingsPage.create("SppHome")
- val entryId = getUniqueEntryId("myEntry", owner)
+ val owner = createSettingsPage("SppHome")
+ val entryId = genEntryId("myEntry", owner)
val emptySliceData = EntrySliceData()
val entryBuilder = SettingsEntryBuilder.create(owner, "myEntry").setSliceDataFn { uri, _ ->
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt
index 6c0c652..8576573 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt
@@ -17,6 +17,7 @@
package com.android.settingslib.spa.framework.common
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.tests.testutils.createSettingsPage
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -29,13 +30,14 @@
assertThat(sppRepoEmpty.getDefaultStartPage()).isEqualTo("")
assertThat(sppRepoEmpty.getAllRootPages()).isEmpty()
+ val nullPage = NullPageProvider.createSettingsPage()
val sppRepoNull =
- SettingsPageProviderRepository(emptyList(), listOf(SettingsPage.createNull()))
+ SettingsPageProviderRepository(emptyList(), listOf(nullPage))
assertThat(sppRepoNull.getDefaultStartPage()).isEqualTo("NULL")
- assertThat(sppRepoNull.getAllRootPages()).contains(SettingsPage.createNull())
+ assertThat(sppRepoNull.getAllRootPages()).contains(nullPage)
- val rootPage1 = SettingsPage.create(name = "Spp1", displayName = "Spp1")
- val rootPage2 = SettingsPage.create(name = "Spp2", displayName = "Spp2")
+ val rootPage1 = createSettingsPage(sppName = "Spp1", displayName = "Spp1")
+ val rootPage2 = createSettingsPage(sppName = "Spp2", displayName = "Spp2")
val sppRepo = SettingsPageProviderRepository(emptyList(), listOf(rootPage1, rootPage2))
val allRoots = sppRepo.getAllRootPages()
assertThat(sppRepo.getDefaultStartPage()).isEqualTo("Spp1")
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt
index 1f5de2d..dc74a10 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt
@@ -22,8 +22,9 @@
import androidx.navigation.navArgument
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.util.genPageId
import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
-import com.android.settingslib.spa.tests.testutils.getUniquePageId
+import com.android.settingslib.spa.tests.testutils.createSettingsPage
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -35,27 +36,25 @@
@Test
fun testNullPage() {
- val page = SettingsPage.createNull()
- assertThat(page.id).isEqualTo(getUniquePageId("NULL"))
+ val page = NullPageProvider.createSettingsPage()
+ assertThat(page.id).isEqualTo(genPageId("NULL"))
assertThat(page.sppName).isEqualTo("NULL")
assertThat(page.displayName).isEqualTo("NULL")
assertThat(page.buildRoute()).isEqualTo("NULL")
assertThat(page.isCreateBy("NULL")).isTrue()
assertThat(page.isCreateBy("Spp")).isFalse()
- assertThat(page.hasRuntimeParam()).isFalse()
assertThat(page.isBrowsable()).isFalse()
}
@Test
fun testRegularPage() {
- val page = SettingsPage.create("mySpp", "SppDisplayName")
- assertThat(page.id).isEqualTo(getUniquePageId("mySpp"))
+ val page = createSettingsPage("mySpp", "SppDisplayName")
+ assertThat(page.id).isEqualTo(genPageId("mySpp"))
assertThat(page.sppName).isEqualTo("mySpp")
assertThat(page.displayName).isEqualTo("SppDisplayName")
assertThat(page.buildRoute()).isEqualTo("mySpp")
assertThat(page.isCreateBy("NULL")).isFalse()
assertThat(page.isCreateBy("mySpp")).isTrue()
- assertThat(page.hasRuntimeParam()).isFalse()
assertThat(page.isBrowsable()).isTrue()
}
@@ -67,7 +66,7 @@
)
val page = spaEnvironment.createPage("SppWithParam", arguments)
assertThat(page.id).isEqualTo(
- getUniquePageId(
+ genPageId(
"SppWithParam", listOf(
navArgument("string_param") { type = NavType.StringType },
navArgument("int_param") { type = NavType.IntType },
@@ -75,10 +74,9 @@
)
)
assertThat(page.sppName).isEqualTo("SppWithParam")
- assertThat(page.displayName).isEqualTo("SppWithParam")
+ assertThat(page.displayName).isEqualTo("SppWithParam/myStr/10")
assertThat(page.buildRoute()).isEqualTo("SppWithParam/myStr/10")
assertThat(page.isCreateBy("SppWithParam")).isTrue()
- assertThat(page.hasRuntimeParam()).isFalse()
assertThat(page.isBrowsable()).isTrue()
}
@@ -91,7 +89,7 @@
)
val page = spaEnvironment.createPage("SppWithRtParam", arguments)
assertThat(page.id).isEqualTo(
- getUniquePageId(
+ genPageId(
"SppWithRtParam", listOf(
navArgument("string_param") { type = NavType.StringType },
navArgument("int_param") { type = NavType.IntType },
@@ -100,10 +98,9 @@
)
)
assertThat(page.sppName).isEqualTo("SppWithRtParam")
- assertThat(page.displayName).isEqualTo("SppWithRtParam")
+ assertThat(page.displayName).isEqualTo("SppWithRtParam/myStr/10")
assertThat(page.buildRoute()).isEqualTo("SppWithRtParam/myStr/10/rtStr")
assertThat(page.isCreateBy("SppWithRtParam")).isTrue()
- assertThat(page.hasRuntimeParam()).isTrue()
assertThat(page.isBrowsable()).isFalse()
}
}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/SpaIntentTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/SpaIntentTest.kt
index 1854728..f974cca 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/SpaIntentTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/SpaIntentTest.kt
@@ -19,9 +19,10 @@
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.common.NullPageProvider
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
import com.google.common.truth.Truth
import org.junit.Before
@@ -40,7 +41,7 @@
@Test
fun testCreateIntent() {
- val nullPage = SettingsPage.createNull()
+ val nullPage = NullPageProvider.createSettingsPage()
Truth.assertThat(nullPage.createIntent()).isNull()
Truth.assertThat(SettingsEntryBuilder.createInject(nullPage).build().createIntent())
.isNull()
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/UniqueIdTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/UniqueIdTest.kt
new file mode 100644
index 0000000..c17f83b
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/UniqueIdTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.util
+
+import androidx.core.os.bundleOf
+import androidx.navigation.NavType
+import androidx.navigation.navArgument
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.tests.testutils.createSettingsPage
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class UniqueIdTest {
+ @Test
+ fun testUniquePageId() {
+ Truth.assertThat(genPageId("mySpp")).isEqualTo("1byojwa")
+
+ val parameter = listOf(
+ navArgument("string_param") { type = NavType.StringType },
+ navArgument("int_param") { type = NavType.IntType },
+ )
+ val arguments = bundleOf(
+ "string_param" to "myStr",
+ "int_param" to 10,
+ )
+ Truth.assertThat(genPageId("mySppWithParam", parameter, arguments)).isEqualTo("1sz4pbq")
+
+ val parameter2 = listOf(
+ navArgument("string_param") { type = NavType.StringType },
+ navArgument("int_param") { type = NavType.IntType },
+ navArgument("rt_param") { type = NavType.StringType },
+ )
+ val arguments2 = bundleOf(
+ "string_param" to "myStr",
+ "int_param" to 10,
+ "rt_param" to "myRtStr",
+ )
+ Truth.assertThat(genPageId("mySppWithRtParam", parameter2, arguments2)).isEqualTo("ts6d8k")
+ }
+
+ @Test
+ fun testUniqueEntryId() {
+ val owner = createSettingsPage("mySpp")
+ val fromPage = createSettingsPage("fromSpp")
+ val toPage = createSettingsPage("toSpp")
+
+ Truth.assertThat(genEntryId("myEntry", owner)).isEqualTo("145pppn")
+ Truth.assertThat(genEntryId("myEntry", owner, fromPage = fromPage, toPage = toPage))
+ .isEqualTo("1m7jzew")
+ }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt
index 530d2ed..341a4a5 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt
@@ -25,10 +25,10 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.util.genEntryId
import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
import com.android.settingslib.spa.tests.testutils.SppHome
import com.android.settingslib.spa.tests.testutils.SppLayer2
-import com.android.settingslib.spa.tests.testutils.getUniqueEntryId
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
@@ -52,7 +52,7 @@
// Slice supported
val page = SppLayer2.createSettingsPage()
- val entryId = getUniqueEntryId("Layer2Entry1", page)
+ val entryId = genEntryId("Layer2Entry1", page)
val sliceUri = Uri.Builder().appendSpaParams(page.buildRoute(), entryId).build()
assertThat(sliceUri.getDestination()).isEqualTo("SppLayer2")
assertThat(sliceUri.getSliceId()).isEqualTo("${entryId}_Bundle[{}]")
@@ -61,7 +61,7 @@
assertThat(sliceDataRepository.getOrBuildSliceData(sliceUri)).isSameInstanceAs(sliceData)
// Slice unsupported
- val entryId2 = getUniqueEntryId("Layer2Entry2", page)
+ val entryId2 = genEntryId("Layer2Entry2", page)
val sliceUri2 = Uri.Builder().appendSpaParams(page.buildRoute(), entryId2).build()
assertThat(sliceUri2.getDestination()).isEqualTo("SppLayer2")
assertThat(sliceUri2.getSliceId()).isEqualTo("${entryId2}_Bundle[{}]")
@@ -73,7 +73,7 @@
SpaEnvironmentFactory.reset(spaEnvironment)
val page = SppLayer2.createSettingsPage()
- val entryId = getUniqueEntryId("Layer2Entry1", page)
+ val entryId = genEntryId("Layer2Entry1", page)
val sliceUri = Uri.Builder().appendSpaParams(page.buildRoute(), entryId).build()
// build slice data first
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SettingsPageHelper.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SettingsPageHelper.kt
new file mode 100644
index 0000000..caf4136
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SettingsPageHelper.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.tests.testutils
+
+import android.os.Bundle
+import androidx.navigation.NamedNavArgument
+import com.android.settingslib.spa.framework.common.SettingsPage
+import com.android.settingslib.spa.framework.util.genPageId
+
+fun createSettingsPage(
+ sppName: String,
+ displayName: String? = null,
+ parameter: List<NamedNavArgument> = emptyList(),
+ arguments: Bundle? = null
+): SettingsPage {
+ return SettingsPage(
+ id = genPageId(sppName, parameter, arguments),
+ sppName = sppName,
+ displayName = displayName ?: sppName,
+ parameter = parameter,
+ arguments = arguments
+ )
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/UniqueIdHelper.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/UniqueIdHelper.kt
deleted file mode 100644
index ce9b791..0000000
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/UniqueIdHelper.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.settingslib.spa.tests.testutils
-
-import android.os.Bundle
-import androidx.navigation.NamedNavArgument
-import com.android.settingslib.spa.framework.common.SettingsPage
-import com.android.settingslib.spa.framework.common.toHashId
-import com.android.settingslib.spa.framework.util.normalize
-
-fun getUniquePageId(
- name: String,
- parameter: List<NamedNavArgument> = emptyList(),
- arguments: Bundle? = null
-): String {
- val normArguments = parameter.normalize(arguments, eraseRuntimeValues = true)
- return "$name:${normArguments?.toString()}".toHashId()
-}
-
-fun getUniquePageId(page: SettingsPage): String {
- return getUniquePageId(page.sppName, page.parameter, page.arguments)
-}
-
-fun getUniqueEntryId(
- name: String,
- owner: SettingsPage,
- fromPage: SettingsPage? = null,
- toPage: SettingsPage? = null
-): String {
- val ownerId = getUniquePageId(owner)
- val fromId = if (fromPage == null) "null" else getUniquePageId(fromPage)
- val toId = if (toPage == null) "null" else getUniquePageId(toPage)
- return "$name:$ownerId($fromId-$toId)".toHashId()
-}
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 8508878..adc6a02 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1315,6 +1315,13 @@
<string name="media_transfer_this_device_name" product="tablet">This tablet</string>
<!-- Name of the phone device with an active remote session. [CHAR LIMIT=30] -->
<string name="media_transfer_this_phone">This phone</string>
+ <!-- Sub status indicates device need premium account. [CHAR LIMIT=NONE] -->
+ <string name="media_output_status_require_premium">Upgrade account to switch</string>
+ <!-- Sub status indicates device not support download content. [CHAR LIMIT=NONE] -->
+ <string name="media_output_status_not_support_downloads">Can\’t play downloads here</string>
+ <!-- Sub status indicates device need to wait after ad. [CHAR LIMIT=NONE] -->
+ <string name="media_output_status_try_after_ad">Try again after the ad</string>
+
<!-- Warning message to tell user is have problem during profile connect, it need to turn off device and back on. [CHAR_LIMIT=NONE] -->
<string name="profile_connect_timeout_subtext">Problem connecting. Turn device off & back on</string>
@@ -1626,4 +1633,12 @@
<string name="back_navigation_animation_summary">Enable system animations for predictive back.</string>
<!-- Developer setting: enable animations when a back gesture is executed, full explanation[CHAR LIMIT=NONE] -->
<string name="back_navigation_animation_dialog">This setting enables system animations for predictive gesture animation. It requires setting per-app "enableOnBackInvokedCallback" to true in the manifest file.</string>
+
+ <!-- [CHAR LIMIT=NONE] Messages shown when users press outside of udfps region during -->
+ <string-array name="udfps_accessibility_touch_hints">
+ <item>Move left</item>
+ <item>Move down</item>
+ <item>Move right</item>
+ <item>Move up</item>
+ </string-array>
</resources>
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index 21e7d81..48d449d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -101,6 +101,9 @@
@VisibleForTesting
static ApplicationsState sInstance;
+ // Whether the app icon cache mechanism is enabled or not.
+ private static boolean sAppIconCacheEnabled = false;
+
public static ApplicationsState getInstance(Application app) {
return getInstance(app, AppGlobals.getPackageManager());
}
@@ -115,6 +118,11 @@
}
}
+ /** Set whether the app icon cache mechanism is enabled or not. */
+ public static void setAppIconCacheEnabled(boolean enabled) {
+ sAppIconCacheEnabled = enabled;
+ }
+
final Context mContext;
final PackageManager mPm;
final IPackageManager mIpm;
@@ -776,7 +784,8 @@
}
private static boolean isAppIconCacheEnabled(Context context) {
- return SETTING_PKG.equals(context.getPackageName());
+ return SETTING_PKG.equals(context.getPackageName())
+ || sAppIconCacheEnabled;
}
void rebuildActiveSessions() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/OWNERS b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/OWNERS
new file mode 100644
index 0000000..09e7991
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/OWNERS
@@ -0,0 +1,5 @@
+# Default reviewers for this and subdirectories.
+emilychuang@google.com
+ykhung@google.com
+
+# Emergency approvers in case the above are not available
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java
index 6fb5555..c036fdb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java
@@ -73,6 +73,8 @@
}
@VisibleForTesting
+ // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
+ @SuppressWarnings("NewApi")
int getDrawableResId() {
int resId;
switch (mRouteInfo.getType()) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 2bdbb16..77e514f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -34,10 +34,12 @@
import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED;
+import android.annotation.Nullable;
import android.annotation.TargetApi;
import android.app.Notification;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
+import android.content.ComponentName;
import android.content.Context;
import android.media.MediaRoute2Info;
import android.media.MediaRouter2Manager;
@@ -191,6 +193,14 @@
&& Api34Impl.preferRouteListingOrdering(mRouterManager, mPackageName);
}
+ @Nullable
+ ComponentName getLinkedItemComponentName() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ return null;
+ }
+ return Api34Impl.getLinkedItemComponentName(mRouterManager, mPackageName);
+ }
+
/**
* Remove a {@code device} from current media.
*
@@ -433,6 +443,8 @@
dispatchDeviceListAdded();
}
+ // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
+ @SuppressWarnings("NewApi")
private void buildAllRoutes() {
for (MediaRoute2Info route : mRouterManager.getAllRoutes()) {
if (DEBUG) {
@@ -452,6 +464,8 @@
return infos;
}
+ // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
+ @SuppressWarnings("NewApi")
private synchronized void buildAvailableRoutes() {
for (MediaRoute2Info route : getAvailableRoutes(mPackageName)) {
if (DEBUG) {
@@ -502,6 +516,8 @@
}
}
+ // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
+ @SuppressWarnings("NewApi")
@VisibleForTesting
void addMediaDevice(MediaRoute2Info route) {
//TODO(b/258141461): Attach flag and disable reason in MediaDevice
@@ -625,8 +641,8 @@
List<RouteListingPreference.Item> finalizedItemList = new ArrayList<>();
List<RouteListingPreference.Item> itemList = routeListingPreference.getItems();
for (RouteListingPreference.Item item : itemList) {
- //Put suggested devices on the top first before further organization
- if (item.getFlags() == RouteListingPreference.Item.FLAG_SUGGESTED_ROUTE) {
+ // Put suggested devices on the top first before further organization
+ if (item.getFlags() == RouteListingPreference.Item.FLAG_SUGGESTED) {
finalizedItemList.add(0, item);
} else {
finalizedItemList.add(item);
@@ -682,6 +698,16 @@
}
@DoNotInline
+ @Nullable
+ static ComponentName getLinkedItemComponentName(
+ MediaRouter2Manager mediaRouter2Manager, String packageName) {
+ RouteListingPreference routeListingPreference =
+ mediaRouter2Manager.getRouteListingPreference(packageName);
+ return routeListingPreference == null ? null
+ : routeListingPreference.getLinkedItemComponentName();
+ }
+
+ @DoNotInline
static void onRouteListingPreferenceUpdated(
String packageName,
RouteListingPreference routeListingPreference,
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index aec1767..24acf8a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -20,6 +20,7 @@
import android.app.Notification;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
+import android.content.ComponentName;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
@@ -217,6 +218,16 @@
}
/**
+ * Returns required component name for system to take the user back to the app by launching an
+ * intent with the returned {@link ComponentName}, using action {@link #ACTION_TRANSFER_MEDIA},
+ * with the extra {@link #EXTRA_ROUTE_ID}.
+ */
+ @Nullable
+ public ComponentName getLinkedItemComponentName() {
+ return mInfoMediaManager.getLinkedItemComponentName();
+ }
+
+ /**
* Start scan connected MediaDevice
*/
public void startScan() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
index d708e57..d242198 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
@@ -30,8 +30,14 @@
import static android.media.MediaRoute2Info.TYPE_USB_HEADSET;
import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES;
import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;
-import static android.media.RouteListingPreference.Item.DISABLE_REASON_NONE;
-import static android.media.RouteListingPreference.Item.FLAG_SUGGESTED_ROUTE;
+import static android.media.RouteListingPreference.Item.FLAG_ONGOING_SESSION;
+import static android.media.RouteListingPreference.Item.FLAG_SUGGESTED;
+import static android.media.RouteListingPreference.Item.SELECTION_BEHAVIOR_TRANSFER;
+import static android.media.RouteListingPreference.Item.SUBTEXT_AD_ROUTING_DISALLOWED;
+import static android.media.RouteListingPreference.Item.SUBTEXT_CUSTOM;
+import static android.media.RouteListingPreference.Item.SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED;
+import static android.media.RouteListingPreference.Item.SUBTEXT_NONE;
+import static android.media.RouteListingPreference.Item.SUBTEXT_SUBSCRIPTION_REQUIRED;
import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED;
@@ -51,6 +57,8 @@
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
+import com.android.settingslib.R;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
@@ -106,6 +114,8 @@
setType(info);
}
+ // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
+ @SuppressWarnings("NewApi")
private void setType(MediaRoute2Info info) {
if (info == null) {
mType = MediaDeviceType.TYPE_BLUETOOTH_DEVICE;
@@ -194,24 +204,56 @@
public abstract String getId();
/**
- * Get disabled reason of device
+ * Get selection behavior of device
*
- * @return disabled reason of device
+ * @return selection behavior of device
*/
- @RouteListingPreference.Item.DisableReason
- public int getDisableReason() {
+ @RouteListingPreference.Item.SubText
+ public int getSelectionBehavior() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && mItem != null
- ? mItem.getDisableReason() : -1;
+ ? mItem.getSelectionBehavior() : SELECTION_BEHAVIOR_TRANSFER;
}
/**
- * Checks if device is has disabled reason
+ * Checks if device is has subtext
*
- * @return true if device has disabled reason
+ * @return true if device has subtext
*/
- public boolean hasDisabledReason() {
+ public boolean hasSubtext() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+ && mItem != null
+ && mItem.getSubText() != SUBTEXT_NONE;
+ }
+
+ /**
+ * Get subtext of device
+ *
+ * @return subtext of device
+ */
+ @RouteListingPreference.Item.SubText
+ public int getSubtext() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && mItem != null
- && mItem.getDisableReason() != DISABLE_REASON_NONE;
+ ? mItem.getSubText() : SUBTEXT_NONE;
+ }
+
+ /**
+ * Returns subtext string for current route.
+ *
+ * @return subtext string for this route
+ */
+ public String getSubtextString() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && mItem != null
+ ? Api34Impl.composeSubtext(mItem, mContext) : null;
+ }
+
+ /**
+ * Checks if device has ongoing shared session, which allow user to join
+ *
+ * @return true if device has ongoing session
+ */
+ public boolean hasOngoingSession() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+ && Api34Impl.hasOngoingSession(mItem);
}
/**
@@ -295,6 +337,8 @@
*
* @return true if the RouteInfo equals TYPE_BLE_HEADSET.
*/
+ // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
+ @SuppressWarnings("NewApi")
public boolean isBLEDevice() {
return mRouteInfo.getType() == TYPE_BLE_HEADSET;
}
@@ -511,7 +555,27 @@
private static class Api34Impl {
@DoNotInline
static boolean isSuggestedDevice(RouteListingPreference.Item item) {
- return item != null && item.getFlags() == FLAG_SUGGESTED_ROUTE;
+ return item != null && (item.getFlags() & FLAG_SUGGESTED) != 0;
+ }
+
+ @DoNotInline
+ static boolean hasOngoingSession(RouteListingPreference.Item item) {
+ return item != null && (item.getFlags() & FLAG_ONGOING_SESSION) != 0;
+ }
+
+ @DoNotInline
+ static String composeSubtext(RouteListingPreference.Item item, Context context) {
+ switch (item.getSubText()) {
+ case SUBTEXT_SUBSCRIPTION_REQUIRED:
+ return context.getString(R.string.media_output_status_require_premium);
+ case SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED:
+ return context.getString(R.string.media_output_status_not_support_downloads);
+ case SUBTEXT_AD_ROUTING_DISALLOWED:
+ return context.getString(R.string.media_output_status_try_after_ad);
+ case SUBTEXT_CUSTOM:
+ return (String) item.getCustomSubtextMessage();
+ }
+ return "";
}
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/OWNERS b/packages/SettingsLib/src/com/android/settingslib/media/OWNERS
index d40f322..3cae39f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/OWNERS
+++ b/packages/SettingsLib/src/com/android/settingslib/media/OWNERS
@@ -1,2 +1,5 @@
# Default reviewers for this and subdirectories.
shaoweishen@google.com
+
+#Android Media - For minor changes and renames only.
+aquilescanta@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
index de16d4a..1c82be9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
@@ -56,6 +56,8 @@
initDeviceRecord();
}
+ // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
+ @SuppressWarnings("NewApi")
@Override
public String getName() {
CharSequence name;
@@ -94,11 +96,15 @@
return mContext.getDrawable(getDrawableResId());
}
+ // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
+ @SuppressWarnings("NewApi")
@VisibleForTesting
int getDrawableResId() {
return mDeviceIconUtil.getIconResIdFromMediaRouteType(mRouteInfo.getType());
}
+ // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
+ @SuppressWarnings("NewApi")
@Override
public String getId() {
String id;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt b/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt
similarity index 97%
rename from packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt
rename to packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt
index 7f3846c..d55a027 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt
@@ -1,4 +1,4 @@
-package com.android.systemui.biometrics
+package com.android.settingslib.udfps
import android.graphics.Rect
import android.view.Surface
diff --git a/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsUtils.java b/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsUtils.java
new file mode 100644
index 0000000..dc8a862
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsUtils.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.udfps;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.util.DisplayUtils;
+import android.util.Log;
+import android.util.RotationUtils;
+import android.view.Display;
+import android.view.DisplayInfo;
+import android.view.MotionEvent;
+import android.view.Surface;
+
+import com.android.settingslib.R;
+
+/** Utility class for working with udfps. */
+public class UdfpsUtils {
+ private static final String TAG = "UdfpsUtils";
+
+ /**
+ * Gets the scale factor representing the user's current resolution / the stable (default)
+ * resolution.
+ *
+ * @param displayInfo The display information.
+ */
+ public float getScaleFactor(DisplayInfo displayInfo) {
+ Display.Mode maxDisplayMode =
+ DisplayUtils.getMaximumResolutionDisplayMode(displayInfo.supportedModes);
+ float scaleFactor =
+ DisplayUtils.getPhysicalPixelDisplaySizeRatio(
+ maxDisplayMode.getPhysicalWidth(),
+ maxDisplayMode.getPhysicalHeight(),
+ displayInfo.getNaturalWidth(),
+ displayInfo.getNaturalHeight()
+ );
+ return (scaleFactor == Float.POSITIVE_INFINITY) ? 1f : scaleFactor;
+ }
+
+ /**
+ * Gets the touch in native coordinates. Map the touch to portrait mode if the device is in
+ * landscape mode.
+ *
+ * @param idx The pointer identifier.
+ * @param event The MotionEvent object containing full information about the event.
+ * @param udfpsOverlayParams The [UdfpsOverlayParams] used.
+ * @return The mapped touch event.
+ */
+ public Point getTouchInNativeCoordinates(int idx, MotionEvent event,
+ UdfpsOverlayParams udfpsOverlayParams) {
+ Point portraitTouch = new Point((int) event.getRawX(idx), (int) event.getRawY(idx));
+ int rot = udfpsOverlayParams.getRotation();
+ if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
+ RotationUtils.rotatePoint(
+ portraitTouch,
+ RotationUtils.deltaRotation(rot, Surface.ROTATION_0),
+ udfpsOverlayParams.getLogicalDisplayWidth(),
+ udfpsOverlayParams.getLogicalDisplayHeight()
+ );
+ }
+
+ // Scale the coordinates to native resolution.
+ float scale = udfpsOverlayParams.getScaleFactor();
+ portraitTouch.x = (int) (portraitTouch.x / scale);
+ portraitTouch.y = (int) (portraitTouch.y / scale);
+ return portraitTouch;
+ }
+
+ /**
+ * This function computes the angle of touch relative to the sensor and maps the angle to a list
+ * of help messages which are announced if accessibility is enabled.
+ *
+ * @return Whether the announcing string is null
+ */
+ public String onTouchOutsideOfSensorArea(boolean touchExplorationEnabled,
+ Context context, int touchX, int touchY, UdfpsOverlayParams udfpsOverlayParams) {
+ if (!touchExplorationEnabled) {
+ return null;
+ }
+
+ String[] touchHints = context.getResources().getStringArray(
+ R.array.udfps_accessibility_touch_hints);
+ if (touchHints.length != 4) {
+ Log.e(TAG, "expected exactly 4 touch hints, got " + touchHints.length + "?");
+ return null;
+ }
+
+ // Scale the coordinates to native resolution.
+ float scale = udfpsOverlayParams.getScaleFactor();
+ float scaledSensorX = udfpsOverlayParams.getSensorBounds().centerX() / scale;
+ float scaledSensorY = udfpsOverlayParams.getSensorBounds().centerY() / scale;
+ String theStr =
+ onTouchOutsideOfSensorAreaImpl(
+ touchHints,
+ touchX,
+ touchY,
+ scaledSensorX,
+ scaledSensorY,
+ udfpsOverlayParams.getRotation()
+ );
+ Log.v(TAG, "Announcing touch outside : $theStr");
+ return theStr;
+ }
+
+ /**
+ * This function computes the angle of touch relative to the sensor and maps the angle to a list
+ * of help messages which are announced if accessibility is enabled.
+ *
+ * There are 4 quadrants of the circle (90 degree arcs)
+ *
+ * [315, 360] && [0, 45) -> touchHints[0] = "Move Fingerprint to the left" [45, 135) ->
+ * touchHints[1] = "Move Fingerprint down" And so on.
+ */
+ private String onTouchOutsideOfSensorAreaImpl(String[] touchHints, float touchX,
+ float touchY, float sensorX, float sensorY, int rotation) {
+ float xRelativeToSensor = touchX - sensorX;
+ // Touch coordinates are with respect to the upper left corner, so reverse
+ // this calculation
+ float yRelativeToSensor = sensorY - touchY;
+ double angleInRad = Math.atan2(yRelativeToSensor, xRelativeToSensor);
+ // If the radians are negative, that means we are counting clockwise.
+ // So we need to add 360 degrees
+ if (angleInRad < 0.0) {
+ angleInRad += 2.0 * Math.PI;
+ }
+ // rad to deg conversion
+ double degrees = Math.toDegrees(angleInRad);
+ double degreesPerBucket = 360.0 / touchHints.length;
+ double halfBucketDegrees = degreesPerBucket / 2.0;
+ // The mapping should be as follows
+ // [315, 360] && [0, 45] -> 0
+ // [45, 135] -> 1
+ int index = (int) ((degrees + halfBucketDegrees) % 360 / degreesPerBucket);
+ index %= touchHints.length;
+
+ // A rotation of 90 degrees corresponds to increasing the index by 1.
+ if (rotation == Surface.ROTATION_90) {
+ index = (index + 1) % touchHints.length;
+ }
+ if (rotation == Surface.ROTATION_270) {
+ index = (index + 3) % touchHints.length;
+ }
+ return touchHints[index];
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
index 2c8aa26..04c1c31 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
@@ -35,6 +35,7 @@
import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothDevice;
+import android.content.ComponentName;
import android.content.Context;
import android.media.MediaRoute2Info;
import android.media.MediaRouter2Manager;
@@ -88,6 +89,8 @@
private MediaManager.MediaDeviceCallback mCallback;
@Mock
private MediaSessionManager mMediaSessionManager;
+ @Mock
+ private ComponentName mComponentName;
private InfoMediaManager mInfoMediaManager;
private Context mContext;
@@ -256,8 +259,10 @@
ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT",
Build.VERSION_CODES.UPSIDE_DOWN_CAKE);
final List<RouteListingPreference.Item> preferenceItemList = new ArrayList<>();
- RouteListingPreference.Item item1 = new RouteListingPreference.Item.Builder(
- TEST_ID_4).setFlags(RouteListingPreference.Item.FLAG_SUGGESTED_ROUTE).build();
+ RouteListingPreference.Item item1 =
+ new RouteListingPreference.Item.Builder(TEST_ID_4)
+ .setFlags(RouteListingPreference.Item.FLAG_SUGGESTED)
+ .build();
RouteListingPreference.Item item2 = new RouteListingPreference.Item.Builder(
TEST_ID_3).build();
preferenceItemList.add(item1);
@@ -372,6 +377,24 @@
assertThat(mInfoMediaManager.preferRouteListingOrdering()).isFalse();
}
+ @Test
+ public void getInAppOnlyItemRoutingReceiver_oldSdkVersion_returnsNull() {
+ assertThat(mInfoMediaManager.getLinkedItemComponentName()).isNull();
+ }
+
+ @Test
+ public void getInAppOnlyItemRoutingReceiver_newSdkVersionWithReceiverExist_returns() {
+ ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT",
+ Build.VERSION_CODES.UPSIDE_DOWN_CAKE);
+ when(mRouterManager.getRouteListingPreference(any())).thenReturn(
+ new RouteListingPreference.Builder().setItems(
+ ImmutableList.of()).setUseSystemOrdering(
+ false).setLinkedItemComponentName(mComponentName).build());
+ mInfoMediaManager.mRouterManager = mRouterManager;
+
+ assertThat(mInfoMediaManager.getLinkedItemComponentName()).isEqualTo(mComponentName);
+ }
+
private List<MediaRoute2Info> getRoutesListWithDuplicatedIds() {
final List<MediaRoute2Info> routes = new ArrayList<>();
final MediaRoute2Info info = mock(MediaRoute2Info.class);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/udfps/UdfpsUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/udfps/UdfpsUtilsTest.java
new file mode 100644
index 0000000..f4f0ef9
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/udfps/UdfpsUtilsTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.udfps;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.Surface;
+
+import com.android.settingslib.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+
+@RunWith(RobolectricTestRunner.class)
+public class UdfpsUtilsTest {
+ @Rule
+ public final MockitoRule rule = MockitoJUnit.rule();
+
+ private Context mContext;
+ private String[] mTouchHints;
+ private UdfpsUtils mUdfpsUtils;
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ mTouchHints = mContext.getResources().getStringArray(
+ R.array.udfps_accessibility_touch_hints);
+ mUdfpsUtils = new UdfpsUtils();
+ }
+
+ @Test
+ public void testTouchOutsideAreaNoRotation() {
+ int rotation = Surface.ROTATION_0;
+ // touch at 0 degrees
+ assertThat(
+ mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+ 0 /* touchX */, 0/* touchY */,
+ new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+ )
+ ).isEqualTo(mTouchHints[0]);
+ // touch at 90 degrees
+ assertThat(
+ mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+ 0 /* touchX */, -1/* touchY */,
+ new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+ )
+ ).isEqualTo(mTouchHints[1]);
+ // touch at 180 degrees
+ assertThat(
+ mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+ -1 /* touchX */, 0/* touchY */,
+ new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+ )
+ ).isEqualTo(mTouchHints[2]);
+ // touch at 270 degrees
+ assertThat(
+ mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+ 0 /* touchX */, 1/* touchY */,
+ new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+ )
+ ).isEqualTo(mTouchHints[3]);
+ }
+
+
+ @Test
+ public void testTouchOutsideAreaNoRotation90Degrees() {
+ int rotation = Surface.ROTATION_90;
+ // touch at 0 degrees -> 90 degrees
+ assertThat(
+ mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+ 0 /* touchX */, 0 /* touchY */,
+ new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+ )
+ ).isEqualTo(mTouchHints[1]);
+ // touch at 90 degrees -> 180 degrees
+ assertThat(
+ mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+ 0 /* touchX */, -1 /* touchY */,
+ new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+ )
+ ).isEqualTo(mTouchHints[2]);
+ // touch at 180 degrees -> 270 degrees
+ assertThat(
+ mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+ -1 /* touchX */, 0 /* touchY */,
+ new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+ )
+ ).isEqualTo(mTouchHints[3]);
+ // touch at 270 degrees -> 0 degrees
+ assertThat(
+ mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+ 0 /* touchX */, 1/* touchY */,
+ new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+ )
+ ).isEqualTo(mTouchHints[0]);
+ }
+
+
+ @Test
+ public void testTouchOutsideAreaNoRotation270Degrees() {
+ int rotation = Surface.ROTATION_270;
+ // touch at 0 degrees -> 270 degrees
+ assertThat(
+ mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+ 0 /* touchX */, 0/* touchY */,
+ new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+ )
+ ).isEqualTo(mTouchHints[3]);
+ // touch at 90 degrees -> 0 degrees
+ assertThat(
+ mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+ 0 /* touchX */, -1/* touchY */,
+ new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+ )
+ ).isEqualTo(mTouchHints[0]);
+ // touch at 180 degrees -> 90 degrees
+ assertThat(
+ mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+ -1 /* touchX */, 0/* touchY */,
+ new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+ )
+ ).isEqualTo(mTouchHints[1]);
+ // touch at 270 degrees -> 180 degrees
+ assertThat(
+ mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+ 0 /* touchX */, 1/* touchY */,
+ new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+ )
+ ).isEqualTo(mTouchHints[2]);
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
index 103512d..21e119a 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
@@ -215,4 +215,20 @@
assertThat(mOnBindListenerAnimationView).isNull();
}
+
+ @Test
+ public void onBindViewHolder_default_shouldNotApplyDynamicColor() {
+ mPreference.onBindViewHolder(mViewHolder);
+
+ assertThat(mPreference.isApplyDynamicColor()).isFalse();
+ }
+
+ @Test
+ public void onBindViewHolder_applyDynamicColor_shouldReturnTrue() {
+ mPreference.applyDynamicColor();
+
+ mPreference.onBindViewHolder(mViewHolder);
+
+ assertThat(mPreference.isApplyDynamicColor()).isTrue();
+ }
}
diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp
index 4a10427..1ac20471 100644
--- a/packages/SettingsProvider/Android.bp
+++ b/packages/SettingsProvider/Android.bp
@@ -33,7 +33,7 @@
static_libs: [
"junit",
"SettingsLibDeviceStateRotationLock",
- "SettingsLibDisplayDensityUtils",
+ "SettingsLibDisplayUtils",
],
platform_apis: true,
certificate: "platform",
@@ -57,7 +57,7 @@
"androidx.test.rules",
"mockito-target-minus-junit4",
"SettingsLibDeviceStateRotationLock",
- "SettingsLibDisplayDensityUtils",
+ "SettingsLibDisplayUtils",
"platform-test-annotations",
"truth-prebuilt",
],
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
index c0818a8..7cd8d70 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
@@ -99,5 +99,7 @@
Settings.Global.Wearable.WEAR_ACTIVITY_AUTO_RESUME_TIMEOUT_MS,
Settings.Global.Wearable.WEAR_ACTIVITY_AUTO_RESUME_TIMEOUT_SET_BY_USER,
Settings.Global.Wearable.DYNAMIC_COLOR_THEME_ENABLED,
+ Settings.Global.HDR_CONVERSION_MODE,
+ Settings.Global.HDR_FORCE_CONVERSION_TYPE,
};
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/LargeScreenSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/LargeScreenSettings.java
new file mode 100644
index 0000000..bdd4869
--- /dev/null
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/LargeScreenSettings.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider.settings.backup;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+
+/** Settings that should not be restored when target device is a large screen
+ * i.e. tablets and foldables in unfolded state
+ */
+public class LargeScreenSettings {
+ private static final float LARGE_SCREEN_MIN_DPS = 600;
+ private static final String LARGE_SCREEN_DO_NOT_RESTORE = "accelerometer_rotation";
+
+ /**
+ * Autorotation setting should not be restored when the target device is a large screen.
+ * (b/243489549)
+ */
+ public static boolean doNotRestoreIfLargeScreenSetting(String key, Context context) {
+ return isLargeScreen(context) && LARGE_SCREEN_DO_NOT_RESTORE.equals(key);
+ }
+
+ // copied from systemui/shared/...Utilities.java
+ // since we don't want to add compile time dependency on sys ui package
+ private static boolean isLargeScreen(Context context) {
+ final WindowManager windowManager = context.getSystemService(WindowManager.class);
+ final Rect bounds = windowManager.getCurrentWindowMetrics().getBounds();
+ float smallestWidth = dpiFromPx(Math.min(bounds.width(), bounds.height()),
+ context.getResources().getConfiguration().densityDpi);
+ return smallestWidth >= LARGE_SCREEN_MIN_DPS;
+ }
+
+ private static float dpiFromPx(float size, int densityDpi) {
+ float densityRatio = (float) densityDpi / DisplayMetrics.DENSITY_DEFAULT;
+ return (size / densityRatio);
+ }
+}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index c133097..bf9e428 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -130,6 +130,7 @@
Settings.Secure.ACTIVE_UNLOCK_ON_FACE_ERRORS,
Settings.Secure.ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO,
Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
+ Settings.Secure.ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS,
Settings.Secure.VR_DISPLAY_MODE,
Settings.Secure.NOTIFICATION_BADGING,
Settings.Secure.NOTIFICATION_DISMISS_RTL,
@@ -227,6 +228,7 @@
Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE,
Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME,
Settings.Secure.CUSTOM_BUGREPORT_HANDLER_APP,
- Settings.Secure.CUSTOM_BUGREPORT_HANDLER_USER
+ Settings.Secure.CUSTOM_BUGREPORT_HANDLER_USER,
+ Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED
};
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index 673a3ab..3ffbabd 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -16,6 +16,9 @@
package android.provider.settings.validators;
+import static android.hardware.display.HdrConversionMode.HDR_CONVERSION_FORCE;
+import static android.hardware.display.HdrConversionMode.HDR_CONVERSION_PASSTHROUGH;
+import static android.hardware.display.HdrConversionMode.HDR_CONVERSION_SYSTEM;
import static android.media.AudioFormat.SURROUND_SOUND_ENCODING;
import static android.provider.settings.validators.SettingsValidators.ANY_INTEGER_VALIDATOR;
import static android.provider.settings.validators.SettingsValidators.ANY_STRING_VALIDATOR;
@@ -26,6 +29,11 @@
import static android.provider.settings.validators.SettingsValidators.PACKAGE_NAME_VALIDATOR;
import static android.provider.settings.validators.SettingsValidators.PERCENTAGE_INTEGER_VALIDATOR;
import static android.view.Display.HdrCapabilities.HDR_TYPES;
+import static android.view.Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION;
+import static android.view.Display.HdrCapabilities.HDR_TYPE_HDR10;
+import static android.view.Display.HdrCapabilities.HDR_TYPE_HDR10_PLUS;
+import static android.view.Display.HdrCapabilities.HDR_TYPE_HLG;
+import static android.view.Display.HdrCapabilities.HDR_TYPE_INVALID;
import android.os.BatteryManager;
import android.provider.Settings.Global;
@@ -346,6 +354,20 @@
VALIDATORS.put(Global.USER_PREFERRED_REFRESH_RATE, NON_NEGATIVE_FLOAT_VALIDATOR);
VALIDATORS.put(Global.USER_PREFERRED_RESOLUTION_HEIGHT, ANY_INTEGER_VALIDATOR);
VALIDATORS.put(Global.USER_PREFERRED_RESOLUTION_WIDTH, ANY_INTEGER_VALIDATOR);
+ VALIDATORS.put(Global.HDR_CONVERSION_MODE, new DiscreteValueValidator(
+ new String[] {
+ String.valueOf(HDR_CONVERSION_PASSTHROUGH),
+ String.valueOf(HDR_CONVERSION_SYSTEM),
+ String.valueOf(HDR_CONVERSION_FORCE)
+ }));
+ VALIDATORS.put(Global.HDR_FORCE_CONVERSION_TYPE, new DiscreteValueValidator(
+ new String[] {
+ String.valueOf(HDR_TYPE_INVALID),
+ String.valueOf(HDR_TYPE_DOLBY_VISION),
+ String.valueOf(HDR_TYPE_HDR10),
+ String.valueOf(HDR_TYPE_HLG),
+ String.valueOf(HDR_TYPE_HDR10_PLUS)
+ }));
VALIDATORS.put(Global.RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO_ENABLED,
new DiscreteValueValidator(new String[]{"0", "1"}));
VALIDATORS.put(Global.Wearable.WET_MODE_ON, BOOLEAN_VALIDATOR);
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 03921e1..f0bc1df 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -189,6 +189,8 @@
VALIDATORS.put(Secure.ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO, ANY_STRING_VALIDATOR);
VALIDATORS.put(Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
ANY_STRING_VALIDATOR);
+ VALIDATORS.put(Secure.ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS,
+ ANY_STRING_VALIDATOR);
VALIDATORS.put(Secure.ASSIST_GESTURE_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.ASSIST_GESTURE_SILENCE_ALERTS_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.ASSIST_GESTURE_WAKE_ENABLED, BOOLEAN_VALIDATOR);
@@ -359,5 +361,6 @@
VALIDATORS.put(Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME, ANY_STRING_VALIDATOR);
VALIDATORS.put(Secure.CUSTOM_BUGREPORT_HANDLER_APP, ANY_STRING_VALIDATOR);
VALIDATORS.put(Secure.CUSTOM_BUGREPORT_HANDLER_USER, ANY_INTEGER_VALIDATOR);
+ VALIDATORS.put(Secure.LOCK_SCREEN_WEATHER_ENABLED, BOOLEAN_VALIDATOR);
}
}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
index 6aa08f2..574fd5a 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
@@ -37,6 +37,7 @@
import android.provider.Settings;
import android.provider.settings.backup.DeviceSpecificSettings;
import android.provider.settings.backup.GlobalSettings;
+import android.provider.settings.backup.LargeScreenSettings;
import android.provider.settings.backup.SecureSettings;
import android.provider.settings.backup.SystemSettings;
import android.provider.settings.validators.GlobalSettingsValidators;
@@ -812,6 +813,12 @@
continue;
}
+ if (LargeScreenSettings.doNotRestoreIfLargeScreenSetting(key, getBaseContext())) {
+ Log.i(TAG, "Skipping restore for setting " + key + " as the target device "
+ + "is a large screen (i.e tablet or foldable in unfolded state)");
+ continue;
+ }
+
String value = null;
boolean hasValueToRestore = false;
if (cachedEntries.indexOfKey(key) >= 0) {
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 25b9906..7aeba95 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -238,6 +238,15 @@
dumpSetting(s, p,
Settings.Global.AUDIO_SAFE_VOLUME_STATE,
GlobalSettingsProto.AUDIO_SAFE_VOLUME_STATE);
+ dumpSetting(s, p,
+ Settings.Global.AUDIO_SAFE_CSD_CURRENT_VALUE,
+ GlobalSettingsProto.AUDIO_SAFE_CSD_CURRENT_VALUE);
+ dumpSetting(s, p,
+ Settings.Global.AUDIO_SAFE_CSD_NEXT_WARNING,
+ GlobalSettingsProto.AUDIO_SAFE_CSD_NEXT_WARNING);
+ dumpSetting(s, p,
+ Settings.Global.AUDIO_SAFE_CSD_DOSE_RECORDS,
+ GlobalSettingsProto.AUDIO_SAFE_CSD_DOSE_RECORDS);
final long autofillToken = p.start(GlobalSettingsProto.AUTOFILL);
dumpSetting(s, p,
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 3782ce4..f1ea482 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -131,6 +131,9 @@
Settings.Global.ART_VERIFIER_VERIFY_DEBUGGABLE,
Settings.Global.ASSISTED_GPS_ENABLED,
Settings.Global.AUDIO_SAFE_VOLUME_STATE,
+ Settings.Global.AUDIO_SAFE_CSD_CURRENT_VALUE,
+ Settings.Global.AUDIO_SAFE_CSD_NEXT_WARNING,
+ Settings.Global.AUDIO_SAFE_CSD_DOSE_RECORDS,
Settings.Global.AUTOFILL_LOGGING_LEVEL,
Settings.Global.AUTOFILL_MAX_PARTITIONS_SIZE,
Settings.Global.AUTOFILL_MAX_VISIBLE_DATASETS,
diff --git a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
index cb37c07..a719d77 100644
--- a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
+++ b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
@@ -677,7 +677,7 @@
if (mScreenshotFd != null) {
writeScreenshotFile(mScreenshotFd, SCREENSHOT_CONTENT);
}
- mIDumpstateListener.onFinished();
+ mIDumpstateListener.onFinished("");
getInstrumentation().waitForIdleSync();
}
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 8e98d2d..64e1bc2 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -418,6 +418,34 @@
android:permission="com.android.systemui.permission.SELF"
android:exported="false" />
+ <activity android:name=".screenshot.AppClipsTrampolineActivity"
+ android:theme="@style/AppClipsTrampolineActivity"
+ android:label="@string/screenshot_preview_description"
+ android:permission="android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE"
+ android:exported="true">
+ <intent-filter android:priority="1">
+ <action android:name="android.intent.action.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".screenshot.AppClipsActivity"
+ android:theme="@style/AppClipsActivity"
+ android:process=":appclips.screenshot"
+ android:label="@string/screenshot_preview_description"
+ android:permission="com.android.systemui.permission.SELF"
+ android:excludeFromRecents="true"
+ android:exported="false"
+ android:noHistory="true" />
+
+ <service android:name=".screenshot.appclips.AppClipsScreenshotHelperService"
+ android:permission="com.android.systemui.permission.SELF"
+ android:exported="false" />
+
+ <service android:name=".screenshot.appclips.AppClipsService"
+ android:permission="android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE"
+ android:exported="true" />
+
<service android:name=".screenrecord.RecordingService"
android:foregroundServiceType="systemExempted"/>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
index 0b1a3e2..2c04630 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
+++ b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
@@ -25,8 +25,17 @@
"androidx.coordinatorlayout_coordinatorlayout",
"androidx.core_core",
"androidx.viewpager_viewpager",
+ "SettingsLibDisplayUtils",
],
+ optimize: {
+ enabled: true,
+ optimize: true,
+ shrink: true,
+ shrink_resources: true,
+ proguard_compatibility: false,
+ },
+
uses_libs: [
"org.apache.http.legacy",
],
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml b/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml
index 26748a9..77412d9 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml
+++ b/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml
@@ -16,11 +16,15 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.android.systemui.accessibility.accessibilitymenu">
- <application>
+
+ <uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS"/>
+
+ <application android:supportsRtl="true">
<service
android:name="com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService"
android:exported="false"
- android:label="Accessibility Menu (System)"
+ android:label="@string/accessibility_menu_service_name"
+ android:icon="@drawable/ic_logo_app"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService"/>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable-sw600dp-night/a11ymenu_intro.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable-sw600dp-night/a11ymenu_intro.xml
new file mode 100644
index 0000000..7a4fbb2
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable-sw600dp-night/a11ymenu_intro.xml
@@ -0,0 +1,276 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="412dp"
+ android:height="300dp"
+ android:viewportWidth="412"
+ android:viewportHeight="300">
+ <group>
+ <clip-path
+ android:pathData="M62.23,50.19L349.77,50.19A16,16 0,0 1,365.77 66.19L365.77,236.14A16,16 0,0 1,349.77 252.14L62.23,252.14A16,16 0,0 1,46.23 236.14L46.23,66.19A16,16 0,0 1,62.23 50.19z"/>
+ <path
+ android:pathData="M62.23,50.19L349.77,50.19A16,16 0,0 1,365.77 66.19L365.77,236.14A16,16 0,0 1,349.77 252.14L62.23,252.14A16,16 0,0 1,46.23 236.14L46.23,66.19A16,16 0,0 1,62.23 50.19z"
+ android:fillColor="#3B4043"/>
+ <path
+ android:pathData="M46.23,52.14h160v200h-160z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M86.21,240.04L86.91,239.34L84.21,236.64H90.11V235.64H84.21L86.91,232.94L86.21,232.24L82.31,236.14L86.21,240.04Z"
+ android:fillColor="#3B4043"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M125.71,220.14V252.14H127.71V220.14H125.71Z"
+ android:fillColor="#3B4043"/>
+ <path
+ android:pathData="M166.21,232.24L165.51,232.94L168.21,235.64H162.31V236.64H168.21L165.51,239.34L166.21,240.04L170.11,236.14L166.21,232.24Z"
+ android:fillColor="#7F868C"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M46.71,221.14H206.71V219.14H46.71V221.14Z"
+ android:fillColor="#3B4043"/>
+ <path
+ android:pathData="M78.71,120.5L78.71,120.5A16,16 0,0 1,94.71 136.5L94.71,136.5A16,16 0,0 1,78.71 152.5L78.71,152.5A16,16 0,0 1,62.71 136.5L62.71,136.5A16,16 0,0 1,78.71 120.5z"
+ android:fillColor="#327969"/>
+ <group>
+ <clip-path
+ android:pathData="M73.36,138.95V134.15H76.56L80.56,130.15V142.85L76.56,138.85H73.36V138.95ZM82.06,133.35C83.26,133.95 84.06,135.15 84.06,136.55C84.06,137.95 83.26,139.15 82.06,139.75V133.35Z"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M88.36,127.05H69.36V146.05H88.36V127.05Z"
+ android:fillColor="#000000"/>
+ </group>
+ <path
+ android:pathData="M78.71,172.5L78.71,172.5A16,16 0,0 1,94.71 188.5L94.71,188.5A16,16 0,0 1,78.71 204.5L78.71,204.5A16,16 0,0 1,62.71 188.5L62.71,188.5A16,16 0,0 1,78.71 172.5z"
+ android:fillColor="#DE9834"/>
+ <path
+ android:pathData="M87.66,188.5L85.06,191.1V194.8H81.36L78.76,197.4L76.16,194.8H72.36V191.1L69.76,188.5L72.36,185.9V182.2H76.06L78.66,179.6L81.26,182.2H84.96V185.9L87.66,188.5ZM73.96,188.5C73.96,191.1 76.06,193.3 78.76,193.3C81.36,193.3 83.56,191.2 83.56,188.5C83.56,185.8 81.46,183.7 78.76,183.7C76.06,183.7 73.96,185.8 73.96,188.5Z"
+ android:fillColor="#000000"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M80.66,187.9H76.66V189.4H80.66V187.9Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M126.71,120.5L126.71,120.5A16,16 0,0 1,142.71 136.5L142.71,136.5A16,16 0,0 1,126.71 152.5L126.71,152.5A16,16 0,0 1,110.71 136.5L110.71,136.5A16,16 0,0 1,126.71 120.5z"
+ android:fillColor="#327969"/>
+ <path
+ android:pathData="M128.31,131.1V129.5C131.51,130.2 133.91,133.1 133.91,136.5C133.91,139.9 131.51,142.7 128.31,143.5V141.9C130.61,141.2 132.31,139.1 132.31,136.6C132.31,134.1 130.61,131.8 128.31,131.1ZM130.31,136.4C130.31,135 129.51,133.8 128.31,133.2V139.6C129.41,139 130.31,137.8 130.31,136.4Z"
+ android:fillColor="#8B3737"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M126.72,142.79V130.13L122.71,134.08H119.5V138.78H122.71L126.72,142.79Z"
+ android:fillColor="#8B3737"/>
+ <group>
+ <clip-path
+ android:pathData="M126.72,142.79V130.13L122.71,134.08H119.5V138.78H122.71L126.72,142.79Z"/>
+ <clip-path
+ android:pathData="M128.31,131.1V129.5C131.51,130.2 133.91,133.1 133.91,136.5C133.91,139.9 131.51,142.7 128.31,143.5V141.9C130.61,141.2 132.31,139.1 132.31,136.6C132.31,134.1 130.61,131.8 128.31,131.1ZM130.31,136.4C130.31,135 129.51,133.8 128.31,133.2V139.6C129.41,139 130.31,137.8 130.31,136.4Z"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M128.31,131.1H78.31V168.15L113.75,178.94L128.31,131.1ZM128.31,129.5L138.99,80.66L78.31,67.38V129.5H128.31ZM128.31,143.5H78.31V207.54L140.44,192.01L128.31,143.5ZM128.31,141.9L113.75,94.07L78.31,104.86V141.9H128.31ZM128.31,133.2L150.67,88.48L78.31,52.3V133.2H128.31ZM128.31,139.6H78.31V223.83L152.25,183.5L128.31,139.6ZM126.72,130.13H176.73V10.79L91.66,94.5L126.72,130.13ZM126.72,142.79L91.37,178.15L176.73,263.5V142.79H126.72ZM122.71,138.78L158.07,103.43L143.43,88.78H122.71V138.78ZM119.5,138.78H69.5V188.78H119.5V138.78ZM119.5,134.08V84.08H69.5V134.08H119.5ZM122.71,134.08V184.08H143.19L157.78,169.72L122.71,134.08ZM178.31,131.1V129.5H78.31V131.1H178.31ZM117.63,178.35C98.03,174.06 83.91,156.76 83.91,136.5H183.91C183.91,109.44 164.99,86.34 138.99,80.66L117.63,178.35ZM83.91,136.5C83.91,115.15 98.93,99.31 116.18,94.99L140.44,192.01C164.09,186.1 183.91,164.66 183.91,136.5H83.91ZM178.31,143.5V141.9H78.31V143.5H178.31ZM142.87,189.74C164.72,183.09 182.31,162.64 182.31,136.6H82.31C82.31,115.57 96.5,99.32 113.75,94.07L142.87,189.74ZM182.31,136.6C182.31,112.64 166.74,90.53 142.87,83.27L113.75,178.94C94.48,173.07 82.31,155.56 82.31,136.6H182.31ZM180.31,136.4C180.31,115.04 167.88,97.08 150.67,88.48L105.95,177.92C91.14,170.52 80.31,154.96 80.31,136.4H180.31ZM78.31,133.2V139.6H178.31V133.2H78.31ZM152.25,183.5C166.09,175.95 180.31,159.6 180.31,136.4H80.31C80.31,116 92.73,102.06 104.37,95.71L152.25,183.5ZM76.72,130.13V142.79H176.73V130.13H76.72ZM162.08,107.44L158.07,103.43L87.36,174.14L91.37,178.15L162.08,107.44ZM122.71,88.78H119.5V188.78H122.71V88.78ZM169.5,138.78V134.08H69.5V138.78H169.5ZM119.5,184.08H122.71V84.08H119.5V184.08ZM157.78,169.72L161.79,165.77L91.66,94.5L87.65,98.44L157.78,169.72Z"
+ android:fillColor="#000000"/>
+ </group>
+ <path
+ android:pathData="M78.71,68.5L78.71,68.5A16,16 0,0 1,94.71 84.5L94.71,84.5A16,16 0,0 1,78.71 100.5L78.71,100.5A16,16 0,0 1,62.71 84.5L62.71,84.5A16,16 0,0 1,78.71 68.5z"
+ android:fillColor="#3B4043"/>
+ <path
+ android:pathData="M86.26,82.15C86.92,82.15 87.46,81.61 87.46,80.95C87.46,80.29 86.92,79.75 86.26,79.75C85.6,79.75 85.06,80.29 85.06,80.95C85.06,81.61 85.6,82.15 86.26,82.15Z"
+ android:fillColor="#34A853"/>
+ <path
+ android:pathData="M82.66,86.15C83.99,86.15 85.06,85.08 85.06,83.75C85.06,82.43 83.99,81.35 82.66,81.35C81.34,81.35 80.26,82.43 80.26,83.75C80.26,85.08 81.34,86.15 82.66,86.15Z"
+ android:fillColor="#EA4335"/>
+ <path
+ android:pathData="M82.66,92.45C84.21,92.45 85.46,91.2 85.46,89.65C85.46,88.11 84.21,86.85 82.66,86.85C81.11,86.85 79.86,88.11 79.86,89.65C79.86,91.2 81.11,92.45 82.66,92.45Z"
+ android:fillColor="#FBBC04"/>
+ <path
+ android:pathData="M74.76,86.15C77.41,86.15 79.56,84 79.56,81.35C79.56,78.7 77.41,76.55 74.76,76.55C72.11,76.55 69.96,78.7 69.96,81.35C69.96,84 72.11,86.15 74.76,86.15Z"
+ android:fillColor="#4285F4"/>
+ <path
+ android:pathData="M174.71,120.5L174.71,120.5A16,16 0,0 1,190.71 136.5L190.71,136.5A16,16 0,0 1,174.71 152.5L174.71,152.5A16,16 0,0 1,158.71 136.5L158.71,136.5A16,16 0,0 1,174.71 120.5z"
+ android:fillColor="#9F3EBF"/>
+ <path
+ android:pathData="M178.53,130.5H171.03V142.2H178.53V130.5Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M170.23,132.8H167.53V140H170.23V132.8Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M182.23,132.8H179.53V140H182.23V132.8Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M126.71,172.5L126.71,172.5A16,16 0,0 1,142.71 188.5L142.71,188.5A16,16 0,0 1,126.71 204.5L126.71,204.5A16,16 0,0 1,110.71 188.5L110.71,188.5A16,16 0,0 1,126.71 172.5z"
+ android:fillColor="#DE9834"/>
+ <path
+ android:pathData="M135.71,188.5L133.11,191.1V194.8H129.31L126.71,197.4L124.11,194.8H120.31V191.1L117.71,188.5L120.31,185.9V182.2H124.01L126.61,179.6L129.21,182.2H132.91V185.9L135.71,188.5ZM121.91,188.5C121.91,191.1 124.01,193.3 126.71,193.3C129.31,193.3 131.51,191.2 131.51,188.5C131.51,185.8 129.41,183.7 126.71,183.7C124.11,183.7 121.91,185.8 121.91,188.5Z"
+ android:fillColor="#000000"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M127.21,187.9V186.4H126.21V187.9H124.71V188.9H126.21V190.4H127.21V188.9H128.71V187.9H127.21Z"
+ android:fillColor="#000000"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M126.71,68.5L126.71,68.5A16,16 0,0 1,142.71 84.5L142.71,84.5A16,16 0,0 1,126.71 100.5L126.71,100.5A16,16 0,0 1,110.71 84.5L110.71,84.5A16,16 0,0 1,126.71 68.5z"
+ android:fillColor="#521BBF"/>
+ <path
+ android:pathData="M128.36,78.51C128.36,79.31 127.66,80.01 126.76,80.01C125.86,80.01 125.16,79.31 125.16,78.51C125.16,77.71 125.86,77.01 126.76,77.01C127.66,76.91 128.36,77.61 128.36,78.51ZM126.76,80.71C128.96,80.71 131.46,80.51 133.46,79.91L133.86,81.41C132.36,81.81 130.66,82.01 129.06,82.21V92.01H127.46V87.51H125.86V92.01H124.36V82.21C122.76,82.11 121.06,81.81 119.56,81.41L119.96,79.91C122.06,80.51 124.46,80.71 126.76,80.71Z"
+ android:fillColor="#000000"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M174.71,172.5L174.71,172.5A16,16 0,0 1,190.71 188.5L190.71,188.5A16,16 0,0 1,174.71 204.5L174.71,204.5A16,16 0,0 1,158.71 188.5L158.71,188.5A16,16 0,0 1,174.71 172.5z"
+ android:fillColor="#438947"/>
+ <path
+ android:pathData="M179.11,185.95H178.41V184.45C178.41,182.45 176.81,180.75 174.71,180.75C172.61,180.75 171.01,182.35 171.01,184.45V185.95H170.31C169.51,185.95 168.81,186.65 168.81,187.45V194.75C168.81,195.55 169.51,196.25 170.31,196.25H179.11C179.91,196.25 180.61,195.55 180.61,194.75V187.45C180.61,186.65 179.91,185.95 179.11,185.95ZM174.71,192.55C173.91,192.55 173.21,191.85 173.21,191.05C173.21,190.25 173.91,189.55 174.71,189.55C175.51,189.55 176.21,190.25 176.21,191.05C176.21,191.95 175.51,192.55 174.71,192.55ZM172.41,185.95H176.91V184.45C176.91,183.15 175.91,182.15 174.61,182.15C173.31,182.15 172.31,183.15 172.31,184.45V185.95H172.41Z"
+ android:fillColor="#000000"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M174.71,68.5L174.71,68.5A16,16 0,0 1,190.71 84.5L190.71,84.5A16,16 0,0 1,174.71 100.5L174.71,100.5A16,16 0,0 1,158.71 84.5L158.71,84.5A16,16 0,0 1,174.71 68.5z"
+ android:fillColor="#80868B"/>
+ <path
+ android:pathData="M173.91,77.35H175.51V85.25H173.91V77.35ZM178.21,80.25L179.31,79.15C180.81,80.45 181.81,82.35 181.81,84.55C181.81,88.45 178.61,91.65 174.71,91.65C170.81,91.65 167.61,88.45 167.61,84.55C167.61,82.35 168.61,80.45 170.11,79.15L171.21,80.25C170.01,81.25 169.21,82.85 169.21,84.55C169.21,87.65 171.71,90.15 174.81,90.15C177.91,90.15 180.41,87.65 180.41,84.55C180.31,82.75 179.41,81.25 178.21,80.25Z"
+ android:fillColor="#000000"
+ android:fillType="evenOdd"/>
+ </group>
+ <path
+ android:pathData="M62.23,51.69L349.77,51.69A14.5,14.5 0,0 1,364.27 66.19L364.27,236.14A14.5,14.5 0,0 1,349.77 250.64L62.23,250.64A14.5,14.5 0,0 1,47.73 236.14L47.73,66.19A14.5,14.5 0,0 1,62.23 51.69z"
+ android:strokeWidth="3"
+ android:fillColor="#00000000"
+ android:strokeColor="#7F868C"/>
+ <path
+ android:pathData="M311.45,50.35C311.45,48.98 312.56,47.87 313.92,47.87L322.84,47.87C324.2,47.87 325.32,48.98 325.32,50.35L319.37,51.34L311.45,50.35Z"
+ android:fillColor="#7F868C"/>
+ <path
+ android:pathData="M263.59,50.35C263.59,48.98 264.7,47.87 266.06,47.87L287.85,47.87C289.22,47.87 290.33,48.98 290.33,50.35L277.45,51.34L263.59,50.35Z"
+ android:fillColor="#7F868C"/>
+ <group>
+ <clip-path
+ android:pathData="M62,50.32L349.54,50.32A16,16 0,0 1,365.54 66.32L365.54,236.27A16,16 0,0 1,349.54 252.27L62,252.27A16,16 0,0 1,46 236.27L46,66.32A16,16 0,0 1,62 50.32z"/>
+ <path
+ android:pathData="M62,50.32L349.54,50.32A16,16 0,0 1,365.54 66.32L365.54,236.27A16,16 0,0 1,349.54 252.27L62,252.27A16,16 0,0 1,46 236.27L46,66.32A16,16 0,0 1,62 50.32z"
+ android:fillColor="#3B4043"/>
+ <path
+ android:pathData="M46,52.27h160v200h-160z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M85.98,240.17L86.68,239.47L83.98,236.77H89.88V235.77H83.98L86.68,233.07L85.98,232.37L82.08,236.27L85.98,240.17Z"
+ android:fillColor="#3B4043"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M125.48,220.27V252.27H127.48V220.27H125.48Z"
+ android:fillColor="#3B4043"/>
+ <path
+ android:pathData="M165.98,232.37L165.28,233.07L167.98,235.77H162.08V236.77H167.98L165.28,239.47L165.98,240.17L169.88,236.27L165.98,232.37Z"
+ android:fillColor="#7F868C"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M46.48,221.27H206.48V219.27H46.48V221.27Z"
+ android:fillColor="#3B4043"/>
+ <path
+ android:pathData="M78.48,120.64L78.48,120.64A16,16 0,0 1,94.48 136.64L94.48,136.64A16,16 0,0 1,78.48 152.64L78.48,152.64A16,16 0,0 1,62.48 136.64L62.48,136.64A16,16 0,0 1,78.48 120.64z"
+ android:fillColor="#7AE2D4"/>
+ <group>
+ <clip-path
+ android:pathData="M73.13,139.09V134.29H76.33L80.33,130.29V142.99L76.33,138.99H73.13V139.09ZM81.83,133.49C83.03,134.09 83.83,135.29 83.83,136.69C83.83,138.09 83.03,139.29 81.83,139.89V133.49Z"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M88.13,127.19H69.13V146.19H88.13V127.19Z"
+ android:fillColor="#000000"/>
+ </group>
+ <path
+ android:pathData="M78.48,172.64L78.48,172.64A16,16 0,0 1,94.48 188.64L94.48,188.64A16,16 0,0 1,78.48 204.64L78.48,204.64A16,16 0,0 1,62.48 188.64L62.48,188.64A16,16 0,0 1,78.48 172.64z"
+ android:fillColor="#FDD663"/>
+ <path
+ android:pathData="M87.43,188.64L84.83,191.24V194.94H81.13L78.53,197.54L75.93,194.94H72.13V191.24L69.53,188.64L72.13,186.04V182.34H75.83L78.43,179.74L81.03,182.34H84.73V186.04L87.43,188.64ZM73.73,188.64C73.73,191.24 75.83,193.44 78.53,193.44C81.13,193.44 83.33,191.34 83.33,188.64C83.33,185.94 81.23,183.84 78.53,183.84C75.83,183.84 73.73,185.94 73.73,188.64Z"
+ android:fillColor="#000000"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M80.43,188.04H76.43V189.54H80.43V188.04Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M126.48,120.64L126.48,120.64A16,16 0,0 1,142.48 136.64L142.48,136.64A16,16 0,0 1,126.48 152.64L126.48,152.64A16,16 0,0 1,110.48 136.64L110.48,136.64A16,16 0,0 1,126.48 120.64z"
+ android:fillColor="#7AE2D4"/>
+ <path
+ android:pathData="M128.08,131.24V129.64C131.28,130.34 133.68,133.24 133.68,136.64C133.68,140.04 131.28,142.84 128.08,143.64V142.04C130.38,141.34 132.08,139.24 132.08,136.74C132.08,134.24 130.38,131.94 128.08,131.24ZM130.08,136.54C130.08,135.14 129.28,133.94 128.08,133.34V139.74C129.18,139.14 130.08,137.94 130.08,136.54Z"
+ android:fillColor="#000000"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M126.49,142.93V130.27L122.49,134.21H119.27V138.92H122.49L126.49,142.93Z"
+ android:fillColor="#000000"/>
+ <group>
+ <clip-path
+ android:pathData="M126.49,142.93V130.27L122.49,134.21H119.27V138.92H122.49L126.49,142.93Z"/>
+ <clip-path
+ android:pathData="M128.08,131.24V129.64C131.28,130.34 133.68,133.24 133.68,136.64C133.68,140.04 131.28,142.84 128.08,143.64V142.04C130.38,141.34 132.08,139.24 132.08,136.74C132.08,134.24 130.38,131.94 128.08,131.24ZM130.08,136.54C130.08,135.14 129.28,133.94 128.08,133.34V139.74C129.18,139.14 130.08,137.94 130.08,136.54Z"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M128.08,131.24H78.08V168.28L113.52,179.07L128.08,131.24ZM128.08,129.64L138.76,80.79L78.08,67.52V129.64H128.08ZM128.08,143.64H78.08V207.68L140.21,192.14L128.08,143.64ZM128.08,142.04L113.52,94.2L78.08,104.99V142.04H128.08ZM128.08,133.34L150.44,88.62L78.08,52.44V133.34H128.08ZM128.08,139.74H78.08V223.96L152.02,183.63L128.08,139.74ZM126.49,130.27H176.49V10.92L91.43,94.63L126.49,130.27ZM126.49,142.93L91.14,178.28L176.49,263.64V142.93H126.49ZM122.49,138.92L157.84,103.56L143.2,88.92H122.49V138.92ZM119.27,138.92H69.27V188.92H119.27V138.92ZM119.27,134.21V84.22H69.27V134.21H119.27ZM122.49,134.21V184.21H142.96L157.55,169.85L122.49,134.21ZM178.08,131.24V129.64H78.08V131.24H178.08ZM117.39,178.48C97.8,174.2 83.68,156.9 83.68,136.64H183.68C183.68,109.57 164.76,86.48 138.76,80.79L117.39,178.48ZM83.68,136.64C83.68,115.28 98.7,99.44 115.95,95.13L140.21,192.14C163.86,186.23 183.68,164.79 183.68,136.64H83.68ZM178.08,143.64V142.04H78.08V143.64H178.08ZM142.64,189.87C164.49,183.22 182.08,162.77 182.08,136.74H82.08C82.08,115.7 96.27,99.45 113.52,94.2L142.64,189.87ZM182.08,136.74C182.08,112.78 166.51,90.67 142.64,83.4L113.52,179.07C94.25,173.2 82.08,155.69 82.08,136.74H182.08ZM180.08,136.54C180.08,115.18 167.65,97.22 150.44,88.62L105.72,178.06C90.91,170.65 80.08,155.1 80.08,136.54H180.08ZM78.08,133.34V139.74H178.08V133.34H78.08ZM152.02,183.63C165.86,176.08 180.08,159.73 180.08,136.54H80.08C80.08,116.14 92.5,102.19 104.14,95.84L152.02,183.63ZM76.49,130.27V142.93H176.49V130.27H76.49ZM161.85,107.57L157.84,103.56L87.13,174.27L91.14,178.28L161.85,107.57ZM122.49,88.92H119.27V188.92H122.49V88.92ZM169.27,138.92V134.21H69.27V138.92H169.27ZM119.27,184.21H122.49V84.22H119.27V184.21ZM157.55,169.85L161.56,165.91L91.43,94.63L87.42,98.58L157.55,169.85Z"
+ android:fillColor="#000000"/>
+ </group>
+ <path
+ android:pathData="M78.48,68.64L78.48,68.64A16,16 0,0 1,94.48 84.64L94.48,84.64A16,16 0,0 1,78.48 100.64L78.48,100.64A16,16 0,0 1,62.48 84.64L62.48,84.64A16,16 0,0 1,78.48 68.64z"
+ android:fillColor="#3B4043"/>
+ <path
+ android:pathData="M86.03,82.29C86.69,82.29 87.23,81.75 87.23,81.09C87.23,80.42 86.69,79.89 86.03,79.89C85.37,79.89 84.83,80.42 84.83,81.09C84.83,81.75 85.37,82.29 86.03,82.29Z"
+ android:fillColor="#34A853"/>
+ <path
+ android:pathData="M82.43,86.29C83.76,86.29 84.83,85.21 84.83,83.89C84.83,82.56 83.76,81.49 82.43,81.49C81.11,81.49 80.03,82.56 80.03,83.89C80.03,85.21 81.11,86.29 82.43,86.29Z"
+ android:fillColor="#EA4335"/>
+ <path
+ android:pathData="M82.43,92.59C83.98,92.59 85.23,91.33 85.23,89.79C85.23,88.24 83.98,86.99 82.43,86.99C80.88,86.99 79.63,88.24 79.63,89.79C79.63,91.33 80.88,92.59 82.43,92.59Z"
+ android:fillColor="#FBBC04"/>
+ <path
+ android:pathData="M74.53,86.29C77.18,86.29 79.33,84.14 79.33,81.49C79.33,78.84 77.18,76.69 74.53,76.69C71.88,76.69 69.73,78.84 69.73,81.49C69.73,84.14 71.88,86.29 74.53,86.29Z"
+ android:fillColor="#4285F4"/>
+ <path
+ android:pathData="M174.48,120.64L174.48,120.64A16,16 0,0 1,190.48 136.64L190.48,136.64A16,16 0,0 1,174.48 152.64L174.48,152.64A16,16 0,0 1,158.48 136.64L158.48,136.64A16,16 0,0 1,174.48 120.64z"
+ android:fillColor="#EFA5DE"/>
+ <path
+ android:pathData="M178.3,130.64H170.8V142.34H178.3V130.64Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M170,132.94H167.3V140.14H170V132.94Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M182,132.94H179.3V140.14H182V132.94Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M126.48,172.64L126.48,172.64A16,16 0,0 1,142.48 188.64L142.48,188.64A16,16 0,0 1,126.48 204.64L126.48,204.64A16,16 0,0 1,110.48 188.64L110.48,188.64A16,16 0,0 1,126.48 172.64z"
+ android:fillColor="#FDD663"/>
+ <path
+ android:pathData="M135.48,188.64L132.88,191.24V194.94H129.08L126.48,197.54L123.88,194.94H120.08V191.24L117.48,188.64L120.08,186.04V182.34H123.78L126.38,179.74L128.98,182.34H132.68V186.04L135.48,188.64ZM121.68,188.64C121.68,191.24 123.78,193.44 126.48,193.44C129.08,193.44 131.28,191.34 131.28,188.64C131.28,185.94 129.18,183.84 126.48,183.84C123.88,183.84 121.68,185.94 121.68,188.64Z"
+ android:fillColor="#000000"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M126.98,188.04V186.54H125.98V188.04H124.48V189.04H125.98V190.54H126.98V189.04H128.48V188.04H126.98Z"
+ android:fillColor="#000000"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M126.48,68.64L126.48,68.64A16,16 0,0 1,142.48 84.64L142.48,84.64A16,16 0,0 1,126.48 100.64L126.48,100.64A16,16 0,0 1,110.48 84.64L110.48,84.64A16,16 0,0 1,126.48 68.64z"
+ android:fillColor="#D9AFFD"/>
+ <path
+ android:pathData="M128.13,78.64C128.13,79.44 127.43,80.14 126.53,80.14C125.63,80.14 124.93,79.44 124.93,78.64C124.93,77.84 125.63,77.14 126.53,77.14C127.43,77.04 128.13,77.74 128.13,78.64ZM126.53,80.84C128.73,80.84 131.23,80.64 133.23,80.04L133.63,81.54C132.13,81.94 130.43,82.14 128.83,82.34V92.14H127.23V87.64H125.63V92.14H124.13V82.34C122.53,82.24 120.83,81.94 119.33,81.54L119.73,80.04C121.83,80.64 124.23,80.84 126.53,80.84Z"
+ android:fillColor="#000000"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M174.48,172.64L174.48,172.64A16,16 0,0 1,190.48 188.64L190.48,188.64A16,16 0,0 1,174.48 204.64L174.48,204.64A16,16 0,0 1,158.48 188.64L158.48,188.64A16,16 0,0 1,174.48 172.64z"
+ android:fillColor="#84E39F"/>
+ <path
+ android:pathData="M178.88,186.09H178.18V184.59C178.18,182.59 176.58,180.89 174.48,180.89C172.38,180.89 170.78,182.49 170.78,184.59V186.09H170.08C169.28,186.09 168.58,186.79 168.58,187.59V194.89C168.58,195.69 169.28,196.39 170.08,196.39H178.88C179.68,196.39 180.38,195.69 180.38,194.89V187.59C180.38,186.79 179.68,186.09 178.88,186.09ZM174.48,192.69C173.68,192.69 172.98,191.99 172.98,191.19C172.98,190.39 173.68,189.69 174.48,189.69C175.28,189.69 175.98,190.39 175.98,191.19C175.98,192.09 175.28,192.69 174.48,192.69ZM172.18,186.09H176.68V184.59C176.68,183.29 175.68,182.29 174.38,182.29C173.08,182.29 172.08,183.29 172.08,184.59V186.09H172.18Z"
+ android:fillColor="#000000"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M174.48,68.64L174.48,68.64A16,16 0,0 1,190.48 84.64L190.48,84.64A16,16 0,0 1,174.48 100.64L174.48,100.64A16,16 0,0 1,158.48 84.64L158.48,84.64A16,16 0,0 1,174.48 68.64z"
+ android:fillColor="#DBDCE0"/>
+ <path
+ android:pathData="M173.68,77.49H175.28V85.39H173.68V77.49ZM177.98,80.39L179.08,79.29C180.58,80.59 181.58,82.49 181.58,84.69C181.58,88.59 178.38,91.79 174.48,91.79C170.58,91.79 167.38,88.59 167.38,84.69C167.38,82.49 168.38,80.59 169.88,79.29L170.98,80.39C169.78,81.39 168.98,82.99 168.98,84.69C168.98,87.79 171.48,90.29 174.58,90.29C177.68,90.29 180.18,87.79 180.18,84.69C180.08,82.89 179.18,81.39 177.98,80.39Z"
+ android:fillColor="#000000"
+ android:fillType="evenOdd"/>
+ </group>
+ <path
+ android:pathData="M62,51.82L349.54,51.82A14.5,14.5 0,0 1,364.04 66.32L364.04,236.27A14.5,14.5 0,0 1,349.54 250.77L62,250.77A14.5,14.5 0,0 1,47.5 236.27L47.5,66.32A14.5,14.5 0,0 1,62 51.82z"
+ android:strokeWidth="3"
+ android:fillColor="#00000000"
+ android:strokeColor="#7F868C"/>
+ <path
+ android:pathData="M311.22,50.49C311.22,49.11 312.33,48 313.7,48L322.61,48C323.98,48 325.08,49.11 325.08,50.49L319.14,51.48L311.22,50.49Z"
+ android:fillColor="#7F868C"/>
+ <path
+ android:pathData="M263.36,50.49C263.36,49.11 264.47,48 265.83,48L287.62,48C288.99,48 290.1,49.11 290.1,50.49L277.22,51.48L263.36,50.49Z"
+ android:fillColor="#7F868C"/>
+</vector>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable-sw600dp/a11ymenu_intro.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable-sw600dp/a11ymenu_intro.xml
new file mode 100644
index 0000000..f0fbc48
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable-sw600dp/a11ymenu_intro.xml
@@ -0,0 +1,141 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="412dp"
+ android:height="300dp"
+ android:viewportWidth="412"
+ android:viewportHeight="300">
+ <group>
+ <clip-path
+ android:pathData="M62.23,50.19L349.77,50.19A16,16 0,0 1,365.77 66.19L365.77,236.14A16,16 0,0 1,349.77 252.14L62.23,252.14A16,16 0,0 1,46.23 236.14L46.23,66.19A16,16 0,0 1,62.23 50.19z"/>
+ <path
+ android:pathData="M62.23,50.19L349.77,50.19A16,16 0,0 1,365.77 66.19L365.77,236.14A16,16 0,0 1,349.77 252.14L62.23,252.14A16,16 0,0 1,46.23 236.14L46.23,66.19A16,16 0,0 1,62.23 50.19z"
+ android:fillColor="#E8EAED"/>
+ <path
+ android:pathData="M46.23,52.14h160v200h-160z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M86.21,240.04L86.91,239.34L84.21,236.64H90.11V235.64H84.21L86.91,232.94L86.21,232.24L82.31,236.14L86.21,240.04Z"
+ android:fillColor="#E8EAED"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M125.71,220.14V252.14H127.71V220.14H125.71Z"
+ android:fillColor="#E8EAED"/>
+ <path
+ android:pathData="M166.21,232.24L165.51,232.94L168.21,235.64H162.31V236.64H168.21L165.51,239.34L166.21,240.04L170.11,236.14L166.21,232.24Z"
+ android:fillColor="#7F868C"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M46.71,221.14H206.71V219.14H46.71V221.14Z"
+ android:fillColor="#E8EAED"/>
+ <path
+ android:pathData="M78.71,120.5L78.71,120.5A16,16 0,0 1,94.71 136.5L94.71,136.5A16,16 0,0 1,78.71 152.5L78.71,152.5A16,16 0,0 1,62.71 136.5L62.71,136.5A16,16 0,0 1,78.71 120.5z"
+ android:fillColor="#327969"/>
+ <group>
+ <clip-path
+ android:pathData="M73.36,138.95V134.15H76.56L80.56,130.15V142.85L76.56,138.85H73.36V138.95ZM82.06,133.35C83.26,133.95 84.06,135.15 84.06,136.55C84.06,137.95 83.26,139.15 82.06,139.75V133.35Z"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M88.36,127.05H69.36V146.05H88.36V127.05Z"
+ android:fillColor="#ffffff"/>
+ </group>
+ <path
+ android:pathData="M78.71,172.5L78.71,172.5A16,16 0,0 1,94.71 188.5L94.71,188.5A16,16 0,0 1,78.71 204.5L78.71,204.5A16,16 0,0 1,62.71 188.5L62.71,188.5A16,16 0,0 1,78.71 172.5z"
+ android:fillColor="#DE9834"/>
+ <path
+ android:pathData="M87.66,188.5L85.06,191.1V194.8H81.36L78.76,197.4L76.16,194.8H72.36V191.1L69.76,188.5L72.36,185.9V182.2H76.06L78.66,179.6L81.26,182.2H84.96V185.9L87.66,188.5ZM73.96,188.5C73.96,191.1 76.06,193.3 78.76,193.3C81.36,193.3 83.56,191.2 83.56,188.5C83.56,185.8 81.46,183.7 78.76,183.7C76.06,183.7 73.96,185.8 73.96,188.5Z"
+ android:fillColor="#ffffff"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M80.66,187.9H76.66V189.4H80.66V187.9Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M126.71,120.5L126.71,120.5A16,16 0,0 1,142.71 136.5L142.71,136.5A16,16 0,0 1,126.71 152.5L126.71,152.5A16,16 0,0 1,110.71 136.5L110.71,136.5A16,16 0,0 1,126.71 120.5z"
+ android:fillColor="#327969"/>
+ <path
+ android:pathData="M128.31,131.1V129.5C131.51,130.2 133.91,133.1 133.91,136.5C133.91,139.9 131.51,142.7 128.31,143.5V141.9C130.61,141.2 132.31,139.1 132.31,136.6C132.31,134.1 130.61,131.8 128.31,131.1ZM130.31,136.4C130.31,135 129.51,133.8 128.31,133.2V139.6C129.41,139 130.31,137.8 130.31,136.4Z"
+ android:fillColor="#ffffff"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M126.72,142.79V130.13L122.71,134.08H119.5V138.78H122.71L126.72,142.79Z"
+ android:fillColor="#ffffff"/>
+ <group>
+ <clip-path
+ android:pathData="M126.72,142.79V130.13L122.71,134.08H119.5V138.78H122.71L126.72,142.79Z"/>
+ <clip-path
+ android:pathData="M128.31,131.1V129.5C131.51,130.2 133.91,133.1 133.91,136.5C133.91,139.9 131.51,142.7 128.31,143.5V141.9C130.61,141.2 132.31,139.1 132.31,136.6C132.31,134.1 130.61,131.8 128.31,131.1ZM130.31,136.4C130.31,135 129.51,133.8 128.31,133.2V139.6C129.41,139 130.31,137.8 130.31,136.4Z"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M128.31,131.1H78.31V168.15L113.75,178.94L128.31,131.1ZM128.31,129.5L138.99,80.66L78.31,67.38V129.5H128.31ZM128.31,143.5H78.31V207.54L140.44,192.01L128.31,143.5ZM128.31,141.9L113.75,94.07L78.31,104.86V141.9H128.31ZM128.31,133.2L150.67,88.48L78.31,52.3V133.2H128.31ZM128.31,139.6H78.31V223.83L152.25,183.5L128.31,139.6ZM126.72,130.13H176.73V10.79L91.66,94.5L126.72,130.13ZM126.72,142.79L91.37,178.15L176.73,263.5V142.79H126.72ZM122.71,138.78L158.07,103.43L143.43,88.78H122.71V138.78ZM119.5,138.78H69.5V188.78H119.5V138.78ZM119.5,134.08V84.08H69.5V134.08H119.5ZM122.71,134.08V184.08H143.19L157.78,169.72L122.71,134.08ZM178.31,131.1V129.5H78.31V131.1H178.31ZM117.63,178.35C98.03,174.06 83.91,156.76 83.91,136.5H183.91C183.91,109.44 164.99,86.34 138.99,80.66L117.63,178.35ZM83.91,136.5C83.91,115.15 98.93,99.31 116.18,94.99L140.44,192.01C164.09,186.1 183.91,164.66 183.91,136.5H83.91ZM178.31,143.5V141.9H78.31V143.5H178.31ZM142.87,189.74C164.72,183.09 182.31,162.64 182.31,136.6H82.31C82.31,115.57 96.5,99.32 113.75,94.07L142.87,189.74ZM182.31,136.6C182.31,112.64 166.74,90.53 142.87,83.27L113.75,178.94C94.48,173.07 82.31,155.56 82.31,136.6H182.31ZM180.31,136.4C180.31,115.04 167.88,97.08 150.67,88.48L105.95,177.92C91.14,170.52 80.31,154.96 80.31,136.4H180.31ZM78.31,133.2V139.6H178.31V133.2H78.31ZM152.25,183.5C166.09,175.95 180.31,159.6 180.31,136.4H80.31C80.31,116 92.73,102.06 104.37,95.71L152.25,183.5ZM76.72,130.13V142.79H176.73V130.13H76.72ZM162.08,107.44L158.07,103.43L87.36,174.14L91.37,178.15L162.08,107.44ZM122.71,88.78H119.5V188.78H122.71V88.78ZM169.5,138.78V134.08H69.5V138.78H169.5ZM119.5,184.08H122.71V84.08H119.5V184.08ZM157.78,169.72L161.79,165.77L91.66,94.5L87.65,98.44L157.78,169.72Z"
+ android:fillColor="#ffffff"/>
+ </group>
+ <path
+ android:pathData="M78.71,68.5L78.71,68.5A16,16 0,0 1,94.71 84.5L94.71,84.5A16,16 0,0 1,78.71 100.5L78.71,100.5A16,16 0,0 1,62.71 84.5L62.71,84.5A16,16 0,0 1,78.71 68.5z"
+ android:fillColor="#E8EAED"/>
+ <path
+ android:pathData="M86.26,82.15C86.92,82.15 87.46,81.61 87.46,80.95C87.46,80.29 86.92,79.75 86.26,79.75C85.6,79.75 85.06,80.29 85.06,80.95C85.06,81.61 85.6,82.15 86.26,82.15Z"
+ android:fillColor="#34A853"/>
+ <path
+ android:pathData="M82.66,86.15C83.99,86.15 85.06,85.08 85.06,83.75C85.06,82.43 83.99,81.35 82.66,81.35C81.34,81.35 80.26,82.43 80.26,83.75C80.26,85.08 81.34,86.15 82.66,86.15Z"
+ android:fillColor="#EA4335"/>
+ <path
+ android:pathData="M82.66,92.45C84.21,92.45 85.46,91.2 85.46,89.65C85.46,88.11 84.21,86.85 82.66,86.85C81.11,86.85 79.86,88.11 79.86,89.65C79.86,91.2 81.11,92.45 82.66,92.45Z"
+ android:fillColor="#FBBC04"/>
+ <path
+ android:pathData="M74.76,86.15C77.41,86.15 79.56,84 79.56,81.35C79.56,78.7 77.41,76.55 74.76,76.55C72.11,76.55 69.96,78.7 69.96,81.35C69.96,84 72.11,86.15 74.76,86.15Z"
+ android:fillColor="#4285F4"/>
+ <path
+ android:pathData="M174.71,120.5L174.71,120.5A16,16 0,0 1,190.71 136.5L190.71,136.5A16,16 0,0 1,174.71 152.5L174.71,152.5A16,16 0,0 1,158.71 136.5L158.71,136.5A16,16 0,0 1,174.71 120.5z"
+ android:fillColor="#9F3EBF"/>
+ <path
+ android:pathData="M178.53,130.5H171.03V142.2H178.53V130.5Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M170.23,132.8H167.53V140H170.23V132.8Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M182.23,132.8H179.53V140H182.23V132.8Z"
+ android:fillColor="#ffffff"/>
+ <path
+ android:pathData="M126.71,172.5L126.71,172.5A16,16 0,0 1,142.71 188.5L142.71,188.5A16,16 0,0 1,126.71 204.5L126.71,204.5A16,16 0,0 1,110.71 188.5L110.71,188.5A16,16 0,0 1,126.71 172.5z"
+ android:fillColor="#DE9834"/>
+ <path
+ android:pathData="M135.71,188.5L133.11,191.1V194.8H129.31L126.71,197.4L124.11,194.8H120.31V191.1L117.71,188.5L120.31,185.9V182.2H124.01L126.61,179.6L129.21,182.2H132.91V185.9L135.71,188.5ZM121.91,188.5C121.91,191.1 124.01,193.3 126.71,193.3C129.31,193.3 131.51,191.2 131.51,188.5C131.51,185.8 129.41,183.7 126.71,183.7C124.11,183.7 121.91,185.8 121.91,188.5Z"
+ android:fillColor="#ffffff"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M127.21,187.9V186.4H126.21V187.9H124.71V188.9H126.21V190.4H127.21V188.9H128.71V187.9H127.21Z"
+ android:fillColor="#ffffff"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M126.71,68.5L126.71,68.5A16,16 0,0 1,142.71 84.5L142.71,84.5A16,16 0,0 1,126.71 100.5L126.71,100.5A16,16 0,0 1,110.71 84.5L110.71,84.5A16,16 0,0 1,126.71 68.5z"
+ android:fillColor="#521BBF"/>
+ <path
+ android:pathData="M128.36,78.51C128.36,79.31 127.66,80.01 126.76,80.01C125.86,80.01 125.16,79.31 125.16,78.51C125.16,77.71 125.86,77.01 126.76,77.01C127.66,76.91 128.36,77.61 128.36,78.51ZM126.76,80.71C128.96,80.71 131.46,80.51 133.46,79.91L133.86,81.41C132.36,81.81 130.66,82.01 129.06,82.21V92.01H127.46V87.51H125.86V92.01H124.36V82.21C122.76,82.11 121.06,81.81 119.56,81.41L119.96,79.91C122.06,80.51 124.46,80.71 126.76,80.71Z"
+ android:fillColor="#ffffff"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M174.71,172.5L174.71,172.5A16,16 0,0 1,190.71 188.5L190.71,188.5A16,16 0,0 1,174.71 204.5L174.71,204.5A16,16 0,0 1,158.71 188.5L158.71,188.5A16,16 0,0 1,174.71 172.5z"
+ android:fillColor="#438947"/>
+ <path
+ android:pathData="M179.11,185.95H178.41V184.45C178.41,182.45 176.81,180.75 174.71,180.75C172.61,180.75 171.01,182.35 171.01,184.45V185.95H170.31C169.51,185.95 168.81,186.65 168.81,187.45V194.75C168.81,195.55 169.51,196.25 170.31,196.25H179.11C179.91,196.25 180.61,195.55 180.61,194.75V187.45C180.61,186.65 179.91,185.95 179.11,185.95ZM174.71,192.55C173.91,192.55 173.21,191.85 173.21,191.05C173.21,190.25 173.91,189.55 174.71,189.55C175.51,189.55 176.21,190.25 176.21,191.05C176.21,191.95 175.51,192.55 174.71,192.55ZM172.41,185.95H176.91V184.45C176.91,183.15 175.91,182.15 174.61,182.15C173.31,182.15 172.31,183.15 172.31,184.45V185.95H172.41Z"
+ android:fillColor="#ffffff"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M174.71,68.5L174.71,68.5A16,16 0,0 1,190.71 84.5L190.71,84.5A16,16 0,0 1,174.71 100.5L174.71,100.5A16,16 0,0 1,158.71 84.5L158.71,84.5A16,16 0,0 1,174.71 68.5z"
+ android:fillColor="#80868B"/>
+ <path
+ android:pathData="M173.91,77.35H175.51V85.25H173.91V77.35ZM178.21,80.25L179.31,79.15C180.81,80.45 181.81,82.35 181.81,84.55C181.81,88.45 178.61,91.65 174.71,91.65C170.81,91.65 167.61,88.45 167.61,84.55C167.61,82.35 168.61,80.45 170.11,79.15L171.21,80.25C170.01,81.25 169.21,82.85 169.21,84.55C169.21,87.65 171.71,90.15 174.81,90.15C177.91,90.15 180.41,87.65 180.41,84.55C180.31,82.75 179.41,81.25 178.21,80.25Z"
+ android:fillColor="#ffffff"
+ android:fillType="evenOdd"/>
+ </group>
+ <path
+ android:pathData="M62.23,51.69L349.77,51.69A14.5,14.5 0,0 1,364.27 66.19L364.27,236.14A14.5,14.5 0,0 1,349.77 250.64L62.23,250.64A14.5,14.5 0,0 1,47.73 236.14L47.73,66.19A14.5,14.5 0,0 1,62.23 51.69z"
+ android:strokeWidth="3"
+ android:fillColor="#00000000"
+ android:strokeColor="#DADCE0"/>
+ <path
+ android:pathData="M311.45,50.35C311.45,48.98 312.56,47.87 313.92,47.87L322.84,47.87C324.2,47.87 325.32,48.98 325.32,50.35L319.37,51.34L311.45,50.35Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M263.59,50.35C263.59,48.98 264.7,47.87 266.06,47.87L287.85,47.87C289.22,47.87 290.33,48.98 290.33,50.35L277.45,51.34L263.59,50.35Z"
+ android:fillColor="#DADCE0"/>
+</vector>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_menu.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_menu.xml
deleted file mode 100644
index 79e0e08d..0000000
--- a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_menu.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-<vector android:height="48dp" android:viewportHeight="192.0"
- android:viewportWidth="192.0" android:width="48dp"
- xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
- <path android:fillColor="#34a853" android:pathData="M37.14,173.74l-28.53,-90a14.53,14.53 0,0 1,5 -15.63L87.15,11a14.21,14.21 0,0 1,17.61 0.12l73.81,58.94a14.53,14.53 0,0 1,4.8 15.57l-28.48,88.18A14.32,14.32 0,0 1,141.22 184H50.84A14.33,14.33 0,0 1,37.14 173.74Z"/>
- <path android:pathData="M137.61,94.07l-17,17 -17,-17 -17,17L70.3,94.72l-17,17L125.66,184h15.56a14.32,14.32 0,0 0,13.67 -10.19l15.25,-47.21Z">
- <aapt:attr name="android:fillColor">
- <gradient android:endX="27152.64"
- android:endY="32745.600000000002"
- android:startX="20910.72"
- android:startY="21934.079999999998" android:type="linear">
- <item android:color="#33263238" android:offset="0.0"/>
- <item android:color="#11205432" android:offset="0.47"/>
- <item android:color="#051E6130" android:offset="1.0"/>
- </gradient>
- </aapt:attr>
- </path>
- <path android:fillAlpha="0.2" android:fillColor="#263238" android:pathData="M50.14,100.11a12,12 0,1 1,11.39 15.77,11.72 11.72,0 0,1 -5,-1.1l-3.41,-3.4ZM129.4,91.88a12,12 0,1 1,-12 12A12,12 0,0 1,129.4 91.88ZM95.4,91.88a12,12 0,1 1,-12 12A12,12 0,0 1,95.42 91.88Z"/>
- <path android:fillColor="#fff"
- android:pathData="M61.53,90.88a12,12 0,1 1,-12 12A12,12 0,0 1,61.53 90.88ZM129.41,90.88a12,12 0,1 1,-12 12A12,12 0,0 1,129.41 90.88ZM95.41,90.88a12,12 0,1 1,-12 12A12,12 0,0 1,95.42 90.88Z"
- android:strokeAlpha="0" android:strokeColor="#000"
- android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="0.5"/>
- <path android:fillAlpha="0.2" android:fillColor="#263238" android:pathData="M184,80.91a14.33,14.33 0,0 1,-0.63 4.7l-28.48,88.18A14.33,14.33 0,0 1,141.21 184H50.84a14.33,14.33 0,0 1,-13.7 -10.26l-28.53,-90A14.49,14.49 0,0 1,8 79.11a14.3,14.3 0,0 0,0.61 3.64l28.53,90A14.33,14.33 0,0 0,50.84 183h90.37a14.33,14.33 0,0 0,13.67 -10.19l28.48,-88.18A14.79,14.79 0,0 0,184 80.91Z"/>
- <path android:fillAlpha="0.2" android:fillColor="#fff" android:pathData="M184,81.89A14.46,14.46 0,0 0,178.57 71L104.76,12.1A14.21,14.21 0,0 0,87.15 12L13.58,69.12A14.5,14.5 0,0 0,8 80.09a14.5,14.5 0,0 1,5.57 -12L87.15,11a14.21,14.21 0,0 1,17.61 0.12L178.57,70A14.48,14.48 0,0 1,184 81.89Z"/>
- <path android:pathData="M37.14,173.74l-28.53,-90a14.53,14.53 0,0 1,5 -15.63L87.15,11a14.21,14.21 0,0 1,17.61 0.12l73.81,58.94a14.53,14.53 0,0 1,4.8 15.57l-28.48,88.18A14.32,14.32 0,0 1,141.22 184H50.84A14.33,14.33 0,0 1,37.14 173.74Z"/>
- <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
- <background android:drawable="@color/colorAccessibilityMenuIcon" />
- <foreground>
- <inset
- android:drawable="@drawable/ic_a11y_menu_round"
- android:inset="21.88%" />
- </foreground>
- </adaptive-icon>
-</vector>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_app.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_app.xml
new file mode 100644
index 0000000..31fe99e1f
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_app.xml
@@ -0,0 +1,8 @@
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@color/colorAccessibilityMenuIcon" />
+ <foreground>
+ <inset
+ android:drawable="@drawable/ic_logo_app_foreground"
+ android:inset="21.88%" />
+ </foreground>
+</adaptive-icon>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_a11y_menu_round.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_app_foreground.xml
similarity index 100%
rename from packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_a11y_menu_round.xml
rename to packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_app_foreground.xml
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
index 76139c6..ca6c332 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
@@ -22,19 +22,28 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
+import android.hardware.display.BrightnessInfo;
import android.hardware.display.DisplayManager;
+import android.media.AudioManager;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
+import android.provider.Settings;
import android.view.Display;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
+import com.android.settingslib.display.BrightnessUtils;
+import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut.ShortcutId;
import com.android.systemui.accessibility.accessibilitymenu.view.A11yMenuOverlayLayout;
+import java.util.List;
+
/** @hide */
public class AccessibilityMenuService extends AccessibilityService
implements View.OnTouchListener {
@@ -42,6 +51,11 @@
private static final long BUFFER_MILLISECONDS_TO_PREVENT_UPDATE_FAILURE = 100L;
+ private static final int BRIGHTNESS_UP_INCREMENT_GAMMA =
+ (int) Math.ceil(BrightnessUtils.GAMMA_SPACE_MAX * 0.11f);
+ private static final int BRIGHTNESS_DOWN_INCREMENT_GAMMA =
+ (int) -Math.ceil(BrightnessUtils.GAMMA_SPACE_MAX * 0.11f);
+
private long mLastTimeTouchedOutside = 0L;
// Timeout used to ignore the A11y button onClick() when ACTION_OUTSIDE is also received on
// clicking on the A11y button.
@@ -51,6 +65,8 @@
private static boolean sInitialized = false;
+ private AudioManager mAudioManager;
+
// TODO(b/136716947): Support multi-display once a11y framework side is ready.
private DisplayManager mDisplayManager;
final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() {
@@ -140,6 +156,7 @@
mDisplayManager = getSystemService(DisplayManager.class);
mDisplayManager.registerDisplayListener(mDisplayListener, null);
+ mAudioManager = getSystemService(AudioManager.class);
sInitialized = true;
}
@@ -170,9 +187,100 @@
* @param view the shortcut button being clicked.
*/
public void handleClick(View view) {
+ // Shortcuts are repeatable in a11y menu rather than unique, so use tag ID to handle.
+ int viewTag = (int) view.getTag();
+
+ if (viewTag == ShortcutId.ID_ASSISTANT_VALUE.ordinal()) {
+ // Always restart the voice command activity, so that the UI is reloaded.
+ startActivityIfIntentIsSafe(
+ new Intent(Intent.ACTION_VOICE_COMMAND),
+ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ } else if (viewTag == ShortcutId.ID_A11YSETTING_VALUE.ordinal()) {
+ startActivityIfIntentIsSafe(
+ new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS),
+ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ } else if (viewTag == ShortcutId.ID_POWER_VALUE.ordinal()) {
+ performGlobalAction(GLOBAL_ACTION_POWER_DIALOG);
+ } else if (viewTag == ShortcutId.ID_RECENT_VALUE.ordinal()) {
+ performGlobalAction(GLOBAL_ACTION_RECENTS);
+ } else if (viewTag == ShortcutId.ID_LOCKSCREEN_VALUE.ordinal()) {
+ performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN);
+ } else if (viewTag == ShortcutId.ID_QUICKSETTING_VALUE.ordinal()) {
+ performGlobalAction(GLOBAL_ACTION_QUICK_SETTINGS);
+ } else if (viewTag == ShortcutId.ID_NOTIFICATION_VALUE.ordinal()) {
+ performGlobalAction(GLOBAL_ACTION_NOTIFICATIONS);
+ } else if (viewTag == ShortcutId.ID_SCREENSHOT_VALUE.ordinal()) {
+ performGlobalAction(GLOBAL_ACTION_TAKE_SCREENSHOT);
+ } else if (viewTag == ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal()) {
+ adjustBrightness(BRIGHTNESS_UP_INCREMENT_GAMMA);
+ return;
+ } else if (viewTag == ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal()) {
+ adjustBrightness(BRIGHTNESS_DOWN_INCREMENT_GAMMA);
+ return;
+ } else if (viewTag == ShortcutId.ID_VOLUME_UP_VALUE.ordinal()) {
+ adjustVolume(AudioManager.ADJUST_RAISE);
+ return;
+ } else if (viewTag == ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal()) {
+ adjustVolume(AudioManager.ADJUST_LOWER);
+ return;
+ }
+
mA11yMenuLayout.hideMenu();
}
+ /**
+ * Adjusts brightness using the same logic and utils class as the SystemUI brightness slider.
+ *
+ * @see BrightnessUtils
+ * @see com.android.systemui.settings.brightness.BrightnessController
+ * @param increment The increment amount in gamma-space
+ */
+ private void adjustBrightness(int increment) {
+ BrightnessInfo info = getDisplay().getBrightnessInfo();
+ int gamma = BrightnessUtils.convertLinearToGammaFloat(
+ info.brightness,
+ info.brightnessMinimum,
+ info.brightnessMaximum
+ );
+ gamma = Math.max(
+ BrightnessUtils.GAMMA_SPACE_MIN,
+ Math.min(BrightnessUtils.GAMMA_SPACE_MAX, gamma + increment));
+
+ float brightness = BrightnessUtils.convertGammaToLinearFloat(
+ gamma,
+ info.brightnessMinimum,
+ info.brightnessMaximum
+ );
+ mDisplayManager.setTemporaryBrightness(getDisplayId(), brightness);
+ mDisplayManager.setBrightness(getDisplayId(), brightness);
+ mA11yMenuLayout.showSnackbar(
+ getString(R.string.brightness_percentage_label,
+ (gamma / (BrightnessUtils.GAMMA_SPACE_MAX / 100))));
+ }
+
+ private void adjustVolume(int direction) {
+ mAudioManager.adjustStreamVolume(
+ AudioManager.STREAM_MUSIC, direction,
+ AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
+ final int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+ final int volume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+ mA11yMenuLayout.showSnackbar(
+ getString(
+ R.string.music_volume_percentage_label,
+ (int) (100.0 / maxVolume * volume))
+ );
+ }
+
+ private void startActivityIfIntentIsSafe(Intent intent, int flag) {
+ PackageManager packageManager = getPackageManager();
+ List<ResolveInfo> activities =
+ packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (!activities.isEmpty()) {
+ intent.setFlags(flag);
+ startActivity(intent);
+ }
+ }
+
@Override
public void onInterrupt() {
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
index 4322d53..74bc910 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
@@ -44,6 +44,20 @@
}
// language=AGSL
companion object {
+ // Default fade in/ out values. The value range is [0,1].
+ const val DEFAULT_FADE_IN_START = 0f
+ const val DEFAULT_FADE_OUT_END = 1f
+
+ const val DEFAULT_BASE_RING_FADE_IN_END = 0.1f
+ const val DEFAULT_BASE_RING_FADE_OUT_START = 0.3f
+
+ const val DEFAULT_SPARKLE_RING_FADE_IN_END = 0.1f
+ const val DEFAULT_SPARKLE_RING_FADE_OUT_START = 0.4f
+
+ const val DEFAULT_CENTER_FILL_FADE_IN_END = 0f
+ const val DEFAULT_CENTER_FILL_FADE_OUT_START = 0f
+ const val DEFAULT_CENTER_FILL_FADE_OUT_END = 0.6f
+
private const val SHADER_UNIFORMS =
"""
uniform vec2 in_center;
@@ -143,11 +157,26 @@
}
private fun subProgress(start: Float, end: Float, progress: Float): Float {
+ // Avoid division by 0.
+ if (start == end) {
+ // If start and end are the same and progress has exceeded the start/ end point,
+ // treat it as 1, otherwise 0.
+ return if (progress > start) 1f else 0f
+ }
+
val min = Math.min(start, end)
val max = Math.max(start, end)
val sub = Math.min(Math.max(progress, min), max)
return (sub - start) / (end - start)
}
+
+ private fun getFade(fadeParams: FadeParams, rawProgress: Float): Float {
+ val fadeIn = subProgress(fadeParams.fadeInStart, fadeParams.fadeInEnd, rawProgress)
+ val fadeOut =
+ 1f - subProgress(fadeParams.fadeOutStart, fadeParams.fadeOutEnd, rawProgress)
+
+ return Math.min(fadeIn, fadeOut)
+ }
}
/** Sets the center position of the ripple. */
@@ -172,17 +201,9 @@
field = value
progress = Interpolators.STANDARD.getInterpolation(value)
- val fadeIn = subProgress(0f, 0.1f, value)
- val fadeOutNoise = subProgress(0.4f, 1f, value)
- var fadeOutRipple = 0f
- var fadeFill = 0f
- if (!rippleFill) {
- fadeFill = subProgress(0f, 0.6f, value)
- fadeOutRipple = subProgress(0.3f, 1f, value)
- }
- setFloatUniform("in_fadeSparkle", Math.min(fadeIn, 1 - fadeOutNoise))
- setFloatUniform("in_fadeFill", 1 - fadeFill)
- setFloatUniform("in_fadeRing", Math.min(fadeIn, 1 - fadeOutRipple))
+ setFloatUniform("in_fadeSparkle", getFade(sparkleRingFadeParams, value))
+ setFloatUniform("in_fadeRing", getFade(baseRingFadeParams, value))
+ setFloatUniform("in_fadeFill", getFade(centerFillFadeParams, value))
}
/** Progress with Standard easing curve applied. */
@@ -232,21 +253,130 @@
setFloatUniform("in_distort_xy", 75 * value)
}
+ /**
+ * Pixel density of the screen that the effects are rendered to.
+ *
+ * <p>This value should come from [resources.displayMetrics.density].
+ */
var pixelDensity: Float = 1.0f
set(value) {
field = value
setFloatUniform("in_pixelDensity", value)
}
- /**
- * True if the ripple should stayed filled in as it expands to give a filled-in circle effect.
- * False for a ring effect.
- */
- var rippleFill: Boolean = false
-
var currentWidth: Float = 0f
private set
var currentHeight: Float = 0f
private set
+
+ /**
+ * True if the ripple should stayed filled in as it expands to give a filled-in circle effect.
+ * False for a ring effect.
+ *
+ * <p>You must reset fade params after changing this.
+ *
+ * TODO(b/265326983): Remove this and only expose fade params.
+ */
+ var rippleFill: Boolean = false
+ set(value) {
+ if (value) {
+ baseRingFadeParams.fadeOutStart = 1f
+ baseRingFadeParams.fadeOutEnd = 1f
+
+ centerFillFadeParams.fadeInStart = 0f
+ centerFillFadeParams.fadeInEnd = 0f
+ centerFillFadeParams.fadeOutStart = 1f
+ centerFillFadeParams.fadeOutEnd = 1f
+ } else {
+ // Set back to the original fade parameters.
+ // Ideally this should be set by the client as they know the initial value.
+ baseRingFadeParams.fadeOutStart = DEFAULT_BASE_RING_FADE_OUT_START
+ baseRingFadeParams.fadeOutEnd = DEFAULT_FADE_OUT_END
+
+ centerFillFadeParams.fadeInStart = DEFAULT_FADE_IN_START
+ centerFillFadeParams.fadeInEnd = DEFAULT_CENTER_FILL_FADE_IN_END
+ centerFillFadeParams.fadeOutStart = DEFAULT_CENTER_FILL_FADE_OUT_START
+ centerFillFadeParams.fadeOutEnd = DEFAULT_CENTER_FILL_FADE_OUT_END
+ }
+ field = value
+ }
+
+ /** Parameters that are used to fade in/ out of the sparkle ring. */
+ val sparkleRingFadeParams =
+ FadeParams(
+ DEFAULT_FADE_IN_START,
+ DEFAULT_SPARKLE_RING_FADE_IN_END,
+ DEFAULT_SPARKLE_RING_FADE_OUT_START,
+ DEFAULT_FADE_OUT_END
+ )
+
+ /**
+ * Parameters that are used to fade in/ out of the base ring.
+ *
+ * <p>Note that the shader draws the sparkle ring on top of the base ring.
+ */
+ val baseRingFadeParams =
+ FadeParams(
+ DEFAULT_FADE_IN_START,
+ DEFAULT_BASE_RING_FADE_IN_END,
+ DEFAULT_BASE_RING_FADE_OUT_START,
+ DEFAULT_FADE_OUT_END
+ )
+
+ /**
+ * Parameters that are used to fade in/ out of the center fill.
+ *
+ * <p>Note that if [rippleFill] is set to true, those will be ignored and the center fill will
+ * be always full alpha.
+ */
+ val centerFillFadeParams =
+ FadeParams(
+ DEFAULT_FADE_IN_START,
+ DEFAULT_CENTER_FILL_FADE_IN_END,
+ DEFAULT_CENTER_FILL_FADE_OUT_START,
+ DEFAULT_CENTER_FILL_FADE_OUT_END
+ )
+
+ /**
+ * Parameters used for fade in and outs of the ripple.
+ *
+ * <p>Note that all the fade in/ outs are "linear" progression.
+ * ```
+ * (opacity)
+ * 1
+ * │
+ * maxAlpha ← ――――――――――――
+ * │ / \
+ * │ / \
+ * minAlpha ←――――/ \―――― (alpha change)
+ * │
+ * │
+ * 0 ―――↑―――↑―――――――――↑―――↑――――1 (progress)
+ * fadeIn fadeOut
+ * Start & End Start & End
+ * ```
+ * <p>If no fade in/ out is needed, set [fadeInStart] and [fadeInEnd] to 0; [fadeOutStart] and
+ * [fadeOutEnd] to 1.
+ */
+ data class FadeParams(
+ /**
+ * The starting point of the fade out which ends at [fadeInEnd], given that the animation
+ * goes from 0 to 1.
+ */
+ var fadeInStart: Float = DEFAULT_FADE_IN_START,
+ /**
+ * The endpoint of the fade in when the fade in starts at [fadeInStart], given that the
+ * animation goes from 0 to 1.
+ */
+ var fadeInEnd: Float,
+ /**
+ * The starting point of the fade out which ends at 1, given that the animation goes from 0
+ * to 1.
+ */
+ var fadeOutStart: Float,
+
+ /** The endpoint of the fade out, given that the animation goes from 0 to 1. */
+ var fadeOutEnd: Float = DEFAULT_FADE_OUT_END,
+ )
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
index bc4796a..3c9328c 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
@@ -117,15 +117,6 @@
rippleShader.color = ColorUtils.setAlphaComponent(color, alpha)
}
- /**
- * Set whether the ripple should remain filled as the ripple expands.
- *
- * See [RippleShader.rippleFill].
- */
- fun setRippleFill(rippleFill: Boolean) {
- rippleShader.rippleFill = rippleFill
- }
-
/** Set the intensity of the sparkles. */
fun setSparkleStrength(strength: Float) {
rippleShader.sparkleStrength = strength
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
index 59b4848..a523cf1 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -21,15 +21,14 @@
import android.provider.Settings
import android.util.Log
import androidx.annotation.OpenForTesting
-import com.android.internal.annotations.Keep
import com.android.systemui.plugins.ClockController
import com.android.systemui.plugins.ClockId
import com.android.systemui.plugins.ClockMetadata
import com.android.systemui.plugins.ClockProvider
import com.android.systemui.plugins.ClockProviderPlugin
+import com.android.systemui.plugins.ClockSettings
import com.android.systemui.plugins.PluginListener
import com.android.systemui.plugins.PluginManager
-import org.json.JSONObject
private val TAG = ClockRegistry::class.simpleName
private const val DEBUG = true
@@ -64,34 +63,54 @@
disconnectClocks(plugin)
}
- open var currentClockId: ClockId
+ open var settings: ClockSettings?
get() {
- return try {
+ try {
val json = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE
)
if (json == null || json.isEmpty()) {
- return fallbackClockId
+ return null
}
- ClockSetting.deserialize(json).clockId
+ return ClockSettings.deserialize(json)
} catch (ex: Exception) {
- Log.e(TAG, "Failed to parse clock setting", ex)
- fallbackClockId
+ Log.e(TAG, "Failed to parse clock settings", ex)
+ return null
}
}
- set(value) {
+ protected set(value) {
try {
- val json = ClockSetting.serialize(ClockSetting(value, System.currentTimeMillis()))
+ val json = if (value != null) {
+ value._applied_timestamp = System.currentTimeMillis()
+ ClockSettings.serialize(value)
+ } else {
+ ""
+ }
+
Settings.Secure.putString(
context.contentResolver,
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, json
)
} catch (ex: Exception) {
- Log.e(TAG, "Failed to set clock setting", ex)
+ Log.e(TAG, "Failed to set clock settings", ex)
}
}
+ private fun mutateSetting(mutator: (ClockSettings) -> Unit) {
+ val settings = this.settings ?: ClockSettings()
+ mutator(settings)
+ this.settings = settings
+ }
+
+ var currentClockId: ClockId
+ get() = settings?.clockId ?: fallbackClockId
+ set(value) { mutateSetting { it.clockId = value } }
+
+ var seedColor: Int?
+ get() = settings?.seedColor
+ set(value) { mutateSetting { it.seedColor = value } }
+
init {
connectClocks(defaultClockProvider)
if (!availableClocks.containsKey(DEFAULT_CLOCK_ID)) {
@@ -194,36 +213,16 @@
return createClock(DEFAULT_CLOCK_ID)!!
}
- private fun createClock(clockId: ClockId): ClockController? =
- availableClocks[clockId]?.provider?.createClock(clockId)
+ private fun createClock(clockId: ClockId): ClockController? {
+ val settings = this.settings ?: ClockSettings()
+ if (clockId != settings.clockId) {
+ settings.clockId = clockId
+ }
+ return availableClocks[clockId]?.provider?.createClock(settings)
+ }
private data class ClockInfo(
val metadata: ClockMetadata,
val provider: ClockProvider
)
-
- @Keep
- data class ClockSetting(
- val clockId: ClockId,
- val _applied_timestamp: Long?
- ) {
- companion object {
- private val KEY_CLOCK_ID = "clockId"
- private val KEY_TIMESTAMP = "_applied_timestamp"
-
- fun serialize(setting: ClockSetting): String {
- return JSONObject()
- .put(KEY_CLOCK_ID, setting.clockId)
- .put(KEY_TIMESTAMP, setting._applied_timestamp)
- .toString()
- }
-
- fun deserialize(jsonStr: String): ClockSetting {
- val json = JSONObject(jsonStr)
- return ClockSetting(
- json.getString(KEY_CLOCK_ID),
- if (!json.isNull(KEY_TIMESTAMP)) json.getLong(KEY_TIMESTAMP) else null)
- }
- }
- }
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index 7645dec..2a40f5c 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -29,6 +29,7 @@
import com.android.systemui.plugins.ClockEvents
import com.android.systemui.plugins.ClockFaceController
import com.android.systemui.plugins.ClockFaceEvents
+import com.android.systemui.plugins.ClockSettings
import com.android.systemui.plugins.log.LogBuffer
import java.io.PrintWriter
import java.util.Locale
@@ -46,6 +47,7 @@
ctx: Context,
private val layoutInflater: LayoutInflater,
private val resources: Resources,
+ private val settings: ClockSettings?,
) : ClockController {
override val smallClock: DefaultClockFaceController
override val largeClock: LargeClockFaceController
@@ -66,12 +68,14 @@
smallClock =
DefaultClockFaceController(
layoutInflater.inflate(R.layout.clock_default_small, parent, false)
- as AnimatableClockView
+ as AnimatableClockView,
+ settings?.seedColor
)
largeClock =
LargeClockFaceController(
layoutInflater.inflate(R.layout.clock_default_large, parent, false)
- as AnimatableClockView
+ as AnimatableClockView,
+ settings?.seedColor
)
clocks = listOf(smallClock.view, largeClock.view)
@@ -85,11 +89,13 @@
animations = DefaultClockAnimations(dozeFraction, foldFraction)
events.onColorPaletteChanged(resources)
events.onTimeZoneChanged(TimeZone.getDefault())
- events.onTimeTick()
+ smallClock.events.onTimeTick()
+ largeClock.events.onTimeTick()
}
open inner class DefaultClockFaceController(
override val view: AnimatableClockView,
+ val seedColor: Int?,
) : ClockFaceController {
// MAGENTA is a placeholder, and will be assigned correctly in initialize
@@ -104,11 +110,16 @@
}
init {
+ if (seedColor != null) {
+ currentColor = seedColor
+ }
view.setColors(currentColor, currentColor)
}
override val events =
object : ClockFaceEvents {
+ override fun onTimeTick() = view.refreshTime()
+
override fun onRegionDarknessChanged(isRegionDark: Boolean) {
this@DefaultClockFaceController.isRegionDark = isRegionDark
updateColor()
@@ -129,7 +140,9 @@
fun updateColor() {
val color =
- if (isRegionDark) {
+ if (seedColor != null) {
+ seedColor
+ } else if (isRegionDark) {
resources.getColor(android.R.color.system_accent1_100)
} else {
resources.getColor(android.R.color.system_accent2_600)
@@ -149,7 +162,8 @@
inner class LargeClockFaceController(
view: AnimatableClockView,
- ) : DefaultClockFaceController(view) {
+ seedColor: Int?,
+ ) : DefaultClockFaceController(view, seedColor) {
override fun recomputePadding(targetRegion: Rect?) {
// We center the view within the targetRegion instead of within the parent
// view by computing the difference and adding that to the padding.
@@ -169,8 +183,6 @@
}
inner class DefaultClockEvents : ClockEvents {
- override fun onTimeTick() = clocks.forEach { it.refreshTime() }
-
override fun onTimeFormatChanged(is24Hr: Boolean) =
clocks.forEach { it.refreshFormat(is24Hr) }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index 4c0504b..0fd1b49 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -22,6 +22,7 @@
import com.android.systemui.plugins.ClockId
import com.android.systemui.plugins.ClockMetadata
import com.android.systemui.plugins.ClockProvider
+import com.android.systemui.plugins.ClockSettings
private val TAG = DefaultClockProvider::class.simpleName
const val DEFAULT_CLOCK_NAME = "Default Clock"
@@ -36,12 +37,12 @@
override fun getClocks(): List<ClockMetadata> =
listOf(ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME))
- override fun createClock(id: ClockId): ClockController {
- if (id != DEFAULT_CLOCK_ID) {
- throw IllegalArgumentException("$id is unsupported by $TAG")
+ override fun createClock(settings: ClockSettings): ClockController {
+ if (settings.clockId != DEFAULT_CLOCK_ID) {
+ throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG")
}
- return DefaultClockController(ctx, layoutInflater, resources)
+ return DefaultClockController(ctx, layoutInflater, resources, settings)
}
override fun getClockThumbnail(id: ClockId): Drawable? {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
index e4e9c46..c120876 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
@@ -181,6 +181,9 @@
/** Flag denoting whether the Wallpaper preview should use the full screen UI. */
const val FLAG_NAME_WALLPAPER_FULLSCREEN_PREVIEW = "wallpaper_fullscreen_preview"
+ /** Flag denoting whether the Monochromatic Theme is enabled. */
+ const val FLAG_NAME_MONOCHROMATIC_THEME = "is_monochromatic_theme_enabled"
+
object Columns {
/** String. Unique ID for the flag. */
const val NAME = "name"
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/MaterialDynamicColors.java b/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/MaterialDynamicColors.java
index 813b22a..91f3309 100644
--- a/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/MaterialDynamicColors.java
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/MaterialDynamicColors.java
@@ -57,19 +57,19 @@
public static final DynamicColor surfaceDim =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 6.0 : 87.0);
- public static final DynamicColor surfaceSub2 =
+ public static final DynamicColor surfaceContainerLowest =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 4.0 : 100.0);
- public static final DynamicColor surfaceSub1 =
+ public static final DynamicColor surfaceContainerLow =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 96.0);
public static final DynamicColor surfaceContainer =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 12.0 : 94.0);
- public static final DynamicColor surfaceAdd1 =
+ public static final DynamicColor surfaceContainerHigh =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 17.0 : 92.0);
- public static final DynamicColor surfaceAdd2 =
+ public static final DynamicColor surfaceContainerHighest =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 22.0 : 90.0);
public static final DynamicColor onSurface =
@@ -366,6 +366,27 @@
public static final DynamicColor textHintInverse =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 90.0);
+ public static final DynamicColor primaryPaletteKeyColor =
+ DynamicColor.fromPalette(
+ (s) -> s.primaryPalette, (s) -> s.primaryPalette.getKeyColor().getTone());
+
+ public static final DynamicColor secondaryPaletteKeyColor =
+ DynamicColor.fromPalette(
+ (s) -> s.secondaryPalette, (s) -> s.secondaryPalette.getKeyColor().getTone());
+
+ public static final DynamicColor tertiaryPaletteKeyColor =
+ DynamicColor.fromPalette(
+ (s) -> s.tertiaryPalette, (s) -> s.tertiaryPalette.getKeyColor().getTone());
+
+ public static final DynamicColor neutralPaletteKeyColor =
+ DynamicColor.fromPalette(
+ (s) -> s.neutralPalette, (s) -> s.neutralPalette.getKeyColor().getTone());
+
+ public static final DynamicColor neutralVariantPaletteKeyColor =
+ DynamicColor.fromPalette(
+ (s) -> s.neutralVariantPalette,
+ (s) -> s.neutralVariantPalette.getKeyColor().getTone());
+
private static ViewingConditions viewingConditionsForAlbers(DynamicScheme scheme) {
return ViewingConditions.defaultWithBackgroundLstar(scheme.isDark ? 30.0 : 80.0);
}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/palettes/TonalPalette.java b/packages/SystemUI/monet/src/com/android/systemui/monet/palettes/TonalPalette.java
index 6911666..8e905ea 100644
--- a/packages/SystemUI/monet/src/com/android/systemui/monet/palettes/TonalPalette.java
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/palettes/TonalPalette.java
@@ -26,6 +26,7 @@
*/
public final class TonalPalette {
Map<Integer, Integer> cache;
+ Hct keyColor;
double hue;
double chroma;
@@ -46,7 +47,7 @@
* @return Tones matching that color's hue and chroma.
*/
public static TonalPalette fromHct(Hct hct) {
- return TonalPalette.fromHueAndChroma(hct.getHue(), hct.getChroma());
+ return new TonalPalette(hct.getHue(), hct.getChroma(), hct);
}
/**
@@ -57,13 +58,52 @@
* @return Tones matching hue and chroma.
*/
public static TonalPalette fromHueAndChroma(double hue, double chroma) {
- return new TonalPalette(hue, chroma);
+ return new TonalPalette(hue, chroma, createKeyColor(hue, chroma));
}
- private TonalPalette(double hue, double chroma) {
+ private TonalPalette(double hue, double chroma, Hct keyColor) {
cache = new HashMap<>();
this.hue = hue;
this.chroma = chroma;
+ this.keyColor = keyColor;
+ }
+
+ /** The key color is the first tone, starting from T50, matching the given hue and chroma. */
+ private static Hct createKeyColor(double hue, double chroma) {
+ double startTone = 50.0;
+ Hct smallestDeltaHct = Hct.from(hue, chroma, startTone);
+ double smallestDelta = Math.abs(smallestDeltaHct.getChroma() - chroma);
+ // Starting from T50, check T+/-delta to see if they match the requested
+ // chroma.
+ //
+ // Starts from T50 because T50 has the most chroma available, on
+ // average. Thus it is most likely to have a direct answer and minimize
+ // iteration.
+ for (double delta = 1.0; delta < 50.0; delta += 1.0) {
+ // Termination condition rounding instead of minimizing delta to avoid
+ // case where requested chroma is 16.51, and the closest chroma is 16.49.
+ // Error is minimized, but when rounded and displayed, requested chroma
+ // is 17, key color's chroma is 16.
+ if (Math.round(chroma) == Math.round(smallestDeltaHct.getChroma())) {
+ return smallestDeltaHct;
+ }
+
+ final Hct hctAdd = Hct.from(hue, chroma, startTone + delta);
+ final double hctAddDelta = Math.abs(hctAdd.getChroma() - chroma);
+ if (hctAddDelta < smallestDelta) {
+ smallestDelta = hctAddDelta;
+ smallestDeltaHct = hctAdd;
+ }
+
+ final Hct hctSubtract = Hct.from(hue, chroma, startTone - delta);
+ final double hctSubtractDelta = Math.abs(hctSubtract.getChroma() - chroma);
+ if (hctSubtractDelta < smallestDelta) {
+ smallestDelta = hctSubtractDelta;
+ smallestDeltaHct = hctSubtract;
+ }
+ }
+
+ return smallestDeltaHct;
}
/**
@@ -98,4 +138,9 @@
public double getHue() {
return this.hue;
}
+
+ /** The key color is the first tone, starting from T50, that matches the palette's chroma. */
+ public Hct getKeyColor() {
+ return this.keyColor;
+ }
}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeExpressive.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeExpressive.java
index e7c6f79..234826e 100644
--- a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeExpressive.java
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeExpressive.java
@@ -34,14 +34,16 @@
isDark,
contrastLevel,
TonalPalette.fromHueAndChroma(
- MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() + 120.0), 40.0),
+ MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() + 240.0), 40.0),
TonalPalette.fromHueAndChroma(
DynamicScheme.getRotatedHue(sourceColorHct, HUES, SECONDARY_ROTATIONS),
24.0),
TonalPalette.fromHueAndChroma(
DynamicScheme.getRotatedHue(sourceColorHct, HUES, TERTIARY_ROTATIONS),
32.0),
- TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 8.0),
- TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 12.0));
+ TonalPalette.fromHueAndChroma(
+ MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() + 15.0), 8.0),
+ TonalPalette.fromHueAndChroma(
+ MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() + 15.0), 12.0));
}
}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeTonalSpot.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeTonalSpot.java
index cd74ac3..8480684 100644
--- a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeTonalSpot.java
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeTonalSpot.java
@@ -28,11 +28,11 @@
Variant.TONAL_SPOT,
isDark,
contrastLevel,
- TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 40.0),
+ TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 36.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 16.0),
TonalPalette.fromHueAndChroma(
MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() + 60.0), 24.0),
- TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 6.0),
+ TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 4.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 8.0));
}
}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeVibrant.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeVibrant.java
index 89337ca..160fab7 100644
--- a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeVibrant.java
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeVibrant.java
@@ -38,7 +38,7 @@
TonalPalette.fromHueAndChroma(
DynamicScheme.getRotatedHue(sourceColorHct, HUES, TERTIARY_ROTATIONS),
32.0),
- TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 8.0),
+ TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 10.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 12.0));
}
}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
index a2a0709..4ef525a 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
@@ -17,11 +17,14 @@
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
+import com.android.internal.annotations.Keep
import com.android.systemui.plugins.annotations.ProvidesInterface
import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.statusbar.Weather
import java.io.PrintWriter
import java.util.Locale
import java.util.TimeZone
+import org.json.JSONObject
/** Identifies a clock design */
typealias ClockId = String
@@ -41,7 +44,13 @@
fun getClocks(): List<ClockMetadata>
/** Initializes and returns the target clock design */
- fun createClock(id: ClockId): ClockController
+ @Deprecated("Use overload with ClockSettings")
+ fun createClock(id: ClockId): ClockController {
+ return createClock(ClockSettings(id, null, null))
+ }
+
+ /** Initializes and returns the target clock design */
+ fun createClock(settings: ClockSettings): ClockController
/** A static thumbnail for rendering in some examples */
fun getClockThumbnail(id: ClockId): Drawable?
@@ -62,11 +71,16 @@
val animations: ClockAnimations
/** Initializes various rendering parameters. If never called, provides reasonable defaults. */
- fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) {
+ fun initialize(
+ resources: Resources,
+ dozeFraction: Float,
+ foldFraction: Float,
+ ) {
events.onColorPaletteChanged(resources)
animations.doze(dozeFraction)
animations.fold(foldFraction)
- events.onTimeTick()
+ smallClock.events.onTimeTick()
+ largeClock.events.onTimeTick()
}
/** Optional method for dumping debug information */
@@ -87,9 +101,6 @@
/** Events that should call when various rendering parameters change */
interface ClockEvents {
- /** Call every time tick */
- fun onTimeTick() {}
-
/** Call whenever timezone changes */
fun onTimeZoneChanged(timeZone: TimeZone) {}
@@ -101,6 +112,9 @@
/** Call whenever the color palette should update */
fun onColorPaletteChanged(resources: Resources) {}
+
+ /** Call whenever the weather data should update */
+ fun onWeatherDataChanged(data: Weather) {}
}
/** Methods which trigger various clock animations */
@@ -131,6 +145,13 @@
/** Events that have specific data about the related face */
interface ClockFaceEvents {
+ /** Call every time tick */
+ fun onTimeTick() {}
+
+ /** Expected interval between calls to onTimeTick. Can always reduce to PER_MINUTE in AOD. */
+ val tickRate: ClockTickRate
+ get() = ClockTickRate.PER_MINUTE
+
/** Region Darkness specific to the clock face */
fun onRegionDarknessChanged(isDark: Boolean) {}
@@ -150,8 +171,46 @@
fun onTargetRegionChanged(targetRegion: Rect?) {}
}
+/** Tick rates for clocks */
+enum class ClockTickRate(val value: Int) {
+ PER_MINUTE(2), // Update the clock once per minute.
+ PER_SECOND(1), // Update the clock once per second.
+ PER_FRAME(0), // Update the clock every second.
+}
+
/** Some data about a clock design */
data class ClockMetadata(
val clockId: ClockId,
val name: String,
)
+
+/** Structure for keeping clock-specific settings */
+@Keep
+data class ClockSettings(
+ var clockId: ClockId? = null,
+ var seedColor: Int? = null,
+ var _applied_timestamp: Long? = null,
+) {
+ companion object {
+ private val KEY_CLOCK_ID = "clockId"
+ private val KEY_SEED_COLOR = "seedColor"
+ private val KEY_TIMESTAMP = "_applied_timestamp"
+
+ fun serialize(setting: ClockSettings): String {
+ return JSONObject()
+ .put(KEY_CLOCK_ID, setting.clockId)
+ .put(KEY_SEED_COLOR, setting.seedColor)
+ .put(KEY_TIMESTAMP, setting._applied_timestamp)
+ .toString()
+ }
+
+ fun deserialize(jsonStr: String): ClockSettings {
+ val json = JSONObject(jsonStr)
+ return ClockSettings(
+ json.getString(KEY_CLOCK_ID),
+ if (!json.isNull(KEY_SEED_COLOR)) json.getInt(KEY_SEED_COLOR) else null,
+ if (!json.isNull(KEY_TIMESTAMP)) json.getLong(KEY_TIMESTAMP) else null
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/Weather.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/Weather.kt
new file mode 100644
index 0000000..85ec42d
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/Weather.kt
@@ -0,0 +1,85 @@
+package com.android.systemui.statusbar
+
+import android.os.Bundle
+
+class Weather(val conditions: WeatherStateIcon, val temperature: Int, val isCelsius: Boolean) {
+ companion object {
+ private const val TAG = "Weather"
+ private const val WEATHER_STATE_ICON_KEY = "weather_state_icon_extra_key"
+ private const val TEMPERATURE_VALUE_KEY = "temperature_value_extra_key"
+ private const val TEMPERATURE_UNIT_KEY = "temperature_unit_extra_key"
+ private const val INVALID_TEMPERATURE = Int.MIN_VALUE
+
+ fun fromBundle(extras: Bundle): Weather? {
+ val icon =
+ WeatherStateIcon.fromInt(
+ extras.getInt(WEATHER_STATE_ICON_KEY, WeatherStateIcon.UNKNOWN_ICON.id)
+ )
+ if (icon == null || icon == WeatherStateIcon.UNKNOWN_ICON) {
+ return null
+ }
+ val temperature = extras.getInt(TEMPERATURE_VALUE_KEY, INVALID_TEMPERATURE)
+ if (temperature == INVALID_TEMPERATURE) {
+ return null
+ }
+ return Weather(icon, temperature, extras.getBoolean(TEMPERATURE_UNIT_KEY))
+ }
+ }
+
+ enum class WeatherStateIcon(val id: Int) {
+ UNKNOWN_ICON(0),
+
+ // Clear, day & night.
+ SUNNY(1),
+ CLEAR_NIGHT(2),
+
+ // Mostly clear, day & night.
+ MOSTLY_SUNNY(3),
+ MOSTLY_CLEAR_NIGHT(4),
+
+ // Partly cloudy, day & night.
+ PARTLY_CLOUDY(5),
+ PARTLY_CLOUDY_NIGHT(6),
+
+ // Mostly cloudy, day & night.
+ MOSTLY_CLOUDY_DAY(7),
+ MOSTLY_CLOUDY_NIGHT(8),
+ CLOUDY(9),
+ HAZE_FOG_DUST_SMOKE(10),
+ DRIZZLE(11),
+ HEAVY_RAIN(12),
+ SHOWERS_RAIN(13),
+
+ // Scattered showers, day & night.
+ SCATTERED_SHOWERS_DAY(14),
+ SCATTERED_SHOWERS_NIGHT(15),
+
+ // Isolated scattered thunderstorms, day & night.
+ ISOLATED_SCATTERED_TSTORMS_DAY(16),
+ ISOLATED_SCATTERED_TSTORMS_NIGHT(17),
+ STRONG_TSTORMS(18),
+ BLIZZARD(19),
+ BLOWING_SNOW(20),
+ FLURRIES(21),
+ HEAVY_SNOW(22),
+
+ // Scattered snow showers, day & night.
+ SCATTERED_SNOW_SHOWERS_DAY(23),
+ SCATTERED_SNOW_SHOWERS_NIGHT(24),
+ SNOW_SHOWERS_SNOW(25),
+ MIXED_RAIN_HAIL_RAIN_SLEET(26),
+ SLEET_HAIL(27),
+ TORNADO(28),
+ TROPICAL_STORM_HURRICANE(29),
+ WINDY_BREEZY(30),
+ WINTRY_MIX_RAIN_SNOW(31);
+
+ companion object {
+ fun fromInt(value: Int) = values().firstOrNull { it.id == value }
+ }
+ }
+
+ override fun toString(): String {
+ return "$conditions $temperature${if (isCelsius) "C" else "F"}"
+ }
+}
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
index 64ece47..ca4028a 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
@@ -105,6 +105,7 @@
android:id="@+id/key1"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key2"
androidprv:digit="1"
androidprv:textView="@+id/pinEntry" />
@@ -112,6 +113,7 @@
android:id="@+id/key2"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key3"
androidprv:digit="2"
androidprv:textView="@+id/pinEntry" />
@@ -119,6 +121,7 @@
android:id="@+id/key3"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key4"
androidprv:digit="3"
androidprv:textView="@+id/pinEntry" />
@@ -126,6 +129,7 @@
android:id="@+id/key4"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key5"
androidprv:digit="4"
androidprv:textView="@+id/pinEntry" />
@@ -133,6 +137,7 @@
android:id="@+id/key5"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key6"
androidprv:digit="5"
androidprv:textView="@+id/pinEntry" />
@@ -140,6 +145,7 @@
android:id="@+id/key6"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key7"
androidprv:digit="6"
androidprv:textView="@+id/pinEntry" />
@@ -147,13 +153,16 @@
android:id="@+id/key7"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key8"
androidprv:digit="7"
androidprv:textView="@+id/pinEntry" />
+
<com.android.keyguard.NumPadKey
android:id="@+id/key8"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key9"
androidprv:digit="8"
androidprv:textView="@+id/pinEntry" />
@@ -161,34 +170,33 @@
android:id="@+id/key9"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/delete_button"
androidprv:digit="9"
androidprv:textView="@+id/pinEntry" />
-
<com.android.keyguard.NumPadButton
android:id="@+id/delete_button"
+ style="@style/NumPadKey.Delete"
android:layout_width="0dp"
android:layout_height="0dp"
- style="@style/NumPadKey.Delete"
- android:contentDescription="@string/keyboardview_keycode_delete"
- />
+ android:accessibilityTraversalBefore="@id/key0"
+ android:contentDescription="@string/keyboardview_keycode_delete" />
<com.android.keyguard.NumPadKey
android:id="@+id/key0"
android:layout_width="0dp"
android:layout_height="0dp"
+ android:accessibilityTraversalBefore="@id/key_enter"
androidprv:digit="0"
androidprv:textView="@+id/pinEntry" />
<com.android.keyguard.NumPadButton
android:id="@+id/key_enter"
+ style="@style/NumPadKey.Enter"
android:layout_width="0dp"
android:layout_height="0dp"
- style="@style/NumPadKey.Enter"
- android:contentDescription="@string/keyboardview_keycode_enter"
- />
-
- </androidx.constraintlayout.widget.ConstraintLayout>
+ android:contentDescription="@string/keyboardview_keycode_enter" />
+</androidx.constraintlayout.widget.ConstraintLayout>
<include layout="@layout/keyguard_eca"
android:id="@+id/keyguard_selector_fade_container"
diff --git a/packages/SettingsLib/DisplayDensityUtils/AndroidManifest.xml b/packages/SystemUI/res/drawable/clipboard_minimized_background.xml
similarity index 64%
rename from packages/SettingsLib/DisplayDensityUtils/AndroidManifest.xml
rename to packages/SystemUI/res/drawable/clipboard_minimized_background.xml
index 0a4e2bb..a179c14 100644
--- a/packages/SettingsLib/DisplayDensityUtils/AndroidManifest.xml
+++ b/packages/SystemUI/res/drawable/clipboard_minimized_background.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2019 The Android Open Source Project
+ ~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -14,8 +14,10 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.settingslib.display">
-
-</manifest>
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <solid android:color="?androidprv:attr/colorAccentSecondary"/>
+ <corners android:radius="10dp"/>
+</shape>
diff --git a/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml b/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml
new file mode 100644
index 0000000..dad2cdf8e
--- /dev/null
+++ b/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml
@@ -0,0 +1,32 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="56dp"
+ android:height="24dp"
+ android:viewportWidth="56"
+ android:viewportHeight="24">
+ <group>
+ <clip-path
+ android:pathData="M12 0H44C50.6274 0 56 5.37258 56 12C56 18.6274 50.6274 24 44 24H12C5.37258 24 0 18.6274 0 12C0 5.37258 5.37258 0 12 0Z"
+ />
+ <path
+ android:pathData="M0 0V24H56V0"
+ android:fillColor="#FFFFFF"
+ />
+ </group>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_content_paste.xml b/packages/SystemUI/res/drawable/ic_content_paste.xml
new file mode 100644
index 0000000..8c8b81e
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_content_paste.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M9,42Q7.7,42 6.85,41.15Q6,40.3 6,39V9Q6,7.7 6.85,6.85Q7.7,6 9,6H19.1Q19.45,4.25 20.825,3.125Q22.2,2 24,2Q25.8,2 27.175,3.125Q28.55,4.25 28.9,6H39Q40.3,6 41.15,6.85Q42,7.7 42,9V39Q42,40.3 41.15,41.15Q40.3,42 39,42ZM9,39H39Q39,39 39,39Q39,39 39,39V9Q39,9 39,9Q39,9 39,9H36V13.5H12V9H9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39ZM24,9Q24.85,9 25.425,8.425Q26,7.85 26,7Q26,6.15 25.425,5.575Q24.85,5 24,5Q23.15,5 22.575,5.575Q22,6.15 22,7Q22,7.85 22.575,8.425Q23.15,9 24,9Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/media_output_status_edit_session.xml b/packages/SystemUI/res/drawable/media_output_status_edit_session.xml
new file mode 100644
index 0000000..0bd45ed
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_status_edit_session.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M1,20V17.2Q1,16.35 1.438,15.637Q1.875,14.925 2.6,14.55Q4.15,13.775 5.75,13.387Q7.35,13 9,13Q10.65,13 12.25,13.387Q13.85,13.775 15.4,14.55Q16.125,14.925 16.562,15.637Q17,16.35 17,17.2V20ZM19,20V17Q19,15.9 18.388,14.887Q17.775,13.875 16.65,13.15Q17.925,13.3 19.05,13.662Q20.175,14.025 21.15,14.55Q22.05,15.05 22.525,15.662Q23,16.275 23,17V20ZM9,12Q7.35,12 6.175,10.825Q5,9.65 5,8Q5,6.35 6.175,5.175Q7.35,4 9,4Q10.65,4 11.825,5.175Q13,6.35 13,8Q13,9.65 11.825,10.825Q10.65,12 9,12ZM19,8Q19,9.65 17.825,10.825Q16.65,12 15,12Q14.725,12 14.3,11.938Q13.875,11.875 13.6,11.8Q14.275,11 14.637,10.025Q15,9.05 15,8Q15,6.95 14.637,5.975Q14.275,5 13.6,4.2Q13.95,4.075 14.3,4.037Q14.65,4 15,4Q16.65,4 17.825,5.175Q19,6.35 19,8Z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/media_output_status_help.xml b/packages/SystemUI/res/drawable/media_output_status_help.xml
new file mode 100644
index 0000000..f93c07ec
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_status_help.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@color/media_dialog_item_main_content"
+ android:pathData="M11.95,18Q12.475,18 12.838,17.637Q13.2,17.275 13.2,16.75Q13.2,16.225 12.838,15.863Q12.475,15.5 11.95,15.5Q11.425,15.5 11.062,15.863Q10.7,16.225 10.7,16.75Q10.7,17.275 11.062,17.637Q11.425,18 11.95,18ZM11.05,14.15H12.9Q12.9,13.325 13.088,12.85Q13.275,12.375 14.15,11.55Q14.8,10.9 15.175,10.312Q15.55,9.725 15.55,8.9Q15.55,7.5 14.525,6.75Q13.5,6 12.1,6Q10.675,6 9.788,6.75Q8.9,7.5 8.55,8.55L10.2,9.2Q10.325,8.75 10.763,8.225Q11.2,7.7 12.1,7.7Q12.9,7.7 13.3,8.137Q13.7,8.575 13.7,9.1Q13.7,9.6 13.4,10.037Q13.1,10.475 12.65,10.85Q11.55,11.825 11.3,12.325Q11.05,12.825 11.05,14.15ZM12,22Q9.925,22 8.1,21.212Q6.275,20.425 4.925,19.075Q3.575,17.725 2.788,15.9Q2,14.075 2,12Q2,9.925 2.788,8.1Q3.575,6.275 4.925,4.925Q6.275,3.575 8.1,2.787Q9.925,2 12,2Q14.075,2 15.9,2.787Q17.725,3.575 19.075,4.925Q20.425,6.275 21.212,8.1Q22,9.925 22,12Q22,14.075 21.212,15.9Q20.425,17.725 19.075,19.075Q17.725,20.425 15.9,21.212Q14.075,22 12,22Z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/media_output_status_session.xml b/packages/SystemUI/res/drawable/media_output_status_session.xml
new file mode 100644
index 0000000..1deba1e
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_status_session.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M4,20V12H8V20ZM10,20V4H14V20ZM16,20V9H20V20Z"/>
+</vector>
diff --git a/packages/SystemUI/res/layout/app_clips_screenshot.xml b/packages/SystemUI/res/layout/app_clips_screenshot.xml
new file mode 100644
index 0000000..5155b77
--- /dev/null
+++ b/packages/SystemUI/res/layout/app_clips_screenshot.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:background="@null"
+ android:id="@+id/root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <Button
+ android:id="@+id/save"
+ style="@android:style/Widget.DeviceDefault.Button.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:text="@string/app_clips_save_add_to_note"
+ android:layout_marginStart="8dp"
+ android:background="@drawable/overlay_button_background"
+ android:textColor="?android:textColorSecondary"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/preview" />
+
+ <Button
+ android:id="@+id/cancel"
+ style="@android:style/Widget.DeviceDefault.Button.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:text="@android:string/cancel"
+ android:layout_marginStart="6dp"
+ android:background="@drawable/overlay_button_background"
+ android:textColor="?android:textColorSecondary"
+ app:layout_constraintStart_toEndOf="@id/save"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/preview" />
+
+ <ImageView
+ android:id="@+id/preview"
+ android:layout_width="0px"
+ android:layout_height="0px"
+ android:paddingHorizontal="48dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="42dp"
+ android:contentDescription="@string/screenshot_preview_description"
+ app:layout_constrainedHeight="true"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintTop_toBottomOf="@id/save"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ tools:background="?android:colorBackground"
+ tools:minHeight="100dp"
+ tools:minWidth="100dp" />
+
+ <com.android.systemui.screenshot.CropView
+ android:id="@+id/crop_view"
+ android:layout_width="0px"
+ android:layout_height="0px"
+ android:paddingTop="8dp"
+ android:paddingBottom="42dp"
+ app:layout_constrainedHeight="true"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintTop_toTopOf="@id/preview"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:handleThickness="@dimen/screenshot_crop_handle_thickness"
+ app:handleColor="?android:attr/colorAccent"
+ app:scrimColor="?android:colorBackgroundFloating"
+ app:scrimAlpha="128"
+ app:containerBackgroundColor="?android:colorBackgroundFloating"
+ tools:background="?android:colorBackground"
+ tools:minHeight="100dp"
+ tools:minWidth="100dp" />
+
+ <com.android.systemui.screenshot.MagnifierView
+ android:id="@+id/magnifier"
+ android:visibility="invisible"
+ android:layout_width="200dp"
+ android:layout_height="200dp"
+ android:elevation="2dp"
+ app:layout_constraintTop_toTopOf="@id/preview"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:handleThickness="@dimen/screenshot_crop_handle_thickness"
+ app:handleColor="?android:attr/colorAccent"
+ app:scrimColor="?android:colorBackgroundFloating"
+ app:scrimAlpha="128"
+ app:borderThickness="4dp"
+ app:borderColor="#fff" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml
index eec3b11..9b01bd8 100644
--- a/packages/SystemUI/res/layout/clipboard_overlay.xml
+++ b/packages/SystemUI/res/layout/clipboard_overlay.xml
@@ -125,6 +125,45 @@
android:layout_width="@dimen/clipboard_preview_size"
android:layout_height="@dimen/clipboard_preview_size"/>
</FrameLayout>
+ <LinearLayout
+ android:id="@+id/minimized_preview"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:elevation="7dp"
+ android:padding="8dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
+ android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
+ android:background="@drawable/clipboard_minimized_background">
+ <ImageView
+ android:src="@drawable/ic_content_paste"
+ android:tint="?attr/overlayButtonTextColor"
+ android:layout_width="24dp"
+ android:layout_height="24dp"/>
+ <ImageView
+ android:src="@*android:drawable/ic_chevron_end"
+ android:tint="?attr/overlayButtonTextColor"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:paddingEnd="-8dp"
+ android:paddingStart="-4dp"/>
+ </LinearLayout>
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/clipboard_content_top"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:barrierDirection="top"
+ app:constraint_referenced_ids="clipboard_preview,minimized_preview"/>
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/clipboard_content_end"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:barrierDirection="end"
+ app:constraint_referenced_ids="clipboard_preview,minimized_preview"/>
<FrameLayout
android:id="@+id/dismiss_button"
android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
@@ -132,10 +171,10 @@
android:elevation="10dp"
android:visibility="gone"
android:alpha="0"
- app:layout_constraintStart_toEndOf="@id/clipboard_preview"
- app:layout_constraintEnd_toEndOf="@id/clipboard_preview"
- app:layout_constraintTop_toTopOf="@id/clipboard_preview"
- app:layout_constraintBottom_toTopOf="@id/clipboard_preview"
+ app:layout_constraintStart_toEndOf="@id/clipboard_content_end"
+ app:layout_constraintEnd_toEndOf="@id/clipboard_content_end"
+ app:layout_constraintTop_toTopOf="@id/clipboard_content_top"
+ app:layout_constraintBottom_toTopOf="@id/clipboard_content_top"
android:contentDescription="@string/clipboard_dismiss_description">
<ImageView
android:id="@+id/dismiss_image"
diff --git a/packages/SystemUI/res/layout/controls_with_favorites.xml b/packages/SystemUI/res/layout/controls_with_favorites.xml
index ee3adba..aa211bf 100644
--- a/packages/SystemUI/res/layout/controls_with_favorites.xml
+++ b/packages/SystemUI/res/layout/controls_with_favorites.xml
@@ -20,7 +20,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
- android:layout_marginTop="@dimen/controls_top_margin"
android:layout_marginBottom="@dimen/controls_header_bottom_margin">
<!-- make sure the header stays centered in the layout by adding a spacer -->
@@ -78,6 +77,7 @@
android:layout_weight="1"
android:orientation="vertical"
android:clipChildren="true"
+ android:paddingHorizontal="16dp"
android:scrollbars="none">
<include layout="@layout/global_actions_controls_list_view" />
@@ -88,10 +88,7 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
- android:layout_marginLeft="@dimen/global_actions_side_margin"
- android:layout_marginRight="@dimen/global_actions_side_margin"
android:background="@drawable/controls_panel_background"
- android:padding="@dimen/global_actions_side_margin"
android:visibility="gone"
/>
</merge>
diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
index 885e5e2..50dcaf3 100644
--- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
@@ -111,5 +111,15 @@
android:visibility="gone"
android:contentDescription="@string/dream_overlay_status_bar_camera_mic_off" />
+ <ImageView
+ android:id="@+id/dream_overlay_assistant_attention_indicator"
+ android:layout_width="@dimen/dream_overlay_grey_chip_width"
+ android:layout_height="match_parent"
+ android:layout_marginStart="@dimen/dream_overlay_status_icon_margin"
+ android:src="@drawable/dream_overlay_assistant_attention_indicator"
+ android:visibility="gone"
+ android:contentDescription=
+ "@string/dream_overlay_status_bar_assistant_attention_indicator" />
+
</LinearLayout>
</com.android.systemui.dreams.DreamOverlayStatusBarView>
diff --git a/packages/SystemUI/res/layout/keyguard_user_switcher_item.xml b/packages/SystemUI/res/layout/keyguard_user_switcher_item.xml
index aaa372a..e39f1a9 100644
--- a/packages/SystemUI/res/layout/keyguard_user_switcher_item.xml
+++ b/packages/SystemUI/res/layout/keyguard_user_switcher_item.xml
@@ -18,6 +18,7 @@
<!-- LinearLayout -->
<com.android.systemui.statusbar.policy.KeyguardUserDetailItemView
+ android:id="@+id/user_item"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
diff --git a/packages/SystemUI/res/layout/media_output_list_item_advanced.xml b/packages/SystemUI/res/layout/media_output_list_item_advanced.xml
index d49b9f1..a650512 100644
--- a/packages/SystemUI/res/layout/media_output_list_item_advanced.xml
+++ b/packages/SystemUI/res/layout/media_output_list_item_advanced.xml
@@ -152,5 +152,16 @@
android:button="@drawable/media_output_item_check_box"
android:visibility="gone"
/>
+ <ImageView
+ android:id="@+id/media_output_item_end_click_icon"
+ android:src="@drawable/media_output_status_edit_session"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:focusable="false"
+ android:importantForAccessibility="no"
+ android:layout_gravity="center"
+ android:indeterminate="true"
+ android:indeterminateOnly="true"
+ android:visibility="gone"/>
</FrameLayout>
</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/media_recommendation_view.xml b/packages/SystemUI/res/layout/media_recommendation_view.xml
index 101fad9..c54c4e4 100644
--- a/packages/SystemUI/res/layout/media_recommendation_view.xml
+++ b/packages/SystemUI/res/layout/media_recommendation_view.xml
@@ -47,7 +47,8 @@
android:singleLine="true"
android:textSize="12sp"
android:gravity="top"
- android:layout_gravity="bottom"/>
+ android:layout_gravity="bottom"
+ android:importantForAccessibility="no"/>
<!-- Album name -->
<TextView
@@ -61,5 +62,26 @@
android:singleLine="true"
android:textSize="11sp"
android:gravity="center_vertical"
- android:layout_gravity="bottom"/>
+ android:layout_gravity="bottom"
+ android:importantForAccessibility="no"/>
+
+ <!-- Seek Bar -->
+ <SeekBar
+ android:id="@+id/media_progress_bar"
+ android:layout_width="match_parent"
+ android:layout_height="12dp"
+ android:layout_gravity="bottom"
+ android:maxHeight="@dimen/qs_media_enabled_seekbar_height"
+ android:thumb="@android:color/transparent"
+ android:splitTrack="false"
+ android:clickable="false"
+ android:progressTint="?android:attr/textColorPrimary"
+ android:progressBackgroundTint="?android:attr/textColorTertiary"
+ android:paddingTop="5dp"
+ android:paddingBottom="5dp"
+ android:paddingStart="0dp"
+ android:paddingEnd="0dp"
+ android:layout_marginEnd="@dimen/qs_media_info_spacing"
+ android:layout_marginStart="@dimen/qs_media_info_spacing"
+ android:layout_marginBottom="@dimen/qs_media_info_spacing"/>
</merge>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/qs_user_detail_item.xml b/packages/SystemUI/res/layout/qs_user_detail_item.xml
index 7c86bc7..ad129e8 100644
--- a/packages/SystemUI/res/layout/qs_user_detail_item.xml
+++ b/packages/SystemUI/res/layout/qs_user_detail_item.xml
@@ -18,6 +18,7 @@
<!-- LinearLayout -->
<com.android.systemui.qs.tiles.UserDetailItemView
+ android:id="@+id/user_item"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml
index a748e29..7e9202c 100644
--- a/packages/SystemUI/res/layout/screenshot_static.xml
+++ b/packages/SystemUI/res/layout/screenshot_static.xml
@@ -149,6 +149,8 @@
app:layout_constraintTop_toBottomOf="@id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintWidth_max="450dp"
+ app:layout_constraintHorizontal_bias="0"
>
<include layout="@layout/screenshot_work_profile_first_run" />
<include layout="@layout/screenshot_detection_notice" />
diff --git a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
index c181724..392d845 100644
--- a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
+++ b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
@@ -4,12 +4,16 @@
android:id="@+id/work_profile_first_run"
android:layout_height="wrap_content"
android:layout_width="match_parent"
+ android:paddingStart="16dp"
+ android:paddingEnd="4dp"
+ android:paddingVertical="16dp"
android:visibility="gone">
<ImageView
android:id="@+id/screenshot_message_icon"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:paddingEnd="4dp"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_marginEnd="12dp"
+ android:layout_gravity="center_vertical"
android:src="@drawable/ic_work_app_badge"/>
<TextView
@@ -17,7 +21,11 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
- android:layout_gravity="start"/>
+ android:layout_gravity="start|center_vertical"
+ android:textSize="18sp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:lineHeight="24sp"
+ />
<FrameLayout
android:id="@+id/message_dismiss_button"
@@ -25,9 +33,9 @@
android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
android:contentDescription="@string/screenshot_dismiss_work_profile">
<ImageView
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_margin="@dimen/overlay_dismiss_button_margin"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="center"
android:src="@drawable/overlay_cancel"/>
</FrameLayout>
</LinearLayout>
diff --git a/packages/SystemUI/res/layout/udfps_fpm_empty_view.xml b/packages/SystemUI/res/layout/udfps_fpm_empty_view.xml
index de43a5e..4799f8c 100644
--- a/packages/SystemUI/res/layout/udfps_fpm_empty_view.xml
+++ b/packages/SystemUI/res/layout/udfps_fpm_empty_view.xml
@@ -19,4 +19,12 @@
android:id="@+id/udfps_animation_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
+ <!-- The layout height/width are placeholders, which will be overwritten by
+ FingerprintSensorPropertiesInternal. -->
+ <View
+ android:id="@+id/udfps_enroll_accessibility_view"
+ android:layout_gravity="center"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/accessibility_fingerprint_label"/>
</com.android.systemui.biometrics.UdfpsFpmEmptyView>
diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml
index fff2544..ac81dcc 100644
--- a/packages/SystemUI/res/values-land/dimens.xml
+++ b/packages/SystemUI/res/values-land/dimens.xml
@@ -64,4 +64,6 @@
<dimen name="qs_panel_padding_top">@dimen/qqs_layout_margin_top</dimen>
<dimen name="qs_panel_padding_top_combined_headers">@dimen/qs_panel_padding_top</dimen>
+
+ <dimen name="controls_padding_horizontal">16dp</dimen>
</resources>
diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml
index db7fb48..4f24d83 100644
--- a/packages/SystemUI/res/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp/dimens.xml
@@ -51,9 +51,6 @@
<!-- Text size for user name in user switcher -->
<dimen name="kg_user_switcher_text_size">18sp</dimen>
- <dimen name="controls_header_bottom_margin">12dp</dimen>
- <dimen name="controls_top_margin">24dp</dimen>
-
<dimen name="global_actions_grid_item_layout_height">80dp</dimen>
<dimen name="qs_brightness_margin_bottom">16dp</dimen>
diff --git a/packages/SystemUI/res/values-sw720dp-land/dimens.xml b/packages/SystemUI/res/values-sw720dp-land/dimens.xml
index 122806a..2b88e55 100644
--- a/packages/SystemUI/res/values-sw720dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp-land/dimens.xml
@@ -17,7 +17,6 @@
*/
-->
<resources>
- <dimen name="controls_padding_horizontal">205dp</dimen>
<dimen name="split_shade_notifications_scrim_margin_bottom">24dp</dimen>
<dimen name="notification_panel_margin_bottom">64dp</dimen>
diff --git a/packages/SystemUI/res/values-sw720dp/dimens.xml b/packages/SystemUI/res/values-sw720dp/dimens.xml
index 927059a..8f59df6 100644
--- a/packages/SystemUI/res/values-sw720dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp/dimens.xml
@@ -19,7 +19,7 @@
<!-- gap on either side of status bar notification icons -->
<dimen name="status_bar_icon_padding">1dp</dimen>
- <dimen name="controls_padding_horizontal">75dp</dimen>
+ <dimen name="controls_padding_horizontal">40dp</dimen>
<dimen name="large_screen_shade_header_height">56dp</dimen>
diff --git a/packages/SystemUI/res/values-television/config.xml b/packages/SystemUI/res/values-television/config.xml
index 375bd39..0ca154e 100644
--- a/packages/SystemUI/res/values-television/config.xml
+++ b/packages/SystemUI/res/values-television/config.xml
@@ -46,4 +46,7 @@
<!-- Whether to tint the icon of the sensor hardware privacy toggle unblock dialog.
Set to false if using a custom icon. -->
<bool name="config_unblockHwSensorIconEnableTint">true</bool>
+
+ <!-- Configuration to set Learn more in device logs as URL link -->
+ <bool name="log_access_confirmation_learn_more_as_link">false</bool>
</resources>
diff --git a/packages/SystemUI/res/values-television/strings.xml b/packages/SystemUI/res/values-television/strings.xml
deleted file mode 100644
index f30b73e..0000000
--- a/packages/SystemUI/res/values-television/strings.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/**
- * 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.
- */
--->
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <!-- Content for the log access confirmation dialog. [CHAR LIMIT=NONE]-->
- <string name="log_access_confirmation_body">Device logs record what happens on your device. Apps can use these logs to find and fix issues.\n\nSome logs may contain sensitive info, so only allow apps you trust to access all device logs.
- \n\nIf you don’t allow this app to access all device logs, it can still access its own logs. Your device manufacturer may still be able to access some logs or info on your device.\n\nLearn more at g.co/android/devicelogs.
- </string>
-
- <!-- Learn more URL for the log access confirmation dialog. [DO NOT TRANSLATE]-->
- <string name="log_access_confirmation_learn_more" translatable="false"></string>
-</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index f5db5ec..050b1e1 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -445,6 +445,13 @@
screenshot has been saved to work profile. If blank, a default icon will be shown. -->
<string name="config_sceenshotWorkProfileFilesApp" translatable="false"></string>
+ <!-- The component name of the screenshot editing activity that provides the App Clips flow.
+ The App Clips flow includes taking a screenshot, showing user screenshot cropping activity
+ and finally letting user send the screenshot to the calling notes app. This activity
+ should not send the screenshot to the calling activity without user consent. -->
+ <string name="config_screenshotAppClipsActivityComponent" translatable="false"
+ >com.android.systemui/com.android.systemui.screenshot.AppClipsActivity</string>
+
<!-- Remote copy default activity. Must handle REMOTE_COPY_ACTION intents.
This name is in the ComponentName flattened format (package/class) -->
<string name="config_remoteCopyPackage" translatable="false"></string>
@@ -835,4 +842,7 @@
<!-- Whether the floating rotation button should be on the left/right in the device's natural
orientation -->
<bool name="floating_rotation_button_position_left">true</bool>
+
+ <!-- Configuration to set Learn more in device logs as URL link -->
+ <bool name="log_access_confirmation_learn_more_as_link">true</bool>
</resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index ebf232f..2df2513 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -43,12 +43,18 @@
<dimen name="navigation_edge_panel_height">268dp</dimen>
<!-- The threshold to drag to trigger the edge action -->
<dimen name="navigation_edge_action_drag_threshold">16dp</dimen>
+ <!-- The drag distance to consider evaluating gesture -->
+ <dimen name="navigation_edge_action_min_distance_to_start_animation">24dp</dimen>
<!-- The threshold to progress back animation for edge swipe -->
<dimen name="navigation_edge_action_progress_threshold">412dp</dimen>
<!-- The minimum display position of the arrow on the screen -->
<dimen name="navigation_edge_arrow_min_y">64dp</dimen>
<!-- The amount by which the arrow is shifted to avoid the finger-->
<dimen name="navigation_edge_finger_offset">64dp</dimen>
+ <!-- The threshold to dynamically activate the edge action -->
+ <dimen name="navigation_edge_action_reactivation_drag_threshold">32dp</dimen>
+ <!-- The threshold to dynamically deactivate the edge action -->
+ <dimen name="navigation_edge_action_deactivation_drag_threshold">32dp</dimen>
<!-- The thickness of the arrow -->
<dimen name="navigation_edge_arrow_thickness">4dp</dimen>
@@ -56,37 +62,61 @@
<dimen name="navigation_edge_minimum_x_delta_for_switch">32dp</dimen>
<!-- entry state -->
+ <item name="navigation_edge_entry_scale" format="float" type="dimen">0.98</item>
<dimen name="navigation_edge_entry_margin">4dp</dimen>
- <dimen name="navigation_edge_entry_background_width">8dp</dimen>
- <dimen name="navigation_edge_entry_background_height">60dp</dimen>
- <dimen name="navigation_edge_entry_edge_corners">30dp</dimen>
- <dimen name="navigation_edge_entry_far_corners">30dp</dimen>
- <dimen name="navigation_edge_entry_arrow_length">10dp</dimen>
- <dimen name="navigation_edge_entry_arrow_height">7dp</dimen>
+ <item name="navigation_edge_entry_background_alpha" format="float" type="dimen">1.0</item>
+ <dimen name="navigation_edge_entry_background_width">0dp</dimen>
+ <dimen name="navigation_edge_entry_background_height">48dp</dimen>
+ <dimen name="navigation_edge_entry_edge_corners">6dp</dimen>
+ <dimen name="navigation_edge_entry_far_corners">6dp</dimen>
+ <item name="navigation_edge_entry_arrow_alpha" format="float" type="dimen">0.0</item>
+ <dimen name="navigation_edge_entry_arrow_length">8.6dp</dimen>
+ <dimen name="navigation_edge_entry_arrow_height">5dp</dimen>
<!-- pre-threshold -->
<dimen name="navigation_edge_pre_threshold_margin">4dp</dimen>
- <dimen name="navigation_edge_pre_threshold_background_width">64dp</dimen>
- <dimen name="navigation_edge_pre_threshold_background_height">60dp</dimen>
- <dimen name="navigation_edge_pre_threshold_edge_corners">22dp</dimen>
- <dimen name="navigation_edge_pre_threshold_far_corners">26dp</dimen>
+ <item name="navigation_edge_pre_threshold_background_alpha" format="float" type="dimen">1.0
+ </item>
+ <item name="navigation_edge_pre_threshold_scale" format="float" type="dimen">0.98</item>
+ <dimen name="navigation_edge_pre_threshold_background_width">51dp</dimen>
+ <dimen name="navigation_edge_pre_threshold_background_height">46dp</dimen>
+ <dimen name="navigation_edge_pre_threshold_edge_corners">16dp</dimen>
+ <dimen name="navigation_edge_pre_threshold_far_corners">20dp</dimen>
+ <item name="navigation_edge_pre_threshold_arrow_alpha" format="float" type="dimen">1.0</item>
+ <dimen name="navigation_edge_pre_threshold_arrow_length">8dp</dimen>
+ <dimen name="navigation_edge_pre_threshold_arrow_height">5.6dp</dimen>
- <!-- post-threshold / active -->
+ <!-- active (post-threshold) -->
+ <item name="navigation_edge_active_scale" format="float" type="dimen">1.0</item>
<dimen name="navigation_edge_active_margin">14dp</dimen>
- <dimen name="navigation_edge_active_background_width">60dp</dimen>
- <dimen name="navigation_edge_active_background_height">60dp</dimen>
- <dimen name="navigation_edge_active_edge_corners">30dp</dimen>
- <dimen name="navigation_edge_active_far_corners">30dp</dimen>
- <dimen name="navigation_edge_active_arrow_length">8dp</dimen>
- <dimen name="navigation_edge_active_arrow_height">9dp</dimen>
+ <item name="navigation_edge_active_background_alpha" format="float" type="dimen">1.0</item>
+ <dimen name="navigation_edge_active_background_width">48dp</dimen>
+ <dimen name="navigation_edge_active_background_height">48dp</dimen>
+ <dimen name="navigation_edge_active_edge_corners">24dp</dimen>
+ <dimen name="navigation_edge_active_far_corners">24dp</dimen>
+ <item name="navigation_edge_active_arrow_alpha" format="float" type="dimen">1.0</item>
+ <dimen name="navigation_edge_active_arrow_length">6.4dp</dimen>
+ <dimen name="navigation_edge_active_arrow_height">7.2dp</dimen>
+ <!-- committed -->
+ <item name="navigation_edge_committed_scale" format="float" type="dimen">0.85</item>
+ <item name="navigation_edge_committed_alpha" format="float" type="dimen">0</item>
+
+ <!-- cancelled -->
+ <dimen name="navigation_edge_cancelled_background_width">0dp</dimen>
+
+ <item name="navigation_edge_stretch_scale" format="float" type="dimen">1.0</item>
<dimen name="navigation_edge_stretch_margin">18dp</dimen>
- <dimen name="navigation_edge_stretch_background_width">74dp</dimen>
- <dimen name="navigation_edge_stretch_background_height">60dp</dimen>
- <dimen name="navigation_edge_stretch_edge_corners">30dp</dimen>
- <dimen name="navigation_edge_stretch_far_corners">30dp</dimen>
- <dimen name="navigation_edge_stretched_arrow_length">7dp</dimen>
- <dimen name="navigation_edge_stretched_arrow_height">10dp</dimen>
+ <dimen name="navigation_edge_stretch_background_width">60dp</dimen>
+ <item name="navigation_edge_stretch_background_alpha" format="float" type="dimen">
+ @dimen/navigation_edge_entry_background_alpha
+ </item>
+ <dimen name="navigation_edge_stretch_background_height">48dp</dimen>
+ <dimen name="navigation_edge_stretch_edge_corners">24dp</dimen>
+ <dimen name="navigation_edge_stretch_far_corners">24dp</dimen>
+ <item name="navigation_edge_strech_arrow_alpha" format="float" type="dimen">1.0</item>
+ <dimen name="navigation_edge_stretched_arrow_length">5.6dp</dimen>
+ <dimen name="navigation_edge_stretched_arrow_height">8dp</dimen>
<dimen name="navigation_edge_cancelled_arrow_length">12dp</dimen>
<dimen name="navigation_edge_cancelled_arrow_height">0dp</dimen>
@@ -1156,7 +1186,7 @@
<!-- Home Controls -->
<dimen name="controls_header_menu_size">48dp</dimen>
- <dimen name="controls_header_bottom_margin">24dp</dimen>
+ <dimen name="controls_header_bottom_margin">16dp</dimen>
<dimen name="controls_header_app_icon_size">24dp</dimen>
<dimen name="controls_top_margin">48dp</dimen>
<dimen name="controls_padding_horizontal">0dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 16bffb7..8d95164 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -240,13 +240,15 @@
<!-- Content description for the right boundary of the screenshot being cropped, with the current position as a percentage. [CHAR LIMIT=NONE] -->
<string name="screenshot_right_boundary_pct">Right boundary <xliff:g id="percent" example="50">%1$d</xliff:g> percent</string>
<!-- Notification displayed when a screenshot is saved in a work profile. [CHAR LIMIT=NONE] -->
- <string name="screenshot_work_profile_notification">Work screenshots are saved in the <xliff:g id="app" example="Work Files">%1$s</xliff:g> app</string>
+ <string name="screenshot_work_profile_notification">Saved in <xliff:g id="app" example="Files">%1$s</xliff:g> in the work profile</string>
<!-- Default name referring to the app on the device that lets the user browse stored files. [CHAR LIMIT=NONE] -->
<string name="screenshot_default_files_app_name">Files</string>
<!-- A notice shown to the user to indicate that an app has detected the screenshot that the user has just taken. [CHAR LIMIT=75] -->
<string name="screenshot_detected_template"><xliff:g id="appName" example="Google Chrome">%1$s</xliff:g> detected this screenshot.</string>
<!-- A notice shown to the user to indicate that multiple apps have detected the screenshot that the user has just taken. [CHAR LIMIT=75] -->
<string name="screenshot_detected_multiple_template"><xliff:g id="appName" example="Google Chrome">%1$s</xliff:g> and other open apps detected this screenshot.</string>
+ <!-- Add to note button used in App Clips flow to return the saved screenshot image to notes app. [CHAR LIMIT=NONE] -->
+ <string name="app_clips_save_add_to_note">Add to note</string>
<!-- Notification title displayed for screen recording [CHAR LIMIT=50]-->
<string name="screenrecord_name">Screen Recorder</string>
@@ -908,14 +910,6 @@
<!-- Message shown when non-bypass face authentication succeeds. [CHAR LIMIT=60] -->
<string name="keyguard_face_successful_unlock_alt1">Face recognized</string>
- <!-- Messages shown when users press outside of udfps region during -->
- <string-array name="udfps_accessibility_touch_hints">
- <item>Move left</item>
- <item>Move down</item>
- <item>Move right</item>
- <item>Move up</item>
- </string-array>
-
<!-- Message shown when face authentication fails and the pin pad is visible. [CHAR LIMIT=60] -->
<string name="keyguard_retry">Swipe up to try again</string>
@@ -2538,8 +2532,7 @@
<string name="media_output_group_title_speakers_and_displays">Speakers & Displays</string>
<!-- Title for Suggested Devices group. [CHAR LIMIT=NONE] -->
<string name="media_output_group_title_suggested_device">Suggested Devices</string>
- <!-- Sub status indicates device need premium account. [CHAR LIMIT=NONE] -->
- <string name="media_output_status_require_premium">Requires premium account</string>
+
<!-- Media Output Broadcast Dialog -->
<!-- Title for Broadcast First Notify Dialog [CHAR LIMIT=60] -->
@@ -2798,6 +2791,8 @@
<string name="dream_overlay_status_bar_mic_off">Mic is off</string>
<!-- Content description for the camera and mic off icon in the dream overlay status bar [CHAR LIMIT=NONE] -->
<string name="dream_overlay_status_bar_camera_mic_off">Camera and mic are off</string>
+ <!-- Content description for the assistant attention indicator [CHAR LIMIT=NONE] -->
+ <string name="dream_overlay_status_bar_assistant_attention_indicator">Assistant is listening</string>
<!-- Content description for the notifications indicator icon in the dream overlay status bar [CHAR LIMIT=NONE] -->
<string name="dream_overlay_status_bar_notification_indicator">{count, plural,
=1 {# notification}
@@ -2844,7 +2839,9 @@
</string>
<!-- Learn more URL for the log access confirmation dialog. [DO NOT TRANSLATE]-->
- <string name="log_access_confirmation_learn_more" translatable="false"><a href="https://support.google.com/android?p=system_logs#topic=7313011">Learn more</a></string>
+ <string name="log_access_confirmation_learn_more_url" translatable="false">https://support.google.com/android?p=system_logs#topic=7313011</string>
+ <string name="log_access_confirmation_learn_more">Learn more</string>
+ <string name="log_access_confirmation_learn_more_at">Learn more at <xliff:g id="url" example="g.co/android/devicelogs">%s</xliff:g></string>
<!--
Template for an action that opens a specific app. [CHAR LIMIT=16]
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index dd87e91..4998d68 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -758,6 +758,18 @@
</style>
<!-- Screenshots -->
+ <style name="AppClipsTrampolineActivity">
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowIsFloating">true</item>
+ <item name="android:backgroundDimEnabled">true</item>
+ </style>
+
+ <style name="AppClipsActivity" parent="LongScreenshotActivity">
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowIsTranslucent">true</item>
+ </style>
+
<style name="LongScreenshotActivity" parent="@android:style/Theme.DeviceDefault.DayNight">
<item name="android:windowNoTitle">true</item>
<item name="android:windowLightStatusBar">true</item>
diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml
index e56e5d5..00a0444 100644
--- a/packages/SystemUI/res/xml/qqs_header.xml
+++ b/packages/SystemUI/res/xml/qqs_header.xml
@@ -48,8 +48,7 @@
app:layout_constrainedWidth="true"
app:layout_constraintStart_toEndOf="@id/clock"
app:layout_constraintEnd_toStartOf="@id/barrier"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintBaseline_toBaselineOf="@id/clock"
app:layout_constraintHorizontal_bias="0"
/>
</Constraint>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
index b7e2494..44c0e16 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
@@ -88,6 +88,7 @@
final ArrayList<WindowContainerToken> pausingTasks = new ArrayList<>();
WindowContainerToken pipTask = null;
WindowContainerToken recentsTask = null;
+ int recentsTaskId = -1;
for (int i = apps.length - 1; i >= 0; --i) {
final ActivityManager.RunningTaskInfo taskInfo = apps[i].taskInfo;
if (apps[i].mode == MODE_CLOSING) {
@@ -106,8 +107,10 @@
// This task is for recents, keep it on top.
t.setLayer(apps[i].leash, info.getChanges().size() * 3 - i);
recentsTask = taskInfo.token;
+ recentsTaskId = taskInfo.taskId;
} else if (taskInfo != null && taskInfo.topActivityType == ACTIVITY_TYPE_HOME) {
recentsTask = taskInfo.token;
+ recentsTaskId = taskInfo.taskId;
}
}
// Also make all the wallpapers opaque since we want the visible from the start
@@ -116,7 +119,7 @@
}
t.apply();
mRecentsSession.setup(controller, info, finishedCallback, pausingTasks, pipTask,
- recentsTask, leashMap, mToken,
+ recentsTask, recentsTaskId, leashMap, mToken,
(info.getFlags() & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0);
recents.onAnimationStart(mRecentsSession, apps, wallpapers, new Rect(0, 0, 0, 0),
new Rect());
@@ -154,6 +157,7 @@
private ArrayList<WindowContainerToken> mPausingTasks = null;
private WindowContainerToken mPipTask = null;
private WindowContainerToken mRecentsTask = null;
+ private int mRecentsTaskId = 0;
private TransitionInfo mInfo = null;
private ArrayList<SurfaceControl> mOpeningLeashes = null;
private boolean mOpeningHome = false;
@@ -167,8 +171,8 @@
void setup(RecentsAnimationControllerCompat wrapped, TransitionInfo info,
IRemoteTransitionFinishedCallback finishCB,
ArrayList<WindowContainerToken> pausingTasks, WindowContainerToken pipTask,
- WindowContainerToken recentsTask, ArrayMap<SurfaceControl, SurfaceControl> leashMap,
- IBinder transition, boolean keyguardLocked) {
+ WindowContainerToken recentsTask, int recentsTaskId, ArrayMap<SurfaceControl,
+ SurfaceControl> leashMap, IBinder transition, boolean keyguardLocked) {
if (mInfo != null) {
throw new IllegalStateException("Trying to run a new recents animation while"
+ " recents is already active.");
@@ -179,6 +183,7 @@
mPausingTasks = pausingTasks;
mPipTask = pipTask;
mRecentsTask = recentsTask;
+ mRecentsTaskId = recentsTaskId;
mLeashMap = leashMap;
mTransition = transition;
mKeyguardLocked = keyguardLocked;
@@ -296,6 +301,15 @@
}
@Override public void setInputConsumerEnabled(boolean enabled) {
+ if (enabled) {
+ // transient launches don't receive focus automatically. Since we are taking over
+ // the gesture now, take focus explicitly.
+ try {
+ ActivityTaskManager.getService().setFocusedTask(mRecentsTaskId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to set focused task", e);
+ }
+ }
if (mWrapped != null) mWrapped.setInputConsumerEnabled(enabled);
}
diff --git a/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt b/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt
index 38fa354..54ae84f9 100644
--- a/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt
@@ -16,18 +16,20 @@
package com.android.keyguard
-import android.annotation.IntDef
import android.content.ContentResolver
import android.database.ContentObserver
import android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_TIMEOUT
import android.net.Uri
import android.os.Handler
+import android.os.PowerManager
+import android.os.PowerManager.WAKE_REASON_UNFOLD_DEVICE
import android.os.UserHandle
import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL
import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO
import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_FACE_ERRORS
import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT
import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED
+import android.provider.Settings.Secure.ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS
import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_WAKE
import android.util.Log
import com.android.keyguard.KeyguardUpdateMonitor.getCurrentUser
@@ -52,23 +54,26 @@
companion object {
const val TAG = "ActiveUnlockConfig"
-
- const val BIOMETRIC_TYPE_NONE = 0
- const val BIOMETRIC_TYPE_ANY_FACE = 1
- const val BIOMETRIC_TYPE_ANY_FINGERPRINT = 2
- const val BIOMETRIC_TYPE_UNDER_DISPLAY_FINGERPRINT = 3
}
- @Retention(AnnotationRetention.SOURCE)
- @IntDef(BIOMETRIC_TYPE_NONE, BIOMETRIC_TYPE_ANY_FACE, BIOMETRIC_TYPE_ANY_FINGERPRINT,
- BIOMETRIC_TYPE_UNDER_DISPLAY_FINGERPRINT)
- annotation class BiometricType
-
/**
* Indicates the origin for an active unlock request.
*/
- enum class ACTIVE_UNLOCK_REQUEST_ORIGIN {
- WAKE, UNLOCK_INTENT, BIOMETRIC_FAIL, ASSISTANT
+ enum class ActiveUnlockRequestOrigin {
+ WAKE,
+ UNLOCK_INTENT,
+ BIOMETRIC_FAIL,
+ ASSISTANT,
+ }
+
+ /**
+ * Biometric type options.
+ */
+ enum class BiometricType(val intValue: Int) {
+ NONE(0),
+ ANY_FACE(1),
+ ANY_FINGERPRINT(2),
+ UNDER_DISPLAY_FINGERPRINT(3),
}
var keyguardUpdateMonitor: KeyguardUpdateMonitor? = null
@@ -76,9 +81,10 @@
private var requestActiveUnlockOnUnlockIntent = false
private var requestActiveUnlockOnBioFail = false
- private var faceErrorsToTriggerBiometricFailOn = mutableSetOf(FACE_ERROR_TIMEOUT)
+ private var faceErrorsToTriggerBiometricFailOn = mutableSetOf<Int>()
private var faceAcquireInfoToTriggerBiometricFailOn = mutableSetOf<Int>()
- private var onUnlockIntentWhenBiometricEnrolled = mutableSetOf<Int>(BIOMETRIC_TYPE_NONE)
+ private var onUnlockIntentWhenBiometricEnrolled = mutableSetOf<Int>()
+ private var wakeupsConsideredUnlockIntents = mutableSetOf<Int>()
private val settingsObserver = object : ContentObserver(handler) {
private val wakeUri = secureSettings.getUriFor(ACTIVE_UNLOCK_ON_WAKE)
@@ -89,16 +95,19 @@
secureSettings.getUriFor(ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO)
private val unlockIntentWhenBiometricEnrolledUri =
secureSettings.getUriFor(ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED)
+ private val wakeupsConsideredUnlockIntentsUri =
+ secureSettings.getUriFor(ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS)
fun register() {
registerUri(
listOf(
- wakeUri,
- unlockIntentUri,
- bioFailUri,
- faceErrorsUri,
- faceAcquireInfoUri,
- unlockIntentWhenBiometricEnrolledUri
+ wakeUri,
+ unlockIntentUri,
+ bioFailUri,
+ faceErrorsUri,
+ faceAcquireInfoUri,
+ unlockIntentWhenBiometricEnrolledUri,
+ wakeupsConsideredUnlockIntentsUri,
)
)
@@ -153,7 +162,7 @@
secureSettings.getStringForUser(ACTIVE_UNLOCK_ON_FACE_ACQUIRE_INFO,
getCurrentUser()),
faceAcquireInfoToTriggerBiometricFailOn,
- setOf<Int>())
+ emptySet())
}
if (selfChange || uris.contains(unlockIntentWhenBiometricEnrolledUri)) {
@@ -162,7 +171,16 @@
ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
getCurrentUser()),
onUnlockIntentWhenBiometricEnrolled,
- setOf(BIOMETRIC_TYPE_NONE))
+ setOf(BiometricType.NONE.intValue))
+ }
+
+ if (selfChange || uris.contains(wakeupsConsideredUnlockIntentsUri)) {
+ processStringArray(
+ secureSettings.getStringForUser(
+ ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS,
+ getCurrentUser()),
+ wakeupsConsideredUnlockIntents,
+ setOf(WAKE_REASON_UNFOLD_DEVICE))
}
}
@@ -181,10 +199,12 @@
out.clear()
stringSetting?.let {
for (code: String in stringSetting.split("|")) {
- try {
- out.add(code.toInt())
- } catch (e: NumberFormatException) {
- Log.e(TAG, "Passed an invalid setting=$code")
+ if (code.isNotEmpty()) {
+ try {
+ out.add(code.toInt())
+ } catch (e: NumberFormatException) {
+ Log.e(TAG, "Passed an invalid setting=$code")
+ }
}
}
} ?: out.addAll(default)
@@ -221,22 +241,30 @@
}
/**
+ * Whether the PowerManager wake reason is considered an unlock intent and should use origin
+ * [ActiveUnlockRequestOrigin.UNLOCK_INTENT] instead of [ActiveUnlockRequestOrigin.WAKE].
+ */
+ fun isWakeupConsideredUnlockIntent(pmWakeReason: Int): Boolean {
+ return wakeupsConsideredUnlockIntents.contains(pmWakeReason)
+ }
+
+ /**
* Whether to trigger active unlock based on where the request is coming from and
* the current settings.
*/
- fun shouldAllowActiveUnlockFromOrigin(requestOrigin: ACTIVE_UNLOCK_REQUEST_ORIGIN): Boolean {
+ fun shouldAllowActiveUnlockFromOrigin(requestOrigin: ActiveUnlockRequestOrigin): Boolean {
return when (requestOrigin) {
- ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE -> requestActiveUnlockOnWakeup
+ ActiveUnlockRequestOrigin.WAKE -> requestActiveUnlockOnWakeup
- ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT ->
+ ActiveUnlockRequestOrigin.UNLOCK_INTENT ->
requestActiveUnlockOnUnlockIntent || requestActiveUnlockOnWakeup ||
(shouldRequestActiveUnlockOnUnlockIntentFromBiometricEnrollment())
- ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL ->
+ ActiveUnlockRequestOrigin.BIOMETRIC_FAIL ->
requestActiveUnlockOnBioFail || requestActiveUnlockOnUnlockIntent ||
requestActiveUnlockOnWakeup
- ACTIVE_UNLOCK_REQUEST_ORIGIN.ASSISTANT -> isActiveUnlockEnabled()
+ ActiveUnlockRequestOrigin.ASSISTANT -> isActiveUnlockEnabled()
}
}
@@ -252,18 +280,18 @@
val udfpsEnrolled = it.isUdfpsEnrolled
if (!anyFaceEnrolled && !anyFingerprintEnrolled) {
- return onUnlockIntentWhenBiometricEnrolled.contains(BIOMETRIC_TYPE_NONE)
+ return onUnlockIntentWhenBiometricEnrolled.contains(BiometricType.NONE.intValue)
}
if (!anyFaceEnrolled && anyFingerprintEnrolled) {
return onUnlockIntentWhenBiometricEnrolled.contains(
- BIOMETRIC_TYPE_ANY_FINGERPRINT) ||
+ BiometricType.ANY_FINGERPRINT.intValue) ||
(udfpsEnrolled && onUnlockIntentWhenBiometricEnrolled.contains(
- BIOMETRIC_TYPE_UNDER_DISPLAY_FINGERPRINT))
+ BiometricType.UNDER_DISPLAY_FINGERPRINT.intValue))
}
if (!anyFingerprintEnrolled && anyFaceEnrolled) {
- return onUnlockIntentWhenBiometricEnrolled.contains(BIOMETRIC_TYPE_ANY_FACE)
+ return onUnlockIntentWhenBiometricEnrolled.contains(BiometricType.ANY_FACE.intValue)
}
}
@@ -275,11 +303,15 @@
pw.println(" requestActiveUnlockOnWakeup=$requestActiveUnlockOnWakeup")
pw.println(" requestActiveUnlockOnUnlockIntent=$requestActiveUnlockOnUnlockIntent")
pw.println(" requestActiveUnlockOnBioFail=$requestActiveUnlockOnBioFail")
- pw.println(" requestActiveUnlockOnUnlockIntentWhenBiometricEnrolled=" +
- "$onUnlockIntentWhenBiometricEnrolled")
+ pw.println(" requestActiveUnlockOnUnlockIntentWhenBiometricEnrolled=${
+ onUnlockIntentWhenBiometricEnrolled.map { BiometricType.values()[it] }
+ }")
pw.println(" requestActiveUnlockOnFaceError=$faceErrorsToTriggerBiometricFailOn")
pw.println(" requestActiveUnlockOnFaceAcquireInfo=" +
"$faceAcquireInfoToTriggerBiometricFailOn")
+ pw.println(" activeUnlockWakeupsConsideredUnlockIntents=${
+ wakeupsConsideredUnlockIntents.map { PowerManager.wakeReasonToString(it) }
+ }")
pw.println("Current state:")
keyguardUpdateMonitor?.let {
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 3a940e9..ea079a9 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -25,6 +25,7 @@
import android.util.TypedValue
import android.view.View
import android.widget.FrameLayout
+import android.view.ViewTreeObserver
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
@@ -42,12 +43,16 @@
import com.android.systemui.log.dagger.KeyguardSmallClockLog
import com.android.systemui.log.dagger.KeyguardLargeClockLog
import com.android.systemui.plugins.ClockController
+import com.android.systemui.plugins.ClockFaceController
+import com.android.systemui.plugins.ClockTickRate
import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogLevel.DEBUG
import com.android.systemui.shared.regionsampling.RegionSampler
+import com.android.systemui.statusbar.Weather
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.concurrency.DelayableExecutor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
@@ -72,7 +77,7 @@
private val configurationController: ConfigurationController,
@Main private val resources: Resources,
private val context: Context,
- @Main private val mainExecutor: Executor,
+ @Main private val mainExecutor: DelayableExecutor,
@Background private val bgExecutor: Executor,
@KeyguardSmallClockLog private val smallLogBuffer: LogBuffer?,
@KeyguardLargeClockLog private val largeLogBuffer: LogBuffer?,
@@ -92,8 +97,11 @@
if (regionSamplingEnabled) {
clock?.smallClock?.view?.addOnLayoutChangeListener(mLayoutChangedListener)
clock?.largeClock?.view?.addOnLayoutChangeListener(mLayoutChangedListener)
+ } else {
+ updateColors()
}
updateFontSizes()
+ updateTimeListeners()
}
}
@@ -208,6 +216,10 @@
}
var regionSampler: RegionSampler? = null
+ var smallTimeListener: TimeListener? = null
+ var largeTimeListener: TimeListener? = null
+ val shouldTimeListenerRun: Boolean
+ get() = isKeyguardVisible && dozeAmount < DOZE_TICKRATE_THRESHOLD
private var smallClockIsDark = true
private var largeClockIsDark = true
@@ -246,6 +258,9 @@
clock?.animations?.doze(if (isDozing) 1f else 0f)
}
}
+
+ smallTimeListener?.update(shouldTimeListenerRun)
+ largeTimeListener?.update(shouldTimeListenerRun)
}
override fun onTimeFormatChanged(timeFormat: String) {
@@ -259,6 +274,12 @@
override fun onUserSwitchComplete(userId: Int) {
clock?.events?.onTimeFormatChanged(DateFormat.is24HourFormat(context))
}
+
+ override fun onWeatherDataChanged(data: Weather?) {
+ if (data != null) {
+ clock?.events?.onWeatherDataChanged(data)
+ }
+ }
}
fun registerListeners(parent: View) {
@@ -285,6 +306,8 @@
}
}
}
+ smallTimeListener?.update(shouldTimeListenerRun)
+ largeTimeListener?.update(shouldTimeListenerRun)
}
fun unregisterListeners() {
@@ -299,6 +322,25 @@
batteryController.removeCallback(batteryCallback)
keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
regionSampler?.stopRegionSampler()
+ smallTimeListener?.stop()
+ largeTimeListener?.stop()
+ }
+
+ private fun updateTimeListeners() {
+ smallTimeListener?.stop()
+ largeTimeListener?.stop()
+
+ smallTimeListener = null
+ largeTimeListener = null
+
+ clock?.smallClock?.let {
+ smallTimeListener = TimeListener(it, mainExecutor)
+ smallTimeListener?.update(shouldTimeListenerRun)
+ }
+ clock?.largeClock?.let {
+ largeTimeListener = TimeListener(it, mainExecutor)
+ largeTimeListener?.update(shouldTimeListenerRun)
+ }
}
private fun updateFontSizes() {
@@ -308,12 +350,18 @@
resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat())
}
+ private fun handleDoze(doze: Float) {
+ dozeAmount = doze
+ clock?.animations?.doze(dozeAmount)
+ smallTimeListener?.update(doze < DOZE_TICKRATE_THRESHOLD)
+ largeTimeListener?.update(doze < DOZE_TICKRATE_THRESHOLD)
+ }
+
@VisibleForTesting
internal fun listenForDozeAmount(scope: CoroutineScope): Job {
return scope.launch {
keyguardInteractor.dozeAmount.collect {
- dozeAmount = it
- clock?.animations?.doze(dozeAmount)
+ handleDoze(it)
}
}
}
@@ -322,8 +370,7 @@
internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job {
return scope.launch {
keyguardTransitionInteractor.dozeAmountTransition.collect {
- dozeAmount = it.value
- clock?.animations?.doze(dozeAmount)
+ handleDoze(it.value)
}
}
}
@@ -338,8 +385,7 @@
keyguardTransitionInteractor.anyStateToAodTransition.filter {
it.transitionState == TransitionState.FINISHED
}.collect {
- dozeAmount = 1f
- clock?.animations?.doze(dozeAmount)
+ handleDoze(1f)
}
}
}
@@ -359,7 +405,54 @@
}
}
+ class TimeListener(val clockFace: ClockFaceController, val executor: DelayableExecutor) {
+ val predrawListener = ViewTreeObserver.OnPreDrawListener {
+ clockFace.events.onTimeTick()
+ true
+ }
+
+ val secondsRunnable = object : Runnable {
+ override fun run() {
+ if (!isRunning) {
+ return
+ }
+
+ executor.executeDelayed(this, 990)
+ clockFace.events.onTimeTick()
+ }
+ }
+
+ var isRunning: Boolean = false
+ private set
+
+ fun start() {
+ if (isRunning) {
+ return
+ }
+
+ isRunning = true
+ when (clockFace.events.tickRate) {
+ ClockTickRate.PER_MINUTE -> {/* Handled by KeyguardClockSwitchController */}
+ ClockTickRate.PER_SECOND -> executor.execute(secondsRunnable)
+ ClockTickRate.PER_FRAME -> {
+ clockFace.view.viewTreeObserver.addOnPreDrawListener(predrawListener)
+ clockFace.view.invalidate()
+ }
+ }
+ }
+
+ fun stop() {
+ if (!isRunning) { return }
+
+ isRunning = false
+ clockFace.view.viewTreeObserver.removeOnPreDrawListener(predrawListener)
+ }
+
+ fun update(shouldRun: Boolean) = if (shouldRun) start() else stop()
+ }
+
companion object {
private val TAG = ClockEventController::class.simpleName!!
+ private val DOZE_TICKRATE_THRESHOLD = 0.99f
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 0685794..e55ac1b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -91,6 +91,7 @@
private ViewGroup mStatusArea;
// If the SMARTSPACE flag is set, keyguard_slice_view is replaced by the following views.
+ private ViewGroup mDateWeatherView;
private View mWeatherView;
private View mSmartspaceView;
@@ -105,6 +106,12 @@
updateDoubleLineClock();
}
};
+ private final ContentObserver mShowWeatherObserver = new ContentObserver(null) {
+ @Override
+ public void onChange(boolean change) {
+ setWeatherVisibility();
+ }
+ };
private final KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener
mKeyguardUnlockAnimationListener =
@@ -201,7 +208,7 @@
// TODO(b/261757708): add content observer for the Settings toggle and add/remove
// weather according to the Settings.
if (mSmartspaceController.isDateWeatherDecoupled()) {
- addWeatherView(viewIndex);
+ addDateWeatherView(viewIndex);
viewIndex += 1;
}
@@ -215,7 +222,15 @@
UserHandle.USER_ALL
);
+ mSecureSettings.registerContentObserverForUser(
+ Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED,
+ false, /* notifyForDescendants */
+ mShowWeatherObserver,
+ UserHandle.USER_ALL
+ );
+
updateDoubleLineClock();
+ setWeatherVisibility();
mKeyguardUnlockAnimationController.addKeyguardUnlockAnimationListener(
mKeyguardUnlockAnimationListener);
@@ -239,6 +254,14 @@
void onLocaleListChanged() {
if (mSmartspaceController.isEnabled()) {
+ if (mSmartspaceController.isDateWeatherDecoupled()) {
+ mDateWeatherView.removeView(mWeatherView);
+ int index = mStatusArea.indexOfChild(mDateWeatherView);
+ if (index >= 0) {
+ mStatusArea.removeView(mDateWeatherView);
+ addDateWeatherView(index);
+ }
+ }
int index = mStatusArea.indexOfChild(mSmartspaceView);
if (index >= 0) {
mStatusArea.removeView(mSmartspaceView);
@@ -247,16 +270,28 @@
}
}
- private void addWeatherView(int index) {
- mWeatherView = mSmartspaceController.buildAndConnectWeatherView(mView);
+ private void addDateWeatherView(int index) {
+ mDateWeatherView = (ViewGroup) mSmartspaceController.buildAndConnectDateView(mView);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
MATCH_PARENT, WRAP_CONTENT);
- mStatusArea.addView(mWeatherView, index, lp);
+ mStatusArea.addView(mDateWeatherView, index, lp);
int startPadding = getContext().getResources().getDimensionPixelSize(
R.dimen.below_clock_padding_start);
int endPadding = getContext().getResources().getDimensionPixelSize(
R.dimen.below_clock_padding_end);
- mWeatherView.setPaddingRelative(startPadding, 0, endPadding, 0);
+ mDateWeatherView.setPaddingRelative(startPadding, 0, endPadding, 0);
+
+ addWeatherView();
+ }
+
+ private void addWeatherView() {
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+ WRAP_CONTENT, WRAP_CONTENT);
+ mWeatherView = mSmartspaceController.buildAndConnectWeatherView(mView);
+ // Place weather right after the date, before the extras
+ final int index = mDateWeatherView.getChildCount() == 0 ? 0 : 1;
+ mDateWeatherView.addView(mWeatherView, index, lp);
+ mWeatherView.setPaddingRelative(0, 0, 4, 0);
}
private void addSmartspaceView(int index) {
@@ -323,7 +358,8 @@
}
ClockController clock = getClock();
if (clock != null) {
- clock.getEvents().onTimeTick();
+ clock.getSmallClock().getEvents().onTimeTick();
+ clock.getLargeClock().getEvents().onTimeTick();
}
}
@@ -427,6 +463,14 @@
}
}
+ private void setWeatherVisibility() {
+ if (mWeatherView != null) {
+ mUiExecutor.execute(
+ () -> mWeatherView.setVisibility(
+ mSmartspaceController.isWeatherEnabled() ? View.VISIBLE : View.GONE));
+ }
+ }
+
/**
* Sets the clipChildren property on relevant views, to allow the smartspace to draw out of
* bounds during the unlock transition.
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
index 8011efd..92e3641 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
@@ -131,6 +131,7 @@
@Override
void resetState() {
+ mMessageAreaController.setMessage(getInitialMessageResId());
mView.setPasswordEntryEnabled(true);
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 57bfe54..20baa81 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -237,7 +237,7 @@
}
if (mUpdateMonitor.isFaceEnrolled()) {
mUpdateMonitor.requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT,
"swipeUpOnBouncer");
}
}
@@ -247,6 +247,7 @@
@Override
public void onThemeChanged() {
reloadColors();
+ reset();
}
@Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index afa9ef6..ec56967 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -109,7 +109,6 @@
import android.os.Message;
import android.os.PowerManager;
import android.os.RemoteException;
-import android.os.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
@@ -154,6 +153,7 @@
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.Weather;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.policy.DevicePostureController;
import com.android.systemui.telephony.TelephonyListenerManager;
@@ -1312,7 +1312,8 @@
}
public boolean getUserHasTrust(int userId) {
- return !isTrustDisabled() && mUserHasTrust.get(userId);
+ return !isTrustDisabled() && mUserHasTrust.get(userId)
+ && isUnlockingWithTrustAgentAllowed();
}
/**
@@ -1320,12 +1321,19 @@
*/
public boolean getUserUnlockedWithBiometric(int userId) {
BiometricAuthenticated fingerprint = mUserFingerprintAuthenticated.get(userId);
- BiometricAuthenticated face = mUserFaceAuthenticated.get(userId);
boolean fingerprintAllowed = fingerprint != null && fingerprint.mAuthenticated
&& isUnlockingWithBiometricAllowed(fingerprint.mIsStrongBiometric);
- boolean faceAllowed = face != null && face.mAuthenticated
+ return fingerprintAllowed || getUserUnlockedWithFace(userId);
+ }
+
+
+ /**
+ * Returns whether the user is unlocked with face.
+ */
+ public boolean getUserUnlockedWithFace(int userId) {
+ BiometricAuthenticated face = mUserFaceAuthenticated.get(userId);
+ return face != null && face.mAuthenticated
&& isUnlockingWithBiometricAllowed(face.mIsStrongBiometric);
- return fingerprintAllowed || faceAllowed;
}
/**
@@ -1400,6 +1408,10 @@
return mUserTrustIsUsuallyManaged.get(userId);
}
+ private boolean isUnlockingWithTrustAgentAllowed() {
+ return isUnlockingWithBiometricAllowed(true);
+ }
+
public boolean isUnlockingWithBiometricAllowed(boolean isStrongBiometric) {
// StrongAuthTracker#isUnlockingWithBiometricAllowed includes
// STRONG_AUTH_REQUIRED_AFTER_LOCKOUT which is the same as mFingerprintLockedOutPermanent;
@@ -1535,7 +1547,7 @@
FACE_AUTH_UPDATED_ASSISTANT_VISIBILITY_CHANGED);
if (mAssistantVisible) {
requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.ASSISTANT,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.ASSISTANT,
"assistant",
false);
}
@@ -1665,7 +1677,7 @@
@Override
public void onAuthenticationFailed() {
requestActiveUnlockDismissKeyguard(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL,
"fingerprintFailure");
handleFingerprintAuthFailed();
}
@@ -1734,7 +1746,7 @@
: mPrimaryBouncerFullyShown ? "bouncer"
: "udfpsFpDown";
requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL,
"faceFailure-" + reason);
handleFaceAuthFailed();
@@ -1761,7 +1773,7 @@
if (mActiveUnlockConfig.shouldRequestActiveUnlockOnFaceError(errMsgId)) {
requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL,
"faceError-" + errMsgId);
}
}
@@ -1773,7 +1785,7 @@
if (mActiveUnlockConfig.shouldRequestActiveUnlockOnFaceAcquireInfo(
acquireInfo)) {
requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL,
"faceAcquireInfo-" + acquireInfo);
}
}
@@ -1913,8 +1925,11 @@
FACE_AUTH_UPDATED_STARTED_WAKING_UP.setExtraInfo(pmWakeReason);
updateFaceListeningState(BIOMETRIC_ACTION_UPDATE,
FACE_AUTH_UPDATED_STARTED_WAKING_UP);
- requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "wakingUp - "
- + PowerManager.wakeReasonToString(pmWakeReason));
+ requestActiveUnlock(
+ mActiveUnlockConfig.isWakeupConsideredUnlockIntent(pmWakeReason)
+ ? ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT
+ : ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE,
+ "wakingUp - " + PowerManager.wakeReasonToString(pmWakeReason));
} else {
mLogger.logSkipUpdateFaceListeningOnWakeup(pmWakeReason);
}
@@ -2478,7 +2493,7 @@
mAuthInterruptActive = active;
updateFaceListeningState(BIOMETRIC_ACTION_UPDATE,
FACE_AUTH_TRIGGERED_ON_REACH_GESTURE_ON_AOD);
- requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "onReach");
+ requestActiveUnlock(ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE, "onReach");
}
/**
@@ -2548,7 +2563,7 @@
* Attempts to trigger active unlock from trust agent.
*/
private void requestActiveUnlock(
- @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
+ @NonNull ActiveUnlockConfig.ActiveUnlockRequestOrigin requestOrigin,
String reason,
boolean dismissKeyguard
) {
@@ -2559,7 +2574,7 @@
final boolean allowRequest =
mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(requestOrigin);
- if (requestOrigin == ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE
+ if (requestOrigin == ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE
&& !allowRequest && mActiveUnlockConfig.isActiveUnlockEnabled()) {
// instead of requesting the active unlock, initiate the unlock
initiateActiveUnlock(reason);
@@ -2578,7 +2593,7 @@
* Only dismisses the keyguard under certain conditions.
*/
public void requestActiveUnlock(
- @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
+ @NonNull ActiveUnlockConfig.ActiveUnlockRequestOrigin requestOrigin,
String extraReason
) {
final boolean canFaceBypass = isFaceEnrolled() && mKeyguardBypassController != null
@@ -2595,7 +2610,7 @@
* Attempts to trigger active unlock from trust agent with a request to dismiss the keyguard.
*/
public void requestActiveUnlockDismissKeyguard(
- @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
+ @NonNull ActiveUnlockConfig.ActiveUnlockRequestOrigin requestOrigin,
String extraReason
) {
requestActiveUnlock(
@@ -2612,7 +2627,7 @@
updateFaceListeningState(BIOMETRIC_ACTION_START,
FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN);
requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT,
"udfpsBouncer");
}
}
@@ -3219,6 +3234,24 @@
}
/**
+ * @param data the weather data (temp, conditions, unit) for weather clock to use
+ */
+ public void sendWeatherData(Weather data) {
+ mHandler.post(()-> {
+ handleWeatherDataUpdate(data); });
+ }
+
+ private void handleWeatherDataUpdate(Weather data) {
+ Assert.isMainThread();
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
+ if (cb != null) {
+ cb.onWeatherDataChanged(data);
+ }
+ }
+ }
+
+ /**
* Handle {@link #MSG_BATTERY_UPDATE}
*/
private void handleBatteryUpdate(BatteryStatus status) {
@@ -3400,7 +3433,7 @@
if (wasPrimaryBouncerFullyShown != mPrimaryBouncerFullyShown) {
if (mPrimaryBouncerFullyShown) {
requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT,
"bouncerFullyShown");
}
for (int i = 0; i < mCallbacks.size(); i++) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
index e6b9ac8..4a7dd24 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
@@ -24,6 +24,7 @@
import com.android.settingslib.fuelgauge.BatteryStatus;
import com.android.systemui.statusbar.KeyguardIndicationController;
+import com.android.systemui.statusbar.Weather;
import java.util.TimeZone;
@@ -58,6 +59,11 @@
public void onTimeFormatChanged(String timeFormat) { }
/**
+ * Called when receive new weather data.
+ */
+ public void onWeatherDataChanged(Weather data) { }
+
+ /**
* Called when the carrier PLMN or SPN changes.
*/
public void onRefreshCarrierInfo() { }
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 201a1d9..c414c08 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -372,7 +372,7 @@
}
fun logUserRequestedUnlock(
- requestOrigin: ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN,
+ requestOrigin: ActiveUnlockConfig.ActiveUnlockRequestOrigin,
reason: String?,
dismissKeyguard: Boolean
) {
diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
index 3e0fa45..54939fd 100644
--- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
@@ -35,6 +35,7 @@
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.settingslib.Utils
import com.android.systemui.animation.Interpolators
+import com.android.systemui.log.ScreenDecorationsLogger
import com.android.systemui.plugins.statusbar.StatusBarStateController
import java.util.concurrent.Executor
@@ -47,7 +48,8 @@
pos: Int,
val statusBarStateController: StatusBarStateController,
val keyguardUpdateMonitor: KeyguardUpdateMonitor,
- val mainExecutor: Executor
+ val mainExecutor: Executor,
+ val logger: ScreenDecorationsLogger,
) : ScreenDecorations.DisplayCutoutView(context, pos) {
private var showScanningAnim = false
private val rimPaint = Paint()
@@ -55,6 +57,7 @@
private var rimAnimator: AnimatorSet? = null
private val rimRect = RectF()
private var cameraProtectionColor = Color.BLACK
+
var faceScanningAnimColor = Utils.getColorAttrDefaultColor(context,
R.attr.wallpaperTextColorAccent)
private var cameraProtectionAnimator: ValueAnimator? = null
@@ -175,15 +178,22 @@
}
if (showScanningAnim) {
// Make sure that our measured height encompasses the extra space for the animation
- mTotalBounds.union(mBoundingRect)
+ mTotalBounds.set(mBoundingRect)
mTotalBounds.union(
rimRect.left.toInt(),
rimRect.top.toInt(),
rimRect.right.toInt(),
rimRect.bottom.toInt())
- setMeasuredDimension(
- resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0),
- resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0))
+ val measuredWidth = resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0)
+ val measuredHeight = resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0)
+ logger.boundingRect(rimRect, "onMeasure: Face scanning animation")
+ logger.boundingRect(mBoundingRect, "onMeasure: Display cutout view bounding rect")
+ logger.boundingRect(mTotalBounds, "onMeasure: TotalBounds")
+ logger.onMeasureDimensions(widthMeasureSpec,
+ heightMeasureSpec,
+ measuredWidth,
+ measuredHeight)
+ setMeasuredDimension(measuredWidth, measuredHeight)
} else {
setMeasuredDimension(
resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index 71f98fa..fb65588 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -64,6 +64,7 @@
import com.android.internal.util.Preconditions;
import com.android.settingslib.Utils;
+import com.android.systemui.biometrics.AuthController;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.decor.CutoutDecorProviderFactory;
@@ -75,6 +76,7 @@
import com.android.systemui.decor.PrivacyDotDecorProviderFactory;
import com.android.systemui.decor.RoundedCornerDecorProviderFactory;
import com.android.systemui.decor.RoundedCornerResDelegate;
+import com.android.systemui.log.ScreenDecorationsLogger;
import com.android.systemui.qs.SettingObserver;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
@@ -120,6 +122,9 @@
R.id.display_cutout_right,
R.id.display_cutout_bottom
};
+ private final ScreenDecorationsLogger mLogger;
+
+ private final AuthController mAuthController;
private DisplayTracker mDisplayTracker;
@VisibleForTesting
@@ -153,6 +158,7 @@
private WindowManager mWindowManager;
private int mRotation;
private SettingObserver mColorInversionSetting;
+ @Nullable
private DelayableExecutor mExecutor;
private Handler mHandler;
boolean mPendingConfigChange;
@@ -172,6 +178,7 @@
DisplayCutoutView overlay = (DisplayCutoutView) getOverlayView(
mFaceScanningViewId);
if (overlay != null) {
+ mLogger.cameraProtectionBoundsForScanningOverlay(bounds);
overlay.setProtection(protectionPath, bounds);
overlay.enableShowProtection(true);
updateOverlayWindowVisibilityIfViewExists(
@@ -184,6 +191,7 @@
}
if (mScreenDecorHwcLayer != null) {
+ mLogger.hwcLayerCameraProtectionBounds(bounds);
mScreenDecorHwcLayer.setProtection(protectionPath, bounds);
mScreenDecorHwcLayer.enableShowProtection(true);
return;
@@ -197,11 +205,12 @@
}
++setProtectionCnt;
final DisplayCutoutView dcv = (DisplayCutoutView) view;
+ mLogger.dcvCameraBounds(id, bounds);
dcv.setProtection(protectionPath, bounds);
dcv.enableShowProtection(true);
}
if (setProtectionCnt == 0) {
- Log.e(TAG, "CutoutView not initialized showCameraProtection");
+ mLogger.cutoutViewNotInitialized();
}
}
@@ -307,7 +316,9 @@
PrivacyDotViewController dotViewController,
ThreadFactory threadFactory,
PrivacyDotDecorProviderFactory dotFactory,
- FaceScanningProviderFactory faceScanningFactory) {
+ FaceScanningProviderFactory faceScanningFactory,
+ ScreenDecorationsLogger logger,
+ AuthController authController) {
mContext = context;
mMainExecutor = mainExecutor;
mSecureSettings = secureSettings;
@@ -319,8 +330,23 @@
mDotFactory = dotFactory;
mFaceScanningFactory = faceScanningFactory;
mFaceScanningViewId = com.android.systemui.R.id.face_scanning_anim;
+ mLogger = logger;
+ mAuthController = authController;
}
+
+ private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() {
+ @Override
+ public void onFaceSensorLocationChanged() {
+ mLogger.onSensorLocationChanged();
+ if (mExecutor != null) {
+ mExecutor.execute(
+ () -> updateOverlayProviderViews(
+ new Integer[]{mFaceScanningViewId}));
+ }
+ }
+ };
+
@Override
public void start() {
if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
@@ -331,6 +357,7 @@
mExecutor = mThreadFactory.buildDelayableExecutorOnHandler(mHandler);
mExecutor.execute(this::startOnScreenDecorationsThread);
mDotViewController.setUiExecutor(mExecutor);
+ mAuthController.addCallback(mAuthControllerCallback);
}
private boolean isPrivacyDotEnabled() {
@@ -1306,7 +1333,7 @@
if (showProtection) {
// Make sure that our measured height encompasses the protection
- mTotalBounds.union(mBoundingRect);
+ mTotalBounds.set(mBoundingRect);
mTotalBounds.union((int) protectionRect.left, (int) protectionRect.top,
(int) protectionRect.right, (int) protectionRect.bottom);
setMeasuredDimension(
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
index 4c1a9fa..b342a29 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
@@ -50,6 +50,7 @@
import android.widget.SeekBar;
import android.widget.Switch;
+import com.android.internal.accessibility.common.MagnificationConstants;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
import com.android.systemui.R;
@@ -139,8 +140,10 @@
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
float scale = progress * A11Y_CHANGE_SCALE_DIFFERENCE + A11Y_SCALE_MIN_VALUE;
- // update persisted scale only when scale >= 2.0
- if (scale >= 2.0f) {
+ // Update persisted scale only when scale >= PERSISTED_SCALE_MIN_VALUE const.
+ // We assume if the scale is lower than the PERSISTED_SCALE_MIN_VALUE, there will be
+ // no obvious magnification effect.
+ if (scale >= MagnificationConstants.PERSISTED_SCALE_MIN_VALUE) {
Settings.Secure.putFloatForUser(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale,
UserHandle.USER_CURRENT);
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java
deleted file mode 100644
index 81d7766..0000000
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java
+++ /dev/null
@@ -1,277 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.accessibility.floatingmenu;
-
-import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED;
-import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_ICON_TYPE;
-import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT;
-import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY;
-import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE;
-import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
-
-import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets;
-import static com.android.systemui.Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP;
-import static com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.ShapeType;
-import static com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.SizeType;
-
-import android.annotation.FloatRange;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.UserHandle;
-import android.provider.Settings;
-import android.text.TextUtils;
-
-import androidx.annotation.NonNull;
-
-import com.android.internal.accessibility.dialog.AccessibilityTarget;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.systemui.Prefs;
-import com.android.systemui.shared.system.SysUiStatsLog;
-import com.android.systemui.util.settings.SecureSettings;
-
-import java.util.List;
-
-/**
- * Contains logic for an accessibility floating menu view.
- */
-public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu {
- private static final int DEFAULT_FADE_EFFECT_IS_ENABLED = 1;
- private static final int DEFAULT_MIGRATION_TOOLTIP_PROMPT_IS_DISABLED = 0;
- @FloatRange(from = 0.0, to = 1.0)
- private static final float DEFAULT_OPACITY_VALUE = 0.55f;
- @FloatRange(from = 0.0, to = 1.0)
- private static final float DEFAULT_POSITION_X_PERCENT = 1.0f;
- @FloatRange(from = 0.0, to = 1.0)
- private static final float DEFAULT_POSITION_Y_PERCENT = 0.77f;
-
- private final Context mContext;
- private final SecureSettings mSecureSettings;
- private final AccessibilityFloatingMenuView mMenuView;
- private final MigrationTooltipView mMigrationTooltipView;
- private final DockTooltipView mDockTooltipView;
- private final Handler mHandler = new Handler(Looper.getMainLooper());
-
- private final ContentObserver mContentObserver =
- new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange) {
- mMenuView.onTargetsChanged(getTargets(mContext, ACCESSIBILITY_BUTTON));
- }
- };
-
- private final ContentObserver mSizeContentObserver =
- new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange) {
- mMenuView.setSizeType(getSizeType());
- }
- };
-
- private final ContentObserver mFadeOutContentObserver =
- new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange) {
- mMenuView.updateOpacityWith(isFadeEffectEnabled(),
- getOpacityValue());
- }
- };
-
- private final ContentObserver mEnabledA11yServicesContentObserver =
- new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange) {
- mMenuView.onEnabledFeaturesChanged();
- }
- };
-
- public AccessibilityFloatingMenu(Context context, SecureSettings secureSettings) {
- mContext = context;
- mSecureSettings = secureSettings;
- mMenuView = new AccessibilityFloatingMenuView(context, getPosition(context));
- mMigrationTooltipView = new MigrationTooltipView(mContext, mMenuView);
- mDockTooltipView = new DockTooltipView(mContext, mMenuView);
- }
-
- @VisibleForTesting
- AccessibilityFloatingMenu(Context context, SecureSettings secureSettings,
- AccessibilityFloatingMenuView menuView) {
- mContext = context;
- mSecureSettings = secureSettings;
- mMenuView = menuView;
- mMigrationTooltipView = new MigrationTooltipView(mContext, mMenuView);
- mDockTooltipView = new DockTooltipView(mContext, mMenuView);
- }
-
- @Override
- public boolean isShowing() {
- return mMenuView.isShowing();
- }
-
- @Override
- public void show() {
- if (isShowing()) {
- return;
- }
- final List<AccessibilityTarget> targetList = getTargets(mContext, ACCESSIBILITY_BUTTON);
- if (targetList.isEmpty()) {
- return;
- }
-
- mMenuView.show();
- mMenuView.onTargetsChanged(targetList);
- mMenuView.updateOpacityWith(isFadeEffectEnabled(),
- getOpacityValue());
- mMenuView.setSizeType(getSizeType());
- mMenuView.setShapeType(getShapeType());
- mMenuView.setOnDragEndListener(this::onDragEnd);
-
- showMigrationTooltipIfNecessary();
-
- registerContentObservers();
- }
-
- @Override
- public void hide() {
- if (!isShowing()) {
- return;
- }
-
- mMenuView.hide();
- mMenuView.setOnDragEndListener(null);
- mMigrationTooltipView.hide();
- mDockTooltipView.hide();
-
- unregisterContentObservers();
- }
-
- @NonNull
- private Position getPosition(Context context) {
- final String absolutePositionString = Prefs.getString(context,
- Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);
-
- if (TextUtils.isEmpty(absolutePositionString)) {
- return new Position(DEFAULT_POSITION_X_PERCENT, DEFAULT_POSITION_Y_PERCENT);
- } else {
- return Position.fromString(absolutePositionString);
- }
- }
-
- // Migration tooltip was the android S feature. It's just used on the Android version from R
- // to S. In addition, it only shows once.
- private void showMigrationTooltipIfNecessary() {
- if (isMigrationTooltipPromptEnabled()) {
- mMigrationTooltipView.show();
-
- mSecureSettings.putInt(
- ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT, /* disabled */ 0);
- }
- }
-
- private boolean isMigrationTooltipPromptEnabled() {
- return mSecureSettings.getInt(
- ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT,
- DEFAULT_MIGRATION_TOOLTIP_PROMPT_IS_DISABLED) == /* enabled */ 1;
- }
-
- private void onDragEnd(Position position) {
- SysUiStatsLog.write(SysUiStatsLog.ACCESSIBILITY_FLOATING_MENU_UI_CHANGED,
- position.getPercentageX(), position.getPercentageY(),
- mContext.getResources().getConfiguration().orientation);
- savePosition(mContext, position);
- showDockTooltipIfNecessary(mContext);
- }
-
- private void savePosition(Context context, Position position) {
- Prefs.putString(context, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION,
- position.toString());
- }
-
- /**
- * Shows tooltip when user drags accessibility floating menu for the first time.
- */
- private void showDockTooltipIfNecessary(Context context) {
- if (!Prefs.get(context).getBoolean(
- HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, false)) {
- // if the menu is an oval, the user has already dragged it out, so show the tooltip.
- if (mMenuView.isOvalShape()) {
- mDockTooltipView.show();
- }
-
- Prefs.putBoolean(context, HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, true);
- }
- }
-
- private boolean isFadeEffectEnabled() {
- return mSecureSettings.getInt(
- ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
- DEFAULT_FADE_EFFECT_IS_ENABLED) == /* enabled */ 1;
- }
-
- private float getOpacityValue() {
- return mSecureSettings.getFloat(
- ACCESSIBILITY_FLOATING_MENU_OPACITY,
- DEFAULT_OPACITY_VALUE);
- }
-
- private int getSizeType() {
- return mSecureSettings.getInt(
- ACCESSIBILITY_FLOATING_MENU_SIZE, SizeType.SMALL);
- }
-
- private int getShapeType() {
- return mSecureSettings.getInt(
- ACCESSIBILITY_FLOATING_MENU_ICON_TYPE,
- ShapeType.OVAL);
- }
-
- private void registerContentObservers() {
- mSecureSettings.registerContentObserverForUser(
- Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
- /* notifyForDescendants */ false, mContentObserver,
- UserHandle.USER_CURRENT);
- mSecureSettings.registerContentObserverForUser(
- Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
- /* notifyForDescendants */ false, mContentObserver,
- UserHandle.USER_CURRENT);
- mSecureSettings.registerContentObserverForUser(
- Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE,
- /* notifyForDescendants */ false, mSizeContentObserver,
- UserHandle.USER_CURRENT);
- mSecureSettings.registerContentObserverForUser(
- Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
- /* notifyForDescendants */ false, mFadeOutContentObserver,
- UserHandle.USER_CURRENT);
- mSecureSettings.registerContentObserverForUser(
- Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY,
- /* notifyForDescendants */ false, mFadeOutContentObserver,
- UserHandle.USER_CURRENT);
- mSecureSettings.registerContentObserverForUser(
- Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
- /* notifyForDescendants */ false,
- mEnabledA11yServicesContentObserver, UserHandle.USER_CURRENT);
- }
-
- private void unregisterContentObservers() {
- mSecureSettings.unregisterContentObserver(mContentObserver);
- mSecureSettings.unregisterContentObserver(mSizeContentObserver);
- mSecureSettings.unregisterContentObserver(mFadeOutContentObserver);
- mSecureSettings.unregisterContentObserver(
- mEnabledA11yServicesContentObserver);
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
index 2c413a2..5924149 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
@@ -19,8 +19,6 @@
import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
-import static com.android.systemui.flags.Flags.A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS;
-
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.os.UserHandle;
@@ -38,7 +36,6 @@
import com.android.systemui.accessibility.AccessibilityButtonModeObserver.AccessibilityButtonMode;
import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.util.settings.SecureSettings;
@@ -59,7 +56,7 @@
private final WindowManager mWindowManager;
private final DisplayManager mDisplayManager;
private final AccessibilityManager mAccessibilityManager;
- private final FeatureFlags mFeatureFlags;
+
private final SecureSettings mSecureSettings;
private final DisplayTracker mDisplayTracker;
@VisibleForTesting
@@ -105,7 +102,6 @@
AccessibilityButtonTargetsObserver accessibilityButtonTargetsObserver,
AccessibilityButtonModeObserver accessibilityButtonModeObserver,
KeyguardUpdateMonitor keyguardUpdateMonitor,
- FeatureFlags featureFlags,
SecureSettings secureSettings,
DisplayTracker displayTracker) {
mContext = context;
@@ -115,7 +111,6 @@
mAccessibilityButtonTargetsObserver = accessibilityButtonTargetsObserver;
mAccessibilityButtonModeObserver = accessibilityButtonModeObserver;
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
- mFeatureFlags = featureFlags;
mSecureSettings = secureSettings;
mDisplayTracker = displayTracker;
@@ -162,7 +157,7 @@
* Handles the accessibility floating menu visibility with the given values.
*
* @param keyguardVisible the keyguard visibility status. Not show the
- * {@link AccessibilityFloatingMenu} when keyguard appears.
+ * {@link MenuView} when keyguard appears.
* @param mode accessibility button mode {@link AccessibilityButtonMode}
* @param targets accessibility button list; it should comes from
* {@link android.provider.Settings.Secure#ACCESSIBILITY_BUTTON_TARGETS}.
@@ -187,16 +182,12 @@
private void showFloatingMenu() {
if (mFloatingMenu == null) {
- if (mFeatureFlags.isEnabled(A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS)) {
- final Display defaultDisplay = mDisplayManager.getDisplay(
- mDisplayTracker.getDefaultDisplayId());
- final Context windowContext = mContext.createWindowContext(defaultDisplay,
- TYPE_NAVIGATION_BAR_PANEL, /* options= */ null);
- mFloatingMenu = new MenuViewLayerController(windowContext, mWindowManager,
- mAccessibilityManager, mSecureSettings);
- } else {
- mFloatingMenu = new AccessibilityFloatingMenu(mContext, mSecureSettings);
- }
+ final Display defaultDisplay = mDisplayManager.getDisplay(
+ mDisplayTracker.getDefaultDisplayId());
+ final Context windowContext = mContext.createWindowContext(defaultDisplay,
+ TYPE_NAVIGATION_BAR_PANEL, /* options= */ null);
+ mFloatingMenu = new MenuViewLayerController(windowContext, mWindowManager,
+ mAccessibilityManager, mSecureSettings);
}
mFloatingMenu.show();
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
deleted file mode 100644
index aff0b1f..0000000
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
+++ /dev/null
@@ -1,921 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.accessibility.floatingmenu;
-
-import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.util.MathUtils.constrain;
-import static android.util.MathUtils.sq;
-import static android.view.WindowInsets.Type.displayCutout;
-import static android.view.WindowInsets.Type.ime;
-import static android.view.WindowInsets.Type.systemBars;
-import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
-
-import static java.util.Objects.requireNonNull;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.annotation.FloatRange;
-import android.annotation.IntDef;
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.graphics.Insets;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.LayerDrawable;
-import android.os.Handler;
-import android.os.Looper;
-import android.view.Gravity;
-import android.view.MotionEvent;
-import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-import android.view.WindowMetrics;
-import android.view.animation.Animation;
-import android.view.animation.OvershootInterpolator;
-import android.view.animation.TranslateAnimation;
-import android.widget.FrameLayout;
-
-import androidx.annotation.DimenRes;
-import androidx.annotation.NonNull;
-import androidx.core.view.AccessibilityDelegateCompat;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
-
-import com.android.internal.accessibility.dialog.AccessibilityTarget;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.systemui.R;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-
-/**
- * Accessibility floating menu is used for the actions of accessibility features, it's also the
- * action set.
- *
- * <p>The number of items would depend on strings key
- * {@link android.provider.Settings.Secure#ACCESSIBILITY_BUTTON_TARGETS}.
- */
-public class AccessibilityFloatingMenuView extends FrameLayout
- implements RecyclerView.OnItemTouchListener {
- private static final int INDEX_MENU_ITEM = 0;
- private static final int FADE_OUT_DURATION_MS = 1000;
- private static final int FADE_EFFECT_DURATION_MS = 3000;
- private static final int SNAP_TO_LOCATION_DURATION_MS = 150;
- private static final int MIN_WINDOW_Y = 0;
-
- private static final int ANIMATION_START_OFFSET = 600;
- private static final int ANIMATION_DURATION_MS = 600;
- private static final float ANIMATION_TO_X_VALUE = 0.5f;
-
- private boolean mIsFadeEffectEnabled;
- private boolean mIsShowing;
- private boolean mIsDownInEnlargedTouchArea;
- private boolean mIsDragging = false;
- @Alignment
- private int mAlignment;
- @SizeType
- private int mSizeType = SizeType.SMALL;
- @VisibleForTesting
- @ShapeType
- int mShapeType = ShapeType.OVAL;
- private int mTemporaryShapeType;
- @RadiusType
- private int mRadiusType;
- private int mMargin;
- private int mPadding;
- // The display width excludes the window insets of the system bar and display cutout.
- private int mDisplayHeight;
- // The display Height excludes the window insets of the system bar and display cutout.
- private int mDisplayWidth;
- private int mIconWidth;
- private int mIconHeight;
- private int mInset;
- private int mDownX;
- private int mDownY;
- private int mRelativeToPointerDownX;
- private int mRelativeToPointerDownY;
- private float mRadius;
- private final Rect mDisplayInsetsRect = new Rect();
- private final Rect mImeInsetsRect = new Rect();
- private final Position mPosition;
- private float mSquareScaledTouchSlop;
- private final Configuration mLastConfiguration;
- private Optional<OnDragEndListener> mOnDragEndListener = Optional.empty();
- private final RecyclerView mListView;
- private final AccessibilityTargetAdapter mAdapter;
- private float mFadeOutValue;
- private final ValueAnimator mFadeOutAnimator;
- @VisibleForTesting
- final ValueAnimator mDragAnimator;
- private final Handler mUiHandler;
- @VisibleForTesting
- final WindowManager.LayoutParams mCurrentLayoutParams;
- private final WindowManager mWindowManager;
- private final List<AccessibilityTarget> mTargets = new ArrayList<>();
-
- @IntDef({
- SizeType.SMALL,
- SizeType.LARGE
- })
- @Retention(RetentionPolicy.SOURCE)
- @interface SizeType {
- int SMALL = 0;
- int LARGE = 1;
- }
-
- @IntDef({
- ShapeType.OVAL,
- ShapeType.HALF_OVAL
- })
- @Retention(RetentionPolicy.SOURCE)
- @interface ShapeType {
- int OVAL = 0;
- int HALF_OVAL = 1;
- }
-
- @IntDef({
- RadiusType.LEFT_HALF_OVAL,
- RadiusType.OVAL,
- RadiusType.RIGHT_HALF_OVAL
- })
- @Retention(RetentionPolicy.SOURCE)
- @interface RadiusType {
- int LEFT_HALF_OVAL = 0;
- int OVAL = 1;
- int RIGHT_HALF_OVAL = 2;
- }
-
- @IntDef({
- Alignment.LEFT,
- Alignment.RIGHT
- })
- @Retention(RetentionPolicy.SOURCE)
- @interface Alignment {
- int LEFT = 0;
- int RIGHT = 1;
- }
-
- /**
- * Interface for a callback to be invoked when the floating menu was dragging.
- */
- interface OnDragEndListener {
-
- /**
- * Called when a drag is completed.
- *
- * @param position Stores information about the position
- */
- void onDragEnd(Position position);
- }
-
- public AccessibilityFloatingMenuView(Context context, @NonNull Position position) {
- this(context, position, new RecyclerView(context));
- }
-
- @VisibleForTesting
- AccessibilityFloatingMenuView(Context context, @NonNull Position position,
- RecyclerView listView) {
- super(context);
-
- mListView = listView;
- mWindowManager = context.getSystemService(WindowManager.class);
- mLastConfiguration = new Configuration(getResources().getConfiguration());
- mAdapter = new AccessibilityTargetAdapter(mTargets);
- mUiHandler = createUiHandler();
- mPosition = position;
- mAlignment = transformToAlignment(mPosition.getPercentageX());
- mRadiusType = (mAlignment == Alignment.RIGHT)
- ? RadiusType.LEFT_HALF_OVAL
- : RadiusType.RIGHT_HALF_OVAL;
-
- updateDimensions();
-
- mCurrentLayoutParams = createDefaultLayoutParams();
-
- mFadeOutAnimator = ValueAnimator.ofFloat(1.0f, mFadeOutValue);
- mFadeOutAnimator.setDuration(FADE_OUT_DURATION_MS);
- mFadeOutAnimator.addUpdateListener(
- (animation) -> setAlpha((float) animation.getAnimatedValue()));
-
- mDragAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
- mDragAnimator.setDuration(SNAP_TO_LOCATION_DURATION_MS);
- mDragAnimator.setInterpolator(new OvershootInterpolator());
- mDragAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mPosition.update(transformCurrentPercentageXToEdge(),
- calculateCurrentPercentageY());
- mAlignment = transformToAlignment(mPosition.getPercentageX());
-
- updateLocationWith(mPosition);
-
- updateInsetWith(getResources().getConfiguration().uiMode, mAlignment);
-
- mRadiusType = (mAlignment == Alignment.RIGHT)
- ? RadiusType.LEFT_HALF_OVAL
- : RadiusType.RIGHT_HALF_OVAL;
- updateRadiusWith(mSizeType, mRadiusType, mTargets.size());
-
- fadeOut();
-
- mOnDragEndListener.ifPresent(
- onDragEndListener -> onDragEndListener.onDragEnd(mPosition));
- }
- });
-
-
- initListView();
- updateStrokeWith(getResources().getConfiguration().uiMode, mAlignment);
- }
-
- @Override
- public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
- @NonNull MotionEvent event) {
- final int currentRawX = (int) event.getRawX();
- final int currentRawY = (int) event.getRawY();
-
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- fadeIn();
-
- mDownX = currentRawX;
- mDownY = currentRawY;
- mRelativeToPointerDownX = mCurrentLayoutParams.x - mDownX;
- mRelativeToPointerDownY = mCurrentLayoutParams.y - mDownY;
- mListView.animate().translationX(0);
- break;
- case MotionEvent.ACTION_MOVE:
- if (mIsDragging
- || hasExceededTouchSlop(mDownX, mDownY, currentRawX, currentRawY)) {
- if (!mIsDragging) {
- mIsDragging = true;
- setRadius(mRadius, RadiusType.OVAL);
- setInset(0, 0);
- }
-
- mTemporaryShapeType =
- isMovingTowardsScreenEdge(mAlignment, currentRawX, mDownX)
- ? ShapeType.HALF_OVAL
- : ShapeType.OVAL;
- final int newWindowX = currentRawX + mRelativeToPointerDownX;
- final int newWindowY = currentRawY + mRelativeToPointerDownY;
- mCurrentLayoutParams.x =
- constrain(newWindowX, getMinWindowX(), getMaxWindowX());
- mCurrentLayoutParams.y = constrain(newWindowY, MIN_WINDOW_Y, getMaxWindowY());
- mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
- }
- break;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- if (mIsDragging) {
- mIsDragging = false;
-
- final int minX = getMinWindowX();
- final int maxX = getMaxWindowX();
- final int endX = mCurrentLayoutParams.x > ((minX + maxX) / 2)
- ? maxX : minX;
- final int endY = mCurrentLayoutParams.y;
- snapToLocation(endX, endY);
-
- setShapeType(mTemporaryShapeType);
-
- // Avoid triggering the listener of the item.
- return true;
- }
-
- // Must switch the oval shape type before tapping the corresponding item in the
- // list view, otherwise it can't work on it.
- if (!isOvalShape()) {
- setShapeType(ShapeType.OVAL);
-
- return true;
- }
-
- fadeOut();
- break;
- default: // Do nothing
- }
-
- // not consume all the events here because keeping the scroll behavior of list view.
- return false;
- }
-
- @Override
- public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
- // Do Nothing
- }
-
- @Override
- public void onRequestDisallowInterceptTouchEvent(boolean b) {
- // Do Nothing
- }
-
- void show() {
- if (isShowing()) {
- return;
- }
-
- mIsShowing = true;
- mWindowManager.addView(this, mCurrentLayoutParams);
-
- setOnApplyWindowInsetsListener((view, insets) -> onWindowInsetsApplied(insets));
- setSystemGestureExclusion();
- }
-
- void hide() {
- if (!isShowing()) {
- return;
- }
-
- mIsShowing = false;
- mDragAnimator.cancel();
- mWindowManager.removeView(this);
-
- setOnApplyWindowInsetsListener(null);
- setSystemGestureExclusion();
- }
-
- boolean isShowing() {
- return mIsShowing;
- }
-
- boolean isOvalShape() {
- return mShapeType == ShapeType.OVAL;
- }
-
- void onTargetsChanged(List<AccessibilityTarget> newTargets) {
- fadeIn();
-
- mTargets.clear();
- mTargets.addAll(newTargets);
- onEnabledFeaturesChanged();
-
- updateRadiusWith(mSizeType, mRadiusType, mTargets.size());
- updateScrollModeWith(hasExceededMaxLayoutHeight());
- setSystemGestureExclusion();
-
- fadeOut();
- }
-
- void setSizeType(@SizeType int newSizeType) {
- fadeIn();
-
- mSizeType = newSizeType;
-
- updateItemViewWith(newSizeType);
- updateRadiusWith(newSizeType, mRadiusType, mTargets.size());
-
- // When the icon sized changed, the menu size and location will be impacted.
- updateLocationWith(mPosition);
- updateScrollModeWith(hasExceededMaxLayoutHeight());
- updateOffsetWith(mShapeType, mAlignment);
- setSystemGestureExclusion();
-
- fadeOut();
- }
-
- void setShapeType(@ShapeType int newShapeType) {
- fadeIn();
-
- mShapeType = newShapeType;
-
- updateOffsetWith(newShapeType, mAlignment);
-
- setOnTouchListener(
- newShapeType == ShapeType.OVAL
- ? null
- : (view, event) -> onTouched(event));
-
- fadeOut();
- }
-
- public void setOnDragEndListener(OnDragEndListener onDragEndListener) {
- mOnDragEndListener = Optional.ofNullable(onDragEndListener);
- }
-
- void startTranslateXAnimation() {
- fadeIn();
-
- final float toXValue = (mAlignment == Alignment.RIGHT)
- ? ANIMATION_TO_X_VALUE
- : -ANIMATION_TO_X_VALUE;
- final TranslateAnimation animation =
- new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0,
- Animation.RELATIVE_TO_SELF, toXValue,
- Animation.RELATIVE_TO_SELF, 0,
- Animation.RELATIVE_TO_SELF, 0);
- animation.setDuration(ANIMATION_DURATION_MS);
- animation.setRepeatMode(Animation.REVERSE);
- animation.setInterpolator(new OvershootInterpolator());
- animation.setRepeatCount(Animation.INFINITE);
- animation.setStartOffset(ANIMATION_START_OFFSET);
- mListView.startAnimation(animation);
- }
-
- void stopTranslateXAnimation() {
- mListView.clearAnimation();
-
- fadeOut();
- }
-
- Rect getWindowLocationOnScreen() {
- final int left = mCurrentLayoutParams.x;
- final int top = mCurrentLayoutParams.y;
- return new Rect(left, top, left + getWindowWidth(), top + getWindowHeight());
- }
-
- void updateOpacityWith(boolean isFadeEffectEnabled, float newOpacityValue) {
- mIsFadeEffectEnabled = isFadeEffectEnabled;
- mFadeOutValue = newOpacityValue;
-
- mFadeOutAnimator.cancel();
- mFadeOutAnimator.setFloatValues(1.0f, mFadeOutValue);
- setAlpha(mIsFadeEffectEnabled ? mFadeOutValue : /* completely opaque */ 1.0f);
- }
-
- void onEnabledFeaturesChanged() {
- mAdapter.notifyDataSetChanged();
- }
-
- @VisibleForTesting
- void fadeIn() {
- if (!mIsFadeEffectEnabled) {
- return;
- }
-
- mFadeOutAnimator.cancel();
- mUiHandler.removeCallbacksAndMessages(null);
- mUiHandler.post(() -> setAlpha(/* completely opaque */ 1.0f));
- }
-
- @VisibleForTesting
- void fadeOut() {
- if (!mIsFadeEffectEnabled) {
- return;
- }
-
- mUiHandler.postDelayed(() -> mFadeOutAnimator.start(), FADE_EFFECT_DURATION_MS);
- }
-
- private boolean onTouched(MotionEvent event) {
- final int action = event.getAction();
- final int currentX = (int) event.getX();
- final int currentY = (int) event.getY();
-
- final int marginStartEnd = getMarginStartEndWith(mLastConfiguration);
- final Rect touchDelegateBounds =
- new Rect(marginStartEnd, mMargin, marginStartEnd + getLayoutWidth(),
- mMargin + getLayoutHeight());
- if (action == MotionEvent.ACTION_DOWN
- && touchDelegateBounds.contains(currentX, currentY)) {
- mIsDownInEnlargedTouchArea = true;
- }
-
- if (!mIsDownInEnlargedTouchArea) {
- return false;
- }
-
- if (action == MotionEvent.ACTION_UP
- || action == MotionEvent.ACTION_CANCEL) {
- mIsDownInEnlargedTouchArea = false;
- }
-
- // In order to correspond to the correct item of list view.
- event.setLocation(currentX - mMargin, currentY - mMargin);
- return mListView.dispatchTouchEvent(event);
- }
-
- private WindowInsets onWindowInsetsApplied(WindowInsets insets) {
- final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
- final Rect displayWindowInsetsRect = getDisplayInsets(windowMetrics).toRect();
- if (!displayWindowInsetsRect.equals(mDisplayInsetsRect)) {
- updateDisplaySizeWith(windowMetrics);
- updateLocationWith(mPosition);
- }
-
- final Rect imeInsetsRect = windowMetrics.getWindowInsets().getInsets(ime()).toRect();
- if (!imeInsetsRect.equals(mImeInsetsRect)) {
- if (isImeVisible(imeInsetsRect)) {
- mImeInsetsRect.set(imeInsetsRect);
- } else {
- mImeInsetsRect.setEmpty();
- }
-
- updateLocationWith(mPosition);
- }
-
- return insets;
- }
-
- private boolean isMovingTowardsScreenEdge(@Alignment int side, int currentRawX, int downX) {
- return (side == Alignment.RIGHT && currentRawX > downX)
- || (side == Alignment.LEFT && downX > currentRawX);
- }
-
- private boolean isImeVisible(Rect imeInsetsRect) {
- return imeInsetsRect.left != 0 || imeInsetsRect.top != 0 || imeInsetsRect.right != 0
- || imeInsetsRect.bottom != 0;
- }
-
- private boolean hasExceededTouchSlop(int startX, int startY, int endX, int endY) {
- return (sq(endX - startX) + sq(endY - startY)) > mSquareScaledTouchSlop;
- }
-
- private void setRadius(float radius, @RadiusType int type) {
- getMenuGradientDrawable().setCornerRadii(createRadii(radius, type));
- }
-
- private float[] createRadii(float radius, @RadiusType int type) {
- if (type == RadiusType.LEFT_HALF_OVAL) {
- return new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius};
- }
-
- if (type == RadiusType.RIGHT_HALF_OVAL) {
- return new float[]{0.0f, 0.0f, radius, radius, radius, radius, 0.0f, 0.0f};
- }
-
- return new float[]{radius, radius, radius, radius, radius, radius, radius, radius};
- }
-
- private Handler createUiHandler() {
- return new Handler(requireNonNull(Looper.myLooper(), "looper must not be null"));
- }
-
- private void updateDimensions() {
- final Resources res = getResources();
-
- updateDisplaySizeWith(mWindowManager.getCurrentWindowMetrics());
-
- mMargin =
- res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin);
- mInset =
- res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_inset);
-
- mSquareScaledTouchSlop =
- sq(ViewConfiguration.get(getContext()).getScaledTouchSlop());
-
- updateItemViewDimensionsWith(mSizeType);
- }
-
- private void updateDisplaySizeWith(WindowMetrics metrics) {
- final Rect displayBounds = metrics.getBounds();
- final Insets displayInsets = getDisplayInsets(metrics);
- mDisplayInsetsRect.set(displayInsets.toRect());
- displayBounds.inset(displayInsets);
- mDisplayWidth = displayBounds.width();
- mDisplayHeight = displayBounds.height();
- }
-
- private void updateItemViewDimensionsWith(@SizeType int sizeType) {
- final Resources res = getResources();
- final int paddingResId =
- sizeType == SizeType.SMALL
- ? R.dimen.accessibility_floating_menu_small_padding
- : R.dimen.accessibility_floating_menu_large_padding;
- mPadding = res.getDimensionPixelSize(paddingResId);
-
- final int iconResId =
- sizeType == SizeType.SMALL
- ? R.dimen.accessibility_floating_menu_small_width_height
- : R.dimen.accessibility_floating_menu_large_width_height;
- mIconWidth = res.getDimensionPixelSize(iconResId);
- mIconHeight = mIconWidth;
- }
-
- private void updateItemViewWith(@SizeType int sizeType) {
- updateItemViewDimensionsWith(sizeType);
-
- mAdapter.setItemPadding(mPadding);
- mAdapter.setIconWidthHeight(mIconWidth);
- mAdapter.notifyDataSetChanged();
- }
-
- private void initListView() {
- final Drawable background =
- getContext().getDrawable(R.drawable.accessibility_floating_menu_background);
- final LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
- final LayoutParams layoutParams =
- new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
- mListView.setLayoutParams(layoutParams);
- final InstantInsetLayerDrawable layerDrawable =
- new InstantInsetLayerDrawable(new Drawable[]{background});
- mListView.setBackground(layerDrawable);
- mListView.setAdapter(mAdapter);
- mListView.setLayoutManager(layoutManager);
- mListView.addOnItemTouchListener(this);
- mListView.animate().setInterpolator(new OvershootInterpolator());
- mListView.setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(mListView) {
- @NonNull
- @Override
- public AccessibilityDelegateCompat getItemDelegate() {
- return new ItemDelegateCompat(this,
- AccessibilityFloatingMenuView.this);
- }
- });
-
- updateListViewWith(mLastConfiguration);
-
- addView(mListView);
- }
-
- private void updateListViewWith(Configuration configuration) {
- updateMarginWith(configuration);
-
- final int elevation =
- getResources().getDimensionPixelSize(R.dimen.accessibility_floating_menu_elevation);
- mListView.setElevation(elevation);
- }
-
- private WindowManager.LayoutParams createDefaultLayoutParams() {
- final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
- WindowManager.LayoutParams.WRAP_CONTENT,
- WindowManager.LayoutParams.WRAP_CONTENT,
- WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
- WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
- PixelFormat.TRANSLUCENT);
- params.receiveInsetsIgnoringZOrder = true;
- params.privateFlags |= PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
- params.windowAnimations = android.R.style.Animation_Translucent;
- params.gravity = Gravity.START | Gravity.TOP;
- params.x = (mAlignment == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX();
-// params.y = (int) (mPosition.getPercentageY() * getMaxWindowY());
- final int currentLayoutY = (int) (mPosition.getPercentageY() * getMaxWindowY());
- params.y = Math.max(MIN_WINDOW_Y, currentLayoutY - getInterval());
- updateAccessibilityTitle(params);
- return params;
- }
-
- @Override
- protected void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- mLastConfiguration.setTo(newConfig);
-
- final int diff = newConfig.diff(mLastConfiguration);
- if ((diff & ActivityInfo.CONFIG_LOCALE) != 0) {
- updateAccessibilityTitle(mCurrentLayoutParams);
- }
-
- updateDimensions();
- updateListViewWith(newConfig);
- updateItemViewWith(mSizeType);
- updateColor();
- updateStrokeWith(newConfig.uiMode, mAlignment);
- updateLocationWith(mPosition);
- updateRadiusWith(mSizeType, mRadiusType, mTargets.size());
- updateScrollModeWith(hasExceededMaxLayoutHeight());
- setSystemGestureExclusion();
- }
-
- @VisibleForTesting
- void snapToLocation(int endX, int endY) {
- mDragAnimator.cancel();
- mDragAnimator.removeAllUpdateListeners();
- mDragAnimator.addUpdateListener(anim -> onDragAnimationUpdate(anim, endX, endY));
- mDragAnimator.start();
- }
-
- private void onDragAnimationUpdate(ValueAnimator animator, int endX, int endY) {
- float value = (float) animator.getAnimatedValue();
- final int newX = (int) (((1 - value) * mCurrentLayoutParams.x) + (value * endX));
- final int newY = (int) (((1 - value) * mCurrentLayoutParams.y) + (value * endY));
-
- mCurrentLayoutParams.x = newX;
- mCurrentLayoutParams.y = newY;
- mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
- }
-
- private int getMinWindowX() {
- return -getMarginStartEndWith(mLastConfiguration);
- }
-
- private int getMaxWindowX() {
- return mDisplayWidth - getMarginStartEndWith(mLastConfiguration) - getLayoutWidth();
- }
-
- private int getMaxWindowY() {
- return mDisplayHeight - getWindowHeight();
- }
-
- private InstantInsetLayerDrawable getMenuLayerDrawable() {
- return (InstantInsetLayerDrawable) mListView.getBackground();
- }
-
- private GradientDrawable getMenuGradientDrawable() {
- return (GradientDrawable) getMenuLayerDrawable().getDrawable(INDEX_MENU_ITEM);
- }
-
- private Insets getDisplayInsets(WindowMetrics metrics) {
- return metrics.getWindowInsets().getInsetsIgnoringVisibility(
- systemBars() | displayCutout());
- }
-
- /**
- * Updates the floating menu to be fixed at the side of the display.
- */
- private void updateLocationWith(Position position) {
- final @Alignment int alignment = transformToAlignment(position.getPercentageX());
- mCurrentLayoutParams.x = (alignment == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX();
- final int currentLayoutY = (int) (position.getPercentageY() * getMaxWindowY());
- mCurrentLayoutParams.y = Math.max(MIN_WINDOW_Y, currentLayoutY - getInterval());
- mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
- }
-
- /**
- * Gets the moving interval to not overlap between the keyboard and menu view.
- *
- * @return the moving interval if they overlap each other, otherwise 0.
- */
- private int getInterval() {
- final int currentLayoutY = (int) (mPosition.getPercentageY() * getMaxWindowY());
- final int imeY = mDisplayHeight - mImeInsetsRect.bottom;
- final int layoutBottomY = currentLayoutY + getWindowHeight();
-
- return layoutBottomY > imeY ? (layoutBottomY - imeY) : 0;
- }
-
- private void updateMarginWith(Configuration configuration) {
- // Avoid overlapping with system bars under landscape mode, update the margins of the menu
- // to align the edge of system bars.
- final int marginStartEnd = getMarginStartEndWith(configuration);
- final LayoutParams layoutParams = (FrameLayout.LayoutParams) mListView.getLayoutParams();
- layoutParams.setMargins(marginStartEnd, mMargin, marginStartEnd, mMargin);
- mListView.setLayoutParams(layoutParams);
- }
-
- private void updateOffsetWith(@ShapeType int shapeType, @Alignment int side) {
- final float halfWidth = getLayoutWidth() / 2.0f;
- final float offset = (shapeType == ShapeType.OVAL) ? 0 : halfWidth;
- mListView.animate().translationX(side == Alignment.RIGHT ? offset : -offset);
- }
-
- private void updateScrollModeWith(boolean hasExceededMaxLayoutHeight) {
- mListView.setOverScrollMode(hasExceededMaxLayoutHeight
- ? OVER_SCROLL_ALWAYS
- : OVER_SCROLL_NEVER);
- }
-
- private void updateColor() {
- final int menuColorResId = R.color.accessibility_floating_menu_background;
- getMenuGradientDrawable().setColor(getResources().getColor(menuColorResId));
- }
-
- private void updateStrokeWith(int uiMode, @Alignment int side) {
- updateInsetWith(uiMode, side);
-
- final boolean isNightMode =
- (uiMode & Configuration.UI_MODE_NIGHT_MASK)
- == Configuration.UI_MODE_NIGHT_YES;
- final Resources res = getResources();
- final int width =
- res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_width);
- final int strokeWidth = isNightMode ? width : 0;
- final int strokeColor = res.getColor(R.color.accessibility_floating_menu_stroke_dark);
- getMenuGradientDrawable().setStroke(strokeWidth, strokeColor);
- }
-
- private void updateRadiusWith(@SizeType int sizeType, @RadiusType int radiusType,
- int itemCount) {
- mRadius =
- getResources().getDimensionPixelSize(getRadiusResId(sizeType, itemCount));
- setRadius(mRadius, radiusType);
- }
-
- private void updateInsetWith(int uiMode, @Alignment int side) {
- final boolean isNightMode =
- (uiMode & Configuration.UI_MODE_NIGHT_MASK)
- == Configuration.UI_MODE_NIGHT_YES;
-
- final int layerInset = isNightMode ? mInset : 0;
- final int insetLeft = (side == Alignment.LEFT) ? layerInset : 0;
- final int insetRight = (side == Alignment.RIGHT) ? layerInset : 0;
- setInset(insetLeft, insetRight);
- }
-
- private void updateAccessibilityTitle(WindowManager.LayoutParams params) {
- params.accessibilityTitle = getResources().getString(
- com.android.internal.R.string.accessibility_select_shortcut_menu_title);
- }
-
- private void setInset(int left, int right) {
- final LayerDrawable layerDrawable = getMenuLayerDrawable();
- if (layerDrawable.getLayerInsetLeft(INDEX_MENU_ITEM) == left
- && layerDrawable.getLayerInsetRight(INDEX_MENU_ITEM) == right) {
- return;
- }
-
- layerDrawable.setLayerInset(INDEX_MENU_ITEM, left, 0, right, 0);
- }
-
- @VisibleForTesting
- boolean hasExceededMaxLayoutHeight() {
- return calculateActualLayoutHeight() > getMaxLayoutHeight();
- }
-
- @Alignment
- private int transformToAlignment(@FloatRange(from = 0.0, to = 1.0) float percentageX) {
- return (percentageX < 0.5f) ? Alignment.LEFT : Alignment.RIGHT;
- }
-
- private float transformCurrentPercentageXToEdge() {
- final float percentageX = calculateCurrentPercentageX();
- return (percentageX < 0.5) ? 0.0f : 1.0f;
- }
-
- private float calculateCurrentPercentageX() {
- return mCurrentLayoutParams.x / (float) getMaxWindowX();
- }
-
- private float calculateCurrentPercentageY() {
- return mCurrentLayoutParams.y / (float) getMaxWindowY();
- }
-
- private int calculateActualLayoutHeight() {
- return (mPadding + mIconHeight) * mTargets.size() + mPadding;
- }
-
- private int getMarginStartEndWith(Configuration configuration) {
- return configuration != null
- && configuration.orientation == ORIENTATION_PORTRAIT
- ? mMargin : 0;
- }
-
- private @DimenRes int getRadiusResId(@SizeType int sizeType, int itemCount) {
- return sizeType == SizeType.SMALL
- ? getSmallSizeResIdWith(itemCount)
- : getLargeSizeResIdWith(itemCount);
- }
-
- private int getSmallSizeResIdWith(int itemCount) {
- return itemCount > 1
- ? R.dimen.accessibility_floating_menu_small_multiple_radius
- : R.dimen.accessibility_floating_menu_small_single_radius;
- }
-
- private int getLargeSizeResIdWith(int itemCount) {
- return itemCount > 1
- ? R.dimen.accessibility_floating_menu_large_multiple_radius
- : R.dimen.accessibility_floating_menu_large_single_radius;
- }
-
- @VisibleForTesting
- Rect getAvailableBounds() {
- return new Rect(0, 0, mDisplayWidth - getWindowWidth(),
- mDisplayHeight - getWindowHeight());
- }
-
- private int getMaxLayoutHeight() {
- return mDisplayHeight - mMargin * 2;
- }
-
- private int getLayoutWidth() {
- return mPadding * 2 + mIconWidth;
- }
-
- private int getLayoutHeight() {
- return Math.min(getMaxLayoutHeight(), calculateActualLayoutHeight());
- }
-
- private int getWindowWidth() {
- return getMarginStartEndWith(mLastConfiguration) * 2 + getLayoutWidth();
- }
-
- private int getWindowHeight() {
- return Math.min(mDisplayHeight, mMargin * 2 + getLayoutHeight());
- }
-
- private void setSystemGestureExclusion() {
- final Rect excludeZone =
- new Rect(0, 0, getWindowWidth(), getWindowHeight());
- post(() -> setSystemGestureExclusionRects(
- mIsShowing
- ? Collections.singletonList(excludeZone)
- : Collections.emptyList()));
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipView.java
deleted file mode 100644
index 61fc7ed..0000000
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipView.java
+++ /dev/null
@@ -1,299 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.accessibility.floatingmenu;
-
-import static android.util.TypedValue.COMPLEX_UNIT_PX;
-import static android.view.View.MeasureSpec.AT_MOST;
-import static android.view.View.MeasureSpec.UNSPECIFIED;
-
-import android.annotation.UiContext;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.graphics.CornerPathEffect;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.ShapeDrawable;
-import android.os.Bundle;
-import android.text.method.MovementMethod;
-import android.util.DisplayMetrics;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.android.settingslib.Utils;
-import com.android.systemui.R;
-import com.android.systemui.recents.TriangleShape;
-
-/**
- * Base tooltip view that shows the information about the operation of the
- * Accessibility floating menu. In addition, the anchor view is only for {@link
- * AccessibilityFloatingMenuView}, it should be more suited for displaying one-off menus to avoid
- * the performance hit for the extra window.
- */
-class BaseTooltipView extends FrameLayout {
- private int mFontSize;
- private int mTextViewMargin;
- private int mTextViewPadding;
- private int mTextViewCornerRadius;
- private int mArrowMargin;
- private int mArrowWidth;
- private int mArrowHeight;
- private int mArrowCornerRadius;
- private int mScreenWidth;
- private boolean mIsShowing;
- private TextView mTextView;
- private final WindowManager.LayoutParams mCurrentLayoutParams;
- private final WindowManager mWindowManager;
- private final AccessibilityFloatingMenuView mAnchorView;
-
- BaseTooltipView(@UiContext Context context, AccessibilityFloatingMenuView anchorView) {
- super(context);
- mWindowManager = context.getSystemService(WindowManager.class);
- mAnchorView = anchorView;
- mCurrentLayoutParams = createDefaultLayoutParams();
-
- initViews();
- }
-
- @Override
- protected void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
-
- mAnchorView.onConfigurationChanged(newConfig);
- updateTooltipView();
-
- mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
- hide();
- }
-
- return super.onTouchEvent(event);
- }
-
- @Override
- public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
- super.onInitializeAccessibilityNodeInfo(info);
-
- info.addAction(AccessibilityAction.ACTION_DISMISS);
- }
-
- @Override
- public boolean performAccessibilityAction(int action, Bundle arguments) {
- if (action == AccessibilityAction.ACTION_DISMISS.getId()) {
- hide();
- return true;
- }
-
- return super.performAccessibilityAction(action, arguments);
- }
-
- void show() {
- if (isShowing()) {
- return;
- }
-
- mIsShowing = true;
- updateTooltipView();
-
- mWindowManager.addView(this, mCurrentLayoutParams);
- }
-
- void hide() {
- if (!isShowing()) {
- return;
- }
-
- mIsShowing = false;
- mWindowManager.removeView(this);
- }
-
- void setDescription(CharSequence text) {
- mTextView.setText(text);
- }
-
- void setMovementMethod(MovementMethod movement) {
- mTextView.setMovementMethod(movement);
- }
-
- private boolean isShowing() {
- return mIsShowing;
- }
-
- private void initViews() {
- final View contentView =
- LayoutInflater.from(getContext()).inflate(
- R.layout.accessibility_floating_menu_tooltip, this, false);
-
- mTextView = contentView.findViewById(R.id.text);
-
- addView(contentView);
- }
-
- private static WindowManager.LayoutParams createDefaultLayoutParams() {
- final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
- WindowManager.LayoutParams.WRAP_CONTENT,
- WindowManager.LayoutParams.WRAP_CONTENT,
- WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
- WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
- PixelFormat.TRANSLUCENT);
- params.windowAnimations = android.R.style.Animation_Translucent;
- params.gravity = Gravity.START | Gravity.TOP;
-
- return params;
- }
-
- private void updateDimensions() {
- final Resources res = getResources();
- final DisplayMetrics dm = res.getDisplayMetrics();
- mScreenWidth = dm.widthPixels;
- mArrowWidth =
- res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_arrow_width);
- mArrowHeight =
- res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_arrow_height);
- mArrowMargin =
- res.getDimensionPixelSize(
- R.dimen.accessibility_floating_tooltip_arrow_margin);
- mArrowCornerRadius =
- res.getDimensionPixelSize(
- R.dimen.accessibility_floating_tooltip_arrow_corner_radius);
- mFontSize =
- res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_font_size);
- mTextViewMargin =
- res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_margin);
- mTextViewPadding =
- res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_padding);
- mTextViewCornerRadius =
- res.getDimensionPixelSize(
- R.dimen.accessibility_floating_tooltip_text_corner_radius);
- }
-
- private void updateTooltipView() {
- updateDimensions();
- updateTextView();
-
- final Rect anchorViewLocation = mAnchorView.getWindowLocationOnScreen();
- updateArrowWith(anchorViewLocation);
- updateWidthWith(anchorViewLocation);
- updateLocationWith(anchorViewLocation);
- }
-
- private void updateTextView() {
- mTextView.setTextSize(COMPLEX_UNIT_PX, mFontSize);
- mTextView.setPadding(mTextViewPadding, mTextViewPadding, mTextViewPadding,
- mTextViewPadding);
-
- final GradientDrawable gradientDrawable = (GradientDrawable) mTextView.getBackground();
- gradientDrawable.setCornerRadius(mTextViewCornerRadius);
- gradientDrawable.setColor(Utils.getColorAttrDefaultColor(getContext(),
- com.android.internal.R.attr.colorAccentPrimary));
- }
-
- private void updateArrowWith(Rect anchorViewLocation) {
- final boolean isAnchorViewOnLeft = isAnchorViewOnLeft(anchorViewLocation);
- final View arrowView = findViewById(isAnchorViewOnLeft
- ? R.id.arrow_left
- : R.id.arrow_right);
- arrowView.setVisibility(VISIBLE);
- drawArrow(arrowView, isAnchorViewOnLeft);
-
- final LinearLayout.LayoutParams layoutParams =
- (LinearLayout.LayoutParams) arrowView.getLayoutParams();
- layoutParams.width = mArrowWidth;
- layoutParams.height = mArrowHeight;
-
- final int leftMargin = isAnchorViewOnLeft ? 0 : mArrowMargin;
- final int rightMargin = isAnchorViewOnLeft ? mArrowMargin : 0;
- layoutParams.setMargins(leftMargin, 0, rightMargin, 0);
- arrowView.setLayoutParams(layoutParams);
- }
-
- private void updateWidthWith(Rect anchorViewLocation) {
- final ViewGroup.LayoutParams layoutParams = mTextView.getLayoutParams();
- layoutParams.width = getTextWidthWith(anchorViewLocation);
- mTextView.setLayoutParams(layoutParams);
- }
-
- private void updateLocationWith(Rect anchorViewLocation) {
- mCurrentLayoutParams.x = isAnchorViewOnLeft(anchorViewLocation)
- ? anchorViewLocation.width()
- : mScreenWidth - getWindowWidthWith(anchorViewLocation)
- - anchorViewLocation.width();
- mCurrentLayoutParams.y =
- anchorViewLocation.centerY() - (getTextHeightWith(anchorViewLocation) / 2);
- }
-
- private void drawArrow(View view, boolean isPointingLeft) {
- final ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
- final TriangleShape triangleShape =
- TriangleShape.createHorizontal(layoutParams.width, layoutParams.height,
- isPointingLeft);
- final ShapeDrawable arrowDrawable = new ShapeDrawable(triangleShape);
- final Paint arrowPaint = arrowDrawable.getPaint();
- arrowPaint.setColor(Utils.getColorAttrDefaultColor(getContext(),
- com.android.internal.R.attr.colorAccentPrimary));
- final CornerPathEffect effect = new CornerPathEffect(mArrowCornerRadius);
- arrowPaint.setPathEffect(effect);
- view.setBackground(arrowDrawable);
- }
-
- private boolean isAnchorViewOnLeft(Rect anchorViewLocation) {
- return anchorViewLocation.left < (mScreenWidth / 2);
- }
-
- private int getTextWidthWith(Rect anchorViewLocation) {
- final int widthSpec =
- MeasureSpec.makeMeasureSpec(getAvailableTextWidthWith(anchorViewLocation), AT_MOST);
- final int heightSpec =
- MeasureSpec.makeMeasureSpec(0, UNSPECIFIED);
- mTextView.measure(widthSpec, heightSpec);
- return mTextView.getMeasuredWidth();
- }
-
- private int getTextHeightWith(Rect anchorViewLocation) {
- final int widthSpec =
- MeasureSpec.makeMeasureSpec(getAvailableTextWidthWith(anchorViewLocation), AT_MOST);
- final int heightSpec =
- MeasureSpec.makeMeasureSpec(0, UNSPECIFIED);
- mTextView.measure(widthSpec, heightSpec);
- return mTextView.getMeasuredHeight();
- }
-
- private int getAvailableTextWidthWith(Rect anchorViewLocation) {
- return mScreenWidth - anchorViewLocation.width() - mArrowWidth - mArrowMargin
- - mTextViewMargin;
- }
-
- private int getWindowWidthWith(Rect anchorViewLocation) {
- return getTextWidthWith(anchorViewLocation) + mArrowWidth + mArrowMargin;
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DockTooltipView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DockTooltipView.java
deleted file mode 100644
index 49056a6..0000000
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DockTooltipView.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.accessibility.floatingmenu;
-
-import android.content.Context;
-
-import com.android.systemui.R;
-
-/**
- * Dock tooltip view that shows the info about moving the Accessibility button to the edge to hide.
- */
-class DockTooltipView extends BaseTooltipView {
- private final AccessibilityFloatingMenuView mAnchorView;
-
- DockTooltipView(Context context, AccessibilityFloatingMenuView anchorView) {
- super(context, anchorView);
- mAnchorView = anchorView;
-
- setDescription(
- getContext().getText(R.string.accessibility_floating_button_docking_tooltip));
- }
-
- @Override
- void hide() {
- super.hide();
-
- mAnchorView.stopTranslateXAnimation();
- }
-
- @Override
- void show() {
- super.show();
-
- mAnchorView.startTranslateXAnimation();
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompat.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompat.java
deleted file mode 100644
index 93b0676..0000000
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompat.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.accessibility.floatingmenu;
-
-import android.content.res.Resources;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
-import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
-
-import com.android.systemui.R;
-import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.ShapeType;
-
-import java.lang.ref.WeakReference;
-
-/**
- * An accessibility item delegate for the individual items of the list view
- * {@link AccessibilityFloatingMenuView}.
- */
-final class ItemDelegateCompat extends RecyclerViewAccessibilityDelegate.ItemDelegate {
- private final WeakReference<AccessibilityFloatingMenuView> mMenuViewRef;
-
- ItemDelegateCompat(@NonNull RecyclerViewAccessibilityDelegate recyclerViewDelegate,
- AccessibilityFloatingMenuView menuView) {
- super(recyclerViewDelegate);
- this.mMenuViewRef = new WeakReference<>(menuView);
- }
-
- @Override
- public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
- super.onInitializeAccessibilityNodeInfo(host, info);
-
- if (mMenuViewRef.get() == null) {
- return;
- }
- final AccessibilityFloatingMenuView menuView = mMenuViewRef.get();
-
- final Resources res = menuView.getResources();
- final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveTopLeft =
- new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.action_move_top_left,
- res.getString(
- R.string.accessibility_floating_button_action_move_top_left));
- info.addAction(moveTopLeft);
-
- final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveTopRight =
- new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
- R.id.action_move_top_right,
- res.getString(
- R.string.accessibility_floating_button_action_move_top_right));
- info.addAction(moveTopRight);
-
- final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveBottomLeft =
- new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
- R.id.action_move_bottom_left,
- res.getString(
- R.string.accessibility_floating_button_action_move_bottom_left));
- info.addAction(moveBottomLeft);
-
- final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveBottomRight =
- new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
- R.id.action_move_bottom_right,
- res.getString(
- R.string.accessibility_floating_button_action_move_bottom_right));
- info.addAction(moveBottomRight);
-
- final int moveEdgeId = menuView.isOvalShape()
- ? R.id.action_move_to_edge_and_hide
- : R.id.action_move_out_edge_and_show;
- final int moveEdgeTextResId = menuView.isOvalShape()
- ? R.string.accessibility_floating_button_action_move_to_edge_and_hide_to_half
- : R.string.accessibility_floating_button_action_move_out_edge_and_show;
- final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveToOrOutEdge =
- new AccessibilityNodeInfoCompat.AccessibilityActionCompat(moveEdgeId,
- res.getString(moveEdgeTextResId));
- info.addAction(moveToOrOutEdge);
- }
-
- @Override
- public boolean performAccessibilityAction(View host, int action, Bundle args) {
- if (mMenuViewRef.get() == null) {
- return super.performAccessibilityAction(host, action, args);
- }
- final AccessibilityFloatingMenuView menuView = mMenuViewRef.get();
-
- menuView.fadeIn();
-
- final Rect bounds = menuView.getAvailableBounds();
- if (action == R.id.action_move_top_left) {
- menuView.setShapeType(ShapeType.OVAL);
- menuView.snapToLocation(bounds.left, bounds.top);
- return true;
- }
-
- if (action == R.id.action_move_top_right) {
- menuView.setShapeType(ShapeType.OVAL);
- menuView.snapToLocation(bounds.right, bounds.top);
- return true;
- }
-
- if (action == R.id.action_move_bottom_left) {
- menuView.setShapeType(ShapeType.OVAL);
- menuView.snapToLocation(bounds.left, bounds.bottom);
- return true;
- }
-
- if (action == R.id.action_move_bottom_right) {
- menuView.setShapeType(ShapeType.OVAL);
- menuView.snapToLocation(bounds.right, bounds.bottom);
- return true;
- }
-
- if (action == R.id.action_move_to_edge_and_hide) {
- menuView.setShapeType(ShapeType.HALF_OVAL);
- return true;
- }
-
- if (action == R.id.action_move_out_edge_and_show) {
- menuView.setShapeType(ShapeType.OVAL);
- return true;
- }
-
- return super.performAccessibilityAction(host, action, args);
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MigrationTooltipView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MigrationTooltipView.java
deleted file mode 100644
index e4f3e31..0000000
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MigrationTooltipView.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.accessibility.floatingmenu;
-
-import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_BUTTON_COMPONENT_NAME;
-
-import android.content.Context;
-import android.content.Intent;
-import android.provider.Settings;
-import android.text.method.LinkMovementMethod;
-
-import com.android.systemui.R;
-
-/**
- * Migration tooltip view that shows the information about the Accessibility button was replaced
- * with the floating menu.
- */
-class MigrationTooltipView extends BaseTooltipView {
- MigrationTooltipView(Context context, AccessibilityFloatingMenuView anchorView) {
- super(context, anchorView);
-
- final Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_DETAILS_SETTINGS);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.putExtra(Intent.EXTRA_COMPONENT_NAME,
- ACCESSIBILITY_BUTTON_COMPONENT_NAME.flattenToShortString());
-
- final AnnotationLinkSpan.LinkInfo linkInfo = new AnnotationLinkSpan.LinkInfo(
- AnnotationLinkSpan.LinkInfo.DEFAULT_ANNOTATION,
- v -> {
- getContext().startActivity(intent);
- hide();
- });
-
- final int textResId = R.string.accessibility_floating_button_migration_tooltip;
- setDescription(AnnotationLinkSpan.linkify(getContext().getText(textResId), linkInfo));
- setMovementMethod(LinkMovementMethod.getInstance());
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
index 436f9df..1f6f6d9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
@@ -46,6 +46,8 @@
private var isDeviceFolded: Boolean = false
private val isSideFps: Boolean
+ private val isReverseDefaultRotation =
+ context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation)
private val screenSizeFoldProvider: ScreenSizeFoldProvider = ScreenSizeFoldProvider(context)
var iconLayoutParamSize: Pair<Int, Int> = Pair(1, 1)
set(value) {
@@ -76,7 +78,7 @@
isSideFps = sideFps
val displayInfo = DisplayInfo()
context.display?.getDisplayInfo(displayInfo)
- if (isSideFps && displayInfo.rotation == Surface.ROTATION_180) {
+ if (isSideFps && getRotationFromDefault(displayInfo.rotation) == Surface.ROTATION_180) {
iconView.rotation = 180f
}
screenSizeFoldProvider.registerCallback(this, context.mainExecutor)
@@ -86,7 +88,7 @@
private fun updateIconSideFps(@BiometricState lastState: Int, @BiometricState newState: Int) {
val displayInfo = DisplayInfo()
context.display?.getDisplayInfo(displayInfo)
- val rotation = displayInfo.rotation
+ val rotation = getRotationFromDefault(displayInfo.rotation)
val iconAnimation = getSideFpsAnimationForTransition(rotation)
val iconViewOverlayAnimation =
getSideFpsOverlayAnimationForTransition(lastState, newState, rotation) ?: return
@@ -104,7 +106,7 @@
iconView.frame = 0
iconViewOverlay.frame = 0
- if (shouldAnimateIconViewForTransition(lastState, newState)) {
+ if (shouldAnimateSfpsIconViewForTransition(lastState, newState)) {
iconView.playAnimation()
}
@@ -169,6 +171,18 @@
STATE_HELP,
STATE_ERROR -> true
STATE_AUTHENTICATING_ANIMATING_IN,
+ STATE_AUTHENTICATING -> oldState == STATE_ERROR || oldState == STATE_HELP
+ STATE_AUTHENTICATED -> true
+ else -> false
+ }
+
+ private fun shouldAnimateSfpsIconViewForTransition(
+ @BiometricState oldState: Int,
+ @BiometricState newState: Int
+ ) = when (newState) {
+ STATE_HELP,
+ STATE_ERROR -> true
+ STATE_AUTHENTICATING_ANIMATING_IN,
STATE_AUTHENTICATING ->
oldState == STATE_ERROR || oldState == STATE_HELP || oldState == STATE_IDLE
STATE_AUTHENTICATED -> true
@@ -217,6 +231,9 @@
return if (id != null) return id else null
}
+ private fun getRotationFromDefault(rotation: Int): Int =
+ if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation
+
@RawRes
private fun getSideFpsAnimationForTransition(rotation: Int): Int = when (rotation) {
Surface.ROTATION_90 -> if (isDeviceFolded) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index dad6ebe..c8cf5d7 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -55,7 +55,6 @@
import android.os.Handler;
import android.os.RemoteException;
import android.os.UserManager;
-import android.util.DisplayUtils;
import android.util.Log;
import android.util.RotationUtils;
import android.util.SparseBooleanArray;
@@ -69,6 +68,8 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.os.SomeArgs;
import com.android.internal.widget.LockPatternUtils;
+import com.android.settingslib.udfps.UdfpsOverlayParams;
+import com.android.settingslib.udfps.UdfpsUtils;
import com.android.systemui.CoreStartable;
import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
import com.android.systemui.biometrics.domain.interactor.LogContextInteractor;
@@ -169,6 +170,7 @@
@NonNull private final UserManager mUserManager;
@NonNull private final LockPatternUtils mLockPatternUtils;
@NonNull private final InteractionJankMonitor mInteractionJankMonitor;
+ @NonNull private final UdfpsUtils mUdfpsUtils;
private final @Background DelayableExecutor mBackgroundExecutor;
private final DisplayInfo mCachedDisplayInfo = new DisplayInfo();
@@ -578,17 +580,7 @@
*/
private void updateSensorLocations() {
mDisplay.getDisplayInfo(mCachedDisplayInfo);
- final Display.Mode maxDisplayMode =
- DisplayUtils.getMaximumResolutionDisplayMode(mCachedDisplayInfo.supportedModes);
- final float scaleFactor = android.util.DisplayUtils.getPhysicalPixelDisplaySizeRatio(
- maxDisplayMode.getPhysicalWidth(), maxDisplayMode.getPhysicalHeight(),
- mCachedDisplayInfo.getNaturalWidth(), mCachedDisplayInfo.getNaturalHeight());
- if (scaleFactor == Float.POSITIVE_INFINITY) {
- mScaleFactor = 1f;
- } else {
- mScaleFactor = scaleFactor;
- }
-
+ mScaleFactor = mUdfpsUtils.getScaleFactor(mCachedDisplayInfo);
updateUdfpsLocation();
updateFingerprintLocation();
updateFaceLocation();
@@ -732,7 +724,8 @@
@NonNull InteractionJankMonitor jankMonitor,
@Main Handler handler,
@Background DelayableExecutor bgExecutor,
- @NonNull VibratorHelper vibrator) {
+ @NonNull VibratorHelper vibrator,
+ @NonNull UdfpsUtils udfpsUtils) {
mContext = context;
mExecution = execution;
mUserManager = userManager;
@@ -753,6 +746,7 @@
mSfpsEnrolledForUser = new SparseBooleanArray();
mFaceEnrolledForUser = new SparseBooleanArray();
mVibratorHelper = vibrator;
+ mUdfpsUtils = udfpsUtils;
mLogContextInteractor = logContextInteractor;
mBiometricPromptInteractor = biometricPromptInteractor;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/EllipseOverlapDetectorParams.kt b/packages/SystemUI/src/com/android/systemui/biometrics/EllipseOverlapDetectorParams.kt
new file mode 100644
index 0000000..ab9b690
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/EllipseOverlapDetectorParams.kt
@@ -0,0 +1,13 @@
+package com.android.systemui.biometrics
+
+/**
+ * Collection of parameters used by EllipseOverlapDetector
+ *
+ * [minOverlap] minimum percentage (float from 0-1) needed to be considered a valid overlap
+ *
+ * [targetSize] percentage (defined as a float of 0-1) of sensor that is considered the target,
+ * expands outward from center
+ *
+ * [stepSize] size of each step when iterating over sensor pixel grid
+ */
+class EllipseOverlapDetectorParams(val minOverlap: Float, val targetSize: Float, val stepSize: Int)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 7217f99..074928a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -50,10 +50,8 @@
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.util.Log;
-import android.util.RotationUtils;
import android.view.LayoutInflater;
import android.view.MotionEvent;
-import android.view.Surface;
import android.view.VelocityTracker;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
@@ -66,6 +64,8 @@
import com.android.internal.util.LatencyTracker;
import com.android.keyguard.FaceAuthApiRequestReason;
import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.settingslib.udfps.UdfpsOverlayParams;
+import com.android.settingslib.udfps.UdfpsUtils;
import com.android.systemui.Dumpable;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.biometrics.dagger.BiometricsBackground;
@@ -168,6 +168,7 @@
@NonNull private final SessionTracker mSessionTracker;
@NonNull private final AlternateBouncerInteractor mAlternateBouncerInteractor;
@NonNull private final SecureSettings mSecureSettings;
+ @NonNull private final UdfpsUtils mUdfpsUtils;
// Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple
// sensors, this, in addition to a lot of the code here, will be updated.
@@ -266,7 +267,7 @@
mUdfpsDisplayMode, mSecureSettings, requestId, reason, callback,
(view, event, fromUdfpsView) -> onTouch(requestId, event,
fromUdfpsView), mActivityLaunchAnimator, mFeatureFlags,
- mPrimaryBouncerInteractor, mAlternateBouncerInteractor)));
+ mPrimaryBouncerInteractor, mAlternateBouncerInteractor, mUdfpsUtils)));
}
@Override
@@ -475,27 +476,6 @@
&& mOverlayParams.getSensorBounds().contains((int) x, (int) y);
}
- private Point getTouchInNativeCoordinates(@NonNull MotionEvent event, int idx) {
- Point portraitTouch = new Point(
- (int) event.getRawX(idx),
- (int) event.getRawY(idx)
- );
- final int rot = mOverlayParams.getRotation();
- if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
- RotationUtils.rotatePoint(portraitTouch,
- RotationUtils.deltaRotation(rot, Surface.ROTATION_0),
- mOverlayParams.getLogicalDisplayWidth(),
- mOverlayParams.getLogicalDisplayHeight()
- );
- }
-
- // Scale the coordinates to native resolution.
- final float scale = mOverlayParams.getScaleFactor();
- portraitTouch.x = (int) (portraitTouch.x / scale);
- portraitTouch.y = (int) (portraitTouch.y / scale);
- return portraitTouch;
- }
-
private void tryDismissingKeyguard() {
if (!mOnFingerDown) {
playStartHaptic();
@@ -557,7 +537,8 @@
final InstanceId sessionIdProvider = mSessionTracker.getSessionId(
getBiometricSessionType());
final int sessionId = (sessionIdProvider != null) ? sessionIdProvider.getId() : -1;
- final int touchConfigId = BOUNDING_BOX_TOUCH_CONFIG_ID;
+ final int touchConfigId = mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_selected_udfps_touch_detection);
SysUiStatsLog.write(SysUiStatsLog.BIOMETRIC_TOUCH_REPORTED, biometricTouchReportedTouchType,
touchConfigId, sessionId, data.getX(), data.getY(), data.getMinor(),
@@ -712,7 +693,8 @@
break;
}
// Map the touch to portrait mode if the device is in landscape mode.
- final Point scaledTouch = getTouchInNativeCoordinates(event, idx);
+ final Point scaledTouch = mUdfpsUtils.getTouchInNativeCoordinates(
+ idx, event, mOverlayParams);
if (actionMoveWithinSensorArea) {
if (mVelocityTracker == null) {
// touches could be injected, so the velocity tracker may not have
@@ -756,16 +738,7 @@
+ "but serverRequest is null");
return;
}
- // Scale the coordinates to native resolution.
- final float scale = mOverlayParams.getScaleFactor();
- final float scaledSensorX =
- mOverlayParams.getSensorBounds().centerX() / scale;
- final float scaledSensorY =
- mOverlayParams.getSensorBounds().centerY() / scale;
-
- mOverlay.onTouchOutsideOfSensorArea(
- scaledTouch.x, scaledTouch.y, scaledSensorX, scaledSensorY,
- mOverlayParams.getRotation());
+ mOverlay.onTouchOutsideOfSensorArea(scaledTouch);
});
}
}
@@ -837,7 +810,8 @@
@NonNull SinglePointerTouchProcessor singlePointerTouchProcessor,
@NonNull SessionTracker sessionTracker,
@NonNull AlternateBouncerInteractor alternateBouncerInteractor,
- @NonNull SecureSettings secureSettings) {
+ @NonNull SecureSettings secureSettings,
+ @NonNull UdfpsUtils udfpsUtils) {
mContext = context;
mExecution = execution;
mVibrator = vibrator;
@@ -879,6 +853,7 @@
mPrimaryBouncerInteractor = primaryBouncerInteractor;
mAlternateBouncerInteractor = alternateBouncerInteractor;
mSecureSettings = secureSettings;
+ mUdfpsUtils = udfpsUtils;
mTouchProcessor = mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)
? singlePointerTouchProcessor : null;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 6680787..55bacef 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -20,6 +20,7 @@
import android.annotation.UiThread
import android.content.Context
import android.graphics.PixelFormat
+import android.graphics.Point
import android.graphics.Rect
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
@@ -46,6 +47,8 @@
import androidx.annotation.LayoutRes
import androidx.annotation.VisibleForTesting
import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.settingslib.udfps.UdfpsUtils
+import com.android.settingslib.udfps.UdfpsOverlayParams
import com.android.systemui.R
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.dump.DumpManager
@@ -101,6 +104,7 @@
private val primaryBouncerInteractor: PrimaryBouncerInteractor,
private val alternateBouncerInteractor: AlternateBouncerInteractor,
private val isDebuggable: Boolean = Build.IS_DEBUGGABLE,
+ private val udfpsUtils: UdfpsUtils
) {
/** The view, when [isShowing], or null. */
var overlayView: UdfpsView? = null
@@ -239,7 +243,9 @@
FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS)) {
// Enroll udfps UI is handled by settings, so use empty view here
UdfpsFpmEmptyViewController(
- view.addUdfpsView(R.layout.udfps_fpm_empty_view),
+ view.addUdfpsView(R.layout.udfps_fpm_empty_view){
+ updateAccessibilityViewLocation(sensorBounds)
+ },
statusBarStateController,
shadeExpansionStateManager,
dialogManager,
@@ -348,81 +354,18 @@
* the angle to a list of help messages which are announced if accessibility is enabled.
*
*/
- fun onTouchOutsideOfSensorArea(
- touchX: Float,
- touchY: Float,
- sensorX: Float,
- sensorY: Float,
- rotation: Int
- ) {
-
- if (!touchExplorationEnabled) {
- return
+ fun onTouchOutsideOfSensorArea(scaledTouch: Point) {
+ val theStr =
+ udfpsUtils.onTouchOutsideOfSensorArea(
+ touchExplorationEnabled,
+ context,
+ scaledTouch.x,
+ scaledTouch.y,
+ overlayParams
+ )
+ if (theStr != null) {
+ animationViewController?.doAnnounceForAccessibility(theStr)
}
- val touchHints =
- context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
- if (touchHints.size != 4) {
- Log.e(TAG, "expected exactly 4 touch hints, got $touchHints.size?")
- return
- }
- val theStr = onTouchOutsideOfSensorAreaImpl(touchX, touchY, sensorX, sensorY, rotation)
- Log.v(TAG, "Announcing touch outside : " + theStr)
- animationViewController?.doAnnounceForAccessibility(theStr)
- }
-
- /**
- * This function computes the angle of touch relative to the sensor and maps
- * the angle to a list of help messages which are announced if accessibility is enabled.
- *
- * There are 4 quadrants of the circle (90 degree arcs)
- *
- * [315, 360] && [0, 45) -> touchHints[0] = "Move Fingerprint to the left"
- * [45, 135) -> touchHints[1] = "Move Fingerprint down"
- * And so on.
- */
- fun onTouchOutsideOfSensorAreaImpl(
- touchX: Float,
- touchY: Float,
- sensorX: Float,
- sensorY: Float,
- rotation: Int
- ): String {
- val touchHints =
- context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
-
- val xRelativeToSensor = touchX - sensorX
- // Touch coordinates are with respect to the upper left corner, so reverse
- // this calculation
- val yRelativeToSensor = sensorY - touchY
-
- var angleInRad =
- Math.atan2(yRelativeToSensor.toDouble(), xRelativeToSensor.toDouble())
- // If the radians are negative, that means we are counting clockwise.
- // So we need to add 360 degrees
- if (angleInRad < 0.0) {
- angleInRad += 2.0 * Math.PI
- }
- // rad to deg conversion
- val degrees = Math.toDegrees(angleInRad)
-
- val degreesPerBucket = 360.0 / touchHints.size
- val halfBucketDegrees = degreesPerBucket / 2.0
- // The mapping should be as follows
- // [315, 360] && [0, 45] -> 0
- // [45, 135] -> 1
- var index = (((degrees + halfBucketDegrees) % 360) / degreesPerBucket).toInt()
- index %= touchHints.size
-
- // A rotation of 90 degrees corresponds to increasing the index by 1.
- if (rotation == Surface.ROTATION_90) {
- index = (index + 1) % touchHints.size
- }
-
- if (rotation == Surface.ROTATION_270) {
- index = (index + 3) % touchHints.size
- }
-
- return touchHints[index]
}
/** Cancel this request. */
@@ -442,7 +385,8 @@
): WindowManager.LayoutParams {
val paddingX = animation?.paddingX ?: 0
val paddingY = animation?.paddingY ?: 0
- if (animation != null && animation.listenForTouchesOutsideView()) {
+ if (!featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION) && animation != null &&
+ animation.listenForTouchesOutsideView()) {
flags = flags or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt
index e8f041e..8352d0a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt
@@ -16,7 +16,11 @@
package com.android.systemui.biometrics
import android.content.Context
+import android.graphics.Rect
import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import com.android.systemui.R
/**
* View corresponding with udfps_fpm_empty_view.xml
@@ -32,4 +36,13 @@
private val fingerprintDrawable: UdfpsFpDrawable = UdfpsFpDrawable(context)
override fun getDrawable(): UdfpsDrawable = fingerprintDrawable
+
+ fun updateAccessibilityViewLocation(sensorBounds: Rect) {
+ val fingerprintAccessibilityView: View = findViewById(R.id.udfps_enroll_accessibility_view)
+ val params: ViewGroup.LayoutParams = fingerprintAccessibilityView.layoutParams
+ params.width = sensorBounds.width()
+ params.height = sensorBounds.height()
+ fingerprintAccessibilityView.layoutParams = params
+ fingerprintAccessibilityView.requestLayout()
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
index 802b9b6..079c0b3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
@@ -32,6 +32,7 @@
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY
import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.settingslib.udfps.UdfpsOverlayParams
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt
index 4e6a06b..28ca41d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt
@@ -25,6 +25,7 @@
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.FrameLayout
+import com.android.settingslib.udfps.UdfpsOverlayParams
private const val TAG = "UdfpsOverlayView"
private const val POINT_SIZE = 10f
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt
index e61c614..06dee7a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt
@@ -26,6 +26,7 @@
import android.util.Log
import android.view.MotionEvent
import android.widget.FrameLayout
+import com.android.settingslib.udfps.UdfpsOverlayParams
import com.android.systemui.R
import com.android.systemui.doze.DozeReceiver
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
index 6f8efba..67d2f30 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
@@ -16,6 +16,7 @@
package com.android.systemui.biometrics.dagger
+import com.android.settingslib.udfps.UdfpsUtils
import com.android.systemui.biometrics.data.repository.PromptRepository
import com.android.systemui.biometrics.data.repository.PromptRepositoryImpl
import com.android.systemui.biometrics.domain.interactor.CredentialInteractor
@@ -54,6 +55,9 @@
@BiometricsBackground
fun providesPluginExecutor(threadFactory: ThreadFactory): Executor =
threadFactory.buildExecutorOnNewThread("biometrics")
+
+ @Provides
+ fun providesUdfpsUtils(): UdfpsUtils = UdfpsUtils()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/UdfpsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/UdfpsModule.kt
index 001fed7..20c3e40 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/UdfpsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/UdfpsModule.kt
@@ -16,6 +16,9 @@
package com.android.systemui.biometrics.dagger
+import android.content.res.Resources
+import com.android.internal.R
+import com.android.systemui.biometrics.EllipseOverlapDetectorParams
import com.android.systemui.biometrics.udfps.BoundingBoxOverlapDetector
import com.android.systemui.biometrics.udfps.EllipseOverlapDetector
import com.android.systemui.biometrics.udfps.OverlapDetector
@@ -33,10 +36,30 @@
@Provides
@SysUISingleton
fun providesOverlapDetector(featureFlags: FeatureFlags): OverlapDetector {
- return if (featureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) {
- EllipseOverlapDetector()
+ if (featureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) {
+ val selectedOption =
+ Resources.getSystem()
+ .getInteger(R.integer.config_selected_udfps_touch_detection)
+ val values =
+ Resources.getSystem()
+ .getStringArray(R.array.config_udfps_touch_detection_options)[
+ selectedOption]
+ .split(",")
+ .map { it.toFloat() }
+
+ return if (values[0] == 1f) {
+ EllipseOverlapDetector(
+ EllipseOverlapDetectorParams(
+ minOverlap = values[3],
+ targetSize = values[2],
+ stepSize = values[4].toInt()
+ )
+ )
+ } else {
+ BoundingBoxOverlapDetector()
+ }
} else {
- BoundingBoxOverlapDetector()
+ return BoundingBoxOverlapDetector()
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt
index 682d38a..2e2970f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt
@@ -18,22 +18,84 @@
import android.graphics.Point
import android.graphics.Rect
-import androidx.annotation.VisibleForTesting
+import android.util.Log
+import com.android.systemui.biometrics.EllipseOverlapDetectorParams
import com.android.systemui.dagger.SysUISingleton
import kotlin.math.cos
-import kotlin.math.pow
import kotlin.math.sin
+private enum class SensorPixelPosition {
+ OUTSIDE, // Pixel that falls outside of sensor circle
+ SENSOR, // Pixel within sensor circle
+ TARGET // Pixel within sensor center target
+}
+
+private val isDebug = true
+private val TAG = "EllipseOverlapDetector"
+
/**
* Approximates the touch as an ellipse and determines whether the ellipse has a sufficient overlap
* with the sensor.
*/
@SysUISingleton
-class EllipseOverlapDetector(private val neededPoints: Int = 2) : OverlapDetector {
-
+class EllipseOverlapDetector(private val params: EllipseOverlapDetectorParams) : OverlapDetector {
override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean {
- val points = calculateSensorPoints(nativeSensorBounds)
- return points.count { checkPoint(it, touchData) } >= neededPoints
+ var isTargetTouched = false
+ var sensorPixels = 0
+ var coveredPixels = 0
+ for (y in nativeSensorBounds.top..nativeSensorBounds.bottom step params.stepSize) {
+ for (x in nativeSensorBounds.left..nativeSensorBounds.right step params.stepSize) {
+ // Check where pixel is within the sensor TODO: (b/265836919) This could be improved
+ // by precomputing these points
+ val pixelPosition =
+ isPartOfSensorArea(
+ x,
+ y,
+ nativeSensorBounds.centerX(),
+ nativeSensorBounds.centerY(),
+ nativeSensorBounds.width() / 2
+ )
+ if (pixelPosition != SensorPixelPosition.OUTSIDE) {
+ sensorPixels++
+
+ // Check if this pixel falls within ellipse touch
+ if (checkPoint(Point(x, y), touchData)) {
+ coveredPixels++
+
+ // Check that at least one covered pixel is within sensor target
+ isTargetTouched =
+ isTargetTouched or (pixelPosition == SensorPixelPosition.TARGET)
+ }
+ }
+ }
+ }
+
+ val percentage: Float = coveredPixels.toFloat() / sensorPixels
+ if (isDebug) {
+ Log.v(
+ TAG,
+ "covered: $coveredPixels, sensor: $sensorPixels, " +
+ "percentage: $percentage, isCenterTouched: $isTargetTouched"
+ )
+ }
+
+ return percentage >= params.minOverlap && isTargetTouched
+ }
+
+ /** Checks if point is in the sensor center target circle, outer circle, or outside of sensor */
+ private fun isPartOfSensorArea(x: Int, y: Int, cX: Int, cY: Int, r: Int): SensorPixelPosition {
+ val dx = cX - x
+ val dy = cY - y
+
+ val disSquared = dx * dx + dy * dy
+
+ return if (disSquared <= (r * params.targetSize) * (r * params.targetSize)) {
+ SensorPixelPosition.TARGET
+ } else if (disSquared <= r * r) {
+ SensorPixelPosition.SENSOR
+ } else {
+ SensorPixelPosition.OUTSIDE
+ }
}
private fun checkPoint(point: Point, touchData: NormalizedTouchData): Boolean {
@@ -45,29 +107,9 @@
val c: Float = sin(touchData.orientation) * (point.x - touchData.x)
val d: Float = cos(touchData.orientation) * (point.y - touchData.y)
val result =
- (a + b).pow(2) / (touchData.minor / 2).pow(2) +
- (c - d).pow(2) / (touchData.major / 2).pow(2)
+ (a + b) * (a + b) / ((touchData.minor / 2) * (touchData.minor / 2)) +
+ (c - d) * (c - d) / ((touchData.major / 2) * (touchData.major / 2))
return result <= 1
}
-
- @VisibleForTesting
- fun calculateSensorPoints(sensorBounds: Rect): List<Point> {
- val sensorX = sensorBounds.centerX()
- val sensorY = sensorBounds.centerY()
- val cornerOffset: Int = sensorBounds.width() / 4
- val sideOffset: Int = sensorBounds.width() / 3
-
- return listOf(
- Point(sensorX - cornerOffset, sensorY - cornerOffset),
- Point(sensorX, sensorY - sideOffset),
- Point(sensorX + cornerOffset, sensorY - cornerOffset),
- Point(sensorX - sideOffset, sensorY),
- Point(sensorX, sensorY),
- Point(sensorX + sideOffset, sensorY),
- Point(sensorX - cornerOffset, sensorY + cornerOffset),
- Point(sensorX, sensorY + sideOffset),
- Point(sensorX + cornerOffset, sensorY + cornerOffset)
- )
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt
index 39ea936..234b383 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt
@@ -21,7 +21,7 @@
import android.view.MotionEvent
import android.view.MotionEvent.INVALID_POINTER_ID
import android.view.Surface
-import com.android.systemui.biometrics.UdfpsOverlayParams
+import com.android.settingslib.udfps.UdfpsOverlayParams
import com.android.systemui.biometrics.udfps.TouchProcessorResult.Failure
import com.android.systemui.biometrics.udfps.TouchProcessorResult.ProcessedTouch
import com.android.systemui.dagger.SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessor.kt
index ffcebf9..4bf0ef6 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessor.kt
@@ -17,7 +17,7 @@
package com.android.systemui.biometrics.udfps
import android.view.MotionEvent
-import com.android.systemui.biometrics.UdfpsOverlayParams
+import com.android.settingslib.udfps.UdfpsOverlayParams
/**
* Determines whether a finger entered or left the area of the under-display fingerprint sensor
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
index fb0c0a6..5ca36ab 100644
--- a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
@@ -72,7 +72,7 @@
height = WindowManager.LayoutParams.MATCH_PARENT
layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
format = PixelFormat.TRANSLUCENT
- type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY
+ type = WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG
fitInsetsTypes = 0 // Ignore insets from all system bars
title = "Wired Charging Animation"
flags = (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
index 1c26841..82bb723 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
@@ -21,6 +21,7 @@
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN;
+import static com.android.systemui.flags.Flags.CLIPBOARD_MINIMIZED_LAYOUT;
import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE;
@@ -35,6 +36,7 @@
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.CoreStartable;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.flags.FeatureFlags;
import javax.inject.Inject;
import javax.inject.Provider;
@@ -57,6 +59,7 @@
private final Provider<ClipboardOverlayController> mOverlayProvider;
private final ClipboardToast mClipboardToast;
private final ClipboardManager mClipboardManager;
+ private final FeatureFlags mFeatureFlags;
private final UiEventLogger mUiEventLogger;
private ClipboardOverlay mClipboardOverlay;
@@ -65,11 +68,13 @@
Provider<ClipboardOverlayController> clipboardOverlayControllerProvider,
ClipboardToast clipboardToast,
ClipboardManager clipboardManager,
+ FeatureFlags featureFlags,
UiEventLogger uiEventLogger) {
mContext = context;
mOverlayProvider = clipboardOverlayControllerProvider;
mClipboardToast = clipboardToast;
mClipboardManager = clipboardManager;
+ mFeatureFlags = featureFlags;
mUiEventLogger = uiEventLogger;
}
@@ -107,7 +112,11 @@
} else {
mUiEventLogger.log(CLIPBOARD_OVERLAY_UPDATED, 0, clipSource);
}
- mClipboardOverlay.setClipData(clipData, clipSource);
+ if (mFeatureFlags.isEnabled(CLIPBOARD_MINIMIZED_LAYOUT)) {
+ mClipboardOverlay.setClipData(clipData, clipSource);
+ } else {
+ mClipboardOverlay.setClipDataLegacy(clipData, clipSource);
+ }
mClipboardOverlay.setOnSessionCompleteListener(() -> {
// Session is complete, free memory until it's needed again.
mClipboardOverlay = null;
@@ -150,6 +159,8 @@
}
interface ClipboardOverlay {
+ void setClipDataLegacy(ClipData clipData, String clipSource);
+
void setClipData(ClipData clipData, String clipSource);
void setOnSessionCompleteListener(Runnable runnable);
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardModel.kt b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardModel.kt
new file mode 100644
index 0000000..c7aaf09
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardModel.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.clipboardoverlay
+
+import android.content.ClipData
+import android.content.ClipDescription.EXTRA_IS_SENSITIVE
+import android.content.Context
+import android.graphics.Bitmap
+import android.text.TextUtils
+import android.util.Log
+import android.util.Size
+import com.android.systemui.R
+import java.io.IOException
+
+data class ClipboardModel(
+ val clipData: ClipData?,
+ val source: String,
+ val type: Type = Type.OTHER,
+ val item: ClipData.Item? = null,
+ val isSensitive: Boolean = false,
+ val isRemote: Boolean = false,
+) {
+ private var _bitmap: Bitmap? = null
+
+ fun dataMatches(other: ClipboardModel?): Boolean {
+ if (other == null) {
+ return false
+ }
+ return source == other.source &&
+ type == other.type &&
+ item?.text == other.item?.text &&
+ item?.uri == other.item?.uri &&
+ isSensitive == other.isSensitive
+ }
+
+ fun loadThumbnail(context: Context): Bitmap? {
+ if (_bitmap == null && type == Type.IMAGE && item?.uri != null) {
+ try {
+ val size = context.resources.getDimensionPixelSize(R.dimen.overlay_x_scale)
+ _bitmap =
+ context.contentResolver.loadThumbnail(item.uri, Size(size, size * 4), null)
+ } catch (e: IOException) {
+ Log.e(TAG, "Thumbnail loading failed!", e)
+ }
+ }
+ return _bitmap
+ }
+
+ internal companion object {
+ private val TAG: String = "ClipboardModel"
+
+ @JvmStatic
+ fun fromClipData(
+ context: Context,
+ utils: ClipboardOverlayUtils,
+ clipData: ClipData?,
+ source: String
+ ): ClipboardModel {
+ if (clipData == null || clipData.itemCount == 0) {
+ return ClipboardModel(clipData, source)
+ }
+ val sensitive = clipData.description?.extras?.getBoolean(EXTRA_IS_SENSITIVE) ?: false
+ val item = clipData.getItemAt(0)!!
+ val type = getType(context, item)
+ val remote = utils.isRemoteCopy(context, clipData, source)
+ return ClipboardModel(clipData, source, type, item, sensitive, remote)
+ }
+
+ private fun getType(context: Context, item: ClipData.Item): Type {
+ return if (!TextUtils.isEmpty(item.text)) {
+ Type.TEXT
+ } else if (
+ item.uri != null &&
+ context.contentResolver.getType(item.uri)?.startsWith("image") == true
+ ) {
+ Type.IMAGE
+ } else {
+ Type.OTHER
+ }
+ }
+ }
+
+ enum class Type {
+ TEXT,
+ IMAGE,
+ OTHER
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index 8c8ee8a..b41f308 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -17,7 +17,6 @@
package com.android.systemui.clipboardoverlay;
import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
-import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_ACTIONS;
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON;
@@ -31,10 +30,9 @@
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT;
+import static com.android.systemui.flags.Flags.CLIPBOARD_MINIMIZED_LAYOUT;
import static com.android.systemui.flags.Flags.CLIPBOARD_REMOTE_BEHAVIOR;
-import static java.util.Objects.requireNonNull;
-
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.app.RemoteAction;
@@ -47,7 +45,6 @@
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
-import android.hardware.display.DisplayManager;
import android.hardware.input.InputManager;
import android.net.Uri;
import android.os.Looper;
@@ -55,14 +52,15 @@
import android.text.TextUtils;
import android.util.Log;
import android.util.Size;
-import android.view.Display;
import android.view.InputEvent;
import android.view.InputEventReceiver;
import android.view.InputMonitor;
import android.view.MotionEvent;
+import android.view.WindowInsets;
import androidx.annotation.NonNull;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -71,7 +69,6 @@
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.screenshot.TimeoutHandler;
-import com.android.systemui.settings.DisplayTracker;
import java.io.IOException;
import java.util.Optional;
@@ -95,8 +92,6 @@
private final Context mContext;
private final ClipboardLogger mClipboardLogger;
private final BroadcastDispatcher mBroadcastDispatcher;
- private final DisplayManager mDisplayManager;
- private final DisplayTracker mDisplayTracker;
private final ClipboardOverlayWindow mWindow;
private final TimeoutHandler mTimeoutHandler;
private final ClipboardOverlayUtils mClipboardUtils;
@@ -122,6 +117,9 @@
private Runnable mOnUiUpdate;
+ private boolean mIsMinimized;
+ private ClipboardModel mClipboardModel;
+
private final ClipboardOverlayView.ClipboardOverlayCallbacks mClipboardCallbacks =
new ClipboardOverlayView.ClipboardOverlayCallbacks() {
@Override
@@ -175,6 +173,13 @@
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
animateOut();
}
+
+ @Override
+ public void onMinimizedViewTapped() {
+ if (mFeatureFlags.isEnabled(CLIPBOARD_MINIMIZED_LAYOUT)) {
+ animateFromMinimized();
+ }
+ }
};
@Inject
@@ -187,33 +192,28 @@
FeatureFlags featureFlags,
ClipboardOverlayUtils clipboardUtils,
@Background Executor bgExecutor,
- UiEventLogger uiEventLogger,
- DisplayTracker displayTracker) {
+ UiEventLogger uiEventLogger) {
+ mContext = context;
mBroadcastDispatcher = broadcastDispatcher;
- mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
- mDisplayTracker = displayTracker;
- final Context displayContext = context.createDisplayContext(getDefaultDisplay());
- mContext = displayContext.createWindowContext(TYPE_SCREENSHOT, null);
mClipboardLogger = new ClipboardLogger(uiEventLogger);
mView = clipboardOverlayView;
mWindow = clipboardOverlayWindow;
- mWindow.init(mView::setInsets, () -> {
+ mWindow.init(this::onInsetsChanged, () -> {
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
hideImmediate();
});
+ mFeatureFlags = featureFlags;
mTimeoutHandler = timeoutHandler;
mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS);
- mFeatureFlags = featureFlags;
mClipboardUtils = clipboardUtils;
mBgExecutor = bgExecutor;
mView.setCallbacks(mClipboardCallbacks);
-
mWindow.withWindowAttached(() -> {
mWindow.setContentView(mView);
mView.setInsets(mWindow.getWindowInsets(),
@@ -258,8 +258,135 @@
broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION);
}
+ @VisibleForTesting
+ void onInsetsChanged(WindowInsets insets, int orientation) {
+ mView.setInsets(insets, orientation);
+ if (mFeatureFlags.isEnabled(CLIPBOARD_MINIMIZED_LAYOUT)) {
+ if (shouldShowMinimized(insets) && !mIsMinimized) {
+ mIsMinimized = true;
+ mView.setMinimized(true);
+ }
+ }
+ }
+
@Override // ClipboardListener.ClipboardOverlay
- public void setClipData(ClipData clipData, String clipSource) {
+ public void setClipData(ClipData data, String source) {
+ ClipboardModel model = ClipboardModel.fromClipData(mContext, mClipboardUtils, data, source);
+ if (mExitAnimator != null && mExitAnimator.isRunning()) {
+ mExitAnimator.cancel();
+ }
+ boolean shouldAnimate = !model.dataMatches(mClipboardModel);
+ mClipboardModel = model;
+ mClipboardLogger.setClipSource(mClipboardModel.getSource());
+ if (shouldAnimate) {
+ reset();
+ mClipboardLogger.setClipSource(mClipboardModel.getSource());
+ if (shouldShowMinimized(mWindow.getWindowInsets())) {
+ mIsMinimized = true;
+ mView.setMinimized(true);
+ } else {
+ setExpandedView();
+ }
+ animateIn();
+ mView.announceForAccessibility(getAccessibilityAnnouncement(mClipboardModel.getType()));
+ } else if (!mIsMinimized) {
+ setExpandedView();
+ }
+ if (mFeatureFlags.isEnabled(CLIPBOARD_REMOTE_BEHAVIOR) && mClipboardModel.isRemote()) {
+ mTimeoutHandler.cancelTimeout();
+ mOnUiUpdate = null;
+ } else {
+ mOnUiUpdate = mTimeoutHandler::resetTimeout;
+ mOnUiUpdate.run();
+ }
+ }
+
+ private void setExpandedView() {
+ final ClipboardModel model = mClipboardModel;
+ mView.setMinimized(false);
+ switch (model.getType()) {
+ case TEXT:
+ if ((mFeatureFlags.isEnabled(CLIPBOARD_REMOTE_BEHAVIOR) && model.isRemote())
+ || DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) {
+ if (model.getItem().getTextLinks() != null) {
+ classifyText(model);
+ }
+ }
+ if (model.isSensitive()) {
+ mView.showTextPreview(mContext.getString(R.string.clipboard_asterisks), true);
+ } else {
+ mView.showTextPreview(model.getItem().getText(), false);
+ }
+ mView.setEditAccessibilityAction(true);
+ mOnPreviewTapped = this::editText;
+ break;
+ case IMAGE:
+ if (model.isSensitive() || model.loadThumbnail(mContext) != null) {
+ mView.showImagePreview(
+ model.isSensitive() ? null : model.loadThumbnail(mContext));
+ mView.setEditAccessibilityAction(true);
+ mOnPreviewTapped = () -> editImage(model.getItem().getUri());
+ } else {
+ // image loading failed
+ mView.showDefaultTextPreview();
+ }
+ break;
+ case OTHER:
+ mView.showDefaultTextPreview();
+ break;
+ }
+ if (mFeatureFlags.isEnabled(CLIPBOARD_REMOTE_BEHAVIOR)) {
+ if (!model.isRemote()) {
+ maybeShowRemoteCopy(model.getClipData());
+ }
+ } else {
+ maybeShowRemoteCopy(model.getClipData());
+ }
+ if (model.getType() != ClipboardModel.Type.OTHER) {
+ mOnShareTapped = () -> shareContent(model.getClipData());
+ mView.showShareChip();
+ }
+ }
+
+ private boolean shouldShowMinimized(WindowInsets insets) {
+ return insets.getInsets(WindowInsets.Type.ime()).bottom > 0;
+ }
+
+ private void animateFromMinimized() {
+ mIsMinimized = false;
+ setExpandedView();
+ animateIn();
+ }
+
+ private String getAccessibilityAnnouncement(ClipboardModel.Type type) {
+ if (type == ClipboardModel.Type.TEXT) {
+ return mContext.getString(R.string.clipboard_text_copied);
+ } else if (type == ClipboardModel.Type.IMAGE) {
+ return mContext.getString(R.string.clipboard_image_copied);
+ } else {
+ return mContext.getString(R.string.clipboard_content_copied);
+ }
+ }
+
+ private void classifyText(ClipboardModel model) {
+ mBgExecutor.execute(() -> {
+ Optional<RemoteAction> remoteAction =
+ mClipboardUtils.getAction(model.getItem(), model.getSource());
+ if (model.equals(mClipboardModel)) {
+ remoteAction.ifPresent(action -> {
+ mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_ACTION_SHOWN);
+ mView.setActionChip(action, () -> {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
+ animateOut();
+ });
+ });
+ }
+ });
+ }
+
+ @Override // ClipboardListener.ClipboardOverlay
+ public void setClipDataLegacy(ClipData clipData, String clipSource) {
if (mExitAnimator != null && mExitAnimator.isRunning()) {
mExitAnimator.cancel();
}
@@ -516,10 +643,6 @@
mClipboardLogger.reset();
}
- private Display getDefaultDisplay() {
- return mDisplayManager.getDisplay(mDisplayTracker.getDefaultDisplayId());
- }
-
static class ClipboardLogger {
private final UiEventLogger mUiEventLogger;
private String mClipSource;
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
index 2d33157..c9e01ce 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
@@ -18,8 +18,6 @@
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static java.util.Objects.requireNonNull;
-
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
@@ -77,6 +75,8 @@
void onShareButtonTapped();
void onPreviewTapped();
+
+ void onMinimizedViewTapped();
}
private static final String TAG = "ClipboardView";
@@ -92,6 +92,7 @@
private ImageView mImagePreview;
private TextView mTextPreview;
private TextView mHiddenPreview;
+ private LinearLayout mMinimizedPreview;
private View mPreviewBorder;
private OverlayActionChip mEditChip;
private OverlayActionChip mShareChip;
@@ -117,18 +118,18 @@
@Override
protected void onFinishInflate() {
- mActionContainerBackground =
- requireNonNull(findViewById(R.id.actions_container_background));
- mActionContainer = requireNonNull(findViewById(R.id.actions));
- mClipboardPreview = requireNonNull(findViewById(R.id.clipboard_preview));
- mImagePreview = requireNonNull(findViewById(R.id.image_preview));
- mTextPreview = requireNonNull(findViewById(R.id.text_preview));
- mHiddenPreview = requireNonNull(findViewById(R.id.hidden_preview));
- mPreviewBorder = requireNonNull(findViewById(R.id.preview_border));
- mEditChip = requireNonNull(findViewById(R.id.edit_chip));
- mShareChip = requireNonNull(findViewById(R.id.share_chip));
- mRemoteCopyChip = requireNonNull(findViewById(R.id.remote_copy_chip));
- mDismissButton = requireNonNull(findViewById(R.id.dismiss_button));
+ mActionContainerBackground = requireViewById(R.id.actions_container_background);
+ mActionContainer = requireViewById(R.id.actions);
+ mClipboardPreview = requireViewById(R.id.clipboard_preview);
+ mPreviewBorder = requireViewById(R.id.preview_border);
+ mImagePreview = requireViewById(R.id.image_preview);
+ mTextPreview = requireViewById(R.id.text_preview);
+ mHiddenPreview = requireViewById(R.id.hidden_preview);
+ mMinimizedPreview = requireViewById(R.id.minimized_preview);
+ mEditChip = requireViewById(R.id.edit_chip);
+ mShareChip = requireViewById(R.id.share_chip);
+ mRemoteCopyChip = requireViewById(R.id.remote_copy_chip);
+ mDismissButton = requireViewById(R.id.dismiss_button);
mEditChip.setAlpha(1);
mShareChip.setAlpha(1);
@@ -163,6 +164,7 @@
mDismissButton.setOnClickListener(v -> clipboardCallbacks.onDismissButtonTapped());
mRemoteCopyChip.setOnClickListener(v -> clipboardCallbacks.onRemoteCopyButtonTapped());
mClipboardPreview.setOnClickListener(v -> clipboardCallbacks.onPreviewTapped());
+ mMinimizedPreview.setOnClickListener(v -> clipboardCallbacks.onMinimizedViewTapped());
}
void setEditAccessibilityAction(boolean editable) {
@@ -177,12 +179,28 @@
}
}
+ void setMinimized(boolean minimized) {
+ if (minimized) {
+ mMinimizedPreview.setVisibility(View.VISIBLE);
+ mClipboardPreview.setVisibility(View.GONE);
+ mPreviewBorder.setVisibility(View.GONE);
+ mActionContainer.setVisibility(View.GONE);
+ mActionContainerBackground.setVisibility(View.GONE);
+ } else {
+ mMinimizedPreview.setVisibility(View.GONE);
+ mClipboardPreview.setVisibility(View.VISIBLE);
+ mPreviewBorder.setVisibility(View.VISIBLE);
+ mActionContainer.setVisibility(View.VISIBLE);
+ }
+ }
+
void setInsets(WindowInsets insets, int orientation) {
FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) getLayoutParams();
if (p == null) {
return;
}
Rect margins = computeMargins(insets, orientation);
+
p.setMargins(margins.left, margins.top, margins.right, margins.bottom);
setLayoutParams(p);
requestLayout();
@@ -204,6 +222,12 @@
(int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
touchRegion.op(tmpRect, Region.Op.UNION);
+ mMinimizedPreview.getBoundsOnScreen(tmpRect);
+ tmpRect.inset(
+ (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
+ (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
+ touchRegion.op(tmpRect, Region.Op.UNION);
+
mDismissButton.getBoundsOnScreen(tmpRect);
touchRegion.op(tmpRect, Region.Op.UNION);
@@ -298,6 +322,8 @@
scaleAnim.setDuration(333);
scaleAnim.addUpdateListener(animation -> {
float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
+ mMinimizedPreview.setScaleX(previewScale);
+ mMinimizedPreview.setScaleY(previewScale);
mClipboardPreview.setScaleX(previewScale);
mClipboardPreview.setScaleY(previewScale);
mPreviewBorder.setScaleX(previewScale);
@@ -319,12 +345,14 @@
alphaAnim.setDuration(283);
alphaAnim.addUpdateListener(animation -> {
float alpha = animation.getAnimatedFraction();
+ mMinimizedPreview.setAlpha(alpha);
mClipboardPreview.setAlpha(alpha);
mPreviewBorder.setAlpha(alpha);
mDismissButton.setAlpha(alpha);
mActionContainer.setAlpha(alpha);
});
+ mMinimizedPreview.setAlpha(0);
mActionContainer.setAlpha(0);
mPreviewBorder.setAlpha(0);
mClipboardPreview.setAlpha(0);
@@ -356,6 +384,8 @@
scaleAnim.setDuration(250);
scaleAnim.addUpdateListener(animation -> {
float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
+ mMinimizedPreview.setScaleX(previewScale);
+ mMinimizedPreview.setScaleY(previewScale);
mClipboardPreview.setScaleX(previewScale);
mClipboardPreview.setScaleY(previewScale);
mPreviewBorder.setScaleX(previewScale);
@@ -377,6 +407,7 @@
alphaAnim.setDuration(166);
alphaAnim.addUpdateListener(animation -> {
float alpha = 1 - animation.getAnimatedFraction();
+ mMinimizedPreview.setAlpha(alpha);
mClipboardPreview.setAlpha(alpha);
mPreviewBorder.setAlpha(alpha);
mDismissButton.setAlpha(alpha);
@@ -399,6 +430,7 @@
mTextPreview.setVisibility(View.GONE);
mImagePreview.setVisibility(View.GONE);
mHiddenPreview.setVisibility(View.GONE);
+ mMinimizedPreview.setVisibility(View.GONE);
v.setVisibility(View.VISIBLE);
}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
index dbe301d..860149d 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
@@ -28,12 +28,13 @@
import android.content.pm.ServiceInfo
import android.os.UserHandle
import android.service.controls.ControlsProviderService
+import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import com.android.settingslib.applications.DefaultAppInfo
import com.android.systemui.R
import java.util.Objects
-class ControlsServiceInfo(
+open class ControlsServiceInfo(
private val context: Context,
val serviceInfo: ServiceInfo
) : DefaultAppInfo(
@@ -64,7 +65,7 @@
* [R.array.config_controlsPreferredPackages] can declare activities for use as a panel.
*/
var panelActivity: ComponentName? = null
- private set
+ protected set
private var resolved: Boolean = false
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
index ce82af7..700f4f6 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -123,16 +123,13 @@
userChanging = false
}
- private val userTrackerCallback = object : UserTracker.Callback {
- override fun onUserChanged(newUser: Int, userContext: Context) {
- userChanging = true
- val newUserHandle = UserHandle.of(newUser)
- if (currentUser == newUserHandle) {
- userChanging = false
- return
- }
- setValuesForUser(newUserHandle)
+ override fun changeUser(newUser: UserHandle) {
+ userChanging = true
+ if (currentUser == newUser) {
+ userChanging = false
+ return
}
+ setValuesForUser(newUser)
}
@VisibleForTesting
@@ -233,7 +230,6 @@
dumpManager.registerDumpable(javaClass.name, this)
resetFavorites()
userChanging = false
- userTracker.addCallback(userTrackerCallback, executor)
context.registerReceiver(
restoreFinishedReceiver,
IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED),
@@ -245,7 +241,6 @@
}
fun destroy() {
- userTracker.removeCallback(userTrackerCallback)
context.unregisterReceiver(restoreFinishedReceiver)
listingController.removeCallback(listingCallback)
}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/StartControlsStartableModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/StartControlsStartableModule.kt
new file mode 100644
index 0000000..3f20c26
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/StartControlsStartableModule.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.controls.dagger
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.controls.start.ControlsStartable
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+abstract class StartControlsStartableModule {
+ @Binds
+ @IntoMap
+ @ClassKey(ControlsStartable::class)
+ abstract fun bindFeature(impl: ControlsStartable): CoreStartable
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt
new file mode 100644
index 0000000..9d99253
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.controls.start
+
+import android.content.Context
+import android.content.res.Resources
+import android.os.UserHandle
+import com.android.systemui.CoreStartable
+import com.android.systemui.R
+import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.ui.SelectedItem
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.settings.UserTracker
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+/**
+ * Started with SystemUI to perform early operations for device controls subsystem (only if enabled)
+ *
+ * In particular, it will perform the following:
+ * * If there is no preferred selection for provider and at least one of the preferred packages
+ * provides a panel, it will select the first one that does.
+ * * If the preferred selection provides a panel, it will bind to that service (to reduce latency on
+ * displaying the panel).
+ *
+ * It will also perform those operations on user change.
+ */
+@SysUISingleton
+class ControlsStartable
+@Inject
+constructor(
+ @Main private val resources: Resources,
+ @Background private val executor: Executor,
+ private val controlsComponent: ControlsComponent,
+ private val userTracker: UserTracker
+) : CoreStartable {
+
+ // These two controllers can only be accessed after `start` method once we've checked if the
+ // feature is enabled
+ private val controlsController: ControlsController
+ get() = controlsComponent.getControlsController().get()
+
+ private val controlsListingController: ControlsListingController
+ get() = controlsComponent.getControlsListingController().get()
+
+ private val userTrackerCallback =
+ object : UserTracker.Callback {
+ override fun onUserChanged(newUser: Int, userContext: Context) {
+ controlsController.changeUser(UserHandle.of(newUser))
+ startForUser()
+ }
+ }
+
+ override fun start() {
+ if (!controlsComponent.isEnabled()) {
+ // Controls is disabled, we don't need this anymore
+ return
+ }
+ startForUser()
+ userTracker.addCallback(userTrackerCallback, executor)
+ }
+
+ private fun startForUser() {
+ selectDefaultPanelIfNecessary()
+ bindToPanel()
+ }
+
+ private fun selectDefaultPanelIfNecessary() {
+ val currentSelection = controlsController.getPreferredSelection()
+ if (currentSelection == SelectedItem.EMPTY_SELECTION) {
+ val availableServices = controlsListingController.getCurrentServices()
+ val panels = availableServices.filter { it.panelActivity != null }
+ resources
+ .getStringArray(R.array.config_controlsPreferredPackages)
+ // Looking for the first element in the string array such that there is one package
+ // that has a panel. It will return null if there are no packages in the array,
+ // or if no packages in the array have a panel associated with it.
+ .firstNotNullOfOrNull { name ->
+ panels.firstOrNull { it.componentName.packageName == name }
+ }
+ ?.let { info ->
+ controlsController.setPreferredSelection(
+ SelectedItem.PanelItem(info.loadLabel(), info.componentName)
+ )
+ }
+ }
+ }
+
+ private fun bindToPanel() {
+ val currentSelection = controlsController.getPreferredSelection()
+ val panels =
+ controlsListingController.getCurrentServices().filter { it.panelActivity != null }
+ if (
+ currentSelection is SelectedItem.PanelItem &&
+ panels.firstOrNull { it.componentName == currentSelection.componentName } != null
+ ) {
+ controlsController.bindComponentForPanel(currentSelection.componentName)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index 9e71bef..58f4835 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -28,6 +28,7 @@
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
+import android.os.Trace
import android.service.controls.Control
import android.service.controls.ControlsProviderService
import android.util.Log
@@ -224,6 +225,7 @@
activityContext: Context
) {
Log.d(ControlsUiController.TAG, "show()")
+ Trace.instant(Trace.TRACE_TAG_APP, "ControlsUiControllerImpl#show")
this.parent = parent
this.onDismiss = onDismiss
this.activityContext = activityContext
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt
index f5764c2..3b6ab20 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt
@@ -27,6 +27,7 @@
import android.graphics.Color
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RoundRectShape
+import android.os.Trace
import com.android.systemui.R
import com.android.systemui.util.boundsOnScreen
import com.android.wm.shell.TaskView
@@ -84,6 +85,7 @@
options,
taskView.boundsOnScreen
)
+ Trace.instant(Trace.TRACE_TAG_APP, "PanelTaskViewController - startActivity")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
index 4eb444e..a5beb4e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
@@ -23,6 +23,8 @@
import com.android.systemui.keyguard.WorkLockActivity;
import com.android.systemui.people.PeopleSpaceActivity;
import com.android.systemui.people.widget.LaunchConversationActivity;
+import com.android.systemui.screenshot.AppClipsActivity;
+import com.android.systemui.screenshot.AppClipsTrampolineActivity;
import com.android.systemui.screenshot.LongScreenshotActivity;
import com.android.systemui.sensorprivacy.SensorUseStartedActivity;
import com.android.systemui.sensorprivacy.television.TvSensorPrivacyChangedActivity;
@@ -119,6 +121,18 @@
@ClassKey(LongScreenshotActivity.class)
public abstract Activity bindLongScreenshotActivity(LongScreenshotActivity activity);
+ /** Inject into AppClipsTrampolineActivity. */
+ @Binds
+ @IntoMap
+ @ClassKey(AppClipsTrampolineActivity.class)
+ public abstract Activity bindAppClipsTrampolineActivity(AppClipsTrampolineActivity activity);
+
+ /** Inject into AppClipsActivity. */
+ @Binds
+ @IntoMap
+ @ClassKey(AppClipsActivity.class)
+ public abstract Activity bindAppClipsActivity(AppClipsActivity activity);
+
/** Inject into LaunchConversationActivity. */
@Binds
@IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 48f90bc..b5a1f45 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -27,7 +27,9 @@
import com.android.systemui.biometrics.AuthController
import com.android.systemui.biometrics.UdfpsOverlay
import com.android.systemui.clipboardoverlay.ClipboardListener
+import com.android.systemui.controls.dagger.StartControlsStartableModule
import com.android.systemui.dagger.qualifiers.PerUser
+import com.android.systemui.dreams.AssistantAttentionMonitor
import com.android.systemui.dreams.DreamMonitor
import com.android.systemui.globalactions.GlobalActionsComponent
import com.android.systemui.keyboard.KeyboardUI
@@ -66,7 +68,10 @@
/**
* Collection of {@link CoreStartable}s that should be run on AOSP.
*/
-@Module(includes = [MultiUserUtilsModule::class])
+@Module(includes = [
+ MultiUserUtilsModule::class,
+ StartControlsStartableModule::class
+])
abstract class SystemUICoreStartableModule {
/** Inject into AuthController. */
@Binds
@@ -309,4 +314,10 @@
@IntoMap
@ClassKey(DreamMonitor::class)
abstract fun bindDreamMonitor(sysui: DreamMonitor): CoreStartable
+
+ /**Inject into AssistantAttentionMonitor */
+ @Binds
+ @IntoMap
+ @ClassKey(AssistantAttentionMonitor::class)
+ abstract fun bindAssistantAttentionMonitor(sysui: AssistantAttentionMonitor): CoreStartable
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index c3f24f0..45872f4 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -218,6 +218,10 @@
abstract BcSmartspaceConfigPlugin optionalBcSmartspaceConfigPlugin();
@BindsOptionalOf
+ @Named(SmartspaceModule.DATE_SMARTSPACE_DATA_PLUGIN)
+ abstract BcSmartspaceDataPlugin optionalDateSmartspaceConfigPlugin();
+
+ @BindsOptionalOf
@Named(SmartspaceModule.WEATHER_SMARTSPACE_DATA_PLUGIN)
abstract BcSmartspaceDataPlugin optionalWeatherSmartspaceConfigPlugin();
diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
index 976afd4..88c0c50 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
@@ -34,6 +34,7 @@
import com.android.systemui.biometrics.AuthController
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.log.ScreenDecorationsLogger
import com.android.systemui.plugins.statusbar.StatusBarStateController
import java.util.concurrent.Executor
import javax.inject.Inject
@@ -45,6 +46,7 @@
private val statusBarStateController: StatusBarStateController,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
@Main private val mainExecutor: Executor,
+ private val logger: ScreenDecorationsLogger,
) : DecorProviderFactory() {
private val display = context.display
private val displayInfo = DisplayInfo()
@@ -82,7 +84,8 @@
authController,
statusBarStateController,
keyguardUpdateMonitor,
- mainExecutor
+ mainExecutor,
+ logger,
)
)
}
@@ -104,7 +107,8 @@
private val authController: AuthController,
private val statusBarStateController: StatusBarStateController,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
- private val mainExecutor: Executor
+ private val mainExecutor: Executor,
+ private val logger: ScreenDecorationsLogger,
) : BoundDecorProvider() {
override val viewId: Int = com.android.systemui.R.id.face_scanning_anim
@@ -136,7 +140,8 @@
alignedBound,
statusBarStateController,
keyguardUpdateMonitor,
- mainExecutor
+ mainExecutor,
+ logger,
)
view.id = viewId
view.setColor(tintColor)
@@ -155,8 +160,9 @@
layoutParams.let { lp ->
lp.width = ViewGroup.LayoutParams.MATCH_PARENT
lp.height = ViewGroup.LayoutParams.MATCH_PARENT
+ logger.faceSensorLocation(authController.faceSensorLocation)
authController.faceSensorLocation?.y?.let { faceAuthSensorHeight ->
- val faceScanningHeight = (faceAuthSensorHeight * 2).toInt()
+ val faceScanningHeight = (faceAuthSensorHeight * 2)
when (rotation) {
Surface.ROTATION_0, Surface.ROTATION_180 ->
lp.height = faceScanningHeight
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/AssistantAttentionMonitor.java b/packages/SystemUI/src/com/android/systemui/dreams/AssistantAttentionMonitor.java
new file mode 100644
index 0000000..49d7f78
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/AssistantAttentionMonitor.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams;
+
+import android.util.Log;
+
+import com.android.systemui.CoreStartable;
+import com.android.systemui.dreams.callbacks.AssistantAttentionCallback;
+import com.android.systemui.dreams.conditions.AssistantAttentionCondition;
+import com.android.systemui.shared.condition.Monitor;
+
+import javax.inject.Inject;
+
+/**
+ * A {@link CoreStartable} to retain a monitor for tracking assistant attention.
+ */
+public class AssistantAttentionMonitor implements CoreStartable {
+ private static final String TAG = "AssistAttentionMonitor";
+
+ // We retain a reference to the monitor so it is not garbage-collected.
+ private final Monitor mConditionMonitor;
+ private final AssistantAttentionCondition mAssistantAttentionCondition;
+ private final AssistantAttentionCallback mCallback;
+
+ @Inject
+ public AssistantAttentionMonitor(
+ Monitor monitor,
+ AssistantAttentionCondition assistantAttentionCondition,
+ AssistantAttentionCallback callback) {
+ mConditionMonitor = monitor;
+ mAssistantAttentionCondition = assistantAttentionCondition;
+ mCallback = callback;
+
+ }
+ @Override
+ public void start() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "started");
+ }
+
+ mConditionMonitor.addSubscription(new Monitor.Subscription.Builder(mCallback)
+ .addCondition(mAssistantAttentionCondition)
+ .build());
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
index 2c7ecb1..b7f6a70 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
@@ -56,6 +56,8 @@
public static final int STATE_LOW_LIGHT_ACTIVE = 1 << 1;
public static final int STATE_DREAM_ENTRY_ANIMATIONS_FINISHED = 1 << 2;
public static final int STATE_DREAM_EXIT_ANIMATIONS_RUNNING = 1 << 3;
+ public static final int STATE_HAS_ASSISTANT_ATTENTION = 1 << 4;
+ public static final int STATE_DREAM_OVERLAY_STATUS_BAR_VISIBLE = 1 << 5;
private static final int OP_CLEAR_STATE = 1;
private static final int OP_SET_STATE = 2;
@@ -251,6 +253,22 @@
return containsState(STATE_DREAM_EXIT_ANIMATIONS_RUNNING);
}
+ /**
+ * Returns whether assistant currently has the user's attention.
+ * @return {@code true} if assistant has the user's attention, {@code false} otherwise.
+ */
+ public boolean hasAssistantAttention() {
+ return containsState(STATE_HAS_ASSISTANT_ATTENTION);
+ }
+
+ /**
+ * Returns whether the dream overlay status bar is currently visible.
+ * @return {@code true} if the status bar is visible, {@code false} otherwise.
+ */
+ public boolean isDreamOverlayStatusBarVisible() {
+ return containsState(STATE_DREAM_OVERLAY_STATUS_BAR_VISIBLE);
+ }
+
private boolean containsState(int state) {
return (mState & state) != 0;
}
@@ -310,6 +328,23 @@
}
/**
+ * Sets whether assistant currently has the user's attention.
+ * @param hasAttention {@code true} if has the user's attention, {@code false} otherwise.
+ */
+ public void setHasAssistantAttention(boolean hasAttention) {
+ modifyState(hasAttention ? OP_SET_STATE : OP_CLEAR_STATE, STATE_HAS_ASSISTANT_ATTENTION);
+ }
+
+ /**
+ * Sets whether the dream overlay status bar is visible.
+ * @param visible {@code true} if the status bar is visible, {@code false} otherwise.
+ */
+ public void setDreamOverlayStatusBarVisible(boolean visible) {
+ modifyState(
+ visible ? OP_SET_STATE : OP_CLEAR_STATE, STATE_DREAM_OVERLAY_STATUS_BAR_VISIBLE);
+ }
+
+ /**
* Returns the available complication types.
*/
@Complication.ComplicationType
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
index 96bce4c..7c1bfed 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
@@ -52,7 +52,8 @@
STATUS_ICON_CAMERA_DISABLED,
STATUS_ICON_MIC_DISABLED,
STATUS_ICON_MIC_CAMERA_DISABLED,
- STATUS_ICON_PRIORITY_MODE_ON
+ STATUS_ICON_PRIORITY_MODE_ON,
+ STATUS_ICON_ASSISTANT_ATTENTION_ACTIVE,
})
public @interface StatusIconType {}
public static final int STATUS_ICON_NOTIFICATIONS = 0;
@@ -62,6 +63,7 @@
public static final int STATUS_ICON_MIC_DISABLED = 4;
public static final int STATUS_ICON_MIC_CAMERA_DISABLED = 5;
public static final int STATUS_ICON_PRIORITY_MODE_ON = 6;
+ public static final int STATUS_ICON_ASSISTANT_ATTENTION_ACTIVE = 7;
private final Map<Integer, View> mStatusIcons = new HashMap<>();
private Context mContext;
@@ -132,6 +134,8 @@
fetchStatusIconForResId(R.id.dream_overlay_notification_indicator));
mStatusIcons.put(STATUS_ICON_PRIORITY_MODE_ON,
addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_priority_mode)));
+ mStatusIcons.put(STATUS_ICON_ASSISTANT_ATTENTION_ACTIVE,
+ fetchStatusIconForResId(R.id.dream_overlay_assistant_attention_indicator));
mSystemStatusViewGroup = findViewById(R.id.dream_overlay_system_status);
mExtraSystemStatusViewGroup = findViewById(R.id.dream_overlay_extra_items);
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
index 90c440c..2221a04 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
@@ -113,6 +113,7 @@
mEntryAnimationsFinished =
mDreamOverlayStateController.areEntryAnimationsFinished();
updateVisibility();
+ updateAssistantAttentionIcon();
}
};
@@ -214,6 +215,7 @@
provider -> provider.removeCallback(mNotificationCountCallback));
mStatusBarItemsProvider.removeCallback(mStatusBarItemsProviderCallback);
mView.removeAllExtraStatusBarItemViews();
+ mDreamOverlayStateController.setDreamOverlayStatusBarVisible(false);
mDreamOverlayStateController.removeCallback(mDreamOverlayStateCallback);
mTouchInsetSession.clear();
@@ -270,12 +272,20 @@
hasAlarm ? buildAlarmContentDescription(alarm) : null);
}
+ private void updateAssistantAttentionIcon() {
+ showIcon(DreamOverlayStatusBarView.STATUS_ICON_ASSISTANT_ATTENTION_ACTIVE,
+ mDreamOverlayStateController.hasAssistantAttention());
+ }
+
private void updateVisibility() {
- if (shouldShowStatusBar()) {
- mView.setVisibility(View.VISIBLE);
- } else {
- mView.setVisibility(View.INVISIBLE);
+ final int currentVisibility = mView.getVisibility();
+ final int newVisibility = shouldShowStatusBar() ? View.VISIBLE : View.INVISIBLE;
+ if (currentVisibility == newVisibility) {
+ return;
}
+
+ mView.setVisibility(newVisibility);
+ mDreamOverlayStateController.setDreamOverlayStatusBarVisible(newVisibility == View.VISIBLE);
}
private String buildAlarmContentDescription(AlarmManager.AlarmClockInfo alarm) {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/callbacks/AssistantAttentionCallback.java b/packages/SystemUI/src/com/android/systemui/dreams/callbacks/AssistantAttentionCallback.java
new file mode 100644
index 0000000..5d8a9b6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/callbacks/AssistantAttentionCallback.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams.callbacks;
+
+import android.util.Log;
+
+import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.shared.condition.Monitor;
+
+import javax.inject.Inject;
+
+/**
+ * A callback that informs {@link DreamOverlayStateController} when assistant has the user's
+ * attention.
+ */
+public class AssistantAttentionCallback implements Monitor.Callback {
+ private static final String TAG = "AssistAttentionCallback";
+
+ private final DreamOverlayStateController mStateController;
+
+ @Inject
+ public AssistantAttentionCallback(DreamOverlayStateController stateController) {
+ mStateController = stateController;
+ }
+
+ @Override
+ public void onConditionsChanged(boolean allConditionsMet) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onConditionChanged:" + allConditionsMet);
+ }
+
+ mStateController.setHasAssistantAttention(allConditionsMet);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java b/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java
new file mode 100644
index 0000000..250cfec
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams.conditions;
+
+import com.android.internal.app.AssistUtils;
+import com.android.internal.app.IVisualQueryDetectionAttentionListener;
+import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.shared.condition.Condition;
+
+import javax.inject.Inject;
+
+/**
+ * {@link AssistantAttentionCondition} provides a signal when assistant has the user's attention.
+ */
+public class AssistantAttentionCondition extends Condition {
+ private final DreamOverlayStateController mDreamOverlayStateController;
+ private final AssistUtils mAssistUtils;
+ private boolean mEnabled;
+
+ private final IVisualQueryDetectionAttentionListener mVisualQueryDetectionAttentionListener =
+ new IVisualQueryDetectionAttentionListener.Stub() {
+ @Override
+ public void onAttentionGained() {
+ updateCondition(true);
+ }
+
+ @Override
+ public void onAttentionLost() {
+ updateCondition(false);
+ }
+ };
+
+ private final DreamOverlayStateController.Callback mCallback =
+ new DreamOverlayStateController.Callback() {
+ @Override
+ public void onStateChanged() {
+ if (mDreamOverlayStateController.isDreamOverlayStatusBarVisible()) {
+ enableVisualQueryDetection();
+ } else {
+ disableVisualQueryDetection();
+ }
+ }
+ };
+
+ @Inject
+ public AssistantAttentionCondition(
+ DreamOverlayStateController dreamOverlayStateController,
+ AssistUtils assistUtils) {
+ mDreamOverlayStateController = dreamOverlayStateController;
+ mAssistUtils = assistUtils;
+ }
+
+ @Override
+ protected void start() {
+ mDreamOverlayStateController.addCallback(mCallback);
+ }
+
+ @Override
+ protected void stop() {
+ disableVisualQueryDetection();
+ mDreamOverlayStateController.removeCallback(mCallback);
+ }
+
+ private void enableVisualQueryDetection() {
+ if (mEnabled) {
+ return;
+ }
+ mEnabled = true;
+ mAssistUtils.enableVisualQueryDetection(mVisualQueryDetectionAttentionListener);
+ }
+
+ private void disableVisualQueryDetection() {
+ if (!mEnabled) {
+ return;
+ }
+ mEnabled = false;
+ mAssistUtils.disableVisualQueryDetection();
+ // Make sure the condition is set to false as well.
+ updateCondition(false);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 7ff27cf..d02eee0 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -85,7 +85,7 @@
// TODO(b/259217907)
@JvmField
val NOTIFICATION_GROUP_DISMISSAL_ANIMATION =
- unreleasedFlag(259217907, "notification_group_dismissal_animation", teamfood = true)
+ releasedFlag(259217907, "notification_group_dismissal_animation")
// TODO(b/257506350): Tracking Bug
@JvmField val FSI_CHROME = unreleasedFlag(117, "fsi_chrome")
@@ -125,8 +125,7 @@
resourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher, "bouncer_user_switcher")
// TODO(b/254512676): Tracking Bug
- @JvmField
- val LOCKSCREEN_CUSTOM_CLOCKS = unreleasedFlag(207, "lockscreen_custom_clocks", teamfood = true)
+ @JvmField val LOCKSCREEN_CUSTOM_CLOCKS = releasedFlag(207, "lockscreen_custom_clocks")
/**
* Whether the clock on a wide lock screen should use the new "stepping" animation for moving
@@ -209,9 +208,7 @@
unreleasedFlag(226, "enable_wallet_contextual_loyalty_cards", teamfood = false)
// TODO(b/242908637): Tracking Bug
- @JvmField
- val WALLPAPER_FULLSCREEN_PREVIEW =
- unreleasedFlag(227, "wallpaper_fullscreen_preview", teamfood = true)
+ @JvmField val WALLPAPER_FULLSCREEN_PREVIEW = releasedFlag(227, "wallpaper_fullscreen_preview")
/** Whether the long-press gesture to open wallpaper picker is enabled. */
// TODO(b/266242192): Tracking Bug
@@ -235,7 +232,7 @@
// TODO(b/258517050): Clean up after the feature is launched.
@JvmField
val SMARTSPACE_DATE_WEATHER_DECOUPLED =
- sysPropBooleanFlag(403, "persist.sysui.ss.dw_decoupled", default = false)
+ sysPropBooleanFlag(403, "persist.sysui.ss.dw_decoupled", default = true)
// 500 - quick settings
@@ -266,11 +263,10 @@
// 600- status bar
// TODO(b/256614753): Tracking Bug
- val NEW_STATUS_BAR_MOBILE_ICONS =
- unreleasedFlag(606, "new_status_bar_mobile_icons", teamfood = true)
+ val NEW_STATUS_BAR_MOBILE_ICONS = releasedFlag(606, "new_status_bar_mobile_icons")
// TODO(b/256614210): Tracking Bug
- val NEW_STATUS_BAR_WIFI_ICON = unreleasedFlag(607, "new_status_bar_wifi_icon", teamfood = true)
+ val NEW_STATUS_BAR_WIFI_ICON = releasedFlag(607, "new_status_bar_wifi_icon")
// TODO(b/256614751): Tracking Bug
val NEW_STATUS_BAR_MOBILE_ICONS_BACKEND =
@@ -311,9 +307,7 @@
val SCREEN_CONTENTS_TRANSLATION = unreleasedFlag(803, "screen_contents_translation")
// 804 - monochromatic themes
- @JvmField
- val MONOCHROMATIC_THEMES =
- sysPropBooleanFlag(804, "persist.sysui.monochromatic", default = false)
+ @JvmField val MONOCHROMATIC_THEME = releasedFlag(804, "monochromatic")
// 900 - media
// TODO(b/254512697): Tracking Bug
@@ -335,30 +329,35 @@
@JvmField val DREAM_MEDIA_TAP_TO_OPEN = unreleasedFlag(906, "dream_media_tap_to_open")
// TODO(b/254513168): Tracking Bug
- @JvmField val UMO_SURFACE_RIPPLE = unreleasedFlag(907, "umo_surface_ripple")
+ @JvmField val UMO_SURFACE_RIPPLE = releasedFlag(907, "umo_surface_ripple")
@JvmField val MEDIA_FALSING_PENALTY = releasedFlag(908, "media_falsing_media")
// TODO(b/261734857): Tracking Bug
- @JvmField val UMO_TURBULENCE_NOISE = unreleasedFlag(909, "umo_turbulence_noise")
+ @JvmField val UMO_TURBULENCE_NOISE = releasedFlag(909, "umo_turbulence_noise")
// TODO(b/263272731): Tracking Bug
- val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE =
- unreleasedFlag(910, "media_ttt_receiver_success_ripple", teamfood = true)
+ val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE = releasedFlag(910, "media_ttt_receiver_success_ripple")
// TODO(b/263512203): Tracking Bug
- val MEDIA_EXPLICIT_INDICATOR = unreleasedFlag(911, "media_explicit_indicator", teamfood = true)
+ val MEDIA_EXPLICIT_INDICATOR = releasedFlag(911, "media_explicit_indicator")
// TODO(b/265813373): Tracking Bug
val MEDIA_TAP_TO_TRANSFER_DISMISS_GESTURE =
unreleasedFlag(912, "media_ttt_dismiss_gesture", teamfood = true)
// TODO(b/266157412): Tracking Bug
- val MEDIA_RETAIN_SESSIONS = unreleasedFlag(913, "media_retain_sessions")
+ val MEDIA_RETAIN_SESSIONS = releasedFlag(913, "media_retain_sessions")
// TODO(b/266739309): Tracking Bug
@JvmField
- val MEDIA_RECOMMENDATION_CARD_UPDATE = unreleasedFlag(914, "media_recommendation_card_update")
+ val MEDIA_RECOMMENDATION_CARD_UPDATE = releasedFlag(914, "media_recommendation_card_update")
+
+ // TODO(b/267007629): Tracking Bug
+ val MEDIA_RESUME_PROGRESS = releasedFlag(915, "media_resume_progress")
+
+ // TODO(b/267166152) : Tracking Bug
+ val MEDIA_RETAIN_RECOMMENDATIONS = releasedFlag(916, "media_retain_recommendations")
// 1000 - dock
val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")
@@ -471,7 +470,7 @@
// TODO(b/254512728): Tracking Bug
@JvmField
- val NEW_BACK_AFFORDANCE = unreleasedFlag(1203, "new_back_affordance", teamfood = false)
+ val NEW_BACK_AFFORDANCE = releasedFlag(1203, "new_back_affordance")
// TODO(b/255854141): Tracking Bug
@JvmField
@@ -490,7 +489,7 @@
// TODO(b/238475428): Tracking Bug
@JvmField
val WM_SHADE_ALLOW_BACK_GESTURE =
- unreleasedFlag(1207, "persist.wm.debug.shade_allow_back_gesture", teamfood = false)
+ sysPropBooleanFlag(1207, "persist.wm.debug.shade_allow_back_gesture", default = false)
// TODO(b/238475428): Tracking Bug
@JvmField
@@ -505,15 +504,17 @@
// 1300 - screenshots
// TODO(b/254513155): Tracking Bug
@JvmField
- val SCREENSHOT_WORK_PROFILE_POLICY =
- unreleasedFlag(1301, "screenshot_work_profile_policy", teamfood = true)
+ val SCREENSHOT_WORK_PROFILE_POLICY = releasedFlag(1301, "screenshot_work_profile_policy")
// TODO(b/264916608): Tracking Bug
- @JvmField val SCREENSHOT_METADATA = unreleasedFlag(1302, "screenshot_metadata")
+ @JvmField val SCREENSHOT_METADATA = unreleasedFlag(1302, "screenshot_metadata", teamfood = true)
// TODO(b/266955521): Tracking bug
@JvmField val SCREENSHOT_DETECTION = unreleasedFlag(1303, "screenshot_detection")
+ // TODO(b/251205791): Tracking Bug
+ @JvmField val SCREENSHOT_APP_CLIPS = releasedFlag(1304, "screenshot_app_clips")
+
// 1400 - columbus
// TODO(b/254512756): Tracking Bug
val QUICK_TAP_IN_PCC = releasedFlag(1400, "quick_tap_in_pcc")
@@ -527,22 +528,25 @@
val CHOOSER_UNBUNDLED = unreleasedFlag(1500, "chooser_unbundled", teamfood = true)
// TODO(b/266983432) Tracking Bug
- val SHARESHEET_CUSTOM_ACTIONS = unreleasedFlag(1501, "sharesheet_custom_actions")
+ val SHARESHEET_CUSTOM_ACTIONS =
+ unreleasedFlag(1501, "sharesheet_custom_actions", teamfood = true)
// TODO(b/266982749) Tracking Bug
- val SHARESHEET_RESELECTION_ACTION = unreleasedFlag(1502, "sharesheet_reselection_action")
+ val SHARESHEET_RESELECTION_ACTION =
+ unreleasedFlag(1502, "sharesheet_reselection_action", teamfood = true)
// TODO(b/266983474) Tracking Bug
- val SHARESHEET_IMAGE_AND_TEXT_PREVIEW = unreleasedFlag(1503, "sharesheet_image_text_preview")
+ val SHARESHEET_IMAGE_AND_TEXT_PREVIEW =
+ unreleasedFlag(1503, "sharesheet_image_text_preview", teamfood = true)
- // 1600 - accessibility
- // TODO(b/262224538): Tracking Bug
- @JvmField
- val A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS =
- releasedFlag(1600, "a11y_floating_menu_fling_spring_animations")
+ // TODO(b/267355521) Tracking Bug
+ val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW =
+ unreleasedFlag(1504, "sharesheet_scrollable_image_preview")
// 1700 - clipboard
@JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior")
+ // TODO(b/267162944): Tracking bug
+ @JvmField val CLIPBOARD_MINIMIZED_LAYOUT = unreleasedFlag(1702, "clipboard_data_model")
// 1800 - shade container
@JvmField
@@ -551,7 +555,7 @@
@JvmField val DUAL_SHADE = releasedFlag(1801, "dual_shade")
// 1900
- @JvmField val NOTE_TASKS = unreleasedFlag(1900, "keycode_flag")
+ @JvmField val NOTE_TASKS = releasedFlag(1900, "keycode_flag")
// 2000 - device controls
@Keep @JvmField val USE_APP_PANELS = releasedFlag(2000, "use_app_panels", teamfood = true)
@@ -562,22 +566,25 @@
@JvmField
val CONTROLS_MANAGEMENT_NEW_FLOWS =
- unreleasedFlag(2002, "controls_management_new_flows", teamfood = true)
+ releasedFlag(2002, "controls_management_new_flows", teamfood = true)
// 2100 - Falsing Manager
@JvmField val FALSING_FOR_LONG_TAPS = releasedFlag(2100, "falsing_for_long_taps")
// 2200 - udfps
// TODO(b/259264861): Tracking Bug
- @JvmField val UDFPS_NEW_TOUCH_DETECTION = unreleasedFlag(2200, "udfps_new_touch_detection")
- @JvmField val UDFPS_ELLIPSE_DEBUG_UI = unreleasedFlag(2201, "udfps_ellipse_debug")
- @JvmField val UDFPS_ELLIPSE_DETECTION = unreleasedFlag(2202, "udfps_ellipse_detection")
+ @JvmField val UDFPS_NEW_TOUCH_DETECTION = releasedFlag(2200, "udfps_new_touch_detection")
+ @JvmField val UDFPS_ELLIPSE_DETECTION = releasedFlag(2201, "udfps_ellipse_detection")
// 2300 - stylus
- @JvmField val TRACK_STYLUS_EVER_USED = unreleasedFlag(2300, "track_stylus_ever_used")
- @JvmField val ENABLE_STYLUS_CHARGING_UI = unreleasedFlag(2301, "enable_stylus_charging_ui")
@JvmField
- val ENABLE_USI_BATTERY_NOTIFICATIONS = unreleasedFlag(2302, "enable_usi_battery_notifications")
+ val TRACK_STYLUS_EVER_USED = unreleasedFlag(2300, "track_stylus_ever_used", teamfood = true)
+ @JvmField
+ val ENABLE_STYLUS_CHARGING_UI =
+ unreleasedFlag(2301, "enable_stylus_charging_ui", teamfood = true)
+ @JvmField
+ val ENABLE_USI_BATTERY_NOTIFICATIONS =
+ unreleasedFlag(2302, "enable_usi_battery_notifications", teamfood = true)
@JvmField val ENABLE_STYLUS_EDUCATION = unreleasedFlag(2303, "enable_stylus_education")
// 2400 - performance tools and debugging info
@@ -589,12 +596,11 @@
// 2500 - output switcher
// TODO(b/261538825): Tracking Bug
@JvmField
- val OUTPUT_SWITCHER_ADVANCED_LAYOUT = unreleasedFlag(2500, "output_switcher_advanced_layout")
+ val OUTPUT_SWITCHER_ADVANCED_LAYOUT = releasedFlag(2500, "output_switcher_advanced_layout")
@JvmField
- val OUTPUT_SWITCHER_ROUTES_PROCESSING =
- unreleasedFlag(2501, "output_switcher_routes_processing")
+ val OUTPUT_SWITCHER_ROUTES_PROCESSING = releasedFlag(2501, "output_switcher_routes_processing")
@JvmField
- val OUTPUT_SWITCHER_DEVICE_STATUS = unreleasedFlag(2502, "output_switcher_device_status")
+ val OUTPUT_SWITCHER_DEVICE_STATUS = releasedFlag(2502, "output_switcher_device_status")
// TODO(b/20911786): Tracking Bug
@JvmField
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt
index d085db9..da91572 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt
@@ -27,16 +27,24 @@
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.util.RingerModeTracker
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import javax.inject.Inject
@SysUISingleton
@@ -46,6 +54,9 @@
private val userFileManager: UserFileManager,
private val ringerModeTracker: RingerModeTracker,
private val audioManager: AudioManager,
+ @Application private val coroutineScope: CoroutineScope,
+ @Main private val mainDispatcher: CoroutineDispatcher,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
) : KeyguardQuickAffordanceConfig {
private var previousNonSilentMode: Int = DEFAULT_LAST_NON_SILENT_VALUE
@@ -58,7 +69,7 @@
override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
ringerModeTracker.ringerModeInternal.asFlow()
- .onStart { emit(getLastNonSilentRingerMode()) }
+ .onStart { getLastNonSilentRingerMode() }
.distinctUntilChanged()
.onEach { mode ->
// only remember last non-SILENT ringer mode
@@ -87,54 +98,60 @@
activationState,
)
}
+ .flowOn(backgroundDispatcher)
override fun onTriggered(
expandable: Expandable?
): KeyguardQuickAffordanceConfig.OnTriggeredResult {
- val newRingerMode: Int
- val currentRingerMode =
- ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE
- if (currentRingerMode == AudioManager.RINGER_MODE_SILENT) {
- newRingerMode = previousNonSilentMode
- } else {
- previousNonSilentMode = currentRingerMode
- newRingerMode = AudioManager.RINGER_MODE_SILENT
- }
+ coroutineScope.launch(backgroundDispatcher) {
+ val newRingerMode: Int
+ val currentRingerMode = audioManager.ringerModeInternal
+ if (currentRingerMode == AudioManager.RINGER_MODE_SILENT) {
+ newRingerMode = previousNonSilentMode
+ } else {
+ previousNonSilentMode = currentRingerMode
+ newRingerMode = AudioManager.RINGER_MODE_SILENT
+ }
- if (currentRingerMode != newRingerMode) {
- audioManager.ringerModeInternal = newRingerMode
+ if (currentRingerMode != newRingerMode) {
+ audioManager.ringerModeInternal = newRingerMode
+ }
}
return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
}
override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState =
- if (audioManager.isVolumeFixed) {
- KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
- } else {
- KeyguardQuickAffordanceConfig.PickerScreenState.Default()
+ withContext(backgroundDispatcher) {
+ if (audioManager.isVolumeFixed) {
+ KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
+ } else {
+ KeyguardQuickAffordanceConfig.PickerScreenState.Default()
+ }
}
/**
* Gets the last non-silent ringer mode from shared-preferences if it exists. This is
* cached by [MuteQuickAffordanceCoreStartable] while this affordance is selected
*/
- private fun getLastNonSilentRingerMode(): Int =
- userFileManager.getSharedPreferences(
- MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME,
- Context.MODE_PRIVATE,
- userTracker.userId
- ).getInt(
- LAST_NON_SILENT_RINGER_MODE_KEY,
- ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE
- )
+ private suspend fun getLastNonSilentRingerMode(): Int =
+ withContext(backgroundDispatcher) {
+ userFileManager.getSharedPreferences(
+ MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME,
+ Context.MODE_PRIVATE,
+ userTracker.userId
+ ).getInt(
+ LAST_NON_SILENT_RINGER_MODE_KEY,
+ ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE
+ )
+ }
private fun <T> LiveData<T>.asFlow(): Flow<T?> =
- conflatedCallbackFlow {
- val observer = Observer { value: T -> trySend(value) }
- observeForever(observer)
- send(value)
- awaitClose { removeObserver(observer) }
- }
+ conflatedCallbackFlow {
+ val observer = Observer { value: T -> trySend(value) }
+ observeForever(observer)
+ send(value)
+ awaitClose { removeObserver(observer) }
+ }.flowOn(mainDispatcher)
companion object {
const val LAST_NON_SILENT_RINGER_MODE_KEY = "key_last_non_silent_ringer_mode"
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt
index 12a6310..cd0805e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt
@@ -23,15 +23,18 @@
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.util.RingerModeTracker
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
import javax.inject.Inject
/**
@@ -45,6 +48,7 @@
private val userFileManager: UserFileManager,
private val keyguardQuickAffordanceRepository: KeyguardQuickAffordanceRepository,
@Application private val coroutineScope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
) : CoreStartable {
private val observer = Observer(this::updateLastNonSilentRingerMode)
@@ -72,15 +76,17 @@
}
private fun updateLastNonSilentRingerMode(lastRingerMode: Int) {
- if (AudioManager.RINGER_MODE_SILENT != lastRingerMode) {
- userFileManager.getSharedPreferences(
- MuteQuickAffordanceConfig.MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME,
- Context.MODE_PRIVATE,
- userTracker.userId
- )
- .edit()
- .putInt(MuteQuickAffordanceConfig.LAST_NON_SILENT_RINGER_MODE_KEY, lastRingerMode)
- .apply()
+ coroutineScope.launch(backgroundDispatcher) {
+ if (AudioManager.RINGER_MODE_SILENT != lastRingerMode) {
+ userFileManager.getSharedPreferences(
+ MuteQuickAffordanceConfig.MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME,
+ Context.MODE_PRIVATE,
+ userTracker.userId
+ )
+ .edit()
+ .putInt(MuteQuickAffordanceConfig.LAST_NON_SILENT_RINGER_MODE_KEY, lastRingerMode)
+ .apply()
+ }
}
}
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricRepository.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
index de15890..0af596a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
@@ -33,7 +33,6 @@
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.user.data.repository.UserRepository
import javax.inject.Inject
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
@@ -48,12 +47,13 @@
import kotlinx.coroutines.flow.transformLatest
/**
- * Acts as source of truth for biometric features.
+ * Acts as source of truth for biometric authentication related settings like enrollments, device
+ * policy, etc.
*
* Abstracts-away data sources and their schemas so the rest of the app doesn't need to worry about
* upstream changes.
*/
-interface BiometricRepository {
+interface BiometricSettingsRepository {
/** Whether any fingerprints are enrolled for the current user. */
val isFingerprintEnrolled: StateFlow<Boolean>
@@ -68,9 +68,8 @@
val isFingerprintEnabledByDevicePolicy: StateFlow<Boolean>
}
-@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
-class BiometricRepositoryImpl
+class BiometricSettingsRepositoryImpl
@Inject
constructor(
context: Context,
@@ -82,7 +81,7 @@
@Application scope: CoroutineScope,
@Background backgroundDispatcher: CoroutineDispatcher,
@Main looper: Looper,
-) : BiometricRepository {
+) : BiometricSettingsRepository {
/** UserId of the current selected user. */
private val selectedUserId: Flow<Int> =
@@ -90,7 +89,7 @@
override val isFingerprintEnrolled: StateFlow<Boolean> =
selectedUserId
- .flatMapLatest { userId ->
+ .flatMapLatest {
conflatedCallbackFlow {
val callback =
object : AuthController.Callback {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
index 2cbc2bb..4a262f5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
@@ -31,7 +31,10 @@
@Binds
fun lightRevealScrimRepository(impl: LightRevealScrimRepositoryImpl): LightRevealScrimRepository
- @Binds fun biometricRepository(impl: BiometricRepositoryImpl): BiometricRepository
+ @Binds
+ fun biometricSettingsRepository(
+ impl: BiometricSettingsRepositoryImpl
+ ): BiometricSettingsRepository
@Binds
fun deviceEntryFingerprintAuthRepository(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt
index 6020ef8..6452e0e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt
@@ -20,7 +20,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.data.repository.BiometricRepository
+import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.LegacyAlternateBouncer
@@ -34,7 +34,7 @@
@Inject
constructor(
private val bouncerRepository: KeyguardBouncerRepository,
- private val biometricRepository: BiometricRepository,
+ private val biometricSettingsRepository: BiometricSettingsRepository,
private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
private val systemClock: SystemClock,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
@@ -99,9 +99,9 @@
fun canShowAlternateBouncerForFingerprint(): Boolean {
return if (isModernAlternateBouncerEnabled) {
bouncerRepository.isAlternateBouncerUIAvailable.value &&
- biometricRepository.isFingerprintEnrolled.value &&
- biometricRepository.isStrongBiometricAllowed.value &&
- biometricRepository.isFingerprintEnabledByDevicePolicy.value &&
+ biometricSettingsRepository.isFingerprintEnrolled.value &&
+ biometricSettingsRepository.isStrongBiometricAllowed.value &&
+ biometricSettingsRepository.isFingerprintEnabledByDevicePolicy.value &&
!deviceEntryFingerprintAuthRepository.isLockedOut.value
} else {
legacyAlternateBouncer != null &&
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index ce61f2f..86f65dde 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -21,6 +21,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isWakingOrStartingToWake
@@ -44,6 +45,7 @@
override fun start() {
listenForDozingToLockscreen()
+ listenForDozingToGone()
}
private fun listenForDozingToLockscreen() {
@@ -68,6 +70,28 @@
}
}
+ private fun listenForDozingToGone() {
+ scope.launch {
+ keyguardInteractor.biometricUnlockState
+ .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ .collect { (biometricUnlockState, lastStartedTransition) ->
+ if (
+ lastStartedTransition.to == KeyguardState.DOZING &&
+ isWakeAndUnlock(biometricUnlockState)
+ ) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.DOZING,
+ KeyguardState.GONE,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
return ValueAnimator().apply {
setInterpolator(Interpolators.LINEAR)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index 57c3b31..fb0fb1f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -154,7 +154,11 @@
val slots = repository.get().getSlotPickerRepresentations()
val slot = slots.find { it.id == slotId } ?: return false
val selections =
- repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).toMutableList()
+ repository
+ .get()
+ .getCurrentSelections()
+ .getOrDefault(slotId, emptyList())
+ .toMutableList()
val alreadySelected = selections.remove(affordanceId)
if (!alreadySelected) {
while (selections.size > 0 && selections.size >= slot.maxSelectedAffordances) {
@@ -203,7 +207,11 @@
}
val selections =
- repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).toMutableList()
+ repository
+ .get()
+ .getCurrentSelections()
+ .getOrDefault(slotId, emptyList())
+ .toMutableList()
return if (selections.remove(affordanceId)) {
repository
.get()
@@ -367,6 +375,10 @@
name = Contract.FlagsTable.FLAG_NAME_WALLPAPER_FULLSCREEN_PREVIEW,
value = featureFlags.isEnabled(Flags.WALLPAPER_FULLSCREEN_PREVIEW),
),
+ KeyguardPickerFlag(
+ name = Contract.FlagsTable.FLAG_NAME_MONOCHROMATIC_THEME,
+ value = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEME)
+ )
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index ad6dbea..53c80f6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -19,7 +19,6 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER
@@ -31,9 +30,6 @@
import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
import com.android.systemui.keyguard.shared.model.TransitionStep
import javax.inject.Inject
-import kotlin.math.max
-import kotlin.math.min
-import kotlin.time.Duration
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
@@ -104,38 +100,4 @@
/* The last completed [KeyguardState] transition */
val finishedKeyguardState: Flow<KeyguardState> =
finishedKeyguardTransitionStep.map { step -> step.to }
-
- /**
- * Transitions will occur over a [totalDuration] with [TransitionStep]s being emitted in the
- * range of [0, 1]. View animations should begin and end within a subset of this range. This
- * function maps the [startTime] and [duration] into [0, 1], when this subset is valid.
- */
- fun transitionStepAnimation(
- flow: Flow<TransitionStep>,
- params: AnimationParams,
- totalDuration: Duration,
- ): Flow<Float> {
- val start = (params.startTime / totalDuration).toFloat()
- val chunks = (totalDuration / params.duration).toFloat()
- var isRunning = false
- return flow
- .map { step ->
- val value = (step.value - start) * chunks
- if (step.transitionState == STARTED) {
- // When starting, make sure to always emit. If a transition is started from the
- // middle, it is possible this animation is being skipped but we need to inform
- // the ViewModels of the last update
- isRunning = true
- max(0f, min(1f, value))
- } else if (isRunning && value >= 1f) {
- // Always send a final value of 1. Because of rounding, [value] may never be
- // exactly 1.
- isRunning = false
- 1f
- } else {
- value
- }
- }
- .filter { value -> value >= 0f && value <= 1f }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
deleted file mode 100644
index 67733e9..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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.systemui.keyguard.shared.model
-
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
-
-/** Animation parameters */
-data class AnimationParams(
- val startTime: Duration = 0.milliseconds,
- val duration: Duration,
-)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
new file mode 100644
index 0000000..ca1e27c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.ui
+
+import android.view.animation.Interpolator
+import com.android.systemui.animation.Interpolators.LINEAR
+import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
+import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
+import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+
+/**
+ * For the given transition params, construct a flow using [createFlow] for the specified portion of
+ * the overall transition.
+ */
+class KeyguardTransitionAnimationFlow(
+ private val transitionDuration: Duration,
+ private val transitionFlow: Flow<TransitionStep>,
+) {
+ /**
+ * Transitions will occur over a [transitionDuration] with [TransitionStep]s being emitted in
+ * the range of [0, 1]. View animations should begin and end within a subset of this range. This
+ * function maps the [startTime] and [duration] into [0, 1], when this subset is valid.
+ */
+ fun createFlow(
+ duration: Duration,
+ onStep: (Float) -> Float,
+ startTime: Duration = 0.milliseconds,
+ onCancel: (() -> Float)? = null,
+ onFinish: (() -> Float)? = null,
+ interpolator: Interpolator = LINEAR,
+ ): Flow<Float> {
+ if (!duration.isPositive()) {
+ throw IllegalArgumentException("duration must be a positive number: $duration")
+ }
+ if ((startTime + duration).compareTo(transitionDuration) > 0) {
+ throw IllegalArgumentException(
+ "startTime($startTime) + duration($duration) must be" +
+ " <= transitionDuration($transitionDuration)"
+ )
+ }
+
+ val start = (startTime / transitionDuration).toFloat()
+ val chunks = (transitionDuration / duration).toFloat()
+ var isComplete = true
+
+ fun stepToValue(step: TransitionStep): Float? {
+ val value = (step.value - start) * chunks
+ return when (step.transitionState) {
+ // When starting, make sure to always emit. If a transition is started from the
+ // middle, it is possible this animation is being skipped but we need to inform
+ // the ViewModels of the last update
+ STARTED -> {
+ isComplete = false
+ max(0f, min(1f, value))
+ }
+ // Always send a final value of 1. Because of rounding, [value] may never be
+ // exactly 1.
+ RUNNING ->
+ if (isComplete) {
+ null
+ } else if (value >= 1f) {
+ isComplete = true
+ 1f
+ } else if (value >= 0f) {
+ value
+ } else {
+ null
+ }
+ else -> null
+ }?.let { onStep(interpolator.getInterpolation(it)) }
+ }
+
+ return transitionFlow
+ .map { step ->
+ when (step.transitionState) {
+ STARTED -> stepToValue(step)
+ RUNNING -> stepToValue(step)
+ CANCELED -> onCancel?.invoke()
+ FINISHED -> onFinish?.invoke()
+ }
+ }
+ .filterNotNull()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
index 3319f9d..ab009f4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -381,82 +381,87 @@
return when (event?.actionMasked) {
MotionEvent.ACTION_DOWN ->
if (viewModel.configKey != null) {
- longPressAnimator =
- view
- .animate()
- .scaleX(PRESSED_SCALE)
- .scaleY(PRESSED_SCALE)
- .setDuration(longPressDurationMs)
- .withEndAction {
- view.setOnClickListener {
- vibratorHelper?.vibrate(
- if (viewModel.isActivated) {
- Vibrations.Activated
- } else {
- Vibrations.Deactivated
- }
- )
- viewModel.onClicked(
- KeyguardQuickAffordanceViewModel.OnClickedParameters(
- configKey = viewModel.configKey,
- expandable = Expandable.fromView(view),
- )
- )
+ if (isUsingAccurateTool(event)) {
+ // For accurate tool types (stylus, mouse, etc.), we don't require a
+ // long-press.
+ } else {
+ // When not using a stylus, we require a long-press to activate the
+ // quick affordance, mostly to do "falsing" (e.g. protect from false
+ // clicks in the pocket/bag).
+ longPressAnimator =
+ view
+ .animate()
+ .scaleX(PRESSED_SCALE)
+ .scaleY(PRESSED_SCALE)
+ .setDuration(longPressDurationMs)
+ .withEndAction {
+ dispatchClick(viewModel.configKey)
+ cancel()
}
- view.performClick()
- view.setOnClickListener(null)
- cancel()
- }
+ }
true
} else {
false
}
MotionEvent.ACTION_MOVE -> {
- if (event.historySize > 0) {
- val distance =
- sqrt(
- (event.y - event.getHistoricalY(0)).pow(2) +
- (event.x - event.getHistoricalX(0)).pow(2)
- )
- if (distance > ViewConfiguration.getTouchSlop()) {
+ if (!isUsingAccurateTool(event)) {
+ // Moving too far while performing a long-press gesture cancels that
+ // gesture.
+ val distanceMoved = distanceMoved(event)
+ if (distanceMoved > ViewConfiguration.getTouchSlop()) {
cancel()
}
}
true
}
MotionEvent.ACTION_UP -> {
- cancel(
- onAnimationEnd =
- if (event.eventTime - event.downTime < longPressDurationMs) {
- Runnable {
- messageDisplayer.invoke(
- R.string.keyguard_affordance_press_too_short
- )
- val amplitude =
- view.context.resources
- .getDimensionPixelSize(
- R.dimen.keyguard_affordance_shake_amplitude
- )
- .toFloat()
- val shakeAnimator =
- ObjectAnimator.ofFloat(
- view,
- "translationX",
- -amplitude / 2,
- amplitude / 2,
+ if (isUsingAccurateTool(event)) {
+ // When using an accurate tool type (stylus, mouse, etc.), we don't require
+ // a long-press gesture to activate the quick affordance. Therefore, lifting
+ // the pointer performs a click.
+ if (
+ viewModel.configKey != null &&
+ distanceMoved(event) <= ViewConfiguration.getTouchSlop()
+ ) {
+ dispatchClick(viewModel.configKey)
+ }
+ } else {
+ // When not using a stylus, lifting the finger/pointer will actually cancel
+ // the long-press gesture. Calling cancel after the quick affordance was
+ // already long-press activated is a no-op, so it's safe to call from here.
+ cancel(
+ onAnimationEnd =
+ if (event.eventTime - event.downTime < longPressDurationMs) {
+ Runnable {
+ messageDisplayer.invoke(
+ R.string.keyguard_affordance_press_too_short
)
- shakeAnimator.duration =
- ShakeAnimationDuration.inWholeMilliseconds
- shakeAnimator.interpolator =
- CycleInterpolator(ShakeAnimationCycles)
- shakeAnimator.start()
+ val amplitude =
+ view.context.resources
+ .getDimensionPixelSize(
+ R.dimen.keyguard_affordance_shake_amplitude
+ )
+ .toFloat()
+ val shakeAnimator =
+ ObjectAnimator.ofFloat(
+ view,
+ "translationX",
+ -amplitude / 2,
+ amplitude / 2,
+ )
+ shakeAnimator.duration =
+ ShakeAnimationDuration.inWholeMilliseconds
+ shakeAnimator.interpolator =
+ CycleInterpolator(ShakeAnimationCycles)
+ shakeAnimator.start()
- vibratorHelper?.vibrate(Vibrations.Shake)
+ vibratorHelper?.vibrate(Vibrations.Shake)
+ }
+ } else {
+ null
}
- } else {
- null
- }
- )
+ )
+ }
true
}
MotionEvent.ACTION_CANCEL -> {
@@ -467,6 +472,28 @@
}
}
+ private fun dispatchClick(
+ configKey: String,
+ ) {
+ view.setOnClickListener {
+ vibratorHelper?.vibrate(
+ if (viewModel.isActivated) {
+ Vibrations.Activated
+ } else {
+ Vibrations.Deactivated
+ }
+ )
+ viewModel.onClicked(
+ KeyguardQuickAffordanceViewModel.OnClickedParameters(
+ configKey = configKey,
+ expandable = Expandable.fromView(view),
+ )
+ )
+ }
+ view.performClick()
+ view.setOnClickListener(null)
+ }
+
private fun cancel(onAnimationEnd: Runnable? = null) {
longPressAnimator?.cancel()
longPressAnimator = null
@@ -475,6 +502,40 @@
companion object {
private const val PRESSED_SCALE = 1.5f
+
+ /**
+ * Returns `true` if the tool type at the given pointer index is an accurate tool (like
+ * stylus or mouse), which means we can trust it to not be a false click; `false`
+ * otherwise.
+ */
+ private fun isUsingAccurateTool(
+ event: MotionEvent,
+ pointerIndex: Int = 0,
+ ): Boolean {
+ return when (event.getToolType(pointerIndex)) {
+ MotionEvent.TOOL_TYPE_STYLUS -> true
+ MotionEvent.TOOL_TYPE_MOUSE -> true
+ else -> false
+ }
+ }
+
+ /**
+ * Returns the amount of distance the pointer moved since the historical record at the
+ * [since] index.
+ */
+ private fun distanceMoved(
+ event: MotionEvent,
+ since: Int = 0,
+ ): Float {
+ return if (event.historySize > 0) {
+ sqrt(
+ (event.y - event.getHistoricalY(since)).pow(2) +
+ (event.x - event.getHistoricalX(since)).pow(2)
+ )
+ } else {
+ 0f
+ }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 8808574..adde595 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -177,7 +177,8 @@
val receiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
- clockController.clock?.events?.onTimeTick()
+ clockController.clock?.smallClock?.events?.onTimeTick()
+ clockController.clock?.largeClock?.events?.onTimeTick()
}
}
broadcastDispatcher.registerReceiver(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
index 6627865..8d6545a4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
@@ -21,15 +21,10 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
-import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
-import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
/**
* Breaks down DREAMING->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -41,39 +36,46 @@
constructor(
private val interactor: KeyguardTransitionInteractor,
) {
+ private val transitionAnimation =
+ KeyguardTransitionAnimationFlow(
+ transitionDuration = TO_LOCKSCREEN_DURATION,
+ transitionFlow = interactor.dreamingToLockscreenTransition,
+ )
/** Dream overlay y-translation on exit */
fun dreamOverlayTranslationY(translatePx: Int): Flow<Float> {
- return flowForAnimation(DREAM_OVERLAY_TRANSLATION_Y).map { value ->
- EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx
- }
+ return transitionAnimation.createFlow(
+ duration = 600.milliseconds,
+ onStep = { it * translatePx },
+ interpolator = EMPHASIZED_ACCELERATE,
+ )
}
/** Dream overlay views alpha - fade out */
- val dreamOverlayAlpha: Flow<Float> = flowForAnimation(DREAM_OVERLAY_ALPHA).map { 1f - it }
+ val dreamOverlayAlpha: Flow<Float> =
+ transitionAnimation.createFlow(
+ duration = 250.milliseconds,
+ onStep = { 1f - it },
+ )
/** Lockscreen views y-translation */
fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
- return merge(
- flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
- -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
- },
- // On end, reset the translation to 0
- interactor.dreamingToLockscreenTransition
- .filter { it.transitionState == FINISHED || it.transitionState == CANCELED }
- .map { 0f }
+ return transitionAnimation.createFlow(
+ duration = TO_LOCKSCREEN_DURATION,
+ onStep = { value -> -translatePx + value * translatePx },
+ // Reset on cancel or finish
+ onFinish = { 0f },
+ onCancel = { 0f },
+ interpolator = EMPHASIZED_DECELERATE,
)
}
/** Lockscreen views alpha */
- val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA)
-
- private fun flowForAnimation(params: AnimationParams): Flow<Float> {
- return interactor.transitionStepAnimation(
- interactor.dreamingToLockscreenTransition,
- params,
- totalDuration = TO_LOCKSCREEN_DURATION
+ val lockscreenAlpha: Flow<Float> =
+ transitionAnimation.createFlow(
+ startTime = 233.milliseconds,
+ duration = 250.milliseconds,
+ onStep = { it },
)
- }
companion object {
/* Length of time before ending the dream activity, in order to start unoccluding */
@@ -81,11 +83,5 @@
@JvmField
val LOCKSCREEN_ANIMATION_DURATION_MS =
(TO_LOCKSCREEN_DURATION - DREAM_ANIMATION_DURATION).inWholeMilliseconds
-
- val DREAM_OVERLAY_TRANSLATION_Y = AnimationParams(duration = 600.milliseconds)
- val DREAM_OVERLAY_ALPHA = AnimationParams(duration = 250.milliseconds)
- val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION)
- val LOCKSCREEN_ALPHA =
- AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
index 5a47960..f16827d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
@@ -20,15 +20,10 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
-import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
-import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
/** Breaks down GONE->DREAMING transition into discrete steps for corresponding views to consume. */
@SysUISingleton
@@ -38,32 +33,28 @@
private val interactor: KeyguardTransitionInteractor,
) {
+ private val transitionAnimation =
+ KeyguardTransitionAnimationFlow(
+ transitionDuration = TO_DREAMING_DURATION,
+ transitionFlow = interactor.goneToDreamingTransition,
+ )
+
/** Lockscreen views y-translation */
fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
- return merge(
- flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
- (EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx)
- },
- // On end, reset the translation to 0
- interactor.goneToDreamingTransition
- .filter { it.transitionState == FINISHED || it.transitionState == CANCELED }
- .map { 0f }
+ return transitionAnimation.createFlow(
+ duration = 500.milliseconds,
+ onStep = { it * translatePx },
+ // Reset on cancel or finish
+ onFinish = { 0f },
+ onCancel = { 0f },
+ interpolator = EMPHASIZED_ACCELERATE,
)
}
/** Lockscreen views alpha */
- val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it }
-
- private fun flowForAnimation(params: AnimationParams): Flow<Float> {
- return interactor.transitionStepAnimation(
- interactor.goneToDreamingTransition,
- params,
- totalDuration = TO_DREAMING_DURATION
+ val lockscreenAlpha: Flow<Float> =
+ transitionAnimation.createFlow(
+ duration = 250.milliseconds,
+ onStep = { 1f - it },
)
- }
-
- companion object {
- val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = 500.milliseconds)
- val LOCKSCREEN_ALPHA = AnimationParams(duration = 250.milliseconds)
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
index e05adbd..bc9dc4f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
@@ -20,15 +20,10 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
-import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
-import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
/**
* Breaks down LOCKSCREEN->DREAMING transition into discrete steps for corresponding views to
@@ -40,35 +35,32 @@
constructor(
private val interactor: KeyguardTransitionInteractor,
) {
+ private val transitionAnimation =
+ KeyguardTransitionAnimationFlow(
+ transitionDuration = TO_DREAMING_DURATION,
+ transitionFlow = interactor.lockscreenToDreamingTransition,
+ )
/** Lockscreen views y-translation */
fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
- return merge(
- flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
- (EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx)
- },
- // On end, reset the translation to 0
- interactor.lockscreenToDreamingTransition
- .filter { it.transitionState == FINISHED || it.transitionState == CANCELED }
- .map { 0f }
+ return transitionAnimation.createFlow(
+ duration = 500.milliseconds,
+ onStep = { it * translatePx },
+ // Reset on cancel or finish
+ onFinish = { 0f },
+ onCancel = { 0f },
+ interpolator = EMPHASIZED_ACCELERATE,
)
}
/** Lockscreen views alpha */
- val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it }
-
- private fun flowForAnimation(params: AnimationParams): Flow<Float> {
- return interactor.transitionStepAnimation(
- interactor.lockscreenToDreamingTransition,
- params,
- totalDuration = TO_DREAMING_DURATION
+ val lockscreenAlpha: Flow<Float> =
+ transitionAnimation.createFlow(
+ duration = 250.milliseconds,
+ onStep = { 1f - it },
)
- }
companion object {
@JvmField val DREAMING_ANIMATION_DURATION_MS = TO_DREAMING_DURATION.inWholeMilliseconds
-
- val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = 500.milliseconds)
- val LOCKSCREEN_ALPHA = AnimationParams(duration = 250.milliseconds)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
index 22d292e..a60665a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
@@ -20,14 +20,10 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_OCCLUDED_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
-import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
/**
* Breaks down LOCKSCREEN->OCCLUDED transition into discrete steps for corresponding views to
@@ -39,33 +35,28 @@
constructor(
private val interactor: KeyguardTransitionInteractor,
) {
+ private val transitionAnimation =
+ KeyguardTransitionAnimationFlow(
+ transitionDuration = TO_OCCLUDED_DURATION,
+ transitionFlow = interactor.lockscreenToOccludedTransition,
+ )
+
+ /** Lockscreen views alpha */
+ val lockscreenAlpha: Flow<Float> =
+ transitionAnimation.createFlow(
+ duration = 250.milliseconds,
+ onStep = { 1f - it },
+ )
/** Lockscreen views y-translation */
fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
- return merge(
- flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
- (EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx)
- },
- // On end, reset the translation to 0
- interactor.lockscreenToOccludedTransition
- .filter { step -> step.transitionState == TransitionState.FINISHED }
- .map { 0f }
+ return transitionAnimation.createFlow(
+ duration = TO_OCCLUDED_DURATION,
+ onStep = { value -> value * translatePx },
+ // Reset on cancel or finish
+ onFinish = { 0f },
+ onCancel = { 0f },
+ interpolator = EMPHASIZED_ACCELERATE,
)
}
-
- /** Lockscreen views alpha */
- val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it }
-
- private fun flowForAnimation(params: AnimationParams): Flow<Float> {
- return interactor.transitionStepAnimation(
- interactor.lockscreenToOccludedTransition,
- params,
- totalDuration = TO_OCCLUDED_DURATION
- )
- }
-
- companion object {
- val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_OCCLUDED_DURATION)
- val LOCKSCREEN_ALPHA = AnimationParams(duration = 250.milliseconds)
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
index e804562..5770f3e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
@@ -20,11 +20,10 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
/**
* Breaks down OCCLUDED->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -36,28 +35,26 @@
constructor(
private val interactor: KeyguardTransitionInteractor,
) {
+ private val transitionAnimation =
+ KeyguardTransitionAnimationFlow(
+ transitionDuration = TO_LOCKSCREEN_DURATION,
+ transitionFlow = interactor.occludedToLockscreenTransition,
+ )
+
/** Lockscreen views y-translation */
fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
- return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
- -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
- }
- }
-
- /** Lockscreen views alpha */
- val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA)
-
- private fun flowForAnimation(params: AnimationParams): Flow<Float> {
- return interactor.transitionStepAnimation(
- interactor.occludedToLockscreenTransition,
- params,
- totalDuration = TO_LOCKSCREEN_DURATION
+ return transitionAnimation.createFlow(
+ duration = TO_LOCKSCREEN_DURATION,
+ onStep = { value -> -translatePx + value * translatePx },
+ interpolator = EMPHASIZED_DECELERATE,
)
}
- companion object {
- @JvmField val LOCKSCREEN_ANIMATION_DURATION_MS = TO_LOCKSCREEN_DURATION.inWholeMilliseconds
- val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION)
- val LOCKSCREEN_ALPHA =
- AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds)
- }
+ /** Lockscreen views alpha */
+ val lockscreenAlpha: Flow<Float> =
+ transitionAnimation.createFlow(
+ startTime = 233.milliseconds,
+ duration = 250.milliseconds,
+ onStep = { it },
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt b/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt
new file mode 100644
index 0000000..5acaa46
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.log
+
+import android.graphics.Point
+import android.graphics.Rect
+import android.graphics.RectF
+import androidx.core.graphics.toRectF
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.ScreenDecorationsLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import javax.inject.Inject
+
+private const val TAG = "ScreenDecorationsLog"
+
+/**
+ * Helper class for logging for [com.android.systemui.ScreenDecorations]
+ *
+ * To enable logcat echoing for an entire buffer:
+ *
+ * ```
+ * adb shell settings put global systemui/buffer/ScreenDecorationsLog <logLevel>
+ *
+ * ```
+ */
+@SysUISingleton
+class ScreenDecorationsLogger
+@Inject
+constructor(
+ @ScreenDecorationsLog private val logBuffer: LogBuffer,
+) {
+ fun cameraProtectionBoundsForScanningOverlay(bounds: Rect) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ { str1 = bounds.toShortString() },
+ { "Face scanning overlay present camera protection bounds: $str1" }
+ )
+ }
+
+ fun hwcLayerCameraProtectionBounds(bounds: Rect) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ { str1 = bounds.toShortString() },
+ { "Hwc layer present camera protection bounds: $str1" }
+ )
+ }
+
+ fun dcvCameraBounds(id: Int, bounds: Rect) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = bounds.toShortString()
+ int1 = id
+ },
+ { "DisplayCutoutView id=$int1 present, camera protection bounds: $str1" }
+ )
+ }
+
+ fun cutoutViewNotInitialized() {
+ logBuffer.log(TAG, ERROR, "CutoutView not initialized showCameraProtection")
+ }
+
+ fun boundingRect(boundingRectangle: RectF, context: String) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = context
+ str2 = boundingRectangle.toShortString()
+ },
+ { "Bounding rect $str1 : $str2" }
+ )
+ }
+
+ fun boundingRect(boundingRectangle: Rect, context: String) {
+ boundingRect(boundingRectangle.toRectF(), context)
+ }
+
+ fun onMeasureDimensions(
+ widthMeasureSpec: Int,
+ heightMeasureSpec: Int,
+ measuredWidth: Int,
+ measuredHeight: Int
+ ) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ {
+ long1 = widthMeasureSpec.toLong()
+ long2 = heightMeasureSpec.toLong()
+ int1 = measuredWidth
+ int2 = measuredHeight
+ },
+ {
+ "Face scanning animation: widthMeasureSpec: $long1 measuredWidth: $int1, " +
+ "heightMeasureSpec: $long2 measuredHeight: $int2"
+ }
+ )
+ }
+
+ fun faceSensorLocation(faceSensorLocation: Point?) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ {
+ int1 = faceSensorLocation?.y?.times(2) ?: 0
+ str1 = "$faceSensorLocation"
+ },
+ { "Reinflating view: Face sensor location: $str1, faceScanningHeight: $int1" }
+ )
+ }
+
+ fun onSensorLocationChanged() {
+ logBuffer.log(TAG, DEBUG, "AuthControllerCallback in ScreenDecorations triggered")
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index f61d135..c02fd9c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -350,6 +350,16 @@
}
/**
+ * Provides a {@link LogBuffer} for use by {@link com.android.systemui.ScreenDecorations}.
+ */
+ @Provides
+ @SysUISingleton
+ @ScreenDecorationsLog
+ public static LogBuffer provideScreenDecorationsLog(LogBufferFactory factory) {
+ return factory.create("ScreenDecorationsLog", 200);
+ }
+
+ /**
* Provides a {@link LogBuffer} for bluetooth-related logs.
*/
@Provides
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/ScreenDecorationsLog.kt
similarity index 63%
copy from packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
copy to packages/SystemUI/src/com/android/systemui/log/dagger/ScreenDecorationsLog.kt
index 497c272..de2a8b6 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ScreenDecorationsLog.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,12 +14,12 @@
* limitations under the License.
*/
-package com.android.credentialmanager.jetpack.developer
+package com.android.systemui.log.dagger
-/**
- * Internal exception used to indicate a parsing error while converting from a framework type to
- * a jetpack type.
- *
- * @hide
- */
-internal class FrameworkClassParsingException : Exception()
\ No newline at end of file
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for ScreenDecorations added by SysUI. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ScreenDecorationsLog
diff --git a/packages/SystemUI/src/com/android/systemui/logcat/LogAccessDialogActivity.java b/packages/SystemUI/src/com/android/systemui/logcat/LogAccessDialogActivity.java
index a88a4ca..4f5bbb7 100644
--- a/packages/SystemUI/src/com/android/systemui/logcat/LogAccessDialogActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/logcat/LogAccessDialogActivity.java
@@ -28,11 +28,10 @@
import android.os.Handler;
import android.os.RemoteException;
import android.os.UserHandle;
-import android.text.Html;
-import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
-import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
import android.util.Slog;
import android.view.ContextThemeWrapper;
@@ -64,7 +63,10 @@
private String mAlertTitle;
private String mAlertBody;
- private String mAlertLearnMore;
+
+ private boolean mAlertLearnMoreLink;
+ private SpannableString mAlertLearnMore;
+
private AlertDialog.Builder mAlertDialog;
private AlertDialog mAlert;
private View mAlertView;
@@ -90,8 +92,20 @@
return;
}
- mAlertBody = getResources().getString(R.string.log_access_confirmation_body);
- mAlertLearnMore = getResources().getString(R.string.log_access_confirmation_learn_more);
+ mAlertBody = getString(R.string.log_access_confirmation_body);
+ mAlertLearnMoreLink = this.getResources()
+ .getBoolean(R.bool.log_access_confirmation_learn_more_as_link);
+ if (mAlertLearnMoreLink) {
+ mAlertLearnMore = new SpannableString(
+ getString(R.string.log_access_confirmation_learn_more));
+ mAlertLearnMore.setSpan(new URLSpan(
+ getString(R.string.log_access_confirmation_learn_more_url)),
+ 0, mAlertLearnMore.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else {
+ mAlertLearnMore = new SpannableString(
+ getString(R.string.log_access_confirmation_learn_more_at,
+ getString(R.string.log_access_confirmation_learn_more_url)));
+ }
// create View
int themeId = R.style.LogAccessDialogTheme;
@@ -180,15 +194,6 @@
return titleString;
}
- private Spannable styleFont(String text) {
- Spannable s = (Spannable) Html.fromHtml(text);
- for (URLSpan span : s.getSpans(0, s.length(), URLSpan.class)) {
- TypefaceSpan typefaceSpan = new TypefaceSpan("google-sans");
- s.setSpan(typefaceSpan, s.getSpanStart(span), s.getSpanEnd(span), 0);
- }
- return s;
- }
-
/**
* Returns the dialog view.
* If we cannot retrieve the package name, it returns null and we decline the full device log
@@ -207,13 +212,12 @@
.setText(mAlertTitle);
if (!TextUtils.isEmpty(mAlertLearnMore)) {
- Spannable mSpannableLearnMore = styleFont(mAlertLearnMore);
-
((TextView) view.findViewById(R.id.log_access_dialog_body))
- .setText(TextUtils.concat(mAlertBody, "\n\n", mSpannableLearnMore));
-
- ((TextView) view.findViewById(R.id.log_access_dialog_body))
+ .setText(TextUtils.concat(mAlertBody, "\n\n", mAlertLearnMore));
+ if (mAlertLearnMoreLink) {
+ ((TextView) view.findViewById(R.id.log_access_dialog_body))
.setMovementMethod(LinkMovementMethod.getInstance());
+ }
} else {
((TextView) view.findViewById(R.id.log_access_dialog_body))
.setText(mAlertBody);
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
index a692ad7..52d4171 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
@@ -28,12 +28,15 @@
import android.os.UserHandle
import android.view.ViewGroup
import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.app.AbstractMultiProfilePagerAdapter.EmptyStateProvider
import com.android.internal.app.AbstractMultiProfilePagerAdapter.MyUserIdProvider
import com.android.internal.app.ChooserActivity
import com.android.internal.app.ResolverListController
import com.android.internal.app.chooser.NotSelectableTargetInfo
import com.android.internal.app.chooser.TargetInfo
import com.android.systemui.R
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorComponent
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorController
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler
@@ -47,6 +50,7 @@
class MediaProjectionAppSelectorActivity(
private val componentFactory: MediaProjectionAppSelectorComponent.Factory,
private val activityLauncher: AsyncActivityLauncher,
+ private val featureFlags: FeatureFlags,
/** This is used to override the dependency in a screenshot test */
@VisibleForTesting
private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?
@@ -56,7 +60,8 @@
constructor(
componentFactory: MediaProjectionAppSelectorComponent.Factory,
activityLauncher: AsyncActivityLauncher,
- ) : this(componentFactory, activityLauncher, null)
+ featureFlags: FeatureFlags
+ ) : this(componentFactory, activityLauncher, featureFlags, listControllerFactory = null)
private lateinit var configurationController: ConfigurationController
private lateinit var controller: MediaProjectionAppSelectorController
@@ -91,6 +96,13 @@
override fun appliedThemeResId(): Int = R.style.Theme_SystemUI_MediaProjectionAppSelector
+ override fun createBlockerEmptyStateProvider(): EmptyStateProvider =
+ if (featureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) {
+ component.emptyStateProvider
+ } else {
+ super.createBlockerEmptyStateProvider()
+ }
+
override fun createListController(userHandle: UserHandle): ResolverListController =
listControllerFactory?.invoke(userHandle) ?: super.createListController(userHandle)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
index d830fc4..c4e76b2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
@@ -45,33 +45,46 @@
import android.util.Log;
import android.view.Window;
-import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog;
import com.android.systemui.screenrecord.MediaProjectionPermissionDialog;
import com.android.systemui.screenrecord.ScreenShareOption;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.util.Utils;
+import javax.inject.Inject;
+
+import dagger.Lazy;
+
public class MediaProjectionPermissionActivity extends Activity
implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
private static final String TAG = "MediaProjectionPermissionActivity";
private static final float MAX_APP_NAME_SIZE_PX = 500f;
private static final String ELLIPSIS = "\u2026";
+ private final FeatureFlags mFeatureFlags;
+ private final Lazy<ScreenCaptureDevicePolicyResolver> mScreenCaptureDevicePolicyResolver;
+
private String mPackageName;
private int mUid;
private IMediaProjectionManager mService;
- private FeatureFlags mFeatureFlags;
private AlertDialog mDialog;
+ @Inject
+ public MediaProjectionPermissionActivity(FeatureFlags featureFlags,
+ Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver) {
+ mFeatureFlags = featureFlags;
+ mScreenCaptureDevicePolicyResolver = screenCaptureDevicePolicyResolver;
+ }
+
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
- mFeatureFlags = Dependency.get(FeatureFlags.class);
mPackageName = getCallingPackage();
IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
mService = IMediaProjectionManager.Stub.asInterface(b);
@@ -104,6 +117,12 @@
return;
}
+ if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) {
+ if (showScreenCaptureDisabledDialogIfNeeded()) {
+ return;
+ }
+ }
+
TextPaint paint = new TextPaint();
paint.setTextSize(42);
@@ -171,16 +190,7 @@
mDialog = dialogBuilder.create();
}
- SystemUIDialog.registerDismissListener(mDialog);
- SystemUIDialog.applyFlags(mDialog);
- SystemUIDialog.setDialogSize(mDialog);
-
- mDialog.setOnCancelListener(this);
- mDialog.create();
- mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true);
-
- final Window w = mDialog.getWindow();
- w.addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+ setUpDialog(mDialog);
mDialog.show();
}
@@ -200,6 +210,32 @@
}
}
+ private void setUpDialog(AlertDialog dialog) {
+ SystemUIDialog.registerDismissListener(dialog);
+ SystemUIDialog.applyFlags(dialog);
+ SystemUIDialog.setDialogSize(dialog);
+
+ dialog.setOnCancelListener(this);
+ dialog.create();
+ dialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true);
+
+ final Window w = dialog.getWindow();
+ w.addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+ }
+
+ private boolean showScreenCaptureDisabledDialogIfNeeded() {
+ final UserHandle hostUserHandle = getHostUserHandle();
+ if (mScreenCaptureDevicePolicyResolver.get()
+ .isScreenCaptureCompletelyDisabled(hostUserHandle)) {
+ AlertDialog dialog = new ScreenCaptureDisabledDialog(this);
+ setUpDialog(dialog);
+ dialog.show();
+ return true;
+ }
+
+ return false;
+ }
+
private void grantMediaProjectionPermission(int screenShareMode) {
try {
if (screenShareMode == ENTIRE_SCREEN) {
@@ -211,7 +247,7 @@
intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION,
projection.asBinder());
intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
- UserHandle.getUserHandleForUid(getLaunchedFromUid()));
+ getHostUserHandle());
intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
// Start activity from the current foreground user to avoid creating a separate
@@ -230,6 +266,10 @@
}
}
+ private UserHandle getHostUserHandle() {
+ return UserHandle.getUserHandleForUid(getLaunchedFromUid());
+ }
+
private IMediaProjection createProjection(int uid, String packageName) throws RemoteException {
return mService.createProjection(uid, packageName,
MediaProjectionManager.TYPE_SCREEN_CAPTURE, false /* permanentGrant */);
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
index be18cbe..b7a2522 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
@@ -92,6 +92,9 @@
/** Whether explicit indicator exists */
val isExplicit: Boolean = false,
+
+ /** Track progress (0 - 1) to display for players where [resumption] is true */
+ val resumeProgress: Double? = null,
) {
companion object {
/** Media is playing on the local device */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
index bba5f35..a057c9f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
@@ -238,6 +238,24 @@
}
/**
+ * Set the progress to a fixed percentage value that cannot be changed by the user.
+ *
+ * @param percent value between 0 and 1
+ */
+ fun updateStaticProgress(percent: Double) {
+ val position = (percent * 100).toInt()
+ _data =
+ Progress(
+ enabled = true,
+ seekAvailable = false,
+ playing = false,
+ scrubbing = false,
+ elapsedTime = position,
+ duration = 100,
+ )
+ }
+
+ /**
* Puts the seek bar into a resumption state.
*
* This should be called when the media session behind the controller has been destroyed.
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
index 8c1ec16..70f2dee 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
@@ -20,6 +20,7 @@
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
+import android.widget.SeekBar
import android.widget.TextView
import com.android.internal.widget.CachingIconView
import com.android.systemui.R
@@ -37,6 +38,7 @@
// Recommendation screen
lateinit var cardIcon: ImageView
lateinit var mediaAppIcons: List<CachingIconView>
+ lateinit var mediaProgressBars: List<SeekBar>
lateinit var cardTitle: TextView
val mediaCoverContainers =
@@ -82,6 +84,13 @@
if (updatedView) {
mediaAppIcons = mediaCoverContainers.map { it.requireViewById(R.id.media_rec_app_icon) }
cardTitle = itemView.requireViewById(R.id.media_rec_title)
+ mediaProgressBars =
+ mediaCoverContainers.map {
+ it.requireViewById<SeekBar?>(R.id.media_progress_bar).apply {
+ // Media playback is in the direction of tape, not time, so it stays LTR
+ layoutDirection = View.LAYOUT_DIRECTION_LTR
+ }
+ }
} else {
cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt
index 1df42c6..dc7a4f1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt
@@ -41,10 +41,12 @@
val recommendations: List<SmartspaceAction>,
/** Intent for the user's initiated dismissal. */
val dismissIntent: Intent?,
- /** The timestamp in milliseconds that headphone is connected. */
+ /** The timestamp in milliseconds that the card was generated */
val headphoneConnectionTimeMillis: Long,
/** Instance ID for [MediaUiEventLogger] */
- val instanceId: InstanceId
+ val instanceId: InstanceId,
+ /** The timestamp in milliseconds indicating when the card should be removed */
+ val expiryTimeMs: Long,
) {
/**
* Indicates if all the data is valid.
@@ -86,5 +88,12 @@
}
}
+/** Key for extras [SmartspaceMediaData.cardAction] indicating why the card was sent */
+const val EXTRA_KEY_TRIGGER_SOURCE = "MEDIA_RECOMMENDATION_TRIGGER_SOURCE"
+/** Value for [EXTRA_KEY_TRIGGER_SOURCE] when the card is sent on headphone connection */
+const val EXTRA_VALUE_TRIGGER_HEADPHONE = "HEADPHONE_CONNECTION"
+/** Value for key [EXTRA_KEY_TRIGGER_SOURCE] when the card is sent as a regular update */
+const val EXTRA_VALUE_TRIGGER_PERIODIC = "PERIODIC_TRIGGER"
+
const val NUM_REQUIRED_RECOMMENDATIONS = 3
private val TAG = SmartspaceMediaData::class.simpleName!!
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
index cf71d67..27f7b97 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
@@ -24,6 +24,7 @@
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.NotificationLockscreenUserManager
@@ -66,7 +67,8 @@
private val lockscreenUserManager: NotificationLockscreenUserManager,
@Main private val executor: Executor,
private val systemClock: SystemClock,
- private val logger: MediaUiEventLogger
+ private val logger: MediaUiEventLogger,
+ private val mediaFlags: MediaFlags,
) : MediaDataManager.Listener {
private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
internal val listeners: Set<MediaDataManager.Listener>
@@ -121,7 +123,9 @@
data: SmartspaceMediaData,
shouldPrioritize: Boolean
) {
- if (!data.isActive) {
+ // With persistent recommendation card, we could get a background update while inactive
+ // Otherwise, consider it an invalid update
+ if (!data.isActive && !mediaFlags.isPersistentSsCardEnabled()) {
Log.d(TAG, "Inactive recommendation data. Skip triggering.")
return
}
@@ -141,7 +145,7 @@
}
}
- val shouldReactivate = !hasActiveMedia() && hasAnyMedia()
+ val shouldReactivate = !hasActiveMedia() && hasAnyMedia() && data.isActive
if (timeSinceActive < smartspaceMaxAgeMillis) {
// It could happen there are existing active media resume cards, then we don't need to
@@ -169,7 +173,7 @@
)
}
}
- } else {
+ } else if (data.isActive) {
// Mark to prioritize Smartspace card if no recent media.
shouldPrioritizeMutable = true
}
@@ -252,7 +256,7 @@
if (dismissIntent == null) {
Log.w(
TAG,
- "Cannot create dismiss action click action: " + "extras missing dismiss_intent."
+ "Cannot create dismiss action click action: extras missing dismiss_intent."
)
} else if (
dismissIntent.getComponent() != null &&
@@ -264,15 +268,21 @@
} else {
broadcastSender.sendBroadcast(dismissIntent)
}
- smartspaceMediaData =
- EMPTY_SMARTSPACE_MEDIA_DATA.copy(
- targetId = smartspaceMediaData.targetId,
- instanceId = smartspaceMediaData.instanceId
+
+ if (mediaFlags.isPersistentSsCardEnabled()) {
+ smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
+ mediaDataManager.setRecommendationInactive(smartspaceMediaData.targetId)
+ } else {
+ smartspaceMediaData =
+ EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+ targetId = smartspaceMediaData.targetId,
+ instanceId = smartspaceMediaData.instanceId,
+ )
+ mediaDataManager.dismissSmartspaceRecommendation(
+ smartspaceMediaData.targetId,
+ delay = 0L,
)
- mediaDataManager.dismissSmartspaceRecommendation(
- smartspaceMediaData.targetId,
- delay = 0L
- )
+ }
}
}
@@ -283,8 +293,15 @@
(smartspaceMediaData.isValid() || reactivatedKey != null))
/** Are there any media entries we should display? */
- fun hasAnyMediaOrRecommendation() =
- userEntries.isNotEmpty() || (smartspaceMediaData.isActive && smartspaceMediaData.isValid())
+ fun hasAnyMediaOrRecommendation(): Boolean {
+ val hasSmartspace =
+ if (mediaFlags.isPersistentSsCardEnabled()) {
+ smartspaceMediaData.isValid()
+ } else {
+ smartspaceMediaData.isActive && smartspaceMediaData.isValid()
+ }
+ return userEntries.isNotEmpty() || hasSmartspace
+ }
/** Are there any media notifications active (excluding the recommendation)? */
fun hasActiveMedia() = userEntries.any { it.value.active }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
index da2164e..0222ea3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
@@ -50,7 +50,6 @@
import android.text.TextUtils
import android.util.Log
import androidx.media.utils.MediaConstants
-import com.android.internal.annotations.VisibleForTesting
import com.android.internal.logging.InstanceId
import com.android.systemui.Dumpable
import com.android.systemui.R
@@ -64,10 +63,13 @@
import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.player.MediaDeviceData
import com.android.systemui.media.controls.models.player.MediaViewHolder
+import com.android.systemui.media.controls.models.recommendation.EXTRA_KEY_TRIGGER_SOURCE
+import com.android.systemui.media.controls.models.recommendation.EXTRA_VALUE_TRIGGER_PERIODIC
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider
import com.android.systemui.media.controls.resume.MediaResumeListener
import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaDataUtils
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.plugins.ActivityStarter
@@ -119,7 +121,6 @@
appUid = Process.INVALID_UID
)
-@VisibleForTesting
internal val EMPTY_SMARTSPACE_MEDIA_DATA =
SmartspaceMediaData(
targetId = "INVALID",
@@ -129,7 +130,8 @@
recommendations = emptyList(),
dismissIntent = null,
headphoneConnectionTimeMillis = 0,
- instanceId = InstanceId.fakeInstanceId(-1)
+ instanceId = InstanceId.fakeInstanceId(-1),
+ expiryTimeMs = 0,
)
fun isMediaNotification(sbn: StatusBarNotification): Boolean {
@@ -548,6 +550,11 @@
if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
onMediaDataLoaded(key, key, it)
}
+
+ if (key == smartspaceMediaData.targetId) {
+ if (DEBUG) Log.d(TAG, "smartspace card expired")
+ dismissSmartspaceRecommendation(key, delay = 0L)
+ }
}
/** Called when the player's [PlaybackState] has been updated with new actions and/or state */
@@ -605,8 +612,8 @@
}
/**
- * Called whenever the recommendation has been expired, or swiped from QQS. This will make the
- * recommendation view to not be shown anymore during this headphone connection session.
+ * Called whenever the recommendation has been expired or removed by the user. This will remove
+ * the recommendation card entirely from the carousel.
*/
fun dismissSmartspaceRecommendation(key: String, delay: Long) {
if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
@@ -628,6 +635,23 @@
)
}
+ /** Called when the recommendation card should no longer be visible in QQS or lockscreen */
+ fun setRecommendationInactive(key: String) {
+ if (!mediaFlags.isPersistentSsCardEnabled()) {
+ Log.e(TAG, "Only persistent recommendation can be inactive!")
+ return
+ }
+ if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive")
+
+ if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
+ // If this doesn't match, or we've already invalidated the data, no action needed
+ return
+ }
+
+ smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
+ notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
+ }
+
private fun loadMediaDataInBgForResumption(
userId: Int,
desc: MediaDescription,
@@ -668,6 +692,11 @@
MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT &&
mediaFlags.isExplicitIndicatorEnabled()
+ val progress =
+ if (mediaFlags.isResumeProgressEnabled()) {
+ MediaDataUtils.getDescriptionProgress(desc.extras)
+ } else null
+
val mediaAction = getResumeMediaAction(resumeAction)
val lastActive = systemClock.elapsedRealtime()
foregroundExecutor.execute {
@@ -698,6 +727,7 @@
instanceId = instanceId,
appUid = appUid,
isExplicit = isExplicit,
+ resumeProgress = progress,
)
)
}
@@ -1261,12 +1291,25 @@
if (DEBUG) {
Log.d(TAG, "Set Smartspace media to be inactive for the data update")
}
- smartspaceMediaData =
- EMPTY_SMARTSPACE_MEDIA_DATA.copy(
- targetId = smartspaceMediaData.targetId,
- instanceId = smartspaceMediaData.instanceId
+ if (mediaFlags.isPersistentSsCardEnabled()) {
+ // Smartspace uses this signal to hide the card (e.g. when it expires or user
+ // disconnects headphones), so treat as setting inactive when flag is on
+ smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
+ notifySmartspaceMediaDataLoaded(
+ smartspaceMediaData.targetId,
+ smartspaceMediaData,
)
- notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false)
+ } else {
+ smartspaceMediaData =
+ EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+ targetId = smartspaceMediaData.targetId,
+ instanceId = smartspaceMediaData.instanceId,
+ )
+ notifySmartspaceMediaDataRemoved(
+ smartspaceMediaData.targetId,
+ immediately = false,
+ )
+ }
}
1 -> {
val newMediaTarget = mediaTargets.get(0)
@@ -1275,7 +1318,7 @@
return
}
if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.")
- smartspaceMediaData = toSmartspaceMediaData(newMediaTarget, isActive = true)
+ smartspaceMediaData = toSmartspaceMediaData(newMediaTarget)
notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
}
else -> {
@@ -1284,7 +1327,7 @@
Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
notifySmartspaceMediaDataRemoved(
smartspaceMediaData.targetId,
- false /* immediately */
+ immediately = false,
)
smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
}
@@ -1490,21 +1533,28 @@
}
/**
- * Converts the pass-in SmartspaceTarget to SmartspaceMediaData with the pass-in active status.
+ * Converts the pass-in SmartspaceTarget to SmartspaceMediaData
*
* @return An empty SmartspaceMediaData with the valid target Id is returned if the
* SmartspaceTarget's data is invalid.
*/
- private fun toSmartspaceMediaData(
- target: SmartspaceTarget,
- isActive: Boolean
- ): SmartspaceMediaData {
+ private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData {
var dismissIntent: Intent? = null
if (target.baseAction != null && target.baseAction.extras != null) {
dismissIntent =
target.baseAction.extras.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY)
as Intent?
}
+
+ val isActive =
+ when {
+ !mediaFlags.isPersistentSsCardEnabled() -> true
+ target.baseAction == null -> true
+ else ->
+ target.baseAction.extras.getString(EXTRA_KEY_TRIGGER_SOURCE) !=
+ EXTRA_VALUE_TRIGGER_PERIODIC
+ }
+
packageName(target)?.let {
return SmartspaceMediaData(
targetId = target.smartspaceTargetId,
@@ -1514,7 +1564,8 @@
recommendations = target.iconGrid,
dismissIntent = dismissIntent,
headphoneConnectionTimeMillis = target.creationTimeMillis,
- instanceId = logger.getNewInstanceId()
+ instanceId = logger.getNewInstanceId(),
+ expiryTimeMs = target.expiryTimeMillis,
)
}
return EMPTY_SMARTSPACE_MEDIA_DATA.copy(
@@ -1522,7 +1573,8 @@
isActive = isActive,
dismissIntent = dismissIntent,
headphoneConnectionTimeMillis = target.creationTimeMillis,
- instanceId = logger.getNewInstanceId()
+ instanceId = logger.getNewInstanceId(),
+ expiryTimeMs = target.expiryTimeMillis,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt
index a898b00..aa46b14 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt
@@ -23,7 +23,9 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -49,10 +51,12 @@
@Main private val mainExecutor: DelayableExecutor,
private val logger: MediaTimeoutLogger,
statusBarStateController: SysuiStatusBarStateController,
- private val systemClock: SystemClock
+ private val systemClock: SystemClock,
+ private val mediaFlags: MediaFlags,
) : MediaDataManager.Listener {
private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf()
+ private val recommendationListeners: MutableMap<String, RecommendationListener> = mutableMapOf()
/**
* Callback representing that a media object is now expired:
@@ -93,6 +97,16 @@
listener.doTimeout()
}
}
+
+ recommendationListeners.forEach { (key, listener) ->
+ if (
+ listener.cancellation != null &&
+ listener.expiration <= systemClock.currentTimeMillis()
+ ) {
+ logger.logTimeoutCancelled(key, "Timed out while dozing")
+ listener.doTimeout()
+ }
+ }
}
}
}
@@ -155,6 +169,30 @@
mediaListeners.remove(key)?.destroy()
}
+ override fun onSmartspaceMediaDataLoaded(
+ key: String,
+ data: SmartspaceMediaData,
+ shouldPrioritize: Boolean
+ ) {
+ if (!mediaFlags.isPersistentSsCardEnabled()) return
+
+ // First check if we already have a listener
+ recommendationListeners.get(key)?.let {
+ if (!it.destroyed) {
+ it.recommendationData = data
+ return
+ }
+ }
+
+ // Otherwise, create a new one
+ recommendationListeners[key] = RecommendationListener(key, data)
+ }
+
+ override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
+ if (!mediaFlags.isPersistentSsCardEnabled()) return
+ recommendationListeners.remove(key)?.destroy()
+ }
+
fun isTimedOut(key: String): Boolean {
return mediaListeners[key]?.timedOut ?: false
}
@@ -335,4 +373,53 @@
}
return true
}
+
+ /** Listens to changes in recommendation card data and schedules a timeout for its expiration */
+ private inner class RecommendationListener(var key: String, data: SmartspaceMediaData) {
+ private var timedOut = false
+ var destroyed = false
+ var expiration = Long.MAX_VALUE
+ private set
+ var cancellation: Runnable? = null
+ private set
+
+ var recommendationData: SmartspaceMediaData = data
+ set(value) {
+ destroyed = false
+ field = value
+ processUpdate()
+ }
+
+ init {
+ recommendationData = data
+ }
+
+ fun destroy() {
+ cancellation?.run()
+ cancellation = null
+ destroyed = true
+ }
+
+ private fun processUpdate() {
+ if (recommendationData.expiryTimeMs != expiration) {
+ // The expiry time changed - cancel and reschedule
+ val timeout =
+ recommendationData.expiryTimeMs -
+ recommendationData.headphoneConnectionTimeMillis
+ logger.logRecommendationTimeoutScheduled(key, timeout)
+ cancellation?.run()
+ cancellation = mainExecutor.executeDelayed({ doTimeout() }, timeout)
+ expiration = recommendationData.expiryTimeMs
+ }
+ }
+
+ fun doTimeout() {
+ cancellation?.run()
+ cancellation = null
+ logger.logTimeout(key)
+ timedOut = true
+ expiration = Long.MAX_VALUE
+ timeoutCallback(key, timedOut)
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt
index 8f3f054..f731dc0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt
@@ -107,6 +107,17 @@
str1 = key
str2 = reason
},
- { "media timeout cancelled for $str1, reason: $str2" }
+ { "timeout cancelled for $str1, reason: $str2" }
+ )
+
+ fun logRecommendationTimeoutScheduled(key: String, timeout: Long) =
+ buffer.log(
+ TAG,
+ LogLevel.VERBOSE,
+ {
+ str1 = key
+ long1 = timeout
+ },
+ { "recommendation timeout scheduled for $str1 in $long1 ms" }
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
index b2ad155..fac1d5e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
@@ -368,7 +368,7 @@
data: SmartspaceMediaData,
shouldPrioritize: Boolean
) {
- debugLogger.logRecommendationLoaded(key)
+ debugLogger.logRecommendationLoaded(key, data.isActive)
// Log the case where the hidden media carousel with the existed inactive resume
// media is shown by the Smartspace signal.
if (data.isActive) {
@@ -442,7 +442,12 @@
logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
}
} else {
- onSmartspaceMediaDataRemoved(data.targetId, immediately = true)
+ if (!mediaFlags.isPersistentSsCardEnabled()) {
+ // Handle update to inactive as a removal
+ onSmartspaceMediaDataRemoved(data.targetId, immediately = true)
+ } else {
+ addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
+ }
}
}
@@ -633,7 +638,19 @@
) =
traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") {
if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel")
- if (MediaPlayerData.getMediaPlayer(key) != null) {
+ MediaPlayerData.getMediaPlayer(key)?.let {
+ if (mediaFlags.isPersistentSsCardEnabled()) {
+ // The card exists, but could have changed active state, so update for sorting
+ MediaPlayerData.addMediaRecommendation(
+ key,
+ data,
+ it,
+ shouldPrioritize,
+ systemClock,
+ debugLogger,
+ update = true,
+ )
+ }
Log.w(TAG, "Skip adding smartspace target in carousel")
return
}
@@ -672,7 +689,7 @@
newRecs,
shouldPrioritize,
systemClock,
- debugLogger
+ debugLogger,
)
updatePlayerToState(newRecs, noAnimation = true)
reorderAllPlayers(curVisibleMediaKey)
@@ -1225,17 +1242,18 @@
player: MediaControlPanel,
shouldPrioritize: Boolean,
clock: SystemClock,
- debugLogger: MediaCarouselControllerLogger? = null
+ debugLogger: MediaCarouselControllerLogger? = null,
+ update: Boolean = false
) {
shouldPrioritizeSs = shouldPrioritize
val removedPlayer = removeMediaPlayer(key)
- if (removedPlayer != null && removedPlayer != player) {
+ if (!update && removedPlayer != null && removedPlayer != player) {
debugLogger?.logPotentialMemoryLeak(key)
}
val sortKey =
MediaSortKey(
isSsMediaRec = true,
- EMPTY.copy(isPlaying = false),
+ EMPTY.copy(active = data.isActive, isPlaying = false),
key,
clock.currentTimeMillis(),
isSsReactivated = true
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt
index eed1bd7..35bda15 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt
@@ -48,8 +48,16 @@
fun logMediaRemoved(key: String) =
buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "removing player $str1" })
- fun logRecommendationLoaded(key: String) =
- buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "add recommendation $str1" })
+ fun logRecommendationLoaded(key: String, isActive: Boolean) =
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ str1 = key
+ bool1 = isActive
+ },
+ { "add recommendation $str1, active $bool1" }
+ )
fun logRecommendationRemoved(key: String, immediately: Boolean) =
buffer.log(
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
index 2fd4f27..1d000eb 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
@@ -57,6 +57,7 @@
import android.view.animation.Interpolator;
import android.widget.ImageButton;
import android.widget.ImageView;
+import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
@@ -116,8 +117,6 @@
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.time.SystemClock;
-import dagger.Lazy;
-
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
@@ -125,6 +124,7 @@
import javax.inject.Inject;
+import dagger.Lazy;
import kotlin.Triple;
import kotlin.Unit;
@@ -524,8 +524,13 @@
}
// Seek Bar
- final MediaController controller = getController();
- mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
+ if (data.getResumption() && data.getResumeProgress() != null) {
+ double progress = data.getResumeProgress();
+ mSeekBarViewModel.updateStaticProgress(progress);
+ } else {
+ final MediaController controller = getController();
+ mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
+ }
// Show the broadcast dialog button only when the le audio is enabled.
mShowBroadcastDialogButton =
@@ -732,9 +737,14 @@
contentDescription =
mRecommendationViewHolder.getGutsViewHolder().getGutsText().getText();
} else if (data != null) {
- contentDescription = mContext.getString(
- R.string.controls_media_smartspace_rec_description,
- data.getAppName(mContext));
+ if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
+ contentDescription = mContext.getString(
+ R.string.controls_media_smartspace_rec_header);
+ } else {
+ contentDescription = mContext.getString(
+ R.string.controls_media_smartspace_rec_description,
+ data.getAppName(mContext));
+ }
} else {
contentDescription = null;
}
@@ -1365,6 +1375,24 @@
hasSubtitle |= !TextUtils.isEmpty(subtitle);
TextView subtitleView = mRecommendationViewHolder.getMediaSubtitles().get(itemIndex);
subtitleView.setText(subtitle);
+
+ // Set up progress bar
+ if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
+ SeekBar mediaProgressBar =
+ mRecommendationViewHolder.getMediaProgressBars().get(itemIndex);
+ TextView mediaSubtitle =
+ mRecommendationViewHolder.getMediaSubtitles().get(itemIndex);
+ // show progress bar if the recommended album is played.
+ Double progress = MediaDataUtils.getDescriptionProgress(recommendation.getExtras());
+ if (progress == null || progress <= 0.0) {
+ mediaProgressBar.setVisibility(View.GONE);
+ mediaSubtitle.setVisibility(View.VISIBLE);
+ } else {
+ mediaProgressBar.setProgress((int) (progress * 100));
+ mediaProgressBar.setVisibility(View.VISIBLE);
+ mediaSubtitle.setVisibility(View.GONE);
+ }
+ }
}
mSmartspaceMediaItemsCount = NUM_REQUIRED_RECOMMENDATIONS;
@@ -1439,6 +1467,12 @@
(title) -> title.setTextColor(textPrimaryColor));
mRecommendationViewHolder.getMediaSubtitles().forEach(
(subtitle) -> subtitle.setTextColor(textSecondaryColor));
+ if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
+ mRecommendationViewHolder.getMediaProgressBars().forEach(
+ (progressBar) -> progressBar.setProgressTintList(
+ ColorStateList.valueOf(textPrimaryColor))
+ );
+ }
mRecommendationViewHolder.getGutsViewHolder().setColors(colorScheme);
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
index bcfceaa..85282a1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
@@ -19,8 +19,12 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.os.Bundle;
import android.text.TextUtils;
+import androidx.core.math.MathUtils;
+import androidx.media.utils.MediaConstants;
+
/**
* Utility class with common methods for media controls
*/
@@ -50,4 +54,35 @@
: unknownName);
return applicationName;
}
+
+ /**
+ * Check the bundle for extras indicating the progress percentage
+ *
+ * @param extras
+ * @return the progress value between 0-1 inclusive if prsent, otherwise null
+ */
+ public static Double getDescriptionProgress(Bundle extras) {
+ if (!extras.containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS)) {
+ return null;
+ }
+
+ int status = extras.getInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS);
+ switch (status) {
+ case MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED:
+ return 0.0;
+ case MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED:
+ return 1.0;
+ case MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED: {
+ if (extras
+ .containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE)) {
+ double percent = extras
+ .getDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE);
+ return MathUtils.clamp(percent, 0.0, 1.0);
+ } else {
+ return 0.5;
+ }
+ }
+ }
+ return null;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
index 81efa36..c3fa76e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
@@ -55,4 +55,10 @@
/** Check whether we show the updated recommendation card. */
fun isRecommendationCardUpdateEnabled() =
featureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)
+
+ /** Check whether to get progress information for resume players */
+ fun isResumeProgressEnabled() = featureFlags.isEnabled(Flags.MEDIA_RESUME_PROGRESS)
+
+ /** If true, do not automatically dismiss the recommendation card */
+ fun isPersistentSsCardEnabled() = featureFlags.isEnabled(Flags.MEDIA_RETAIN_RECOMMENDATIONS)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index d6c6a81..3b45615 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -16,14 +16,18 @@
package com.android.systemui.media.dialog;
-import static android.media.RouteListingPreference.Item.DISABLE_REASON_SUBSCRIPTION_REQUIRED;
+import static android.media.RouteListingPreference.Item.SELECTION_BEHAVIOR_GO_TO_APP;
+import static android.media.RouteListingPreference.Item.SELECTION_BEHAVIOR_NONE;
+import static android.media.RouteListingPreference.Item.SELECTION_BEHAVIOR_TRANSFER;
+import static android.media.RouteListingPreference.Item.SUBTEXT_AD_ROUTING_DISALLOWED;
+import static android.media.RouteListingPreference.Item.SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED;
+import static android.media.RouteListingPreference.Item.SUBTEXT_SUBSCRIPTION_REQUIRED;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
-import android.media.RouteListingPreference;
import android.os.Build;
import android.util.Log;
import android.view.View;
@@ -50,6 +54,8 @@
private static final String TAG = "MediaOutputAdapter";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+ private static final float DEVICE_DISCONNECTED_ALPHA = 0.5f;
+ private static final float DEVICE_CONNECTED_ALPHA = 1f;
public MediaOutputAdapter(MediaOutputController controller) {
super(controller);
@@ -194,16 +200,51 @@
updateFullItemClickListener(v -> onItemClick(v, device));
setSingleLineLayout(getItemTitle(device));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
- && mController.isSubStatusSupported() && device.hasDisabledReason()) {
- //update to subtext with device status
- setUpDeviceIcon(device);
- mSubTitleText.setText(
- Api34Impl.composeDisabledReason(device.getDisableReason(), mContext));
- updateConnectionFailedStatusIcon();
- updateFullItemClickListener(null);
- setTwoLineLayout(device, false /* bFocused */, false /* showSeekBar */,
- false /* showProgressBar */, true /* showSubtitle */,
- true /* showStatus */);
+ && mController.isSubStatusSupported()
+ && mController.isAdvancedLayoutSupported() && device.hasSubtext()) {
+ boolean isActiveWithOngoingSession =
+ (device.hasOngoingSession() && currentlyConnected);
+ boolean isHost = mController.isVolumeControlEnabled(device)
+ && isActiveWithOngoingSession;
+ if (isHost) {
+ mCurrentActivePosition = position;
+ updateTitleIcon(R.drawable.media_output_icon_volume,
+ mController.getColorItemContent());
+ mSubTitleText.setText(device.getSubtextString());
+ updateTwoLineLayoutContentAlpha(DEVICE_CONNECTED_ALPHA);
+ updateEndClickAreaAsSessionEditing(device);
+ setTwoLineLayout(device, null /* title */, true /* bFocused */,
+ true /* showSeekBar */, false /* showProgressBar */,
+ true /* showSubtitle */, false /* showStatus */,
+ true /* showEndTouchArea */, false /* isFakeActive */);
+ initSeekbar(device, isCurrentSeekbarInvisible);
+ } else {
+ if (isActiveWithOngoingSession) {
+ //Selected device which has ongoing session, disable seekbar since we
+ //only allow volume control on Host
+ initSeekbar(device, isCurrentSeekbarInvisible);
+ mCurrentActivePosition = position;
+ }
+ setUpDeviceIcon(device);
+ mSubTitleText.setText(device.getSubtextString());
+ Drawable deviceStatusIcon =
+ isActiveWithOngoingSession ? mContext.getDrawable(
+ R.drawable.media_output_status_session)
+ : Api34Impl.getDeviceStatusIconBasedOnSelectionBehavior(
+ device,
+ mContext);
+ if (deviceStatusIcon != null) {
+ updateDeviceStatusIcon(deviceStatusIcon);
+ }
+ updateTwoLineLayoutContentAlpha(
+ updateClickActionBasedOnSelectionBehavior(device)
+ ? DEVICE_CONNECTED_ALPHA : DEVICE_DISCONNECTED_ALPHA);
+ setTwoLineLayout(device, isActiveWithOngoingSession /* bFocused */,
+ isActiveWithOngoingSession /* showSeekBar */,
+ false /* showProgressBar */, true /* showSubtitle */,
+ deviceStatusIcon != null /* showStatus */,
+ isActiveWithOngoingSession /* isFakeActive */);
+ }
} else if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) {
setUpDeviceIcon(device);
updateConnectionFailedStatusIcon();
@@ -211,7 +252,7 @@
updateFullItemClickListener(v -> onItemClick(v, device));
setTwoLineLayout(device, false /* bFocused */, false /* showSeekBar */,
false /* showProgressBar */, true /* showSubtitle */,
- true /* showStatus */);
+ true /* showStatus */, false /*isFakeActive*/);
} else if (device.getState() == MediaDeviceState.STATE_GROUPING) {
setUpDeviceIcon(device);
updateProgressBarColor();
@@ -220,6 +261,7 @@
false /* showEndTouchArea */);
} else if (mController.getSelectedMediaDevice().size() > 1
&& isDeviceIncluded(mController.getSelectedMediaDevice(), device)) {
+ // selected device in group
boolean isDeviceDeselectable = isDeviceIncluded(
mController.getDeselectableMediaDevice(), device);
if (!mController.isAdvancedLayoutSupported()) {
@@ -235,6 +277,7 @@
initSeekbar(device, isCurrentSeekbarInvisible);
} else if (!mController.hasAdjustVolumeUserRestriction()
&& currentlyConnected) {
+ // single selected device
if (isMutingExpectedDeviceExist
&& !mController.isCurrentConnectedDeviceRemote()) {
// mark as disconnected and set special click listener
@@ -266,6 +309,7 @@
initSeekbar(device, isCurrentSeekbarInvisible);
}
} else if (isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
+ //groupable device
setUpDeviceIcon(device);
updateGroupableCheckBox(false, true, device);
if (mController.isAdvancedLayoutSupported()) {
@@ -280,7 +324,12 @@
} else {
setUpDeviceIcon(device);
setSingleLineLayout(getItemTitle(device));
- updateFullItemClickListener(v -> onItemClick(v, device));
+ if (mController.isAdvancedLayoutSupported()
+ && mController.isSubStatusSupported()) {
+ updateClickActionBasedOnSelectionBehavior(device);
+ } else {
+ updateFullItemClickListener(v -> onItemClick(v, device));
+ }
}
}
}
@@ -292,12 +341,48 @@
ColorStateList(states, colors));
}
+ private void updateTwoLineLayoutContentAlpha(float alphaValue) {
+ mSubTitleText.setAlpha(alphaValue);
+ mTitleIcon.setAlpha(alphaValue);
+ mTwoLineTitleText.setAlpha(alphaValue);
+ mStatusIcon.setAlpha(alphaValue);
+ }
+
+ private void updateEndClickAreaAsSessionEditing(MediaDevice device) {
+ mEndClickIcon.setOnClickListener(null);
+ mEndTouchArea.setOnClickListener(null);
+ updateEndClickAreaColor(mController.getColorSeekbarProgress());
+ mEndClickIcon.setColorFilter(mController.getColorItemContent());
+ mEndClickIcon.setOnClickListener(
+ v -> mController.tryToLaunchInAppRoutingIntent(device.getId(), v));
+ mEndTouchArea.setOnClickListener(v -> mCheckBox.performClick());
+ }
+
+ public void updateEndClickAreaColor(int color) {
+ if (mController.isAdvancedLayoutSupported()) {
+ mEndTouchArea.getBackground().setColorFilter(
+ new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
+ }
+ }
+
+ private boolean updateClickActionBasedOnSelectionBehavior(MediaDevice device) {
+ View.OnClickListener clickListener = Api34Impl.getClickListenerBasedOnSelectionBehavior(
+ device, mController, v -> onItemClick(v, device));
+ updateFullItemClickListener(clickListener);
+ return clickListener != null;
+ }
+
private void updateConnectionFailedStatusIcon() {
mStatusIcon.setImageDrawable(
mContext.getDrawable(R.drawable.media_output_status_failed));
mStatusIcon.setColorFilter(mController.getColorItemContent());
}
+ private void updateDeviceStatusIcon(Drawable drawable) {
+ mStatusIcon.setImageDrawable(drawable);
+ mStatusIcon.setColorFilter(mController.getColorItemContent());
+ }
+
private void updateProgressBarColor() {
mProgressBar.getIndeterminateDrawable().setColorFilter(
new PorterDuffColorFilter(
@@ -411,13 +496,30 @@
@RequiresApi(34)
private static class Api34Impl {
@DoNotInline
- static String composeDisabledReason(@RouteListingPreference.Item.DisableReason int reason,
- Context context) {
- switch(reason) {
- case DISABLE_REASON_SUBSCRIPTION_REQUIRED:
- return context.getString(R.string.media_output_status_require_premium);
+ static View.OnClickListener getClickListenerBasedOnSelectionBehavior(MediaDevice device,
+ MediaOutputController controller, View.OnClickListener defaultTransferListener) {
+ switch (device.getSelectionBehavior()) {
+ case SELECTION_BEHAVIOR_NONE:
+ return null;
+ case SELECTION_BEHAVIOR_TRANSFER:
+ return defaultTransferListener;
+ case SELECTION_BEHAVIOR_GO_TO_APP:
+ return v -> controller.tryToLaunchInAppRoutingIntent(device.getId(), v);
}
- return "";
+ return defaultTransferListener;
+ }
+
+ @DoNotInline
+ static Drawable getDeviceStatusIconBasedOnSelectionBehavior(MediaDevice device,
+ Context context) {
+ switch (device.getSubtext()) {
+ case SUBTEXT_AD_ROUTING_DISALLOWED:
+ case SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED:
+ return context.getDrawable(R.drawable.media_output_status_failed);
+ case SUBTEXT_SUBSCRIPTION_REQUIRED:
+ return context.getDrawable(R.drawable.media_output_status_help);
+ }
+ return null;
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index dc75538..b1cbee8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -147,6 +147,7 @@
final ImageView mStatusIcon;
final CheckBox mCheckBox;
final ViewGroup mEndTouchArea;
+ final ImageView mEndClickIcon;
@VisibleForTesting
MediaOutputSeekbar mSeekBar;
private String mDeviceId;
@@ -168,11 +169,13 @@
mCheckBox = view.requireViewById(R.id.check_box);
mEndTouchArea = view.requireViewById(R.id.end_action_area);
if (mController.isAdvancedLayoutSupported()) {
+ mEndClickIcon = view.requireViewById(R.id.media_output_item_end_click_icon);
mVolumeValueText = view.requireViewById(R.id.volume_value);
mIconAreaLayout = view.requireViewById(R.id.icon_area);
} else {
mVolumeValueText = null;
mIconAreaLayout = null;
+ mEndClickIcon = null;
}
initAnimator();
}
@@ -218,20 +221,7 @@
.mutate();
mItemLayout.setBackground(backgroundDrawable);
if (showSeekBar) {
- final ClipDrawable clipDrawable =
- (ClipDrawable) ((LayerDrawable) mSeekBar.getProgressDrawable())
- .findDrawableByLayerId(android.R.id.progress);
- final GradientDrawable progressDrawable =
- (GradientDrawable) clipDrawable.getDrawable();
- if (mController.isAdvancedLayoutSupported()) {
- progressDrawable.setCornerRadii(
- new float[]{0, 0, mController.getActiveRadius(),
- mController.getActiveRadius(),
- mController.getActiveRadius(),
- mController.getActiveRadius(), 0, 0});
- } else {
- progressDrawable.setCornerRadius(mController.getActiveRadius());
- }
+ updateSeekbarProgressBackground();
}
}
mItemLayout.getBackground().setColorFilter(new PorterDuffColorFilter(
@@ -265,25 +255,52 @@
}
void setTwoLineLayout(MediaDevice device, boolean bFocused, boolean showSeekBar,
- boolean showProgressBar, boolean showSubtitle, boolean showStatus) {
+ boolean showProgressBar, boolean showSubtitle, boolean showStatus,
+ boolean isFakeActive) {
setTwoLineLayout(device, null, bFocused, showSeekBar, showProgressBar, showSubtitle,
- showStatus);
+ showStatus, false, isFakeActive);
}
- private void setTwoLineLayout(MediaDevice device, CharSequence title, boolean bFocused,
+ void setTwoLineLayout(MediaDevice device, CharSequence title, boolean bFocused,
boolean showSeekBar, boolean showProgressBar, boolean showSubtitle,
- boolean showStatus) {
+ boolean showStatus , boolean showEndTouchArea, boolean isFakeActive) {
mTitleText.setVisibility(View.GONE);
mTwoLineLayout.setVisibility(View.VISIBLE);
mStatusIcon.setVisibility(showStatus ? View.VISIBLE : View.GONE);
mSeekBar.setAlpha(1);
mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE);
- final Drawable backgroundDrawable = mContext.getDrawable(
- R.drawable.media_output_item_background)
- .mutate();
- backgroundDrawable.setColorFilter(new PorterDuffColorFilter(
- mController.getColorItemBackground(),
- PorterDuff.Mode.SRC_IN));
+ final Drawable backgroundDrawable;
+ if (mController.isAdvancedLayoutSupported() && mController.isSubStatusSupported()) {
+ backgroundDrawable = mContext.getDrawable(
+ showSeekBar ? R.drawable.media_output_item_background_active
+ : R.drawable.media_output_item_background).mutate();
+ backgroundDrawable.setColorFilter(new PorterDuffColorFilter(
+ showSeekBar ? mController.getColorConnectedItemBackground()
+ : mController.getColorItemBackground(), PorterDuff.Mode.SRC_IN));
+ mIconAreaLayout.getBackground().setColorFilter(new PorterDuffColorFilter(
+ showProgressBar || isFakeActive
+ ? mController.getColorConnectedItemBackground()
+ : showSeekBar ? mController.getColorSeekbarProgress()
+ : mController.getColorItemBackground(),
+ PorterDuff.Mode.SRC_IN));
+ if (showSeekBar) {
+ updateSeekbarProgressBackground();
+ }
+ //update end click area by isActive
+ mEndTouchArea.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE);
+ mEndClickIcon.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE);
+ ViewGroup.MarginLayoutParams params =
+ (ViewGroup.MarginLayoutParams) mItemLayout.getLayoutParams();
+ params.rightMargin = showEndTouchArea ? mController.getItemMarginEndSelectable()
+ : mController.getItemMarginEndDefault();
+ } else {
+ backgroundDrawable = mContext.getDrawable(
+ R.drawable.media_output_item_background)
+ .mutate();
+ backgroundDrawable.setColorFilter(new PorterDuffColorFilter(
+ mController.getColorItemBackground(),
+ PorterDuff.Mode.SRC_IN));
+ }
mItemLayout.setBackground(backgroundDrawable);
mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE);
mSubTitleText.setVisibility(showSubtitle ? View.VISIBLE : View.GONE);
@@ -295,6 +312,23 @@
Typeface.NORMAL));
}
+ void updateSeekbarProgressBackground() {
+ final ClipDrawable clipDrawable =
+ (ClipDrawable) ((LayerDrawable) mSeekBar.getProgressDrawable())
+ .findDrawableByLayerId(android.R.id.progress);
+ final GradientDrawable progressDrawable =
+ (GradientDrawable) clipDrawable.getDrawable();
+ if (mController.isAdvancedLayoutSupported()) {
+ progressDrawable.setCornerRadii(
+ new float[]{0, 0, mController.getActiveRadius(),
+ mController.getActiveRadius(),
+ mController.getActiveRadius(),
+ mController.getActiveRadius(), 0, 0});
+ } else {
+ progressDrawable.setCornerRadius(mController.getActiveRadius());
+ }
+ }
+
void initSeekbar(MediaDevice device, boolean isCurrentSeekbarInvisible) {
if (!mController.isVolumeControlEnabled(device)) {
disableSeekBar();
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index 4803371..35819e3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -263,10 +263,10 @@
mMediaOutputController.releaseSession();
dismiss();
});
- mAppButton.setOnClickListener(v -> mMediaOutputController.tryToLaunchMediaApplication());
+ mAppButton.setOnClickListener(mMediaOutputController::tryToLaunchMediaApplication);
if (mMediaOutputController.isAdvancedLayoutSupported()) {
mMediaMetadataSectionLayout.setOnClickListener(
- v -> mMediaOutputController.tryToLaunchMediaApplication());
+ mMediaOutputController::tryToLaunchMediaApplication);
}
}
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 5f5c686..2aedd36 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -16,6 +16,8 @@
package com.android.systemui.media.dialog;
+import static android.media.RouteListingPreference.ACTION_TRANSFER_MEDIA;
+import static android.media.RouteListingPreference.EXTRA_ROUTE_ID;
import static android.provider.Settings.ACTION_BLUETOOTH_SETTINGS;
import android.annotation.CallbackExecutor;
@@ -24,6 +26,7 @@
import android.app.Notification;
import android.app.WallpaperColors;
import android.bluetooth.BluetoothLeBroadcast;
+import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@@ -382,12 +385,29 @@
return mContext.getPackageManager().getLaunchIntentForPackage(mPackageName);
}
- void tryToLaunchMediaApplication() {
+ void tryToLaunchInAppRoutingIntent(String routeId, View view) {
+ ComponentName componentName = mLocalMediaManager.getLinkedItemComponentName();
+ if (componentName != null) {
+ ActivityLaunchAnimator.Controller controller =
+ mDialogLaunchAnimator.createActivityLaunchController(view);
+ Intent launchIntent = new Intent(ACTION_TRANSFER_MEDIA);
+ launchIntent.setComponent(componentName);
+ launchIntent.putExtra(EXTRA_ROUTE_ID, routeId);
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mCallback.dismissDialog();
+ mContext.startActivity(launchIntent);
+ mActivityStarter.startActivity(launchIntent, true, controller);
+ }
+ }
+
+ void tryToLaunchMediaApplication(View view) {
+ ActivityLaunchAnimator.Controller controller =
+ mDialogLaunchAnimator.createActivityLaunchController(view);
Intent launchIntent = getAppLaunchIntent();
if (launchIntent != null) {
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mCallback.dismissDialog();
- mContext.startActivity(launchIntent);
+ mActivityStarter.startActivity(launchIntent, true, controller);
}
}
@@ -684,19 +704,21 @@
devices.removeAll(targetMediaDevices);
targetMediaDevices.addAll(devices);
}
- mMediaItemList.clear();
- mMediaItemList.addAll(
- targetMediaDevices.stream().map(MediaItem::new).collect(Collectors.toList()));
+ List<MediaItem> finalMediaItems = targetMediaDevices.stream().map(
+ MediaItem::new).collect(Collectors.toList());
dividerItems.forEach((key, item) -> {
- mMediaItemList.add(key, item);
+ finalMediaItems.add(key, item);
});
- mMediaItemList.add(new MediaItem());
+ finalMediaItems.add(new MediaItem());
+ mMediaItemList.clear();
+ mMediaItemList.addAll(finalMediaItems);
}
}
private void categorizeMediaItems(MediaDevice connectedMediaDevice, List<MediaDevice> devices,
boolean needToHandleMutingExpectedDevice) {
synchronized (mMediaDevicesLock) {
+ List<MediaItem> finalMediaItems = new ArrayList<>();
Set<String> selectedDevicesIds = getSelectedMediaDevice().stream().map(
MediaDevice::getId).collect(Collectors.toSet());
if (connectedMediaDevice != null) {
@@ -706,32 +728,32 @@
boolean displayGroupAdded = false;
for (MediaDevice device : devices) {
if (needToHandleMutingExpectedDevice && device.isMutingExpectedDevice()) {
- mMediaItemList.add(0, new MediaItem(device));
+ finalMediaItems.add(0, new MediaItem(device));
} else if (!needToHandleMutingExpectedDevice && selectedDevicesIds.contains(
device.getId())) {
- mMediaItemList.add(0, new MediaItem(device));
+ finalMediaItems.add(0, new MediaItem(device));
} else {
if (device.isSuggestedDevice() && !suggestedDeviceAdded) {
- attachGroupDivider(mContext.getString(
+ attachGroupDivider(finalMediaItems, mContext.getString(
R.string.media_output_group_title_suggested_device));
suggestedDeviceAdded = true;
} else if (!device.isSuggestedDevice() && !displayGroupAdded) {
- attachGroupDivider(mContext.getString(
+ attachGroupDivider(finalMediaItems, mContext.getString(
R.string.media_output_group_title_speakers_and_displays));
displayGroupAdded = true;
}
- mMediaItemList.add(new MediaItem(device));
+ finalMediaItems.add(new MediaItem(device));
}
}
- mMediaItemList.add(new MediaItem());
+ finalMediaItems.add(new MediaItem());
+ mMediaItemList.clear();
+ mMediaItemList.addAll(finalMediaItems);
}
}
- private void attachGroupDivider(String title) {
- synchronized (mMediaDevicesLock) {
- mMediaItemList.add(
- new MediaItem(title, MediaItem.MediaItemType.TYPE_GROUP_DIVIDER));
- }
+ private void attachGroupDivider(List<MediaItem> mediaItems, String title) {
+ mediaItems.add(
+ new MediaItem(title, MediaItem.MediaItemType.TYPE_GROUP_DIVIDER));
}
private void attachRangeInfo(List<MediaDevice> devices) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
index f1acae8..997370b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
@@ -34,7 +34,7 @@
init {
setupShader(RippleShader.RippleShape.CIRCLE)
- setRippleFill(true)
+ setupRippleFadeParams()
setSparkleStrength(0f)
isStarted = false
}
@@ -72,7 +72,7 @@
animator.removeAllUpdateListeners()
// Only show the outline as ripple expands and disappears when animation ends.
- setRippleFill(false)
+ removeRippleFill()
val startingPercentage = calculateStartingPercentage(newHeight)
animator.duration = EXPAND_TO_FULL_DURATION
@@ -103,6 +103,32 @@
return 1 - remainingPercentage
}
+ private fun setupRippleFadeParams() {
+ with(rippleShader) {
+ // No fade out for the base ring.
+ baseRingFadeParams.fadeOutStart = 1f
+ baseRingFadeParams.fadeOutEnd = 1f
+
+ // No fade in and outs for the center fill, as we always draw it.
+ centerFillFadeParams.fadeInStart = 0f
+ centerFillFadeParams.fadeInEnd = 0f
+ centerFillFadeParams.fadeOutStart = 1f
+ centerFillFadeParams.fadeOutEnd = 1f
+ }
+ }
+
+ private fun removeRippleFill() {
+ with(rippleShader) {
+ baseRingFadeParams.fadeOutStart = RippleShader.DEFAULT_BASE_RING_FADE_OUT_START
+ baseRingFadeParams.fadeOutEnd = RippleShader.DEFAULT_FADE_OUT_END
+
+ centerFillFadeParams.fadeInStart = RippleShader.DEFAULT_FADE_IN_START
+ centerFillFadeParams.fadeInEnd = RippleShader.DEFAULT_CENTER_FILL_FADE_IN_END
+ centerFillFadeParams.fadeOutStart = RippleShader.DEFAULT_CENTER_FILL_FADE_OUT_START
+ centerFillFadeParams.fadeOutEnd = RippleShader.DEFAULT_CENTER_FILL_FADE_OUT_END
+ }
+ }
+
companion object {
const val DEFAULT_DURATION = 333L
const val EXPAND_TO_FULL_DURATION = 1000L
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
index 1f27582..537dbb9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
@@ -136,9 +136,9 @@
),
) {
override fun isValidNextState(nextState: ChipStateSender): Boolean {
- return nextState == FAR_FROM_RECEIVER ||
- nextState == ALMOST_CLOSE_TO_START_CAST ||
- nextState == TRANSFER_TO_THIS_DEVICE_TRIGGERED
+ // Since _SUCCEEDED is the end of a transfer sequence, we should be able to move to any
+ // state that represents the beginning of a new sequence.
+ return stateIsStartOfSequence(nextState)
}
},
@@ -158,9 +158,9 @@
),
) {
override fun isValidNextState(nextState: ChipStateSender): Boolean {
- return nextState == FAR_FROM_RECEIVER ||
- nextState == ALMOST_CLOSE_TO_END_CAST ||
- nextState == TRANSFER_TO_RECEIVER_TRIGGERED
+ // Since _SUCCEEDED is the end of a transfer sequence, we should be able to move to any
+ // state that represents the beginning of a new sequence.
+ return stateIsStartOfSequence(nextState)
}
},
@@ -173,9 +173,9 @@
endItem = SenderEndItem.Error,
) {
override fun isValidNextState(nextState: ChipStateSender): Boolean {
- return nextState == FAR_FROM_RECEIVER ||
- nextState == ALMOST_CLOSE_TO_START_CAST ||
- nextState == TRANSFER_TO_THIS_DEVICE_TRIGGERED
+ // Since _FAILED is the end of a transfer sequence, we should be able to move to any
+ // state that represents the beginning of a new sequence.
+ return stateIsStartOfSequence(nextState)
}
},
@@ -188,9 +188,9 @@
endItem = SenderEndItem.Error,
) {
override fun isValidNextState(nextState: ChipStateSender): Boolean {
- return nextState == FAR_FROM_RECEIVER ||
- nextState == ALMOST_CLOSE_TO_END_CAST ||
- nextState == TRANSFER_TO_RECEIVER_TRIGGERED
+ // Since _FAILED is the end of a transfer sequence, we should be able to move to any
+ // state that represents the beginning of a new sequence.
+ return stateIsStartOfSequence(nextState)
}
},
@@ -210,9 +210,9 @@
}
override fun isValidNextState(nextState: ChipStateSender): Boolean {
- return nextState == FAR_FROM_RECEIVER ||
- nextState.transferStatus == TransferStatus.NOT_STARTED ||
- nextState.transferStatus == TransferStatus.IN_PROGRESS
+ // When far away, we can go to any state that represents the start of a transfer
+ // sequence.
+ return stateIsStartOfSequence(nextState)
}
};
@@ -227,6 +227,20 @@
return Text.Loaded(context.getString(stringResId!!, otherDeviceName))
}
+ /**
+ * Returns true if moving from this state to [nextState] is a valid transition.
+ *
+ * In general, we expect a media transfer go to through a sequence of states:
+ * For push-to-receiver:
+ * - ALMOST_CLOSE_TO_START_CAST => TRANSFER_TO_RECEIVER_TRIGGERED =>
+ * TRANSFER_TO_RECEIVER_(SUCCEEDED|FAILED)
+ * - ALMOST_CLOSE_TO_END_CAST => TRANSFER_TO_THIS_DEVICE_TRIGGERED =>
+ * TRANSFER_TO_THIS_DEVICE_(SUCCEEDED|FAILED)
+ *
+ * This method should validate that the states go through approximately that sequence.
+ *
+ * See b/221265848 for more details.
+ */
abstract fun isValidNextState(nextState: ChipStateSender): Boolean
companion object {
@@ -276,6 +290,18 @@
return currentState.isValidNextState(desiredState)
}
+
+ /**
+ * Returns true if [state] represents a state at the beginning of a sequence and false
+ * otherwise.
+ */
+ private fun stateIsStartOfSequence(state: ChipStateSender): Boolean {
+ return state == FAR_FROM_RECEIVER ||
+ state.transferStatus == TransferStatus.NOT_STARTED ||
+ // It's possible to skip the NOT_STARTED phase and go immediately into the
+ // IN_PROGRESS phase.
+ state.transferStatus == TransferStatus.IN_PROGRESS
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
index e665d83..1678c6e 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
@@ -24,6 +24,7 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.media.MediaProjectionAppSelectorActivity
import com.android.systemui.media.MediaProjectionAppSelectorActivity.Companion.EXTRA_HOST_APP_USER_HANDLE
+import com.android.systemui.media.MediaProjectionPermissionActivity
import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerLabelLoader
import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerThumbnailLoader
import com.android.systemui.mediaprojection.appselector.data.AppIconLoader
@@ -45,10 +46,10 @@
import dagger.Subcomponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
-import javax.inject.Qualifier
-import javax.inject.Scope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
+import javax.inject.Qualifier
+import javax.inject.Scope
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class MediaProjectionAppSelector
@@ -67,6 +68,11 @@
fun provideMediaProjectionAppSelectorActivity(
activity: MediaProjectionAppSelectorActivity
): Activity
+
+ @Binds
+ @IntoMap
+ @ClassKey(MediaProjectionPermissionActivity::class)
+ fun bindsMediaProjectionPermissionActivity(impl: MediaProjectionPermissionActivity): Activity
}
/**
@@ -149,6 +155,7 @@
val controller: MediaProjectionAppSelectorController
val recentsViewController: MediaProjectionRecentsViewController
+ val emptyStateProvider: MediaProjectionBlockerEmptyStateProvider
@get:HostUserHandle val hostUserHandle: UserHandle
@get:PersonalProfile val personalProfileUserHandle: UserHandle
diff --git a/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt b/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt
index 1324d2c..c4a1ed4 100644
--- a/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt
@@ -19,7 +19,6 @@
import android.view.WindowManagerGlobal
import com.android.app.motiontool.DdmHandleMotionTool
import com.android.app.motiontool.MotionToolManager
-import com.android.app.viewcapture.ViewCapture
import com.android.systemui.CoreStartable
import dagger.Binds
import dagger.Module
@@ -38,17 +37,12 @@
}
@Provides
- fun provideMotionToolManager(
- viewCapture: ViewCapture,
- windowManagerGlobal: WindowManagerGlobal
- ): MotionToolManager {
- return MotionToolManager.getInstance(viewCapture, windowManagerGlobal)
+ fun provideMotionToolManager(windowManagerGlobal: WindowManagerGlobal): MotionToolManager {
+ return MotionToolManager.getInstance(windowManagerGlobal)
}
@Provides
fun provideWindowManagerGlobal(): WindowManagerGlobal = WindowManagerGlobal.getInstance()
-
- @Provides fun provideViewCapture(): ViewCapture = ViewCapture.getInstance()
}
@Binds
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 79b4b3a..d5d7325 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -30,7 +30,6 @@
import static android.view.InsetsState.ITYPE_LEFT_GESTURES;
import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
import static android.view.InsetsState.ITYPE_RIGHT_GESTURES;
-import static android.view.InsetsState.containsType;
import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
@@ -85,7 +84,6 @@
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.InsetsFrameProvider;
-import android.view.InsetsState.InternalInsetsType;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Surface;
@@ -97,6 +95,7 @@
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.InternalInsetsInfo;
import android.view.ViewTreeObserver.OnComputeInternalInsetsListener;
+import android.view.WindowInsets;
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsController.Appearance;
import android.view.WindowInsetsController.Behavior;
@@ -191,7 +190,7 @@
private static final String EXTRA_TRANSIENT_STATE = "transient_state";
/** Allow some time inbetween the long press for back and recents. */
- private static final int LOCK_TO_APP_GESTURE_TOLERENCE = 200;
+ private static final int LOCK_TO_APP_GESTURE_TOLERANCE = 200;
private static final long AUTODIM_TIMEOUT_MS = 2250;
private final Context mContext;
@@ -1150,12 +1149,11 @@
}
@Override
- public void showTransient(int displayId, @InternalInsetsType int[] types,
- boolean isGestureOnSystemBar) {
+ public void showTransient(int displayId, @InsetsType int types, boolean isGestureOnSystemBar) {
if (displayId != mDisplayId) {
return;
}
- if (!containsType(types, ITYPE_NAVIGATION_BAR)) {
+ if ((types & WindowInsets.Type.navigationBars()) == 0) {
return;
}
if (!mTransientShown) {
@@ -1166,11 +1164,11 @@
}
@Override
- public void abortTransient(int displayId, @InternalInsetsType int[] types) {
+ public void abortTransient(int displayId, @InsetsType int types) {
if (displayId != mDisplayId) {
return;
}
- if (!containsType(types, ITYPE_NAVIGATION_BAR)) {
+ if ((types & WindowInsets.Type.navigationBars()) == 0) {
return;
}
clearTransient();
@@ -1439,7 +1437,7 @@
// If we recently long-pressed the other button then they were
// long-pressed 'together'
- if ((time - mLastLockToAppLongPress) < LOCK_TO_APP_GESTURE_TOLERENCE) {
+ if ((time - mLastLockToAppLongPress) < LOCK_TO_APP_GESTURE_TOLERANCE) {
stopLockTaskMode = true;
return true;
} else if (v.getId() == btnId1) {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
index f3712e6..c3d7369 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
@@ -20,8 +20,6 @@
import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN;
import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
-import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
-import static android.view.InsetsState.containsType;
import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
@@ -41,7 +39,6 @@
import android.app.StatusBarManager;
import android.app.StatusBarManager.WindowVisibleState;
-import android.content.ComponentName;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
@@ -52,6 +49,7 @@
import android.util.Log;
import android.view.Display;
import android.view.View;
+import android.view.WindowInsets;
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsController.Appearance;
import android.view.WindowInsetsController.Behavior;
@@ -68,7 +66,6 @@
import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
import com.android.systemui.recents.OverviewProxyService;
import com.android.systemui.shared.recents.utilities.Utilities;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
@@ -401,11 +398,11 @@
}
@Override
- public void showTransient(int displayId, int[] types, boolean isGestureOnSystemBar) {
+ public void showTransient(int displayId, @InsetsType int types, boolean isGestureOnSystemBar) {
if (displayId != mDisplayId) {
return;
}
- if (!containsType(types, ITYPE_EXTRA_NAVIGATION_BAR)) {
+ if ((types & WindowInsets.Type.navigationBars()) == 0) {
return;
}
if (!mTaskbarTransientShowing) {
@@ -415,11 +412,11 @@
}
@Override
- public void abortTransient(int displayId, int[] types) {
+ public void abortTransient(int displayId, @InsetsType int types) {
if (displayId != mDisplayId) {
return;
}
- if (!containsType(types, ITYPE_EXTRA_NAVIGATION_BAR)) {
+ if ((types & WindowInsets.Type.navigationBars()) == 0) {
return;
}
clearTransient();
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
index 2822435..f335733 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
@@ -1,14 +1,20 @@
package com.android.systemui.navigationbar.gestural
import android.content.Context
+import android.content.res.Configuration
+import android.content.res.TypedArray
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
+import android.util.MathUtils.min
+import android.util.TypedValue
import android.view.View
+import androidx.appcompat.view.ContextThemeWrapper
import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
+import com.android.internal.R.style.Theme_DeviceDefault
import com.android.internal.util.LatencyTracker
import com.android.settingslib.Utils
import com.android.systemui.navigationbar.gestural.BackPanelController.DelayedOnAnimationEndListener
@@ -16,7 +22,10 @@
private const val TAG = "BackPanel"
private const val DEBUG = false
-class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : View(context) {
+class BackPanel(
+ context: Context,
+ private val latencyTracker: LatencyTracker
+) : View(context) {
var arrowsPointLeft = false
set(value) {
@@ -45,52 +54,54 @@
/**
* The length of the arrow measured horizontally. Used for animating [arrowPath]
*/
- private var arrowLength = AnimatedFloat("arrowLength", SpringForce())
+ private var arrowLength = AnimatedFloat(
+ name = "arrowLength",
+ minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS
+ )
/**
* The height of the arrow measured vertically from its center to its top (i.e. half the total
* height). Used for animating [arrowPath]
*/
- private var arrowHeight = AnimatedFloat("arrowHeight", SpringForce())
-
- private val backgroundWidth = AnimatedFloat(
- name = "backgroundWidth",
- SpringForce().apply {
- stiffness = 600f
- dampingRatio = 0.65f
- }
+ var arrowHeight = AnimatedFloat(
+ name = "arrowHeight",
+ minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ROTATION_DEGREES
)
- private val backgroundHeight = AnimatedFloat(
- name = "backgroundHeight",
- SpringForce().apply {
- stiffness = 600f
- dampingRatio = 0.65f
- }
+ val backgroundWidth = AnimatedFloat(
+ name = "backgroundWidth",
+ minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
+ minimumValue = 0f,
+ )
+
+ val backgroundHeight = AnimatedFloat(
+ name = "backgroundHeight",
+ minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
+ minimumValue = 0f,
)
/**
* Corners of the background closer to the edge of the screen (where the arrow appeared from).
* Used for animating [arrowBackgroundRect]
*/
- private val backgroundEdgeCornerRadius = AnimatedFloat(
- name = "backgroundEdgeCornerRadius",
- SpringForce().apply {
- stiffness = 400f
- dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
- }
- )
+ val backgroundEdgeCornerRadius = AnimatedFloat("backgroundEdgeCornerRadius")
/**
* Corners of the background further from the edge of the screens (toward the direction the
* arrow is being dragged). Used for animating [arrowBackgroundRect]
*/
- private val backgroundFarCornerRadius = AnimatedFloat(
- name = "backgroundDragCornerRadius",
- SpringForce().apply {
- stiffness = 2200f
- dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
- }
+ val backgroundFarCornerRadius = AnimatedFloat("backgroundFarCornerRadius")
+
+ var scale = AnimatedFloat(
+ name = "scale",
+ minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_SCALE,
+ minimumValue = 0f
+ )
+
+ val scalePivotX = AnimatedFloat(
+ name = "scalePivotX",
+ minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
+ minimumValue = backgroundWidth.pos / 2,
)
/**
@@ -98,34 +109,40 @@
* background's margin relative to the screen edge. The arrow will be centered within the
* background.
*/
- private var horizontalTranslation = AnimatedFloat("horizontalTranslation", SpringForce())
+ var horizontalTranslation = AnimatedFloat(name = "horizontalTranslation")
- private val currentAlpha: FloatPropertyCompat<BackPanel> =
- object : FloatPropertyCompat<BackPanel>("currentAlpha") {
- override fun setValue(panel: BackPanel, value: Float) {
- panel.alpha = value
- }
+ var arrowAlpha = AnimatedFloat(
+ name = "arrowAlpha",
+ minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ALPHA,
+ minimumValue = 0f,
+ maximumValue = 1f
+ )
- override fun getValue(panel: BackPanel): Float = panel.alpha
- }
+ val backgroundAlpha = AnimatedFloat(
+ name = "backgroundAlpha",
+ minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ALPHA,
+ minimumValue = 0f,
+ maximumValue = 1f
+ )
- private val alphaAnimation = SpringAnimation(this, currentAlpha)
- .setSpring(
- SpringForce()
- .setStiffness(60f)
- .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
- )
+ private val allAnimatedFloat = setOf(
+ arrowLength,
+ arrowHeight,
+ backgroundWidth,
+ backgroundEdgeCornerRadius,
+ backgroundFarCornerRadius,
+ scalePivotX,
+ scale,
+ horizontalTranslation,
+ arrowAlpha,
+ backgroundAlpha
+ )
/**
* Canvas vertical translation. How far up/down the arrow and background appear relative to the
* canvas.
*/
- private var verticalTranslation: AnimatedFloat = AnimatedFloat(
- name = "verticalTranslation",
- SpringForce().apply {
- stiffness = SpringForce.STIFFNESS_MEDIUM
- }
- )
+ var verticalTranslation = AnimatedFloat("verticalTranslation")
/**
* Use for drawing debug info. Can only be set if [DEBUG]=true
@@ -136,28 +153,67 @@
}
internal fun updateArrowPaint(arrowThickness: Float) {
- // Arrow constants
+
arrowPaint.strokeWidth = arrowThickness
- arrowPaint.color =
- Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary)
- arrowBackgroundPaint.color = Utils.getColorAccentDefaultColor(context)
+ val isDeviceInNightTheme = resources.configuration.uiMode and
+ Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
+
+ val colorControlActivated = ContextThemeWrapper(context, Theme_DeviceDefault)
+ .run {
+ val typedValue = TypedValue()
+ val a: TypedArray = obtainStyledAttributes(typedValue.data,
+ intArrayOf(android.R.attr.colorControlActivated))
+ val color = a.getColor(0, 0)
+ a.recycle()
+ color
+ }
+
+ val colorPrimary =
+ Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary)
+
+ arrowPaint.color = Utils.getColorAccentDefaultColor(context)
+
+ arrowBackgroundPaint.color = if (isDeviceInNightTheme) {
+ colorPrimary
+ } else {
+ colorControlActivated
+ }
}
- private inner class AnimatedFloat(name: String, springForce: SpringForce) {
+ inner class AnimatedFloat(
+ name: String,
+ private val minimumVisibleChange: Float? = null,
+ private val minimumValue: Float? = null,
+ private val maximumValue: Float? = null,
+ ) {
+
// The resting position when not stretched by a touch drag
private var restingPosition = 0f
// The current position as updated by the SpringAnimation
var pos = 0f
- set(v) {
+ private set(v) {
if (field != v) {
field = v
invalidate()
}
}
- val animation: SpringAnimation
+ private val animation: SpringAnimation
+ var spring: SpringForce
+ get() = animation.spring
+ set(value) {
+ animation.cancel()
+ animation.spring = value
+ }
+
+ val isRunning: Boolean
+ get() = animation.isRunning
+
+ fun addEndListener(listener: DelayedOnAnimationEndListener) {
+ animation.addEndListener(listener)
+ }
init {
val floatProp = object : FloatPropertyCompat<AnimatedFloat>(name) {
@@ -167,8 +223,12 @@
override fun getValue(animatedFloat: AnimatedFloat): Float = animatedFloat.pos
}
- animation = SpringAnimation(this, floatProp)
- animation.spring = springForce
+ animation = SpringAnimation(this, floatProp).apply {
+ spring = SpringForce()
+ this@AnimatedFloat.minimumValue?.let { setMinValue(it) }
+ this@AnimatedFloat.maximumValue?.let { setMaxValue(it) }
+ this@AnimatedFloat.minimumVisibleChange?.let { minimumVisibleChange = it }
+ }
}
fun snapTo(newPosition: Float) {
@@ -178,8 +238,24 @@
pos = newPosition
}
- fun stretchTo(stretchAmount: Float) {
- animation.animateToFinalPosition(restingPosition + stretchAmount)
+ fun snapToRestingPosition() {
+ snapTo(restingPosition)
+ }
+
+
+ fun stretchTo(
+ stretchAmount: Float,
+ startingVelocity: Float? = null,
+ springForce: SpringForce? = null
+ ) {
+ animation.apply {
+ startingVelocity?.let {
+ cancel()
+ setStartVelocity(it)
+ }
+ springForce?.let { spring = springForce }
+ animateToFinalPosition(restingPosition + stretchAmount)
+ }
}
/**
@@ -188,18 +264,23 @@
*
* The [restingPosition] will remain unchanged. Only the animation is updated.
*/
- fun stretchBy(finalPosition: Float, amount: Float) {
- val stretchedAmount = amount * (finalPosition - restingPosition)
+ fun stretchBy(finalPosition: Float?, amount: Float) {
+ val stretchedAmount = amount * ((finalPosition ?: 0f) - restingPosition)
animation.animateToFinalPosition(restingPosition + stretchedAmount)
}
- fun updateRestingPosition(pos: Float, animated: Boolean) {
+ fun updateRestingPosition(pos: Float?, animated: Boolean = true) {
+ if (pos == null) return
+
restingPosition = pos
- if (animated)
+ if (animated) {
animation.animateToFinalPosition(restingPosition)
- else
+ } else {
snapTo(restingPosition)
+ }
}
+
+ fun cancel() = animation.cancel()
}
init {
@@ -224,126 +305,203 @@
return arrowPath
}
- fun addEndListener(endListener: DelayedOnAnimationEndListener): Boolean {
- return if (alphaAnimation.isRunning) {
- alphaAnimation.addEndListener(endListener)
- true
- } else if (horizontalTranslation.animation.isRunning) {
- horizontalTranslation.animation.addEndListener(endListener)
+ fun addAnimationEndListener(
+ animatedFloat: AnimatedFloat,
+ endListener: DelayedOnAnimationEndListener
+ ): Boolean {
+ return if (animatedFloat.isRunning) {
+ animatedFloat.addEndListener(endListener)
true
} else {
- endListener.runNow()
+ endListener.run()
false
}
}
+ fun cancelAnimations() {
+ allAnimatedFloat.forEach { it.cancel() }
+ }
+
fun setStretch(
- horizontalTranslationStretchAmount: Float,
- arrowStretchAmount: Float,
- backgroundWidthStretchAmount: Float,
- fullyStretchedDimens: EdgePanelParams.BackIndicatorDimens
+ horizontalTranslationStretchAmount: Float,
+ arrowStretchAmount: Float,
+ arrowAlphaStretchAmount: Float,
+ backgroundAlphaStretchAmount: Float,
+ backgroundWidthStretchAmount: Float,
+ backgroundHeightStretchAmount: Float,
+ edgeCornerStretchAmount: Float,
+ farCornerStretchAmount: Float,
+ fullyStretchedDimens: EdgePanelParams.BackIndicatorDimens
) {
horizontalTranslation.stretchBy(
- finalPosition = fullyStretchedDimens.horizontalTranslation,
- amount = horizontalTranslationStretchAmount
+ finalPosition = fullyStretchedDimens.horizontalTranslation,
+ amount = horizontalTranslationStretchAmount
)
arrowLength.stretchBy(
- finalPosition = fullyStretchedDimens.arrowDimens.length,
- amount = arrowStretchAmount
+ finalPosition = fullyStretchedDimens.arrowDimens.length,
+ amount = arrowStretchAmount
)
arrowHeight.stretchBy(
- finalPosition = fullyStretchedDimens.arrowDimens.height,
- amount = arrowStretchAmount
+ finalPosition = fullyStretchedDimens.arrowDimens.height,
+ amount = arrowStretchAmount
+ )
+ arrowAlpha.stretchBy(
+ finalPosition = fullyStretchedDimens.arrowDimens.alpha,
+ amount = arrowAlphaStretchAmount
+ )
+ backgroundAlpha.stretchBy(
+ finalPosition = fullyStretchedDimens.backgroundDimens.alpha,
+ amount = backgroundAlphaStretchAmount
)
backgroundWidth.stretchBy(
- finalPosition = fullyStretchedDimens.backgroundDimens.width,
- amount = backgroundWidthStretchAmount
+ finalPosition = fullyStretchedDimens.backgroundDimens.width,
+ amount = backgroundWidthStretchAmount
+ )
+ backgroundHeight.stretchBy(
+ finalPosition = fullyStretchedDimens.backgroundDimens.height,
+ amount = backgroundHeightStretchAmount
+ )
+ backgroundEdgeCornerRadius.stretchBy(
+ finalPosition = fullyStretchedDimens.backgroundDimens.edgeCornerRadius,
+ amount = edgeCornerStretchAmount
+ )
+ backgroundFarCornerRadius.stretchBy(
+ finalPosition = fullyStretchedDimens.backgroundDimens.farCornerRadius,
+ amount = farCornerStretchAmount
)
}
+ fun popOffEdge(startingVelocity: Float) {
+ val heightStretchAmount = startingVelocity * 50
+ val widthStretchAmount = startingVelocity * 150
+ val scaleStretchAmount = startingVelocity * 0.8f
+ backgroundHeight.stretchTo(stretchAmount = 0f, startingVelocity = -heightStretchAmount)
+ backgroundWidth.stretchTo(stretchAmount = 0f, startingVelocity = widthStretchAmount)
+ scale.stretchTo(stretchAmount = 0f, startingVelocity = -scaleStretchAmount)
+ }
+
+ fun popScale(startingVelocity: Float) {
+ scalePivotX.snapTo(backgroundWidth.pos / 2)
+ scale.stretchTo(stretchAmount = 0f, startingVelocity = startingVelocity)
+ }
+
+ fun popArrowAlpha(startingVelocity: Float, springForce: SpringForce? = null) {
+ arrowAlpha.stretchTo(stretchAmount = 0f, startingVelocity = startingVelocity,
+ springForce = springForce)
+ }
+
fun resetStretch() {
- horizontalTranslation.stretchTo(0f)
- arrowLength.stretchTo(0f)
- arrowHeight.stretchTo(0f)
- backgroundWidth.stretchTo(0f)
- backgroundHeight.stretchTo(0f)
- backgroundEdgeCornerRadius.stretchTo(0f)
- backgroundFarCornerRadius.stretchTo(0f)
+ backgroundAlpha.snapTo(1f)
+ verticalTranslation.snapTo(0f)
+ scale.snapTo(1f)
+
+ horizontalTranslation.snapToRestingPosition()
+ arrowLength.snapToRestingPosition()
+ arrowHeight.snapToRestingPosition()
+ arrowAlpha.snapToRestingPosition()
+ backgroundWidth.snapToRestingPosition()
+ backgroundHeight.snapToRestingPosition()
+ backgroundEdgeCornerRadius.snapToRestingPosition()
+ backgroundFarCornerRadius.snapToRestingPosition()
}
/**
* Updates resting arrow and background size not accounting for stretch
*/
internal fun setRestingDimens(
- restingParams: EdgePanelParams.BackIndicatorDimens,
- animate: Boolean
+ restingParams: EdgePanelParams.BackIndicatorDimens,
+ animate: Boolean = true
) {
- horizontalTranslation.updateRestingPosition(restingParams.horizontalTranslation, animate)
+ horizontalTranslation.updateRestingPosition(restingParams.horizontalTranslation)
+ scale.updateRestingPosition(restingParams.scale)
+ arrowAlpha.updateRestingPosition(restingParams.arrowDimens.alpha)
+ backgroundAlpha.updateRestingPosition(restingParams.backgroundDimens.alpha)
+
arrowLength.updateRestingPosition(restingParams.arrowDimens.length, animate)
arrowHeight.updateRestingPosition(restingParams.arrowDimens.height, animate)
+ scalePivotX.updateRestingPosition(restingParams.backgroundDimens.width, animate)
backgroundWidth.updateRestingPosition(restingParams.backgroundDimens.width, animate)
backgroundHeight.updateRestingPosition(restingParams.backgroundDimens.height, animate)
backgroundEdgeCornerRadius.updateRestingPosition(
- restingParams.backgroundDimens.edgeCornerRadius,
- animate
+ restingParams.backgroundDimens.edgeCornerRadius, animate
)
backgroundFarCornerRadius.updateRestingPosition(
- restingParams.backgroundDimens.farCornerRadius,
- animate
+ restingParams.backgroundDimens.farCornerRadius, animate
)
}
fun animateVertically(yPos: Float) = verticalTranslation.stretchTo(yPos)
- fun setArrowStiffness(arrowStiffness: Float, arrowDampingRatio: Float) {
- arrowLength.animation.spring.apply {
- stiffness = arrowStiffness
- dampingRatio = arrowDampingRatio
- }
- arrowHeight.animation.spring.apply {
- stiffness = arrowStiffness
- dampingRatio = arrowDampingRatio
- }
+ fun setSpring(
+ horizontalTranslation: SpringForce? = null,
+ verticalTranslation: SpringForce? = null,
+ scale: SpringForce? = null,
+ arrowLength: SpringForce? = null,
+ arrowHeight: SpringForce? = null,
+ arrowAlpha: SpringForce? = null,
+ backgroundAlpha: SpringForce? = null,
+ backgroundFarCornerRadius: SpringForce? = null,
+ backgroundEdgeCornerRadius: SpringForce? = null,
+ backgroundWidth: SpringForce? = null,
+ backgroundHeight: SpringForce? = null,
+ ) {
+ arrowLength?.let { this.arrowLength.spring = it }
+ arrowHeight?.let { this.arrowHeight.spring = it }
+ arrowAlpha?.let { this.arrowAlpha.spring = it }
+ backgroundAlpha?.let { this.backgroundAlpha.spring = it }
+ backgroundFarCornerRadius?.let { this.backgroundFarCornerRadius.spring = it }
+ backgroundEdgeCornerRadius?.let { this.backgroundEdgeCornerRadius.spring = it }
+ scale?.let { this.scale.spring = it }
+ backgroundWidth?.let { this.backgroundWidth.spring = it }
+ backgroundHeight?.let { this.backgroundHeight.spring = it }
+ horizontalTranslation?.let { this.horizontalTranslation.spring = it }
+ verticalTranslation?.let { this.verticalTranslation.spring = it }
}
override fun hasOverlappingRendering() = false
override fun onDraw(canvas: Canvas) {
- var edgeCorner = backgroundEdgeCornerRadius.pos
+ val edgeCorner = backgroundEdgeCornerRadius.pos
val farCorner = backgroundFarCornerRadius.pos
val halfHeight = backgroundHeight.pos / 2
+ val canvasWidth = width
+ val backgroundWidth = backgroundWidth.pos
+ val scalePivotX = scalePivotX.pos
canvas.save()
- if (!isLeftPanel) canvas.scale(-1f, 1f, width / 2.0f, 0f)
+ if (!isLeftPanel) canvas.scale(-1f, 1f, canvasWidth / 2.0f, 0f)
canvas.translate(
- horizontalTranslation.pos,
- height * 0.5f + verticalTranslation.pos
+ horizontalTranslation.pos,
+ height * 0.5f + verticalTranslation.pos
)
+ canvas.scale(scale.pos, scale.pos, scalePivotX, 0f)
+
val arrowBackground = arrowBackgroundRect.apply {
left = 0f
top = -halfHeight
- right = backgroundWidth.pos
+ right = backgroundWidth
bottom = halfHeight
}.toPathWithRoundCorners(
- topLeft = edgeCorner,
- bottomLeft = edgeCorner,
- topRight = farCorner,
- bottomRight = farCorner
+ topLeft = edgeCorner,
+ bottomLeft = edgeCorner,
+ topRight = farCorner,
+ bottomRight = farCorner
)
- canvas.drawPath(arrowBackground, arrowBackgroundPaint)
+ canvas.drawPath(arrowBackground,
+ arrowBackgroundPaint.apply { alpha = (255 * backgroundAlpha.pos).toInt() })
val dx = arrowLength.pos
val dy = arrowHeight.pos
// How far the arrow bounding box should be from the edge of the screen. Measured from
// either the tip or the back of the arrow, whichever is closer
- var arrowOffset = (backgroundWidth.pos - dx) / 2
+ val arrowOffset = (backgroundWidth - dx) / 2
canvas.translate(
- /* dx= */ arrowOffset,
- /* dy= */ 0f /* pass 0 for the y position since the canvas was already translated */
+ /* dx= */ arrowOffset,
+ /* dy= */ 0f /* pass 0 for the y position since the canvas was already translated */
)
val arrowPointsAwayFromEdge = !arrowsPointLeft.xor(isLeftPanel)
@@ -355,6 +513,8 @@
}
val arrowPath = calculateArrowPath(dx = dx, dy = dy)
+ val arrowPaint = arrowPaint
+ .apply { alpha = (255 * min(arrowAlpha.pos, backgroundAlpha.pos)).toInt() }
canvas.drawPath(arrowPath, arrowPaint)
canvas.restore()
@@ -372,26 +532,17 @@
}
private fun RectF.toPathWithRoundCorners(
- topLeft: Float = 0f,
- topRight: Float = 0f,
- bottomRight: Float = 0f,
- bottomLeft: Float = 0f
+ topLeft: Float = 0f,
+ topRight: Float = 0f,
+ bottomRight: Float = 0f,
+ bottomLeft: Float = 0f
): Path = Path().apply {
val corners = floatArrayOf(
- topLeft, topLeft,
- topRight, topRight,
- bottomRight, bottomRight,
- bottomLeft, bottomLeft
+ topLeft, topLeft,
+ topRight, topRight,
+ bottomRight, bottomRight,
+ bottomLeft, bottomLeft
)
addRoundRect(this@toPathWithRoundCorners, corners, Path.Direction.CW)
}
-
- fun cancelAlphaAnimations() {
- alphaAnimation.cancel()
- alpha = 1f
- }
-
- fun fadeOut() {
- alphaAnimation.animateToFinalPosition(0f)
- }
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
index 1950c69..e0ba543 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -24,18 +24,16 @@
import android.os.SystemClock
import android.os.VibrationEffect
import android.util.Log
-import android.util.MathUtils.constrain
-import android.util.MathUtils.saturate
+import android.util.MathUtils
import android.view.Gravity
import android.view.MotionEvent
import android.view.VelocityTracker
-import android.view.View
import android.view.ViewConfiguration
import android.view.WindowManager
-import android.view.animation.DecelerateInterpolator
-import android.view.animation.PathInterpolator
+import androidx.annotation.VisibleForTesting
+import androidx.core.os.postDelayed
+import androidx.core.view.isVisible
import androidx.dynamicanimation.animation.DynamicAnimation
-import androidx.dynamicanimation.animation.SpringForce
import com.android.internal.util.LatencyTracker
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.MotionEventsHandlerBase
@@ -51,58 +49,42 @@
import kotlin.math.sign
private const val TAG = "BackPanelController"
-private const val DEBUG = false
-
private const val ENABLE_FAILSAFE = true
-private const val FAILSAFE_DELAY_MS: Long = 350
+private const val PX_PER_SEC = 1000
+private const val PX_PER_MS = 1
-/**
- * The time required between the arrow-appears vibration effect and the back-committed vibration
- * effect. If the arrow is flung quickly, the phone only vibrates once. However, if the arrow is
- * held on the screen for a long time, it will vibrate a second time when the back gesture is
- * committed.
- */
-private const val GESTURE_DURATION_FOR_CLICK_MS = 400
+internal const val MIN_DURATION_ACTIVE_ANIMATION = 300L
+private const val MIN_DURATION_CANCELLED_ANIMATION = 200L
+private const val MIN_DURATION_COMMITTED_ANIMATION = 200L
+private const val MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION = 50L
+private const val MIN_DURATION_CONSIDERED_AS_FLING = 100L
-/**
- * The min duration arrow remains on screen during a fling event.
- */
-private const val FLING_MIN_APPEARANCE_DURATION = 235L
+private const val FAILSAFE_DELAY_MS = 350L
+private const val POP_ON_FLING_DELAY = 160L
-/**
- * The min duration arrow remains on screen during a fling event.
- */
-private const val MIN_FLING_VELOCITY = 3000
+internal val VIBRATE_ACTIVATED_EFFECT =
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)
-/**
- * The amount of rubber banding we do for the vertical translation
- */
-private const val RUBBER_BAND_AMOUNT = 15
+internal val VIBRATE_DEACTIVATED_EFFECT =
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)
-private const val ARROW_APPEAR_STIFFNESS = 600f
-private const val ARROW_APPEAR_DAMPING_RATIO = 0.4f
-private const val ARROW_DISAPPEAR_STIFFNESS = 1200f
-private const val ARROW_DISAPPEAR_DAMPING_RATIO = SpringForce.DAMPING_RATIO_NO_BOUNCY
+private const val DEBUG = false
-/**
- * The interpolator used to rubber band
- */
-private val RUBBER_BAND_INTERPOLATOR = PathInterpolator(1.0f / 5.0f, 1.0f, 1.0f, 1.0f)
-
-private val DECELERATE_INTERPOLATOR = DecelerateInterpolator()
-
-private val DECELERATE_INTERPOLATOR_SLOW = DecelerateInterpolator(0.7f)
-
-class BackPanelController private constructor(
- context: Context,
- private val windowManager: WindowManager,
- private val viewConfiguration: ViewConfiguration,
- @Main private val mainHandler: Handler,
- private val vibratorHelper: VibratorHelper,
- private val configurationController: ConfigurationController,
- latencyTracker: LatencyTracker
-) : ViewController<BackPanel>(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin {
+class BackPanelController internal constructor(
+ context: Context,
+ private val windowManager: WindowManager,
+ private val viewConfiguration: ViewConfiguration,
+ @Main private val mainHandler: Handler,
+ private val vibratorHelper: VibratorHelper,
+ private val configurationController: ConfigurationController,
+ private val latencyTracker: LatencyTracker
+) : ViewController<BackPanel>(
+ BackPanel(
+ context,
+ latencyTracker
+ )
+), NavigationEdgeBackPlugin {
/**
* Injectable instance to create a new BackPanelController.
@@ -111,44 +93,44 @@
* BackPanelController, and we need to match EdgeBackGestureHandler's context.
*/
class Factory @Inject constructor(
- private val windowManager: WindowManager,
- private val viewConfiguration: ViewConfiguration,
- @Main private val mainHandler: Handler,
- private val vibratorHelper: VibratorHelper,
- private val configurationController: ConfigurationController,
- private val latencyTracker: LatencyTracker
+ private val windowManager: WindowManager,
+ private val viewConfiguration: ViewConfiguration,
+ @Main private val mainHandler: Handler,
+ private val vibratorHelper: VibratorHelper,
+ private val configurationController: ConfigurationController,
+ private val latencyTracker: LatencyTracker
) {
/** Construct a [BackPanelController]. */
fun create(context: Context): BackPanelController {
val backPanelController = BackPanelController(
- context,
- windowManager,
- viewConfiguration,
- mainHandler,
- vibratorHelper,
- configurationController,
- latencyTracker
+ context,
+ windowManager,
+ viewConfiguration,
+ mainHandler,
+ vibratorHelper,
+ configurationController,
+ latencyTracker
)
backPanelController.init()
return backPanelController
}
}
- private var params: EdgePanelParams = EdgePanelParams(resources)
- private var currentState: GestureState = GestureState.GONE
+ @VisibleForTesting
+ internal var params: EdgePanelParams = EdgePanelParams(resources)
+ @VisibleForTesting
+ internal var currentState: GestureState = GestureState.GONE
private var previousState: GestureState = GestureState.GONE
- // Phone should only vibrate the first time the arrow is activated
- private var hasHapticPlayed = false
-
// Screen attributes
private lateinit var layoutParams: WindowManager.LayoutParams
private val displaySize = Point()
private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback
-
+ private var previousXTranslationOnActiveOffset = 0f
private var previousXTranslation = 0f
private var totalTouchDelta = 0f
+ private var touchDeltaStartX = 0f
private var velocityTracker: VelocityTracker? = null
set(value) {
if (field != value) field?.recycle()
@@ -162,8 +144,18 @@
// The x,y position of the first touch event
private var startX = 0f
private var startY = 0f
+ private var startIsLeft: Boolean? = null
- private var gestureStartTime = 0L
+ private var gestureSinceActionDown = 0L
+ private var gestureEntryTime = 0L
+ private var gestureActiveTime = 0L
+ private var gestureInactiveOrEntryTime = 0L
+ private var gestureArrowStrokeVisibleTime = 0L
+
+ private val elapsedTimeSinceActionDown
+ get() = SystemClock.uptimeMillis() - gestureSinceActionDown
+ private val elapsedTimeSinceEntry
+ get() = SystemClock.uptimeMillis() - gestureEntryTime
// Whether the current gesture has moved a sufficiently large amount,
// so that we can unambiguously start showing the ENTRY animation
@@ -171,7 +163,7 @@
private val failsafeRunnable = Runnable { onFailsafe() }
- private enum class GestureState {
+ internal enum class GestureState {
/* Arrow is off the screen and invisible */
GONE,
@@ -192,17 +184,6 @@
/* back action currently cancelling, arrow soon to be GONE */
CANCELLED;
-
- /**
- * @return true if the current state responds to touch move events in some way (e.g. by
- * stretching the back indicator)
- */
- fun isInteractive(): Boolean {
- return when (this) {
- ENTRY, ACTIVE, INACTIVE -> true
- GONE, FLUNG, COMMITTED, CANCELLED -> false
- }
- }
}
/**
@@ -210,50 +191,43 @@
* runnable is not called if the animation is cancelled
*/
inner class DelayedOnAnimationEndListener internal constructor(
- private val handler: Handler,
- private val runnable: Runnable,
- private val minDuration: Long
+ private val handler: Handler,
+ private val runnableDelay: Long,
+ val runnable: Runnable,
) : DynamicAnimation.OnAnimationEndListener {
+
override fun onAnimationEnd(
- animation: DynamicAnimation<*>,
- canceled: Boolean,
- value: Float,
- velocity: Float
+ animation: DynamicAnimation<*>,
+ canceled: Boolean,
+ value: Float,
+ velocity: Float
) {
animation.removeEndListener(this)
+
if (!canceled) {
- // Total elapsed time of the gesture and the animation
- val totalElapsedTime = SystemClock.uptimeMillis() - gestureStartTime
+
// The delay between finishing this animation and starting the runnable
- val delay = max(0, minDuration - totalElapsedTime)
+ val delay = max(0, runnableDelay - elapsedTimeSinceEntry)
+
handler.postDelayed(runnable, delay)
}
}
- internal fun runNow() {
- runnable.run()
- }
+ internal fun run() = runnable.run()
}
- private val setCommittedEndListener =
- DelayedOnAnimationEndListener(
- mainHandler,
- { updateArrowState(GestureState.COMMITTED) },
- minDuration = FLING_MIN_APPEARANCE_DURATION
- )
+ private val onEndSetCommittedStateListener = DelayedOnAnimationEndListener(mainHandler, 0L) {
+ updateArrowState(GestureState.COMMITTED)
+ }
- private val setGoneEndListener =
- DelayedOnAnimationEndListener(
- mainHandler,
- {
+
+ private val onEndSetGoneStateListener =
+ DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) {
cancelFailsafe()
updateArrowState(GestureState.GONE)
- },
- minDuration = 0
- )
+ }
- // Vibration
- private var vibrationTime: Long = 0
+ private val playAnimationThenSetGoneOnAlphaEnd = Runnable { playAnimationThenSetGoneEnd() }
// Minimum of the screen's width or the predefined threshold
private var fullyStretchedThreshold = 0f
@@ -280,7 +254,7 @@
updateConfiguration()
updateArrowDirection(configurationController.isLayoutRtl)
updateArrowState(GestureState.GONE, force = true)
- updateRestingArrowDimens(animated = false, currentState)
+ updateRestingArrowDimens()
configurationController.addCallback(configurationListener)
}
@@ -297,22 +271,57 @@
velocityTracker!!.addMovement(event)
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
- resetOnDown()
+ gestureSinceActionDown = SystemClock.uptimeMillis()
+ cancelAllPendingAnimations()
startX = event.x
startY = event.y
- gestureStartTime = SystemClock.uptimeMillis()
+
+ updateArrowState(GestureState.GONE)
+ updateYStartPosition(startY)
+
+ // reset animation properties
+ startIsLeft = mView.isLeftPanel
+ hasPassedDragSlop = false
+ mView.resetStretch()
}
MotionEvent.ACTION_MOVE -> {
- // only go to the ENTRY state after some minimum motion has occurred
if (dragSlopExceeded(event.x, startX)) {
handleMoveEvent(event)
}
}
MotionEvent.ACTION_UP -> {
- if (currentState == GestureState.ACTIVE) {
- updateArrowState(if (isFlung()) GestureState.FLUNG else GestureState.COMMITTED)
- } else if (currentState != GestureState.GONE) { // if invisible, skip animation
- updateArrowState(GestureState.CANCELLED)
+ when (currentState) {
+ GestureState.ENTRY -> {
+ if (isFlungAwayFromEdge(endX = event.x)) {
+ updateArrowState(GestureState.ACTIVE)
+ updateArrowState(GestureState.FLUNG)
+ } else {
+ updateArrowState(GestureState.CANCELLED)
+ }
+ }
+ GestureState.INACTIVE -> {
+ if (isFlungAwayFromEdge(endX = event.x)) {
+ mainHandler.postDelayed(MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION) {
+ updateArrowState(GestureState.ACTIVE)
+ updateArrowState(GestureState.FLUNG)
+ }
+ } else {
+ updateArrowState(GestureState.CANCELLED)
+ }
+ }
+ GestureState.ACTIVE -> {
+ if (elapsedTimeSinceEntry < MIN_DURATION_CONSIDERED_AS_FLING) {
+ updateArrowState(GestureState.FLUNG)
+ } else {
+ updateArrowState(GestureState.COMMITTED)
+ }
+ }
+ GestureState.GONE,
+ GestureState.FLUNG,
+ GestureState.COMMITTED,
+ GestureState.CANCELLED -> {
+ updateArrowState(GestureState.CANCELLED)
+ }
}
velocityTracker = null
}
@@ -326,6 +335,14 @@
}
}
+ private fun cancelAllPendingAnimations() {
+ cancelFailsafe()
+ mView.cancelAnimations()
+ mainHandler.removeCallbacks(onEndSetCommittedStateListener.runnable)
+ mainHandler.removeCallbacks(onEndSetGoneStateListener.runnable)
+ mainHandler.removeCallbacks(playAnimationThenSetGoneOnAlphaEnd)
+ }
+
/**
* Returns false until the current gesture exceeds the touch slop threshold,
* and returns true thereafter (we reset on the subsequent back gesture).
@@ -336,7 +353,7 @@
private fun dragSlopExceeded(curX: Float, startX: Float): Boolean {
if (hasPassedDragSlop) return true
- if (abs(curX - startX) > viewConfiguration.scaledTouchSlop) {
+ if (abs(curX - startX) > viewConfiguration.scaledEdgeSlop) {
// Reset the arrow to the side
updateArrowState(GestureState.ENTRY)
@@ -349,39 +366,46 @@
}
private fun updateArrowStateOnMove(yTranslation: Float, xTranslation: Float) {
- if (!currentState.isInteractive())
- return
+
+ val isWithinYActivationThreshold = xTranslation * 2 >= yTranslation
when (currentState) {
- // Check if we should transition from ENTRY to ACTIVE
- GestureState.ENTRY ->
- if (xTranslation > params.swipeTriggerThreshold) {
+ GestureState.ENTRY -> {
+ if (xTranslation > params.staticTriggerThreshold) {
updateArrowState(GestureState.ACTIVE)
}
+ }
+ GestureState.ACTIVE -> {
+ val isPastDynamicDeactivationThreshold =
+ totalTouchDelta <= params.deactivationSwipeTriggerThreshold
+ val isMinDurationElapsed =
+ elapsedTimeSinceActionDown > MIN_DURATION_ACTIVE_ANIMATION
- // Abort if we had continuous motion toward the edge for a while, OR the direction
- // in Y is bigger than X * 2
- GestureState.ACTIVE ->
- if ((totalTouchDelta < 0 && -totalTouchDelta > params.minDeltaForSwitch) ||
- (yTranslation > xTranslation * 2)
+ if (isMinDurationElapsed && (!isWithinYActivationThreshold ||
+ isPastDynamicDeactivationThreshold)
) {
updateArrowState(GestureState.INACTIVE)
}
+ }
+ GestureState.INACTIVE -> {
+ val isPastStaticThreshold =
+ xTranslation > params.staticTriggerThreshold
+ val isPastDynamicReactivationThreshold = totalTouchDelta > 0 &&
+ abs(totalTouchDelta) >=
+ params.reactivationTriggerThreshold
- // Re-activate if we had continuous motion away from the edge for a while
- GestureState.INACTIVE ->
- if (totalTouchDelta > 0 && totalTouchDelta > params.minDeltaForSwitch) {
+ if (isPastStaticThreshold &&
+ isPastDynamicReactivationThreshold &&
+ isWithinYActivationThreshold
+ ) {
updateArrowState(GestureState.ACTIVE)
}
-
- // By default assume the current direction is kept
+ }
else -> {}
}
}
private fun handleMoveEvent(event: MotionEvent) {
- if (!currentState.isInteractive())
- return
val x = event.x
val y = event.y
@@ -401,23 +425,44 @@
previousXTranslation = xTranslation
if (abs(xDelta) > 0) {
- if (sign(xDelta) == sign(totalTouchDelta)) {
+ val range =
+ params.run { deactivationSwipeTriggerThreshold..reactivationTriggerThreshold }
+ val isTouchInContinuousDirection =
+ sign(xDelta) == sign(totalTouchDelta) || totalTouchDelta in range
+
+ if (isTouchInContinuousDirection) {
// Direction has NOT changed, so keep counting the delta
totalTouchDelta += xDelta
} else {
// Direction has changed, so reset the delta
totalTouchDelta = xDelta
+ touchDeltaStartX = x
}
}
updateArrowStateOnMove(yTranslation, xTranslation)
+
when (currentState) {
- GestureState.ACTIVE ->
- stretchActiveBackIndicator(fullScreenStretchProgress(xTranslation))
- GestureState.ENTRY ->
- stretchEntryBackIndicator(preThresholdStretchProgress(xTranslation))
- GestureState.INACTIVE ->
- mView.resetStretch()
+ GestureState.ACTIVE -> {
+ stretchActiveBackIndicator(fullScreenProgress(xTranslation))
+ }
+ GestureState.ENTRY -> {
+ val progress = staticThresholdProgress(xTranslation)
+ stretchEntryBackIndicator(progress)
+
+ params.arrowStrokeAlphaSpring.get(progress).takeIf { it.isNewState }?.let {
+ mView.popArrowAlpha(0f, it.value)
+ }
+ }
+ GestureState.INACTIVE -> {
+ val progress = reactivationThresholdProgress(totalTouchDelta)
+ stretchInactiveBackIndicator(progress)
+
+ params.arrowStrokeAlphaSpring.get(progress).takeIf { it.isNewState }?.let {
+ gestureArrowStrokeVisibleTime = SystemClock.uptimeMillis()
+ mView.popArrowAlpha(0f, it.value)
+ }
+ }
else -> {}
}
@@ -428,21 +473,22 @@
private fun setVerticalTranslation(yOffset: Float) {
val yTranslation = abs(yOffset)
val maxYOffset = (mView.height - params.entryIndicator.backgroundDimens.height) / 2f
- val yProgress = saturate(yTranslation / (maxYOffset * RUBBER_BAND_AMOUNT))
- mView.animateVertically(
- RUBBER_BAND_INTERPOLATOR.getInterpolation(yProgress) * maxYOffset *
+ val rubberbandAmount = 15f
+ val yProgress = MathUtils.saturate(yTranslation / (maxYOffset * rubberbandAmount))
+ val yPosition = params.translationInterpolator.getInterpolation(yProgress) *
+ maxYOffset *
sign(yOffset)
- )
+ mView.animateVertically(yPosition)
}
/**
- * @return the relative position of the drag from the time after the arrow is activated until
+ * Tracks the relative position of the drag from the time after the arrow is activated until
* the arrow is fully stretched (between 0.0 - 1.0f)
*/
- private fun fullScreenStretchProgress(xTranslation: Float): Float {
- return saturate(
- (xTranslation - params.swipeTriggerThreshold) /
- (fullyStretchedThreshold - params.swipeTriggerThreshold)
+ private fun fullScreenProgress(xTranslation: Float): Float {
+ return MathUtils.saturate(
+ (xTranslation - previousXTranslationOnActiveOffset) /
+ (fullyStretchedThreshold - previousXTranslationOnActiveOffset)
)
}
@@ -450,26 +496,74 @@
* Tracks the relative position of the drag from the entry until the threshold where the arrow
* activates (between 0.0 - 1.0f)
*/
- private fun preThresholdStretchProgress(xTranslation: Float): Float {
- return saturate(xTranslation / params.swipeTriggerThreshold)
+ private fun staticThresholdProgress(xTranslation: Float): Float {
+ return MathUtils.saturate(xTranslation / params.staticTriggerThreshold)
+ }
+
+ private fun reactivationThresholdProgress(totalTouchDelta: Float): Float {
+ return MathUtils.saturate(totalTouchDelta / params.reactivationTriggerThreshold)
}
private fun stretchActiveBackIndicator(progress: Float) {
- val rubberBandIterpolation = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
mView.setStretch(
- horizontalTranslationStretchAmount = rubberBandIterpolation,
- arrowStretchAmount = rubberBandIterpolation,
- backgroundWidthStretchAmount = DECELERATE_INTERPOLATOR_SLOW.getInterpolation(progress),
- params.fullyStretchedIndicator
+ horizontalTranslationStretchAmount = params.translationInterpolator
+ .getInterpolation(progress),
+ arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
+ backgroundWidthStretchAmount = params.activeWidthInterpolator
+ .getInterpolation(progress),
+ backgroundAlphaStretchAmount = 1f,
+ backgroundHeightStretchAmount = 1f,
+ arrowAlphaStretchAmount = 1f,
+ edgeCornerStretchAmount = 1f,
+ farCornerStretchAmount = 1f,
+ fullyStretchedDimens = params.fullyStretchedIndicator
)
}
private fun stretchEntryBackIndicator(progress: Float) {
mView.setStretch(
- horizontalTranslationStretchAmount = 0f,
- arrowStretchAmount = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress),
- backgroundWidthStretchAmount = DECELERATE_INTERPOLATOR.getInterpolation(progress),
- params.preThresholdIndicator
+ horizontalTranslationStretchAmount = 0f,
+ arrowStretchAmount = params.arrowAngleInterpolator
+ .getInterpolation(progress),
+ backgroundWidthStretchAmount = params.entryWidthInterpolator
+ .getInterpolation(progress),
+ backgroundHeightStretchAmount = params.heightInterpolator
+ .getInterpolation(progress),
+ backgroundAlphaStretchAmount = 1f,
+ arrowAlphaStretchAmount = params.arrowStrokeAlphaInterpolator.get(progress).value,
+ edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
+ farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
+ fullyStretchedDimens = params.preThresholdIndicator
+ )
+ }
+
+ private var previousPreThresholdWidthInterpolator = params.entryWidthTowardsEdgeInterpolator
+ fun preThresholdWidthStretchAmount(progress: Float): Float {
+ val interpolator = run {
+ val isPastSlop = abs(totalTouchDelta) > ViewConfiguration.get(context).scaledTouchSlop
+ if (isPastSlop) {
+ if (totalTouchDelta > 0) {
+ params.entryWidthInterpolator
+ } else params.entryWidthTowardsEdgeInterpolator
+ } else {
+ previousPreThresholdWidthInterpolator
+ }.also { previousPreThresholdWidthInterpolator = it }
+ }
+ return interpolator.getInterpolation(progress).coerceAtLeast(0f)
+ }
+
+ private fun stretchInactiveBackIndicator(progress: Float) {
+ mView.setStretch(
+ horizontalTranslationStretchAmount = 0f,
+ arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
+ backgroundWidthStretchAmount = preThresholdWidthStretchAmount(progress),
+ backgroundHeightStretchAmount = params.heightInterpolator
+ .getInterpolation(progress),
+ backgroundAlphaStretchAmount = 1f,
+ arrowAlphaStretchAmount = params.arrowStrokeAlphaInterpolator.get(progress).value,
+ edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
+ farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
+ fullyStretchedDimens = params.preThresholdIndicator
)
}
@@ -487,8 +581,7 @@
}
}
- override fun setInsets(insetLeft: Int, insetRight: Int) {
- }
+ override fun setInsets(insetLeft: Int, insetRight: Int) = Unit
override fun setBackCallback(callback: NavigationEdgeBackPlugin.BackCallback) {
backCallback = callback
@@ -500,65 +593,57 @@
}
override fun setMotionEventsHandler(motionEventsHandler: MotionEventsHandlerBase?) {
- TODO("Not yet implemented")
+ // TODO(255697805): Integrate MotionEventHandler for trackpad.
}
- private fun isFlung() = velocityTracker!!.run {
- computeCurrentVelocity(1000)
- abs(xVelocity) > MIN_FLING_VELOCITY
+ private fun isDragAwayFromEdge(velocityPxPerSecThreshold: Int = 0) = velocityTracker!!.run {
+ computeCurrentVelocity(PX_PER_SEC)
+ val velocity = xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1)
+ velocity > velocityPxPerSecThreshold
}
- private fun playFlingBackAnimation() {
- playAnimation(setCommittedEndListener)
+ private fun isFlungAwayFromEdge(endX: Float, startX: Float = touchDeltaStartX): Boolean {
+ val minDistanceConsideredForFling = ViewConfiguration.get(context).scaledTouchSlop
+ val flingDistance = abs(endX - startX)
+ val isPastFlingVelocity = isDragAwayFromEdge(
+ velocityPxPerSecThreshold =
+ ViewConfiguration.get(context).scaledMinimumFlingVelocity)
+ return flingDistance > minDistanceConsideredForFling && isPastFlingVelocity
}
- private fun playCommitBackAnimation() {
- // Check if we should vibrate again
- if (previousState != GestureState.FLUNG) {
- velocityTracker!!.computeCurrentVelocity(1000)
- val isSlow = abs(velocityTracker!!.xVelocity) < 500
- val hasNotVibratedRecently =
- SystemClock.uptimeMillis() - vibrationTime >= GESTURE_DURATION_FOR_CLICK_MS
- if (isSlow || hasNotVibratedRecently) {
- vibratorHelper.vibrate(VibrationEffect.EFFECT_CLICK)
- }
- }
- // Dispatch the actual back trigger
- if (DEBUG) Log.d(TAG, "playCommitBackAnimation() invoked triggerBack() on backCallback")
- backCallback.triggerBack()
-
- playAnimation(setGoneEndListener)
- }
-
- private fun playCancelBackAnimation() {
- backCallback.cancelBack()
- playAnimation(setGoneEndListener)
- }
-
- /**
- * @return true if the animation is running, false otherwise. Some transitions don't animate
- */
- private fun playAnimation(endListener: DelayedOnAnimationEndListener) {
- updateRestingArrowDimens(animated = true, currentState)
-
- if (!mView.addEndListener(endListener)) {
+ private fun playHorizontalAnimationThen(onEnd: DelayedOnAnimationEndListener) {
+ updateRestingArrowDimens()
+ if (!mView.addAnimationEndListener(mView.horizontalTranslation, onEnd)) {
scheduleFailsafe()
}
}
- private fun resetOnDown() {
- hasPassedDragSlop = false
- hasHapticPlayed = false
- totalTouchDelta = 0f
- vibrationTime = 0
- cancelFailsafe()
+ private fun playAnimationThenSetGoneEnd() {
+ updateRestingArrowDimens()
+ if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) {
+ scheduleFailsafe()
+ }
}
- private fun updateYPosition(touchY: Float) {
+ private fun playWithBackgroundWidthAnimation(
+ onEnd: DelayedOnAnimationEndListener,
+ delay: Long = 0L
+ ) {
+ if (delay == 0L) {
+ updateRestingArrowDimens()
+ if (!mView.addAnimationEndListener(mView.backgroundWidth, onEnd)) {
+ scheduleFailsafe()
+ }
+ } else {
+ mainHandler.postDelayed(delay) { playWithBackgroundWidthAnimation(onEnd, delay = 0L) }
+ }
+ }
+
+ private fun updateYStartPosition(touchY: Float) {
var yPosition = touchY - params.fingerOffset
yPosition = max(yPosition, params.minArrowYPosition.toFloat())
yPosition -= layoutParams.height / 2.0f
- layoutParams.y = constrain(yPosition.toInt(), 0, displaySize.y)
+ layoutParams.y = MathUtils.constrain(yPosition.toInt(), 0, displaySize.y)
}
override fun setDisplaySize(displaySize: Point) {
@@ -569,53 +654,135 @@
/**
* Updates resting arrow and background size not accounting for stretch
*/
- private fun updateRestingArrowDimens(animated: Boolean, currentState: GestureState) {
- if (animated) {
- when (currentState) {
- GestureState.ENTRY, GestureState.ACTIVE, GestureState.FLUNG ->
- mView.setArrowStiffness(ARROW_APPEAR_STIFFNESS, ARROW_APPEAR_DAMPING_RATIO)
- GestureState.CANCELLED -> mView.fadeOut()
- else ->
- mView.setArrowStiffness(
- ARROW_DISAPPEAR_STIFFNESS,
- ARROW_DISAPPEAR_DAMPING_RATIO
- )
+ private fun updateRestingArrowDimens() {
+ when (currentState) {
+ GestureState.GONE,
+ GestureState.ENTRY -> {
+ mView.setSpring(
+ arrowLength = params.entryIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.entryIndicator.arrowDimens.heightSpring,
+ arrowAlpha = params.entryIndicator.arrowDimens.alphaSpring,
+ scale = params.entryIndicator.scaleSpring,
+ verticalTranslation = params.entryIndicator.verticalTranslationSpring,
+ horizontalTranslation = params.entryIndicator.horizontalTranslationSpring,
+ backgroundAlpha = params.entryIndicator.backgroundDimens.alphaSpring,
+ backgroundWidth = params.entryIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.entryIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius = params.entryIndicator.backgroundDimens
+ .edgeCornerRadiusSpring,
+ backgroundFarCornerRadius = params.entryIndicator.backgroundDimens
+ .farCornerRadiusSpring,
+ )
}
+ GestureState.INACTIVE -> {
+ mView.setSpring(
+ arrowLength = params.preThresholdIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.preThresholdIndicator.arrowDimens.heightSpring,
+ horizontalTranslation = params.preThresholdIndicator
+ .horizontalTranslationSpring,
+ scale = params.preThresholdIndicator.scaleSpring,
+ backgroundWidth = params.preThresholdIndicator.backgroundDimens
+ .widthSpring,
+ backgroundHeight = params.preThresholdIndicator.backgroundDimens
+ .heightSpring,
+ backgroundEdgeCornerRadius = params.preThresholdIndicator.backgroundDimens
+ .edgeCornerRadiusSpring,
+ backgroundFarCornerRadius = params.preThresholdIndicator.backgroundDimens
+ .farCornerRadiusSpring,
+ )
+ }
+ GestureState.ACTIVE -> {
+ mView.setSpring(
+ arrowLength = params.activeIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.activeIndicator.arrowDimens.heightSpring,
+ scale = params.activeIndicator.scaleSpring,
+ horizontalTranslation = params.activeIndicator.horizontalTranslationSpring,
+ backgroundWidth = params.activeIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.activeIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius = params.activeIndicator.backgroundDimens
+ .edgeCornerRadiusSpring,
+ backgroundFarCornerRadius = params.activeIndicator.backgroundDimens
+ .farCornerRadiusSpring,
+ )
+ }
+ GestureState.FLUNG -> {
+ mView.setSpring(
+ arrowLength = params.flungIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.flungIndicator.arrowDimens.heightSpring,
+ backgroundWidth = params.flungIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.flungIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius = params.flungIndicator.backgroundDimens
+ .edgeCornerRadiusSpring,
+ backgroundFarCornerRadius = params.flungIndicator.backgroundDimens
+ .farCornerRadiusSpring,
+ )
+ }
+ GestureState.COMMITTED -> {
+ mView.setSpring(
+ arrowLength = params.committedIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.committedIndicator.arrowDimens.heightSpring,
+ scale = params.committedIndicator.scaleSpring,
+ backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius = params.committedIndicator.backgroundDimens
+ .edgeCornerRadiusSpring,
+ backgroundFarCornerRadius = params.committedIndicator.backgroundDimens
+ .farCornerRadiusSpring,
+ )
+ }
+ else -> {}
}
+
mView.setRestingDimens(
- restingParams = EdgePanelParams.BackIndicatorDimens(
- horizontalTranslation = when (currentState) {
- GestureState.GONE -> -params.activeIndicator.backgroundDimens.width
- // Position the committed arrow slightly further off the screen so we do not
- // see part of it bouncing
- GestureState.COMMITTED ->
- -params.activeIndicator.backgroundDimens.width * 1.5f
- GestureState.FLUNG -> params.fullyStretchedIndicator.horizontalTranslation
- GestureState.ACTIVE -> params.activeIndicator.horizontalTranslation
- GestureState.ENTRY, GestureState.INACTIVE, GestureState.CANCELLED ->
- params.entryIndicator.horizontalTranslation
- },
- arrowDimens = when (currentState) {
- GestureState.ACTIVE, GestureState.INACTIVE,
- GestureState.COMMITTED, GestureState.FLUNG -> params.activeIndicator.arrowDimens
- GestureState.CANCELLED -> params.cancelledArrowDimens
- GestureState.GONE, GestureState.ENTRY -> params.entryIndicator.arrowDimens
- },
- backgroundDimens = when (currentState) {
- GestureState.GONE, GestureState.ENTRY -> params.entryIndicator.backgroundDimens
- else ->
- params.activeIndicator.backgroundDimens.copy(
- edgeCornerRadius =
- if (currentState == GestureState.INACTIVE ||
- currentState == GestureState.CANCELLED
- )
- params.cancelledEdgeCornerRadius
- else
- params.activeIndicator.backgroundDimens.edgeCornerRadius
- )
- }
- ),
- animate = animated
+ animate = !(currentState == GestureState.FLUNG ||
+ currentState == GestureState.COMMITTED),
+ restingParams = EdgePanelParams.BackIndicatorDimens(
+ scale = when (currentState) {
+ GestureState.ACTIVE,
+ GestureState.FLUNG,
+ -> params.activeIndicator.scale
+ GestureState.COMMITTED -> params.committedIndicator.scale
+ else -> params.preThresholdIndicator.scale
+ },
+ scalePivotX = when (currentState) {
+ GestureState.GONE,
+ GestureState.ENTRY,
+ GestureState.INACTIVE,
+ GestureState.CANCELLED -> params.preThresholdIndicator.scalePivotX
+ else -> params.committedIndicator.scalePivotX
+ },
+ horizontalTranslation = when (currentState) {
+ GestureState.GONE -> {
+ params.activeIndicator.backgroundDimens.width?.times(-1)
+ }
+ GestureState.ENTRY,
+ GestureState.INACTIVE -> params.entryIndicator.horizontalTranslation
+ GestureState.FLUNG -> params.activeIndicator.horizontalTranslation
+ GestureState.ACTIVE -> params.activeIndicator.horizontalTranslation
+ GestureState.CANCELLED -> {
+ params.cancelledIndicator.horizontalTranslation
+ }
+ else -> null
+ },
+ arrowDimens = when (currentState) {
+ GestureState.GONE,
+ GestureState.ENTRY,
+ GestureState.INACTIVE -> params.entryIndicator.arrowDimens
+ GestureState.ACTIVE -> params.activeIndicator.arrowDimens
+ GestureState.FLUNG,
+ GestureState.COMMITTED -> params.committedIndicator.arrowDimens
+ GestureState.CANCELLED -> params.cancelledIndicator.arrowDimens
+ },
+ backgroundDimens = when (currentState) {
+ GestureState.GONE,
+ GestureState.ENTRY,
+ GestureState.INACTIVE -> params.entryIndicator.backgroundDimens
+ GestureState.ACTIVE -> params.activeIndicator.backgroundDimens
+ GestureState.FLUNG -> params.activeIndicator.backgroundDimens
+ GestureState.COMMITTED -> params.committedIndicator.backgroundDimens
+ GestureState.CANCELLED -> params.cancelledIndicator.backgroundDimens
+ }
+ )
)
}
@@ -628,42 +795,123 @@
private fun updateArrowState(newState: GestureState, force: Boolean = false) {
if (!force && currentState == newState) return
- if (DEBUG) Log.d(TAG, "updateArrowState $currentState -> $newState")
previousState = currentState
currentState = newState
- if (currentState == GestureState.GONE) {
- mView.cancelAlphaAnimations()
- mView.visibility = View.GONE
- } else {
- mView.visibility = View.VISIBLE
+
+ when (currentState) {
+ GestureState.CANCELLED -> {
+ backCallback.cancelBack()
+ }
+ GestureState.FLUNG,
+ GestureState.COMMITTED -> {
+ // When flung, trigger back immediately but don't fire again
+ // once state resolves to committed.
+ if (previousState != GestureState.FLUNG) backCallback.triggerBack()
+ }
+ GestureState.ENTRY,
+ GestureState.INACTIVE -> {
+ backCallback.setTriggerBack(false)
+ }
+ GestureState.ACTIVE -> {
+ backCallback.setTriggerBack(true)
+ }
+ GestureState.GONE -> { }
}
when (currentState) {
// Transitioning to GONE never animates since the arrow is (presumably) already off the
// screen
- GestureState.GONE -> updateRestingArrowDimens(animated = false, currentState)
+ GestureState.GONE -> {
+ updateRestingArrowDimens()
+ mView.isVisible = false
+ }
GestureState.ENTRY -> {
- updateYPosition(startY)
- updateRestingArrowDimens(animated = true, currentState)
+ mView.isVisible = true
+
+ updateRestingArrowDimens()
+ gestureEntryTime = SystemClock.uptimeMillis()
+ gestureInactiveOrEntryTime = SystemClock.uptimeMillis()
}
GestureState.ACTIVE -> {
- updateRestingArrowDimens(animated = true, currentState)
- // Vibrate the first time we transition to ACTIVE
- if (!hasHapticPlayed) {
- hasHapticPlayed = true
- vibrationTime = SystemClock.uptimeMillis()
- vibratorHelper.vibrate(VibrationEffect.EFFECT_TICK)
+ previousXTranslationOnActiveOffset = previousXTranslation
+ gestureActiveTime = SystemClock.uptimeMillis()
+
+ updateRestingArrowDimens()
+
+ vibratorHelper.cancel()
+ mainHandler.postDelayed(10L) {
+ vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT)
+ }
+
+ val startingVelocity = convertVelocityToSpringStartingVelocity(
+ valueOnFastVelocity = 0f,
+ valueOnSlowVelocity = if (previousState == GestureState.ENTRY) 2f else 4.5f
+ )
+
+ when (previousState) {
+ GestureState.ENTRY,
+ GestureState.INACTIVE -> {
+ mView.popOffEdge(startingVelocity)
+ }
+ GestureState.COMMITTED -> {
+ // if previous state was committed then this activation
+ // was due to a quick second swipe. Don't pop the arrow this time
+ }
+ else -> { }
}
}
+
GestureState.INACTIVE -> {
- updateRestingArrowDimens(animated = true, currentState)
+ gestureInactiveOrEntryTime = SystemClock.uptimeMillis()
+
+ val startingVelocity = convertVelocityToSpringStartingVelocity(
+ valueOnFastVelocity = -1.05f,
+ valueOnSlowVelocity = -1.50f
+ )
+ mView.popOffEdge(startingVelocity)
+
+ vibratorHelper.vibrate(VIBRATE_DEACTIVATED_EFFECT)
+ updateRestingArrowDimens()
}
- GestureState.FLUNG -> playFlingBackAnimation()
- GestureState.COMMITTED -> playCommitBackAnimation()
- GestureState.CANCELLED -> playCancelBackAnimation()
+ GestureState.FLUNG -> {
+ mainHandler.postDelayed(POP_ON_FLING_DELAY) { mView.popScale(1.9f) }
+ playHorizontalAnimationThen(onEndSetCommittedStateListener)
+ }
+ GestureState.COMMITTED -> {
+ if (previousState == GestureState.FLUNG) {
+ playAnimationThenSetGoneEnd()
+ } else {
+ mView.popScale(3f)
+ mainHandler.postDelayed(
+ playAnimationThenSetGoneOnAlphaEnd,
+ MIN_DURATION_COMMITTED_ANIMATION
+ )
+ }
+ }
+ GestureState.CANCELLED -> {
+ val delay = max(0, MIN_DURATION_CANCELLED_ANIMATION - elapsedTimeSinceEntry)
+ playWithBackgroundWidthAnimation(onEndSetGoneStateListener, delay)
+
+ params.arrowStrokeAlphaSpring.get(0f).takeIf { it.isNewState }?.let {
+ mView.popArrowAlpha(0f, it.value)
+ }
+ mainHandler.postDelayed(10L) { vibratorHelper.cancel() }
+ }
}
}
+ private fun convertVelocityToSpringStartingVelocity(
+ valueOnFastVelocity: Float,
+ valueOnSlowVelocity: Float,
+ ): Float {
+ val factor = velocityTracker?.run {
+ computeCurrentVelocity(PX_PER_MS)
+ MathUtils.smoothStep(0f, 3f, abs(xVelocity))
+ } ?: valueOnFastVelocity
+
+ return MathUtils.lerp(valueOnFastVelocity, valueOnSlowVelocity, 1 - factor)
+ }
+
private fun scheduleFailsafe() {
if (!ENABLE_FAILSAFE) return
cancelFailsafe()
@@ -690,24 +938,24 @@
init {
if (DEBUG) mView.drawDebugInfo = { canvas ->
val debugStrings = listOf(
- "$currentState",
- "startX=$startX",
- "startY=$startY",
- "xDelta=${"%.1f".format(totalTouchDelta)}",
- "xTranslation=${"%.1f".format(previousXTranslation)}",
- "pre=${"%.0f".format(preThresholdStretchProgress(previousXTranslation) * 100)}%",
- "post=${"%.0f".format(fullScreenStretchProgress(previousXTranslation) * 100)}%"
+ "$currentState",
+ "startX=$startX",
+ "startY=$startY",
+ "xDelta=${"%.1f".format(totalTouchDelta)}",
+ "xTranslation=${"%.1f".format(previousXTranslation)}",
+ "pre=${"%.0f".format(staticThresholdProgress(previousXTranslation) * 100)}%",
+ "post=${"%.0f".format(fullScreenProgress(previousXTranslation) * 100)}%"
)
val debugPaint = Paint().apply {
color = Color.WHITE
}
val debugInfoBottom = debugStrings.size * 32f + 4f
canvas.drawRect(
- 4f,
- 4f,
- canvas.width.toFloat(),
- debugStrings.size * 32f + 4f,
- debugPaint
+ 4f,
+ 4f,
+ canvas.width.toFloat(),
+ debugStrings.size * 32f + 4f,
+ debugPaint
)
debugPaint.apply {
color = Color.BLACK
@@ -733,9 +981,71 @@
canvas.drawLine(x, debugInfoBottom, x, canvas.height.toFloat(), debugPaint)
}
- drawVerticalLine(x = params.swipeTriggerThreshold, color = Color.BLUE)
+ drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE)
+ drawVerticalLine(x = params.deactivationSwipeTriggerThreshold, color = Color.BLUE)
drawVerticalLine(x = startX, color = Color.GREEN)
drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY)
}
}
}
+
+/**
+ * In addition to a typical step function which returns one or two
+ * values based on a threshold, `Step` also gracefully handles quick
+ * changes in input near the threshold value that would typically
+ * result in the output rapidly changing.
+ *
+ * In the context of Back arrow, the arrow's stroke opacity should
+ * always appear transparent or opaque. Using a typical Step function,
+ * this would resulting in a flickering appearance as the output would
+ * change rapidly. `Step` addresses this by moving the threshold after
+ * it is crossed so it cannot be easily crossed again with small changes
+ * in touch events.
+ */
+class Step<T>(
+ private val threshold: Float,
+ private val factor: Float = 1.1f,
+ private val postThreshold: T,
+ private val preThreshold: T
+) {
+
+ data class Value<T>(val value: T, val isNewState: Boolean)
+
+ private val lowerFactor = 2 - factor
+
+ private lateinit var startValue: Value<T>
+ private lateinit var previousValue: Value<T>
+ private var hasCrossedUpperBoundAtLeastOnce = false
+ private var progress: Float = 0f
+
+ init {
+ reset()
+ }
+
+ fun reset() {
+ hasCrossedUpperBoundAtLeastOnce = false
+ progress = 0f
+ startValue = Value(preThreshold, false)
+ previousValue = startValue
+ }
+
+ fun get(progress: Float): Value<T> {
+ this.progress = progress
+
+ val hasCrossedUpperBound = progress > threshold * factor
+ val hasCrossedLowerBound = progress > threshold * lowerFactor
+
+ return when {
+ hasCrossedUpperBound && !hasCrossedUpperBoundAtLeastOnce -> {
+ hasCrossedUpperBoundAtLeastOnce = true
+ Value(postThreshold, true)
+ }
+ hasCrossedLowerBound -> previousValue.copy(isNewState = false)
+ hasCrossedUpperBoundAtLeastOnce -> {
+ hasCrossedUpperBoundAtLeastOnce = false
+ Value(preThreshold, true)
+ }
+ else -> startValue
+ }.also { previousValue = it }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
index d56537b..0c00022 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
@@ -1,52 +1,82 @@
package com.android.systemui.navigationbar.gestural
import android.content.res.Resources
+import android.util.TypedValue
+import androidx.core.animation.PathInterpolator
+import androidx.dynamicanimation.animation.SpringForce
import com.android.systemui.R
data class EdgePanelParams(private var resources: Resources) {
data class ArrowDimens(
- val length: Float = 0f,
- val height: Float = 0f
+ val length: Float = 0f,
+ val height: Float = 0f,
+ val alpha: Float = 0f,
+ var alphaSpring: SpringForce? = null,
+ val heightSpring: SpringForce? = null,
+ val lengthSpring: SpringForce? = null,
)
data class BackgroundDimens(
- val width: Float = 0f,
- val height: Float = 0f,
- val edgeCornerRadius: Float = 0f,
- val farCornerRadius: Float = 0f
+ val width: Float? = 0f,
+ val height: Float = 0f,
+ val edgeCornerRadius: Float = 0f,
+ val farCornerRadius: Float = 0f,
+ val alpha: Float = 0f,
+ val widthSpring: SpringForce? = null,
+ val heightSpring: SpringForce? = null,
+ val farCornerRadiusSpring: SpringForce? = null,
+ val edgeCornerRadiusSpring: SpringForce? = null,
+ val alphaSpring: SpringForce? = null,
)
data class BackIndicatorDimens(
- val horizontalTranslation: Float = 0f,
- val arrowDimens: ArrowDimens = ArrowDimens(),
- val backgroundDimens: BackgroundDimens = BackgroundDimens()
+ val horizontalTranslation: Float? = 0f,
+ val scale: Float = 0f,
+ val scalePivotX: Float = 0f,
+ val arrowDimens: ArrowDimens,
+ val backgroundDimens: BackgroundDimens,
+ val verticalTranslationSpring: SpringForce? = null,
+ val horizontalTranslationSpring: SpringForce? = null,
+ val scaleSpring: SpringForce? = null,
)
- var arrowThickness: Float = 0f
+ lateinit var entryIndicator: BackIndicatorDimens
private set
- var entryIndicator = BackIndicatorDimens()
+ lateinit var activeIndicator: BackIndicatorDimens
private set
- var activeIndicator = BackIndicatorDimens()
+ lateinit var cancelledIndicator: BackIndicatorDimens
private set
- var preThresholdIndicator = BackIndicatorDimens()
+ lateinit var flungIndicator: BackIndicatorDimens
private set
- var fullyStretchedIndicator = BackIndicatorDimens()
+ lateinit var committedIndicator: BackIndicatorDimens
private set
- var cancelledEdgeCornerRadius: Float = 0f
+ lateinit var preThresholdIndicator: BackIndicatorDimens
private set
- var cancelledArrowDimens = ArrowDimens()
+ lateinit var fullyStretchedIndicator: BackIndicatorDimens
+ private set
// navigation bar edge constants
var arrowPaddingEnd: Int = 0
private set
+ var arrowThickness: Float = 0f
+ private set
+ lateinit var arrowStrokeAlphaSpring: Step<SpringForce>
+ private set
+ lateinit var arrowStrokeAlphaInterpolator: Step<Float>
+ private set
// The closest to y
var minArrowYPosition: Int = 0
private set
var fingerOffset: Int = 0
private set
- var swipeTriggerThreshold: Float = 0f
+ var staticTriggerThreshold: Float = 0f
+ private set
+ var reactivationTriggerThreshold: Float = 0f
+ private set
+ var deactivationSwipeTriggerThreshold: Float = 0f
+ get() = -field
private set
var swipeProgressThreshold: Float = 0f
private set
@@ -55,6 +85,26 @@
var minDeltaForSwitch: Int = 0
private set
+ var minDragToStartAnimation: Float = 0f
+ private set
+
+ lateinit var entryWidthInterpolator: PathInterpolator
+ private set
+ lateinit var entryWidthTowardsEdgeInterpolator: PathInterpolator
+ private set
+ lateinit var activeWidthInterpolator: PathInterpolator
+ private set
+ lateinit var arrowAngleInterpolator: PathInterpolator
+ private set
+ lateinit var translationInterpolator: PathInterpolator
+ private set
+ lateinit var farCornerInterpolator: PathInterpolator
+ private set
+ lateinit var edgeCornerInterpolator: PathInterpolator
+ private set
+ lateinit var heightInterpolator: PathInterpolator
+ private set
+
init {
update(resources)
}
@@ -63,6 +113,10 @@
return resources.getDimension(id)
}
+ private fun getDimenFloat(id: Int): Float {
+ return TypedValue().run { resources.getValue(id, this, true); float }
+ }
+
private fun getPx(id: Int): Int {
return resources.getDimensionPixelSize(id)
}
@@ -73,72 +127,200 @@
arrowPaddingEnd = getPx(R.dimen.navigation_edge_panel_padding)
minArrowYPosition = getPx(R.dimen.navigation_edge_arrow_min_y)
fingerOffset = getPx(R.dimen.navigation_edge_finger_offset)
- swipeTriggerThreshold = getDimen(R.dimen.navigation_edge_action_drag_threshold)
+ staticTriggerThreshold = getDimen(R.dimen.navigation_edge_action_drag_threshold)
+ reactivationTriggerThreshold =
+ getDimen(R.dimen.navigation_edge_action_reactivation_drag_threshold)
+ deactivationSwipeTriggerThreshold =
+ getDimen(R.dimen.navigation_edge_action_deactivation_drag_threshold)
swipeProgressThreshold = getDimen(R.dimen.navigation_edge_action_progress_threshold)
minDeltaForSwitch = getPx(R.dimen.navigation_edge_minimum_x_delta_for_switch)
+ minDragToStartAnimation =
+ getDimen(R.dimen.navigation_edge_action_min_distance_to_start_animation)
+
+ entryWidthInterpolator = PathInterpolator(.19f, 1.27f, .71f, .86f)
+ entryWidthTowardsEdgeInterpolator = PathInterpolator(1f, -3f, 1f, 1.2f)
+ activeWidthInterpolator = PathInterpolator(.15f, .48f, .46f, .89f)
+ arrowAngleInterpolator = entryWidthInterpolator
+ translationInterpolator = PathInterpolator(0.2f, 1.0f, 1.0f, 1.0f)
+ farCornerInterpolator = PathInterpolator(.03f, .19f, .14f, 1.09f)
+ edgeCornerInterpolator = PathInterpolator(0f, 1.11f, .85f, .84f)
+ heightInterpolator = PathInterpolator(1f, .05f, .9f, -0.29f)
+
+ val showArrowOnProgressValue = .2f
+ val showArrowOnProgressValueFactor = 1.05f
+
+ val entryActiveHorizontalTranslationSpring = createSpring(675f, 0.8f)
+ val activeCommittedArrowLengthSpring = createSpring(1500f, 0.29f)
+ val activeCommittedArrowHeightSpring = createSpring(1500f, 0.29f)
+ val flungCommittedEdgeCornerSpring = createSpring(10000f, 1f)
+ val flungCommittedFarCornerSpring = createSpring(10000f, 1f)
+ val flungCommittedWidthSpring = createSpring(10000f, 1f)
+ val flungCommittedHeightSpring = createSpring(10000f, 1f)
entryIndicator = BackIndicatorDimens(
- horizontalTranslation = getDimen(R.dimen.navigation_edge_entry_margin),
- arrowDimens = ArrowDimens(
- length = getDimen(R.dimen.navigation_edge_entry_arrow_length),
- height = getDimen(R.dimen.navigation_edge_entry_arrow_height),
- ),
- backgroundDimens = BackgroundDimens(
- width = getDimen(R.dimen.navigation_edge_entry_background_width),
- height = getDimen(R.dimen.navigation_edge_entry_background_height),
- edgeCornerRadius = getDimen(R.dimen.navigation_edge_entry_edge_corners),
- farCornerRadius = getDimen(R.dimen.navigation_edge_entry_far_corners)
- )
+ horizontalTranslation = getDimen(R.dimen.navigation_edge_entry_margin),
+ scale = getDimenFloat(R.dimen.navigation_edge_entry_scale),
+ scalePivotX = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
+ horizontalTranslationSpring = entryActiveHorizontalTranslationSpring,
+ verticalTranslationSpring = createSpring(10000f, 0.9f),
+ scaleSpring = createSpring(120f, 0.8f),
+ arrowDimens = ArrowDimens(
+ length = getDimen(R.dimen.navigation_edge_entry_arrow_length),
+ height = getDimen(R.dimen.navigation_edge_entry_arrow_height),
+ alpha = 0f,
+ alphaSpring = createSpring(200f, 1f),
+ lengthSpring = createSpring(600f, 0.4f),
+ heightSpring = createSpring(600f, 0.4f),
+ ),
+ backgroundDimens = BackgroundDimens(
+ alpha = 1f,
+ width = getDimen(R.dimen.navigation_edge_entry_background_width),
+ height = getDimen(R.dimen.navigation_edge_entry_background_height),
+ edgeCornerRadius = getDimen(R.dimen.navigation_edge_entry_edge_corners),
+ farCornerRadius = getDimen(R.dimen.navigation_edge_entry_far_corners),
+ alphaSpring = createSpring(900f, 1f),
+ widthSpring = createSpring(450f, 0.65f),
+ heightSpring = createSpring(1500f, 0.45f),
+ farCornerRadiusSpring = createSpring(300f, 0.5f),
+ edgeCornerRadiusSpring = createSpring(150f, 0.5f),
+ )
)
activeIndicator = BackIndicatorDimens(
- horizontalTranslation = getDimen(R.dimen.navigation_edge_active_margin),
- arrowDimens = ArrowDimens(
- length = getDimen(R.dimen.navigation_edge_active_arrow_length),
- height = getDimen(R.dimen.navigation_edge_active_arrow_height),
- ),
- backgroundDimens = BackgroundDimens(
- width = getDimen(R.dimen.navigation_edge_active_background_width),
- height = getDimen(R.dimen.navigation_edge_active_background_height),
- edgeCornerRadius = getDimen(R.dimen.navigation_edge_active_edge_corners),
- farCornerRadius = getDimen(R.dimen.navigation_edge_active_far_corners)
-
- )
+ horizontalTranslation = getDimen(R.dimen.navigation_edge_active_margin),
+ scale = getDimenFloat(R.dimen.navigation_edge_active_scale),
+ horizontalTranslationSpring = entryActiveHorizontalTranslationSpring,
+ scaleSpring = createSpring(450f, 0.415f),
+ arrowDimens = ArrowDimens(
+ length = getDimen(R.dimen.navigation_edge_active_arrow_length),
+ height = getDimen(R.dimen.navigation_edge_active_arrow_height),
+ alpha = 1f,
+ lengthSpring = activeCommittedArrowLengthSpring,
+ heightSpring = activeCommittedArrowHeightSpring,
+ ),
+ backgroundDimens = BackgroundDimens(
+ alpha = 1f,
+ width = getDimen(R.dimen.navigation_edge_active_background_width),
+ height = getDimen(R.dimen.navigation_edge_active_background_height),
+ edgeCornerRadius = getDimen(R.dimen.navigation_edge_active_edge_corners),
+ farCornerRadius = getDimen(R.dimen.navigation_edge_active_far_corners),
+ widthSpring = createSpring(375f, 0.675f),
+ heightSpring = createSpring(10000f, 1f),
+ edgeCornerRadiusSpring = createSpring(600f, 0.36f),
+ farCornerRadiusSpring = createSpring(2500f, 0.855f),
+ )
)
preThresholdIndicator = BackIndicatorDimens(
- horizontalTranslation = getDimen(R.dimen.navigation_edge_pre_threshold_margin),
- arrowDimens = ArrowDimens(
- length = entryIndicator.arrowDimens.length,
- height = entryIndicator.arrowDimens.height,
- ),
- backgroundDimens = BackgroundDimens(
- width = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
- height = getDimen(R.dimen.navigation_edge_pre_threshold_background_height),
- edgeCornerRadius = getDimen(R.dimen.navigation_edge_pre_threshold_edge_corners),
- farCornerRadius = getDimen(R.dimen.navigation_edge_pre_threshold_far_corners)
- )
+ horizontalTranslation = getDimen(R.dimen.navigation_edge_pre_threshold_margin),
+ scale = getDimenFloat(R.dimen.navigation_edge_pre_threshold_scale),
+ scalePivotX = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
+ scaleSpring = createSpring(120f, 0.8f),
+ horizontalTranslationSpring = createSpring(6000f, 1f),
+ arrowDimens = ArrowDimens(
+ length = getDimen(R.dimen.navigation_edge_pre_threshold_arrow_length),
+ height = getDimen(R.dimen.navigation_edge_pre_threshold_arrow_height),
+ alpha = 1f,
+ lengthSpring = createSpring(100f, 0.6f),
+ heightSpring = createSpring(100f, 0.6f),
+ ),
+ backgroundDimens = BackgroundDimens(
+ alpha = 1f,
+ width = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
+ height = getDimen(R.dimen.navigation_edge_pre_threshold_background_height),
+ edgeCornerRadius =
+ getDimen(R.dimen.navigation_edge_pre_threshold_edge_corners),
+ farCornerRadius =
+ getDimen(R.dimen.navigation_edge_pre_threshold_far_corners),
+ widthSpring = createSpring(200f, 0.65f),
+ heightSpring = createSpring(1500f, 0.45f),
+ farCornerRadiusSpring = createSpring(200f, 1f),
+ edgeCornerRadiusSpring = createSpring(150f, 0.5f),
+ )
+ )
+
+ committedIndicator = activeIndicator.copy(
+ horizontalTranslation = null,
+ arrowDimens = activeIndicator.arrowDimens.copy(
+ lengthSpring = activeCommittedArrowLengthSpring,
+ heightSpring = activeCommittedArrowHeightSpring,
+ ),
+ backgroundDimens = activeIndicator.backgroundDimens.copy(
+ alpha = 0f,
+ // explicitly set to null to preserve previous width upon state change
+ width = null,
+ widthSpring = flungCommittedWidthSpring,
+ heightSpring = flungCommittedHeightSpring,
+ edgeCornerRadiusSpring = flungCommittedEdgeCornerSpring,
+ farCornerRadiusSpring = flungCommittedFarCornerSpring,
+ ),
+ scale = 0.85f,
+ scaleSpring = createSpring(650f, 1f),
+ )
+
+ flungIndicator = committedIndicator.copy(
+ arrowDimens = committedIndicator.arrowDimens.copy(
+ lengthSpring = createSpring(850f, 0.46f),
+ heightSpring = createSpring(850f, 0.46f),
+ ),
+ backgroundDimens = committedIndicator.backgroundDimens.copy(
+ widthSpring = flungCommittedWidthSpring,
+ heightSpring = flungCommittedHeightSpring,
+ edgeCornerRadiusSpring = flungCommittedEdgeCornerSpring,
+ farCornerRadiusSpring = flungCommittedFarCornerSpring,
+ )
+ )
+
+ cancelledIndicator = entryIndicator.copy(
+ backgroundDimens = entryIndicator.backgroundDimens.copy(width = 0f)
)
fullyStretchedIndicator = BackIndicatorDimens(
- horizontalTranslation = getDimen(R.dimen.navigation_edge_stretch_margin),
- arrowDimens = ArrowDimens(
- length = getDimen(R.dimen.navigation_edge_stretched_arrow_length),
- height = getDimen(R.dimen.navigation_edge_stretched_arrow_height),
- ),
- backgroundDimens = BackgroundDimens(
- width = getDimen(R.dimen.navigation_edge_stretch_background_width),
- height = getDimen(R.dimen.navigation_edge_stretch_background_height),
- edgeCornerRadius = getDimen(R.dimen.navigation_edge_stretch_edge_corners),
- farCornerRadius = getDimen(R.dimen.navigation_edge_stretch_far_corners)
+ horizontalTranslation = getDimen(R.dimen.navigation_edge_stretch_margin),
+ scale = getDimenFloat(R.dimen.navigation_edge_stretch_scale),
+ horizontalTranslationSpring = null,
+ verticalTranslationSpring = null,
+ scaleSpring = null,
+ arrowDimens = ArrowDimens(
+ length = getDimen(R.dimen.navigation_edge_stretched_arrow_length),
+ height = getDimen(R.dimen.navigation_edge_stretched_arrow_height),
+ alpha = 1f,
+ alphaSpring = null,
+ heightSpring = null,
+ lengthSpring = null,
+ ),
+ backgroundDimens = BackgroundDimens(
+ alpha = 1f,
+ width = getDimen(R.dimen.navigation_edge_stretch_background_width),
+ height = getDimen(R.dimen.navigation_edge_stretch_background_height),
+ edgeCornerRadius = getDimen(R.dimen.navigation_edge_stretch_edge_corners),
+ farCornerRadius = getDimen(R.dimen.navigation_edge_stretch_far_corners),
+ alphaSpring = null,
+ widthSpring = null,
+ heightSpring = null,
+ edgeCornerRadiusSpring = null,
+ farCornerRadiusSpring = null,
+ )
+ )
+
+ arrowStrokeAlphaInterpolator = Step(
+ threshold = showArrowOnProgressValue,
+ factor = showArrowOnProgressValueFactor,
+ postThreshold = 1f,
+ preThreshold = 0f
+ )
+
+ entryIndicator.arrowDimens.alphaSpring?.let { alphaSpring ->
+ arrowStrokeAlphaSpring = Step(
+ threshold = showArrowOnProgressValue,
+ factor = showArrowOnProgressValueFactor,
+ postThreshold = alphaSpring,
+ preThreshold = SpringForce().setStiffness(2000f).setDampingRatio(1f)
)
- )
-
- cancelledEdgeCornerRadius = getDimen(R.dimen.navigation_edge_cancelled_edge_corners)
-
- cancelledArrowDimens = ArrowDimens(
- length = getDimen(R.dimen.navigation_edge_cancelled_arrow_length),
- height = getDimen(R.dimen.navigation_edge_cancelled_arrow_height)
- )
+ }
}
}
+
+fun createSpring(stiffness: Float, dampingRatio: Float): SpringForce {
+ return SpringForce().setStiffness(stiffness).setDampingRatio(dampingRatio)
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java
index 335172e..30d2d7a 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java
@@ -16,7 +16,7 @@
package com.android.systemui.navigationbar.gestural;
-import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
+import static android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE;
import android.content.Context;
import android.view.MotionEvent;
@@ -24,14 +24,12 @@
public final class Utilities {
- private static final int TRACKPAD_GESTURE_SCALE = 60;
+ private static final int TRACKPAD_GESTURE_SCALE = 200;
public static boolean isTrackpadMotionEvent(boolean isTrackpadGestureBackEnabled,
MotionEvent event) {
- // TODO: ideally should use event.getClassification(), but currently only the move
- // events get assigned the correct classification.
return isTrackpadGestureBackEnabled
- && (event.getSource() & SOURCE_TOUCHSCREEN) != SOURCE_TOUCHSCREEN;
+ && event.getClassification() == CLASSIFICATION_MULTI_FINGER_SWIPE;
}
public static int getTrackpadScale(Context context) {
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 6bfe1a0..be615d6 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -20,9 +20,12 @@
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Context
+import android.content.Intent
import android.content.pm.PackageManager
import android.os.UserManager
import android.util.Log
+import com.android.internal.logging.UiEvent
+import com.android.internal.logging.UiEventLogger
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
import com.android.systemui.util.kotlin.getOrNull
@@ -42,11 +45,12 @@
@Inject
constructor(
private val context: Context,
- private val intentResolver: NoteTaskIntentResolver,
+ private val resolver: NoteTaskInfoResolver,
private val optionalBubbles: Optional<Bubbles>,
private val optionalKeyguardManager: Optional<KeyguardManager>,
private val optionalUserManager: Optional<UserManager>,
@NoteTaskEnabledKey private val isEnabled: Boolean,
+ private val uiEventLogger: UiEventLogger,
) {
/**
@@ -64,7 +68,9 @@
*
* That will let users open other apps in full screen, and take contextual notes.
*/
- fun showNoteTask(isInMultiWindowMode: Boolean = false) {
+ @JvmOverloads
+ fun showNoteTask(isInMultiWindowMode: Boolean = false, uiEvent: ShowNoteTaskUiEvent? = null) {
+
if (!isEnabled) return
val bubbles = optionalBubbles.getOrNull() ?: return
@@ -74,9 +80,12 @@
// TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing.
if (!userManager.isUserUnlocked) return
- val intent = intentResolver.resolveIntent() ?: return
+ val noteTaskInfo = resolver.resolveInfo() ?: return
+
+ uiEvent?.let { uiEventLogger.log(it, noteTaskInfo.uid, noteTaskInfo.packageName) }
// TODO(b/266686199): We should handle when app not available. For now, we log.
+ val intent = noteTaskInfo.toCreateNoteIntent()
try {
if (isInMultiWindowMode || keyguardManager.isKeyguardLocked) {
context.startActivity(intent)
@@ -84,9 +93,7 @@
bubbles.showOrHideAppBubble(intent)
}
} catch (e: ActivityNotFoundException) {
- val message =
- "Activity not found for action: ${NoteTaskIntentResolver.ACTION_CREATE_NOTE}."
- Log.e(TAG, message, e)
+ Log.e(TAG, "Activity not found for action: $ACTION_CREATE_NOTE.", e)
}
}
@@ -114,10 +121,47 @@
)
}
+ /** IDs of UI events accepted by [showNoteTask]. */
+ enum class ShowNoteTaskUiEvent(private val _id: Int) : UiEventLogger.UiEventEnum {
+ @UiEvent(doc = "User opened a note by tapping on the lockscreen shortcut.")
+ NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE(1294),
+
+ /* ktlint-disable max-line-length */
+ @UiEvent(
+ doc =
+ "User opened a note by pressing the stylus tail button while the screen was unlocked."
+ )
+ NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON(1295),
+ @UiEvent(
+ doc =
+ "User opened a note by pressing the stylus tail button while the screen was locked."
+ )
+ NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED(1296),
+ @UiEvent(doc = "User opened a note by tapping on an app shortcut.")
+ NOTE_OPENED_VIA_SHORTCUT(1297);
+
+ override fun getId() = _id
+ }
+
companion object {
private val TAG = NoteTaskController::class.simpleName.orEmpty()
+ private fun NoteTaskInfoResolver.NoteTaskInfo.toCreateNoteIntent(): Intent {
+ return Intent(ACTION_CREATE_NOTE)
+ .setPackage(packageName)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ // EXTRA_USE_STYLUS_MODE does not mean a stylus is in-use, but a stylus entrypoint
+ // was used to start it.
+ .putExtra(INTENT_EXTRA_USE_STYLUS_MODE, true)
+ }
+
// TODO(b/254604589): Use final KeyEvent.KEYCODE_* instead.
const val NOTE_TASK_KEY_EVENT = 311
+
+ // TODO(b/265912743): Use Intent.ACTION_CREATE_NOTE instead.
+ const val ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE"
+
+ // TODO(b/265912743): Use Intent.INTENT_EXTRA_USE_STYLUS_MODE instead.
+ const val INTENT_EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt
new file mode 100644
index 0000000..bd822d4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfoResolver.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notetask
+
+import android.app.role.RoleManager
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import android.util.Log
+import javax.inject.Inject
+
+internal class NoteTaskInfoResolver
+@Inject
+constructor(
+ private val context: Context,
+ private val roleManager: RoleManager,
+ private val packageManager: PackageManager,
+) {
+ fun resolveInfo(): NoteTaskInfo? {
+ // TODO(b/267634412): Select UserHandle depending on where the user initiated note-taking.
+ val user = context.user
+ val packageName = roleManager.getRoleHoldersAsUser(ROLE_NOTES, user).firstOrNull()
+
+ if (packageName.isNullOrEmpty()) return null
+
+ return NoteTaskInfo(packageName, packageManager.getUidOf(packageName, user))
+ }
+
+ /** Package name and kernel user-ID of a note-taking app. */
+ data class NoteTaskInfo(val packageName: String, val uid: Int)
+
+ companion object {
+ private val TAG = NoteTaskInfoResolver::class.simpleName.orEmpty()
+
+ private val EMPTY_APPLICATION_INFO_FLAGS = PackageManager.ApplicationInfoFlags.of(0)!!
+
+ /**
+ * Returns the kernel user-ID of [packageName] for a [user]. Returns zero if the app cannot
+ * be found.
+ */
+ private fun PackageManager.getUidOf(packageName: String, user: UserHandle): Int {
+ val applicationInfo =
+ try {
+ getApplicationInfoAsUser(packageName, EMPTY_APPLICATION_INFO_FLAGS, user)
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.e(TAG, "Couldn't find notes app UID", e)
+ return 0
+ }
+ return applicationInfo.uid
+ }
+
+ // TODO(b/265912743): Use RoleManager.NOTES_ROLE instead.
+ const val ROLE_NOTES = "android.app.role.NOTES"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index d5f4a5a..d40bf2b 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -16,8 +16,10 @@
package com.android.systemui.notetask
+import android.app.KeyguardManager
import androidx.annotation.VisibleForTesting
import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.util.kotlin.getOrNull
import com.android.wm.shell.bubbles.Bubbles
import java.util.Optional
import javax.inject.Inject
@@ -30,6 +32,7 @@
private val noteTaskController: NoteTaskController,
private val commandQueue: CommandQueue,
@NoteTaskEnabledKey private val isEnabled: Boolean,
+ private val optionalKeyguardManager: Optional<KeyguardManager>,
) {
@VisibleForTesting
@@ -37,11 +40,21 @@
object : CommandQueue.Callbacks {
override fun handleSystemKey(keyCode: Int) {
if (keyCode == NoteTaskController.NOTE_TASK_KEY_EVENT) {
- noteTaskController.showNoteTask()
+ showNoteTask()
}
}
}
+ private fun showNoteTask() {
+ val uiEvent =
+ if (optionalKeyguardManager.isKeyguardLocked) {
+ NoteTaskController.ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED
+ } else {
+ NoteTaskController.ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON
+ }
+ noteTaskController.showNoteTask(uiEvent = uiEvent)
+ }
+
fun initialize() {
if (isEnabled && optionalBubbles.isPresent) {
commandQueue.addCallback(callbacks)
@@ -49,3 +62,7 @@
noteTaskController.setNoteTaskShortcutEnabled(isEnabled)
}
}
+
+private val Optional<KeyguardManager>.isKeyguardLocked: Boolean
+ // If there's no KeyguardManager, assume that the keyguard is not locked.
+ get() = getOrNull()?.isKeyguardLocked ?: false
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
deleted file mode 100644
index 11dc1d7..0000000
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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.systemui.notetask
-
-import android.app.role.RoleManager
-import android.content.Context
-import android.content.Intent
-import javax.inject.Inject
-
-internal class NoteTaskIntentResolver
-@Inject
-constructor(
- private val context: Context,
- private val roleManager: RoleManager,
-) {
-
- fun resolveIntent(): Intent? {
- val packageName = roleManager.getRoleHoldersAsUser(ROLE_NOTES, context.user).firstOrNull()
-
- if (packageName.isNullOrEmpty()) return null
-
- return Intent(ACTION_CREATE_NOTE)
- .setPackage(packageName)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- // EXTRA_USE_STYLUS_MODE does not mean a stylus is in-use, but a stylus entrypoint was
- // used to start it.
- .putExtra(INTENT_EXTRA_USE_STYLUS_MODE, true)
- }
-
- companion object {
- // TODO(b/265912743): Use Intent.ACTION_CREATE_NOTE instead.
- const val ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE"
-
- // TODO(b/265912743): Use RoleManager.NOTES_ROLE instead.
- const val ROLE_NOTES = "android.app.role.NOTES"
-
- // TODO(b/265912743): Use Intent.INTENT_EXTRA_USE_STYLUS_MODE instead.
- const val INTENT_EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE"
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
index ec6a16a..b8800a2 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
@@ -51,7 +51,7 @@
featureFlags: FeatureFlags,
roleManager: RoleManager,
): Boolean {
- val isRoleAvailable = roleManager.isRoleAvailable(NoteTaskIntentResolver.ROLE_NOTES)
+ val isRoleAvailable = roleManager.isRoleAvailable(NoteTaskInfoResolver.ROLE_NOTES)
val isFeatureEnabled = featureFlags.isEnabled(Flags.NOTE_TASKS)
return isRoleAvailable && isFeatureEnabled
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt
index cfbaa48..43869cc 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt
@@ -27,6 +27,7 @@
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnTriggeredResult
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.PickerScreenState
import com.android.systemui.notetask.NoteTaskController
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
import com.android.systemui.notetask.NoteTaskEnabledKey
import javax.inject.Inject
import kotlinx.coroutines.flow.flowOf
@@ -64,7 +65,9 @@
}
override fun onTriggered(expandable: Expandable?): OnTriggeredResult {
- noteTaskController.showNoteTask()
+ noteTaskController.showNoteTask(
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE
+ )
return OnTriggeredResult.Handled
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
index f203e7a..3ac5bfa 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
@@ -21,7 +21,7 @@
import android.os.Bundle
import androidx.activity.ComponentActivity
import com.android.systemui.notetask.NoteTaskController
-import com.android.systemui.notetask.NoteTaskIntentResolver
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
import javax.inject.Inject
/** Activity responsible for launching the note experience, and finish. */
@@ -34,7 +34,10 @@
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- noteTaskController.showNoteTask(isInMultiWindowMode)
+ noteTaskController.showNoteTask(
+ isInMultiWindowMode = isInMultiWindowMode,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT,
+ )
finish()
}
@@ -46,7 +49,7 @@
return Intent(context, LaunchNoteTaskActivity::class.java).apply {
// Intent's action must be set in shortcuts, or an exception will be thrown.
// TODO(b/254606432): Use Intent.ACTION_CREATE_NOTE instead.
- action = NoteTaskIntentResolver.ACTION_CREATE_NOTE
+ action = NoteTaskController.ACTION_CREATE_NOTE
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
index 8d8903d..2522e1c 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
@@ -75,6 +75,8 @@
import com.android.systemui.util.settings.GlobalSettings;
import com.android.systemui.volume.Events;
+import dagger.Lazy;
+
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.text.NumberFormat;
@@ -83,8 +85,6 @@
import javax.inject.Inject;
-import dagger.Lazy;
-
/**
*/
@SysUISingleton
@@ -132,7 +132,6 @@
private static final String ACTION_AUTO_SAVER_NO_THANKS =
"PNW.autoSaverNoThanks";
- private static final String ACTION_ENABLE_SEVERE_BATTERY_DIALOG = "PNW.enableSevereDialog";
private static final String EXTRA_SCHEDULED_BY_PERCENTAGE =
"extra_scheduled_by_percentage";
public static final String BATTERY_SAVER_SCHEDULE_SCREEN_INTENT_ACTION =
@@ -155,7 +154,6 @@
private final Intent mOpenBatterySettings = settings(Intent.ACTION_POWER_USAGE_SUMMARY);
private final Intent mOpenBatterySaverSettings =
settings(Settings.ACTION_BATTERY_SAVER_SETTINGS);
- private final boolean mUseSevereDialog;
private int mBatteryLevel;
private int mBucket;
@@ -177,7 +175,6 @@
private ActivityStarter mActivityStarter;
private final BroadcastSender mBroadcastSender;
private final UiEventLogger mUiEventLogger;
- private GlobalSettings mGlobalSettings;
private final UserTracker mUserTracker;
private final Lazy<BatteryController> mBatteryControllerLazy;
private final DialogLaunchAnimator mDialogLaunchAnimator;
@@ -198,9 +195,7 @@
mBroadcastSender = broadcastSender;
mBatteryControllerLazy = batteryControllerLazy;
mDialogLaunchAnimator = dialogLaunchAnimator;
- mUseSevereDialog = mContext.getResources().getBoolean(R.bool.config_severe_battery_dialog);
mUiEventLogger = uiEventLogger;
- mGlobalSettings = globalSettings;
mUserTracker = userTracker;
}
@@ -287,20 +282,6 @@
}
protected void showWarningNotification() {
- if (mGlobalSettings.getInt(Global.LOW_POWER_MODE_REMINDER_ENABLED, 1) == 0) {
- return;
- }
- if (showSevereLowBatteryDialog()) {
- mBroadcastSender.sendBroadcast(new Intent(ACTION_ENABLE_SEVERE_BATTERY_DIALOG)
- .setPackage(mContext.getPackageName())
- .putExtra(EXTRA_SCHEDULED_BY_PERCENTAGE, isScheduledByPercentage())
- .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
- | Intent.FLAG_RECEIVER_FOREGROUND));
- // Reset the state once dialog been enabled
- dismissLowBatteryNotification();
- mPlaySound = false;
- return;
- }
if (isScheduledByPercentage()) {
return;
}
@@ -349,10 +330,6 @@
mNoMan.notifyAsUser(TAG_BATTERY, SystemMessage.NOTE_POWER_LOW, n, UserHandle.ALL);
}
- private boolean showSevereLowBatteryDialog() {
- return mBucket < -1 && mUseSevereDialog;
- }
-
/**
* Checking battery saver schedule mode is set as "Based on percentage" or not.
*
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
index 464b6e7..048d40c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
@@ -48,6 +48,7 @@
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_ENABLED
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_INFORM_JOB_SCHEDULER_OF_PENDING_APP_STOP
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_FOOTER_DOT
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_USER_VISIBLE_JOBS
@@ -158,6 +159,7 @@
private const val DEFAULT_TASK_MANAGER_SHOW_FOOTER_DOT = false
private const val DEFAULT_TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS = true
private const val DEFAULT_TASK_MANAGER_SHOW_USER_VISIBLE_JOBS = true
+ private const val DEFAULT_TASK_MANAGER_INFORM_JOB_SCHEDULER_OF_PENDING_APP_STOP = true
}
override var newChangesSinceDialogWasDismissed = false
@@ -173,6 +175,9 @@
private var showUserVisibleJobs = DEFAULT_TASK_MANAGER_SHOW_USER_VISIBLE_JOBS
+ private var informJobSchedulerOfPendingAppStop =
+ DEFAULT_TASK_MANAGER_INFORM_JOB_SCHEDULER_OF_PENDING_APP_STOP
+
override val includesUserVisibleJobs: Boolean
get() = showUserVisibleJobs
@@ -233,6 +238,11 @@
NAMESPACE_SYSTEMUI,
TASK_MANAGER_SHOW_USER_VISIBLE_JOBS, DEFAULT_TASK_MANAGER_SHOW_USER_VISIBLE_JOBS)
+ informJobSchedulerOfPendingAppStop = deviceConfigProxy.getBoolean(
+ NAMESPACE_SYSTEMUI,
+ TASK_MANAGER_INFORM_JOB_SCHEDULER_OF_PENDING_APP_STOP,
+ DEFAULT_TASK_MANAGER_INFORM_JOB_SCHEDULER_OF_PENDING_APP_STOP)
+
try {
activityManager.registerForegroundServiceObserver(foregroundServiceObserver)
// Clumping FGS and user-visible jobs here and showing a single entry and button
@@ -262,10 +272,13 @@
showStopBtnForUserAllowlistedApps)
var wasShowingUserVisibleJobs = showUserVisibleJobs
showUserVisibleJobs = it.getBoolean(
- TASK_MANAGER_SHOW_USER_VISIBLE_JOBS, showUserVisibleJobs)
+ TASK_MANAGER_SHOW_USER_VISIBLE_JOBS, showUserVisibleJobs)
if (showUserVisibleJobs != wasShowingUserVisibleJobs) {
onShowUserVisibleJobsFlagChanged()
}
+ informJobSchedulerOfPendingAppStop = it.getBoolean(
+ TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS,
+ informJobSchedulerOfPendingAppStop)
}
_isAvailable.value = deviceConfigProxy.getBoolean(
@@ -475,14 +488,11 @@
private fun stopPackage(userId: Int, packageName: String, timeStarted: Long) {
logEvent(stopped = true, packageName, userId, timeStarted)
val userPackageKey = UserPackage(userId, packageName)
- if (showUserVisibleJobs &&
- runningTaskIdentifiers[userPackageKey]?.hasRunningJobs() == true) {
+ if (showUserVisibleJobs || informJobSchedulerOfPendingAppStop) {
// TODO(255768978): allow fine-grained job control
- jobScheduler.stopUserVisibleJobsForUser(packageName, userId)
+ jobScheduler.notePendingUserRequestedAppStop(packageName, userId, "task manager")
}
- if (runningTaskIdentifiers[userPackageKey]?.hasFgs() == true) {
- activityManager.stopAppForUser(packageName, userId)
- }
+ activityManager.stopAppForUser(packageName, userId)
}
private fun onShowUserVisibleJobsFlagChanged() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
index 5bc209a..7316f46 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
@@ -203,8 +203,7 @@
// For now, restrict to debug users.
return Build.isDebuggable()
&& mDreamSupported
- // TODO(b/257333623): Allow the Dock User to be non-SystemUser user in HSUM.
- && (!mDreamOnlyEnabledForDockUser || mUserTracker.getUserHandle().isSystem());
+ && (!mDreamOnlyEnabledForDockUser || mUserTracker.getUserInfo().isMain());
}
@VisibleForTesting
@@ -224,7 +223,8 @@
private ComponentName getActiveDream() {
try {
- final ComponentName[] dreams = mDreamManager.getDreamComponents();
+ final ComponentName[] dreams = mDreamManager.getDreamComponentsForUser(
+ mUserTracker.getUserId());
return dreams != null && dreams.length > 0 ? dreams[0] : null;
} catch (RemoteException e) {
Log.w(TAG, "Failed to get active dream", e);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
index 28dd986..d423048 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
@@ -259,17 +259,19 @@
Log.d(TAG, "setWifiIndicators: " + indicators);
}
mWifiInfo.mEnabled = indicators.enabled;
- if (indicators.qsIcon == null) {
- return;
- }
- mWifiInfo.mConnected = indicators.qsIcon.visible;
- mWifiInfo.mWifiSignalIconId = indicators.qsIcon.icon;
- mWifiInfo.mWifiSignalContentDescription = indicators.qsIcon.contentDescription;
- mWifiInfo.mEnabled = indicators.enabled;
mWifiInfo.mSsid = indicators.description;
mWifiInfo.mIsTransient = indicators.isTransient;
mWifiInfo.mStatusLabel = indicators.statusLabel;
- refreshState(mWifiInfo);
+ if (indicators.qsIcon != null) {
+ mWifiInfo.mConnected = indicators.qsIcon.visible;
+ mWifiInfo.mWifiSignalIconId = indicators.qsIcon.icon;
+ mWifiInfo.mWifiSignalContentDescription = indicators.qsIcon.contentDescription;
+ refreshState(mWifiInfo);
+ } else {
+ mWifiInfo.mConnected = false;
+ mWifiInfo.mWifiSignalIconId = 0;
+ mWifiInfo.mWifiSignalContentDescription = null;
+ }
}
@Override
@@ -533,6 +535,9 @@
if (DEBUG) {
Log.d(TAG, "handleUpdateEthernetState: " + "EthernetCallbackInfo = " + cb.toString());
}
+ if (!cb.mConnected) {
+ return;
+ }
final Resources r = mContext.getResources();
state.label = r.getString(R.string.quick_settings_internet_label);
state.state = Tile.STATE_ACTIVE;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
index 64a8a14..ad00069 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
@@ -171,8 +171,9 @@
getHost().collapsePanels();
};
- Dialog dialog = mController.createScreenRecordDialog(mContext, mFlags,
+ final Dialog dialog = mController.createScreenRecordDialog(mContext, mFlags,
mDialogLaunchAnimator, mActivityStarter, onStartRecordingClicked);
+
ActivityStarter.OnDismissAction dismissAction = () -> {
if (shouldAnimateFromView) {
mDialogLaunchAnimator.showFromView(dialog, view, new DialogCuj(
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
index 431b28f..acb6d96 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
@@ -38,6 +38,8 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.settings.UserTracker;
@@ -48,6 +50,8 @@
import javax.inject.Inject;
+import dagger.Lazy;
+
/**
* Helper class to initiate a screen recording
*/
@@ -63,6 +67,8 @@
private CountDownTimer mCountDownTimer = null;
private final Executor mMainExecutor;
private final BroadcastDispatcher mBroadcastDispatcher;
+ private final Context mContext;
+ private final FeatureFlags mFlags;
private final UserContextProvider mUserContextProvider;
private final UserTracker mUserTracker;
@@ -73,6 +79,8 @@
private CopyOnWriteArrayList<RecordingStateChangeCallback> mListeners =
new CopyOnWriteArrayList<>();
+ private final Lazy<ScreenCaptureDevicePolicyResolver> mDevicePolicyResolver;
+
@VisibleForTesting
final UserTracker.Callback mUserChangedCallback =
new UserTracker.Callback() {
@@ -103,9 +111,15 @@
@Inject
public RecordingController(@Main Executor mainExecutor,
BroadcastDispatcher broadcastDispatcher,
+ Context context,
+ FeatureFlags flags,
UserContextProvider userContextProvider,
+ Lazy<ScreenCaptureDevicePolicyResolver> devicePolicyResolver,
UserTracker userTracker) {
mMainExecutor = mainExecutor;
+ mContext = context;
+ mFlags = flags;
+ mDevicePolicyResolver = devicePolicyResolver;
mBroadcastDispatcher = broadcastDispatcher;
mUserContextProvider = userContextProvider;
mUserTracker = userTracker;
@@ -115,14 +129,30 @@
mInteractiveBroadcastOption = options.toBundle();
}
- /** Create a dialog to show screen recording options to the user. */
+ /**
+ * MediaProjection host is SystemUI for the screen recorder, so return 'my user handle'
+ */
+ private UserHandle getHostUserHandle() {
+ return UserHandle.of(UserHandle.myUserId());
+ }
+
+ /** Create a dialog to show screen recording options to the user.
+ * If screen capturing is currently not allowed it will return a dialog
+ * that warns users about it. */
public Dialog createScreenRecordDialog(Context context, FeatureFlags flags,
DialogLaunchAnimator dialogLaunchAnimator,
ActivityStarter activityStarter,
@Nullable Runnable onStartRecordingClicked) {
+ if (mFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)
+ && mDevicePolicyResolver.get()
+ .isScreenCaptureCompletelyDisabled(getHostUserHandle())) {
+ return new ScreenCaptureDisabledDialog(mContext);
+ }
+
return flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)
- ? new ScreenRecordPermissionDialog(context, this, activityStarter,
- dialogLaunchAnimator, mUserContextProvider, onStartRecordingClicked)
+ ? new ScreenRecordPermissionDialog(context, getHostUserHandle(), this,
+ activityStarter, dialogLaunchAnimator, mUserContextProvider,
+ onStartRecordingClicked)
: new ScreenRecordDialog(context, this, activityStarter,
mUserContextProvider, flags, dialogLaunchAnimator, onStartRecordingClicked);
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
index 68e3dcd..dd21be9 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
@@ -42,6 +42,7 @@
/** Dialog to select screen recording options */
class ScreenRecordPermissionDialog(
context: Context?,
+ private val hostUserHandle: UserHandle,
private val controller: RecordingController,
private val activityStarter: ActivityStarter,
private val dialogLaunchAnimator: DialogLaunchAnimator,
@@ -79,11 +80,9 @@
CaptureTargetResultReceiver()
)
- // Send SystemUI's user handle as the host app user handle because SystemUI
- // is the 'host app' (the app that receives screen capture data)
intent.putExtra(
MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
- UserHandle.of(UserHandle.myUserId())
+ hostUserHandle
)
val animationController = dialogLaunchAnimator.createActivityLaunchController(v!!)
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt
index 70ea2b5..48aa60f 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt
@@ -17,8 +17,10 @@
package com.android.systemui.screenshot
import android.content.pm.PackageManager
+import android.view.Display
import android.view.IWindowManager
import android.view.ViewGroup
+import android.view.WindowManager
import android.widget.TextView
import com.android.systemui.R
import javax.inject.Inject
@@ -34,8 +36,18 @@
* notified.
*/
fun maybeNotifyOfScreenshot(data: ScreenshotData): List<CharSequence> {
- // TODO: actually ask the window manager once API is available.
- return listOf()
+ // No notification for screenshots from overview.
+ if (data.source == WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW) return listOf()
+
+ // Notify listeners, retrieve a list of listening components.
+ val components = windowManager.notifyScreenshotListeners(Display.DEFAULT_DISPLAY)
+
+ // Convert component names to app names.
+ return components.map {
+ packageManager
+ .getActivityInfo(it, PackageManager.ComponentInfoFlags.of(0))
+ .loadLabel(packageManager)
+ }
}
fun populateView(view: ViewGroup, appNames: List<CharSequence>) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java
index c891686..7a62bae 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java
@@ -46,6 +46,8 @@
SCREENSHOT_SAVED(306),
@UiEvent(doc = "screenshot failed to save")
SCREENSHOT_NOT_SAVED(336),
+ @UiEvent(doc = "failed to capture screenshot")
+ SCREENSHOT_CAPTURE_FAILED(1281),
@UiEvent(doc = "screenshot preview tapped")
SCREENSHOT_PREVIEW_TAPPED(307),
@UiEvent(doc = "screenshot edit button tapped")
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
index 4214c8f..8035d19 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
@@ -26,6 +26,7 @@
import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
import static com.android.systemui.screenshot.LogConfig.DEBUG_SERVICE;
import static com.android.systemui.screenshot.LogConfig.logTag;
+import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED;
import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER;
import android.annotation.MainThread;
@@ -202,6 +203,7 @@
// animation and error notification.
if (!mUserManager.isUserUnlocked()) {
Log.w(TAG, "Skipping screenshot because storage is locked!");
+ logFailedRequest(request);
mNotificationsController.notifyScreenshotError(
R.string.screenshot_failed_to_save_user_locked_text);
callback.reportError();
@@ -212,6 +214,7 @@
mBgExecutor.execute(() -> {
Log.w(TAG, "Skipping screenshot because an IT admin has disabled "
+ "screenshots on the device");
+ logFailedRequest(request);
String blockedByAdminText = mDevicePolicyManager.getResources().getString(
SCREENSHOT_BLOCKED_BY_ADMIN,
() -> mContext.getString(R.string.screenshot_blocked_by_admin));
@@ -225,38 +228,43 @@
if (mFeatureFlags.isEnabled(Flags.SCREENSHOT_METADATA)) {
Log.d(TAG, "Processing screenshot data");
ScreenshotData screenshotData = ScreenshotData.fromRequest(request);
- mProcessor.processAsync(screenshotData,
- (data) -> dispatchToController(data, onSaved, callback));
+ try {
+ mProcessor.processAsync(screenshotData,
+ (data) -> dispatchToController(data, onSaved, callback));
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to process screenshot request!", e);
+ logFailedRequest(request);
+ mNotificationsController.notifyScreenshotError(
+ R.string.screenshot_failed_to_capture_text);
+ callback.reportError();
+ }
} else {
- mProcessor.processAsync(request,
- (r) -> dispatchToController(r, onSaved, callback));
+ try {
+ mProcessor.processAsync(request,
+ (r) -> dispatchToController(r, onSaved, callback));
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to process screenshot request!", e);
+ logFailedRequest(request);
+ mNotificationsController.notifyScreenshotError(
+ R.string.screenshot_failed_to_capture_text);
+ callback.reportError();
+ }
}
}
private void dispatchToController(ScreenshotData screenshot,
Consumer<Uri> uriConsumer, RequestCallback callback) {
-
mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(screenshot.getSource()), 0,
screenshot.getPackageNameString());
-
- if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
- && screenshot.getBitmap() == null) {
- Log.e(TAG, "Got null bitmap from screenshot message");
- mNotificationsController.notifyScreenshotError(
- R.string.screenshot_failed_to_capture_text);
- callback.reportError();
- return;
- }
-
mScreenshot.handleScreenshot(screenshot, uriConsumer, callback);
}
private void dispatchToController(ScreenshotRequest request,
Consumer<Uri> uriConsumer, RequestCallback callback) {
-
ComponentName topComponent = request.getTopComponent();
- mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(request.getSource()), 0,
- topComponent == null ? "" : topComponent.getPackageName());
+ String packageName = topComponent == null ? "" : topComponent.getPackageName();
+ mUiEventLogger.log(
+ ScreenshotEvent.getScreenshotSource(request.getSource()), 0, packageName);
switch (request.getType()) {
case WindowManager.TAKE_SCREENSHOT_FULLSCREEN:
@@ -275,21 +283,22 @@
int taskId = request.getTaskId();
int userId = request.getUserId();
- if (screenshot == null) {
- Log.e(TAG, "Got null bitmap from screenshot message");
- mNotificationsController.notifyScreenshotError(
- R.string.screenshot_failed_to_capture_text);
- callback.reportError();
- } else {
- mScreenshot.handleImageAsScreenshot(screenshot, screenBounds, insets,
- taskId, userId, topComponent, uriConsumer, callback);
- }
+ mScreenshot.handleImageAsScreenshot(screenshot, screenBounds, insets,
+ taskId, userId, topComponent, uriConsumer, callback);
break;
default:
- Log.w(TAG, "Invalid screenshot option: " + request.getType());
+ Log.wtf(TAG, "Invalid screenshot option: " + request.getType());
}
}
+ private void logFailedRequest(ScreenshotRequest request) {
+ ComponentName topComponent = request.getTopComponent();
+ String packageName = topComponent == null ? "" : topComponent.getPackageName();
+ mUiEventLogger.log(
+ ScreenshotEvent.getScreenshotSource(request.getSource()), 0, packageName);
+ mUiEventLogger.log(SCREENSHOT_CAPTURE_FAILED, 0, packageName);
+ }
+
private static void sendComplete(Messenger target) {
try {
if (DEBUG_CALLBACK) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt
index 66b7842..1b728b8 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt
@@ -67,10 +67,7 @@
}
// If label wasn't loaded, use a default
- val badgedLabel =
- packageManager.getUserBadgedLabel(label ?: defaultFileAppName(), userHandle)
-
- return WorkProfileFirstRunData(badgedLabel, badgedIcon)
+ return WorkProfileFirstRunData(label ?: defaultFileAppName(), badgedIcon)
}
return null
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java
new file mode 100644
index 0000000..f8d86a0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot;
+
+import static com.android.systemui.screenshot.AppClipsTrampolineActivity.ACTION_FINISH_FROM_TRAMPOLINE;
+import static com.android.systemui.screenshot.AppClipsTrampolineActivity.EXTRA_RESULT_RECEIVER;
+import static com.android.systemui.screenshot.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI;
+import static com.android.systemui.screenshot.AppClipsTrampolineActivity.PERMISSION_SELF;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+
+import androidx.activity.ComponentActivity;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.settingslib.Utils;
+import com.android.systemui.R;
+
+import javax.inject.Inject;
+
+/**
+ * An {@link Activity} to take a screenshot for the App Clips flow and presenting a screenshot
+ * editing tool.
+ *
+ * <p>An App Clips flow includes:
+ * <ul>
+ * <li>Checking if calling activity meets the prerequisites. This is done by
+ * {@link AppClipsTrampolineActivity}.
+ * <li>Performing the screenshot.
+ * <li>Showing a screenshot editing tool.
+ * <li>Returning the screenshot to the {@link AppClipsTrampolineActivity} so that it can return
+ * the screenshot to the calling activity after explicit user consent.
+ * </ul>
+ *
+ * <p>This {@link Activity} runs in its own separate process to isolate memory intensive image
+ * editing from SysUI process.
+ *
+ * TODO(b/267309532): Polish UI and animations.
+ */
+public final class AppClipsActivity extends ComponentActivity {
+
+ private final AppClipsViewModel.Factory mViewModelFactory;
+ private final BroadcastReceiver mBroadcastReceiver;
+ private final IntentFilter mIntentFilter;
+
+ private View mLayout;
+ private View mRoot;
+ private ImageView mPreview;
+ private CropView mCropView;
+ private MagnifierView mMagnifierView;
+ private Button mSave;
+ private Button mCancel;
+ private AppClipsViewModel mViewModel;
+
+ private ResultReceiver mResultReceiver;
+
+ @Inject
+ public AppClipsActivity(AppClipsViewModel.Factory viewModelFactory) {
+ mViewModelFactory = viewModelFactory;
+
+ mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // Trampoline activity was dismissed so finish this activity.
+ if (ACTION_FINISH_FROM_TRAMPOLINE.equals(intent.getAction())) {
+ if (!isFinishing()) {
+ // Nullify the ResultReceiver so that result cannot be sent as trampoline
+ // activity is already finishing.
+ mResultReceiver = null;
+ finish();
+ }
+ }
+ }
+ };
+
+ mIntentFilter = new IntentFilter(ACTION_FINISH_FROM_TRAMPOLINE);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ overridePendingTransition(0, 0);
+ super.onCreate(savedInstanceState);
+
+ // Register the broadcast receiver that informs when the trampoline activity is dismissed.
+ registerReceiver(mBroadcastReceiver, mIntentFilter, PERMISSION_SELF, null,
+ RECEIVER_NOT_EXPORTED);
+
+ Intent intent = getIntent();
+ mResultReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER, ResultReceiver.class);
+ if (mResultReceiver == null) {
+ setErrorThenFinish(Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ return;
+ }
+
+ // Inflate layout but don't add it yet as it should be added after the screenshot is ready
+ // for preview.
+ mLayout = getLayoutInflater().inflate(R.layout.app_clips_screenshot, null);
+ mRoot = mLayout.findViewById(R.id.root);
+
+ mSave = mLayout.findViewById(R.id.save);
+ mCancel = mLayout.findViewById(R.id.cancel);
+ mSave.setOnClickListener(this::onClick);
+ mCancel.setOnClickListener(this::onClick);
+
+ mMagnifierView = mLayout.findViewById(R.id.magnifier);
+ mCropView = mLayout.findViewById(R.id.crop_view);
+ mCropView.setCropInteractionListener(mMagnifierView);
+
+ mPreview = mLayout.findViewById(R.id.preview);
+ mPreview.addOnLayoutChangeListener(
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
+ updateImageDimensions());
+
+ mViewModel = new ViewModelProvider(this, mViewModelFactory).get(AppClipsViewModel.class);
+ mViewModel.getScreenshot().observe(this, this::setScreenshot);
+ mViewModel.getResultLiveData().observe(this, this::setResultThenFinish);
+ mViewModel.getErrorLiveData().observe(this, this::setErrorThenFinish);
+
+ if (savedInstanceState == null) {
+ mViewModel.performScreenshot();
+ }
+ }
+
+ @Override
+ public void finish() {
+ super.finish();
+ overridePendingTransition(0, 0);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ unregisterReceiver(mBroadcastReceiver);
+
+ // If neither error nor result was set, it implies that the activity is finishing due to
+ // some other reason such as user dismissing this activity using back gesture. Inform error.
+ if (isFinishing() && mViewModel.getErrorLiveData().getValue() == null
+ && mViewModel.getResultLiveData().getValue() == null) {
+ // Set error but don't finish as the activity is already finishing.
+ setError(Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ }
+ }
+
+ private void setScreenshot(Bitmap screenshot) {
+ // Set background, status and navigation bar colors as the activity is no longer
+ // translucent.
+ int colorBackgroundFloating = Utils.getColorAttr(this,
+ android.R.attr.colorBackgroundFloating).getDefaultColor();
+ mRoot.setBackgroundColor(colorBackgroundFloating);
+
+ BitmapDrawable drawable = new BitmapDrawable(getResources(), screenshot);
+ mPreview.setImageDrawable(drawable);
+ mPreview.setAlpha(1f);
+
+ mMagnifierView.setDrawable(drawable, screenshot.getWidth(), screenshot.getHeight());
+
+ // Screenshot is now available so set content view.
+ setContentView(mLayout);
+ }
+
+ private void onClick(View view) {
+ mSave.setEnabled(false);
+ mCancel.setEnabled(false);
+
+ int id = view.getId();
+ if (id == R.id.save) {
+ saveScreenshotThenFinish();
+ } else {
+ setErrorThenFinish(Intent.CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED);
+ }
+ }
+
+ private void saveScreenshotThenFinish() {
+ Drawable drawable = mPreview.getDrawable();
+ if (drawable == null) {
+ setErrorThenFinish(Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ return;
+ }
+
+ Rect bounds = mCropView.getCropBoundaries(drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight());
+
+ if (bounds.isEmpty()) {
+ setErrorThenFinish(Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ return;
+ }
+
+ updateImageDimensions();
+ mViewModel.saveScreenshotThenFinish(drawable, bounds);
+ }
+
+ private void setResultThenFinish(Uri uri) {
+ if (mResultReceiver == null) {
+ return;
+ }
+
+ Bundle data = new Bundle();
+ data.putInt(Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE,
+ Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
+ data.putParcelable(EXTRA_SCREENSHOT_URI, uri);
+ try {
+ mResultReceiver.send(Activity.RESULT_OK, data);
+ } catch (Exception e) {
+ // Do nothing.
+ }
+
+ // Nullify the ResultReceiver before finishing to avoid resending the result.
+ mResultReceiver = null;
+ finish();
+ }
+
+ private void setErrorThenFinish(int errorCode) {
+ setError(errorCode);
+ finish();
+ }
+
+ private void setError(int errorCode) {
+ if (mResultReceiver == null) {
+ return;
+ }
+
+ Bundle data = new Bundle();
+ data.putInt(Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, errorCode);
+ try {
+ mResultReceiver.send(RESULT_OK, data);
+ } catch (Exception e) {
+ // Do nothing.
+ }
+
+ // Nullify the ResultReceiver to avoid resending the result.
+ mResultReceiver = null;
+ }
+
+ private void updateImageDimensions() {
+ Drawable drawable = mPreview.getDrawable();
+ if (drawable == null) {
+ return;
+ }
+
+ Rect bounds = drawable.getBounds();
+ float imageRatio = bounds.width() / (float) bounds.height();
+ int previewWidth = mPreview.getWidth() - mPreview.getPaddingLeft()
+ - mPreview.getPaddingRight();
+ int previewHeight = mPreview.getHeight() - mPreview.getPaddingTop()
+ - mPreview.getPaddingBottom();
+ float viewRatio = previewWidth / (float) previewHeight;
+
+ if (imageRatio > viewRatio) {
+ // Image is full width and height is constrained, compute extra padding to inform
+ // CropView.
+ int imageHeight = (int) (previewHeight * viewRatio / imageRatio);
+ int extraPadding = (previewHeight - imageHeight) / 2;
+ mCropView.setExtraPadding(extraPadding, extraPadding);
+ mCropView.setImageWidth(previewWidth);
+ } else {
+ // Image is full height.
+ mCropView.setExtraPadding(mPreview.getPaddingTop(), mPreview.getPaddingBottom());
+ mCropView.setImageWidth((int) (previewHeight * imageRatio));
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java
new file mode 100644
index 0000000..65fb4c9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.appclips;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.view.Display;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.infra.AndroidFuture;
+import com.android.internal.infra.ServiceConnector;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
+
+import javax.inject.Inject;
+
+/** An intermediary singleton object to help communicating with the cross process service. */
+@SysUISingleton
+public class AppClipsCrossProcessHelper {
+
+ private final ServiceConnector<IAppClipsScreenshotHelperService> mProxyConnector;
+
+ @Inject
+ public AppClipsCrossProcessHelper(@Application Context context) {
+ mProxyConnector = new ServiceConnector.Impl<IAppClipsScreenshotHelperService>(context,
+ new Intent(context, AppClipsScreenshotHelperService.class),
+ Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY
+ | Context.BIND_NOT_VISIBLE, context.getUserId(),
+ IAppClipsScreenshotHelperService.Stub::asInterface);
+ }
+
+ /**
+ * Returns a {@link Bitmap} captured in the SysUI process, {@code null} in case of an error.
+ *
+ * <p>Note: The SysUI process captures a {@link ScreenshotHardwareBufferInternal} which is ok to
+ * pass around but not a {@link Bitmap}.
+ */
+ @Nullable
+ public Bitmap takeScreenshot() {
+ try {
+ AndroidFuture<ScreenshotHardwareBufferInternal> future =
+ mProxyConnector.postForResult(
+ service -> service.takeScreenshot(Display.DEFAULT_DISPLAY));
+ return future.get().createBitmapThenCloseBuffer();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java
new file mode 100644
index 0000000..6f8c365
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.appclips;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.window.ScreenCapture.ScreenshotHardwareBuffer;
+import android.window.ScreenCapture.ScreenshotSync;
+
+import androidx.annotation.Nullable;
+
+import com.android.systemui.screenshot.AppClipsActivity;
+import com.android.wm.shell.bubbles.Bubbles;
+
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+/**
+ * A helper service that runs in SysUI process and helps {@link AppClipsActivity} which runs in its
+ * own separate process take a screenshot.
+ */
+public class AppClipsScreenshotHelperService extends Service {
+
+ private final Optional<Bubbles> mOptionalBubbles;
+
+ @Inject
+ public AppClipsScreenshotHelperService(Optional<Bubbles> optionalBubbles) {
+ mOptionalBubbles = optionalBubbles;
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return new IAppClipsScreenshotHelperService.Stub() {
+ @Override
+ @Nullable
+ public ScreenshotHardwareBufferInternal takeScreenshot(int displayId) {
+ if (mOptionalBubbles.isEmpty()) {
+ return null;
+ }
+
+ ScreenshotSync screenshotSync =
+ mOptionalBubbles.get().getScreenshotExcludingBubble(displayId);
+ ScreenshotHardwareBuffer screenshotHardwareBuffer = screenshotSync.get();
+ if (screenshotHardwareBuffer == null) {
+ return null;
+ }
+
+ return new ScreenshotHardwareBufferInternal(screenshotHardwareBuffer);
+ }
+ };
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
new file mode 100644
index 0000000..d0b7ad3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.appclips;
+
+import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS;
+
+import android.app.Activity;
+import android.app.Service;
+import android.app.StatusBarManager;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.IBinder;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.statusbar.IAppClipsService;
+import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.wm.shell.bubbles.Bubbles;
+
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+/**
+ * A service that communicates with {@link StatusBarManager} to support the
+ * {@link StatusBarManager#canLaunchCaptureContentActivityForNote(Activity)} API.
+ */
+public class AppClipsService extends Service {
+
+ @Application private final Context mContext;
+ private final FeatureFlags mFeatureFlags;
+ private final Optional<Bubbles> mOptionalBubbles;
+ private final DevicePolicyManager mDevicePolicyManager;
+ private final boolean mAreTaskAndTimeIndependentPrerequisitesMet;
+
+ @Inject
+ public AppClipsService(@Application Context context, FeatureFlags featureFlags,
+ Optional<Bubbles> optionalBubbles, DevicePolicyManager devicePolicyManager) {
+ mContext = context;
+ mFeatureFlags = featureFlags;
+ mOptionalBubbles = optionalBubbles;
+ mDevicePolicyManager = devicePolicyManager;
+
+ mAreTaskAndTimeIndependentPrerequisitesMet = checkIndependentVariables();
+ }
+
+ private boolean checkIndependentVariables() {
+ if (!mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)) {
+ return false;
+ }
+
+ if (mOptionalBubbles.isEmpty()) {
+ return false;
+ }
+
+ return isComponentValid();
+ }
+
+ private boolean isComponentValid() {
+ ComponentName componentName;
+ try {
+ componentName = ComponentName.unflattenFromString(
+ mContext.getString(R.string.config_screenshotAppClipsActivityComponent));
+ } catch (Resources.NotFoundException e) {
+ return false;
+ }
+
+ return componentName != null
+ && !componentName.getPackageName().isEmpty()
+ && !componentName.getClassName().isEmpty();
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return new IAppClipsService.Stub() {
+ @Override
+ public boolean canLaunchCaptureContentActivityForNote(int taskId) {
+ if (!mAreTaskAndTimeIndependentPrerequisitesMet) {
+ return false;
+ }
+
+ if (!mOptionalBubbles.get().isAppBubbleTaskId(taskId)) {
+ return false;
+ }
+
+ return !mDevicePolicyManager.getScreenCaptureDisabled(null);
+ }
+ };
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java
new file mode 100644
index 0000000..4759cc6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot;
+
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED;
+import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE;
+import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
+
+import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS;
+
+import android.app.Activity;
+import android.app.admin.DevicePolicyManager;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.ResultReceiver;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.notetask.NoteTaskController;
+import com.android.wm.shell.bubbles.Bubbles;
+
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+/**
+ * A trampoline activity that is responsible for:
+ * <ul>
+ * <li>Performing precondition checks before starting the actual screenshot activity.
+ * <li>Communicating with the screenshot activity and the calling activity.
+ * </ul>
+ *
+ * <p>As this activity is started in a bubble app, the windowing for this activity is restricted
+ * to the parent bubble app. The screenshot editing activity, see {@link AppClipsActivity}, is
+ * started in a regular activity window using {@link Intent#FLAG_ACTIVITY_NEW_TASK}. However,
+ * {@link Activity#startActivityForResult(Intent, int)} is not compatible with
+ * {@link Intent#FLAG_ACTIVITY_NEW_TASK}. So, this activity acts as a trampoline activity to
+ * abstract the complexity of communication with the screenshot editing activity for a simpler
+ * developer experience.
+ *
+ * TODO(b/267309532): Polish UI and animations.
+ */
+public class AppClipsTrampolineActivity extends Activity {
+
+ private static final String TAG = AppClipsTrampolineActivity.class.getSimpleName();
+ public static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
+ public static final String EXTRA_SCREENSHOT_URI = TAG + "SCREENSHOT_URI";
+ public static final String ACTION_FINISH_FROM_TRAMPOLINE = TAG + "FINISH_FROM_TRAMPOLINE";
+ static final String EXTRA_RESULT_RECEIVER = TAG + "RESULT_RECEIVER";
+
+ private final DevicePolicyManager mDevicePolicyManager;
+ private final FeatureFlags mFeatureFlags;
+ private final Optional<Bubbles> mOptionalBubbles;
+ private final NoteTaskController mNoteTaskController;
+ private final ResultReceiver mResultReceiver;
+
+ private Intent mKillAppClipsBroadcastIntent;
+
+ @Inject
+ public AppClipsTrampolineActivity(DevicePolicyManager devicePolicyManager, FeatureFlags flags,
+ Optional<Bubbles> optionalBubbles, NoteTaskController noteTaskController,
+ @Main Handler mainHandler) {
+ mDevicePolicyManager = devicePolicyManager;
+ mFeatureFlags = flags;
+ mOptionalBubbles = optionalBubbles;
+ mNoteTaskController = noteTaskController;
+
+ mResultReceiver = createResultReceiver(mainHandler);
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (savedInstanceState != null) {
+ return;
+ }
+
+ if (!mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)) {
+ finish();
+ return;
+ }
+
+ if (mOptionalBubbles.isEmpty()) {
+ setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ return;
+ }
+
+ if (!mOptionalBubbles.get().isAppBubbleTaskId(getTaskId())) {
+ setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED);
+ return;
+ }
+
+ if (mDevicePolicyManager.getScreenCaptureDisabled(null)) {
+ setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN);
+ return;
+ }
+
+ ComponentName componentName;
+ try {
+ componentName = ComponentName.unflattenFromString(
+ getString(R.string.config_screenshotAppClipsActivityComponent));
+ } catch (Resources.NotFoundException e) {
+ setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ return;
+ }
+
+ if (componentName == null || componentName.getPackageName().isEmpty()
+ || componentName.getClassName().isEmpty()) {
+ setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ return;
+ }
+
+ Intent intent = new Intent().setComponent(componentName).addFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK).putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver);
+ try {
+ // Start the App Clips activity.
+ startActivity(intent);
+
+ // Set up the broadcast intent that will inform the above App Clips activity to finish
+ // when this trampoline activity is finished.
+ mKillAppClipsBroadcastIntent =
+ new Intent(ACTION_FINISH_FROM_TRAMPOLINE)
+ .setComponent(componentName)
+ .setPackage(componentName.getPackageName());
+ } catch (ActivityNotFoundException e) {
+ setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ if (isFinishing() && mKillAppClipsBroadcastIntent != null) {
+ sendBroadcast(mKillAppClipsBroadcastIntent, PERMISSION_SELF);
+ }
+ }
+
+ private void setErrorResultAndFinish(int errorCode) {
+ setResult(RESULT_OK,
+ new Intent().putExtra(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, errorCode));
+ finish();
+ }
+
+ private class AppClipsResultReceiver extends ResultReceiver {
+
+ AppClipsResultReceiver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (isFinishing()) {
+ // It's too late, trampoline activity is finishing or already finished.
+ // Return early.
+ return;
+ }
+
+ // Package the response that should be sent to the calling activity.
+ Intent convertedData = new Intent();
+ int statusCode = CAPTURE_CONTENT_FOR_NOTE_FAILED;
+ if (resultData != null) {
+ statusCode = resultData.getInt(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE,
+ CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ }
+ convertedData.putExtra(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, statusCode);
+
+ if (statusCode == CAPTURE_CONTENT_FOR_NOTE_SUCCESS) {
+ Uri uri = resultData.getParcelable(EXTRA_SCREENSHOT_URI, Uri.class);
+ convertedData.setData(uri).addFlags(FLAG_GRANT_READ_URI_PERMISSION);
+ }
+
+ // Broadcast no longer required, setting it to null.
+ mKillAppClipsBroadcastIntent = null;
+
+ // Expand the note bubble before returning the result. As App Clips API is only
+ // available when in a bubble, isInMultiWindowMode is always false below.
+ mNoteTaskController.showNoteTask(false);
+ setResult(RESULT_OK, convertedData);
+ finish();
+ }
+ }
+
+ /**
+ * @return a {@link ResultReceiver} by initializing an {@link AppClipsResultReceiver} and
+ * converting it into a generic {@link ResultReceiver} to pass across a different but trusted
+ * process.
+ */
+ private ResultReceiver createResultReceiver(@Main Handler handler) {
+ AppClipsResultReceiver appClipsResultReceiver = new AppClipsResultReceiver(handler);
+ Parcel parcel = Parcel.obtain();
+ appClipsResultReceiver.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+
+ ResultReceiver resultReceiver = ResultReceiver.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+ return resultReceiver;
+ }
+
+ /** This is a test only API for mocking response from {@link AppClipsActivity}. */
+ @VisibleForTesting
+ public ResultReceiver getResultReceiverForTest() {
+ return mResultReceiver;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java
new file mode 100644
index 0000000..5a7b5f9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot;
+
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED;
+
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.HardwareRenderer;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.RenderNode;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Process;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.screenshot.appclips.AppClipsCrossProcessHelper;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.time.ZonedDateTime;
+import java.util.UUID;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+
+/** A {@link ViewModel} to help with the App Clips screenshot flow. */
+final class AppClipsViewModel extends ViewModel {
+
+ private final AppClipsCrossProcessHelper mAppClipsCrossProcessHelper;
+ private final ImageExporter mImageExporter;
+ @Main
+ private final Executor mMainExecutor;
+ @Background
+ private final Executor mBgExecutor;
+
+ private final MutableLiveData<Bitmap> mScreenshotLiveData;
+ private final MutableLiveData<Uri> mResultLiveData;
+ private final MutableLiveData<Integer> mErrorLiveData;
+
+ AppClipsViewModel(AppClipsCrossProcessHelper appClipsCrossProcessHelper,
+ ImageExporter imageExporter, @Main Executor mainExecutor,
+ @Background Executor bgExecutor) {
+ mAppClipsCrossProcessHelper = appClipsCrossProcessHelper;
+ mImageExporter = imageExporter;
+ mMainExecutor = mainExecutor;
+ mBgExecutor = bgExecutor;
+
+ mScreenshotLiveData = new MutableLiveData<>();
+ mResultLiveData = new MutableLiveData<>();
+ mErrorLiveData = new MutableLiveData<>();
+ }
+
+ /** Grabs a screenshot and updates the {@link Bitmap} set in screenshot {@link LiveData}. */
+ void performScreenshot() {
+ mBgExecutor.execute(() -> {
+ Bitmap screenshot = mAppClipsCrossProcessHelper.takeScreenshot();
+ mMainExecutor.execute(() -> {
+ if (screenshot == null) {
+ mErrorLiveData.setValue(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ } else {
+ mScreenshotLiveData.setValue(screenshot);
+ }
+ });
+ });
+ }
+
+ /** Returns a {@link LiveData} that holds the captured screenshot. */
+ LiveData<Bitmap> getScreenshot() {
+ return mScreenshotLiveData;
+ }
+
+ /** Returns a {@link LiveData} that holds the {@link Uri} where screenshot is saved. */
+ LiveData<Uri> getResultLiveData() {
+ return mResultLiveData;
+ }
+
+ /**
+ * Returns a {@link LiveData} that holds the error codes for
+ * {@link Intent#EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE}.
+ */
+ LiveData<Integer> getErrorLiveData() {
+ return mErrorLiveData;
+ }
+
+ /**
+ * Saves the provided {@link Drawable} to storage then informs the result {@link Uri} to
+ * {@link LiveData}.
+ */
+ void saveScreenshotThenFinish(Drawable screenshotDrawable, Rect bounds) {
+ mBgExecutor.execute(() -> {
+ // Render the screenshot bitmap in background.
+ Bitmap screenshotBitmap = renderBitmap(screenshotDrawable, bounds);
+
+ // Export and save the screenshot in background.
+ // TODO(b/267310185): Save to work profile UserHandle.
+ ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export(
+ mBgExecutor, UUID.randomUUID(), screenshotBitmap, ZonedDateTime.now(),
+ Process.myUserHandle());
+
+ // Get the result and update state on main thread.
+ exportFuture.addListener(() -> {
+ try {
+ ImageExporter.Result result = exportFuture.get();
+ if (result.uri == null) {
+ mErrorLiveData.setValue(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ return;
+ }
+
+ mResultLiveData.setValue(result.uri);
+ } catch (CancellationException | InterruptedException | ExecutionException e) {
+ mErrorLiveData.setValue(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ }
+ }, mMainExecutor);
+ });
+ }
+
+ private static Bitmap renderBitmap(Drawable drawable, Rect bounds) {
+ final RenderNode output = new RenderNode("Screenshot save");
+ output.setPosition(0, 0, bounds.width(), bounds.height());
+ RecordingCanvas canvas = output.beginRecording();
+ canvas.translate(-bounds.left, -bounds.top);
+ canvas.clipRect(bounds);
+ drawable.draw(canvas);
+ output.endRecording();
+ return HardwareRenderer.createHardwareBitmap(output, bounds.width(), bounds.height());
+ }
+
+ /** Helper factory to help with injecting {@link AppClipsViewModel}. */
+ static final class Factory implements ViewModelProvider.Factory {
+
+ private final AppClipsCrossProcessHelper mAppClipsCrossProcessHelper;
+ private final ImageExporter mImageExporter;
+ @Main
+ private final Executor mMainExecutor;
+ @Background
+ private final Executor mBgExecutor;
+
+ @Inject
+ Factory(AppClipsCrossProcessHelper appClipsCrossProcessHelper, ImageExporter imageExporter,
+ @Main Executor mainExecutor, @Background Executor bgExecutor) {
+ mAppClipsCrossProcessHelper = appClipsCrossProcessHelper;
+ mImageExporter = imageExporter;
+ mMainExecutor = mainExecutor;
+ mBgExecutor = bgExecutor;
+ }
+
+ @NonNull
+ @Override
+ public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
+ if (modelClass != AppClipsViewModel.class) {
+ throw new IllegalArgumentException();
+ }
+
+ //noinspection unchecked
+ return (T) new AppClipsViewModel(mAppClipsCrossProcessHelper, mImageExporter,
+ mMainExecutor, mBgExecutor);
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/IAppClipsScreenshotHelperService.aidl b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/IAppClipsScreenshotHelperService.aidl
new file mode 100644
index 0000000..640e742
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/IAppClipsScreenshotHelperService.aidl
@@ -0,0 +1,29 @@
+/**
+ * Copyright (C) 2023, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.appclips;
+
+import android.os.Bundle;
+
+import com.android.systemui.screenshot.appclips.ScreenshotHardwareBufferInternal;
+
+/**
+ * A helper service that runs in SysUI process and helps {@link AppClipsActivity} which runs in its
+ * own separate process take a screenshot.
+ */
+interface IAppClipsScreenshotHelperService {
+ @nullable ScreenshotHardwareBufferInternal takeScreenshot(in int displayId);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/ScreenshotHardwareBufferInternal.aidl b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/ScreenshotHardwareBufferInternal.aidl
new file mode 100644
index 0000000..3a7b944
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/ScreenshotHardwareBufferInternal.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (C) 2023, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.appclips;
+
+parcelable ScreenshotHardwareBufferInternal;
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/ScreenshotHardwareBufferInternal.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/ScreenshotHardwareBufferInternal.java
new file mode 100644
index 0000000..3b107f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/ScreenshotHardwareBufferInternal.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.appclips;
+
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
+import android.graphics.ParcelableColorSpace;
+import android.hardware.HardwareBuffer;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.window.ScreenCapture.ScreenshotHardwareBuffer;
+
+/**
+ * An internal version of {@link ScreenshotHardwareBuffer} that helps with parceling the information
+ * necessary for creating a {@link Bitmap}.
+ */
+public class ScreenshotHardwareBufferInternal implements Parcelable {
+
+ public static final Creator<ScreenshotHardwareBufferInternal> CREATOR =
+ new Creator<>() {
+ @Override
+ public ScreenshotHardwareBufferInternal createFromParcel(Parcel in) {
+ return new ScreenshotHardwareBufferInternal(in);
+ }
+
+ @Override
+ public ScreenshotHardwareBufferInternal[] newArray(int size) {
+ return new ScreenshotHardwareBufferInternal[size];
+ }
+ };
+ private final HardwareBuffer mHardwareBuffer;
+ private final ParcelableColorSpace mParcelableColorSpace;
+
+ public ScreenshotHardwareBufferInternal(
+ ScreenshotHardwareBuffer screenshotHardwareBuffer) {
+ mHardwareBuffer = screenshotHardwareBuffer.getHardwareBuffer();
+ mParcelableColorSpace = new ParcelableColorSpace(
+ screenshotHardwareBuffer.getColorSpace());
+ }
+
+ private ScreenshotHardwareBufferInternal(Parcel in) {
+ mHardwareBuffer = in.readParcelable(HardwareBuffer.class.getClassLoader(),
+ HardwareBuffer.class);
+ mParcelableColorSpace = in.readParcelable(ParcelableColorSpace.class.getClassLoader(),
+ ParcelableColorSpace.class);
+ }
+
+ /**
+ * Returns a {@link Bitmap} represented by the underlying data and successively closes the
+ * internal {@link HardwareBuffer}. See,
+ * {@link Bitmap#wrapHardwareBuffer(HardwareBuffer, ColorSpace)} and
+ * {@link HardwareBuffer#close()} for more information.
+ */
+ public Bitmap createBitmapThenCloseBuffer() {
+ Bitmap bitmap = Bitmap.wrapHardwareBuffer(mHardwareBuffer,
+ mParcelableColorSpace.getColorSpace());
+ mHardwareBuffer.close();
+ return bitmap;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mHardwareBuffer, flags);
+ dest.writeParcelable(mParcelableColorSpace, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ScreenshotHardwareBufferInternal)) {
+ return false;
+ }
+
+ ScreenshotHardwareBufferInternal other = (ScreenshotHardwareBufferInternal) obj;
+ return mHardwareBuffer.equals(other.mHardwareBuffer) && mParcelableColorSpace.equals(
+ other.mParcelableColorSpace);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
index fdb0100..22e238c0 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
@@ -24,6 +24,8 @@
import com.android.systemui.screenshot.ScreenshotPolicyImpl;
import com.android.systemui.screenshot.ScreenshotProxyService;
import com.android.systemui.screenshot.TakeScreenshotService;
+import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService;
+import com.android.systemui.screenshot.appclips.AppClipsService;
import dagger.Binds;
import dagger.Module;
@@ -52,4 +54,13 @@
@Binds
abstract ImageCapture bindImageCaptureImpl(ImageCaptureImpl capture);
+ @Binds
+ @IntoMap
+ @ClassKey(AppClipsScreenshotHelperService.class)
+ abstract Service bindAppClipsScreenshotHelperService(AppClipsScreenshotHelperService service);
+
+ @Binds
+ @IntoMap
+ @ClassKey(AppClipsService.class)
+ abstract Service bindAppClipsService(AppClipsService service);
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 479510a..f53f824 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -685,6 +685,7 @@
private boolean mInstantExpanding;
private boolean mAnimateAfterExpanding;
private boolean mIsFlinging;
+ private boolean mLastFlingWasExpanding;
private String mViewName;
private float mInitialExpandY;
private float mInitialExpandX;
@@ -2142,6 +2143,7 @@
@VisibleForTesting
void flingToHeight(float vel, boolean expand, float target,
float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
+ mLastFlingWasExpanding = expand;
mHeadsUpTouchHelper.notifyFling(!expand);
mKeyguardStateController.notifyPanelFlingStart(!expand /* flingingToDismiss */);
setClosingWithAlphaFadeout(!expand && !isOnKeyguard() && getFadeoutAlpha() == 1.0f);
@@ -2531,7 +2533,7 @@
}
// defer touches on QQS to shade while shade is collapsing. Added margin for error
// as sometimes the qsExpansionFraction can be a tiny value instead of 0 when in QQS.
- if (!mSplitShadeEnabled
+ if (!mSplitShadeEnabled && !mLastFlingWasExpanding
&& computeQsExpansionFraction() <= 0.01 && getExpandedFraction() < 1.0) {
mShadeLog.logMotionEvent(event,
"handleQsTouch: shade touched while collapsing, QS tracking disabled");
@@ -3151,17 +3153,11 @@
}
// The padding on this area is large enough that we can use a cheaper clipping strategy
mKeyguardStatusViewController.setClipBounds(clipStatusView ? mLastQsClipBounds : null);
- if (!qsVisible && mSplitShadeEnabled) {
- // On the lockscreen when qs isn't visible, we don't want the bounds of the shade to
- // be visible, otherwise you can see the bounds once swiping up to see bouncer
- mScrimController.setNotificationsBounds(0, 0, 0, 0);
- } else {
- // Increase the height of the notifications scrim when not in split shade
- // (e.g. portrait tablet) so the rounded corners are not visible at the bottom,
- // in this case they are rendered off-screen
- final int notificationsScrimBottom = mSplitShadeEnabled ? bottom : bottom + radius;
- mScrimController.setNotificationsBounds(left, top, right, notificationsScrimBottom);
- }
+ // Increase the height of the notifications scrim when not in split shade
+ // (e.g. portrait tablet) so the rounded corners are not visible at the bottom,
+ // in this case they are rendered off-screen
+ final int notificationsScrimBottom = mSplitShadeEnabled ? bottom : bottom + radius;
+ mScrimController.setNotificationsBounds(left, top, right, notificationsScrimBottom);
if (mSplitShadeEnabled) {
mKeyguardStatusBarViewController.setNoTopClipping();
@@ -3222,6 +3218,12 @@
private int calculateQsBottomPosition(float qsExpansionFraction) {
if (mTransitioningToFullShadeProgress > 0.0f) {
return mTransitionToFullShadeQSPosition;
+ } else if (mSplitShadeEnabled) {
+ // in split shade - outside lockscreen transition handled above - we simply jump between
+ // two qs expansion values - either shade is closed and qs expansion is 0 or shade is
+ // open and qs expansion is 1
+ int qsBottomTarget = mQs.getDesiredHeight() + mLargeScreenShadeHeaderHeight;
+ return qsExpansionFraction > 0 ? qsBottomTarget : 0;
} else {
int qsBottomYFrom = (int) getHeaderTranslation() + mQs.getQsMinExpansionHeight();
int expandedTopMargin = mUseLargeScreenShadeHeader ? mLargeScreenShadeHeaderHeight : 0;
@@ -4125,7 +4127,7 @@
if (didFaceAuthRun) {
mUpdateMonitor.requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT,
"lockScreenEmptySpaceTap");
} else {
mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_HINT,
@@ -6135,6 +6137,11 @@
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
+ if (mTracking) {
+ // TODO(b/247126247) fix underlying issue. Should be ACTION_POINTER_DOWN.
+ mShadeLog.d("Don't intercept down event while already tracking");
+ return false;
+ }
mCentralSurfaces.userActivity();
mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation;
mMinExpandHeight = 0.0f;
@@ -6222,6 +6229,11 @@
"onTouch: duplicate down event detected... ignoring");
return true;
}
+ if (mTracking) {
+ // TODO(b/247126247) fix underlying issue. Should be ACTION_POINTER_DOWN.
+ mShadeLog.d("Don't handle down event while already tracking");
+ return true;
+ }
mLastTouchDownTime = event.getDownTime();
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/OWNERS b/packages/SystemUI/src/com/android/systemui/shade/OWNERS
index d71fbf6..c8b6a2e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/shade/OWNERS
@@ -7,6 +7,7 @@
per-file NotificationsQuickSettingsContainer.java = kozynski@google.com, asc@google.com
per-file NotificationsQSContainerController.kt = kozynski@google.com, asc@google.com
per-file *ShadeHeader* = kozynski@google.com, asc@google.com
+per-file *Shade* = justinweir@google.com
per-file NotificationShadeWindowViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com
per-file NotificationShadeWindowView.java = pixel@google.com, cinek@google.com, juliacr@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
index 393279b..641131e 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
@@ -44,6 +44,11 @@
const val DREAM_SMARTSPACE_PRECONDITION = "dream_smartspace_precondition"
/**
+ * The BcSmartspaceDataPlugin for the standalone date (+alarm+dnd).
+ */
+ const val DATE_SMARTSPACE_DATA_PLUGIN = "date_smartspace_data_plugin"
+
+ /**
* The BcSmartspaceDataPlugin for the standalone weather.
*/
const val WEATHER_SMARTSPACE_DATA_PLUGIN = "weather_smartspace_data_plugin"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 3aaad87..2cf1f53 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -53,7 +53,6 @@
import android.os.RemoteException;
import android.util.Pair;
import android.util.SparseArray;
-import android.view.InsetsState.InternalInsetsType;
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsController.Appearance;
import android.view.WindowInsetsController.Behavior;
@@ -371,22 +370,22 @@
String packageName, LetterboxDetails[] letterboxDetails) { }
/**
- * @see IStatusBar#showTransient(int, int[], boolean).
+ * @see IStatusBar#showTransient(int, int, boolean).
*/
- default void showTransient(int displayId, @InternalInsetsType int[] types) { }
+ default void showTransient(int displayId, @InsetsType int types) { }
/**
- * @see IStatusBar#showTransient(int, int[], boolean).
+ * @see IStatusBar#showTransient(int, int, boolean).
*/
- default void showTransient(int displayId, @InternalInsetsType int[] types,
+ default void showTransient(int displayId, @InsetsType int types,
boolean isGestureOnSystemBar) {
showTransient(displayId, types);
}
/**
- * @see IStatusBar#abortTransient(int, int[]).
+ * @see IStatusBar#abortTransient(int, int).
*/
- default void abortTransient(int displayId, @InternalInsetsType int[] types) { }
+ default void abortTransient(int displayId, @InsetsType int types) { }
/**
* Called to notify System UI that a warning about the device going to sleep
@@ -1131,17 +1130,23 @@
}
@Override
- public void showTransient(int displayId, int[] types, boolean isGestureOnSystemBar) {
+ public void showTransient(int displayId, int types, boolean isGestureOnSystemBar) {
synchronized (mLock) {
- mHandler.obtainMessage(MSG_SHOW_TRANSIENT, displayId, isGestureOnSystemBar ? 1 : 0,
- types).sendToTarget();
+ SomeArgs args = SomeArgs.obtain();
+ args.argi1 = displayId;
+ args.argi2 = types;
+ args.argi3 = isGestureOnSystemBar ? 1 : 0;
+ mHandler.obtainMessage(MSG_SHOW_TRANSIENT, args).sendToTarget();
}
}
@Override
- public void abortTransient(int displayId, int[] types) {
+ public void abortTransient(int displayId, int types) {
synchronized (mLock) {
- mHandler.obtainMessage(MSG_ABORT_TRANSIENT, displayId, 0, types).sendToTarget();
+ SomeArgs args = SomeArgs.obtain();
+ args.argi1 = displayId;
+ args.argi2 = types;
+ mHandler.obtainMessage(MSG_ABORT_TRANSIENT, args).sendToTarget();
}
}
@@ -1644,17 +1649,21 @@
args.recycle();
break;
case MSG_SHOW_TRANSIENT: {
- final int displayId = msg.arg1;
- final int[] types = (int[]) msg.obj;
- final boolean isGestureOnSystemBar = msg.arg2 != 0;
+ args = (SomeArgs) msg.obj;
+ final int displayId = args.argi1;
+ final int types = args.argi2;
+ final boolean isGestureOnSystemBar = args.argi3 != 0;
+ args.recycle();
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).showTransient(displayId, types, isGestureOnSystemBar);
}
break;
}
case MSG_ABORT_TRANSIENT: {
- final int displayId = msg.arg1;
- final int[] types = (int[]) msg.obj;
+ args = (SomeArgs) msg.obj;
+ final int displayId = args.argi1;
+ final int types = args.argi2;
+ args.recycle();
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).abortTransient(displayId, types);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 1c4e319..8d68bce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -26,6 +26,7 @@
import static android.view.View.VISIBLE;
import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_RECOGNIZED;
+import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED;
import static com.android.keyguard.KeyguardUpdateMonitor.getCurrentUser;
import static com.android.systemui.DejankUtils.whitelistIpcs;
import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.IMPORTANT_MSG_MIN_DURATION;
@@ -551,23 +552,23 @@
.build(),
true
);
- if (!TextUtils.isEmpty(mBiometricMessageFollowUp)) {
- mRotateTextViewController.updateIndication(
- INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
- new KeyguardIndication.Builder()
- .setMessage(mBiometricMessageFollowUp)
- .setMinVisibilityMillis(IMPORTANT_MSG_MIN_DURATION)
- .setTextColor(mInitialTextColorState)
- .build(),
- true
- );
- } else {
- mRotateTextViewController.hideIndication(
- INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP);
- }
} else {
- mRotateTextViewController.hideIndication(INDICATION_TYPE_BIOMETRIC_MESSAGE);
- mRotateTextViewController.hideIndication(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP);
+ mRotateTextViewController.hideIndication(
+ INDICATION_TYPE_BIOMETRIC_MESSAGE);
+ }
+ if (!TextUtils.isEmpty(mBiometricMessageFollowUp)) {
+ mRotateTextViewController.updateIndication(
+ INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+ new KeyguardIndication.Builder()
+ .setMessage(mBiometricMessageFollowUp)
+ .setMinVisibilityMillis(IMPORTANT_MSG_MIN_DURATION)
+ .setTextColor(mInitialTextColorState)
+ .build(),
+ true
+ );
+ } else {
+ mRotateTextViewController.hideIndication(
+ INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP);
}
}
@@ -796,7 +797,8 @@
*/
private void showBiometricMessage(CharSequence biometricMessage,
@Nullable CharSequence biometricMessageFollowUp) {
- if (TextUtils.equals(biometricMessage, mBiometricMessage)) {
+ if (TextUtils.equals(biometricMessage, mBiometricMessage)
+ && TextUtils.equals(biometricMessageFollowUp, mBiometricMessageFollowUp)) {
return;
}
@@ -805,7 +807,8 @@
mHandler.removeMessages(MSG_SHOW_ACTION_TO_UNLOCK);
hideBiometricMessageDelayed(
- mBiometricMessageFollowUp != null
+ !TextUtils.isEmpty(mBiometricMessage)
+ && !TextUtils.isEmpty(mBiometricMessageFollowUp)
? IMPORTANT_MSG_MIN_DURATION * 2
: DEFAULT_HIDE_DELAY_MS
);
@@ -1103,6 +1106,8 @@
&& msgId != BIOMETRIC_HELP_FACE_NOT_RECOGNIZED;
final boolean faceAuthFailed = biometricSourceType == FACE
&& msgId == BIOMETRIC_HELP_FACE_NOT_RECOGNIZED; // ran through matcher & failed
+ final boolean fpAuthFailed = biometricSourceType == FINGERPRINT
+ && msgId == BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED; // ran matcher & failed
final boolean isUnlockWithFingerprintPossible = canUnlockWithFingerprint();
final boolean isCoExFaceAcquisitionMessage =
faceAuthSoftError && isUnlockWithFingerprintPossible;
@@ -1125,6 +1130,22 @@
mContext.getString(R.string.keyguard_face_failed),
mContext.getString(R.string.keyguard_suggest_fingerprint)
);
+ } else if (fpAuthFailed
+ && mKeyguardUpdateMonitor.getUserUnlockedWithFace(getCurrentUser())) {
+ // face had already previously unlocked the device, so instead of showing a
+ // fingerprint error, tell them they have already unlocked with face auth
+ // and how to enter their device
+ showBiometricMessage(
+ mContext.getString(R.string.keyguard_face_successful_unlock),
+ mContext.getString(R.string.keyguard_unlock)
+ );
+ } else if (fpAuthFailed
+ && mKeyguardUpdateMonitor.getUserHasTrust(
+ KeyguardUpdateMonitor.getCurrentUser())) {
+ showBiometricMessage(
+ getTrustGrantedIndication(),
+ mContext.getString(R.string.keyguard_unlock)
+ );
} else {
showBiometricMessage(helpString);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index 5b62d30..81c7197 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -31,10 +31,12 @@
import android.os.UserHandle
import android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS
import android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS
+import android.provider.Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED
import android.util.Log
import android.view.ContextThemeWrapper
import android.view.View
import android.view.ViewGroup
+import com.android.keyguard.KeyguardUpdateMonitor
import com.android.settingslib.Utils
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
@@ -52,12 +54,15 @@
import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.regionsampling.RegionSampler
import com.android.systemui.shared.regionsampling.UpdateColorCallback
+import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DATE_SMARTSPACE_DATA_PLUGIN
import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.WEATHER_SMARTSPACE_DATA_PLUGIN
+import com.android.systemui.statusbar.Weather
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.util.concurrency.Execution
import com.android.systemui.util.settings.SecureSettings
+import java.time.Instant
import java.util.Optional
import java.util.concurrent.Executor
import javax.inject.Inject
@@ -80,10 +85,13 @@
private val statusBarStateController: StatusBarStateController,
private val deviceProvisionedController: DeviceProvisionedController,
private val bypassController: KeyguardBypassController,
+ private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val execution: Execution,
@Main private val uiExecutor: Executor,
@Background private val bgExecutor: Executor,
@Main private val handler: Handler,
+ @Named(DATE_SMARTSPACE_DATA_PLUGIN)
+ optionalDatePlugin: Optional<BcSmartspaceDataPlugin>,
@Named(WEATHER_SMARTSPACE_DATA_PLUGIN)
optionalWeatherPlugin: Optional<BcSmartspaceDataPlugin>,
optionalPlugin: Optional<BcSmartspaceDataPlugin>,
@@ -94,6 +102,7 @@
}
private var session: SmartspaceSession? = null
+ private val datePlugin: BcSmartspaceDataPlugin? = optionalDatePlugin.orElse(null)
private val weatherPlugin: BcSmartspaceDataPlugin? = optionalWeatherPlugin.orElse(null)
private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null)
private val configPlugin: BcSmartspaceConfigPlugin? = optionalConfigPlugin.orElse(null)
@@ -161,6 +170,17 @@
}
isContentUpdatedOnce = true
}
+
+ val now = Instant.now()
+ val weatherTarget = targets.find { t ->
+ t.featureType == SmartspaceTarget.FEATURE_WEATHER &&
+ now.isAfter(Instant.ofEpochMilli(t.creationTimeMillis)) &&
+ now.isBefore(Instant.ofEpochMilli(t.expiryTimeMillis))
+ }
+ if (weatherTarget != null) {
+ val weatherData = Weather.fromBundle(weatherTarget.baseAction.extras)
+ keyguardUpdateMonitor.sendWeatherData(weatherData)
+ }
}
private val userTrackerCallback = object : UserTracker.Callback {
@@ -223,7 +243,18 @@
execution.assertIsMainThread()
return featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED) &&
- weatherPlugin != null
+ datePlugin != null && weatherPlugin != null
+ }
+
+ fun isWeatherEnabled(): Boolean {
+ execution.assertIsMainThread()
+ val defaultValue = context.getResources().getBoolean(
+ com.android.internal.R.bool.config_lockscreenWeatherEnabledByDefault)
+ val showWeather = secureSettings.getIntForUser(
+ LOCK_SCREEN_WEATHER_ENABLED,
+ if (defaultValue) 1 else 0,
+ userTracker.userId) == 1
+ return showWeather
}
private fun updateBypassEnabled() {
@@ -232,6 +263,25 @@
}
/**
+ * Constructs the date view and connects it to the smartspace service.
+ */
+ fun buildAndConnectDateView(parent: ViewGroup): View? {
+ execution.assertIsMainThread()
+
+ if (!isEnabled()) {
+ throw RuntimeException("Cannot build view when not enabled")
+ }
+ if (!isDateWeatherDecoupled()) {
+ throw RuntimeException("Cannot build date view when not decoupled")
+ }
+
+ val view = buildView(parent, datePlugin)
+ connectSession()
+
+ return view
+ }
+
+ /**
* Constructs the weather view and connects it to the smartspace service.
*/
fun buildAndConnectWeatherView(parent: ViewGroup): View? {
@@ -309,7 +359,7 @@
}
private fun connectSession() {
- if (weatherPlugin == null && plugin == null) return
+ if (datePlugin == null && weatherPlugin == null && plugin == null) return
if (session != null || smartspaceViews.isEmpty()) {
return
}
@@ -347,6 +397,7 @@
statusBarStateController.addCallback(statusBarStateListener)
bypassController.registerOnBypassStateChangedListener(bypassStateChangedListener)
+ datePlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
weatherPlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
plugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
@@ -384,6 +435,8 @@
bypassController.unregisterOnBypassStateChangedListener(bypassStateChangedListener)
session = null
+ datePlugin?.registerSmartspaceEventNotifier(null)
+
weatherPlugin?.registerSmartspaceEventNotifier(null)
weatherPlugin?.onTargetsAvailable(emptyList())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DeviceProvisionedCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DeviceProvisionedCoordinator.java
index 0c95eab..058042c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DeviceProvisionedCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DeviceProvisionedCoordinator.java
@@ -71,9 +71,11 @@
* marking them as relevant for setup are allowed to show when device is unprovisioned
*/
private boolean showNotificationEvenIfUnprovisioned(StatusBarNotification sbn) {
- // system_server checks the permission so systemui can just check whether the
- // extra exists
- return sbn.getNotification().extras.getBoolean(Notification.EXTRA_ALLOW_DURING_SETUP);
+ final boolean hasPermission = checkUidPermission(
+ Manifest.permission.NOTIFICATION_DURING_SETUP,
+ sbn.getUid()) == PackageManager.PERMISSION_GRANTED;
+ return hasPermission
+ && sbn.getNotification().extras.getBoolean(Notification.EXTRA_ALLOW_DURING_SETUP);
}
private int checkUidPermission(String permission, int uid) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
index 5f6a5cb..26f97de 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
@@ -63,7 +63,7 @@
* are not.
*/
public class NotificationLogger implements StateListener {
- private static final String TAG = "NotificationLogger";
+ static final String TAG = "NotificationLogger";
private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
/** The minimum delay in ms between reports of notification visibility. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt
index ec8501a..cc1103d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt
@@ -18,6 +18,7 @@
package com.android.systemui.statusbar.notification.logging
import android.app.StatsManager
+import android.util.Log
import android.util.StatsEvent
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
@@ -25,6 +26,7 @@
import com.android.systemui.shared.system.SysUiStatsLog
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.util.traceSection
+import java.lang.Exception
import java.util.concurrent.Executor
import javax.inject.Inject
import kotlin.math.roundToInt
@@ -82,43 +84,56 @@
return StatsManager.PULL_SKIP
}
- // Notifications can only be retrieved on the main thread, so switch to that thread.
- val notifications = getAllNotificationsOnMainThread()
- val notificationMemoryUse =
- NotificationMemoryMeter.notificationMemoryUse(notifications)
- .sortedWith(
- compareBy(
- { it.packageName },
- { it.objectUsage.style },
- { it.notificationKey }
+ try {
+ // Notifications can only be retrieved on the main thread, so switch to that thread.
+ val notifications = getAllNotificationsOnMainThread()
+ val notificationMemoryUse =
+ NotificationMemoryMeter.notificationMemoryUse(notifications)
+ .sortedWith(
+ compareBy(
+ { it.packageName },
+ { it.objectUsage.style },
+ { it.notificationKey }
+ )
+ )
+ val usageData = aggregateMemoryUsageData(notificationMemoryUse)
+ usageData.forEach { (_, use) ->
+ data.add(
+ SysUiStatsLog.buildStatsEvent(
+ SysUiStatsLog.NOTIFICATION_MEMORY_USE,
+ use.uid,
+ use.style,
+ use.count,
+ use.countWithInflatedViews,
+ toKb(use.smallIconObject),
+ use.smallIconBitmapCount,
+ toKb(use.largeIconObject),
+ use.largeIconBitmapCount,
+ toKb(use.bigPictureObject),
+ use.bigPictureBitmapCount,
+ toKb(use.extras),
+ toKb(use.extenders),
+ toKb(use.smallIconViews),
+ toKb(use.largeIconViews),
+ toKb(use.systemIconViews),
+ toKb(use.styleViews),
+ toKb(use.customViews),
+ toKb(use.softwareBitmaps),
+ use.seenCount
)
)
- val usageData = aggregateMemoryUsageData(notificationMemoryUse)
- usageData.forEach { (_, use) ->
- data.add(
- SysUiStatsLog.buildStatsEvent(
- SysUiStatsLog.NOTIFICATION_MEMORY_USE,
- use.uid,
- use.style,
- use.count,
- use.countWithInflatedViews,
- toKb(use.smallIconObject),
- use.smallIconBitmapCount,
- toKb(use.largeIconObject),
- use.largeIconBitmapCount,
- toKb(use.bigPictureObject),
- use.bigPictureBitmapCount,
- toKb(use.extras),
- toKb(use.extenders),
- toKb(use.smallIconViews),
- toKb(use.largeIconViews),
- toKb(use.systemIconViews),
- toKb(use.styleViews),
- toKb(use.customViews),
- toKb(use.softwareBitmaps),
- use.seenCount
- )
- )
+ }
+ } catch (e: InterruptedException) {
+ // This can happen if the device is sleeping or view walking takes too long.
+ // The statsd collector will interrupt the thread and we need to handle it
+ // gracefully.
+ Log.w(NotificationLogger.TAG, "Timed out when measuring notification memory.", e)
+ return@traceSection StatsManager.PULL_SKIP
+ } catch (e: Exception) {
+ // Error while collecting data, this should not crash prod SysUI. Just
+ // log WTF and move on.
+ Log.wtf(NotificationLogger.TAG, "Failed to measure notification memory.", e)
+ return@traceSection StatsManager.PULL_SKIP
}
return StatsManager.PULL_SUCCESS
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
index 2d04211..6491223 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
@@ -184,19 +184,21 @@
private fun computeDrawableUse(drawable: Drawable, seenObjects: HashSet<Int>): Int =
when (drawable) {
is BitmapDrawable -> {
- val ref = System.identityHashCode(drawable.bitmap)
- if (seenObjects.contains(ref)) {
- 0
- } else {
- seenObjects.add(ref)
- drawable.bitmap.allocationByteCount
- }
+ drawable.bitmap?.let {
+ val ref = System.identityHashCode(it)
+ if (seenObjects.contains(ref)) {
+ 0
+ } else {
+ seenObjects.add(ref)
+ it.allocationByteCount
+ }
+ } ?: 0
}
else -> 0
}
private fun isDrawableSoftwareBitmap(drawable: Drawable) =
- drawable is BitmapDrawable && drawable.bitmap.config != Bitmap.Config.HARDWARE
+ drawable is BitmapDrawable && drawable.bitmap?.config != Bitmap.Config.HARDWARE
private fun identifierForView(view: View) =
if (view.id == View.NO_ID) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index aab36da..1fb7eb5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -147,7 +147,7 @@
private boolean mShadeNeedsToClose = false;
@VisibleForTesting
- static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f;
+ static final float RUBBER_BAND_FACTOR_NORMAL = 0.1f;
private static final float RUBBER_BAND_FACTOR_AFTER_EXPAND = 0.15f;
private static final float RUBBER_BAND_FACTOR_ON_PANEL_EXPAND = 0.21f;
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index 856d7de..fecaa3a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -16,9 +16,6 @@
package com.android.systemui.statusbar.phone;
-import static android.view.InsetsState.ITYPE_STATUS_BAR;
-import static android.view.InsetsState.containsType;
-
import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE;
import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING;
@@ -36,8 +33,8 @@
import android.os.Vibrator;
import android.util.Log;
import android.util.Slog;
-import android.view.InsetsState.InternalInsetsType;
import android.view.KeyEvent;
+import android.view.WindowInsets;
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsController.Appearance;
import android.view.WindowInsetsController.Behavior;
@@ -168,11 +165,11 @@
}
@Override
- public void abortTransient(int displayId, @InternalInsetsType int[] types) {
+ public void abortTransient(int displayId, @InsetsType int types) {
if (displayId != mDisplayId) {
return;
}
- if (!containsType(types, ITYPE_STATUS_BAR)) {
+ if ((types & WindowInsets.Type.statusBars()) == 0) {
return;
}
mCentralSurfaces.clearTransient();
@@ -489,12 +486,11 @@
}
@Override
- public void showTransient(int displayId, @InternalInsetsType int[] types,
- boolean isGestureOnSystemBar) {
+ public void showTransient(int displayId, @InsetsType int types, boolean isGestureOnSystemBar) {
if (displayId != mDisplayId) {
return;
}
- if (!containsType(types, ITYPE_STATUS_BAR)) {
+ if ((types & WindowInsets.Type.statusBars()) == 0) {
return;
}
mCentralSurfaces.showTransientUnchecked();
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 e595ddf..1966a66 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -21,8 +21,6 @@
import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
import static android.app.StatusBarManager.WindowVisibleState;
import static android.app.StatusBarManager.windowStateToString;
-import static android.view.InsetsState.ITYPE_STATUS_BAR;
-import static android.view.InsetsState.containsType;
import static android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS;
import static android.view.WindowInsetsController.APPEARANCE_OPAQUE_STATUS_BARS;
import static android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS;
@@ -100,6 +98,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewRootImpl;
+import android.view.WindowInsets;
import android.view.WindowInsetsController.Appearance;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
@@ -943,7 +942,7 @@
// Set up the initial notification state. This needs to happen before CommandQueue.disable()
setUpPresenter();
- if (containsType(result.mTransientBarTypes, ITYPE_STATUS_BAR)) {
+ if ((result.mTransientBarTypes & WindowInsets.Type.statusBars()) != 0) {
showTransientUnchecked();
}
mCommandQueueCallbacks.onSystemBarAttributesChanged(mDisplayId, result.mAppearance,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
index c248a50..b88531e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
@@ -352,7 +352,7 @@
}
private boolean willAnimateFromLockScreenToAod() {
- return getAlwaysOn() && mKeyguardVisible;
+ return shouldControlScreenOff() && mKeyguardVisible;
}
private boolean getBoolean(String propName, int resId) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
index 01af486..c163a89 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
@@ -89,14 +89,19 @@
private float mPanelExpansion;
/**
- * Burn-in prevention x translation.
+ * Max burn-in prevention x translation.
*/
- private int mBurnInPreventionOffsetX;
+ private int mMaxBurnInPreventionOffsetX;
/**
- * Burn-in prevention y translation for clock layouts.
+ * Max burn-in prevention y translation for clock layouts.
*/
- private int mBurnInPreventionOffsetYClock;
+ private int mMaxBurnInPreventionOffsetYClock;
+
+ /**
+ * Current burn-in prevention y translation.
+ */
+ private float mCurrentBurnInOffsetY;
/**
* Doze/AOD transition amount.
@@ -155,9 +160,9 @@
mContainerTopPadding =
res.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
- mBurnInPreventionOffsetX = res.getDimensionPixelSize(
+ mMaxBurnInPreventionOffsetX = res.getDimensionPixelSize(
R.dimen.burn_in_prevention_offset_x);
- mBurnInPreventionOffsetYClock = res.getDimensionPixelSize(
+ mMaxBurnInPreventionOffsetYClock = res.getDimensionPixelSize(
R.dimen.burn_in_prevention_offset_y_clock);
}
@@ -215,7 +220,10 @@
if (mBypassEnabled) {
return (int) (mUnlockedStackScrollerPadding + mOverStretchAmount);
} else if (mIsSplitShade) {
- return clockYPosition - mSplitShadeTopNotificationsMargin + mUserSwitchHeight;
+ // mCurrentBurnInOffsetY is subtracted to make notifications not follow clock adjustment
+ // for burn-in. It can make pulsing notification go too high and it will get clipped
+ return clockYPosition - mSplitShadeTopNotificationsMargin + mUserSwitchHeight
+ - (int) mCurrentBurnInOffsetY;
} else {
return clockYPosition + mKeyguardStatusHeight;
}
@@ -255,11 +263,11 @@
// This will keep the clock at the top but out of the cutout area
float shift = 0;
- if (clockY - mBurnInPreventionOffsetYClock < mCutoutTopInset) {
- shift = mCutoutTopInset - (clockY - mBurnInPreventionOffsetYClock);
+ if (clockY - mMaxBurnInPreventionOffsetYClock < mCutoutTopInset) {
+ shift = mCutoutTopInset - (clockY - mMaxBurnInPreventionOffsetYClock);
}
- int burnInPreventionOffsetY = mBurnInPreventionOffsetYClock; // requested offset
+ int burnInPreventionOffsetY = mMaxBurnInPreventionOffsetYClock; // requested offset
final boolean hasUdfps = mUdfpsTop > -1;
if (hasUdfps && !mIsClockTopAligned) {
// ensure clock doesn't overlap with the udfps icon
@@ -267,8 +275,8 @@
// sometimes the clock textView extends beyond udfps, so let's just use the
// space above the KeyguardStatusView/clock as our burn-in offset
burnInPreventionOffsetY = (int) (clockY - mCutoutTopInset) / 2;
- if (mBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
- burnInPreventionOffsetY = mBurnInPreventionOffsetYClock;
+ if (mMaxBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
+ burnInPreventionOffsetY = mMaxBurnInPreventionOffsetYClock;
}
shift = -burnInPreventionOffsetY;
} else {
@@ -276,16 +284,18 @@
float lowerSpace = mUdfpsTop - mClockBottom;
// center the burn-in offset within the upper + lower space
burnInPreventionOffsetY = (int) (lowerSpace + upperSpace) / 2;
- if (mBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
- burnInPreventionOffsetY = mBurnInPreventionOffsetYClock;
+ if (mMaxBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
+ burnInPreventionOffsetY = mMaxBurnInPreventionOffsetYClock;
}
shift = (lowerSpace - upperSpace) / 2;
}
}
+ float fullyDarkBurnInOffset = burnInPreventionOffsetY(burnInPreventionOffsetY);
float clockYDark = clockY
- + burnInPreventionOffsetY(burnInPreventionOffsetY)
+ + fullyDarkBurnInOffset
+ shift;
+ mCurrentBurnInOffsetY = MathUtils.lerp(0, fullyDarkBurnInOffset, darkAmount);
return (int) (MathUtils.lerp(clockY, clockYDark, darkAmount) + mOverStretchAmount);
}
@@ -325,7 +335,7 @@
}
private float burnInPreventionOffsetX() {
- return getBurnInOffset(mBurnInPreventionOffsetX, true /* xAxis */);
+ return getBurnInOffset(mMaxBurnInPreventionOffsetX, true /* xAxis */);
}
public static class Result {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
index 4550cb2..8ee2c6f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
@@ -76,7 +76,7 @@
FaceAuthApiRequestReason.PICK_UP_GESTURE_TRIGGERED
)
keyguardUpdateMonitor.requestActiveUnlock(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE,
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE,
"KeyguardLiftController")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
index 344d233..c1c6c88 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
@@ -23,6 +23,7 @@
import android.view.View;
import android.view.ViewStub;
+import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.LockIconView;
import com.android.systemui.R;
import com.android.systemui.battery.BatteryMeterView;
@@ -67,6 +68,7 @@
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateController;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.util.CarrierConfigTracker;
import com.android.systemui.util.settings.SecureSettings;
@@ -299,7 +301,9 @@
OperatorNameViewController.Factory operatorNameViewControllerFactory,
SecureSettings secureSettings,
@Main Executor mainExecutor,
- DumpManager dumpManager
+ DumpManager dumpManager,
+ StatusBarWindowStateController statusBarWindowStateController,
+ KeyguardUpdateMonitor keyguardUpdateMonitor
) {
return new CollapsedStatusBarFragment(statusBarFragmentComponentFactory,
ongoingCallController,
@@ -320,7 +324,9 @@
operatorNameViewControllerFactory,
secureSettings,
mainExecutor,
- dumpManager);
+ dumpManager,
+ statusBarWindowStateController,
+ keyguardUpdateMonitor);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index cbd27cf..65becf7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -44,6 +44,7 @@
import androidx.annotation.VisibleForTesting;
+import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.Dumpable;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
@@ -72,6 +73,8 @@
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallListener;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateListener;
import com.android.systemui.util.CarrierConfigTracker;
import com.android.systemui.util.CarrierConfigTracker.CarrierConfigChangedListener;
import com.android.systemui.util.CarrierConfigTracker.DefaultDataSubscriptionChangedListener;
@@ -129,6 +132,8 @@
private final SecureSettings mSecureSettings;
private final Executor mMainExecutor;
private final DumpManager mDumpManager;
+ private final StatusBarWindowStateController mStatusBarWindowStateController;
+ private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
private List<String> mBlockedIcons = new ArrayList<>();
private Map<Startable, Startable.State> mStartableStates = new ArrayMap<>();
@@ -164,6 +169,22 @@
}
};
+ /**
+ * Whether we've launched the secure camera over the lockscreen, but haven't yet received a
+ * status bar window state change afterward.
+ *
+ * We wait for this state change (which will tell us whether to show/hide the status bar icons)
+ * so that there is no flickering/jump cutting during the camera launch.
+ */
+ private boolean mWaitingForWindowStateChangeAfterCameraLaunch = false;
+
+ /**
+ * Listener that updates {@link #mWaitingForWindowStateChangeAfterCameraLaunch} when it receives
+ * a new status bar window state.
+ */
+ private final StatusBarWindowStateListener mStatusBarWindowStateListener = state ->
+ mWaitingForWindowStateChangeAfterCameraLaunch = false;
+
@SuppressLint("ValidFragment")
public CollapsedStatusBarFragment(
StatusBarFragmentComponent.Factory statusBarFragmentComponentFactory,
@@ -185,7 +206,9 @@
OperatorNameViewController.Factory operatorNameViewControllerFactory,
SecureSettings secureSettings,
@Main Executor mainExecutor,
- DumpManager dumpManager
+ DumpManager dumpManager,
+ StatusBarWindowStateController statusBarWindowStateController,
+ KeyguardUpdateMonitor keyguardUpdateMonitor
) {
mStatusBarFragmentComponentFactory = statusBarFragmentComponentFactory;
mOngoingCallController = ongoingCallController;
@@ -207,6 +230,20 @@
mSecureSettings = secureSettings;
mMainExecutor = mainExecutor;
mDumpManager = dumpManager;
+ mStatusBarWindowStateController = statusBarWindowStateController;
+ mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mStatusBarWindowStateController.addListener(mStatusBarWindowStateListener);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mStatusBarWindowStateController.removeListener(mStatusBarWindowStateListener);
}
@Override
@@ -254,6 +291,11 @@
mCarrierConfigTracker.addDefaultDataSubscriptionChangedListener(mDefaultDataListener);
}
+ @Override
+ public void onCameraLaunchGestureDetected(int source) {
+ mWaitingForWindowStateChangeAfterCameraLaunch = true;
+ }
+
@VisibleForTesting
void updateBlockedIcons() {
mBlockedIcons.clear();
@@ -466,6 +508,27 @@
&& mNotificationPanelViewController.hideStatusBarIconsWhenExpanded()) {
return true;
}
+
+ // When launching the camera over the lockscreen, the icons become visible momentarily
+ // before animating out, since we're not yet aware that the launching camera activity is
+ // fullscreen. Even once the activity finishes launching, it takes a short time before WM
+ // decides that the top app wants to hide the icons and tells us to hide them. To ensure
+ // that this high-visibility animation is smooth, keep the icons hidden during a camera
+ // launch until we receive a window state change which indicates that the activity is done
+ // launching and WM has decided to show/hide the icons. For extra safety (to ensure the
+ // icons don't remain hidden somehow) we double check that the camera is still showing, the
+ // status bar window isn't hidden, and we're still occluded as well, though these checks
+ // are typically unnecessary.
+ final boolean hideIconsForSecureCamera =
+ (mWaitingForWindowStateChangeAfterCameraLaunch ||
+ !mStatusBarWindowStateController.windowIsShowing()) &&
+ mKeyguardUpdateMonitor.isSecureCameraLaunchedOverKeyguard() &&
+ mKeyguardStateController.isOccluded();
+
+ if (hideIconsForSecureCamera) {
+ return true;
+ }
+
return mStatusBarHideIconsForBouncerManager.getShouldHideStatusBarIconsForBouncer();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt
index 5479b92..85729c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt
@@ -20,16 +20,21 @@
import android.telephony.TelephonyManager.DATA_CONNECTING
import android.telephony.TelephonyManager.DATA_DISCONNECTED
import android.telephony.TelephonyManager.DATA_DISCONNECTING
+import android.telephony.TelephonyManager.DATA_HANDOVER_IN_PROGRESS
+import android.telephony.TelephonyManager.DATA_SUSPENDED
import android.telephony.TelephonyManager.DATA_UNKNOWN
import android.telephony.TelephonyManager.DataState
/** Internal enum representation of the telephony data connection states */
-enum class DataConnectionState(@DataState val dataState: Int) {
- Connected(DATA_CONNECTED),
- Connecting(DATA_CONNECTING),
- Disconnected(DATA_DISCONNECTED),
- Disconnecting(DATA_DISCONNECTING),
- Unknown(DATA_UNKNOWN),
+enum class DataConnectionState {
+ Connected,
+ Connecting,
+ Disconnected,
+ Disconnecting,
+ Suspended,
+ HandoverInProgress,
+ Unknown,
+ Invalid,
}
fun @receiver:DataState Int.toDataConnectionType(): DataConnectionState =
@@ -38,6 +43,8 @@
DATA_CONNECTING -> DataConnectionState.Connecting
DATA_DISCONNECTED -> DataConnectionState.Disconnected
DATA_DISCONNECTING -> DataConnectionState.Disconnecting
+ DATA_SUSPENDED -> DataConnectionState.Suspended
+ DATA_HANDOVER_IN_PROGRESS -> DataConnectionState.HandoverInProgress
DATA_UNKNOWN -> DataConnectionState.Unknown
- else -> throw IllegalArgumentException("unknown data state received $this")
+ else -> DataConnectionState.Invalid
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
index 012b9ec..ed7f60b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
@@ -26,6 +26,7 @@
import android.telephony.TelephonyCallback.SignalStrengthsListener
import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyManager
+import androidx.annotation.VisibleForTesting
import com.android.systemui.log.table.Diffable
import com.android.systemui.log.table.TableRowLogger
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Disconnected
@@ -94,7 +95,7 @@
) : Diffable<MobileConnectionModel> {
override fun logDiffs(prevVal: MobileConnectionModel, row: TableRowLogger) {
if (prevVal.dataConnectionState != dataConnectionState) {
- row.logChange(COL_CONNECTION_STATE, dataConnectionState.toString())
+ row.logChange(COL_CONNECTION_STATE, dataConnectionState.name)
}
if (prevVal.isEmergencyOnly != isEmergencyOnly) {
@@ -125,8 +126,12 @@
row.logChange(COL_PRIMARY_LEVEL, primaryLevel)
}
- if (prevVal.dataActivityDirection != dataActivityDirection) {
- row.logChange(COL_ACTIVITY_DIRECTION, dataActivityDirection.toString())
+ if (prevVal.dataActivityDirection.hasActivityIn != dataActivityDirection.hasActivityIn) {
+ row.logChange(COL_ACTIVITY_DIRECTION_IN, dataActivityDirection.hasActivityIn)
+ }
+
+ if (prevVal.dataActivityDirection.hasActivityOut != dataActivityDirection.hasActivityOut) {
+ row.logChange(COL_ACTIVITY_DIRECTION_OUT, dataActivityDirection.hasActivityOut)
}
if (prevVal.carrierNetworkChangeActive != carrierNetworkChangeActive) {
@@ -139,7 +144,7 @@
}
override fun logFull(row: TableRowLogger) {
- row.logChange(COL_CONNECTION_STATE, dataConnectionState.toString())
+ row.logChange(COL_CONNECTION_STATE, dataConnectionState.name)
row.logChange(COL_EMERGENCY, isEmergencyOnly)
row.logChange(COL_ROAMING, isRoaming)
row.logChange(COL_OPERATOR, operatorAlphaShort)
@@ -147,11 +152,13 @@
row.logChange(COL_IS_GSM, isGsm)
row.logChange(COL_CDMA_LEVEL, cdmaLevel)
row.logChange(COL_PRIMARY_LEVEL, primaryLevel)
- row.logChange(COL_ACTIVITY_DIRECTION, dataActivityDirection.toString())
+ row.logChange(COL_ACTIVITY_DIRECTION_IN, dataActivityDirection.hasActivityIn)
+ row.logChange(COL_ACTIVITY_DIRECTION_OUT, dataActivityDirection.hasActivityOut)
row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChangeActive)
row.logChange(COL_RESOLVED_NETWORK_TYPE, resolvedNetworkType.toString())
}
+ @VisibleForTesting
companion object {
const val COL_EMERGENCY = "EmergencyOnly"
const val COL_ROAMING = "Roaming"
@@ -161,7 +168,8 @@
const val COL_CDMA_LEVEL = "CdmaLevel"
const val COL_PRIMARY_LEVEL = "PrimaryLevel"
const val COL_CONNECTION_STATE = "ConnectionState"
- const val COL_ACTIVITY_DIRECTION = "DataActivity"
+ const val COL_ACTIVITY_DIRECTION_IN = "DataActivity.In"
+ const val COL_ACTIVITY_DIRECTION_OUT = "DataActivity.Out"
const val COL_CARRIER_NETWORK_CHANGE = "CarrierNetworkChangeActive"
const val COL_RESOLVED_NETWORK_TYPE = "NetworkType"
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index 9ae38e9..0e4a432 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -28,6 +28,7 @@
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.util.CarrierConfigTracker
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -37,10 +38,12 @@
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
@@ -100,6 +103,7 @@
constructor(
private val mobileConnectionsRepo: MobileConnectionsRepository,
private val carrierConfigTracker: CarrierConfigTracker,
+ private val logger: ConnectivityPipelineLogger,
userSetupRepo: UserSetupRepository,
@Application private val scope: CoroutineScope,
) : MobileIconsInteractor {
@@ -168,6 +172,8 @@
}
}
}
+ .distinctUntilChanged()
+ .onEach { logger.logFilteredSubscriptionsChanged(it) }
override val defaultDataSubId = mobileConnectionsRepo.defaultDataSubId
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
index 829a5ca..ef75713 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
@@ -23,6 +23,8 @@
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import java.io.PrintWriter
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -30,7 +32,9 @@
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -51,13 +55,17 @@
interactor: MobileIconsInteractor,
private val iconController: StatusBarIconController,
private val iconsViewModelFactory: MobileIconsViewModel.Factory,
+ private val logger: ConnectivityPipelineLogger,
@Application private val scope: CoroutineScope,
private val statusBarPipelineFlags: StatusBarPipelineFlags,
) : CoreStartable {
private val mobileSubIds: Flow<List<Int>> =
- interactor.filteredSubscriptions.mapLatest { subscriptions ->
- subscriptions.map { subscriptionModel -> subscriptionModel.subscriptionId }
- }
+ interactor.filteredSubscriptions
+ .mapLatest { subscriptions ->
+ subscriptions.map { subscriptionModel -> subscriptionModel.subscriptionId }
+ }
+ .distinctUntilChanged()
+ .onEach { logger.logUiAdapterSubIdsUpdated(it) }
/**
* We expose the list of tracked subscriptions as a flow of a list of ints, where each int is
@@ -72,6 +80,9 @@
/** In order to keep the logs tame, we will reuse the same top-level mobile icons view model */
val mobileIconsViewModel = iconsViewModelFactory.create(mobileSubIdsState)
+ private var isCollecting: Boolean = false
+ private var lastValue: List<Int>? = null
+
override fun start() {
// Only notify the icon controller if we want to *render* the new icons.
// Note that this flow may still run if
@@ -79,8 +90,18 @@
// get the logging data without rendering.
if (statusBarPipelineFlags.useNewMobileIcons()) {
scope.launch {
- mobileSubIds.collectLatest { iconController.setNewMobileIconSubIds(it) }
+ isCollecting = true
+ mobileSubIds.collectLatest {
+ logger.logUiAdapterSubIdsSentToIconController(it)
+ lastValue = it
+ iconController.setNewMobileIconSubIds(it)
+ }
}
}
}
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.println("isCollecting=$isCollecting")
+ pw.println("Last values sent to icon controller: $lastValue")
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
index 3e81c7c..a4b2abc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -29,6 +29,7 @@
import androidx.lifecycle.repeatOnLifecycle
import com.android.settingslib.graph.SignalDrawable
import com.android.systemui.R
+import com.android.systemui.common.ui.binder.ContentDescriptionViewBinder
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.StatusBarIconView
@@ -97,6 +98,12 @@
}
}
+ launch {
+ viewModel.contentDescription.distinctUntilChanged().collect {
+ ContentDescriptionViewBinder.bind(it, view)
+ }
+ }
+
// Set the network type icon
launch {
viewModel.networkTypeIcon.distinctUntilChanged().collect { dataTypeId ->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
index 5e935616..9e2024a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH
+import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH_NONE
import com.android.settingslib.graph.SignalDrawable
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
@@ -43,6 +45,7 @@
val subscriptionId: Int
/** An int consumable by [SignalDrawable] for display */
val iconId: Flow<Int>
+ val contentDescription: Flow<ContentDescription>
val roaming: Flow<Boolean>
/** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
val networkTypeIcon: Flow<Icon?>
@@ -102,6 +105,23 @@
.stateIn(scope, SharingStarted.WhileSubscribed(), initial)
}
+ override val contentDescription: Flow<ContentDescription> = run {
+ val initial = ContentDescription.Resource(PHONE_SIGNAL_STRENGTH_NONE)
+ combine(
+ iconInteractor.level,
+ iconInteractor.isInService,
+ ) { level, isInService ->
+ val resId =
+ when {
+ isInService -> PHONE_SIGNAL_STRENGTH[level]
+ else -> PHONE_SIGNAL_STRENGTH_NONE
+ }
+ ContentDescription.Resource(resId)
+ }
+ .distinctUntilChanged()
+ .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
+ }
+
private val showNetworkTypeIcon: Flow<Boolean> =
combine(
iconInteractor.isDataConnected,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
index 491f3a5..7c7ffaf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
@@ -25,6 +25,7 @@
import com.android.systemui.log.dagger.StatusBarConnectivityLog
import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.toString
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -201,6 +202,35 @@
)
}
+ // TODO(b/238425913): We should split this class into mobile-specific and wifi-specific loggers.
+
+ fun logFilteredSubscriptionsChanged(subs: List<SubscriptionModel>) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ { str1 = subs.toString() },
+ { "Filtered subscriptions updated: $str1" },
+ )
+ }
+
+ fun logUiAdapterSubIdsUpdated(subs: List<Int>) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ { str1 = subs.toString() },
+ { "Sub IDs in MobileUiAdapter updated internally: $str1" },
+ )
+ }
+
+ fun logUiAdapterSubIdsSentToIconController(subs: List<Int>) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ { str1 = subs.toString() },
+ { "Sub IDs in MobileUiAdapter being sent to icon controller: $str1" },
+ )
+ }
+
companion object {
const val SB_LOGGING_TAG = "SbConnectivity"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
index 8669047..c45b420 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
@@ -95,7 +95,7 @@
.logDiffsForTable(
wifiTableLogBuffer,
columnPrefix = "",
- columnName = "isWifiEnabled",
+ columnName = "isEnabled",
initialValue = wifiManager.isWifiEnabled,
)
.stateIn(
@@ -141,7 +141,7 @@
.logDiffsForTable(
wifiTableLogBuffer,
columnPrefix = "",
- columnName = "isWifiDefault",
+ columnName = "isDefault",
initialValue = false,
)
.stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
@@ -212,7 +212,7 @@
.distinctUntilChanged()
.logDiffsForTable(
wifiTableLogBuffer,
- columnPrefix = "wifiNetwork",
+ columnPrefix = "",
initialValue = WIFI_NETWORK_DEFAULT,
)
// There will be multiple wifi icons in different places that will frequently
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt
index e491d2b..094bcf9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt
@@ -53,4 +53,4 @@
}
}
-private const val COL_ICON = "wifiIcon"
+private const val COL_ICON = "icon"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
index cc6fdcc..9ad36fd5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
@@ -438,6 +438,11 @@
}
@Override
+ public void onLockedOutStateChanged(BiometricSourceType biometricSourceType) {
+ update(false /* updateAlways */);
+ }
+
+ @Override
public void onKeyguardVisibilityChanged(boolean visible) {
update(false /* updateAlways */);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
index 60f6df6..8f424b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
@@ -61,6 +61,10 @@
listeners.add(listener)
}
+ fun removeListener(listener: StatusBarWindowStateListener) {
+ listeners.remove(listener)
+ }
+
/** Returns true if the window is currently showing. */
fun windowIsShowing() = windowState == WINDOW_STATE_SHOWING
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
index ec6965a..899b0c2 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
@@ -80,6 +80,26 @@
)
}
+ /** Logs that there was a failure to animate the view in. */
+ fun logAnimateInFailure() {
+ buffer.log(
+ tag,
+ LogLevel.WARNING,
+ {},
+ { "View's appearance animation failed. Forcing view display manually." },
+ )
+ }
+
+ /** Logs that there was a failure to animate the view out. */
+ fun logAnimateOutFailure() {
+ buffer.log(
+ tag,
+ LogLevel.WARNING,
+ {},
+ { "View's disappearance animation failed." },
+ )
+ }
+
fun logViewHidden(info: T) {
buffer.log(
tag,
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarAnimator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarAnimator.kt
new file mode 100644
index 0000000..01a81de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarAnimator.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.temporarydisplay.chipbar
+
+import android.view.View
+import android.view.ViewGroup
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.animation.ViewHierarchyAnimator
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.util.children
+import javax.inject.Inject
+
+/**
+ * A class controlling chipbar animations. Typically delegates to [ViewHierarchyAnimator].
+ *
+ * Used so that animations can be mocked out in tests.
+ */
+@SysUISingleton
+open class ChipbarAnimator @Inject constructor() {
+ /**
+ * Animates [innerView] and its children into view.
+ *
+ * @return true if the animation was successfully started and false if the animation can't be
+ * run for any reason.
+ *
+ * See [ViewHierarchyAnimator.animateAddition].
+ */
+ open fun animateViewIn(innerView: ViewGroup, onAnimationEnd: Runnable): Boolean {
+ return ViewHierarchyAnimator.animateAddition(
+ innerView,
+ ViewHierarchyAnimator.Hotspot.TOP,
+ Interpolators.EMPHASIZED_DECELERATE,
+ duration = ANIMATION_IN_DURATION,
+ includeMargins = true,
+ includeFadeIn = true,
+ onAnimationEnd = onAnimationEnd,
+ )
+ }
+
+ /**
+ * Animates [innerView] and its children out of view.
+ *
+ * @return true if the animation was successfully started and false if the animation can't be
+ * run for any reason.
+ *
+ * See [ViewHierarchyAnimator.animateRemoval].
+ */
+ open fun animateViewOut(innerView: ViewGroup, onAnimationEnd: Runnable): Boolean {
+ return ViewHierarchyAnimator.animateRemoval(
+ innerView,
+ ViewHierarchyAnimator.Hotspot.TOP,
+ Interpolators.EMPHASIZED_ACCELERATE,
+ ANIMATION_OUT_DURATION,
+ includeMargins = true,
+ onAnimationEnd,
+ )
+ }
+
+ /** Force shows this view and all child views. Should be used in case [animateViewIn] fails. */
+ fun forceDisplayView(innerView: View) {
+ innerView.alpha = 1f
+ if (innerView is ViewGroup) {
+ innerView.children.forEach { forceDisplayView(it) }
+ }
+ }
+}
+
+private const val ANIMATION_IN_DURATION = 500L
+private const val ANIMATION_OUT_DURATION = 250L
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
index 46f13cc..696134c 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -32,8 +32,6 @@
import com.android.internal.widget.CachingIconView
import com.android.systemui.Gefingerpoken
import com.android.systemui.R
-import com.android.systemui.animation.Interpolators
-import com.android.systemui.animation.ViewHierarchyAnimator
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
import com.android.systemui.common.shared.model.Text.Companion.loadText
@@ -78,6 +76,7 @@
configurationController: ConfigurationController,
dumpManager: DumpManager,
powerManager: PowerManager,
+ private val chipbarAnimator: ChipbarAnimator,
private val falsingManager: FalsingManager,
private val falsingCollector: FalsingCollector,
private val swipeChipbarAwayGestureHandler: SwipeChipbarAwayGestureHandler?,
@@ -206,23 +205,17 @@
}
override fun animateViewIn(view: ViewGroup) {
+ // We can only request focus once the animation finishes.
val onAnimationEnd = Runnable {
maybeGetAccessibilityFocus(view.getTag(INFO_TAG) as ChipbarInfo?, view)
}
- val added =
- ViewHierarchyAnimator.animateAddition(
- view.getInnerView(),
- ViewHierarchyAnimator.Hotspot.TOP,
- Interpolators.EMPHASIZED_DECELERATE,
- duration = ANIMATION_IN_DURATION,
- includeMargins = true,
- includeFadeIn = true,
- // We can only request focus once the animation finishes.
- onAnimationEnd = onAnimationEnd,
- )
- // If the view doesn't get animated, the [onAnimationEnd] runnable won't get run. So, just
- // run it immediately.
- if (!added) {
+ val animatedIn = chipbarAnimator.animateViewIn(view.getInnerView(), onAnimationEnd)
+
+ // If the view doesn't get animated, the [onAnimationEnd] runnable won't get run and the
+ // views would remain un-displayed. So, just force-set/run those items immediately.
+ if (!animatedIn) {
+ logger.logAnimateInFailure()
+ chipbarAnimator.forceDisplayView(view.getInnerView())
onAnimationEnd.run()
}
}
@@ -230,18 +223,11 @@
override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
val innerView = view.getInnerView()
innerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_NONE
- val removed =
- ViewHierarchyAnimator.animateRemoval(
- innerView,
- ViewHierarchyAnimator.Hotspot.TOP,
- Interpolators.EMPHASIZED_ACCELERATE,
- ANIMATION_OUT_DURATION,
- includeMargins = true,
- onAnimationEnd,
- )
+ val removed = chipbarAnimator.animateViewOut(innerView, onAnimationEnd)
// If the view doesn't get animated, the [onAnimationEnd] runnable won't get run. So, just
// run it immediately.
if (!removed) {
+ logger.logAnimateOutFailure()
onAnimationEnd.run()
}
@@ -299,8 +285,6 @@
}
}
-private const val ANIMATION_IN_DURATION = 500L
-private const val ANIMATION_OUT_DURATION = 250L
@IdRes private val INFO_TAG = R.id.tag_chipbar_info
private const val SWIPE_UP_GESTURE_REASON = "SWIPE_UP_GESTURE_DETECTED"
private const val TAG = "ChipbarCoordinator"
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 9f6e602..6b507ba 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -115,6 +115,7 @@
private final SecureSettings mSecureSettings;
private final Executor mMainExecutor;
private final Handler mBgHandler;
+ private final boolean mIsMonochromaticEnabled;
private final Context mContext;
private final boolean mIsMonetEnabled;
private final UserTracker mUserTracker;
@@ -356,14 +357,24 @@
};
@Inject
- public ThemeOverlayController(Context context, BroadcastDispatcher broadcastDispatcher,
- @Background Handler bgHandler, @Main Executor mainExecutor,
- @Background Executor bgExecutor, ThemeOverlayApplier themeOverlayApplier,
- SecureSettings secureSettings, WallpaperManager wallpaperManager,
- UserManager userManager, DeviceProvisionedController deviceProvisionedController,
- UserTracker userTracker, DumpManager dumpManager, FeatureFlags featureFlags,
- @Main Resources resources, WakefulnessLifecycle wakefulnessLifecycle) {
+ public ThemeOverlayController(
+ Context context,
+ BroadcastDispatcher broadcastDispatcher,
+ @Background Handler bgHandler,
+ @Main Executor mainExecutor,
+ @Background Executor bgExecutor,
+ ThemeOverlayApplier themeOverlayApplier,
+ SecureSettings secureSettings,
+ WallpaperManager wallpaperManager,
+ UserManager userManager,
+ DeviceProvisionedController deviceProvisionedController,
+ UserTracker userTracker,
+ DumpManager dumpManager,
+ FeatureFlags featureFlags,
+ @Main Resources resources,
+ WakefulnessLifecycle wakefulnessLifecycle) {
mContext = context;
+ mIsMonochromaticEnabled = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEME);
mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET);
mDeviceProvisionedController = deviceProvisionedController;
mBroadcastDispatcher = broadcastDispatcher;
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index 8cb4deb..5cf01af 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -17,8 +17,11 @@
package com.android.systemui.user.data.repository
+import android.app.IActivityManager
+import android.app.UserSwitchObserver
import android.content.Context
import android.content.pm.UserInfo
+import android.os.IRemoteCallback
import android.os.UserHandle
import android.os.UserManager
import android.provider.Settings
@@ -30,6 +33,8 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR
import com.android.systemui.settings.UserTracker
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.util.settings.GlobalSettings
@@ -68,6 +73,12 @@
/** [UserInfo] of the currently-selected user. */
val selectedUserInfo: Flow<UserInfo>
+ /** Whether user switching is currently in progress. */
+ val userSwitchingInProgress: Flow<Boolean>
+
+ /** User ID of the main user. */
+ val mainUserId: Int
+
/** User ID of the last non-guest selected user. */
val lastSelectedNonGuestUserId: Int
@@ -108,6 +119,8 @@
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val globalSettings: GlobalSettings,
private val tracker: UserTracker,
+ private val activityManager: IActivityManager,
+ featureFlags: FeatureFlags,
) : UserRepository {
private val _userSwitcherSettings = MutableStateFlow(runBlocking { getSettings() })
@@ -120,7 +133,9 @@
private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
- override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
+ override var mainUserId: Int = UserHandle.USER_NULL
+ private set
+ override var lastSelectedNonGuestUserId: Int = UserHandle.USER_NULL
private set
override val isGuestUserAutoCreated: Boolean =
@@ -129,6 +144,10 @@
private var _isGuestUserResetting: Boolean = false
override var isGuestUserResetting: Boolean = _isGuestUserResetting
+ private val _isUserSwitchingInProgress = MutableStateFlow(false)
+ override val userSwitchingInProgress: Flow<Boolean>
+ get() = _isUserSwitchingInProgress
+
override val isGuestUserCreationScheduled = AtomicBoolean()
override val isStatusBarUserChipEnabled: Boolean =
@@ -141,6 +160,9 @@
init {
observeSelectedUser()
observeUserSettings()
+ if (featureFlags.isEnabled(FACE_AUTH_REFACTOR)) {
+ observeUserSwitching()
+ }
}
override fun refreshUsers() {
@@ -155,6 +177,11 @@
// The guest user is always last, regardless of creation time.
.sortedBy { it.isGuest }
}
+
+ if (mainUserId == UserHandle.USER_NULL) {
+ val mainUser = withContext(backgroundDispatcher) { manager.mainUser }
+ mainUser?.let { mainUserId = it.identifier }
+ }
}
}
@@ -166,6 +193,28 @@
return _userSwitcherSettings.value.isSimpleUserSwitcher
}
+ private fun observeUserSwitching() {
+ conflatedCallbackFlow {
+ val callback =
+ object : UserSwitchObserver() {
+ override fun onUserSwitching(newUserId: Int, reply: IRemoteCallback) {
+ trySendWithFailureLogging(true, TAG, "userSwitching started")
+ }
+
+ override fun onUserSwitchComplete(newUserId: Int) {
+ trySendWithFailureLogging(false, TAG, "userSwitching completed")
+ }
+ }
+ activityManager.registerUserSwitchObserver(callback, TAG)
+ trySendWithFailureLogging(false, TAG, "initial value defaulting to false")
+ awaitClose { activityManager.unregisterUserSwitchObserver(callback) }
+ }
+ .onEach { _isUserSwitchingInProgress.value = it }
+ // TODO (b/262838215), Make this stateIn and initialize directly in field declaration
+ // once the flag is launched
+ .launchIn(applicationScope)
+ }
+
private fun observeSelectedUser() {
conflatedCallbackFlow {
fun send() {
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
index a374885..0a07439 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
@@ -139,11 +139,11 @@
}
applicationScope.launch {
- var newUserId = UserHandle.USER_SYSTEM
+ var newUserId = repository.mainUserId
if (targetUserId == UserHandle.USER_NULL) {
// When a target user is not specified switch to last non guest user:
val lastSelectedNonGuestUserHandle = repository.lastSelectedNonGuestUserId
- if (lastSelectedNonGuestUserHandle != UserHandle.USER_SYSTEM) {
+ if (lastSelectedNonGuestUserHandle != repository.mainUserId) {
val info =
withContext(backgroundDispatcher) {
manager.getUserInfo(lastSelectedNonGuestUserHandle)
@@ -215,8 +215,11 @@
// Create a new guest in the foreground, and then immediately switch to it
val newGuestId = create(showDialog, dismissDialog)
if (newGuestId == UserHandle.USER_NULL) {
- Log.e(TAG, "Could not create new guest, switching back to system user")
- switchUser(UserHandle.USER_SYSTEM)
+ Log.e(TAG, "Could not create new guest, switching back to main user")
+ val mainUser = withContext(backgroundDispatcher) { manager.mainUser?.identifier }
+
+ mainUser?.let { switchUser(it) }
+
withContext(backgroundDispatcher) {
manager.removeUserWhenPossible(
UserHandle.of(currentUser.id),
diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml
index 2c1e681..ed2772a 100644
--- a/packages/SystemUI/tests/AndroidManifest.xml
+++ b/packages/SystemUI/tests/AndroidManifest.xml
@@ -159,6 +159,12 @@
android:enabled="false"
tools:replace="android:authorities"
android:grantUriPermissions="true" />
+
+ <activity
+ android:name="com.android.systemui.screenshot.appclips.AppClipsTrampolineActivityTest$AppClipsTrampolineActivityTestable"
+ android:exported="false"
+ android:permission="com.android.systemui.permission.SELF"
+ android:excludeFromRecents="true" />
</application>
<instrumentation android:name="android.testing.TestableInstrumentation"
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ActiveUnlockConfigTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ActiveUnlockConfigTest.kt
index 39cc34b..e8d50ca 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ActiveUnlockConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ActiveUnlockConfigTest.kt
@@ -21,6 +21,8 @@
import android.hardware.biometrics.BiometricFaceConstants
import android.net.Uri
import android.os.Handler
+import android.os.PowerManager
+import android.os.PowerManager.WAKE_REASON_BIOMETRIC
import android.os.UserHandle
import android.provider.Settings
import androidx.test.filters.SmallTest
@@ -48,6 +50,8 @@
private val fakeFaceErrorsUri = Uri.Builder().appendPath("face-errors").build()
private val fakeFaceAcquiredUri = Uri.Builder().appendPath("face-acquired").build()
private val fakeUnlockIntentBioEnroll = Uri.Builder().appendPath("unlock-intent-bio").build()
+ private val fakeWakeupsConsideredUnlockIntents =
+ Uri.Builder().appendPath("wakeups-considered-unlock-intent").build()
@Mock
private lateinit var secureSettings: SecureSettings
@@ -82,6 +86,9 @@
`when`(secureSettings.getUriFor(
Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED))
.thenReturn(fakeUnlockIntentBioEnroll)
+ `when`(secureSettings.getUriFor(
+ Settings.Secure.ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS))
+ .thenReturn(fakeWakeupsConsideredUnlockIntents)
activeUnlockConfig = ActiveUnlockConfig(
handler,
@@ -92,18 +99,18 @@
}
@Test
- fun testRegsitersForSettingsChanges() {
+ fun registersForSettingsChanges() {
verifyRegisterSettingObserver()
}
@Test
- fun testOnWakeupSettingChanged() {
+ fun onWakeupSettingChanged() {
verifyRegisterSettingObserver()
// GIVEN no active unlock settings enabled
assertFalse(
activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE)
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE)
)
// WHEN unlock on wake is allowed
@@ -114,26 +121,26 @@
// THEN active unlock triggers allowed on: wake, unlock-intent, and biometric failure
assertTrue(
activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE)
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE)
)
assertTrue(
activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT)
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT)
)
assertTrue(
activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL)
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL)
)
}
@Test
- fun testOnUnlockIntentSettingChanged() {
+ fun onUnlockIntentSettingChanged() {
verifyRegisterSettingObserver()
// GIVEN no active unlock settings enabled
assertFalse(
activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT)
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT)
)
// WHEN unlock on biometric failed is allowed
@@ -143,15 +150,15 @@
// THEN active unlock triggers allowed on: biometric failure ONLY
assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE))
assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT))
assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
}
@Test
- fun testOnBioFailSettingChanged() {
+ fun onBioFailSettingChanged() {
verifyRegisterSettingObserver()
// GIVEN no active unlock settings enabled and triggering unlock intent on biometric
@@ -161,7 +168,7 @@
0)).thenReturn("")
updateSetting(fakeUnlockIntentBioEnroll)
assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
// WHEN unlock on biometric failed is allowed
`when`(secureSettings.getIntForUser(Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL,
@@ -170,15 +177,15 @@
// THEN active unlock triggers allowed on: biometric failure ONLY
assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE))
assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT))
assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
}
@Test
- fun testFaceErrorSettingsChanged() {
+ fun faceErrorSettingsChanged() {
verifyRegisterSettingObserver()
// GIVEN unlock on biometric fail
@@ -200,7 +207,7 @@
}
@Test
- fun testFaceAcquiredSettingsChanged() {
+ fun faceAcquiredSettingsChanged() {
verifyRegisterSettingObserver()
// GIVEN unlock on biometric fail
@@ -228,7 +235,7 @@
}
@Test
- fun testTriggerOnUnlockIntentWhenBiometricEnrolledNone() {
+ fun triggerOnUnlockIntentWhenBiometricEnrolledNone() {
verifyRegisterSettingObserver()
// GIVEN unlock on biometric fail
@@ -244,16 +251,16 @@
// WHEN unlock intent is allowed when NO biometrics are enrolled (0)
`when`(secureSettings.getStringForUser(
Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
- 0)).thenReturn("${ActiveUnlockConfig.BIOMETRIC_TYPE_NONE}")
+ 0)).thenReturn("${ActiveUnlockConfig.BiometricType.NONE.intValue}")
updateSetting(fakeUnlockIntentBioEnroll)
// THEN active unlock triggers allowed on unlock intent
assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT))
}
@Test
- fun testTriggerOnUnlockIntentWhenBiometricEnrolledFingerprintOrFaceOnly() {
+ fun triggerOnUnlockIntentWhenBiometricEnrolledFingerprintOrFaceOnly() {
verifyRegisterSettingObserver()
// GIVEN unlock on biometric fail
@@ -263,7 +270,7 @@
// GIVEN fingerprint and face are both enrolled
activeUnlockConfig.keyguardUpdateMonitor = keyguardUpdateMonitor
- `when`(keyguardUpdateMonitor.isFaceEnrolled()).thenReturn(true)
+ `when`(keyguardUpdateMonitor.isFaceEnrolled).thenReturn(true)
`when`(keyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(0)).thenReturn(true)
// WHEN unlock intent is allowed when ONLY fingerprint is enrolled or NO biometircs
@@ -271,29 +278,99 @@
`when`(secureSettings.getStringForUser(
Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT_WHEN_BIOMETRIC_ENROLLED,
0)).thenReturn(
- "${ActiveUnlockConfig.BIOMETRIC_TYPE_ANY_FACE}" +
- "|${ActiveUnlockConfig.BIOMETRIC_TYPE_ANY_FINGERPRINT}")
+ "${ActiveUnlockConfig.BiometricType.ANY_FACE.intValue}" +
+ "|${ActiveUnlockConfig.BiometricType.ANY_FINGERPRINT.intValue}")
updateSetting(fakeUnlockIntentBioEnroll)
// THEN active unlock triggers NOT allowed on unlock intent
assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT))
// WHEN fingerprint ONLY enrolled
- `when`(keyguardUpdateMonitor.isFaceEnrolled()).thenReturn(false)
+ `when`(keyguardUpdateMonitor.isFaceEnrolled).thenReturn(false)
`when`(keyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(0)).thenReturn(true)
// THEN active unlock triggers allowed on unlock intent
assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT))
// WHEN face ONLY enrolled
- `when`(keyguardUpdateMonitor.isFaceEnrolled()).thenReturn(true)
+ `when`(keyguardUpdateMonitor.isFaceEnrolled).thenReturn(true)
`when`(keyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(0)).thenReturn(false)
// THEN active unlock triggers allowed on unlock intent
assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT))
+ }
+
+ @Test
+ fun isWakeupConsideredUnlockIntent_singleValue() {
+ verifyRegisterSettingObserver()
+
+ // GIVEN lift is considered an unlock intent
+ `when`(secureSettings.getStringForUser(
+ Settings.Secure.ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS,
+ 0)).thenReturn(PowerManager.WAKE_REASON_LIFT.toString())
+ updateSetting(fakeWakeupsConsideredUnlockIntents)
+
+ // THEN only WAKE_REASON_LIFT is considered an unlock intent
+ for (wakeReason in 0..WAKE_REASON_BIOMETRIC) {
+ if (wakeReason == PowerManager.WAKE_REASON_LIFT) {
+ assertTrue(activeUnlockConfig.isWakeupConsideredUnlockIntent(wakeReason))
+ } else {
+ assertFalse(activeUnlockConfig.isWakeupConsideredUnlockIntent(wakeReason))
+ }
+ }
+ }
+
+ @Test
+ fun isWakeupConsideredUnlockIntent_multiValue() {
+ verifyRegisterSettingObserver()
+
+ // GIVEN lift and tap are considered an unlock intent
+ `when`(secureSettings.getStringForUser(
+ Settings.Secure.ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS,
+ 0)).thenReturn(
+ PowerManager.WAKE_REASON_LIFT.toString() +
+ "|" +
+ PowerManager.WAKE_REASON_TAP.toString()
+ )
+ updateSetting(fakeWakeupsConsideredUnlockIntents)
+
+ // THEN WAKE_REASON_LIFT and WAKE_REASON TAP are considered an unlock intent
+ for (wakeReason in 0..WAKE_REASON_BIOMETRIC) {
+ if (wakeReason == PowerManager.WAKE_REASON_LIFT ||
+ wakeReason == PowerManager.WAKE_REASON_TAP) {
+ assertTrue(activeUnlockConfig.isWakeupConsideredUnlockIntent(wakeReason))
+ } else {
+ assertFalse(activeUnlockConfig.isWakeupConsideredUnlockIntent(wakeReason))
+ }
+ }
+ assertTrue(activeUnlockConfig.isWakeupConsideredUnlockIntent(PowerManager.WAKE_REASON_LIFT))
+ assertTrue(activeUnlockConfig.isWakeupConsideredUnlockIntent(PowerManager.WAKE_REASON_TAP))
+ assertFalse(activeUnlockConfig.isWakeupConsideredUnlockIntent(
+ PowerManager.WAKE_REASON_UNFOLD_DEVICE))
+ }
+
+ @Test
+ fun isWakeupConsideredUnlockIntent_emptyValues() {
+ verifyRegisterSettingObserver()
+
+ // GIVEN lift and tap are considered an unlock intent
+ `when`(secureSettings.getStringForUser(
+ Settings.Secure.ACTIVE_UNLOCK_WAKEUPS_CONSIDERED_UNLOCK_INTENTS,
+ 0)).thenReturn(" ")
+ updateSetting(fakeWakeupsConsideredUnlockIntents)
+
+ // THEN no wake up gestures are considered an unlock intent
+ for (wakeReason in 0..WAKE_REASON_BIOMETRIC) {
+ assertFalse(activeUnlockConfig.isWakeupConsideredUnlockIntent(wakeReason))
+ }
+ assertFalse(activeUnlockConfig.isWakeupConsideredUnlockIntent(
+ PowerManager.WAKE_REASON_LIFT))
+ assertFalse(activeUnlockConfig.isWakeupConsideredUnlockIntent(PowerManager.WAKE_REASON_TAP))
+ assertFalse(activeUnlockConfig.isWakeupConsideredUnlockIntent(
+ PowerManager.WAKE_REASON_UNFOLD_DEVICE))
}
private fun updateSetting(uri: Uri) {
@@ -312,6 +389,7 @@
verifyRegisterSettingObserver(fakeFaceErrorsUri)
verifyRegisterSettingObserver(fakeFaceAcquiredUri)
verifyRegisterSettingObserver(fakeUnlockIntentBioEnroll)
+ verifyRegisterSettingObserver(fakeWakeupsConsideredUnlockIntents)
}
private fun verifyRegisterSettingObserver(uri: Uri) {
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index b9c23d4..43a2017 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -32,10 +32,12 @@
import com.android.systemui.plugins.ClockEvents
import com.android.systemui.plugins.ClockFaceController
import com.android.systemui.plugins.ClockFaceEvents
+import com.android.systemui.plugins.ClockTickRate
import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
@@ -72,7 +74,7 @@
@Mock private lateinit var animations: ClockAnimations
@Mock private lateinit var events: ClockEvents
@Mock private lateinit var clock: ClockController
- @Mock private lateinit var mainExecutor: Executor
+ @Mock private lateinit var mainExecutor: DelayableExecutor
@Mock private lateinit var bgExecutor: Executor
@Mock private lateinit var featureFlags: FeatureFlags
@Mock private lateinit var smallClockController: ClockFaceController
@@ -97,6 +99,8 @@
whenever(largeClockController.events).thenReturn(largeClockEvents)
whenever(clock.events).thenReturn(events)
whenever(clock.animations).thenReturn(animations)
+ whenever(smallClockEvents.tickRate).thenReturn(ClockTickRate.PER_MINUTE)
+ whenever(largeClockEvents.tickRate).thenReturn(ClockTickRate.PER_MINUTE)
repository = FakeKeyguardRepository()
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index a4180fd..ccc4e4a 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -33,6 +33,7 @@
import android.provider.Settings;
import android.testing.AndroidTestingRunner;
import android.view.View;
+import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
@@ -47,6 +48,7 @@
import com.android.systemui.plugins.ClockController;
import com.android.systemui.plugins.ClockEvents;
import com.android.systemui.plugins.ClockFaceController;
+import com.android.systemui.plugins.ClockFaceEvents;
import com.android.systemui.plugins.log.LogBuffer;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shared.clocks.AnimatableClockView;
@@ -99,6 +101,8 @@
@Mock
private ClockEvents mClockEvents;
@Mock
+ private ClockFaceEvents mClockFaceEvents;
+ @Mock
DumpManager mDumpManager;
@Mock
ClockEventController mClockEventController;
@@ -118,6 +122,11 @@
@Mock
private LogBuffer mLogBuffer;
+ private final View mFakeDateView = (View) (new ViewGroup(mContext) {
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {}
+ });
+ private final View mFakeWeatherView = new View(mContext);
private final View mFakeSmartspaceView = new View(mContext);
private KeyguardClockSwitchController mController;
@@ -145,6 +154,8 @@
when(mLargeClockView.getContext()).thenReturn(getContext());
when(mView.isAttachedToWindow()).thenReturn(true);
+ when(mSmartspaceController.buildAndConnectDateView(any())).thenReturn(mFakeDateView);
+ when(mSmartspaceController.buildAndConnectWeatherView(any())).thenReturn(mFakeWeatherView);
when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView);
mExecutor = new FakeExecutor(new FakeSystemClock());
mController = new KeyguardClockSwitchController(
@@ -168,6 +179,8 @@
when(mClockController.getLargeClock()).thenReturn(mLargeClockController);
when(mClockController.getSmallClock()).thenReturn(mSmallClockController);
when(mClockController.getEvents()).thenReturn(mClockEvents);
+ when(mSmallClockController.getEvents()).thenReturn(mClockFaceEvents);
+ when(mLargeClockController.getEvents()).thenReturn(mClockFaceEvents);
when(mClockController.getAnimations()).thenReturn(mClockAnimations);
when(mClockRegistry.createCurrentClock()).thenReturn(mClockController);
when(mClockEventController.getClock()).thenReturn(mClockController);
@@ -252,6 +265,19 @@
}
@Test
+ public void onLocaleListChanged_rebuildsSmartspaceViews_whenDecouplingEnabled() {
+ when(mSmartspaceController.isEnabled()).thenReturn(true);
+ when(mSmartspaceController.isDateWeatherDecoupled()).thenReturn(true);
+ mController.init();
+
+ mController.onLocaleListChanged();
+ // Should be called once on initial setup, then once again for locale change
+ verify(mSmartspaceController, times(2)).buildAndConnectDateView(mView);
+ verify(mSmartspaceController, times(2)).buildAndConnectWeatherView(mView);
+ verify(mSmartspaceController, times(2)).buildAndConnectView(mView);
+ }
+
+ @Test
public void testSmartspaceDisabledShowsKeyguardStatusArea() {
when(mSmartspaceController.isEnabled()).thenReturn(false);
mController.init();
@@ -274,8 +300,9 @@
ArgumentCaptor<ContentObserver> observerCaptor =
ArgumentCaptor.forClass(ContentObserver.class);
mController.init();
- verify(mSecureSettings).registerContentObserverForUser(any(String.class),
- anyBoolean(), observerCaptor.capture(), eq(UserHandle.USER_ALL));
+ verify(mSecureSettings).registerContentObserverForUser(
+ eq(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK),
+ anyBoolean(), observerCaptor.capture(), eq(UserHandle.USER_ALL));
ContentObserver observer = observerCaptor.getValue();
mExecutor.runAllReady();
@@ -321,6 +348,22 @@
assertEquals(0, mController.getClockBottom(10));
}
+ @Test
+ public void testChangeLockscreenWeatherEnabledSetsWeatherViewVisible() {
+ when(mSmartspaceController.isWeatherEnabled()).thenReturn(true);
+ ArgumentCaptor<ContentObserver> observerCaptor =
+ ArgumentCaptor.forClass(ContentObserver.class);
+ mController.init();
+ verify(mSecureSettings).registerContentObserverForUser(
+ eq(Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED), anyBoolean(),
+ observerCaptor.capture(), eq(UserHandle.USER_ALL));
+ ContentObserver observer = observerCaptor.getValue();
+ mExecutor.runAllReady();
+ // When a settings change has occurred, check that view is visible.
+ observer.onChange(true);
+ mExecutor.runAllReady();
+ assertEquals(View.VISIBLE, mFakeWeatherView.getVisibility());
+ }
private void verifyAttachment(VerificationMode times) {
verify(mClockRegistry, times).registerClockChangeListener(
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
index d912793..082c8cc 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
@@ -134,4 +134,10 @@
keyguardPasswordViewController.startAppearAnimation()
verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean())
}
+
+ @Test
+ fun testMessageIsSetWhenReset() {
+ keyguardPasswordViewController.resetState()
+ verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
index b742100..0881e61 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
@@ -120,4 +120,10 @@
public void testGetInitialMessageResId() {
assertThat(mKeyguardPinViewController.getInitialMessageResId()).isNotEqualTo(0);
}
+
+ @Test
+ public void testMessageIsSetWhenReset() {
+ mKeyguardPinViewController.resetState();
+ verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pin);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index 075ef9d..0d65f12 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -583,6 +583,8 @@
verify(mKeyguardSecurityViewFlipperController).clearViews();
verify(mKeyguardSecurityViewFlipperController).getSecurityView(any(SecurityMode.class),
any(KeyguardSecurityCallback.class));
+ verify(mView).reset();
+ verify(mKeyguardSecurityViewFlipperController).reset();
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 4d95a22..ed9b5cf 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -685,12 +685,36 @@
// WHEN fingerprint is locked out
fingerprintErrorLockedOut();
- // THEN unlocking with fingeprint is not allowed
+ // THEN unlocking with fingerprint is not allowed
Assert.assertFalse(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
BiometricSourceType.FINGERPRINT));
}
@Test
+ public void trustAgentHasTrust() {
+ // WHEN user has trust
+ mKeyguardUpdateMonitor.onTrustChanged(true, true, getCurrentUser(), 0, null);
+
+ // THEN user is considered as "having trust" and bouncer can be skipped
+ Assert.assertTrue(mKeyguardUpdateMonitor.getUserHasTrust(getCurrentUser()));
+ Assert.assertTrue(mKeyguardUpdateMonitor.getUserCanSkipBouncer(getCurrentUser()));
+ }
+
+ @Test
+ public void trustAgentHasTrust_fingerprintLockout() {
+ // GIVEN user has trust
+ mKeyguardUpdateMonitor.onTrustChanged(true, true, getCurrentUser(), 0, null);
+ Assert.assertTrue(mKeyguardUpdateMonitor.getUserHasTrust(getCurrentUser()));
+
+ // WHEN fingerprint is locked out
+ fingerprintErrorLockedOut();
+
+ // THEN user is NOT considered as "having trust" and bouncer cannot be skipped
+ Assert.assertFalse(mKeyguardUpdateMonitor.getUserHasTrust(getCurrentUser()));
+ Assert.assertFalse(mKeyguardUpdateMonitor.getUserCanSkipBouncer(getCurrentUser()));
+ }
+
+ @Test
public void testTriesToAuthenticate_whenBouncer() {
setKeyguardBouncerVisibility(true);
@@ -2182,7 +2206,7 @@
// GIVEN active unlock triggers on biometric failures
when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
.thenReturn(true);
// WHEN fingerprint fails
@@ -2205,7 +2229,7 @@
// GIVEN active unlock triggers on biometric failures
when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
.thenReturn(true);
// WHEN face fails & bypass is not allowed
@@ -2229,7 +2253,7 @@
// GIVEN active unlock triggers on biometric failures
when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
.thenReturn(true);
// WHEN face fails & bypass is not allowed
@@ -2251,7 +2275,7 @@
// GIVEN active unlock triggers on biometric failures
when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(
- ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL))
.thenReturn(true);
// WHEN face fails & on the bouncer
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index e918c1c..4cf5a4b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -21,6 +21,7 @@
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.systemui.dump.LogBufferHelperKt.logcatLogBuffer;
import static com.google.common.truth.Truth.assertThat;
@@ -33,8 +34,10 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
@@ -88,6 +91,7 @@
import com.android.systemui.decor.PrivacyDotCornerDecorProviderImpl;
import com.android.systemui.decor.PrivacyDotDecorProviderFactory;
import com.android.systemui.decor.RoundedCornerResDelegate;
+import com.android.systemui.log.ScreenDecorationsLogger;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.settings.UserTracker;
@@ -102,8 +106,12 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
import java.util.ArrayList;
import java.util.List;
@@ -117,7 +125,7 @@
private WindowManager mWindowManager;
private DisplayManager mDisplayManager;
private SecureSettings mSecureSettings;
- private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
+ private FakeExecutor mExecutor;
private final FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
private FakeThreadFactory mThreadFactory;
private ArrayList<DecorProvider> mPrivacyDecorProviders;
@@ -159,6 +167,8 @@
private PrivacyDotViewController.ShowingListener mPrivacyDotShowingListener;
@Mock
private CutoutDecorProviderFactory mCutoutFactory;
+ @Captor
+ private ArgumentCaptor<AuthController.Callback> mAuthControllerCallback;
private List<DecorProvider> mMockCutoutList;
@Before
@@ -167,6 +177,7 @@
Handler mainHandler = new Handler(TestableLooper.get(this).getLooper());
mSecureSettings = new FakeSettings();
+ mExecutor = new FakeExecutor(new FakeSystemClock());
mThreadFactory = new FakeThreadFactory(mExecutor);
mThreadFactory.setHandler(mainHandler);
@@ -219,11 +230,14 @@
mAuthController,
mStatusBarStateController,
mKeyguardUpdateMonitor,
- mExecutor));
+ mExecutor,
+ new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer"))));
mScreenDecorations = spy(new ScreenDecorations(mContext, mExecutor, mSecureSettings,
mTunerService, mUserTracker, mDisplayTracker, mDotViewController, mThreadFactory,
- mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory) {
+ mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
+ new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")),
+ mAuthController) {
@Override
public void start() {
super.start();
@@ -1161,6 +1175,44 @@
}
@Test
+ public void faceSensorLocationChangesReloadsFaceScanningOverlay() {
+ mFaceScanningProviders = new ArrayList<>();
+ mFaceScanningProviders.add(mFaceScanningDecorProvider);
+ when(mFaceScanningProviderFactory.getProviders()).thenReturn(mFaceScanningProviders);
+ when(mFaceScanningProviderFactory.getHasProviders()).thenReturn(true);
+ ScreenDecorations screenDecorations = new ScreenDecorations(mContext, mExecutor,
+ mSecureSettings, mTunerService, mUserTracker, mDisplayTracker, mDotViewController,
+ mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
+ new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), mAuthController);
+ screenDecorations.start();
+ verify(mAuthController).addCallback(mAuthControllerCallback.capture());
+ when(mContext.getDisplay()).thenReturn(mDisplay);
+ when(mDisplay.getDisplayInfo(any())).thenAnswer(new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ DisplayInfo displayInfo = invocation.getArgument(0);
+ int modeId = 1;
+ displayInfo.modeId = modeId;
+ displayInfo.supportedModes = new Display.Mode[]{new Display.Mode(modeId, 1024, 1024,
+ 90)};
+ return false;
+ }
+ });
+ mExecutor.runAllReady();
+ clearInvocations(mFaceScanningDecorProvider);
+
+ AuthController.Callback callback = mAuthControllerCallback.getValue();
+ callback.onFaceSensorLocationChanged();
+ mExecutor.runAllReady();
+
+ verify(mFaceScanningDecorProvider).onReloadResAndMeasure(any(),
+ anyInt(),
+ anyInt(),
+ anyInt(),
+ any());
+ }
+
+ @Test
public void testPrivacyDotShowingListenerWorkWellWithNullParameter() {
mPrivacyDotShowingListener.onPrivacyDotShown(null);
mPrivacyDotShowingListener.onPrivacyDotHidden(null);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
index aac0b51..3da7261 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
@@ -19,8 +19,6 @@
import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR;
-import static com.android.systemui.flags.Flags.A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS;
-
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -46,7 +44,6 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
-import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.util.settings.SecureSettings;
@@ -74,6 +71,7 @@
public MockitoRule mockito = MockitoJUnit.rule();
private Context mContextWrapper;
+ private WindowManager mWindowManager;
private AccessibilityManager mAccessibilityManager;
private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
private AccessibilityFloatingMenuController mController;
@@ -97,6 +95,7 @@
}
};
+ mWindowManager = mContext.getSystemService(WindowManager.class);
mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
mLastButtonTargets = Settings.Secure.getStringForUser(mContextWrapper.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT);
@@ -158,7 +157,8 @@
public void onKeyguardVisibilityChanged_showing_destroyWidget() {
enableAccessibilityFloatingMenuConfig();
mController = setUpController();
- mController.mFloatingMenu = new AccessibilityFloatingMenu(mContextWrapper, mSecureSettings);
+ mController.mFloatingMenu = new MenuViewLayerController(mContextWrapper, mWindowManager,
+ mAccessibilityManager, mSecureSettings);
captureKeyguardUpdateMonitorCallback();
mKeyguardCallback.onUserUnlocked();
@@ -184,7 +184,8 @@
final int fakeUserId = 1;
enableAccessibilityFloatingMenuConfig();
mController = setUpController();
- mController.mFloatingMenu = new AccessibilityFloatingMenu(mContextWrapper, mSecureSettings);
+ mController.mFloatingMenu = new MenuViewLayerController(mContextWrapper, mWindowManager,
+ mAccessibilityManager, mSecureSettings);
captureKeyguardUpdateMonitorCallback();
mKeyguardCallback.onUserSwitching(fakeUserId);
@@ -197,7 +198,8 @@
final int fakeUserId = 1;
enableAccessibilityFloatingMenuConfig();
mController = setUpController();
- mController.mFloatingMenu = new AccessibilityFloatingMenu(mContextWrapper, mSecureSettings);
+ mController.mFloatingMenu = new MenuViewLayerController(mContextWrapper, mWindowManager,
+ mAccessibilityManager, mSecureSettings);
captureKeyguardUpdateMonitorCallback();
mKeyguardCallback.onUserUnlocked();
mKeyguardCallback.onKeyguardVisibilityChanged(true);
@@ -318,40 +320,18 @@
}
@Test
- public void onTargetsChanged_flingSpringAnimationsDisabled_floatingMenuIsCreated() {
- Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
- Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU,
- UserHandle.USER_CURRENT);
- final FakeFeatureFlags featureFlags = new FakeFeatureFlags();
- featureFlags.set(A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS, false);
-
- mController = setUpController();
- mController.onAccessibilityButtonTargetsChanged(TEST_A11Y_BTN_TARGETS);
-
- assertThat(mController.mFloatingMenu).isInstanceOf(AccessibilityFloatingMenu.class);
- }
-
- @Test
public void onTargetsChanged_isFloatingViewLayerControllerCreated() {
Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU,
UserHandle.USER_CURRENT);
- final FakeFeatureFlags featureFlags = new FakeFeatureFlags();
- featureFlags.set(A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS, true);
- mController = setUpController(featureFlags);
+ mController = setUpController();
mController.onAccessibilityButtonTargetsChanged(TEST_A11Y_BTN_TARGETS);
assertThat(mController.mFloatingMenu).isInstanceOf(MenuViewLayerController.class);
}
private AccessibilityFloatingMenuController setUpController() {
- final FakeFeatureFlags featureFlags = new FakeFeatureFlags();
- featureFlags.set(A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS, false);
- return setUpController(featureFlags);
- }
-
- private AccessibilityFloatingMenuController setUpController(FakeFeatureFlags featureFlags) {
final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
final FakeDisplayTracker displayTracker = new FakeDisplayTracker(mContext);
@@ -361,7 +341,7 @@
final AccessibilityFloatingMenuController controller =
new AccessibilityFloatingMenuController(mContextWrapper, windowManager,
displayManager, mAccessibilityManager, mTargetsObserver, mModeObserver,
- mKeyguardUpdateMonitor, featureFlags, mSecureSettings, displayTracker);
+ mKeyguardUpdateMonitor, mSecureSettings, displayTracker);
controller.init();
return controller;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java
deleted file mode 100644
index 04345fd..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.accessibility.floatingmenu;
-
-import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.doReturn;
-
-import android.content.Context;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.view.accessibility.AccessibilityManager;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.util.settings.SecureSettings;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/** Tests for {@link AccessibilityFloatingMenu}. */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class AccessibilityFloatingMenuTest extends SysuiTestCase {
-
- @Rule
- public MockitoRule mockito = MockitoJUnit.rule();
-
- @Mock
- private AccessibilityManager mAccessibilityManager;
- @Mock
- private SecureSettings mSecureSettings;
-
- private AccessibilityFloatingMenuView mMenuView;
- private AccessibilityFloatingMenu mMenu;
-
- @Before
- public void initMenu() {
- final List<String> assignedTargets = new ArrayList<>();
- mContext.addMockSystemService(Context.ACCESSIBILITY_SERVICE, mAccessibilityManager);
- assignedTargets.add(MAGNIFICATION_CONTROLLER_NAME);
- doReturn(assignedTargets).when(mAccessibilityManager).getAccessibilityShortcutTargets(
- anyInt());
-
- final Position position = new Position(0, 0);
- mMenuView = new AccessibilityFloatingMenuView(mContext, position);
- mMenu = new AccessibilityFloatingMenu(mContext, mSecureSettings, mMenuView);
- }
-
- @Test
- public void showMenuView_success() {
- mMenu.show();
-
- assertThat(mMenuView.isShowing()).isTrue();
- }
-
- @Test
- public void hideMenuView_success() {
- mMenu.show();
- mMenu.hide();
-
- assertThat(mMenuView.isShowing()).isFalse();
- }
-
- @Test
- public void showMenuView_emptyTarget_notShow() {
- final List<String> emptyTargets = new ArrayList<>();
- doReturn(emptyTargets).when(mAccessibilityManager).getAccessibilityShortcutTargets(
- anyInt());
-
- mMenu.show();
-
- assertThat(mMenuView.isShowing()).isFalse();
- }
-
- @After
- public void tearDown() {
- mMenu.hide();
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java
deleted file mode 100644
index bebd871..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java
+++ /dev/null
@@ -1,527 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.accessibility.floatingmenu;
-
-import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.view.View.OVER_SCROLL_ALWAYS;
-import static android.view.View.OVER_SCROLL_NEVER;
-import static android.view.WindowInsets.Type.displayCutout;
-import static android.view.WindowInsets.Type.ime;
-import static android.view.WindowInsets.Type.systemBars;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyFloat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Insets;
-import android.graphics.Rect;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.LayerDrawable;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewPropertyAnimator;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-import android.view.WindowMetrics;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.accessibility.dialog.AccessibilityTarget;
-import com.android.systemui.R;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.accessibility.MotionEventHelper;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/** Tests for {@link AccessibilityFloatingMenuView}. */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class AccessibilityFloatingMenuViewTest extends SysuiTestCase {
-
- @Rule
- public MockitoRule mockito = MockitoJUnit.rule();
-
- private final MotionEventHelper mMotionEventHelper = new MotionEventHelper();
- private final List<AccessibilityTarget> mTargets = new ArrayList<>(
- Collections.singletonList(mock(AccessibilityTarget.class)));
-
- private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);
-
- @Mock
- private WindowManager mWindowManager;
- @Mock
- private ViewPropertyAnimator mAnimator;
- @Mock
- private WindowMetrics mWindowMetrics;
- private MotionEvent mInterceptMotionEvent;
- private AccessibilityFloatingMenuView mMenuView;
- private RecyclerView mListView = new RecyclerView(mContext);
-
- private int mMenuWindowHeight;
- private int mMenuHalfWidth;
- private int mMenuHalfHeight;
- private int mDisplayHalfWidth;
- private int mDisplayHalfHeight;
- private int mMaxWindowX;
- private int mMaxWindowY;
- private final int mDisplayWindowWidth = 1080;
- private final int mDisplayWindowHeight = 2340;
-
- @Before
- public void initMenuView() {
- final WindowManager wm = mContext.getSystemService(WindowManager.class);
- doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
- mWindowManager).getMaximumWindowMetrics();
- mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
- when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
- when(mWindowMetrics.getBounds()).thenReturn(new Rect(0, 0, mDisplayWindowWidth,
- mDisplayWindowHeight));
- when(mWindowMetrics.getWindowInsets()).thenReturn(fakeDisplayInsets());
- mMenuView = spy(
- new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition, mListView));
- }
-
- @Before
- public void setUpMatrices() {
- final Resources res = mContext.getResources();
- final int margin =
- res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin);
- final int padding =
- res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_padding);
- final int iconWidthHeight =
- res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_width_height);
- final int menuWidth = padding * 2 + iconWidthHeight;
- final int menuHeight = (padding + iconWidthHeight) * mTargets.size() + padding;
- mMenuHalfWidth = menuWidth / 2;
- mMenuHalfHeight = menuHeight / 2;
- mDisplayHalfWidth = mDisplayWindowWidth / 2;
- mDisplayHalfHeight = mDisplayWindowHeight / 2;
- int marginStartEnd =
- mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT
- ? margin : 0;
- mMaxWindowX = mDisplayWindowWidth - marginStartEnd - menuWidth;
- mMenuWindowHeight = menuHeight + margin * 2;
- mMaxWindowY = mDisplayWindowHeight - mMenuWindowHeight;
- }
-
- @Test
- public void initListView_success() {
- assertThat(mListView.getCompatAccessibilityDelegate()).isNotNull();
- assertThat(mMenuView.getChildCount()).isEqualTo(1);
- }
-
- @Test
- public void showMenuView_success() {
- mMenuView.show();
-
- assertThat(mMenuView.isShowing()).isTrue();
- verify(mWindowManager).addView(eq(mMenuView), any(WindowManager.LayoutParams.class));
- }
-
- @Test
- public void showMenuView_showTwice_addViewOnce() {
- mMenuView.show();
- mMenuView.show();
-
- assertThat(mMenuView.isShowing()).isTrue();
- verify(mWindowManager, times(1)).addView(eq(mMenuView),
- any(WindowManager.LayoutParams.class));
- }
-
- @Test
- public void hideMenuView_success() {
- mMenuView.show();
- mMenuView.hide();
-
- assertThat(mMenuView.isShowing()).isFalse();
- verify(mWindowManager).removeView(eq(mMenuView));
- }
-
- @Test
- public void hideMenuView_hideTwice_removeViewOnce() {
- mMenuView.show();
- mMenuView.hide();
- mMenuView.hide();
-
- assertThat(mMenuView.isShowing()).isFalse();
- verify(mWindowManager, times(1)).removeView(eq(mMenuView));
- }
-
- @Test
- public void hideMenuViewWhenStartingAnimation_animatorNotRunning() {
- mMenuView.show();
-
- mMenuView.mDragAnimator.start();
- mMenuView.hide();
-
- assertThat(mMenuView.mDragAnimator.isRunning()).isFalse();
- }
-
- @Test
- public void onTargetsChanged_singleTarget_expectedRadii() {
- final Position alignRightPosition = new Position(1.0f, 0.0f);
- final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
- alignRightPosition);
- setupBasicMenuView(menuView);
-
- menuView.onTargetsChanged(mTargets);
-
- final View view = menuView.getChildAt(0);
- final LayerDrawable layerDrawable = (LayerDrawable) view.getBackground();
- final GradientDrawable gradientDrawable =
- (GradientDrawable) layerDrawable.getDrawable(0);
- final float smallRadius =
- getContext().getResources().getDimensionPixelSize(
- R.dimen.accessibility_floating_menu_small_single_radius);
- final float[] expectedRadii =
- new float[]{smallRadius, smallRadius, 0.0f, 0.0f, 0.0f, 0.0f, smallRadius,
- smallRadius};
- assertThat(gradientDrawable.getCornerRadii()).isEqualTo(expectedRadii);
- }
-
- @Test
- public void setSizeType_alignRightAndLargeSize_expectedRadii() {
- final RecyclerView listView = spy(new RecyclerView(mContext));
- final Position alignRightPosition = new Position(1.0f, 0.0f);
- final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
- alignRightPosition, listView);
- setupBasicMenuView(menuView);
-
- menuView.setSizeType(/* largeSize */ 1);
-
- final LayerDrawable layerDrawable =
- (LayerDrawable) listView.getBackground();
- final GradientDrawable gradientDrawable =
- (GradientDrawable) layerDrawable.getDrawable(0);
- final float largeRadius = getContext().getResources().getDimensionPixelSize(
- R.dimen.accessibility_floating_menu_large_single_radius);
- final float[] expectedRadii =
- new float[] {largeRadius, largeRadius, 0.0f, 0.0f, 0.0f, 0.0f, largeRadius,
- largeRadius};
- assertThat(gradientDrawable.getCornerRadii()).isEqualTo(expectedRadii);
- }
-
- @Test
- public void setShapeType_halfCircle_translationX() {
- final RecyclerView listView = spy(new RecyclerView(mContext));
- final AccessibilityFloatingMenuView menuView =
- new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition, listView);
- setupBasicMenuView(menuView);
- doReturn(mAnimator).when(listView).animate();
-
- menuView.setShapeType(/* halfOvalShape */ 1);
-
- verify(mAnimator).translationX(anyFloat());
- }
-
- @Test
- public void onTargetsChanged_fadeInOut() {
- final InOrder inOrderMenuView = inOrder(mMenuView);
-
- mMenuView.onTargetsChanged(mTargets);
-
- inOrderMenuView.verify(mMenuView).fadeIn();
- inOrderMenuView.verify(mMenuView).fadeOut();
- }
-
- @Test
- public void setSizeType_fadeInOut() {
- final InOrder inOrderMenuView = inOrder(mMenuView);
-
- mMenuView.setSizeType(/* smallSize */ 0);
-
- inOrderMenuView.verify(mMenuView).fadeIn();
- inOrderMenuView.verify(mMenuView).fadeOut();
- }
-
- @Test
- public void tapOnAndDragMenu_interceptUpEvent() {
- final RecyclerView listView = new RecyclerView(mContext);
- final TestAccessibilityFloatingMenu menuView =
- new TestAccessibilityFloatingMenu(mContext, mPlaceholderPosition, listView);
- setupBasicMenuView(menuView);
- final int currentWindowX = menuView.mCurrentLayoutParams.x;
- final int currentWindowY = menuView.mCurrentLayoutParams.y;
- final MotionEvent downEvent =
- mMotionEventHelper.obtainMotionEvent(0, 1,
- MotionEvent.ACTION_DOWN,
- currentWindowX + /* offsetXToMenuCenterX */ mMenuHalfWidth,
- currentWindowY + /* offsetYToMenuCenterY */ mMenuHalfHeight);
- final MotionEvent moveEvent =
- mMotionEventHelper.obtainMotionEvent(2, 3,
- MotionEvent.ACTION_MOVE,
- /* displayCenterX */mDisplayHalfWidth
- - /* offsetXToDisplayLeftHalfRegion */ 10,
- /* displayCenterY */ mDisplayHalfHeight);
- final MotionEvent upEvent =
- mMotionEventHelper.obtainMotionEvent(4, 5,
- MotionEvent.ACTION_UP,
- /* displayCenterX */ mDisplayHalfWidth
- - /* offsetXToDisplayLeftHalfRegion */ 10,
- /* displayCenterY */ mDisplayHalfHeight);
-
- listView.dispatchTouchEvent(downEvent);
- listView.dispatchTouchEvent(moveEvent);
- listView.dispatchTouchEvent(upEvent);
-
- assertThat(mInterceptMotionEvent.getAction()).isEqualTo(MotionEvent.ACTION_UP);
- }
-
- @Test
- public void tapOnAndDragMenu_matchLocation() {
- final float expectedX = 1.0f;
- final float expectedY = 0.7f;
- final Position position = new Position(expectedX, expectedY);
- final RecyclerView listView = new RecyclerView(mContext);
- final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
- position, listView);
- setupBasicMenuView(menuView);
- final int currentWindowX = menuView.mCurrentLayoutParams.x;
- final int currentWindowY = menuView.mCurrentLayoutParams.y;
- final MotionEvent downEvent =
- mMotionEventHelper.obtainMotionEvent(0, 1,
- MotionEvent.ACTION_DOWN,
- currentWindowX + /* offsetXToMenuCenterX */ mMenuHalfWidth,
- currentWindowY + /* offsetYToMenuCenterY */ mMenuHalfHeight);
- final MotionEvent moveEvent =
- mMotionEventHelper.obtainMotionEvent(2, 3,
- MotionEvent.ACTION_MOVE,
- /* displayCenterX */mDisplayHalfWidth
- + /* offsetXToDisplayRightHalfRegion */ 10,
- /* displayCenterY */ mDisplayHalfHeight);
- final MotionEvent upEvent =
- mMotionEventHelper.obtainMotionEvent(4, 5,
- MotionEvent.ACTION_UP,
- /* displayCenterX */ mDisplayHalfWidth
- + /* offsetXToDisplayRightHalfRegion */ 10,
- /* displayCenterY */ mDisplayHalfHeight);
-
- listView.dispatchTouchEvent(downEvent);
- listView.dispatchTouchEvent(moveEvent);
- listView.dispatchTouchEvent(upEvent);
- menuView.mDragAnimator.end();
-
- assertThat((float) menuView.mCurrentLayoutParams.x).isWithin(1.0f).of(mMaxWindowX);
- assertThat((float) menuView.mCurrentLayoutParams.y).isWithin(1.0f).of(
- /* newWindowY = displayCenterY - offsetY */ mDisplayHalfHeight - mMenuHalfHeight);
- }
-
-
- @Test
- public void tapOnAndDragMenuToDisplaySide_transformShapeHalfOval() {
- final Position alignRightPosition = new Position(1.0f, 0.8f);
- final RecyclerView listView = new RecyclerView(mContext);
- final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
- alignRightPosition, listView);
- setupBasicMenuView(menuView);
-
- final int currentWindowX = menuView.mCurrentLayoutParams.x;
- final int currentWindowY = menuView.mCurrentLayoutParams.y;
- final MotionEvent downEvent =
- mMotionEventHelper.obtainMotionEvent(0, 1,
- MotionEvent.ACTION_DOWN,
- currentWindowX + /* offsetXToMenuCenterX */ mMenuHalfWidth,
- currentWindowY + /* offsetYToMenuCenterY */ mMenuHalfHeight);
- final MotionEvent moveEvent =
- mMotionEventHelper.obtainMotionEvent(2, 3,
- MotionEvent.ACTION_MOVE,
- /* downX */(currentWindowX + mMenuHalfWidth)
- + /* offsetXToDisplayRightSide */ mMenuHalfWidth,
- /* downY */ (currentWindowY + mMenuHalfHeight));
- final MotionEvent upEvent =
- mMotionEventHelper.obtainMotionEvent(4, 5,
- MotionEvent.ACTION_UP,
- /* downX */(currentWindowX + mMenuHalfWidth)
- + /* offsetXToDisplayRightSide */ mMenuHalfWidth,
- /* downY */ (currentWindowY + mMenuHalfHeight));
-
- listView.dispatchTouchEvent(downEvent);
- listView.dispatchTouchEvent(moveEvent);
- listView.dispatchTouchEvent(upEvent);
-
- assertThat(menuView.mShapeType).isEqualTo(/* halfOval */ 1);
- }
-
- @Test
- public void onTargetsChanged_exceedAvailableHeight_overScrollAlways() {
- doReturn(true).when(mMenuView).hasExceededMaxLayoutHeight();
-
- mMenuView.onTargetsChanged(mTargets);
-
- assertThat(mListView.getOverScrollMode()).isEqualTo(OVER_SCROLL_ALWAYS);
- }
-
- @Test
- public void onTargetsChanged_notExceedAvailableHeight_overScrollNever() {
- doReturn(false).when(mMenuView).hasExceededMaxLayoutHeight();
-
- mMenuView.onTargetsChanged(mTargets);
-
- assertThat(mListView.getOverScrollMode()).isEqualTo(OVER_SCROLL_NEVER);
- }
-
- @Test
- public void showMenuView_insetsListener_overlapWithIme_menuViewShifted() {
- final int offset = 200;
-
- final Position alignRightPosition = new Position(1.0f, 0.8f);
- final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
- alignRightPosition);
- setupBasicMenuView(menuView);
- final WindowInsets imeInset = fakeImeInsetWith(menuView, offset);
- when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
- when(mWindowMetrics.getWindowInsets()).thenReturn(imeInset);
- final int expectedLayoutY = menuView.mCurrentLayoutParams.y - offset;
- menuView.dispatchApplyWindowInsets(imeInset);
-
- assertThat(menuView.mCurrentLayoutParams.y).isEqualTo(expectedLayoutY);
- }
-
- @Test
- public void hideIme_onMenuViewShifted_menuViewMovedBack() {
- final int offset = 200;
- setupBasicMenuView(mMenuView);
- final WindowInsets imeInset = fakeImeInsetWith(mMenuView, offset);
- when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
- when(mWindowMetrics.getWindowInsets()).thenReturn(imeInset);
- final int expectedLayoutY = mMenuView.mCurrentLayoutParams.y;
- mMenuView.dispatchApplyWindowInsets(imeInset);
-
- mMenuView.dispatchApplyWindowInsets(
- new WindowInsets.Builder().setVisible(ime(), false).build());
-
- assertThat(mMenuView.mCurrentLayoutParams.y).isEqualTo(expectedLayoutY);
- }
-
- @Test
- public void showMenuAndIme_withHigherIme_alignDisplayTopEdge() {
- final int offset = 99999;
-
- setupBasicMenuView(mMenuView);
- final WindowInsets imeInset = fakeImeInsetWith(mMenuView, offset);
- when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
- when(mWindowMetrics.getWindowInsets()).thenReturn(imeInset);
- mMenuView.dispatchApplyWindowInsets(imeInset);
-
- assertThat(mMenuView.mCurrentLayoutParams.y).isEqualTo(0);
- }
-
- @Test
- public void testConstructor_withPosition_expectedPosition() {
- final float expectedX = 1.0f;
- final float expectedY = 0.7f;
- final Position position = new Position(expectedX, expectedY);
-
- final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
- position);
- setupBasicMenuView(menuView);
-
- assertThat((float) menuView.mCurrentLayoutParams.x).isWithin(1.0f).of(mMaxWindowX);
- assertThat((float) menuView.mCurrentLayoutParams.y).isWithin(1.0f).of(
- expectedY * mMaxWindowY);
- }
-
- @After
- public void tearDown() {
- mInterceptMotionEvent = null;
- mMotionEventHelper.recycleEvents();
- mListView = null;
- }
-
- private void setupBasicMenuView(AccessibilityFloatingMenuView menuView) {
- menuView.show();
- menuView.onTargetsChanged(mTargets);
- menuView.setSizeType(0);
- menuView.setShapeType(0);
- }
-
- /**
- * Based on the current menu status, fake the ime inset component {@link WindowInsets} used
- * for testing.
- *
- * @param menuView {@link AccessibilityFloatingMenuView} that needs to be changed
- * @param offset is used for the y-axis position of ime higher than the y-axis position of menu
- * @return the ime inset
- */
- private WindowInsets fakeImeInsetWith(AccessibilityFloatingMenuView menuView, int offset) {
- // Ensure the keyboard has overlapped on the menu view.
- final int fakeImeHeight =
- mDisplayWindowHeight - (menuView.mCurrentLayoutParams.y + mMenuWindowHeight)
- + offset;
- return new WindowInsets.Builder()
- .setVisible(ime(), true)
- .setInsets(ime(), Insets.of(0, 0, 0, fakeImeHeight))
- .build();
- }
-
- private WindowInsets fakeDisplayInsets() {
- final int fakeStatusBarHeight = 75;
- final int fakeNavigationBarHeight = 125;
- return new WindowInsets.Builder()
- .setVisible(systemBars() | displayCutout(), true)
- .setInsets(systemBars() | displayCutout(),
- Insets.of(0, fakeStatusBarHeight, 0, fakeNavigationBarHeight))
- .build();
- }
-
- private class TestAccessibilityFloatingMenu extends AccessibilityFloatingMenuView {
- TestAccessibilityFloatingMenu(Context context, Position position, RecyclerView listView) {
- super(context, position, listView);
- }
-
- @Override
- public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
- @NonNull MotionEvent event) {
- final boolean intercept = super.onInterceptTouchEvent(recyclerView, event);
-
- if (intercept) {
- mInterceptMotionEvent = event;
- }
-
- return intercept;
- }
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java
deleted file mode 100644
index 3553a0a..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.accessibility.floatingmenu;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.view.MotionEvent;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-import android.view.WindowMetrics;
-import android.view.accessibility.AccessibilityNodeInfo;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.accessibility.MotionEventHelper;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/** Tests for {@link BaseTooltipView}. */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class BaseTooltipViewTest extends SysuiTestCase {
-
- @Mock
- private WindowManager mWindowManager;
-
- @Mock
- private WindowMetrics mWindowMetrics;
-
- private AccessibilityFloatingMenuView mMenuView;
- private BaseTooltipView mToolTipView;
-
- private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);
- private final MotionEventHelper mMotionEventHelper = new MotionEventHelper();
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
-
- final WindowManager wm = mContext.getSystemService(WindowManager.class);
- doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
- mWindowManager).getMaximumWindowMetrics();
- mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
- when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
- when(mWindowMetrics.getBounds()).thenReturn(new Rect());
- when(mWindowMetrics.getWindowInsets()).thenReturn(new WindowInsets.Builder().build());
-
- mMenuView = new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition);
- mToolTipView = new BaseTooltipView(mContext, mMenuView);
- }
-
- @Test
- public void showToolTipView_success() {
- mToolTipView.show();
-
- verify(mWindowManager).addView(eq(mToolTipView), any(WindowManager.LayoutParams.class));
- }
-
- @Test
- public void touchOutsideWhenToolTipViewShown_dismiss() {
- final MotionEvent outsideEvent =
- mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0,
- /* eventTime= */1,
- MotionEvent.ACTION_OUTSIDE,
- /* x= */ 0,
- /* y= */ 0);
-
- mToolTipView.show();
- mToolTipView.dispatchTouchEvent(outsideEvent);
-
- verify(mWindowManager).removeView(mToolTipView);
- }
-
- @Test
- public void getAccessibilityActionList_matchResult() {
- final AccessibilityNodeInfo infos = new AccessibilityNodeInfo();
- mToolTipView.onInitializeAccessibilityNodeInfo(infos);
-
- assertThat(infos.getActionList().size()).isEqualTo(1);
- }
-
- @Test
- public void accessibilityAction_dismiss_success() {
- final BaseTooltipView tooltipView =
- spy(new BaseTooltipView(mContext, mMenuView));
-
- final boolean isActionPerformed =
- tooltipView.performAccessibilityAction(
- AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS.getId(),
- /* arguments= */ null);
-
- assertThat(isActionPerformed).isTrue();
- verify(tooltipView).hide();
- }
-
- @After
- public void tearDown() {
- mToolTipView.hide();
- mMotionEventHelper.recycleEvents();
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java
deleted file mode 100644
index 9eba49d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.accessibility.floatingmenu;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.view.MotionEvent;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-import android.view.WindowMetrics;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.accessibility.MotionEventHelper;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/** Tests for {@link DockTooltipView}. */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class DockTooltipViewTest extends SysuiTestCase {
-
- @Mock
- private WindowManager mWindowManager;
-
- @Mock
- private WindowMetrics mWindowMetrics;
-
- private AccessibilityFloatingMenuView mMenuView;
- private DockTooltipView mDockTooltipView;
- private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);
- private final MotionEventHelper mMotionEventHelper = new MotionEventHelper();
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
-
- final WindowManager wm = mContext.getSystemService(WindowManager.class);
- doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
- mWindowManager).getMaximumWindowMetrics();
- mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
- when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
- when(mWindowMetrics.getBounds()).thenReturn(new Rect());
- when(mWindowMetrics.getWindowInsets()).thenReturn(new WindowInsets.Builder().build());
-
- mMenuView = spy(new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition));
- mDockTooltipView = new DockTooltipView(mContext, mMenuView);
- }
-
- @Test
- public void showTooltip_success() {
- mDockTooltipView.show();
-
- verify(mMenuView).startTranslateXAnimation();
- verify(mWindowManager).addView(eq(mDockTooltipView), any(WindowManager.LayoutParams.class));
- }
-
- @Test
- public void hideTooltip_success() {
- mDockTooltipView.show();
- mDockTooltipView.hide();
-
- verify(mMenuView).stopTranslateXAnimation();
- verify(mWindowManager).removeView(mDockTooltipView);
- }
-
- @Test
- public void touchOutsideWhenToolTipViewShown_stopAnimation() {
- final MotionEvent outsideEvent =
- mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0,
- /* eventTime= */ 1,
- MotionEvent.ACTION_OUTSIDE,
- /* x= */ 0,
- /* y= */ 0);
-
- mDockTooltipView.show();
- mDockTooltipView.dispatchTouchEvent(outsideEvent);
-
- verify(mMenuView).stopTranslateXAnimation();
- }
-
- @After
- public void tearDown() {
- mMotionEventHelper.recycleEvents();
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompatTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompatTest.java
deleted file mode 100644
index ea104a7..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompatTest.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.accessibility.floatingmenu;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-import android.view.WindowMetrics;
-import android.view.accessibility.AccessibilityNodeInfo;
-
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.R;
-import com.android.systemui.SysuiTestCase;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-/** Tests for {@link ItemDelegateCompat}. */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class ItemDelegateCompatTest extends SysuiTestCase {
- @Rule
- public MockitoRule mockito = MockitoJUnit.rule();
-
- @Mock
- private WindowManager mWindowManager;
-
- @Mock
- private WindowMetrics mWindowMetrics;
- private RecyclerView mListView;
- private AccessibilityFloatingMenuView mMenuView;
- private ItemDelegateCompat mItemDelegateCompat;
- private final Rect mAvailableBounds = new Rect(100, 200, 300, 400);
- private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);
-
- @Before
- public void setUp() {
- final WindowManager wm = mContext.getSystemService(WindowManager.class);
- doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
- mWindowManager).getMaximumWindowMetrics();
- mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
- when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
- when(mWindowMetrics.getBounds()).thenReturn(new Rect());
- when(mWindowMetrics.getWindowInsets()).thenReturn(new WindowInsets.Builder().build());
-
- mListView = new RecyclerView(mContext);
- mMenuView =
- spy(new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition, mListView));
- mItemDelegateCompat =
- new ItemDelegateCompat(new RecyclerViewAccessibilityDelegate(mListView), mMenuView);
- }
-
- @Test
- public void getAccessibilityActionList_matchResult() {
- final AccessibilityNodeInfoCompat info =
- new AccessibilityNodeInfoCompat(new AccessibilityNodeInfo());
-
- mItemDelegateCompat.onInitializeAccessibilityNodeInfo(mListView, info);
-
- assertThat(info.getActionList().size()).isEqualTo(5);
- }
-
- @Test
- public void performAccessibilityMoveTopLeftAction_halfOval_success() {
- doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
- mMenuView.setShapeType(/* halfOvalShape */ 1);
-
- final boolean moveTopLeftAction =
- mItemDelegateCompat.performAccessibilityAction(mListView, R.id.action_move_top_left,
- null);
-
- assertThat(moveTopLeftAction).isTrue();
- assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
- verify(mMenuView).snapToLocation(mAvailableBounds.left, mAvailableBounds.top);
- }
-
- @Test
- public void performAccessibilityMoveTopRightAction_halfOval_success() {
- doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
- mMenuView.setShapeType(/* halfOvalShape */ 1);
-
- final boolean moveTopRightAction =
- mItemDelegateCompat.performAccessibilityAction(mListView,
- R.id.action_move_top_right, null);
-
- assertThat(moveTopRightAction).isTrue();
- assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
- verify(mMenuView).snapToLocation(mAvailableBounds.right, mAvailableBounds.top);
- }
-
- @Test
- public void performAccessibilityMoveBottomLeftAction_halfOval_success() {
- doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
- mMenuView.setShapeType(/* halfOvalShape */ 1);
-
- final boolean moveBottomLeftAction =
- mItemDelegateCompat.performAccessibilityAction(mListView,
- R.id.action_move_bottom_left, null);
-
- assertThat(moveBottomLeftAction).isTrue();
- assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
- verify(mMenuView).snapToLocation(mAvailableBounds.left, mAvailableBounds.bottom);
- }
-
- @Test
- public void performAccessibilityMoveBottomRightAction_halfOval_success() {
- doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
- mMenuView.setShapeType(/* halfOvalShape */ 1);
-
- final boolean moveBottomRightAction =
- mItemDelegateCompat.performAccessibilityAction(mListView,
- R.id.action_move_bottom_right, null);
-
- assertThat(moveBottomRightAction).isTrue();
- assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
- verify(mMenuView).snapToLocation(mAvailableBounds.right, mAvailableBounds.bottom);
- }
-
- @Test
- public void performAccessibilityMoveOutEdgeAction_halfOval_success() {
- doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
- mMenuView.setShapeType(/* halfOvalShape */ 1);
-
- final boolean moveOutEdgeAndShowAction =
- mItemDelegateCompat.performAccessibilityAction(mListView,
- R.id.action_move_out_edge_and_show, null);
-
- assertThat(moveOutEdgeAndShowAction).isTrue();
- assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
- }
-
- @Test
- public void setupAccessibilityActions_oval_hasActionMoveToEdgeAndHide() {
- final AccessibilityNodeInfoCompat info =
- new AccessibilityNodeInfoCompat(new AccessibilityNodeInfo());
- mMenuView.setShapeType(/* ovalShape */ 0);
-
- mItemDelegateCompat.onInitializeAccessibilityNodeInfo(mListView, info);
-
- assertThat(info.getActionList().stream().anyMatch(
- action -> action.getId() == R.id.action_move_to_edge_and_hide)).isTrue();
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MigrationTooltipViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MigrationTooltipViewTest.java
deleted file mode 100644
index 2fb0a90..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MigrationTooltipViewTest.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.accessibility.floatingmenu;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.text.SpannableString;
-import android.text.method.LinkMovementMethod;
-import android.widget.TextView;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.R;
-import com.android.systemui.SysuiTestCase;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/** Tests for {@link MigrationTooltipView}. */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class MigrationTooltipViewTest extends SysuiTestCase {
-
- private TextView mTextView;
- private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);
-
- @Before
- public void setUp() {
- final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
- mPlaceholderPosition);
- final MigrationTooltipView toolTipView = new MigrationTooltipView(mContext, menuView);
- mTextView = toolTipView.findViewById(R.id.text);
- }
-
- @Test
- public void onCreate_setLinkMovementMethod() {
- assertThat(mTextView.getMovementMethod()).isInstanceOf(LinkMovementMethod.class);
- }
-
- @Test
- public void onCreate_setDescription_matchTextAndSpanNum() {
- final CharSequence expectedTextWithoutSpan =
- AnnotationLinkSpan.linkify(mContext.getText(
- R.string.accessibility_floating_button_migration_tooltip)).toString();
- final SpannableString spannableString = (SpannableString) mTextView.getText();
- final int AnnotationLinkSpanNum =
- spannableString.getSpans(/* queryStart= */ 0, spannableString.length(),
- AnnotationLinkSpan.class).length;
-
- assertThat(AnnotationLinkSpanNum).isEqualTo(1);
- assertThat(mTextView.getText().toString().contentEquals(expectedTextWithoutSpan)).isTrue();
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
index 5afe49e..0d00e8a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -85,6 +85,7 @@
import com.android.internal.R;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.widget.LockPatternUtils;
+import com.android.settingslib.udfps.UdfpsUtils;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
import com.android.systemui.biometrics.domain.interactor.LogContextInteractor;
@@ -167,6 +168,8 @@
private BiometricPromptCredentialInteractor mBiometricPromptCredentialInteractor;
@Mock
private CredentialViewModel mCredentialViewModel;
+ @Mock
+ private UdfpsUtils mUdfpsUtils;
@Captor
private ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> mFpAuthenticatorsRegisteredCaptor;
@@ -958,7 +961,7 @@
mPanelInteractionDetector, mUserManager, mLockPatternUtils, mUdfpsLogger,
mLogContextInteractor, () -> mBiometricPromptCredentialInteractor,
() -> mCredentialViewModel, mInteractionJankMonitor, mHandler,
- mBackgroundExecutor, mVibratorHelper);
+ mBackgroundExecutor, mVibratorHelper, mUdfpsUtils);
}
@Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
index b5c0f20..41beada 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
@@ -58,7 +58,7 @@
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags.MODERN_ALTERNATE_BOUNCER
-import com.android.systemui.keyguard.data.repository.FakeBiometricRepository
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
@@ -137,7 +137,7 @@
alternateBouncerInteractor =
AlternateBouncerInteractor(
keyguardBouncerRepository,
- FakeBiometricRepository(),
+ FakeBiometricSettingsRepository(),
FakeDeviceEntryFingerprintAuthRepository(),
FakeSystemClock(),
mock(KeyguardUpdateMonitor::class.java),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index 36ed6d5..9866163 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -38,6 +38,8 @@
import android.view.accessibility.AccessibilityManager
import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.settingslib.udfps.UdfpsOverlayParams
+import com.android.settingslib.udfps.UdfpsUtils
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ActivityLaunchAnimator
@@ -110,6 +112,7 @@
@Mock private lateinit var featureFlags: FeatureFlags
@Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
@Mock private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
+ @Mock private lateinit var udfpsUtils: UdfpsUtils
@Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true }
@@ -144,7 +147,7 @@
configurationController, keyguardStateController, unlockedScreenOffAnimationController,
udfpsDisplayMode, secureSettings, REQUEST_ID, reason,
controllerCallback, onTouch, activityLaunchAnimator, featureFlags,
- primaryBouncerInteractor, alternateBouncerInteractor, isDebuggable
+ primaryBouncerInteractor, alternateBouncerInteractor, isDebuggable, udfpsUtils
)
block()
}
@@ -400,109 +403,6 @@
assertThat(controllerOverlay.matchesRequestId(REQUEST_ID)).isTrue()
assertThat(controllerOverlay.matchesRequestId(REQUEST_ID + 1)).isFalse()
}
-
- @Test
- fun testTouchOutsideAreaNoRotation() = withReason(REASON_ENROLL_ENROLLING) {
- val touchHints =
- context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
- val rotation = Surface.ROTATION_0
- // touch at 0 degrees
- assertThat(
- controllerOverlay.onTouchOutsideOfSensorAreaImpl(
- 0.0f /* x */, 0.0f /* y */,
- 0.0f /* sensorX */, 0.0f /* sensorY */, rotation
- )
- ).isEqualTo(touchHints[0])
- // touch at 90 degrees
- assertThat(
- controllerOverlay.onTouchOutsideOfSensorAreaImpl(
- 0.0f /* x */, -1.0f /* y */,
- 0.0f /* sensorX */, 0.0f /* sensorY */, rotation
- )
- ).isEqualTo(touchHints[1])
- // touch at 180 degrees
- assertThat(
- controllerOverlay.onTouchOutsideOfSensorAreaImpl(
- -1.0f /* x */, 0.0f /* y */,
- 0.0f /* sensorX */, 0.0f /* sensorY */, rotation
- )
- ).isEqualTo(touchHints[2])
- // touch at 270 degrees
- assertThat(
- controllerOverlay.onTouchOutsideOfSensorAreaImpl(
- 0.0f /* x */, 1.0f /* y */,
- 0.0f /* sensorX */, 0.0f /* sensorY */, rotation
- )
- ).isEqualTo(touchHints[3])
- }
-
- fun testTouchOutsideAreaNoRotation90Degrees() = withReason(REASON_ENROLL_ENROLLING) {
- val touchHints =
- context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
- val rotation = Surface.ROTATION_90
- // touch at 0 degrees -> 90 degrees
- assertThat(
- controllerOverlay.onTouchOutsideOfSensorAreaImpl(
- 0.0f /* x */, 0.0f /* y */,
- 0.0f /* sensorX */, 0.0f /* sensorY */, rotation
- )
- ).isEqualTo(touchHints[1])
- // touch at 90 degrees -> 180 degrees
- assertThat(
- controllerOverlay.onTouchOutsideOfSensorAreaImpl(
- 0.0f /* x */, -1.0f /* y */,
- 0.0f /* sensorX */, 0.0f /* sensorY */, rotation
- )
- ).isEqualTo(touchHints[2])
- // touch at 180 degrees -> 270 degrees
- assertThat(
- controllerOverlay.onTouchOutsideOfSensorAreaImpl(
- -1.0f /* x */, 0.0f /* y */,
- 0.0f /* sensorX */, 0.0f /* sensorY */, rotation
- )
- ).isEqualTo(touchHints[3])
- // touch at 270 degrees -> 0 degrees
- assertThat(
- controllerOverlay.onTouchOutsideOfSensorAreaImpl(
- 0.0f /* x */, 1.0f /* y */,
- 0.0f /* sensorX */, 0.0f /* sensorY */, rotation
- )
- ).isEqualTo(touchHints[0])
- }
-
- fun testTouchOutsideAreaNoRotation270Degrees() = withReason(REASON_ENROLL_ENROLLING) {
- val touchHints =
- context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
- val rotation = Surface.ROTATION_270
- // touch at 0 degrees -> 270 degrees
- assertThat(
- controllerOverlay.onTouchOutsideOfSensorAreaImpl(
- 0.0f /* x */, 0.0f /* y */,
- 0.0f /* sensorX */, 0.0f /* sensorY */, rotation
- )
- ).isEqualTo(touchHints[3])
- // touch at 90 degrees -> 0 degrees
- assertThat(
- controllerOverlay.onTouchOutsideOfSensorAreaImpl(
- 0.0f /* x */, -1.0f /* y */,
- 0.0f /* sensorX */, 0.0f /* sensorY */, rotation
- )
- ).isEqualTo(touchHints[0])
- // touch at 180 degrees -> 90 degrees
- assertThat(
- controllerOverlay.onTouchOutsideOfSensorAreaImpl(
- -1.0f /* x */, 0.0f /* y */,
- 0.0f /* sensorX */, 0.0f /* sensorY */, rotation
- )
- ).isEqualTo(touchHints[1])
- // touch at 270 degrees -> 180 degrees
- assertThat(
- controllerOverlay.onTouchOutsideOfSensorAreaImpl(
- 0.0f /* x */, 1.0f /* y */,
- 0.0f /* sensorX */, 0.0f /* sensorY */, rotation
- )
- ).isEqualTo(touchHints[2])
- }
}
private class EnrollListener(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index dd7082a..17c262d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -71,6 +71,8 @@
import com.android.internal.logging.InstanceIdSequence;
import com.android.internal.util.LatencyTracker;
import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.settingslib.udfps.UdfpsOverlayParams;
+import com.android.settingslib.udfps.UdfpsUtils;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.ActivityLaunchAnimator;
@@ -230,10 +232,12 @@
private ScreenLifecycle.Observer mScreenObserver;
private FingerprintSensorPropertiesInternal mOpticalProps;
private FingerprintSensorPropertiesInternal mUltrasonicProps;
+ private UdfpsUtils mUdfpsUtils;
@Before
public void setUp() {
Execution execution = new FakeExecution();
+ mUdfpsUtils = new UdfpsUtils();
when(mLayoutInflater.inflate(R.layout.udfps_view, null, false))
.thenReturn(mUdfpsView);
@@ -305,7 +309,7 @@
mUnlockedScreenOffAnimationController, mSystemUIDialogManager, mLatencyTracker,
mActivityLaunchAnimator, alternateTouchProvider, mBiometricExecutor,
mPrimaryBouncerInteractor, mSinglePointerTouchProcessor, mSessionTracker,
- mAlternateBouncerInteractor, mSecureSettings);
+ mAlternateBouncerInteractor, mSecureSettings, mUdfpsUtils);
verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture());
mOverlayController = mOverlayCaptor.getValue();
verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt
index d2183b3..c73ff1d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt
@@ -26,7 +26,7 @@
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.data.BouncerView
-import com.android.systemui.keyguard.data.repository.BiometricRepository
+import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepositoryImpl
@@ -92,7 +92,7 @@
mAlternateBouncerInteractor =
AlternateBouncerInteractor(
keyguardBouncerRepository,
- mock(BiometricRepository::class.java),
+ mock(BiometricSettingsRepository::class.java),
mock(DeviceEntryFingerprintAuthRepository::class.java),
mock(SystemClock::class.java),
mock(KeyguardUpdateMonitor::class.java),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
index 44fa4eb..07b4a64 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
@@ -25,6 +25,7 @@
import android.view.LayoutInflater
import android.view.Surface
import androidx.test.filters.SmallTest
+import com.android.settingslib.udfps.UdfpsOverlayParams
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.any
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt
index af46d9b..4b41537 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt
@@ -16,30 +16,23 @@
package com.android.systemui.biometrics.udfps
-import android.graphics.Point
import android.graphics.Rect
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.EllipseOverlapDetectorParams
import com.google.common.truth.Truth.assertThat
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.junit.runners.Parameterized.Parameters
-import org.mockito.Mockito.spy
-import org.mockito.Mockito.`when` as whenEver
@SmallTest
@RunWith(Parameterized::class)
class EllipseOverlapDetectorTest(val testCase: TestCase) : SysuiTestCase() {
- val underTest = spy(EllipseOverlapDetector(neededPoints = 1))
-
- @Before
- fun setUp() {
- // Use one single center point for testing, required or total number of points may change
- whenEver(underTest.calculateSensorPoints(SENSOR))
- .thenReturn(listOf(Point(SENSOR.centerX(), SENSOR.centerY())))
- }
+ val underTest =
+ EllipseOverlapDetector(
+ EllipseOverlapDetectorParams(minOverlap = .4f, targetSize = .2f, stepSize = 1)
+ )
@Test
fun isGoodOverlap() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt
index 8e20303..c40fd4fa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt
@@ -23,8 +23,8 @@
import android.view.Surface
import android.view.Surface.Rotation
import androidx.test.filters.SmallTest
+import com.android.settingslib.udfps.UdfpsOverlayParams
import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.UdfpsOverlayParams
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
index 71c335e..7177919 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
@@ -16,6 +16,8 @@
package com.android.systemui.clipboardoverlay;
+import static com.android.systemui.flags.Flags.CLIPBOARD_MINIMIZED_LAYOUT;
+
import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE;
import static org.junit.Assert.assertEquals;
@@ -38,6 +40,7 @@
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FakeFeatureFlags;
import org.junit.Before;
import org.junit.Test;
@@ -60,6 +63,7 @@
private ClipboardOverlayController mOverlayController;
@Mock
private ClipboardToast mClipboardToast;
+ private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@Mock
private UiEventLogger mUiEventLogger;
@@ -93,8 +97,10 @@
when(mClipboardManager.getPrimaryClip()).thenReturn(mSampleClipData);
when(mClipboardManager.getPrimaryClipSource()).thenReturn(mSampleSource);
+ mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, true);
+
mClipboardListener = new ClipboardListener(getContext(), mOverlayControllerProvider,
- mClipboardToast, mClipboardManager, mUiEventLogger);
+ mClipboardToast, mClipboardManager, mFeatureFlags, mUiEventLogger);
}
@@ -187,4 +193,34 @@
verify(mClipboardToast, times(1)).showCopiedToast();
verifyZeroInteractions(mOverlayControllerProvider);
}
+
+ @Test
+ public void test_minimizedLayoutFlagOff_usesLegacy() {
+ mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false);
+
+ mClipboardListener.start();
+ mClipboardListener.onPrimaryClipChanged();
+
+ verify(mOverlayControllerProvider).get();
+
+ verify(mOverlayController).setClipDataLegacy(
+ mClipDataCaptor.capture(), mStringCaptor.capture());
+
+ assertEquals(mSampleClipData, mClipDataCaptor.getValue());
+ assertEquals(mSampleSource, mStringCaptor.getValue());
+ }
+
+ @Test
+ public void test_minimizedLayoutFlagOn_usesNew() {
+ mClipboardListener.start();
+ mClipboardListener.onPrimaryClipChanged();
+
+ verify(mOverlayControllerProvider).get();
+
+ verify(mOverlayController).setClipData(
+ mClipDataCaptor.capture(), mStringCaptor.capture());
+
+ assertEquals(mSampleClipData, mClipDataCaptor.getValue());
+ assertEquals(mSampleSource, mStringCaptor.getValue());
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt
new file mode 100644
index 0000000..faef35e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.systemui.clipboardoverlay
+
+import android.content.ClipData
+import android.content.ClipDescription
+import android.content.ContentResolver
+import android.content.Context
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.PersistableBundle
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.whenever
+import java.io.IOException
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ClipboardModelTest : SysuiTestCase() {
+ @Mock private lateinit var mClipboardUtils: ClipboardOverlayUtils
+ @Mock private lateinit var mMockContext: Context
+ @Mock private lateinit var mMockContentResolver: ContentResolver
+ private lateinit var mSampleClipData: ClipData
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ mSampleClipData = ClipData("Test", arrayOf("text/plain"), ClipData.Item("Test Item"))
+ }
+
+ @Test
+ fun test_nullClipData() {
+ val model = ClipboardModel.fromClipData(mContext, mClipboardUtils, null, "test source")
+ assertNull(model.clipData)
+ assertEquals("test source", model.source)
+ assertEquals(ClipboardModel.Type.OTHER, model.type)
+ assertNull(model.item)
+ assertFalse(model.isSensitive)
+ assertFalse(model.isRemote)
+ assertNull(model.loadThumbnail(mContext))
+ }
+
+ @Test
+ fun test_textClipData() {
+ val source = "test source"
+ val model = ClipboardModel.fromClipData(mContext, mClipboardUtils, mSampleClipData, source)
+ assertEquals(mSampleClipData, model.clipData)
+ assertEquals(source, model.source)
+ assertEquals(ClipboardModel.Type.TEXT, model.type)
+ assertEquals(mSampleClipData.getItemAt(0), model.item)
+ assertFalse(model.isSensitive)
+ assertFalse(model.isRemote)
+ assertNull(model.loadThumbnail(mContext))
+ }
+
+ @Test
+ fun test_sensitiveExtra() {
+ val description = mSampleClipData.description
+ val b = PersistableBundle()
+ b.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
+ description.extras = b
+ val data = ClipData(description, mSampleClipData.getItemAt(0))
+ val (_, _, _, _, sensitive) =
+ ClipboardModel.fromClipData(mContext, mClipboardUtils, data, "")
+ assertTrue(sensitive)
+ }
+
+ @Test
+ fun test_remoteExtra() {
+ whenever(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(true)
+ val model = ClipboardModel.fromClipData(mContext, mClipboardUtils, mSampleClipData, "")
+ assertTrue(model.isRemote)
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun test_imageClipData() {
+ val testBitmap = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888)
+ whenever(mMockContext.contentResolver).thenReturn(mMockContentResolver)
+ whenever(mMockContext.resources).thenReturn(mContext.resources)
+ whenever(mMockContentResolver.loadThumbnail(any(), any(), any())).thenReturn(testBitmap)
+ whenever(mMockContentResolver.getType(any())).thenReturn("image")
+ val imageClipData =
+ ClipData("Test", arrayOf("text/plain"), ClipData.Item(Uri.parse("test")))
+ val model = ClipboardModel.fromClipData(mMockContext, mClipboardUtils, imageClipData, "")
+ assertEquals(ClipboardModel.Type.IMAGE, model.type)
+ assertEquals(testBitmap, model.loadThumbnail(mMockContext))
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun test_imageClipData_loadFailure() {
+ whenever(mMockContext.contentResolver).thenReturn(mMockContentResolver)
+ whenever(mMockContext.resources).thenReturn(mContext.resources)
+ whenever(mMockContentResolver.loadThumbnail(any(), any(), any())).thenThrow(IOException())
+ whenever(mMockContentResolver.getType(any())).thenReturn("image")
+ val imageClipData =
+ ClipData("Test", arrayOf("text/plain"), ClipData.Item(Uri.parse("test")))
+ val model = ClipboardModel.fromClipData(mMockContext, mClipboardUtils, imageClipData, "")
+ assertEquals(ClipboardModel.Type.IMAGE, model.type)
+ assertNull(model.loadThumbnail(mMockContext))
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
index ca5b7af..0ac2667 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -16,13 +16,17 @@
package com.android.systemui.clipboardoverlay;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_SHOWN;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHARE_TAPPED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
+import static com.android.systemui.flags.Flags.CLIPBOARD_MINIMIZED_LAYOUT;
import static com.android.systemui.flags.Flags.CLIPBOARD_REMOTE_BEHAVIOR;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -35,8 +39,11 @@
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.Context;
+import android.graphics.Insets;
+import android.graphics.Rect;
import android.net.Uri;
import android.os.PersistableBundle;
+import android.view.WindowInsets;
import android.view.textclassifier.TextLinks;
import androidx.test.filters.SmallTest;
@@ -102,11 +109,14 @@
when(mClipboardOverlayView.getEnterAnimation()).thenReturn(mAnimator);
when(mClipboardOverlayView.getExitAnimation()).thenReturn(mAnimator);
+ when(mClipboardOverlayWindow.getWindowInsets()).thenReturn(
+ getImeInsets(new Rect(0, 0, 0, 0)));
mSampleClipData = new ClipData("Test", new String[]{"text/plain"},
new ClipData.Item("Test Item"));
mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, false);
+ mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, true); // turned off for legacy tests
mOverlayController = new ClipboardOverlayController(
mContext,
@@ -118,8 +128,7 @@
mFeatureFlags,
mClipboardUtils,
mExecutor,
- mUiEventLogger,
- mDisplayTracker);
+ mUiEventLogger);
verify(mClipboardOverlayView).setCallbacks(mOverlayCallbacksCaptor.capture());
mCallbacks = mOverlayCallbacksCaptor.getValue();
}
@@ -130,6 +139,159 @@
}
@Test
+ public void test_setClipData_nullData_legacy() {
+ ClipData clipData = null;
+ mOverlayController.setClipDataLegacy(clipData, "");
+
+ verify(mClipboardOverlayView, times(1)).showDefaultTextPreview();
+ verify(mClipboardOverlayView, times(0)).showShareChip();
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_setClipData_invalidImageData_legacy() {
+ ClipData clipData = new ClipData("", new String[]{"image/png"},
+ new ClipData.Item(Uri.parse("")));
+
+ mOverlayController.setClipDataLegacy(clipData, "");
+
+ verify(mClipboardOverlayView, times(1)).showDefaultTextPreview();
+ verify(mClipboardOverlayView, times(0)).showShareChip();
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_setClipData_textData_legacy() {
+ mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+ verify(mClipboardOverlayView, times(1)).showTextPreview("Test Item", false);
+ verify(mClipboardOverlayView, times(1)).showShareChip();
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_setClipData_sensitiveTextData_legacy() {
+ ClipDescription description = mSampleClipData.getDescription();
+ PersistableBundle b = new PersistableBundle();
+ b.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true);
+ description.setExtras(b);
+ ClipData data = new ClipData(description, mSampleClipData.getItemAt(0));
+ mOverlayController.setClipDataLegacy(data, "");
+
+ verify(mClipboardOverlayView, times(1)).showTextPreview("••••••", true);
+ verify(mClipboardOverlayView, times(1)).showShareChip();
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_setClipData_repeatedCalls_legacy() {
+ when(mAnimator.isRunning()).thenReturn(true);
+
+ mOverlayController.setClipDataLegacy(mSampleClipData, "");
+ mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_viewCallbacks_onShareTapped_legacy() {
+ mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+ mCallbacks.onShareButtonTapped();
+
+ verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "");
+ verify(mClipboardOverlayView, times(1)).getExitAnimation();
+ }
+
+ @Test
+ public void test_viewCallbacks_onDismissTapped_legacy() {
+ mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+ mCallbacks.onDismissButtonTapped();
+
+ verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, "");
+ verify(mClipboardOverlayView, times(1)).getExitAnimation();
+ }
+
+ @Test
+ public void test_multipleDismissals_dismissesOnce_legacy() {
+ mCallbacks.onSwipeDismissInitiated(mAnimator);
+ mCallbacks.onDismissButtonTapped();
+ mCallbacks.onSwipeDismissInitiated(mAnimator);
+ mCallbacks.onDismissButtonTapped();
+
+ verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SWIPE_DISMISSED, 0, null);
+ verify(mUiEventLogger, never()).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
+ }
+
+ @Test
+ public void test_remoteCopy_withFlagOn_legacy() {
+ mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true);
+ when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(true);
+
+ mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+ verify(mTimeoutHandler, never()).resetTimeout();
+ }
+
+ @Test
+ public void test_remoteCopy_withFlagOff_legacy() {
+ when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(true);
+
+ mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+ verify(mTimeoutHandler).resetTimeout();
+ }
+
+ @Test
+ public void test_nonRemoteCopy_legacy() {
+ mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true);
+ when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(false);
+
+ mOverlayController.setClipDataLegacy(mSampleClipData, "");
+
+ verify(mTimeoutHandler).resetTimeout();
+ }
+
+ @Test
+ public void test_logsUseLastClipSource_legacy() {
+ mOverlayController.setClipDataLegacy(mSampleClipData, "first.package");
+ mCallbacks.onDismissButtonTapped();
+ mOverlayController.setClipDataLegacy(mSampleClipData, "second.package");
+ mCallbacks.onDismissButtonTapped();
+
+ verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, "first.package");
+ verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, "second.package");
+ verifyNoMoreInteractions(mUiEventLogger);
+ }
+
+ @Test
+ public void test_logOnClipboardActionsShown_legacy() {
+ ClipData.Item item = mSampleClipData.getItemAt(0);
+ item.setTextLinks(Mockito.mock(TextLinks.class));
+ mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true);
+ when(mClipboardUtils.isRemoteCopy(any(Context.class), any(ClipData.class), anyString()))
+ .thenReturn(true);
+ when(mClipboardUtils.getAction(any(ClipData.Item.class), anyString()))
+ .thenReturn(Optional.of(Mockito.mock(RemoteAction.class)));
+ when(mClipboardOverlayView.post(any(Runnable.class))).thenAnswer(new Answer<Object>() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ ((Runnable) invocation.getArgument(0)).run();
+ return null;
+ }
+ });
+
+ mOverlayController.setClipDataLegacy(
+ new ClipData(mSampleClipData.getDescription(), item), "actionShownSource");
+ mExecutor.runAllReady();
+
+ verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_ACTION_SHOWN, 0, "actionShownSource");
+ verifyNoMoreInteractions(mUiEventLogger);
+ }
+
+ // start of refactored setClipData tests
+ @Test
public void test_setClipData_nullData() {
ClipData clipData = null;
mOverlayController.setClipData(clipData, "");
@@ -280,4 +442,43 @@
verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_ACTION_SHOWN, 0, "actionShownSource");
verifyNoMoreInteractions(mUiEventLogger);
}
+
+ @Test
+ public void test_noInsets_showsExpanded() {
+ mOverlayController.setClipData(mSampleClipData, "");
+
+ verify(mClipboardOverlayView, never()).setMinimized(true);
+ verify(mClipboardOverlayView).setMinimized(false);
+ verify(mClipboardOverlayView).showTextPreview("Test Item", false);
+ }
+
+ @Test
+ public void test_insets_showsMinimized() {
+ when(mClipboardOverlayWindow.getWindowInsets()).thenReturn(
+ getImeInsets(new Rect(0, 0, 0, 1)));
+ mOverlayController.setClipData(mSampleClipData, "");
+
+ verify(mClipboardOverlayView).setMinimized(true);
+ verify(mClipboardOverlayView, never()).setMinimized(false);
+ verify(mClipboardOverlayView, never()).showTextPreview(any(), anyBoolean());
+
+ mCallbacks.onMinimizedViewTapped();
+
+ verify(mClipboardOverlayView).setMinimized(false);
+ verify(mClipboardOverlayView).showTextPreview("Test Item", false);
+ }
+
+ @Test
+ public void test_insetsChanged_minimizes() {
+ mOverlayController.setClipData(mSampleClipData, "");
+ verify(mClipboardOverlayView, never()).setMinimized(true);
+
+ WindowInsets insetsWithKeyboard = getImeInsets(new Rect(0, 0, 0, 1));
+ mOverlayController.onInsetsChanged(insetsWithKeyboard, ORIENTATION_PORTRAIT);
+ verify(mClipboardOverlayView).setMinimized(true);
+ }
+
+ private static WindowInsets getImeInsets(Rect r) {
+ return new WindowInsets.Builder().setInsets(WindowInsets.Type.ime(), Insets.of(r)).build();
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
index d54babf..e35b2a3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
@@ -104,8 +104,6 @@
ArgumentCaptor<ControlsBindingController.LoadCallback>
@Captor
- private lateinit var userTrackerCallbackCaptor: ArgumentCaptor<UserTracker.Callback>
- @Captor
private lateinit var listingCallbackCaptor:
ArgumentCaptor<ControlsListingController.ControlsListingCallback>
@@ -178,10 +176,6 @@
)
controller.auxiliaryPersistenceWrapper = auxiliaryPersistenceWrapper
- verify(userTracker).addCallback(
- capture(userTrackerCallbackCaptor), any()
- )
-
verify(listingController).addCallback(capture(listingCallbackCaptor))
}
@@ -539,7 +533,7 @@
reset(persistenceWrapper)
- userTrackerCallbackCaptor.value.onUserChanged(otherUser, mContext)
+ controller.changeUser(UserHandle.of(otherUser))
verify(persistenceWrapper).changeFileAndBackupManager(any(), any())
verify(persistenceWrapper).readFavorites()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt
new file mode 100644
index 0000000..7ecaca6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.controls.start
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.ServiceInfo
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.ui.SelectedItem
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.time.FakeSystemClock
+import java.util.Optional
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ControlsStartableTest : SysuiTestCase() {
+
+ @Mock private lateinit var controlsController: ControlsController
+ @Mock private lateinit var controlsListingController: ControlsListingController
+ @Mock private lateinit var userTracker: UserTracker
+
+ private lateinit var fakeExecutor: FakeExecutor
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ context.orCreateTestableResources.addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf<String>()
+ )
+
+ fakeExecutor = FakeExecutor(FakeSystemClock())
+ }
+
+ @Test
+ fun testDisabledNothingIsCalled() {
+ createStartable(enabled = false).start()
+
+ verifyZeroInteractions(controlsController, controlsListingController, userTracker)
+ }
+
+ @Test
+ fun testNoPreferredPackagesNoDefaultSelected_noNewSelection() {
+ `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+ val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController, never()).setPreferredSelection(any())
+ }
+
+ @Test
+ fun testPreferredPackagesNotInstalled_noNewSelection() {
+ context.orCreateTestableResources.addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf(TEST_PACKAGE_PANEL)
+ )
+ `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+ `when`(controlsListingController.getCurrentServices()).thenReturn(emptyList())
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController, never()).setPreferredSelection(any())
+ }
+
+ @Test
+ fun testPreferredPackageNotPanel_noNewSelection() {
+ context.orCreateTestableResources.addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf(TEST_PACKAGE_PANEL)
+ )
+ `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+ val listings = listOf(ControlsServiceInfo(TEST_COMPONENT, "not panel", hasPanel = false))
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController, never()).setPreferredSelection(any())
+ }
+
+ @Test
+ fun testExistingSelection_noNewSelection() {
+ context.orCreateTestableResources.addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf(TEST_PACKAGE_PANEL)
+ )
+ `when`(controlsController.getPreferredSelection())
+ .thenReturn(mock<SelectedItem.PanelItem>())
+ val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController, never()).setPreferredSelection(any())
+ }
+
+ @Test
+ fun testPanelAdded() {
+ context.orCreateTestableResources.addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf(TEST_PACKAGE_PANEL)
+ )
+ `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+ val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController).setPreferredSelection(listings[0].toPanelItem())
+ }
+
+ @Test
+ fun testMultiplePreferredOnlyOnePanel_panelAdded() {
+ context.orCreateTestableResources.addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf("other_package", TEST_PACKAGE_PANEL)
+ )
+ `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+ val listings =
+ listOf(
+ ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true),
+ ControlsServiceInfo(ComponentName("other_package", "cls"), "non panel", false)
+ )
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController).setPreferredSelection(listings[0].toPanelItem())
+ }
+
+ @Test
+ fun testMultiplePreferredMultiplePanels_firstPreferredAdded() {
+ context.orCreateTestableResources.addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf(TEST_PACKAGE_PANEL, "other_package")
+ )
+ `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+ val listings =
+ listOf(
+ ControlsServiceInfo(ComponentName("other_package", "cls"), "panel", true),
+ ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)
+ )
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController).setPreferredSelection(listings[1].toPanelItem())
+ }
+
+ @Test
+ fun testPreferredSelectionIsPanel_bindOnStart() {
+ val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+ `when`(controlsController.getPreferredSelection()).thenReturn(listings[0].toPanelItem())
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController).bindComponentForPanel(TEST_COMPONENT_PANEL)
+ }
+
+ @Test
+ fun testPreferredSelectionPanel_listingNoPanel_notBind() {
+ val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = false))
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+ `when`(controlsController.getPreferredSelection())
+ .thenReturn(SelectedItem.PanelItem("panel", TEST_COMPONENT_PANEL))
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController, never()).bindComponentForPanel(any())
+ }
+
+ @Test
+ fun testNotPanelSelection_noBind() {
+ val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = false))
+ `when`(controlsListingController.getCurrentServices()).thenReturn(listings)
+ `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION)
+
+ createStartable(enabled = true).start()
+
+ verify(controlsController, never()).bindComponentForPanel(any())
+ }
+
+ private fun createStartable(enabled: Boolean): ControlsStartable {
+ val component: ControlsComponent =
+ mock() {
+ `when`(isEnabled()).thenReturn(enabled)
+ if (enabled) {
+ `when`(getControlsController()).thenReturn(Optional.of(controlsController))
+ `when`(getControlsListingController())
+ .thenReturn(Optional.of(controlsListingController))
+ } else {
+ `when`(getControlsController()).thenReturn(Optional.empty())
+ `when`(getControlsListingController()).thenReturn(Optional.empty())
+ }
+ }
+ return ControlsStartable(context.resources, fakeExecutor, component, userTracker)
+ }
+
+ private fun ControlsServiceInfo(
+ componentName: ComponentName,
+ label: CharSequence,
+ hasPanel: Boolean
+ ): ControlsServiceInfo {
+ val serviceInfo =
+ ServiceInfo().apply {
+ applicationInfo = ApplicationInfo()
+ packageName = componentName.packageName
+ name = componentName.className
+ }
+ return FakeControlsServiceInfo(context, serviceInfo, label, hasPanel)
+ }
+
+ private class FakeControlsServiceInfo(
+ context: Context,
+ serviceInfo: ServiceInfo,
+ private val label: CharSequence,
+ hasPanel: Boolean
+ ) : ControlsServiceInfo(context, serviceInfo) {
+
+ init {
+ if (hasPanel) {
+ panelActivity = serviceInfo.componentName
+ }
+ }
+
+ override fun loadLabel(): CharSequence {
+ return label
+ }
+ }
+
+ companion object {
+ private fun ControlsServiceInfo.toPanelItem(): SelectedItem.PanelItem {
+ if (panelActivity == null) {
+ throw IllegalArgumentException("$this is not a panel")
+ }
+ return SelectedItem.PanelItem(loadLabel(), componentName)
+ }
+
+ private const val TEST_PACKAGE = "pkg"
+ private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "service")
+ private const val TEST_PACKAGE_PANEL = "pkg.panel"
+ private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
index b7d0f29..0f25764 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
@@ -289,4 +289,36 @@
verify(mCallback, times(1)).onStateChanged();
assertThat(stateController.areEntryAnimationsFinished()).isTrue();
}
+
+ @Test
+ public void testNotifyDreamOverlayStatusBarVisibleChanged() {
+ final DreamOverlayStateController stateController =
+ new DreamOverlayStateController(mExecutor, true);
+
+ stateController.addCallback(mCallback);
+ mExecutor.runAllReady();
+ assertThat(stateController.isDreamOverlayStatusBarVisible()).isFalse();
+
+ stateController.setDreamOverlayStatusBarVisible(true);
+ mExecutor.runAllReady();
+
+ verify(mCallback, times(1)).onStateChanged();
+ assertThat(stateController.isDreamOverlayStatusBarVisible()).isTrue();
+ }
+
+ @Test
+ public void testNotifyHasAssistantAttentionChanged() {
+ final DreamOverlayStateController stateController =
+ new DreamOverlayStateController(mExecutor, true);
+
+ stateController.addCallback(mCallback);
+ mExecutor.runAllReady();
+ assertThat(stateController.hasAssistantAttention()).isFalse();
+
+ stateController.setHasAssistantAttention(true);
+ mExecutor.runAllReady();
+
+ verify(mCallback, times(1)).onStateChanged();
+ assertThat(stateController.hasAssistantAttention()).isTrue();
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
index 596b903..d16b757 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
@@ -468,6 +468,21 @@
}
@Test
+ public void testAssistantAttentionIconShownWhenAttentionGained() {
+ mController.onViewAttached();
+
+ when(mDreamOverlayStateController.hasAssistantAttention()).thenReturn(true);
+
+ final ArgumentCaptor<DreamOverlayStateController.Callback> callbackCapture =
+ ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
+ verify(mDreamOverlayStateController).addCallback(callbackCapture.capture());
+ callbackCapture.getValue().onStateChanged();
+
+ verify(mView).showIcon(
+ DreamOverlayStatusBarView.STATUS_ICON_ASSISTANT_ATTENTION_ACTIVE, true, null);
+ }
+
+ @Test
public void testStatusBarHiddenWhenSystemStatusBarShown() {
mController.onViewAttached();
@@ -572,6 +587,13 @@
assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE);
}
+ @Test
+ public void testDreamOverlayStatusBarVisibleSetToFalseOnDetach() {
+ mController.onViewAttached();
+ mController.onViewDetached();
+ verify(mDreamOverlayStateController).setDreamOverlayStatusBarVisible(false);
+ }
+
private StatusBarWindowStateListener updateStatusBarWindowState(boolean show) {
when(mStatusBarWindowStateController.windowIsShowing()).thenReturn(show);
final ArgumentCaptor<StatusBarWindowStateListener>
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java
new file mode 100644
index 0000000..ef1061f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams.conditions;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.RemoteException;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.app.AssistUtils;
+import com.android.internal.app.IVisualQueryDetectionAttentionListener;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.shared.condition.Condition;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class AssistantAttentionConditionTest extends SysuiTestCase {
+ @Mock
+ Condition.Callback mCallback;
+ @Mock
+ AssistUtils mAssistUtils;
+ @Mock
+ DreamOverlayStateController mDreamOverlayStateController;
+
+ private AssistantAttentionCondition mAssistantAttentionCondition;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ mAssistantAttentionCondition =
+ new AssistantAttentionCondition(mDreamOverlayStateController, mAssistUtils);
+ // Adding a callback also starts the condition.
+ mAssistantAttentionCondition.addCallback(mCallback);
+ }
+
+ @Test
+ public void testEnableVisualQueryDetection() {
+ final ArgumentCaptor<DreamOverlayStateController.Callback> argumentCaptor =
+ ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
+ verify(mDreamOverlayStateController).addCallback(argumentCaptor.capture());
+
+ when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(true);
+ argumentCaptor.getValue().onStateChanged();
+
+ verify(mAssistUtils).enableVisualQueryDetection(any());
+ }
+
+ @Test
+ public void testDisableVisualQueryDetection() {
+ final ArgumentCaptor<DreamOverlayStateController.Callback> argumentCaptor =
+ ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
+ verify(mDreamOverlayStateController).addCallback(argumentCaptor.capture());
+
+ when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(true);
+ argumentCaptor.getValue().onStateChanged();
+ when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(false);
+ argumentCaptor.getValue().onStateChanged();
+
+ verify(mAssistUtils).disableVisualQueryDetection();
+ }
+
+ @Test
+ public void testAttentionChangedTriggersCondition() throws RemoteException {
+ final ArgumentCaptor<DreamOverlayStateController.Callback> callbackCaptor =
+ ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
+ verify(mDreamOverlayStateController).addCallback(callbackCaptor.capture());
+
+ when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(true);
+ callbackCaptor.getValue().onStateChanged();
+
+ final ArgumentCaptor<IVisualQueryDetectionAttentionListener> listenerCaptor =
+ ArgumentCaptor.forClass(IVisualQueryDetectionAttentionListener.class);
+ verify(mAssistUtils).enableVisualQueryDetection(listenerCaptor.capture());
+
+ listenerCaptor.getValue().onAttentionGained();
+ assertThat(mAssistantAttentionCondition.isConditionMet()).isTrue();
+
+ listenerCaptor.getValue().onAttentionLost();
+ assertThat(mAssistantAttentionCondition.isConditionMet()).isFalse();
+
+ verify(mCallback, times(2)).onConditionChanged(eq(mAssistantAttentionCondition));
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt
index a3740d8..925c06f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt
@@ -19,7 +19,6 @@
import android.content.Context
import android.media.AudioManager
-import androidx.lifecycle.LiveData
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.settings.UserFileManager
@@ -27,10 +26,12 @@
import com.android.systemui.util.RingerModeTracker
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
@@ -57,13 +58,15 @@
@Mock
private lateinit var userFileManager: UserFileManager
+ private lateinit var testDispatcher: TestDispatcher
private lateinit var testScope: TestScope
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- testScope = TestScope()
+ testDispatcher = StandardTestDispatcher()
+ testScope = TestScope(testDispatcher)
whenever(userTracker.userContext).thenReturn(context)
whenever(userFileManager.getSharedPreferences(any(), any(), any()))
@@ -74,7 +77,10 @@
userTracker,
userFileManager,
ringerModeTracker,
- audioManager
+ audioManager,
+ testScope.backgroundScope,
+ testDispatcher,
+ testDispatcher,
)
}
@@ -103,17 +109,16 @@
}
@Test
- fun `triggered - state was previously NORMAL - currently SILENT - move to previous state`() {
+ fun `triggered - state was previously NORMAL - currently SILENT - move to previous state`() = testScope.runTest {
//given
val ringerModeCapture = argumentCaptor<Int>()
- val ringerModeInternal = mock<LiveData<Int>>()
- whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
- whenever(ringerModeInternal.value).thenReturn(AudioManager.RINGER_MODE_NORMAL)
+ whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL)
underTest.onTriggered(null)
- whenever(ringerModeInternal.value).thenReturn(AudioManager.RINGER_MODE_SILENT)
+ whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_SILENT)
//when
val result = underTest.onTriggered(null)
+ runCurrent()
verify(audioManager, times(2)).ringerModeInternal = ringerModeCapture.capture()
//then
@@ -122,15 +127,14 @@
}
@Test
- fun `triggered - state is not SILENT - move to SILENT ringer`() {
+ fun `triggered - state is not SILENT - move to SILENT ringer`() = testScope.runTest {
//given
val ringerModeCapture = argumentCaptor<Int>()
- val ringerModeInternal = mock<LiveData<Int>>()
- whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
- whenever(ringerModeInternal.value).thenReturn(AudioManager.RINGER_MODE_NORMAL)
+ whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL)
//when
val result = underTest.onTriggered(null)
+ runCurrent()
verify(audioManager).ringerModeInternal = ringerModeCapture.capture()
//then
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt
index 26601b6..34f3ed8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt
@@ -37,6 +37,8 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -67,6 +69,7 @@
@Mock
private lateinit var keyguardQuickAffordanceRepository: KeyguardQuickAffordanceRepository
+ private lateinit var testDispatcher: TestDispatcher
private lateinit var testScope: TestScope
private lateinit var underTest: MuteQuickAffordanceCoreStartable
@@ -83,7 +86,8 @@
val emission = MutableStateFlow(mapOf("testQuickAffordanceKey" to listOf(config)))
whenever(keyguardQuickAffordanceRepository.selections).thenReturn(emission)
- testScope = TestScope()
+ testDispatcher = StandardTestDispatcher()
+ testScope = TestScope(testDispatcher)
underTest = MuteQuickAffordanceCoreStartable(
featureFlags,
@@ -91,7 +95,8 @@
ringerModeTracker,
userFileManager,
keyguardQuickAffordanceRepository,
- testScope,
+ testScope.backgroundScope,
+ testDispatcher,
)
}
@@ -158,6 +163,7 @@
runCurrent()
verify(ringerModeInternal).observeForever(observerCaptor.capture())
observerCaptor.value.onChanged(newRingerMode)
+ runCurrent()
val result = sharedPrefs.getInt("key_last_non_silent_ringer_mode", -1)
//then
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
similarity index 94%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricRepositoryTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
index 4aac7d0..ddd1049 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
@@ -33,7 +33,6 @@
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestScope
@@ -48,12 +47,11 @@
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@RunWith(AndroidTestingRunner::class)
-class BiometricRepositoryTest : SysuiTestCase() {
- private lateinit var underTest: BiometricRepository
+class BiometricSettingsRepositoryTest : SysuiTestCase() {
+ private lateinit var underTest: BiometricSettingsRepository
@Mock private lateinit var authController: AuthController
@Mock private lateinit var lockPatternUtils: LockPatternUtils
@@ -73,11 +71,11 @@
userRepository = FakeUserRepository()
}
- private suspend fun createBiometricRepository() {
+ private suspend fun createBiometricSettingsRepository() {
userRepository.setUserInfos(listOf(PRIMARY_USER))
userRepository.setSelectedUserInfo(PRIMARY_USER)
underTest =
- BiometricRepositoryImpl(
+ BiometricSettingsRepositoryImpl(
context = context,
lockPatternUtils = lockPatternUtils,
broadcastDispatcher = fakeBroadcastDispatcher,
@@ -93,7 +91,7 @@
@Test
fun fingerprintEnrollmentChange() =
testScope.runTest {
- createBiometricRepository()
+ createBiometricSettingsRepository()
val fingerprintEnabledByDevicePolicy = collectLastValue(underTest.isFingerprintEnrolled)
runCurrent()
@@ -119,7 +117,7 @@
@Test
fun strongBiometricAllowedChange() =
testScope.runTest {
- createBiometricRepository()
+ createBiometricSettingsRepository()
val strongBiometricAllowed = collectLastValue(underTest.isStrongBiometricAllowed)
runCurrent()
@@ -142,7 +140,7 @@
@Test
fun fingerprintDisabledByDpmChange() =
testScope.runTest {
- createBiometricRepository()
+ createBiometricSettingsRepository()
val fingerprintEnabledByDevicePolicy =
collectLastValue(underTest.isFingerprintEnabledByDevicePolicy)
runCurrent()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt
index 28974f8..8caf60f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt
@@ -22,7 +22,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.data.repository.FakeBiometricRepository
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepositoryImpl
@@ -47,7 +47,7 @@
class AlternateBouncerInteractorTest : SysuiTestCase() {
private lateinit var underTest: AlternateBouncerInteractor
private lateinit var bouncerRepository: KeyguardBouncerRepository
- private lateinit var biometricRepository: FakeBiometricRepository
+ private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
private lateinit var deviceEntryFingerprintAuthRepository:
FakeDeviceEntryFingerprintAuthRepository
@Mock private lateinit var systemClock: SystemClock
@@ -65,13 +65,13 @@
TestCoroutineScope(),
bouncerLogger,
)
- biometricRepository = FakeBiometricRepository()
+ biometricSettingsRepository = FakeBiometricSettingsRepository()
deviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
featureFlags = FakeFeatureFlags().apply { this.set(Flags.MODERN_ALTERNATE_BOUNCER, true) }
underTest =
AlternateBouncerInteractor(
bouncerRepository,
- biometricRepository,
+ biometricSettingsRepository,
deviceEntryFingerprintAuthRepository,
systemClock,
keyguardUpdateMonitor,
@@ -96,7 +96,7 @@
@Test
fun canShowAlternateBouncerForFingerprint_noFingerprintsEnrolled() {
givenCanShowAlternateBouncer()
- biometricRepository.setFingerprintEnrolled(false)
+ biometricSettingsRepository.setFingerprintEnrolled(false)
assertFalse(underTest.canShowAlternateBouncerForFingerprint())
}
@@ -104,7 +104,7 @@
@Test
fun canShowAlternateBouncerForFingerprint_strongBiometricNotAllowed() {
givenCanShowAlternateBouncer()
- biometricRepository.setStrongBiometricAllowed(false)
+ biometricSettingsRepository.setStrongBiometricAllowed(false)
assertFalse(underTest.canShowAlternateBouncerForFingerprint())
}
@@ -112,7 +112,7 @@
@Test
fun canShowAlternateBouncerForFingerprint_devicePolicyDoesNotAllowFingerprint() {
givenCanShowAlternateBouncer()
- biometricRepository.setFingerprintEnabledByDevicePolicy(false)
+ biometricSettingsRepository.setFingerprintEnabledByDevicePolicy(false)
assertFalse(underTest.canShowAlternateBouncerForFingerprint())
}
@@ -159,13 +159,13 @@
private fun givenCanShowAlternateBouncer() {
bouncerRepository.setAlternateBouncerUIAvailable(true)
- biometricRepository.setFingerprintEnrolled(true)
- biometricRepository.setStrongBiometricAllowed(true)
- biometricRepository.setFingerprintEnabledByDevicePolicy(true)
+ biometricSettingsRepository.setFingerprintEnrolled(true)
+ biometricSettingsRepository.setStrongBiometricAllowed(true)
+ biometricSettingsRepository.setFingerprintEnabledByDevicePolicy(true)
deviceEntryFingerprintAuthRepository.setLockedOut(false)
}
private fun givenCannotShowAlternateBouncer() {
- biometricRepository.setFingerprintEnrolled(false)
+ biometricSettingsRepository.setFingerprintEnrolled(false)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index 3a871b4..702f3763 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -24,6 +24,7 @@
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositoryImpl
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -437,6 +438,43 @@
}
@Test
+ fun `DOZING to GONE`() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to DOZING
+ runner.startTransition(
+ testScope,
+ TransitionInfo(
+ ownerName = "",
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.DOZING,
+ animator =
+ ValueAnimator().apply {
+ duration = 10
+ interpolator = Interpolators.LINEAR
+ },
+ )
+ )
+ runCurrent()
+ reset(mockTransitionRepository)
+
+ // WHEN biometrics succeeds with wake and unlock mode
+ keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(mockTransitionRepository).startTransition(capture())
+ }
+ // THEN a transition to DOZING should occur
+ assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor")
+ assertThat(info.from).isEqualTo(KeyguardState.DOZING)
+ assertThat(info.to).isEqualTo(KeyguardState.GONE)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
fun `GONE to DOZING`() =
testScope.runTest {
// GIVEN a device with AOD not available
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
new file mode 100644
index 0000000..a5b78b74
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardTransitionAnimationFlowTest : SysuiTestCase() {
+ private lateinit var underTest: KeyguardTransitionAnimationFlow
+ private lateinit var repository: FakeKeyguardTransitionRepository
+
+ @Before
+ fun setUp() {
+ repository = FakeKeyguardTransitionRepository()
+ underTest =
+ KeyguardTransitionAnimationFlow(
+ 1000.milliseconds,
+ repository.transitions,
+ )
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun zeroDurationThrowsException() = runTest {
+ val flow = underTest.createFlow(duration = 0.milliseconds, onStep = { it })
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun startTimePlusDurationGreaterThanTransitionDurationThrowsException() = runTest {
+ val flow =
+ underTest.createFlow(
+ startTime = 300.milliseconds,
+ duration = 800.milliseconds,
+ onStep = { it }
+ )
+ }
+
+ @Test
+ fun onFinishRunsWhenSpecified() = runTest {
+ val flow =
+ underTest.createFlow(
+ duration = 100.milliseconds,
+ onStep = { it },
+ onFinish = { 10f },
+ )
+ var animationValues = collectLastValue(flow)
+ repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+ assertThat(animationValues()).isEqualTo(10f)
+ }
+
+ @Test
+ fun onCancelRunsWhenSpecified() = runTest {
+ val flow =
+ underTest.createFlow(
+ duration = 100.milliseconds,
+ onStep = { it },
+ onCancel = { 100f },
+ )
+ var animationValues = collectLastValue(flow)
+ repository.sendTransitionStep(step(0.5f, TransitionState.CANCELED))
+ assertThat(animationValues()).isEqualTo(100f)
+ }
+
+ @Test
+ fun usesStartTime() = runTest {
+ val flow =
+ underTest.createFlow(
+ startTime = 500.milliseconds,
+ duration = 500.milliseconds,
+ onStep = { it },
+ )
+ var animationValues = collectLastValue(flow)
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ assertThat(animationValues()).isEqualTo(0f)
+
+ // Should not emit a value
+ repository.sendTransitionStep(step(0.1f, TransitionState.RUNNING))
+
+ repository.sendTransitionStep(step(0.5f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 0f)
+ repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 0.2f)
+ repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 0.6f)
+ repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 1f)
+ }
+
+ @Test
+ fun usesInterpolator() = runTest {
+ val flow =
+ underTest.createFlow(
+ duration = 1000.milliseconds,
+ interpolator = EMPHASIZED_ACCELERATE,
+ onStep = { it },
+ )
+ var animationValues = collectLastValue(flow)
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0f))
+ repository.sendTransitionStep(step(0.5f, TransitionState.RUNNING))
+ assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0.5f))
+ repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
+ assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0.6f))
+ repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
+ assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0.8f))
+ repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
+ assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(1f))
+ }
+
+ @Test
+ fun usesOnStepToDoubleValue() = runTest {
+ val flow =
+ underTest.createFlow(
+ duration = 1000.milliseconds,
+ onStep = { it * 2 },
+ )
+ var animationValues = collectLastValue(flow)
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ assertFloat(animationValues(), 0f)
+ repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 0.6f)
+ repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 1.2f)
+ repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 1.6f)
+ repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 2f)
+ }
+
+ private fun assertFloat(actual: Float?, expected: Float) {
+ assertThat(actual!!).isWithin(0.01f).of(expected)
+ }
+
+ private fun step(
+ value: Float,
+ state: TransitionState = TransitionState.RUNNING
+ ): TransitionStep {
+ return TransitionStep(
+ from = KeyguardState.GONE,
+ to = KeyguardState.DREAMING,
+ value = value,
+ transitionState = state,
+ ownerName = "GoneToDreamingTransitionViewModelTest"
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
index 5571663..06e397d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
@@ -18,19 +18,13 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
-import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_TRANSLATION_Y
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -46,6 +40,7 @@
class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() {
private lateinit var underTest: DreamingToLockscreenTransitionViewModel
private lateinit var repository: FakeKeyguardTransitionRepository
+ private lateinit var transitionAnimation: KeyguardTransitionAnimationFlow
@Before
fun setUp() {
@@ -63,32 +58,18 @@
val job =
underTest.dreamOverlayTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+ // Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.5f))
+ repository.sendTransitionStep(step(0.6f))
+ // ...up to here
+ repository.sendTransitionStep(step(0.8f))
repository.sendTransitionStep(step(1f))
- // Only 3 values should be present, since the dream overlay runs for a small fraction
- // of the overall animation time
- assertThat(values.size).isEqualTo(3)
- assertThat(values[0])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0f, DREAM_OVERLAY_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[1])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0.3f, DREAM_OVERLAY_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[2])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0.5f, DREAM_OVERLAY_TRANSLATION_Y)
- ) * pixels
- )
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
job.cancel()
}
@@ -100,16 +81,18 @@
val job = underTest.dreamOverlayAlpha.onEach { values.add(it) }.launchIn(this)
+ // Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.1f))
repository.sendTransitionStep(step(0.5f))
+ // ...up to here
repository.sendTransitionStep(step(1f))
// Only two values should be present, since the dream overlay runs for a small fraction
// of the overall animation time
- assertThat(values.size).isEqualTo(2)
- assertThat(values[0]).isEqualTo(1f - animValue(0f, DREAM_OVERLAY_ALPHA))
- assertThat(values[1]).isEqualTo(1f - animValue(0.1f, DREAM_OVERLAY_ALPHA))
+ assertThat(values.size).isEqualTo(4)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
job.cancel()
}
@@ -121,19 +104,15 @@
val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.1f))
- // Should start running here...
repository.sendTransitionStep(step(0.2f))
repository.sendTransitionStep(step(0.3f))
- // ...up to here
repository.sendTransitionStep(step(1f))
- // Only two values should be present, since the dream overlay runs for a small fraction
- // of the overall animation time
- assertThat(values.size).isEqualTo(2)
- assertThat(values[0]).isEqualTo(animValue(0.2f, LOCKSCREEN_ALPHA))
- assertThat(values[1]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA))
+ assertThat(values.size).isEqualTo(4)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
job.cancel()
}
@@ -147,58 +126,27 @@
val job =
underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.5f))
repository.sendTransitionStep(step(1f))
- assertThat(values.size).isEqualTo(4)
- assertThat(values[0])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(0f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[1])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[2])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[3])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(1f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
job.cancel()
}
- private fun animValue(stepValue: Float, params: AnimationParams): Float {
- val totalDuration = TO_LOCKSCREEN_DURATION
- val startValue = (params.startTime / totalDuration).toFloat()
-
- val multiplier = (totalDuration / params.duration).toFloat()
- return (stepValue - startValue) * multiplier
- }
-
- private fun step(value: Float): TransitionStep {
+ private fun step(
+ value: Float,
+ state: TransitionState = TransitionState.RUNNING
+ ): TransitionStep {
return TransitionStep(
from = KeyguardState.DREAMING,
to = KeyguardState.LOCKSCREEN,
value = value,
- transitionState = TransitionState.RUNNING,
+ transitionState = state,
ownerName = "DreamingToLockscreenTransitionViewModelTest"
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
index 7fa204b..14c3b50 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
@@ -18,16 +18,12 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -59,20 +55,18 @@
val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
// Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.1f))
repository.sendTransitionStep(step(0.2f))
- // ...up to here
repository.sendTransitionStep(step(0.3f))
+ // ...up to here
repository.sendTransitionStep(step(1f))
// Only three values should be present, since the dream overlay runs for a small
- // fraction
- // of the overall animation time
- assertThat(values.size).isEqualTo(3)
- assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA))
- assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA))
- assertThat(values[2]).isEqualTo(1f - animValue(0.2f, LOCKSCREEN_ALPHA))
+ // fraction of the overall animation time
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
job.cancel()
}
@@ -87,45 +81,19 @@
underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
// Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.5f))
- // ...up to here
- repository.sendTransitionStep(step(1f))
// And a final reset event on CANCEL
repository.sendTransitionStep(step(0.8f, TransitionState.CANCELED))
- assertThat(values.size).isEqualTo(4)
- assertThat(values[0])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[1])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[2])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[3]).isEqualTo(0f)
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+
job.cancel()
}
- private fun animValue(stepValue: Float, params: AnimationParams): Float {
- val totalDuration = TO_DREAMING_DURATION
- val startValue = (params.startTime / totalDuration).toFloat()
-
- val multiplier = (totalDuration / params.duration).toFloat()
- return (stepValue - startValue) * multiplier
- }
-
private fun step(
value: Float,
state: TransitionState = TransitionState.RUNNING
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
index 539fc2c..ed31dc3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -18,16 +18,12 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -59,19 +55,18 @@
val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
// Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.1f))
repository.sendTransitionStep(step(0.2f))
- // ...up to here
repository.sendTransitionStep(step(0.3f))
+ // ...up to here
repository.sendTransitionStep(step(1f))
// Only three values should be present, since the dream overlay runs for a small
// fraction of the overall animation time
- assertThat(values.size).isEqualTo(3)
- assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA))
- assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA))
- assertThat(values[2]).isEqualTo(1f - animValue(0.2f, LOCKSCREEN_ALPHA))
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
job.cancel()
}
@@ -85,47 +80,22 @@
val job =
underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
- // Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.5f))
- // ...up to here
repository.sendTransitionStep(step(1f))
// And a final reset event on FINISHED
repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
- assertThat(values.size).isEqualTo(4)
- assertThat(values[0])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[1])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[2])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[3]).isEqualTo(0f)
+ assertThat(values.size).isEqualTo(6)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+ // Validate finished value
+ assertThat(values[5]).isEqualTo(0f)
job.cancel()
}
- private fun animValue(stepValue: Float, params: AnimationParams): Float {
- val totalDuration = TO_DREAMING_DURATION
- val startValue = (params.startTime / totalDuration).toFloat()
-
- val multiplier = (totalDuration / params.duration).toFloat()
- return (stepValue - startValue) * multiplier
- }
-
private fun step(
value: Float,
state: TransitionState = TransitionState.RUNNING
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index 759345f..458b315 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -18,16 +18,12 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_OCCLUDED_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -59,19 +55,18 @@
val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
// Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.1f))
repository.sendTransitionStep(step(0.4f))
- // ...up to here
repository.sendTransitionStep(step(0.7f))
+ // ...up to here
repository.sendTransitionStep(step(1f))
// Only 3 values should be present, since the dream overlay runs for a small fraction
// of the overall animation time
- assertThat(values.size).isEqualTo(3)
- assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA))
- assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA))
- assertThat(values[2]).isEqualTo(1f - animValue(0.4f, LOCKSCREEN_ALPHA))
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
job.cancel()
}
@@ -86,54 +81,51 @@
underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
// Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.5f))
repository.sendTransitionStep(step(1f))
// ...up to here
- assertThat(values.size).isEqualTo(4)
- assertThat(values[0])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[1])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[2])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[3])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(1f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+
job.cancel()
}
- private fun animValue(stepValue: Float, params: AnimationParams): Float {
- val totalDuration = TO_OCCLUDED_DURATION
- val startValue = (params.startTime / totalDuration).toFloat()
+ @Test
+ fun lockscreenTranslationYIsCanceled() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
- val multiplier = (totalDuration / params.duration).toFloat()
- return (stepValue - startValue) * multiplier
- }
+ val pixels = 100
+ val job =
+ underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
- private fun step(value: Float): TransitionStep {
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.3f))
+ repository.sendTransitionStep(step(0.3f, TransitionState.CANCELED))
+
+ assertThat(values.size).isEqualTo(4)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+
+ // Cancel will reset the translation
+ assertThat(values[3]).isEqualTo(0)
+
+ job.cancel()
+ }
+
+ private fun step(
+ value: Float,
+ state: TransitionState = TransitionState.RUNNING,
+ ): TransitionStep {
return TransitionStep(
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.OCCLUDED,
value = value,
- transitionState = TransitionState.RUNNING,
+ transitionState = state,
ownerName = "LockscreenToOccludedTransitionViewModelTest"
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
index 98d292d..a36214e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
@@ -18,16 +18,12 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -58,21 +54,19 @@
val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
- repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ repository.sendTransitionStep(step(0.1f))
// Should start running here...
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.4f))
repository.sendTransitionStep(step(0.5f))
- // ...up to here
repository.sendTransitionStep(step(0.6f))
+ // ...up to here
+ repository.sendTransitionStep(step(0.8f))
repository.sendTransitionStep(step(1f))
- // Only two values should be present, since the dream overlay runs for a small fraction
- // of the overall animation time
- assertThat(values.size).isEqualTo(3)
- assertThat(values[0]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA))
- assertThat(values[1]).isEqualTo(animValue(0.4f, LOCKSCREEN_ALPHA))
- assertThat(values[2]).isEqualTo(animValue(0.5f, LOCKSCREEN_ALPHA))
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
job.cancel()
}
@@ -86,58 +80,27 @@
val job =
underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.5f))
repository.sendTransitionStep(step(1f))
- assertThat(values.size).isEqualTo(4)
- assertThat(values[0])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(0f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[1])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[2])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[3])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(1f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
job.cancel()
}
- private fun animValue(stepValue: Float, params: AnimationParams): Float {
- val totalDuration = TO_LOCKSCREEN_DURATION
- val startValue = (params.startTime / totalDuration).toFloat()
-
- val multiplier = (totalDuration / params.duration).toFloat()
- return (stepValue - startValue) * multiplier
- }
-
- private fun step(value: Float): TransitionStep {
+ private fun step(
+ value: Float,
+ state: TransitionState = TransitionState.RUNNING
+ ): TransitionStep {
return TransitionStep(
from = KeyguardState.OCCLUDED,
to = KeyguardState.LOCKSCREEN,
value = value,
- transitionState = TransitionState.RUNNING,
+ transitionState = state,
ownerName = "OccludedToLockscreenTransitionViewModelTest"
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt
index 1d6e980..670f117 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt
@@ -113,5 +113,6 @@
recommendations = emptyList(),
dismissIntent = null,
headphoneConnectionTimeMillis = 0,
- instanceId = InstanceId.fakeInstanceId(-1)
+ instanceId = InstanceId.fakeInstanceId(-1),
+ expiryTimeMs = 0,
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
index c0639f3..0a5b124 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
@@ -79,7 +79,7 @@
USER_ID, true, APP, null, ARTIST, TITLE, null,
new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null,
MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L,
- InstanceId.fakeInstanceId(-1), -1, false);
+ InstanceId.fakeInstanceId(-1), -1, false, null);
mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt
index 9d33e6f..eb6235c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt
@@ -27,11 +27,13 @@
import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.ui.MediaPlayerData
+import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executor
@@ -40,11 +42,11 @@
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
private const val KEY = "TEST_KEY"
@@ -72,6 +74,7 @@
@Mock private lateinit var smartspaceData: SmartspaceMediaData
@Mock private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction
@Mock private lateinit var logger: MediaUiEventLogger
+ @Mock private lateinit var mediaFlags: MediaFlags
private lateinit var mediaDataFilter: MediaDataFilter
private lateinit var dataMain: MediaData
@@ -82,6 +85,7 @@
fun setup() {
MockitoAnnotations.initMocks(this)
MediaPlayerData.clear()
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
mediaDataFilter =
MediaDataFilter(
context,
@@ -90,7 +94,8 @@
lockscreenUserManager,
executor,
clock,
- logger
+ logger,
+ mediaFlags
)
mediaDataFilter.mediaDataManager = mediaDataManager
mediaDataFilter.addListener(listener)
@@ -108,19 +113,20 @@
)
dataGuest = dataMain.copy(userId = USER_GUEST)
- `when`(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
- `when`(smartspaceData.isActive).thenReturn(true)
- `when`(smartspaceData.isValid()).thenReturn(true)
- `when`(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE)
- `when`(smartspaceData.recommendations).thenReturn(listOf(smartspaceMediaRecommendationItem))
- `when`(smartspaceData.headphoneConnectionTimeMillis)
+ whenever(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
+ whenever(smartspaceData.isActive).thenReturn(true)
+ whenever(smartspaceData.isValid()).thenReturn(true)
+ whenever(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE)
+ whenever(smartspaceData.recommendations)
+ .thenReturn(listOf(smartspaceMediaRecommendationItem))
+ whenever(smartspaceData.headphoneConnectionTimeMillis)
.thenReturn(clock.currentTimeMillis() - 100)
- `when`(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID)
+ whenever(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID)
}
private fun setUser(id: Int) {
- `when`(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false)
- `when`(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true)
+ whenever(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false)
+ whenever(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true)
mediaDataFilter.handleUserSwitched(id)
}
@@ -277,7 +283,7 @@
@Test
fun hasActiveMediaOrRecommendation_inactiveRecommendationSet_returnsFalse() {
- `when`(smartspaceData.isActive).thenReturn(false)
+ whenever(smartspaceData.isActive).thenReturn(false)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
@@ -285,7 +291,7 @@
@Test
fun hasActiveMediaOrRecommendation_invalidRecommendationSet_returnsFalse() {
- `when`(smartspaceData.isValid()).thenReturn(false)
+ whenever(smartspaceData.isValid()).thenReturn(false)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
@@ -293,8 +299,8 @@
@Test
fun hasActiveMediaOrRecommendation_activeAndValidRecommendationSet_returnsTrue() {
- `when`(smartspaceData.isActive).thenReturn(true)
- `when`(smartspaceData.isValid()).thenReturn(true)
+ whenever(smartspaceData.isActive).thenReturn(true)
+ whenever(smartspaceData.isValid()).thenReturn(true)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
@@ -349,7 +355,7 @@
@Test
fun testOnSmartspaceMediaDataLoaded_noMedia_inactiveRec_showsNothing() {
- `when`(smartspaceData.isActive).thenReturn(false)
+ whenever(smartspaceData.isActive).thenReturn(false)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
@@ -379,7 +385,7 @@
@Test
fun testOnSmartspaceMediaDataLoaded_noRecentMedia_inactiveRec_showsNothing() {
- `when`(smartspaceData.isActive).thenReturn(false)
+ whenever(smartspaceData.isActive).thenReturn(false)
val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
@@ -395,7 +401,7 @@
@Test
fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_inactiveRec_showsNothing() {
- `when`(smartspaceData.isActive).thenReturn(false)
+ whenever(smartspaceData.isActive).thenReturn(false)
// WHEN we have media that was recently played, but not currently active
val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
@@ -418,7 +424,7 @@
@Test
fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeInvalidRec_usesMedia() {
- `when`(smartspaceData.isValid()).thenReturn(false)
+ whenever(smartspaceData.isValid()).thenReturn(false)
// WHEN we have media that was recently played, but not currently active
val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
@@ -513,4 +519,59 @@
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
}
+
+ @Test
+ fun testOnSmartspaceLoaded_persistentEnabled_isInactive_notifiesListeners() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+ whenever(smartspaceData.isActive).thenReturn(false)
+
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ verify(listener)
+ .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+ assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+ assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isTrue()
+ }
+
+ @Test
+ fun testOnSmartspaceLoaded_persistentEnabled_inactive_hasRecentMedia_staysInactive() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+ whenever(smartspaceData.isActive).thenReturn(false)
+
+ // If there is media that was recently played but inactive
+ val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+ verify(listener)
+ .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+ // And an inactive recommendation is loaded
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ // Smartspace is loaded but the media stays inactive
+ verify(listener)
+ .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+ verify(listener, never())
+ .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+ assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+ assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isTrue()
+ }
+
+ @Test
+ fun testOnSwipeToDismiss_persistentEnabled_recommendationSetInactive() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+
+ val data =
+ EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+ targetId = SMARTSPACE_KEY,
+ isActive = true,
+ packageName = SMARTSPACE_PACKAGE,
+ recommendations = listOf(smartspaceMediaRecommendationItem),
+ )
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, data)
+ mediaDataFilter.onSwipeToDismiss()
+
+ verify(mediaDataManager).setRecommendationInactive(eq(SMARTSPACE_KEY))
+ verify(mediaDataManager, never())
+ .dismissSmartspaceRecommendation(eq(SMARTSPACE_KEY), anyLong())
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
index 1ac6695..44e2fbd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
@@ -46,6 +46,8 @@
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dump.DumpManager
import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.EXTRA_KEY_TRIGGER_SOURCE
+import com.android.systemui.media.controls.models.recommendation.EXTRA_VALUE_TRIGGER_PERIODIC
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider
import com.android.systemui.media.controls.resume.MediaResumeListener
@@ -82,6 +84,8 @@
private const val KEY = "KEY"
private const val KEY_2 = "KEY_2"
private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
+private const val SMARTSPACE_CREATION_TIME = 1234L
+private const val SMARTSPACE_EXPIRY_TIME = 5678L
private const val PACKAGE_NAME = "com.example.app"
private const val SYSTEM_PACKAGE_NAME = "com.android.systemui"
private const val APP_NAME = "SystemUI"
@@ -230,10 +234,12 @@
whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE)
whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA)
whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList)
- whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L)
+ whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(SMARTSPACE_CREATION_TIME)
+ whenever(mediaSmartspaceTarget.expiryTimeMillis).thenReturn(SMARTSPACE_EXPIRY_TIME)
whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false)
whenever(mediaFlags.isExplicitIndicatorEnabled()).thenReturn(true)
whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(false)
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
}
@@ -644,27 +650,8 @@
build()
}
val currentTime = clock.elapsedRealtime()
- mediaDataManager.addResumptionControls(
- USER_ID,
- desc,
- Runnable {},
- session.sessionToken,
- APP_NAME,
- pendingIntent,
- PACKAGE_NAME
- )
- assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
- assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
- // THEN the media data indicates that it is for resumption
- verify(listener)
- .onMediaDataLoaded(
- eq(PACKAGE_NAME),
- eq(null),
- capture(mediaDataCaptor),
- eq(true),
- eq(0),
- eq(false)
- )
+ addResumeControlAndLoad(desc)
+
val data = mediaDataCaptor.value
assertThat(data.resumption).isTrue()
assertThat(data.song).isEqualTo(SESSION_TITLE)
@@ -690,27 +677,8 @@
build()
}
val currentTime = clock.elapsedRealtime()
- mediaDataManager.addResumptionControls(
- USER_ID,
- desc,
- Runnable {},
- session.sessionToken,
- APP_NAME,
- pendingIntent,
- PACKAGE_NAME
- )
- assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
- assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
- // THEN the media data indicates that it is for resumption
- verify(listener)
- .onMediaDataLoaded(
- eq(PACKAGE_NAME),
- eq(null),
- capture(mediaDataCaptor),
- eq(true),
- eq(0),
- eq(false)
- )
+ addResumeControlAndLoad(desc)
+
val data = mediaDataCaptor.value
assertThat(data.resumption).isTrue()
assertThat(data.song).isEqualTo(SESSION_TITLE)
@@ -723,6 +691,84 @@
}
@Test
+ fun testAddResumptionControls_hasPartialProgress() {
+ whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
+
+ // WHEN resumption controls are added with partial progress
+ val progress = 0.5
+ val extras =
+ Bundle().apply {
+ putInt(
+ MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+ MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
+ )
+ putDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress)
+ }
+ val desc =
+ MediaDescription.Builder().run {
+ setTitle(SESSION_TITLE)
+ setExtras(extras)
+ build()
+ }
+ addResumeControlAndLoad(desc)
+
+ val data = mediaDataCaptor.value
+ assertThat(data.resumption).isTrue()
+ assertThat(data.resumeProgress).isEqualTo(progress)
+ }
+
+ @Test
+ fun testAddResumptionControls_hasNotPlayedProgress() {
+ whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
+
+ // WHEN resumption controls are added that have not been played
+ val extras =
+ Bundle().apply {
+ putInt(
+ MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+ MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
+ )
+ }
+ val desc =
+ MediaDescription.Builder().run {
+ setTitle(SESSION_TITLE)
+ setExtras(extras)
+ build()
+ }
+ addResumeControlAndLoad(desc)
+
+ val data = mediaDataCaptor.value
+ assertThat(data.resumption).isTrue()
+ assertThat(data.resumeProgress).isEqualTo(0)
+ }
+
+ @Test
+ fun testAddResumptionControls_hasFullProgress() {
+ whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
+
+ // WHEN resumption controls are added with progress info
+ val extras =
+ Bundle().apply {
+ putInt(
+ MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+ MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
+ )
+ }
+ val desc =
+ MediaDescription.Builder().run {
+ setTitle(SESSION_TITLE)
+ setExtras(extras)
+ build()
+ }
+ addResumeControlAndLoad(desc)
+
+ // THEN the media data includes the progress
+ val data = mediaDataCaptor.value
+ assertThat(data.resumption).isTrue()
+ assertThat(data.resumeProgress).isEqualTo(1)
+ }
+
+ @Test
fun testResumptionDisabled_dismissesResumeControls() {
// WHEN there are resume controls and resumption is switched off
val desc =
@@ -730,26 +776,8 @@
setTitle(SESSION_TITLE)
build()
}
- mediaDataManager.addResumptionControls(
- USER_ID,
- desc,
- Runnable {},
- session.sessionToken,
- APP_NAME,
- pendingIntent,
- PACKAGE_NAME
- )
- assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
- assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
- verify(listener)
- .onMediaDataLoaded(
- eq(PACKAGE_NAME),
- eq(null),
- capture(mediaDataCaptor),
- eq(true),
- eq(0),
- eq(false)
- )
+ addResumeControlAndLoad(desc)
+
val data = mediaDataCaptor.value
mediaDataManager.setMediaResumptionEnabled(false)
@@ -825,8 +853,9 @@
cardAction = mediaSmartspaceBaseAction,
recommendations = validRecommendationList,
dismissIntent = DISMISS_INTENT,
- headphoneConnectionTimeMillis = 1234L,
- instanceId = InstanceId.fakeInstanceId(instanceId)
+ headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+ instanceId = InstanceId.fakeInstanceId(instanceId),
+ expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
)
),
eq(false)
@@ -848,8 +877,9 @@
targetId = KEY_MEDIA_SMARTSPACE,
isActive = true,
dismissIntent = DISMISS_INTENT,
- headphoneConnectionTimeMillis = 1234L,
- instanceId = InstanceId.fakeInstanceId(instanceId)
+ headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+ instanceId = InstanceId.fakeInstanceId(instanceId),
+ expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
)
),
eq(false)
@@ -879,8 +909,9 @@
targetId = KEY_MEDIA_SMARTSPACE,
isActive = true,
dismissIntent = null,
- headphoneConnectionTimeMillis = 1234L,
- instanceId = InstanceId.fakeInstanceId(instanceId)
+ headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+ instanceId = InstanceId.fakeInstanceId(instanceId),
+ expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
)
),
eq(false)
@@ -909,6 +940,129 @@
}
@Test
+ fun testOnSmartspaceMediaDataLoaded_persistentEnabled_headphoneTrigger_isActive() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+ smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+ val instanceId = instanceIdSequence.lastInstanceId
+
+ verify(listener)
+ .onSmartspaceMediaDataLoaded(
+ eq(KEY_MEDIA_SMARTSPACE),
+ eq(
+ SmartspaceMediaData(
+ targetId = KEY_MEDIA_SMARTSPACE,
+ isActive = true,
+ packageName = PACKAGE_NAME,
+ cardAction = mediaSmartspaceBaseAction,
+ recommendations = validRecommendationList,
+ dismissIntent = DISMISS_INTENT,
+ headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+ instanceId = InstanceId.fakeInstanceId(instanceId),
+ expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
+ )
+ ),
+ eq(false)
+ )
+ }
+
+ @Test
+ fun testOnSmartspaceMediaDataLoaded_persistentEnabled_periodicTrigger_notActive() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+ val extras =
+ Bundle().apply {
+ putString("package_name", PACKAGE_NAME)
+ putParcelable("dismiss_intent", DISMISS_INTENT)
+ putString(EXTRA_KEY_TRIGGER_SOURCE, EXTRA_VALUE_TRIGGER_PERIODIC)
+ }
+ whenever(mediaSmartspaceBaseAction.extras).thenReturn(extras)
+
+ smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+ val instanceId = instanceIdSequence.lastInstanceId
+
+ verify(listener)
+ .onSmartspaceMediaDataLoaded(
+ eq(KEY_MEDIA_SMARTSPACE),
+ eq(
+ SmartspaceMediaData(
+ targetId = KEY_MEDIA_SMARTSPACE,
+ isActive = false,
+ packageName = PACKAGE_NAME,
+ cardAction = mediaSmartspaceBaseAction,
+ recommendations = validRecommendationList,
+ dismissIntent = DISMISS_INTENT,
+ headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+ instanceId = InstanceId.fakeInstanceId(instanceId),
+ expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
+ )
+ ),
+ eq(false)
+ )
+ }
+
+ @Test
+ fun testOnSmartspaceMediaDataLoaded_persistentEnabled_noTargets_inactive() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+
+ smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+ val instanceId = instanceIdSequence.lastInstanceId
+
+ smartspaceMediaDataProvider.onTargetsAvailable(listOf())
+ uiExecutor.advanceClockToLast()
+ uiExecutor.runAllReady()
+
+ verify(listener)
+ .onSmartspaceMediaDataLoaded(
+ eq(KEY_MEDIA_SMARTSPACE),
+ eq(
+ SmartspaceMediaData(
+ targetId = KEY_MEDIA_SMARTSPACE,
+ isActive = false,
+ packageName = PACKAGE_NAME,
+ cardAction = mediaSmartspaceBaseAction,
+ recommendations = validRecommendationList,
+ dismissIntent = DISMISS_INTENT,
+ headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+ instanceId = InstanceId.fakeInstanceId(instanceId),
+ expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
+ )
+ ),
+ eq(false)
+ )
+ verify(listener, never()).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false))
+ }
+
+ @Test
+ fun testSetRecommendationInactive_notifiesListeners() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+
+ smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+ val instanceId = instanceIdSequence.lastInstanceId
+
+ mediaDataManager.setRecommendationInactive(KEY_MEDIA_SMARTSPACE)
+ uiExecutor.advanceClockToLast()
+ uiExecutor.runAllReady()
+
+ verify(listener)
+ .onSmartspaceMediaDataLoaded(
+ eq(KEY_MEDIA_SMARTSPACE),
+ eq(
+ SmartspaceMediaData(
+ targetId = KEY_MEDIA_SMARTSPACE,
+ isActive = false,
+ packageName = PACKAGE_NAME,
+ cardAction = mediaSmartspaceBaseAction,
+ recommendations = validRecommendationList,
+ dismissIntent = DISMISS_INTENT,
+ headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
+ instanceId = InstanceId.fakeInstanceId(instanceId),
+ expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
+ )
+ ),
+ eq(false)
+ )
+ }
+
+ @Test
fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() {
// WHEN media recommendation setting is off
Settings.Secure.putInt(
@@ -1690,4 +1844,29 @@
stateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 1.0f)
whenever(controller.playbackState).thenReturn(stateBuilder.build())
}
+
+ /** Helper function to add a resumption control and capture the resulting MediaData */
+ private fun addResumeControlAndLoad(desc: MediaDescription) {
+ mediaDataManager.addResumptionControls(
+ USER_ID,
+ desc,
+ Runnable {},
+ session.sessionToken,
+ APP_NAME,
+ pendingIntent,
+ PACKAGE_NAME
+ )
+ assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+ assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+
+ verify(listener)
+ .onMediaDataLoaded(
+ eq(PACKAGE_NAME),
+ eq(null),
+ capture(mediaDataCaptor),
+ eq(true),
+ eq(0),
+ eq(false)
+ )
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt
index 92bf84c..8baa06a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt
@@ -25,13 +25,16 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.media.controls.MediaTestUtils
import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -48,7 +51,6 @@
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit
private const val KEY = "KEY"
@@ -56,6 +58,7 @@
private const val SESSION_KEY = "SESSION_KEY"
private const val SESSION_ARTIST = "SESSION_ARTIST"
private const val SESSION_TITLE = "SESSION_TITLE"
+private const val SMARTSPACE_KEY = "SMARTSPACE_KEY"
private fun <T> anyObject(): T {
return Mockito.anyObject<T>()
@@ -85,10 +88,13 @@
private lateinit var resumeData: MediaData
private lateinit var mediaTimeoutListener: MediaTimeoutListener
private var clock = FakeSystemClock()
+ @Mock private lateinit var mediaFlags: MediaFlags
+ @Mock private lateinit var smartspaceData: SmartspaceMediaData
@Before
fun setup() {
- `when`(mediaControllerFactory.create(any())).thenReturn(mediaController)
+ whenever(mediaControllerFactory.create(any())).thenReturn(mediaController)
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
executor = FakeExecutor(clock)
mediaTimeoutListener =
MediaTimeoutListener(
@@ -96,7 +102,8 @@
executor,
logger,
statusBarStateController,
- clock
+ clock,
+ mediaFlags,
)
mediaTimeoutListener.timeoutCallback = timeoutCallback
mediaTimeoutListener.stateCallback = stateCallback
@@ -133,9 +140,9 @@
@Test
fun testOnMediaDataLoaded_registersPlaybackListener() {
val playingState = mock(android.media.session.PlaybackState::class.java)
- `when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
+ whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
- `when`(mediaController.playbackState).thenReturn(playingState)
+ whenever(mediaController.playbackState).thenReturn(playingState)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
verify(logger).logPlaybackState(eq(KEY), eq(playingState))
@@ -188,8 +195,8 @@
// To playing
val playingState = mock(android.media.session.PlaybackState::class.java)
- `when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
- `when`(mediaController.playbackState).thenReturn(playingState)
+ whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
+ whenever(mediaController.playbackState).thenReturn(playingState)
mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
verify(mediaController).unregisterCallback(anyObject())
verify(mediaController).registerCallback(anyObject())
@@ -208,8 +215,8 @@
// Migrate, still not playing
val playingState = mock(android.media.session.PlaybackState::class.java)
- `when`(playingState.state).thenReturn(PlaybackState.STATE_PAUSED)
- `when`(mediaController.playbackState).thenReturn(playingState)
+ whenever(playingState.state).thenReturn(PlaybackState.STATE_PAUSED)
+ whenever(mediaController.playbackState).thenReturn(playingState)
mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
// The number of queued timeout tasks remains the same. The timeout task isn't cancelled nor
@@ -296,8 +303,8 @@
// WHEN we get an update with media playing
val playingState = mock(android.media.session.PlaybackState::class.java)
- `when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
- `when`(mediaController.playbackState).thenReturn(playingState)
+ whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
+ whenever(mediaController.playbackState).thenReturn(playingState)
val mediaPlaying = mediaData.copy(isPlaying = true)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPlaying)
@@ -347,7 +354,7 @@
// WHEN regular media is paused
val pausedState =
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
- `when`(mediaController.playbackState).thenReturn(pausedState)
+ whenever(mediaController.playbackState).thenReturn(pausedState)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
assertThat(executor.numPending()).isEqualTo(1)
@@ -379,7 +386,7 @@
// AND that media is resumed
val playingState =
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
- `when`(mediaController.playbackState).thenReturn(playingState)
+ whenever(mediaController.playbackState).thenReturn(playingState)
mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData)
// THEN the timeout length is changed to a regular media control
@@ -593,8 +600,91 @@
assertThat(executor.numPending()).isEqualTo(1)
}
+ @Test
+ fun testSmartspaceDataLoaded_schedulesTimeout() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+ val duration = 60_000
+ val createTime = 1234L
+ val expireTime = createTime + duration
+ whenever(smartspaceData.headphoneConnectionTimeMillis).thenReturn(createTime)
+ whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
+
+ mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+ assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(executor.advanceClockToNext()).isEqualTo(duration)
+ }
+
+ @Test
+ fun testSmartspaceMediaData_timesOut_invokesCallback() {
+ // Given a pending timeout
+ testSmartspaceDataLoaded_schedulesTimeout()
+
+ executor.runAllReady()
+ verify(timeoutCallback).invoke(eq(SMARTSPACE_KEY), eq(true))
+ }
+
+ @Test
+ fun testSmartspaceDataLoaded_alreadyExists_updatesTimeout() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+ val duration = 100
+ val createTime = 1234L
+ val expireTime = createTime + duration
+ whenever(smartspaceData.headphoneConnectionTimeMillis).thenReturn(createTime)
+ whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
+
+ mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+ assertThat(executor.numPending()).isEqualTo(1)
+
+ val expiryLonger = expireTime + duration
+ whenever(smartspaceData.expiryTimeMs).thenReturn(expiryLonger)
+ mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(executor.advanceClockToNext()).isEqualTo(duration * 2)
+ }
+
+ @Test
+ fun testSmartspaceDataRemoved_cancelTimeout() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+
+ mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+ assertThat(executor.numPending()).isEqualTo(1)
+
+ mediaTimeoutListener.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+ assertThat(executor.numPending()).isEqualTo(0)
+ }
+
+ @Test
+ fun testSmartspaceData_dozedPastTimeout_invokedOnWakeup() {
+ // Given a pending timeout
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+ verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
+ val duration = 60_000
+ val createTime = 1234L
+ val expireTime = createTime + duration
+ whenever(smartspaceData.headphoneConnectionTimeMillis).thenReturn(createTime)
+ whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
+
+ mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+ assertThat(executor.numPending()).isEqualTo(1)
+
+ // And we doze past the scheduled timeout
+ val time = clock.currentTimeMillis()
+ clock.setElapsedRealtime(time + duration * 2)
+ assertThat(executor.numPending()).isEqualTo(1)
+
+ // Then when no longer dozing, the timeout runs immediately
+ dozingCallbackCaptor.value.onDozingChanged(false)
+ verify(timeoutCallback).invoke(eq(SMARTSPACE_KEY), eq(true))
+ verify(logger).logTimeout(eq(SMARTSPACE_KEY))
+
+ // and cancel any later scheduled timeout
+ assertThat(executor.numPending()).isEqualTo(0)
+ }
+
private fun loadMediaDataWithPlaybackState(state: PlaybackState) {
- `when`(mediaController.playbackState).thenReturn(state)
+ whenever(mediaController.playbackState).thenReturn(state)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
index 5e5dc8b..e201b6b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
@@ -47,6 +47,7 @@
import com.android.systemui.util.time.FakeSystemClock
import javax.inject.Provider
import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.junit.Before
import org.junit.Ignore
@@ -126,6 +127,7 @@
whenever(mediaControlPanelFactory.get()).thenReturn(panel)
whenever(panel.mediaViewController).thenReturn(mediaViewController)
whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData)
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
MediaPlayerData.clear()
}
@@ -703,4 +705,39 @@
mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
)
}
+
+ @Test
+ fun testRecommendation_persistentEnabled_newSmartspaceLoaded_updatesSort() {
+ testRecommendation_persistentEnabled_inactiveSmartspaceDataLoaded_isAdded()
+
+ // When an update to existing smartspace data is loaded
+ listener.value.onSmartspaceMediaDataLoaded(
+ SMARTSPACE_KEY,
+ EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
+ true
+ )
+
+ // Then the carousel is updated
+ assertTrue(MediaPlayerData.playerKeys().elementAt(0).data.active)
+ assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(0).data.active)
+ }
+
+ @Test
+ fun testRecommendation_persistentEnabled_inactiveSmartspaceDataLoaded_isAdded() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+
+ // When inactive smartspace data is loaded
+ listener.value.onSmartspaceMediaDataLoaded(
+ SMARTSPACE_KEY,
+ EMPTY_SMARTSPACE_MEDIA_DATA,
+ false
+ )
+
+ // Then it is added to the carousel with correct state
+ assertTrue(MediaPlayerData.playerKeys().elementAt(0).isSsMediaRec)
+ assertFalse(MediaPlayerData.playerKeys().elementAt(0).data.active)
+
+ assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(0).isSsMediaRec)
+ assertFalse(MediaPlayerData.visiblePlayerKeys().elementAt(0).data.active)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
index ce22b19..55a33b6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
@@ -52,6 +52,7 @@
import androidx.constraintlayout.widget.Barrier
import androidx.constraintlayout.widget.ConstraintSet
import androidx.lifecycle.LiveData
+import androidx.media.utils.MediaConstants
import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
import com.android.internal.widget.CachingIconView
@@ -206,6 +207,12 @@
@Mock private lateinit var coverContainer3: ViewGroup
@Mock private lateinit var recAppIconItem: CachingIconView
@Mock private lateinit var recCardTitle: TextView
+ @Mock private lateinit var recProgressBar1: SeekBar
+ @Mock private lateinit var recProgressBar2: SeekBar
+ @Mock private lateinit var recProgressBar3: SeekBar
+ @Mock private lateinit var recSubtitleMock1: TextView
+ @Mock private lateinit var recSubtitleMock2: TextView
+ @Mock private lateinit var recSubtitleMock3: TextView
@Mock private lateinit var coverItem: ImageView
private lateinit var coverItem1: ImageView
private lateinit var coverItem2: ImageView
@@ -906,6 +913,17 @@
}
@Test
+ fun bind_resumeState_withProgress() {
+ val progress = 0.5
+ val state = mediaData.copy(resumption = true, resumeProgress = progress)
+
+ player.attachPlayer(viewHolder)
+ player.bindPlayer(state, PACKAGE)
+
+ verify(seekBarViewModel).updateStaticProgress(progress)
+ }
+
+ @Test
fun bindNotificationActions() {
val icon = context.getDrawable(android.R.drawable.ic_media_play)
val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
@@ -2070,6 +2088,10 @@
whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle)
whenever(recommendationViewHolder.mediaCoverItems)
.thenReturn(listOf(coverItem, coverItem, coverItem))
+ whenever(recommendationViewHolder.mediaProgressBars)
+ .thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3))
+ whenever(recommendationViewHolder.mediaSubtitles)
+ .thenReturn(listOf(recSubtitleMock1, recSubtitleMock2, recSubtitleMock3))
val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bmp)
@@ -2108,6 +2130,65 @@
}
@Test
+ fun bindRecommendationWithProgressBars() {
+ fakeFeatureFlag.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, true)
+ whenever(recommendationViewHolder.mediaAppIcons)
+ .thenReturn(listOf(recAppIconItem, recAppIconItem, recAppIconItem))
+ whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle)
+ whenever(recommendationViewHolder.mediaCoverItems)
+ .thenReturn(listOf(coverItem, coverItem, coverItem))
+ whenever(recommendationViewHolder.mediaProgressBars)
+ .thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3))
+ whenever(recommendationViewHolder.mediaSubtitles)
+ .thenReturn(listOf(recSubtitleMock1, recSubtitleMock2, recSubtitleMock3))
+
+ val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(bmp)
+ canvas.drawColor(Color.RED)
+ val albumArt = Icon.createWithBitmap(bmp)
+ val bundle =
+ Bundle().apply {
+ putInt(
+ MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+ MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
+ )
+ putDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.5)
+ }
+ val data =
+ smartspaceData.copy(
+ recommendations =
+ listOf(
+ SmartspaceAction.Builder("id1", "title1")
+ .setSubtitle("subtitle1")
+ .setIcon(albumArt)
+ .setExtras(bundle)
+ .build(),
+ SmartspaceAction.Builder("id2", "title2")
+ .setSubtitle("subtitle1")
+ .setIcon(albumArt)
+ .setExtras(Bundle.EMPTY)
+ .build(),
+ SmartspaceAction.Builder("id3", "title3")
+ .setSubtitle("subtitle1")
+ .setIcon(albumArt)
+ .setExtras(Bundle.EMPTY)
+ .build()
+ )
+ )
+
+ player.attachRecommendation(recommendationViewHolder)
+ player.bindRecommendation(data)
+
+ verify(recProgressBar1).setProgress(50)
+ verify(recProgressBar1).visibility = View.VISIBLE
+ verify(recProgressBar2).visibility = View.GONE
+ verify(recProgressBar3).visibility = View.GONE
+ verify(recSubtitleMock1).visibility = View.GONE
+ verify(recSubtitleMock2).visibility = View.VISIBLE
+ verify(recSubtitleMock3).visibility = View.VISIBLE
+ }
+
+ @Test
fun onButtonClick_touchRippleFlagEnabled_playsTouchRipple() {
fakeFeatureFlag.set(Flags.UMO_SURFACE_RIPPLE, true)
val semanticActions =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 5564774..f779845 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -16,7 +16,11 @@
package com.android.systemui.media.dialog;
-import static android.media.RouteListingPreference.Item.DISABLE_REASON_SUBSCRIPTION_REQUIRED;
+import static android.media.RouteListingPreference.Item.SELECTION_BEHAVIOR_GO_TO_APP;
+import static android.media.RouteListingPreference.Item.SELECTION_BEHAVIOR_NONE;
+import static android.media.RouteListingPreference.Item.SUBTEXT_AD_ROUTING_DISALLOWED;
+import static android.media.RouteListingPreference.Item.SUBTEXT_CUSTOM;
+import static android.media.RouteListingPreference.Item.SUBTEXT_SUBSCRIPTION_REQUIRED;
import static com.google.common.truth.Truth.assertThat;
@@ -62,6 +66,7 @@
private static final String TEST_DEVICE_ID_1 = "test_device_id_1";
private static final String TEST_DEVICE_ID_2 = "test_device_id_2";
private static final String TEST_SESSION_NAME = "test_session_name";
+ private static final String TEST_CUSTOM_SUBTEXT = "custom subtext";
private static final int TEST_MAX_VOLUME = 20;
private static final int TEST_CURRENT_VOLUME = 10;
@@ -431,12 +436,45 @@
}
@Test
- public void subStatusSupported_onBindViewHolder_bindFailedStateDevice_verifyView() {
+ public void subStatusSupported_onBindViewHolder_bindHostDeviceWithOngoingSession_verifyView() {
+ when(mMediaOutputController.isSubStatusSupported()).thenReturn(true);
+ when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true);
+ when(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(true);
+ when(mMediaDevice1.hasSubtext()).thenReturn(true);
+ when(mMediaDevice1.getSubtext()).thenReturn(SUBTEXT_CUSTOM);
+ when(mMediaDevice1.getSubtextString()).thenReturn(TEST_CUSTOM_SUBTEXT);
+ when(mMediaDevice1.hasOngoingSession()).thenReturn(true);
+ when(mMediaDevice1.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP);
+ mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ .onCreateViewHolder(new LinearLayout(mContext), 0);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mEndClickIcon.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mStatusIcon.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mSubTitleText.getText().toString()).isEqualTo(TEST_CUSTOM_SUBTEXT);
+ assertThat(mViewHolder.mTwoLineTitleText.getText().toString()).isEqualTo(
+ TEST_DEVICE_NAME_1);
+ assertThat(mViewHolder.mContainerLayout.hasOnClickListeners()).isFalse();
+ }
+
+ @Test
+ public void subStatusSupported_onBindViewHolder_bindDeviceRequirePremium_verifyView() {
String deviceStatus = (String) mContext.getText(
R.string.media_output_status_require_premium);
when(mMediaOutputController.isSubStatusSupported()).thenReturn(true);
- when(mMediaDevice2.hasDisabledReason()).thenReturn(true);
- when(mMediaDevice2.getDisableReason()).thenReturn(DISABLE_REASON_SUBSCRIPTION_REQUIRED);
+ when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true);
+ when(mMediaDevice2.hasSubtext()).thenReturn(true);
+ when(mMediaDevice2.getSubtext()).thenReturn(SUBTEXT_SUBSCRIPTION_REQUIRED);
+ when(mMediaDevice2.getSubtextString()).thenReturn(deviceStatus);
+ when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP);
+ mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ .onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
@@ -444,9 +482,64 @@
assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mStatusIcon.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mSubTitleText.getText()).isEqualTo(deviceStatus);
assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mContainerLayout.hasOnClickListeners()).isTrue();
+ }
+
+ @Test
+ public void subStatusSupported_onBindViewHolder_bindDeviceWithAdPlaying_verifyView() {
+ String deviceStatus = (String) mContext.getText(
+ R.string.media_output_status_try_after_ad);
+ when(mMediaOutputController.isSubStatusSupported()).thenReturn(true);
+ when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true);
+ when(mMediaDevice2.hasSubtext()).thenReturn(true);
+ when(mMediaDevice2.getSubtext()).thenReturn(SUBTEXT_AD_ROUTING_DISALLOWED);
+ when(mMediaDevice2.getSubtextString()).thenReturn(deviceStatus);
+ when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_NONE);
+ mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ .onCreateViewHolder(new LinearLayout(mContext), 0);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mStatusIcon.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mSubTitleText.getText().toString()).isEqualTo(deviceStatus);
+ assertThat(mViewHolder.mTwoLineTitleText.getText().toString()).isEqualTo(
+ TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mContainerLayout.hasOnClickListeners()).isFalse();
+ }
+
+ @Test
+ public void subStatusSupported_onBindViewHolder_bindDeviceWithOngoingSession_verifyView() {
+ when(mMediaOutputController.isSubStatusSupported()).thenReturn(true);
+ when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true);
+ when(mMediaDevice1.hasSubtext()).thenReturn(true);
+ when(mMediaDevice1.getSubtext()).thenReturn(SUBTEXT_CUSTOM);
+ when(mMediaDevice1.getSubtextString()).thenReturn(TEST_CUSTOM_SUBTEXT);
+ when(mMediaDevice1.hasOngoingSession()).thenReturn(true);
+ when(mMediaDevice1.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP);
+ mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ .onCreateViewHolder(new LinearLayout(mContext), 0);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mStatusIcon.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mSubTitleText.getText().toString()).isEqualTo(TEST_CUSTOM_SUBTEXT);
+ assertThat(mViewHolder.mTwoLineTitleText.getText().toString()).isEqualTo(
+ TEST_DEVICE_NAME_1);
+ assertThat(mViewHolder.mContainerLayout.hasOnClickListeners()).isTrue();
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
index 7c36e46..0bdcaf4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -240,7 +240,6 @@
verify(mMediaController, never()).unregisterCallback(any());
}
-
@Test
public void stop_nearbyMediaDevicesManagerNotNull_unregistersNearbyDevicesCallback() {
mMediaOutputController.start(mCb);
@@ -253,7 +252,7 @@
@Test
public void tryToLaunchMediaApplication_nullIntent_skip() {
- mMediaOutputController.tryToLaunchMediaApplication();
+ mMediaOutputController.tryToLaunchMediaApplication(mDialogLaunchView);
verify(mCb, never()).dismissDialog();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
index c63ca3d..db890f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
@@ -45,6 +45,7 @@
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
+import com.android.systemui.temporarydisplay.chipbar.ChipbarAnimator
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
import com.android.systemui.temporarydisplay.chipbar.ChipbarLogger
import com.android.systemui.temporarydisplay.chipbar.SwipeChipbarAwayGestureHandler
@@ -145,6 +146,7 @@
configurationController,
dumpManager,
powerManager,
+ ChipbarAnimator(),
falsingManager,
falsingCollector,
swipeHandler,
@@ -622,7 +624,7 @@
}
@Test
- fun commandQueueCallback_receiverSucceededThenReceiverTriggered_invalidTransitionLogged() {
+ fun commandQueueCallback_receiverSucceededThenThisDeviceSucceeded_invalidTransitionLogged() {
displayReceiverTriggered()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
@@ -632,7 +634,7 @@
reset(windowManager)
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
routeInfo,
null
)
@@ -642,7 +644,7 @@
}
@Test
- fun commandQueueCallback_thisDeviceSucceededThenThisDeviceTriggered_invalidTransitionLogged() {
+ fun commandQueueCallback_thisDeviceSucceededThenReceiverSucceeded_invalidTransitionLogged() {
displayThisDeviceTriggered()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
@@ -652,7 +654,7 @@
reset(windowManager)
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
- StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
routeInfo,
null
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeContentTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeContentTest.java
index 9de55b1..1ddfc4d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeContentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeContentTest.java
@@ -34,6 +34,22 @@
public final class SchemeContentTest extends SysuiTestCase {
@Test
+ public void testKeyColors() {
+ SchemeContent scheme = new SchemeContent(Hct.fromInt(0xff0000ff), false, 0.0);
+
+ assertThat(MaterialDynamicColors.primaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff080CFF);
+ assertThat(MaterialDynamicColors.secondaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff656DD3);
+ assertThat(MaterialDynamicColors.tertiaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff81009F);
+ assertThat(MaterialDynamicColors.neutralPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff767684);
+ assertThat(MaterialDynamicColors.neutralVariantPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff757589);
+ }
+
+ @Test
public void lightTheme_minContrast_primary() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFF1218FF);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeExpressiveTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeExpressiveTest.java
index 3ef9280..31e8711 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeExpressiveTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeExpressiveTest.java
@@ -34,158 +34,174 @@
public final class SchemeExpressiveTest extends SysuiTestCase {
@Test
+ public void testKeyColors() {
+ SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 0.0);
+
+ assertThat(MaterialDynamicColors.primaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff35855F);
+ assertThat(MaterialDynamicColors.secondaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff8C6D8C);
+ assertThat(MaterialDynamicColors.tertiaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff806EA1);
+ assertThat(MaterialDynamicColors.neutralPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff79757F);
+ assertThat(MaterialDynamicColors.neutralVariantPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff7A7585);
+ }
+
+ @Test
public void lightTheme_minContrast_primary() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, -1.0);
- assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffad603c);
+ assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff32835D);
}
@Test
public void lightTheme_standardContrast_primary() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 0.0);
- assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff924b28);
+ assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff146C48);
}
@Test
public void lightTheme_maxContrast_primary() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 1.0);
- assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff401400);
+ assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff002818);
}
@Test
public void lightTheme_minContrast_primaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
- 0xffffdbcc);
+ 0xffA2F4C6);
}
@Test
public void lightTheme_standardContrast_primaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
- 0xffffdbcc);
+ 0xffA2F4C6);
}
@Test
public void lightTheme_maxContrast_primaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
- 0xff6f3010);
+ 0xff004D31);
}
@Test
public void lightTheme_minContrast_onPrimaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
- 0xff99512e);
+ 0xff1e724e);
}
@Test
public void lightTheme_standardContrast_onPrimaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
- 0xff351000);
+ 0xff002112);
}
@Test
public void lightTheme_maxContrast_onPrimaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
- 0xffffd0bc);
+ 0xff9aebbe);
}
@Test
public void lightTheme_minContrast_surface() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, -1.0);
- assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+ assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffdf7ff);
}
@Test
public void lightTheme_standardContrast_surface() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 0.0);
- assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+ assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffdf7ff);
}
@Test
public void lightTheme_maxContrast_surface() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 1.0);
- assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+ assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffdf7ff);
}
@Test
public void darkTheme_minContrast_primary() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, -1.0);
- assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffad603c);
+ assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff32835d);
}
@Test
public void darkTheme_standardContrast_primary() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 0.0);
- assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffffb595);
+ assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff87d7ab);
}
@Test
public void darkTheme_maxContrast_primary() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 1.0);
- assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfffff3ee);
+ assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffd5ffe4);
}
@Test
public void darkTheme_minContrast_primaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
- 0xff743413);
+ 0xff005234);
}
@Test
public void darkTheme_standardContrast_primaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
- 0xff743413);
+ 0xff005234);
}
@Test
public void darkTheme_maxContrast_primaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
- 0xffffbb9e);
+ 0xff8bdbaf);
}
@Test
public void darkTheme_minContrast_onPrimaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
- 0xfff99f75);
+ 0xff76c59b);
}
@Test
public void darkTheme_standardContrast_onPrimaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
- 0xffffdbcc);
+ 0xffa2f4c6);
}
@Test
public void darkTheme_maxContrast_onPrimaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
- 0xff622706);
+ 0xff004229);
}
@Test
public void darkTheme_minContrast_surface() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, -1.0);
- assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a);
+ assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff14121a);
}
@Test
public void darkTheme_standardContrast_surface() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 0.0);
- assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a);
+ assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff14121a);
}
@Test
public void darkTheme_maxContrast_surface() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 1.0);
- assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a);
+ assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff14121a);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFidelityTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFidelityTest.java
index cfd0518..511f83b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFidelityTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFidelityTest.java
@@ -32,6 +32,21 @@
@SmallTest
@RunWith(JUnit4.class)
public final class SchemeFidelityTest extends SysuiTestCase {
+ @Test
+ public void testKeyColors() {
+ SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 0.0);
+
+ assertThat(MaterialDynamicColors.primaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff080CFF);
+ assertThat(MaterialDynamicColors.secondaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff656DD3);
+ assertThat(MaterialDynamicColors.tertiaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff9D0002);
+ assertThat(MaterialDynamicColors.neutralPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff767684);
+ assertThat(MaterialDynamicColors.neutralVariantPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff757589);
+ }
@Test
public void lightTheme_minContrast_primary() {
@@ -251,5 +266,4 @@
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12121d);
}
-
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFruitSaladTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFruitSaladTest.java
index ce6f6d9..4cf14d5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFruitSaladTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFruitSaladTest.java
@@ -34,6 +34,22 @@
public final class SchemeFruitSaladTest extends SysuiTestCase {
@Test
+ public void testKeyColors() {
+ SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 0.0);
+
+ assertThat(MaterialDynamicColors.primaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff0091C0);
+ assertThat(MaterialDynamicColors.secondaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff3A7E9E);
+ assertThat(MaterialDynamicColors.tertiaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff6E72AC);
+ assertThat(MaterialDynamicColors.neutralPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff777682);
+ assertThat(MaterialDynamicColors.neutralVariantPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff75758B);
+ }
+
+ @Test
public void lightTheme_minContrast_primary() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff007ea7);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeMonochromeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeMonochromeTest.java
index 9ffc523..3eca4dc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeMonochromeTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeMonochromeTest.java
@@ -34,6 +34,22 @@
public final class SchemeMonochromeTest extends SysuiTestCase {
@Test
+ public void testKeyColors() {
+ SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 0.0);
+
+ assertThat(MaterialDynamicColors.primaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff070707);
+ assertThat(MaterialDynamicColors.secondaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff070707);
+ assertThat(MaterialDynamicColors.tertiaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff070707);
+ assertThat(MaterialDynamicColors.neutralPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff070707);
+ assertThat(MaterialDynamicColors.neutralVariantPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff070707);
+ }
+
+ @Test
public void lightTheme_minContrast_primary() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff747474);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeNeutralTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeNeutralTest.java
index c6892f9..71e9f4d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeNeutralTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeNeutralTest.java
@@ -34,6 +34,22 @@
public final class SchemeNeutralTest extends SysuiTestCase {
@Test
+ public void testKeyColors() {
+ SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 0.0);
+
+ assertThat(MaterialDynamicColors.primaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff767685);
+ assertThat(MaterialDynamicColors.secondaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff777680);
+ assertThat(MaterialDynamicColors.tertiaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff75758B);
+ assertThat(MaterialDynamicColors.neutralPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff787678);
+ assertThat(MaterialDynamicColors.neutralVariantPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff787678);
+ }
+
+ @Test
public void lightTheme_minContrast_primary() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff737383);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeRainbowTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeRainbowTest.java
index a083622..cc6f044 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeRainbowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeRainbowTest.java
@@ -34,6 +34,22 @@
public final class SchemeRainbowTest extends SysuiTestCase {
@Test
+ public void testKeyColors() {
+ SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 0.0);
+
+ assertThat(MaterialDynamicColors.primaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff696FC4);
+ assertThat(MaterialDynamicColors.secondaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff75758B);
+ assertThat(MaterialDynamicColors.tertiaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff936B84);
+ assertThat(MaterialDynamicColors.neutralPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff070707);
+ assertThat(MaterialDynamicColors.neutralVariantPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff070707);
+ }
+
+ @Test
public void lightTheme_minContrast_primary() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff676DC1);
@@ -230,5 +246,4 @@
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313);
}
-
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeTonalSpotTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeTonalSpotTest.java
index b0c7f10..e4880d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeTonalSpotTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeTonalSpotTest.java
@@ -34,21 +34,37 @@
public final class SchemeTonalSpotTest extends SysuiTestCase {
@Test
+ public void testKeyColors() {
+ SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
+
+ assertThat(MaterialDynamicColors.primaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff6E72AC);
+ assertThat(MaterialDynamicColors.secondaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff75758B);
+ assertThat(MaterialDynamicColors.tertiaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff936B84);
+ assertThat(MaterialDynamicColors.neutralPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff78767A);
+ assertThat(MaterialDynamicColors.neutralVariantPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff777680);
+ }
+
+ @Test
public void lightTheme_minContrast_primary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
- assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff6a6fb1);
+ assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff6c70aa);
}
@Test
public void lightTheme_standardContrast_primary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
- assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff545999);
+ assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff555992);
}
@Test
public void lightTheme_maxContrast_primary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
- assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff161a59);
+ assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff181c51);
}
@Test
@@ -69,21 +85,21 @@
public void lightTheme_maxContrast_primaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
- 0xff383c7c);
+ 0xff3a3e74);
}
@Test
public void lightTheme_minContrast_onPrimaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
- 0xff5a5fa0);
+ 0xff5C5F98);
}
@Test
public void lightTheme_standardContrast_onPrimaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
- 0xff0e1253);
+ 0xff11144B);
}
@Test
@@ -96,37 +112,37 @@
@Test
public void lightTheme_minContrast_surface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
- assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+ assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xffFCF8FD);
}
@Test
public void lightTheme_standardContrast_surface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
- assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+ assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xffFCF8FD);
}
@Test
public void lightTheme_maxContrast_surface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
- assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+ assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xffFCF8FD);
}
@Test
public void lightTheme_minContrast_onSurface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
- assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xff5f5e65);
+ assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xff605E62);
}
@Test
public void lightTheme_standardContrast_onSurface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
- assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xff1b1b21);
+ assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xff1B1B1F);
}
@Test
public void lightTheme_maxContrast_onSurface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
- assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xff1a1a20);
+ assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xff1B1A1E);
}
@Test
@@ -186,7 +202,7 @@
@Test
public void darkTheme_minContrast_primary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
- assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff6a6fb1);
+ assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff6C70AA);
}
@Test
@@ -205,14 +221,14 @@
public void darkTheme_minContrast_primaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
- 0xff3c4180);
+ 0xff3E4278);
}
@Test
public void darkTheme_standardContrast_primaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
- 0xff3c4180);
+ 0xff3E4278);
}
@Test
@@ -226,7 +242,7 @@
public void darkTheme_minContrast_onPrimaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
- 0xffabaff7);
+ 0xffadb0ef);
}
@Test
@@ -240,7 +256,7 @@
public void darkTheme_maxContrast_onPrimaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
- 0xff2e3271);
+ 0xff30346A);
}
@Test
@@ -321,36 +337,36 @@
@Test
public void darkTheme_minContrast_surface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
- assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131318);
+ assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131316);
}
@Test
public void darkTheme_standardContrast_surface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
- assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131318);
+ assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131316);
}
@Test
public void darkTheme_maxContrast_surface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
- assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131318);
+ assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131316);
}
@Test
public void darkTheme_minContrast_onSurface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
- assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xffa4a2a9);
+ assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xffa4a2a6);
}
@Test
public void darkTheme_standardContrast_onSurface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
- assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xffe4e1e9);
+ assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xffe5e1e6);
}
@Test
public void darkTheme_maxContrast_onSurface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
- assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xffe5e2ea);
+ assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xffe6e2e7);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeVibrantTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeVibrantTest.java
index f056a16..e5963a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeVibrantTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeVibrantTest.java
@@ -34,6 +34,22 @@
public final class SchemeVibrantTest extends SysuiTestCase {
@Test
+ public void testKeyColors() {
+ SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 0.0);
+
+ assertThat(MaterialDynamicColors.primaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff080CFF);
+ assertThat(MaterialDynamicColors.secondaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff7B7296);
+ assertThat(MaterialDynamicColors.tertiaryPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff886C9D);
+ assertThat(MaterialDynamicColors.neutralPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff777682);
+ assertThat(MaterialDynamicColors.neutralVariantPaletteKeyColor.getArgb(scheme))
+ .isSameColorAs(0xff767685);
+ }
+
+ @Test
public void lightTheme_minContrast_primary() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5660ff);
@@ -195,18 +211,18 @@
@Test
public void darkTheme_minContrast_surface() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, -1.0);
- assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a);
+ assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131C);
}
@Test
public void darkTheme_standardContrast_surface() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 0.0);
- assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a);
+ assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131C);
}
@Test
public void darkTheme_maxContrast_surface() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 1.0);
- assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a);
+ assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131C);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
new file mode 100644
index 0000000..f42bfb8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.navigationbar.gestural
+
+import android.os.Handler
+import android.os.Looper
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_UP
+import android.view.ViewConfiguration
+import android.view.WindowManager
+import androidx.test.filters.SmallTest
+import com.android.internal.util.LatencyTracker
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.NavigationEdgeBackPlugin
+import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class BackPanelControllerTest : SysuiTestCase() {
+ companion object {
+ private const val START_X: Float = 0f
+ }
+ private lateinit var mBackPanelController: BackPanelController
+ private lateinit var testableLooper: TestableLooper
+ private var triggerThreshold: Float = 0.0f
+ private val touchSlop = ViewConfiguration.get(context).scaledEdgeSlop
+ @Mock private lateinit var vibratorHelper: VibratorHelper
+ @Mock private lateinit var windowManager: WindowManager
+ @Mock private lateinit var configurationController: ConfigurationController
+ @Mock private lateinit var latencyTracker: LatencyTracker
+ @Mock private lateinit var layoutParams: WindowManager.LayoutParams
+ @Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ mBackPanelController =
+ BackPanelController(
+ context,
+ windowManager,
+ ViewConfiguration.get(context),
+ Handler.createAsync(Looper.myLooper()),
+ vibratorHelper,
+ configurationController,
+ latencyTracker
+ )
+ mBackPanelController.setLayoutParams(layoutParams)
+ mBackPanelController.setBackCallback(backCallback)
+ mBackPanelController.setIsLeftPanel(true)
+ testableLooper = TestableLooper.get(this)
+ triggerThreshold = mBackPanelController.params.staticTriggerThreshold
+ }
+
+ @Test
+ fun handlesActionDown() {
+ startTouch()
+
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.GONE)
+ }
+
+ @Test
+ fun staysHiddenBeforeSlopCrossed() {
+ startTouch()
+ // Move just enough to not cross the touch slop
+ continueTouch(START_X + touchSlop - 1)
+
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.GONE)
+ }
+
+ @Test
+ fun handlesDragSlopCrossed() {
+ startTouch()
+ continueTouch(START_X + touchSlop + 1)
+
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.ENTRY)
+ }
+
+ @Test
+ fun handlesBackCommitted() {
+ startTouch()
+ // Move once to cross the touch slop
+ continueTouch(START_X + touchSlop.toFloat() + 1)
+ // Move again to cross the back trigger threshold
+ continueTouch(START_X + touchSlop + triggerThreshold + 1)
+
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.ACTIVE)
+ verify(backCallback).setTriggerBack(true)
+ testableLooper.moveTimeForward(100)
+ testableLooper.processAllMessages()
+ verify(vibratorHelper).vibrate(VIBRATE_ACTIVATED_EFFECT)
+
+ finishTouchActionUp(START_X + touchSlop + triggerThreshold + 1)
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.FLUNG)
+ verify(backCallback).triggerBack()
+ }
+
+ @Test
+ fun handlesBackCancelled() {
+ startTouch()
+ continueTouch(START_X + touchSlop.toFloat() + 1)
+ continueTouch(
+ START_X + touchSlop + triggerThreshold -
+ mBackPanelController.params.deactivationSwipeTriggerThreshold
+ )
+ clearInvocations(backCallback)
+ Thread.sleep(MIN_DURATION_ACTIVE_ANIMATION)
+ // Move in the opposite direction to cross the deactivation threshold and cancel back
+ continueTouch(START_X)
+
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.INACTIVE)
+ verify(backCallback).setTriggerBack(false)
+ verify(vibratorHelper).vibrate(VIBRATE_DEACTIVATED_EFFECT)
+
+ finishTouchActionUp(START_X)
+ verify(backCallback).cancelBack()
+ }
+
+ private fun startTouch() {
+ mBackPanelController.onMotionEvent(createMotionEvent(ACTION_DOWN, START_X, 0f))
+ }
+
+ private fun continueTouch(x: Float) {
+ mBackPanelController.onMotionEvent(createMotionEvent(ACTION_MOVE, x, 0f))
+ }
+
+ private fun finishTouchActionUp(x: Float) {
+ mBackPanelController.onMotionEvent(createMotionEvent(ACTION_UP, x, 0f))
+ }
+
+ private fun createMotionEvent(action: Int, x: Float, y: Float): MotionEvent {
+ return MotionEvent.obtain(0L, 0L, action, x, y, 0)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/MotionEventsHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/MotionEventsHandlerTest.java
index 509d5f0..70ba306 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/MotionEventsHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/MotionEventsHandlerTest.java
@@ -16,10 +16,10 @@
package com.android.systemui.navigationbar.gestural;
-import static android.view.InputDevice.SOURCE_TOUCHPAD;
-import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
+import static android.view.InputDevice.SOURCE_MOUSE;
import static android.view.MotionEvent.AXIS_GESTURE_X_OFFSET;
import static android.view.MotionEvent.AXIS_GESTURE_Y_OFFSET;
+import static android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE;
import static com.google.common.truth.Truth.assertThat;
@@ -57,14 +57,9 @@
@Test
public void onTouchEvent_touchScreen_hasCorrectDisplacements() {
MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 100, 100, 0);
- // TODO: change to use classification after gesture library is ported.
- down.setSource(SOURCE_TOUCHSCREEN);
MotionEvent move1 = MotionEvent.obtain(0, 1, MotionEvent.ACTION_MOVE, 150, 125, 0);
- move1.setSource(SOURCE_TOUCHSCREEN);
MotionEvent move2 = MotionEvent.obtain(0, 2, MotionEvent.ACTION_MOVE, 200, 150, 0);
- move2.setSource(SOURCE_TOUCHSCREEN);
MotionEvent up = MotionEvent.obtain(0, 3, MotionEvent.ACTION_UP, 250, 175, 0);
- up.setSource(SOURCE_TOUCHSCREEN);
mMotionEventsHandler.onMotionEvent(down);
mMotionEventsHandler.onMotionEvent(move1);
@@ -90,8 +85,8 @@
downPointerProperties[0].id = 1;
downPointerProperties[0].toolType = MotionEvent.TOOL_TYPE_FINGER;
MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1,
- downPointerProperties, downPointerCoords, 0, 0, 1.0f, 1.0f, 0, 0,
- SOURCE_TOUCHPAD, 0);
+ downPointerProperties, downPointerCoords, 0, 0, 1.0f, 1.0f, 0, 0, SOURCE_MOUSE,
+ 0, 0, CLASSIFICATION_MULTI_FINGER_SWIPE);
MotionEvent.PointerCoords[] movePointerCoords1 = new MotionEvent.PointerCoords[1];
movePointerCoords1[0] = new MotionEvent.PointerCoords();
@@ -103,8 +98,8 @@
movePointerProperties1[0].id = 1;
movePointerProperties1[0].toolType = MotionEvent.TOOL_TYPE_FINGER;
MotionEvent move1 = MotionEvent.obtain(0, 1, MotionEvent.ACTION_MOVE, 1,
- movePointerProperties1, movePointerCoords1, 0, 0, 1.0f, 1.0f, 0, 0, SOURCE_TOUCHPAD,
- 0);
+ movePointerProperties1, movePointerCoords1, 0, 0, 1.0f, 1.0f, 0, 0, SOURCE_MOUSE,
+ 0, 0, CLASSIFICATION_MULTI_FINGER_SWIPE);
MotionEvent.PointerCoords[] movePointerCoords2 = new MotionEvent.PointerCoords[1];
movePointerCoords2[0] = new MotionEvent.PointerCoords();
@@ -116,8 +111,8 @@
movePointerProperties2[0].id = 1;
movePointerProperties2[0].toolType = MotionEvent.TOOL_TYPE_FINGER;
MotionEvent move2 = MotionEvent.obtain(0, 2, MotionEvent.ACTION_MOVE, 1,
- movePointerProperties2, movePointerCoords2, 0, 0, 1.0f, 1.0f, 0, 0, SOURCE_TOUCHPAD,
- 0);
+ movePointerProperties2, movePointerCoords2, 0, 0, 1.0f, 1.0f, 0, 0, SOURCE_MOUSE,
+ 0, 0, CLASSIFICATION_MULTI_FINGER_SWIPE);
MotionEvent.PointerCoords[] upPointerCoords = new MotionEvent.PointerCoords[1];
upPointerCoords[0] = new MotionEvent.PointerCoords();
@@ -129,7 +124,8 @@
upPointerProperties2[0].id = 1;
upPointerProperties2[0].toolType = MotionEvent.TOOL_TYPE_FINGER;
MotionEvent up = MotionEvent.obtain(0, 2, MotionEvent.ACTION_UP, 1,
- upPointerProperties2, upPointerCoords, 0, 0, 1.0f, 1.0f, 0, 0, SOURCE_TOUCHPAD, 0);
+ upPointerProperties2, upPointerCoords, 0, 0, 1.0f, 1.0f, 0, 0, SOURCE_MOUSE,
+ 0, 0, CLASSIFICATION_MULTI_FINGER_SWIPE);
mMotionEventsHandler.onMotionEvent(down);
mMotionEventsHandler.onMotionEvent(move1);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index 8440455..39c4e06 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -23,10 +23,14 @@
import android.os.UserManager
import android.test.suitebuilder.annotation.SmallTest
import androidx.test.runner.AndroidJUnit4
+import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
-import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE
+import com.android.systemui.notetask.NoteTaskController.Companion.INTENT_EXTRA_USE_STYLUS_MODE
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
+import com.android.systemui.notetask.NoteTaskInfoResolver.NoteTaskInfo
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.android.wm.shell.bubbles.Bubbles
@@ -36,8 +40,8 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
-import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations
/**
@@ -50,24 +54,23 @@
@RunWith(AndroidJUnit4::class)
internal class NoteTaskControllerTest : SysuiTestCase() {
- private val notesIntent = Intent(ACTION_CREATE_NOTE)
-
@Mock lateinit var context: Context
@Mock lateinit var packageManager: PackageManager
- @Mock lateinit var noteTaskIntentResolver: NoteTaskIntentResolver
+ @Mock lateinit var resolver: NoteTaskInfoResolver
@Mock lateinit var bubbles: Bubbles
@Mock lateinit var optionalBubbles: Optional<Bubbles>
@Mock lateinit var keyguardManager: KeyguardManager
@Mock lateinit var optionalKeyguardManager: Optional<KeyguardManager>
@Mock lateinit var optionalUserManager: Optional<UserManager>
@Mock lateinit var userManager: UserManager
+ @Mock lateinit var uiEventLogger: UiEventLogger
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(context.packageManager).thenReturn(packageManager)
- whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(notesIntent)
+ whenever(resolver.resolveInfo()).thenReturn(NoteTaskInfo(NOTES_PACKAGE_NAME, NOTES_UID))
whenever(optionalBubbles.orElse(null)).thenReturn(bubbles)
whenever(optionalKeyguardManager.orElse(null)).thenReturn(keyguardManager)
whenever(optionalUserManager.orElse(null)).thenReturn(userManager)
@@ -77,101 +80,182 @@
private fun createNoteTaskController(isEnabled: Boolean = true): NoteTaskController {
return NoteTaskController(
context = context,
- intentResolver = noteTaskIntentResolver,
+ resolver = resolver,
optionalBubbles = optionalBubbles,
optionalKeyguardManager = optionalKeyguardManager,
optionalUserManager = optionalUserManager,
isEnabled = isEnabled,
+ uiEventLogger = uiEventLogger,
)
}
// region showNoteTask
@Test
- fun showNoteTask_keyguardIsLocked_shouldStartActivity() {
+ fun showNoteTask_keyguardIsLocked_shouldStartActivityAndLogUiEvent() {
whenever(keyguardManager.isKeyguardLocked).thenReturn(true)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE,
+ )
- verify(context).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ val intentCaptor = argumentCaptor<Intent>()
+ verify(context).startActivity(capture(intentCaptor))
+ intentCaptor.value.let { intent ->
+ assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
+ assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
+ assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+ assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
+ }
+ verifyZeroInteractions(bubbles)
+ verify(uiEventLogger)
+ .log(
+ ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE,
+ NOTES_UID,
+ NOTES_PACKAGE_NAME
+ )
}
@Test
- fun showNoteTask_keyguardIsUnlocked_shouldStartBubbles() {
+ fun showNoteTask_keyguardIsUnlocked_shouldStartBubblesAndLogUiEvent() {
whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+ )
- verify(bubbles).showOrHideAppBubble(notesIntent)
- verify(context, never()).startActivity(notesIntent)
+ verifyZeroInteractions(context)
+ val intentCaptor = argumentCaptor<Intent>()
+ verify(bubbles).showOrHideAppBubble(capture(intentCaptor))
+ intentCaptor.value.let { intent ->
+ assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
+ assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
+ assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+ assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
+ }
+ verify(uiEventLogger)
+ .log(
+ ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+ NOTES_UID,
+ NOTES_PACKAGE_NAME
+ )
}
@Test
- fun showNoteTask_isInMultiWindowMode_shouldStartActivity() {
+ fun showNoteTask_keyguardIsUnlocked_uiEventIsNull_shouldStartBubblesWithoutLoggingUiEvent() {
whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = true)
+ createNoteTaskController().showNoteTask(isInMultiWindowMode = false, uiEvent = null)
- verify(context).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context)
+ val intentCaptor = argumentCaptor<Intent>()
+ verify(bubbles).showOrHideAppBubble(capture(intentCaptor))
+ intentCaptor.value.let { intent ->
+ assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
+ assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
+ assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+ assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
+ }
+ verifyZeroInteractions(uiEventLogger)
+ }
+
+ @Test
+ fun showNoteTask_isInMultiWindowMode_shouldStartActivityAndLogUiEvent() {
+ whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
+
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = true,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT,
+ )
+
+ val intentCaptor = argumentCaptor<Intent>()
+ verify(context).startActivity(capture(intentCaptor))
+ intentCaptor.value.let { intent ->
+ assertThat(intent.action).isEqualTo(NoteTaskController.ACTION_CREATE_NOTE)
+ assertThat(intent.`package`).isEqualTo(NOTES_PACKAGE_NAME)
+ assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+ assertThat(intent.getBooleanExtra(INTENT_EXTRA_USE_STYLUS_MODE, false)).isTrue()
+ }
+ verifyZeroInteractions(bubbles)
+ verify(uiEventLogger)
+ .log(ShowNoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT, NOTES_UID, NOTES_PACKAGE_NAME)
}
@Test
fun showNoteTask_bubblesIsNull_shouldDoNothing() {
whenever(optionalBubbles.orElse(null)).thenReturn(null)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON
+ )
- verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context, bubbles, uiEventLogger)
}
@Test
fun showNoteTask_keyguardManagerIsNull_shouldDoNothing() {
whenever(optionalKeyguardManager.orElse(null)).thenReturn(null)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+ )
- verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context, bubbles, uiEventLogger)
}
@Test
fun showNoteTask_userManagerIsNull_shouldDoNothing() {
whenever(optionalUserManager.orElse(null)).thenReturn(null)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+ )
- verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context, bubbles, uiEventLogger)
}
@Test
fun showNoteTask_intentResolverReturnsNull_shouldDoNothing() {
- whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(null)
+ whenever(resolver.resolveInfo()).thenReturn(null)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+ )
- verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context, bubbles, uiEventLogger)
}
@Test
fun showNoteTask_flagDisabled_shouldDoNothing() {
- createNoteTaskController(isEnabled = false).showNoteTask()
+ createNoteTaskController(isEnabled = false)
+ .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON)
- verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context, bubbles, uiEventLogger)
}
@Test
fun showNoteTask_userIsLocked_shouldDoNothing() {
whenever(userManager.isUserUnlocked).thenReturn(false)
- createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
+ createNoteTaskController()
+ .showNoteTask(
+ isInMultiWindowMode = false,
+ uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON,
+ )
- verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showOrHideAppBubble(notesIntent)
+ verifyZeroInteractions(context, bubbles, uiEventLogger)
}
// endregion
@@ -206,4 +290,9 @@
assertThat(argument.value.flattenToString()).isEqualTo(expected.flattenToString())
}
// endregion
+
+ private companion object {
+ const val NOTES_PACKAGE_NAME = "com.android.note.app"
+ const val NOTES_UID = 123456
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt
new file mode 100644
index 0000000..d6495d8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notetask
+
+import android.app.role.RoleManager
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.test.suitebuilder.annotation.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.MockitoAnnotations
+
+/**
+ * Tests for [NoteTaskInfoResolver].
+ *
+ * Build/Install/Run:
+ * - atest SystemUITests:NoteTaskInfoResolverTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class NoteTaskInfoResolverTest : SysuiTestCase() {
+
+ @Mock lateinit var packageManager: PackageManager
+ @Mock lateinit var roleManager: RoleManager
+
+ private lateinit var underTest: NoteTaskInfoResolver
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ underTest = NoteTaskInfoResolver(context, roleManager, packageManager)
+ }
+
+ @Test
+ fun resolveInfo_shouldReturnInfo() {
+ val packageName = "com.android.note.app"
+ val uid = 123456
+ whenever(roleManager.getRoleHoldersAsUser(NoteTaskInfoResolver.ROLE_NOTES, context.user))
+ .then { listOf(packageName) }
+ whenever(
+ packageManager.getApplicationInfoAsUser(
+ eq(packageName),
+ any<PackageManager.ApplicationInfoFlags>(),
+ eq(context.user)
+ )
+ )
+ .thenReturn(ApplicationInfo().apply { this.uid = uid })
+
+ val actual = underTest.resolveInfo()
+
+ requireNotNull(actual) { "Note task info must not be null" }
+ assertThat(actual.packageName).isEqualTo(packageName)
+ assertThat(actual.uid).isEqualTo(uid)
+ }
+
+ @Test
+ fun resolveInfo_packageManagerThrowsException_shouldReturnInfoWithZeroUid() {
+ val packageName = "com.android.note.app"
+ whenever(roleManager.getRoleHoldersAsUser(NoteTaskInfoResolver.ROLE_NOTES, context.user))
+ .then { listOf(packageName) }
+ whenever(
+ packageManager.getApplicationInfoAsUser(
+ eq(packageName),
+ any<PackageManager.ApplicationInfoFlags>(),
+ eq(context.user)
+ )
+ )
+ .thenThrow(PackageManager.NameNotFoundException(packageName))
+
+ val actual = underTest.resolveInfo()
+
+ requireNotNull(actual) { "Note task info must not be null" }
+ assertThat(actual.packageName).isEqualTo(packageName)
+ assertThat(actual.uid).isEqualTo(0)
+ }
+
+ @Test
+ fun resolveInfo_noRoleHolderIsSet_shouldReturnNull() {
+ whenever(roleManager.getRoleHoldersAsUser(eq(NoteTaskInfoResolver.ROLE_NOTES), any()))
+ .then { listOf<String>() }
+
+ val actual = underTest.resolveInfo()
+
+ assertThat(actual).isNull()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
index 010ac5b..53720ff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -15,11 +15,14 @@
*/
package com.android.systemui.notetask
+import android.app.KeyguardManager
import android.test.suitebuilder.annotation.SmallTest
import android.view.KeyEvent
import androidx.test.runner.AndroidJUnit4
import com.android.systemui.SysuiTestCase
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.wm.shell.bubbles.Bubbles
import java.util.Optional
@@ -30,6 +33,7 @@
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations
/**
@@ -55,12 +59,16 @@
whenever(optionalBubbles.orElse(null)).thenReturn(bubbles)
}
- private fun createNoteTaskInitializer(isEnabled: Boolean = true): NoteTaskInitializer {
+ private fun createNoteTaskInitializer(
+ isEnabled: Boolean = true,
+ optionalKeyguardManager: Optional<KeyguardManager> = Optional.empty(),
+ ): NoteTaskInitializer {
return NoteTaskInitializer(
optionalBubbles = optionalBubbles,
noteTaskController = noteTaskController,
commandQueue = commandQueue,
isEnabled = isEnabled,
+ optionalKeyguardManager = optionalKeyguardManager,
)
}
@@ -105,19 +113,44 @@
// region handleSystemKey
@Test
- fun handleSystemKey_receiveValidSystemKey_shouldShowNoteTask() {
- createNoteTaskInitializer()
+ fun handleSystemKey_receiveValidSystemKey_keyguardNotLocked_shouldShowNoteTaskWithUnlocked() {
+ val keyguardManager =
+ mock<KeyguardManager>() { whenever(isKeyguardLocked).thenReturn(false) }
+ createNoteTaskInitializer(optionalKeyguardManager = Optional.of(keyguardManager))
.callbacks
.handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT)
- verify(noteTaskController).showNoteTask()
+ verify(noteTaskController)
+ .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON)
+ }
+
+ @Test
+ fun handleSystemKey_receiveValidSystemKey_keyguardLocked_shouldShowNoteTaskWithLocked() {
+ val keyguardManager =
+ mock<KeyguardManager>() { whenever(isKeyguardLocked).thenReturn(true) }
+ createNoteTaskInitializer(optionalKeyguardManager = Optional.of(keyguardManager))
+ .callbacks
+ .handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT)
+
+ verify(noteTaskController)
+ .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED)
+ }
+
+ @Test
+ fun handleSystemKey_receiveValidSystemKey_nullKeyguardManager_shouldShowNoteTaskWithUnlocked() {
+ createNoteTaskInitializer(optionalKeyguardManager = Optional.empty())
+ .callbacks
+ .handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT)
+
+ verify(noteTaskController)
+ .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON)
}
@Test
fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() {
createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_UNKNOWN)
- verify(noteTaskController, never()).showNoteTask()
+ verifyZeroInteractions(noteTaskController)
}
// endregion
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
deleted file mode 100644
index 18be92b..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * 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.systemui.notetask
-
-import android.app.role.RoleManager
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.test.suitebuilder.annotation.SmallTest
-import androidx.test.runner.AndroidJUnit4
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.any
-import org.mockito.MockitoAnnotations
-
-/**
- * Tests for [NoteTaskIntentResolver].
- *
- * Build/Install/Run:
- * - atest SystemUITests:NoteTaskIntentResolverTest
- */
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-internal class NoteTaskIntentResolverTest : SysuiTestCase() {
-
- @Mock lateinit var packageManager: PackageManager
- @Mock lateinit var roleManager: RoleManager
-
- private lateinit var underTest: NoteTaskIntentResolver
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- underTest = NoteTaskIntentResolver(context, roleManager)
- }
-
- @Test
- fun resolveIntent_shouldReturnIntentInStylusMode() {
- val packageName = "com.android.note.app"
- whenever(roleManager.getRoleHoldersAsUser(NoteTaskIntentResolver.ROLE_NOTES, context.user))
- .then { listOf(packageName) }
-
- val actual = underTest.resolveIntent()
-
- requireNotNull(actual) { "Intent must not be null" }
- assertThat(actual.action).isEqualTo(ACTION_CREATE_NOTE)
- assertThat(actual.`package`).isEqualTo(packageName)
- val expectedExtra = actual.getExtra(NoteTaskIntentResolver.INTENT_EXTRA_USE_STYLUS_MODE)
- assertThat(expectedExtra).isEqualTo(true)
- val expectedFlag = actual.flags and Intent.FLAG_ACTIVITY_NEW_TASK
- assertThat(expectedFlag).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
- }
-
- @Test
- fun resolveIntent_noRoleHolderIsSet_shouldReturnNull() {
- whenever(roleManager.getRoleHoldersAsUser(eq(NoteTaskIntentResolver.ROLE_NOTES), any()))
- .then { listOf<String>() }
-
- val actual = underTest.resolveIntent()
-
- assertThat(actual).isNull()
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt
index a1d42a0..cdc683f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt
@@ -27,7 +27,7 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.LockScreenState
import com.android.systemui.notetask.NoteTaskController
-import com.android.systemui.util.mockito.whenever
+import com.android.systemui.notetask.NoteTaskController.ShowNoteTaskUiEvent
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
@@ -53,7 +53,6 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- whenever(noteTaskController.showNoteTask()).then {}
}
private fun createUnderTest(isEnabled: Boolean) =
@@ -96,6 +95,7 @@
underTest.onTriggered(expandable = null)
- verify(noteTaskController).showNoteTask()
+ verify(noteTaskController)
+ .showNoteTask(uiEvent = ShowNoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
index 3da7a22..8019fb5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
@@ -42,7 +42,6 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.UserHandle;
-import android.provider.Settings;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -158,16 +157,6 @@
}
@Test
- public void testDisableLowBatteryReminder_noNotification() {
- mGlobalSettings.putInt(Settings.Global.LOW_POWER_MODE_REMINDER_ENABLED, 0);
-
- mPowerNotificationWarnings.showLowBatteryWarning(false);
-
- verify(mMockNotificationManager, times(0))
- .notifyAsUser(anyString(), eq(SystemMessage.NOTE_POWER_LOW), any(), any());
- }
-
- @Test
public void testShowLowBatteryNotification_NotifyAsUser() {
mPowerNotificationWarnings.showLowBatteryWarning(false);
verify(mMockNotificationManager, times(1))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java
index a13bece..15545a4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java
@@ -29,9 +29,9 @@
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Intent;
+import android.content.pm.UserInfo;
import android.os.Handler;
import android.os.RemoteException;
-import android.os.UserHandle;
import android.provider.Settings;
import android.service.dreams.IDreamManager;
import android.service.quicksettings.Tile;
@@ -123,7 +123,7 @@
// Should not be available if component is not set
mSecureSettings.putInt(Settings.Secure.SCREENSAVER_ENABLED, 1);
- when(mDreamManager.getDreamComponents()).thenReturn(null);
+ when(mDreamManager.getDreamComponentsForUser(mUserTracker.getUserId())).thenReturn(null);
mTestableLooper.processAllMessages();
assertEquals(Tile.STATE_UNAVAILABLE, mTile.getState().state);
@@ -134,9 +134,8 @@
public void testInactiveWhenDreaming() throws RemoteException {
setScreensaverEnabled(true);
- when(mDreamManager.getDreamComponents()).thenReturn(new ComponentName[]{
- COLORS_DREAM_COMPONENT_NAME
- });
+ when(mDreamManager.getDreamComponentsForUser(mUserTracker.getUserId()))
+ .thenReturn(new ComponentName[]{COLORS_DREAM_COMPONENT_NAME});
when(mDreamManager.isDreaming()).thenReturn(false);
mTile.refreshState();
@@ -148,9 +147,8 @@
public void testActive() throws RemoteException {
setScreensaverEnabled(true);
- when(mDreamManager.getDreamComponents()).thenReturn(new ComponentName[]{
- COLORS_DREAM_COMPONENT_NAME
- });
+ when(mDreamManager.getDreamComponentsForUser(mUserTracker.getUserId()))
+ .thenReturn(new ComponentName[]{COLORS_DREAM_COMPONENT_NAME});
when(mDreamManager.isDreaming()).thenReturn(true);
mTile.refreshState();
@@ -162,9 +160,8 @@
public void testClick() throws RemoteException {
// Set the AOSP dream enabled as the base setup.
setScreensaverEnabled(true);
- when(mDreamManager.getDreamComponents()).thenReturn(new ComponentName[]{
- COLORS_DREAM_COMPONENT_NAME
- });
+ when(mDreamManager.getDreamComponentsForUser(mUserTracker.getUserId()))
+ .thenReturn(new ComponentName[]{COLORS_DREAM_COMPONENT_NAME});
when(mDreamManager.isDreaming()).thenReturn(false);
mTile.refreshState();
@@ -203,21 +200,21 @@
DreamTile supportedTileAllUsers = constructTileForTest(true, false);
- UserHandle systemUserHandle = mock(UserHandle.class);
- when(systemUserHandle.isSystem()).thenReturn(true);
+ UserInfo mainUserInfo = mock(UserInfo.class);
+ when(mainUserInfo.isMain()).thenReturn(true);
- UserHandle nonSystemUserHandle = mock(UserHandle.class);
- when(nonSystemUserHandle.isSystem()).thenReturn(false);
+ UserInfo nonMainUserInfo = mock(UserInfo.class);
+ when(nonMainUserInfo.isMain()).thenReturn(false);
- when(mUserTracker.getUserHandle()).thenReturn(systemUserHandle);
+ when(mUserTracker.getUserInfo()).thenReturn(mainUserInfo);
assertTrue(supportedTileAllUsers.isAvailable());
- when(mUserTracker.getUserHandle()).thenReturn(nonSystemUserHandle);
+ when(mUserTracker.getUserInfo()).thenReturn(nonMainUserInfo);
assertTrue(supportedTileAllUsers.isAvailable());
DreamTile supportedTileOnlySystemUser = constructTileForTest(true, true);
- when(mUserTracker.getUserHandle()).thenReturn(systemUserHandle);
+ when(mUserTracker.getUserInfo()).thenReturn(mainUserInfo);
assertTrue(supportedTileOnlySystemUser.isAvailable());
- when(mUserTracker.getUserHandle()).thenReturn(nonSystemUserHandle);
+ when(mUserTracker.getUserInfo()).thenReturn(nonMainUserInfo);
assertFalse(supportedTileOnlySystemUser.isAvailable());
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
index 80c39cf..addca9d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
@@ -37,10 +37,12 @@
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.QSTileHost;
import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
import com.android.systemui.statusbar.connectivity.AccessPointController;
import com.android.systemui.statusbar.connectivity.IconState;
import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.statusbar.connectivity.WifiIndicators;
import org.junit.Before;
import org.junit.Test;
@@ -135,4 +137,24 @@
assertThat(mTile.getState().secondaryLabel)
.isNotEqualTo(mContext.getString(R.string.status_bar_airplane));
}
+
+ @Test
+ public void setIsAirplaneMode_APM_enabled_after_wifi_disconnected() {
+ WifiIndicators wifiIndicators = new WifiIndicators(
+ /* enabled= */ true,
+ /* statusIcon= */ null,
+ /* qsIcon= */ null,
+ /* activityIn= */ false,
+ /* activityOut= */ false,
+ /* description= */ null,
+ /* isTransient= */ false,
+ /* statusLabel= */ null
+ );
+ mTile.mSignalCallback.setWifiIndicators(wifiIndicators);
+ IconState state = new IconState(true, 0, "");
+ mTile.mSignalCallback.setIsAirplaneMode(state);
+ mTestableLooper.processAllMessages();
+ assertThat(mTile.getState().icon).isEqualTo(
+ QSTileImpl.ResourceIcon.get(R.drawable.ic_qs_no_internet_unavailable));
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
index 8127ccc..6e6833d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
@@ -16,13 +16,15 @@
package com.android.systemui.screenrecord;
+import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
-
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import android.app.Dialog;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Looper;
@@ -31,7 +33,13 @@
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog;
+import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.util.concurrency.FakeExecutor;
@@ -61,8 +69,15 @@
@Mock
private UserContextProvider mUserContextProvider;
@Mock
+ private ScreenCaptureDevicePolicyResolver mDevicePolicyResolver;
+ @Mock
+ private DialogLaunchAnimator mDialogLaunchAnimator;
+ @Mock
+ private ActivityStarter mActivityStarter;
+ @Mock
private UserTracker mUserTracker;
+ private FakeFeatureFlags mFeatureFlags;
private RecordingController mController;
private static final int USER_ID = 10;
@@ -70,8 +85,9 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mController = new RecordingController(mMainExecutor, mBroadcastDispatcher,
- mUserContextProvider, mUserTracker);
+ mFeatureFlags = new FakeFeatureFlags();
+ mController = new RecordingController(mMainExecutor, mBroadcastDispatcher, mContext,
+ mFeatureFlags, mUserContextProvider, () -> mDevicePolicyResolver, mUserTracker);
mController.addCallback(mCallback);
}
@@ -190,4 +206,67 @@
verify(mCallback).onRecordingEnd();
assertFalse(mController.isRecording());
}
+
+ @Test
+ public void testPoliciesFlagDisabled_screenCapturingNotAllowed_returnsNullDevicePolicyDialog() {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+
+ mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, true);
+ mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, false);
+ when(mDevicePolicyResolver.isScreenCaptureCompletelyDisabled((any()))).thenReturn(true);
+
+ Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags,
+ mDialogLaunchAnimator, mActivityStarter, /* onStartRecordingClicked= */ null);
+
+ assertThat(dialog).isInstanceOf(ScreenRecordPermissionDialog.class);
+ }
+
+ @Test
+ public void testPartialScreenSharingDisabled_returnsLegacyDialog() {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+
+ mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, false);
+ mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, false);
+
+ Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags,
+ mDialogLaunchAnimator, mActivityStarter, /* onStartRecordingClicked= */ null);
+
+ assertThat(dialog).isInstanceOf(ScreenRecordDialog.class);
+ }
+
+ @Test
+ public void testPoliciesFlagEnabled_screenCapturingNotAllowed_returnsDevicePolicyDialog() {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+
+ mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, true);
+ mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, true);
+ when(mDevicePolicyResolver.isScreenCaptureCompletelyDisabled((any()))).thenReturn(true);
+
+ Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags,
+ mDialogLaunchAnimator, mActivityStarter, /* onStartRecordingClicked= */ null);
+
+ assertThat(dialog).isInstanceOf(ScreenCaptureDisabledDialog.class);
+ }
+
+ @Test
+ public void testPoliciesFlagEnabled_screenCapturingAllowed_returnsNullDevicePolicyDialog() {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+
+ mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, true);
+ mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, true);
+ when(mDevicePolicyResolver.isScreenCaptureCompletelyDisabled((any()))).thenReturn(false);
+
+ Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags,
+ mDialogLaunchAnimator, mActivityStarter, /* onStartRecordingClicked= */ null);
+
+ assertThat(dialog).isInstanceOf(ScreenRecordPermissionDialog.class);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
index 0aa3621..5b094c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.screenrecord
+import android.os.UserHandle
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.View
@@ -59,6 +60,7 @@
dialog =
ScreenRecordPermissionDialog(
context,
+ UserHandle.of(0),
controller,
starter,
dialogLaunchAnimator,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
index 541d6c2..2e73c0b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
@@ -23,7 +23,7 @@
import android.graphics.Rect
import android.hardware.HardwareBuffer
import android.os.UserHandle
-import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
+import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER
import android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER
import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
@@ -35,6 +35,7 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
+import org.junit.Assert
import org.junit.Test
private const val USER_ID = 1
@@ -55,7 +56,7 @@
flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
val request =
- ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD).build()
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
val processor = RequestProcessor(imageCapture, policy, flags, scope)
var result: ScreenshotRequest? = null
@@ -78,8 +79,10 @@
fun testProcessAsync_ScreenshotData() {
flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
- val request = ScreenshotData.fromRequest(
- ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD).build())
+ val request =
+ ScreenshotData.fromRequest(
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
+ )
val processor = RequestProcessor(imageCapture, policy, flags, scope)
var result: ScreenshotData? = null
@@ -102,7 +105,7 @@
flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
val request =
- ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD).build()
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
val processor = RequestProcessor(imageCapture, policy, flags, scope)
val processedRequest = processor.process(request)
@@ -162,7 +165,7 @@
)
val request =
- ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD).build()
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
val processor = RequestProcessor(imageCapture, policy, flags, scope)
val processedRequest = processor.process(request)
@@ -191,6 +194,32 @@
}
@Test
+ fun testFullScreenshot_managedProfile_nullBitmap() {
+ flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+
+ // Provide a null task bitmap when asked
+ imageCapture.image = null
+
+ // Indicate that the primary content belongs to a manged profile
+ policy.setManagedProfile(USER_ID, true)
+ policy.setDisplayContentInfo(
+ policy.getDefaultDisplayId(),
+ DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID)
+ )
+
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
+ val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+ Assert.assertThrows(IllegalStateException::class.java) {
+ runBlocking { processor.process(request) }
+ }
+ Assert.assertThrows(IllegalStateException::class.java) {
+ runBlocking { processor.process(ScreenshotData.fromRequest(request)) }
+ }
+ }
+
+ @Test
fun testProvidedImageScreenshot_workProfilePolicyDisabled() = runBlocking {
flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt
new file mode 100644
index 0000000..1f18d91
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot
+
+import android.content.ComponentName
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.testing.AndroidTestingRunner
+import android.view.Display
+import android.view.IWindowManager
+import android.view.WindowManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ScreenshotDetectionControllerTest {
+
+ @Mock lateinit var windowManager: IWindowManager
+
+ @Mock lateinit var packageManager: PackageManager
+
+ lateinit var controller: ScreenshotDetectionController
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ controller = ScreenshotDetectionController(windowManager, packageManager)
+ }
+
+ @Test
+ fun testMaybeNotifyOfScreenshot_ignoresOverview() {
+ val data = ScreenshotData.forTesting()
+ data.source = WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW
+
+ val list = controller.maybeNotifyOfScreenshot(data)
+
+ assertTrue(list.isEmpty())
+ verify(windowManager, never()).notifyScreenshotListeners(any())
+ }
+
+ @Test
+ fun testMaybeNotifyOfScreenshot_emptySet() {
+ val data = ScreenshotData.forTesting()
+ data.source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
+
+ whenever(windowManager.notifyScreenshotListeners(eq(Display.DEFAULT_DISPLAY)))
+ .thenReturn(listOf())
+
+ val list = controller.maybeNotifyOfScreenshot(data)
+
+ assertTrue(list.isEmpty())
+ }
+
+ @Test
+ fun testMaybeNotifyOfScreenshot_oneApp() {
+ val data = ScreenshotData.forTesting()
+ data.source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
+
+ val component = ComponentName("package1", "class1")
+ val appName = "app name"
+ val activityInfo = mock(ActivityInfo::class.java)
+
+ whenever(
+ packageManager.getActivityInfo(
+ eq(component),
+ any(PackageManager.ComponentInfoFlags::class.java)
+ )
+ )
+ .thenReturn(activityInfo)
+ whenever(activityInfo.loadLabel(eq(packageManager))).thenReturn(appName)
+
+ whenever(windowManager.notifyScreenshotListeners(eq(Display.DEFAULT_DISPLAY)))
+ .thenReturn(listOf(component))
+
+ val list = controller.maybeNotifyOfScreenshot(data)
+
+ assertEquals(1, list.size)
+ assertEquals(appName, list[0])
+ }
+
+ @Test
+ fun testMaybeNotifyOfScreenshot_multipleApps() {
+ val data = ScreenshotData.forTesting()
+ data.source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
+
+ val component1 = ComponentName("package1", "class1")
+ val component2 = ComponentName("package2", "class2")
+ val component3 = ComponentName("package3", "class3")
+ val appName1 = "app name 1"
+ val appName2 = "app name 2"
+ val appName3 = "app name 3"
+
+ val activityInfo1 = mock(ActivityInfo::class.java)
+ val activityInfo2 = mock(ActivityInfo::class.java)
+ val activityInfo3 = mock(ActivityInfo::class.java)
+
+ whenever(
+ packageManager.getActivityInfo(
+ eq(component1),
+ any(PackageManager.ComponentInfoFlags::class.java)
+ )
+ )
+ .thenReturn(activityInfo1)
+ whenever(
+ packageManager.getActivityInfo(
+ eq(component2),
+ any(PackageManager.ComponentInfoFlags::class.java)
+ )
+ )
+ .thenReturn(activityInfo2)
+ whenever(
+ packageManager.getActivityInfo(
+ eq(component3),
+ any(PackageManager.ComponentInfoFlags::class.java)
+ )
+ )
+ .thenReturn(activityInfo3)
+
+ whenever(activityInfo1.loadLabel(eq(packageManager))).thenReturn(appName1)
+ whenever(activityInfo2.loadLabel(eq(packageManager))).thenReturn(appName2)
+ whenever(activityInfo3.loadLabel(eq(packageManager))).thenReturn(appName3)
+
+ whenever(windowManager.notifyScreenshotListeners(eq(Display.DEFAULT_DISPLAY)))
+ .thenReturn(listOf(component1, component2, component3))
+
+ val list = controller.maybeNotifyOfScreenshot(data)
+
+ assertEquals(3, list.size)
+ assertEquals(appName1, list[0])
+ assertEquals(appName2, list[1])
+ assertEquals(appName3, list[2])
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
index 74969d0..1fa2ace 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
@@ -38,8 +38,9 @@
import com.android.internal.util.ScreenshotRequest
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY
import com.android.systemui.flags.Flags.SCREENSHOT_METADATA
+import com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED
import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_OTHER
import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_REQUESTED_OVERVIEW
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback
@@ -47,6 +48,7 @@
import com.android.systemui.util.mockito.argThat
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
import java.util.function.Consumer
import org.junit.Assert.assertEquals
import org.junit.Before
@@ -55,10 +57,10 @@
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.isNull
import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doThrow
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyZeroInteractions
-import org.mockito.Mockito.`when` as whenever
private const val USER_ID = 1
private const val TASK_ID = 11
@@ -107,18 +109,20 @@
// Stub request processor as a synchronous no-op for tests with the flag enabled
doAnswer {
- val request: ScreenshotRequest = it.getArgument(0) as ScreenshotRequest
- val consumer: Consumer<ScreenshotRequest> = it.getArgument(1)
- consumer.accept(request)
- }.`when`(requestProcessor).processAsync(
- /* request= */ any(ScreenshotRequest::class.java), /* callback= */ any())
+ val request: ScreenshotRequest = it.getArgument(0) as ScreenshotRequest
+ val consumer: Consumer<ScreenshotRequest> = it.getArgument(1)
+ consumer.accept(request)
+ }
+ .whenever(requestProcessor)
+ .processAsync(/* request= */ any(ScreenshotRequest::class.java), /* callback= */ any())
doAnswer {
- val request: ScreenshotData = it.getArgument(0) as ScreenshotData
- val consumer: Consumer<ScreenshotData> = it.getArgument(1)
- consumer.accept(request)
- }.`when`(requestProcessor).processAsync(
- /* screenshot= */ any(ScreenshotData::class.java), /* callback= */ any())
+ val request: ScreenshotData = it.getArgument(0) as ScreenshotData
+ val consumer: Consumer<ScreenshotData> = it.getArgument(1)
+ consumer.accept(request)
+ }
+ .whenever(requestProcessor)
+ .processAsync(/* screenshot= */ any(ScreenshotData::class.java), /* callback= */ any())
// Flipped in selected test cases
flags.set(SCREENSHOT_WORK_PROFILE_POLICY, false)
@@ -162,37 +166,52 @@
/* requestCallback = */ any()
)
- assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1)
+ assertEquals("Expected one UiEvent", 1, eventLogger.numLogs())
val logEvent = eventLogger.get(0)
- assertEquals("Expected SCREENSHOT_REQUESTED UiEvent",
- logEvent.eventId, SCREENSHOT_REQUESTED_KEY_OTHER.id)
- assertEquals("Expected supplied package name",
- topComponent.packageName, eventLogger.get(0).packageName)
+ assertEquals(
+ "Expected SCREENSHOT_REQUESTED UiEvent",
+ logEvent.eventId,
+ SCREENSHOT_REQUESTED_KEY_OTHER.id
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ eventLogger.get(0).packageName
+ )
}
@Test
fun takeScreenshotFullscreen_screenshotDataEnabled() {
flags.set(SCREENSHOT_METADATA, true)
- val request = ScreenshotRequest.Builder(
- TAKE_SCREENSHOT_FULLSCREEN,
- SCREENSHOT_KEY_OTHER).setTopComponent(topComponent).build()
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+ .setTopComponent(topComponent)
+ .build()
- service.handleRequest(request, { /* onSaved */ }, callback)
+ service.handleRequest(request, { /* onSaved */}, callback)
- verify(controller, times(1)).handleScreenshot(
- eq(ScreenshotData.fromRequest(request)),
- /* onSavedListener = */ any(),
- /* requestCallback = */ any())
+ verify(controller, times(1))
+ .handleScreenshot(
+ eq(ScreenshotData.fromRequest(request)),
+ /* onSavedListener = */ any(),
+ /* requestCallback = */ any()
+ )
assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1)
val logEvent = eventLogger.get(0)
- assertEquals("Expected SCREENSHOT_REQUESTED UiEvent",
- logEvent.eventId, SCREENSHOT_REQUESTED_KEY_OTHER.id)
- assertEquals("Expected supplied package name",
- topComponent.packageName, eventLogger.get(0).packageName)
+ assertEquals(
+ "Expected SCREENSHOT_REQUESTED UiEvent",
+ logEvent.eventId,
+ SCREENSHOT_REQUESTED_KEY_OTHER.id
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ eventLogger.get(0).packageName
+ )
}
@Test
@@ -224,7 +243,7 @@
/* requestCallback = */ any()
)
- assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1)
+ assertEquals("Expected one UiEvent", 1, eventLogger.numLogs())
val logEvent = eventLogger.get(0)
assertEquals(
@@ -241,6 +260,8 @@
@Test
fun takeScreenshotFullscreen_userLocked() {
+ flags.set(SCREENSHOT_METADATA, true)
+
whenever(userManager.isUserUnlocked).thenReturn(false)
val request =
@@ -253,10 +274,36 @@
verify(notificationsController, times(1)).notifyScreenshotError(anyInt())
verify(callback, times(1)).reportError()
verifyZeroInteractions(controller)
+
+ assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+ val requestEvent = eventLogger.get(0)
+ assertEquals(
+ "Expected SCREENSHOT_REQUESTED_* UiEvent",
+ SCREENSHOT_REQUESTED_KEY_OTHER.id,
+ requestEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ requestEvent.packageName
+ )
+ val failureEvent = eventLogger.get(1)
+ assertEquals(
+ "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
+ SCREENSHOT_CAPTURE_FAILED.id,
+ failureEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ failureEvent.packageName
+ )
}
@Test
fun takeScreenshotFullscreen_screenCaptureDisabled_allUsers() {
+ flags.set(SCREENSHOT_METADATA, true)
+
whenever(devicePolicyManager.getScreenCaptureDisabled(isNull(), eq(UserHandle.USER_ALL)))
.thenReturn(true)
@@ -279,6 +326,206 @@
// error shown: Toast.makeText(...).show(), untestable
verify(callback, times(1)).reportError()
verifyZeroInteractions(controller)
+ assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+ val requestEvent = eventLogger.get(0)
+ assertEquals(
+ "Expected SCREENSHOT_REQUESTED_* UiEvent",
+ SCREENSHOT_REQUESTED_KEY_OTHER.id,
+ requestEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ requestEvent.packageName
+ )
+ val failureEvent = eventLogger.get(1)
+ assertEquals(
+ "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
+ SCREENSHOT_CAPTURE_FAILED.id,
+ failureEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ failureEvent.packageName
+ )
+ }
+
+ @Test
+ fun takeScreenshotFullscreen_userLocked_metadataDisabled() {
+ flags.set(SCREENSHOT_METADATA, false)
+ whenever(userManager.isUserUnlocked).thenReturn(false)
+
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+ .setTopComponent(topComponent)
+ .build()
+
+ service.handleRequest(request, { /* onSaved */}, callback)
+
+ verify(notificationsController, times(1)).notifyScreenshotError(anyInt())
+ verify(callback, times(1)).reportError()
+ verifyZeroInteractions(controller)
+
+ assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+ val requestEvent = eventLogger.get(0)
+ assertEquals(
+ "Expected SCREENSHOT_REQUESTED_* UiEvent",
+ SCREENSHOT_REQUESTED_KEY_OTHER.id,
+ requestEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ requestEvent.packageName
+ )
+ val failureEvent = eventLogger.get(1)
+ assertEquals(
+ "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
+ SCREENSHOT_CAPTURE_FAILED.id,
+ failureEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ failureEvent.packageName
+ )
+ }
+
+ @Test
+ fun takeScreenshotFullscreen_screenCaptureDisabled_allUsers_metadataDisabled() {
+ flags.set(SCREENSHOT_METADATA, false)
+
+ whenever(devicePolicyManager.getScreenCaptureDisabled(isNull(), eq(UserHandle.USER_ALL)))
+ .thenReturn(true)
+
+ whenever(
+ devicePolicyResourcesManager.getString(
+ eq(SCREENSHOT_BLOCKED_BY_ADMIN),
+ /* Supplier<String> */
+ any(),
+ )
+ )
+ .thenReturn("SCREENSHOT_BLOCKED_BY_ADMIN")
+
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+ .setTopComponent(topComponent)
+ .build()
+
+ service.handleRequest(request, { /* onSaved */}, callback)
+
+ // error shown: Toast.makeText(...).show(), untestable
+ verify(callback, times(1)).reportError()
+ verifyZeroInteractions(controller)
+ assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+ val requestEvent = eventLogger.get(0)
+ assertEquals(
+ "Expected SCREENSHOT_REQUESTED_* UiEvent",
+ SCREENSHOT_REQUESTED_KEY_OTHER.id,
+ requestEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ requestEvent.packageName
+ )
+ val failureEvent = eventLogger.get(1)
+ assertEquals(
+ "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
+ SCREENSHOT_CAPTURE_FAILED.id,
+ failureEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ failureEvent.packageName
+ )
+ }
+
+ @Test
+ fun takeScreenshot_workProfile_nullBitmap_metadataDisabled() {
+ flags.set(SCREENSHOT_METADATA, false)
+
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+ .setTopComponent(topComponent)
+ .build()
+
+ doThrow(IllegalStateException::class.java)
+ .whenever(requestProcessor)
+ .processAsync(any(ScreenshotRequest::class.java), any())
+
+ service.handleRequest(request, { /* onSaved */}, callback)
+
+ verify(callback, times(1)).reportError()
+ verify(notificationsController, times(1)).notifyScreenshotError(anyInt())
+ verifyZeroInteractions(controller)
+ assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+ val requestEvent = eventLogger.get(0)
+ assertEquals(
+ "Expected SCREENSHOT_REQUESTED_* UiEvent",
+ SCREENSHOT_REQUESTED_KEY_OTHER.id,
+ requestEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ requestEvent.packageName
+ )
+ val failureEvent = eventLogger.get(1)
+ assertEquals(
+ "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
+ SCREENSHOT_CAPTURE_FAILED.id,
+ failureEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ failureEvent.packageName
+ )
+ }
+ @Test
+ fun takeScreenshot_workProfile_nullBitmap() {
+ flags.set(SCREENSHOT_METADATA, true)
+
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+ .setTopComponent(topComponent)
+ .build()
+
+ doThrow(IllegalStateException::class.java)
+ .whenever(requestProcessor)
+ .processAsync(any(ScreenshotData::class.java), any())
+
+ service.handleRequest(request, { /* onSaved */}, callback)
+
+ verify(callback, times(1)).reportError()
+ verify(notificationsController, times(1)).notifyScreenshotError(anyInt())
+ verifyZeroInteractions(controller)
+ assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+ val requestEvent = eventLogger.get(0)
+ assertEquals(
+ "Expected SCREENSHOT_REQUESTED_* UiEvent",
+ SCREENSHOT_REQUESTED_KEY_OTHER.id,
+ requestEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ requestEvent.packageName
+ )
+ val failureEvent = eventLogger.get(1)
+ assertEquals(
+ "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
+ SCREENSHOT_CAPTURE_FAILED.id,
+ failureEvent.eventId
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ failureEvent.packageName
+ )
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java
index 576652f..3440f91 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java
@@ -59,9 +59,7 @@
@RunWith(AndroidTestingRunner.class)
public class WorkProfileMessageControllerTest extends SysuiTestCase {
private static final String DEFAULT_LABEL = "default label";
- private static final String BADGED_DEFAULT_LABEL = "badged default label";
private static final String APP_LABEL = "app label";
- private static final String BADGED_APP_LABEL = "badged app label";
private static final UserHandle NON_WORK_USER = UserHandle.of(0);
private static final UserHandle WORK_USER = UserHandle.of(10);
@@ -91,10 +89,6 @@
eq(WorkProfileMessageController.SHARED_PREFERENCES_NAME),
eq(Context.MODE_PRIVATE))).thenReturn(mSharedPreferences);
when(mMockContext.getString(ArgumentMatchers.anyInt())).thenReturn(DEFAULT_LABEL);
- when(mPackageManager.getUserBadgedLabel(eq(DEFAULT_LABEL), any()))
- .thenReturn(BADGED_DEFAULT_LABEL);
- when(mPackageManager.getUserBadgedLabel(eq(APP_LABEL), any()))
- .thenReturn(BADGED_APP_LABEL);
when(mPackageManager.getActivityIcon(any(ComponentName.class)))
.thenReturn(mActivityIcon);
when(mPackageManager.getUserBadgedIcon(
@@ -133,7 +127,7 @@
WorkProfileMessageController.WorkProfileFirstRunData data =
mMessageController.onScreenshotTaken(WORK_USER);
- assertEquals(BADGED_DEFAULT_LABEL, data.getAppName());
+ assertEquals(DEFAULT_LABEL, data.getAppName());
assertNull(data.getIcon());
}
@@ -142,7 +136,7 @@
WorkProfileMessageController.WorkProfileFirstRunData data =
mMessageController.onScreenshotTaken(WORK_USER);
- assertEquals(BADGED_APP_LABEL, data.getAppName());
+ assertEquals(APP_LABEL, data.getAppName());
assertEquals(mBadgedActivityIcon, data.getIcon());
}
@@ -151,7 +145,7 @@
ViewGroup layout = (ViewGroup) LayoutInflater.from(mContext).inflate(
R.layout.screenshot_work_profile_first_run, null);
WorkProfileMessageController.WorkProfileFirstRunData data =
- new WorkProfileMessageController.WorkProfileFirstRunData(BADGED_APP_LABEL,
+ new WorkProfileMessageController.WorkProfileFirstRunData(APP_LABEL,
mBadgedActivityIcon);
final CountDownLatch countdown = new CountDownLatch(1);
mMessageController.populateView(layout, data, () -> {
@@ -163,7 +157,7 @@
assertEquals(mBadgedActivityIcon, image.getDrawable());
TextView text = layout.findViewById(R.id.screenshot_message_content);
// The app name is used in a template, but at least validate that it was inserted.
- assertTrue(text.getText().toString().contains(BADGED_APP_LABEL));
+ assertTrue(text.getText().toString().contains(APP_LABEL));
// Validate that clicking the dismiss button calls back properly.
assertEquals(1, countdown.getCount());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperServiceTest.java
new file mode 100644
index 0000000..6e8f5fe
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperServiceTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.appclips;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Intent;
+import android.graphics.ColorSpace;
+import android.hardware.HardwareBuffer;
+import android.os.RemoteException;
+import android.view.Display;
+import android.window.ScreenCapture.ScreenshotHardwareBuffer;
+import android.window.ScreenCapture.ScreenshotSync;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.wm.shell.bubbles.Bubbles;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Optional;
+
+@RunWith(AndroidJUnit4.class)
+public final class AppClipsScreenshotHelperServiceTest extends SysuiTestCase {
+
+ private static final Intent FAKE_INTENT = new Intent();
+ private static final int DEFAULT_DISPLAY = Display.DEFAULT_DISPLAY;
+ private static final HardwareBuffer FAKE_HARDWARE_BUFFER =
+ HardwareBuffer.create(1, 1, HardwareBuffer.RGBA_8888, 1,
+ HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
+ private static final ColorSpace FAKE_COLOR_SPACE = ColorSpace.get(ColorSpace.Named.SRGB);
+ private static final ScreenshotHardwareBufferInternal EXPECTED_SCREENSHOT_BUFFER =
+ new ScreenshotHardwareBufferInternal(
+ new ScreenshotHardwareBuffer(FAKE_HARDWARE_BUFFER, FAKE_COLOR_SPACE, false,
+ false));
+
+ @Mock private Optional<Bubbles> mBubblesOptional;
+ @Mock private Bubbles mBubbles;
+ @Mock private ScreenshotHardwareBuffer mScreenshotHardwareBuffer;
+ @Mock private ScreenshotSync mScreenshotSync;
+
+ private AppClipsScreenshotHelperService mAppClipsScreenshotHelperService;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mAppClipsScreenshotHelperService = new AppClipsScreenshotHelperService(mBubblesOptional);
+ }
+
+ @Test
+ public void emptyBubbles_shouldReturnNull() throws RemoteException {
+ when(mBubblesOptional.isEmpty()).thenReturn(true);
+
+ assertThat(getInterface().takeScreenshot(DEFAULT_DISPLAY)).isNull();
+ }
+
+ @Test
+ public void bubblesPresent_screenshotFailed_ShouldReturnNull() throws RemoteException {
+ when(mBubblesOptional.isEmpty()).thenReturn(false);
+ when(mBubblesOptional.get()).thenReturn(mBubbles);
+ when(mBubbles.getScreenshotExcludingBubble(DEFAULT_DISPLAY)).thenReturn(mScreenshotSync);
+ when(mScreenshotSync.get()).thenReturn(null);
+
+ assertThat(getInterface().takeScreenshot(DEFAULT_DISPLAY)).isNull();
+ }
+
+ @Test
+ public void bubblesPresent_screenshotSuccess_shouldReturnScreenshot() throws RemoteException {
+ when(mBubblesOptional.isEmpty()).thenReturn(false);
+ when(mBubblesOptional.get()).thenReturn(mBubbles);
+ when(mBubbles.getScreenshotExcludingBubble(DEFAULT_DISPLAY)).thenReturn(mScreenshotSync);
+ when(mScreenshotSync.get()).thenReturn(mScreenshotHardwareBuffer);
+ when(mScreenshotHardwareBuffer.getHardwareBuffer()).thenReturn(FAKE_HARDWARE_BUFFER);
+ when(mScreenshotHardwareBuffer.getColorSpace()).thenReturn(FAKE_COLOR_SPACE);
+
+ assertThat(getInterface().takeScreenshot(DEFAULT_DISPLAY)).isEqualTo(
+ EXPECTED_SCREENSHOT_BUFFER);
+ }
+
+ private IAppClipsScreenshotHelperService getInterface() {
+ return IAppClipsScreenshotHelperService.Stub.asInterface(
+ mAppClipsScreenshotHelperService.onBind(FAKE_INTENT));
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java
new file mode 100644
index 0000000..b55fe36
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.appclips;
+
+import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.statusbar.IAppClipsService;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.wm.shell.bubbles.Bubbles;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Optional;
+
+@RunWith(AndroidJUnit4.class)
+public final class AppClipsServiceTest extends SysuiTestCase {
+
+ private static final Intent FAKE_INTENT = new Intent();
+ private static final int FAKE_TASK_ID = 42;
+ private static final String EMPTY = "";
+
+ @Mock @Application private Context mMockContext;
+ @Mock private FeatureFlags mFeatureFlags;
+ @Mock private Optional<Bubbles> mOptionalBubbles;
+ @Mock private Bubbles mBubbles;
+ @Mock private DevicePolicyManager mDevicePolicyManager;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void flagOff_shouldReturnFalse() throws RemoteException {
+ when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(false);
+
+ assertThat(getInterfaceWithRealContext()
+ .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse();
+ }
+
+ @Test
+ public void emptyBubbles_shouldReturnFalse() throws RemoteException {
+ when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+ when(mOptionalBubbles.isEmpty()).thenReturn(true);
+
+ assertThat(getInterfaceWithRealContext()
+ .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse();
+ }
+
+ @Test
+ public void taskIdNotAppBubble_shouldReturnFalse() throws RemoteException {
+ when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+ when(mOptionalBubbles.isEmpty()).thenReturn(false);
+ when(mOptionalBubbles.get()).thenReturn(mBubbles);
+ when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(false);
+
+ assertThat(getInterfaceWithRealContext()
+ .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse();
+ }
+
+ @Test
+ public void dpmScreenshotBlocked_shouldReturnFalse() throws RemoteException {
+ when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+ when(mOptionalBubbles.isEmpty()).thenReturn(false);
+ when(mOptionalBubbles.get()).thenReturn(mBubbles);
+ when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
+ when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(true);
+
+ assertThat(getInterfaceWithRealContext()
+ .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse();
+ }
+
+ @Test
+ public void configComponentNameNotValid_shouldReturnFalse() throws RemoteException {
+ when(mMockContext.getString(anyInt())).thenReturn(EMPTY);
+ when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+ when(mOptionalBubbles.isEmpty()).thenReturn(false);
+ when(mOptionalBubbles.get()).thenReturn(mBubbles);
+ when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
+ when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false);
+
+ assertThat(getInterfaceWithMockContext()
+ .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse();
+ }
+
+ @Test
+ public void allPrerequisitesSatisfy_shouldReturnTrue() throws RemoteException {
+ when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+ when(mOptionalBubbles.isEmpty()).thenReturn(false);
+ when(mOptionalBubbles.get()).thenReturn(mBubbles);
+ when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
+ when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false);
+
+ assertThat(getInterfaceWithRealContext()
+ .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isTrue();
+ }
+
+ private IAppClipsService getInterfaceWithRealContext() {
+ AppClipsService appClipsService = new AppClipsService(getContext(), mFeatureFlags,
+ mOptionalBubbles, mDevicePolicyManager);
+ return getInterfaceFromService(appClipsService);
+ }
+
+ private IAppClipsService getInterfaceWithMockContext() {
+ AppClipsService appClipsService = new AppClipsService(mMockContext, mFeatureFlags,
+ mOptionalBubbles, mDevicePolicyManager);
+ return getInterfaceFromService(appClipsService);
+ }
+
+ private static IAppClipsService getInterfaceFromService(AppClipsService appClipsService) {
+ IBinder iBinder = appClipsService.onBind(FAKE_INTENT);
+ return IAppClipsService.Stub.asInterface(iBinder);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java
new file mode 100644
index 0000000..295d127
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.appclips;
+
+import static android.app.Instrumentation.ActivityResult;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED;
+import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE;
+
+import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS;
+import static com.android.systemui.screenshot.AppClipsTrampolineActivity.ACTION_FINISH_FROM_TRAMPOLINE;
+import static com.android.systemui.screenshot.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI;
+import static com.android.systemui.screenshot.AppClipsTrampolineActivity.PERMISSION_SELF;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.intercepting.SingleActivityFactory;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.notetask.NoteTaskController;
+import com.android.systemui.screenshot.AppClipsTrampolineActivity;
+import com.android.wm.shell.bubbles.Bubbles;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Optional;
+
+@RunWith(AndroidTestingRunner.class)
+public final class AppClipsTrampolineActivityTest extends SysuiTestCase {
+
+ private static final String TEST_URI_STRING = "www.test-uri.com";
+ private static final Uri TEST_URI = Uri.parse(TEST_URI_STRING);
+ private static final int TIME_OUT = 5000;
+
+ @Mock
+ private DevicePolicyManager mDevicePolicyManager;
+ @Mock
+ private FeatureFlags mFeatureFlags;
+ @Mock
+ private Optional<Bubbles> mOptionalBubbles;
+ @Mock
+ private Bubbles mBubbles;
+ @Mock
+ private NoteTaskController mNoteTaskController;
+ @Main
+ private Handler mMainHandler;
+
+ // Using the deprecated ActivityTestRule and SingleActivityFactory to help with injecting mocks
+ // and getting result from activity both of which are difficult to do in newer APIs.
+ private final SingleActivityFactory<AppClipsTrampolineActivityTestable> mFactory =
+ new SingleActivityFactory<>(AppClipsTrampolineActivityTestable.class) {
+ @Override
+ protected AppClipsTrampolineActivityTestable create(Intent unUsed) {
+ return new AppClipsTrampolineActivityTestable(mDevicePolicyManager,
+ mFeatureFlags, mOptionalBubbles, mNoteTaskController, mMainHandler);
+ }
+ };
+
+ @Rule
+ public final ActivityTestRule<AppClipsTrampolineActivityTestable> mActivityRule =
+ new ActivityTestRule<>(mFactory, false, false);
+
+ private Context mContext;
+ private Intent mActivityIntent;
+ private ComponentName mExpectedComponentName;
+ private Intent mKillAppClipsActivityBroadcast;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = getContext();
+ mMainHandler = mContext.getMainThreadHandler();
+
+ mActivityIntent = new Intent(mContext, AppClipsTrampolineActivityTestable.class);
+ mExpectedComponentName = ComponentName.unflattenFromString(
+ mContext.getString(
+ R.string.config_screenshotAppClipsActivityComponent));
+ mKillAppClipsActivityBroadcast = new Intent(ACTION_FINISH_FROM_TRAMPOLINE)
+ .setComponent(mExpectedComponentName)
+ .setPackage(mExpectedComponentName.getPackageName());
+ }
+
+ @After
+ public void tearDown() {
+ mContext.sendBroadcast(mKillAppClipsActivityBroadcast, PERMISSION_SELF);
+ mActivityRule.finishActivity();
+ }
+
+ @Test
+ public void configComponentName_shouldResolve() {
+ // Verify component name is setup - has package and class name.
+ assertThat(mExpectedComponentName).isNotNull();
+ assertThat(mExpectedComponentName.getPackageName()).isNotEmpty();
+ assertThat(mExpectedComponentName.getClassName()).isNotEmpty();
+
+ // Verify an intent when launched with above component resolves to the same component to
+ // confirm that component from above is available in framework.
+ Intent appClipsActivityIntent = new Intent();
+ appClipsActivityIntent.setComponent(mExpectedComponentName);
+ ResolveInfo resolveInfo = getContext().getPackageManager().resolveActivity(
+ appClipsActivityIntent, PackageManager.ResolveInfoFlags.of(0));
+ ActivityInfo activityInfo = resolveInfo.activityInfo;
+
+ assertThat(activityInfo.packageName).isEqualTo(
+ mExpectedComponentName.getPackageName());
+ assertThat(activityInfo.name).isEqualTo(mExpectedComponentName.getClassName());
+ }
+
+ @Test
+ public void flagOff_shouldFinishWithResultCancel() {
+ when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(false);
+
+ mActivityRule.launchActivity(mActivityIntent);
+
+ assertThat(mActivityRule.getActivityResult().getResultCode())
+ .isEqualTo(Activity.RESULT_CANCELED);
+ }
+
+ @Test
+ public void bubblesEmpty_shouldFinishWithFailed() {
+ when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+ when(mOptionalBubbles.isEmpty()).thenReturn(true);
+
+ mActivityRule.launchActivity(mActivityIntent);
+
+ ActivityResult actualResult = mActivityRule.getActivityResult();
+ assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
+ assertThat(getStatusCodeExtra(actualResult.getResultData()))
+ .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ }
+
+ @Test
+ public void taskIdNotAppBubble_shouldFinishWithWindowModeUnsupported() {
+ when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+ when(mOptionalBubbles.isEmpty()).thenReturn(false);
+ when(mOptionalBubbles.get()).thenReturn(mBubbles);
+ when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(false);
+
+ mActivityRule.launchActivity(mActivityIntent);
+
+ ActivityResult actualResult = mActivityRule.getActivityResult();
+ assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
+ assertThat(getStatusCodeExtra(actualResult.getResultData()))
+ .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED);
+ }
+
+ @Test
+ public void dpmScreenshotBlocked_shouldFinishWithBlockedByAdmin() {
+ when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+ when(mOptionalBubbles.isEmpty()).thenReturn(false);
+ when(mOptionalBubbles.get()).thenReturn(mBubbles);
+ when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(true);
+ when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(true);
+
+ mActivityRule.launchActivity(mActivityIntent);
+
+ ActivityResult actualResult = mActivityRule.getActivityResult();
+ assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
+ assertThat(getStatusCodeExtra(actualResult.getResultData()))
+ .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN);
+ }
+
+ @Test
+ public void startAppClipsActivity_userCanceled_shouldReturnUserCanceled() {
+ mockToSatisfyAllPrerequisites();
+
+ AppClipsTrampolineActivityTestable activity = mActivityRule.launchActivity(mActivityIntent);
+ waitForIdleSync();
+
+ Bundle bundle = new Bundle();
+ bundle.putInt(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE,
+ CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED);
+ activity.getResultReceiverForTest().send(Activity.RESULT_OK, bundle);
+ waitForIdleSync();
+
+ ActivityResult actualResult = mActivityRule.getActivityResult();
+ assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
+ assertThat(getStatusCodeExtra(actualResult.getResultData()))
+ .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED);
+ }
+
+ @Test
+ public void startAppClipsActivity_shouldReturnSuccess() {
+ mockToSatisfyAllPrerequisites();
+
+ AppClipsTrampolineActivityTestable activity = mActivityRule.launchActivity(mActivityIntent);
+ waitForIdleSync();
+
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(EXTRA_SCREENSHOT_URI, TEST_URI);
+ bundle.putInt(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
+ activity.getResultReceiverForTest().send(Activity.RESULT_OK, bundle);
+ waitForIdleSync();
+
+ ActivityResult actualResult = mActivityRule.getActivityResult();
+ assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
+ assertThat(getStatusCodeExtra(actualResult.getResultData()))
+ .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
+ assertThat(actualResult.getResultData().getData()).isEqualTo(TEST_URI);
+ }
+
+ private void mockToSatisfyAllPrerequisites() {
+ when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+ when(mOptionalBubbles.isEmpty()).thenReturn(false);
+ when(mOptionalBubbles.get()).thenReturn(mBubbles);
+ when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(true);
+ when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false);
+ }
+
+ public static final class AppClipsTrampolineActivityTestable extends
+ AppClipsTrampolineActivity {
+
+ public AppClipsTrampolineActivityTestable(DevicePolicyManager devicePolicyManager,
+ FeatureFlags flags,
+ Optional<Bubbles> optionalBubbles,
+ NoteTaskController noteTaskController,
+ @Main Handler mainHandler) {
+ super(devicePolicyManager, flags, optionalBubbles, noteTaskController, mainHandler);
+ }
+ }
+
+ private static int getStatusCodeExtra(Intent intent) {
+ return intent.getIntExtra(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, -100);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java
new file mode 100644
index 0000000..d5af7ce1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot;
+
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.net.Uri;
+import android.os.UserHandle;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.screenshot.appclips.AppClipsCrossProcessHelper;
+
+import com.google.common.util.concurrent.Futures;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.time.ZonedDateTime;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
+@RunWith(AndroidJUnit4.class)
+public final class AppClipsViewModelTest extends SysuiTestCase {
+
+ private static final Bitmap FAKE_BITMAP = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888);
+ private static final Drawable FAKE_DRAWABLE = new ShapeDrawable();
+ private static final Rect FAKE_RECT = new Rect();
+ private static final Uri FAKE_URI = Uri.parse("www.test-uri.com");
+
+ @Mock private AppClipsCrossProcessHelper mAppClipsCrossProcessHelper;
+ @Mock private ImageExporter mImageExporter;
+
+ private com.android.systemui.screenshot.AppClipsViewModel mViewModel;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mViewModel = new AppClipsViewModel.Factory(mAppClipsCrossProcessHelper, mImageExporter,
+ getContext().getMainExecutor(), directExecutor()).create(AppClipsViewModel.class);
+ }
+
+ @Test
+ public void performScreenshot_fails_shouldUpdateErrorWithFailed() {
+ when(mAppClipsCrossProcessHelper.takeScreenshot()).thenReturn(null);
+
+ mViewModel.performScreenshot();
+ waitForIdleSync();
+
+ verify(mAppClipsCrossProcessHelper).takeScreenshot();
+ assertThat(mViewModel.getErrorLiveData().getValue())
+ .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ assertThat(mViewModel.getResultLiveData().getValue()).isNull();
+ }
+
+ @Test
+ public void performScreenshot_succeeds_shouldUpdateScreenshotWithBitmap() {
+ when(mAppClipsCrossProcessHelper.takeScreenshot()).thenReturn(FAKE_BITMAP);
+
+ mViewModel.performScreenshot();
+ waitForIdleSync();
+
+ verify(mAppClipsCrossProcessHelper).takeScreenshot();
+ assertThat(mViewModel.getErrorLiveData().getValue()).isNull();
+ assertThat(mViewModel.getScreenshot().getValue()).isEqualTo(FAKE_BITMAP);
+ }
+
+ @Test
+ public void saveScreenshot_throwsError_shouldUpdateErrorWithFailed() {
+ when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), any(
+ ZonedDateTime.class), any(UserHandle.class))).thenReturn(
+ Futures.immediateFailedFuture(new ExecutionException(new Throwable())));
+
+ mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT);
+ waitForIdleSync();
+
+ assertThat(mViewModel.getErrorLiveData().getValue())
+ .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ assertThat(mViewModel.getResultLiveData().getValue()).isNull();
+ }
+
+ @Test
+ public void saveScreenshot_failsSilently_shouldUpdateErrorWithFailed() {
+ when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), any(
+ ZonedDateTime.class), any(UserHandle.class))).thenReturn(
+ Futures.immediateFuture(new ImageExporter.Result()));
+
+ mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT);
+ waitForIdleSync();
+
+ assertThat(mViewModel.getErrorLiveData().getValue())
+ .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ assertThat(mViewModel.getResultLiveData().getValue()).isNull();
+ }
+
+ @Test
+ public void saveScreenshot_succeeds_shouldUpdateResultWithUri() {
+ ImageExporter.Result result = new ImageExporter.Result();
+ result.uri = FAKE_URI;
+ when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), any(
+ ZonedDateTime.class), any(UserHandle.class))).thenReturn(
+ Futures.immediateFuture(result));
+
+ mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT);
+ waitForIdleSync();
+
+ assertThat(mViewModel.getErrorLiveData().getValue()).isNull();
+ assertThat(mViewModel.getResultLiveData().getValue()).isEqualTo(FAKE_URI);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
index 4d7741ad..78bebb9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
@@ -27,6 +27,7 @@
import com.android.systemui.plugins.ClockId
import com.android.systemui.plugins.ClockMetadata
import com.android.systemui.plugins.ClockProviderPlugin
+import com.android.systemui.plugins.ClockSettings
import com.android.systemui.plugins.PluginListener
import com.android.systemui.plugins.PluginManager
import com.android.systemui.util.mockito.argumentCaptor
@@ -59,7 +60,7 @@
private lateinit var pluginListener: PluginListener<ClockProviderPlugin>
private lateinit var registry: ClockRegistry
- private var settingValue: String = ""
+ private var settingValue: ClockSettings? = null
companion object {
private fun failFactory(): ClockController {
@@ -79,7 +80,8 @@
private val thumbnailCallbacks = mutableMapOf<ClockId, () -> Drawable?>()
override fun getClocks() = metadata
- override fun createClock(id: ClockId): ClockController = createCallbacks[id]!!()
+ override fun createClock(settings: ClockSettings): ClockController =
+ createCallbacks[settings.clockId!!]!!()
override fun getClockThumbnail(id: ClockId): Drawable? = thumbnailCallbacks[id]!!()
fun addClock(
@@ -110,7 +112,7 @@
userHandle = UserHandle.USER_ALL,
defaultClockProvider = fakeDefaultProvider
) {
- override var currentClockId: ClockId
+ override var settings: ClockSettings?
get() = settingValue
set(value) { settingValue = value }
}
@@ -185,7 +187,7 @@
.addClock("clock_1", "clock 1")
.addClock("clock_2", "clock 2")
- settingValue = "clock_3"
+ settingValue = ClockSettings("clock_3", null, null)
val plugin2 = FakeClockPlugin()
.addClock("clock_3", "clock 3", { mockClock })
.addClock("clock_4", "clock 4")
@@ -203,7 +205,7 @@
.addClock("clock_1", "clock 1")
.addClock("clock_2", "clock 2")
- settingValue = "clock_3"
+ settingValue = ClockSettings("clock_3", null, null)
val plugin2 = FakeClockPlugin()
.addClock("clock_3", "clock 3")
.addClock("clock_4", "clock 4")
@@ -222,7 +224,7 @@
.addClock("clock_1", "clock 1")
.addClock("clock_2", "clock 2")
- settingValue = "clock_3"
+ settingValue = ClockSettings("clock_3", null, null)
val plugin2 = FakeClockPlugin()
.addClock("clock_3", "clock 3", { mockClock })
.addClock("clock_4", "clock 4")
@@ -242,8 +244,8 @@
@Test
fun jsonDeserialization_gotExpectedObject() {
- val expected = ClockRegistry.ClockSetting("ID", 500)
- val actual = ClockRegistry.ClockSetting.deserialize("""{
+ val expected = ClockSettings("ID", null, 500)
+ val actual = ClockSettings.deserialize("""{
"clockId":"ID",
"_applied_timestamp":500
}""")
@@ -252,15 +254,15 @@
@Test
fun jsonDeserialization_noTimestamp_gotExpectedObject() {
- val expected = ClockRegistry.ClockSetting("ID", null)
- val actual = ClockRegistry.ClockSetting.deserialize("{\"clockId\":\"ID\"}")
+ val expected = ClockSettings("ID", null, null)
+ val actual = ClockSettings.deserialize("{\"clockId\":\"ID\"}")
assertEquals(expected, actual)
}
@Test
fun jsonDeserialization_nullTimestamp_gotExpectedObject() {
- val expected = ClockRegistry.ClockSetting("ID", null)
- val actual = ClockRegistry.ClockSetting.deserialize("""{
+ val expected = ClockSettings("ID", null, null)
+ val actual = ClockSettings.deserialize("""{
"clockId":"ID",
"_applied_timestamp":null
}""")
@@ -269,22 +271,22 @@
@Test(expected = JSONException::class)
fun jsonDeserialization_noId_threwException() {
- val expected = ClockRegistry.ClockSetting("ID", 500)
- val actual = ClockRegistry.ClockSetting.deserialize("{\"_applied_timestamp\":500}")
+ val expected = ClockSettings("ID", null, 500)
+ val actual = ClockSettings.deserialize("{\"_applied_timestamp\":500}")
assertEquals(expected, actual)
}
@Test
fun jsonSerialization_gotExpectedString() {
val expected = "{\"clockId\":\"ID\",\"_applied_timestamp\":500}"
- val actual = ClockRegistry.ClockSetting.serialize( ClockRegistry.ClockSetting("ID", 500))
+ val actual = ClockSettings.serialize(ClockSettings("ID", null, 500))
assertEquals(expected, actual)
}
@Test
fun jsonSerialization_noTimestamp_gotExpectedString() {
val expected = "{\"clockId\":\"ID\"}"
- val actual = ClockRegistry.ClockSetting.serialize( ClockRegistry.ClockSetting("ID", null))
+ val actual = ClockSettings.serialize(ClockSettings("ID", null, null))
assertEquals(expected, actual)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
index a7588dd..cd2efc0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
@@ -114,7 +114,8 @@
@Test
fun defaultClock_events_onTimeTick() {
val clock = provider.createClock(DEFAULT_CLOCK_ID)
- clock.events.onTimeTick()
+ clock.smallClock.events.onTimeTick()
+ clock.largeClock.events.onTimeTick()
verify(mockSmallClockView).refreshTime()
verify(mockLargeClockView).refreshTime()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
index b1ca1c0..f581154 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -18,8 +18,6 @@
import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT;
import static android.inputmethodservice.InputMethodService.IME_INVISIBLE;
import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_STATUS_BAR;
import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -158,7 +156,7 @@
@Test
public void testShowTransient() {
- int[] types = new int[]{ITYPE_STATUS_BAR, ITYPE_NAVIGATION_BAR};
+ int types = WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars();
mCommandQueue.showTransient(DEFAULT_DISPLAY, types, true /* isGestureOnSystemBar */);
waitForIdleSync();
verify(mCallbacks).showTransient(eq(DEFAULT_DISPLAY), eq(types), eq(true));
@@ -166,7 +164,7 @@
@Test
public void testShowTransientForSecondaryDisplay() {
- int[] types = new int[]{ITYPE_STATUS_BAR, ITYPE_NAVIGATION_BAR};
+ int types = WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars();
mCommandQueue.showTransient(SECONDARY_DISPLAY, types, true /* isGestureOnSystemBar */);
waitForIdleSync();
verify(mCallbacks).showTransient(eq(SECONDARY_DISPLAY), eq(types), eq(true));
@@ -174,7 +172,7 @@
@Test
public void testAbortTransient() {
- int[] types = new int[]{ITYPE_STATUS_BAR, ITYPE_NAVIGATION_BAR};
+ int types = WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars();
mCommandQueue.abortTransient(DEFAULT_DISPLAY, types);
waitForIdleSync();
verify(mCallbacks).abortTransient(eq(DEFAULT_DISPLAY), eq(types));
@@ -182,7 +180,7 @@
@Test
public void testAbortTransientForSecondaryDisplay() {
- int[] types = new int[]{ITYPE_STATUS_BAR, ITYPE_NAVIGATION_BAR};
+ int types = WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars();
mCommandQueue.abortTransient(SECONDARY_DISPLAY, types);
waitForIdleSync();
verify(mCallbacks).abortTransient(eq(SECONDARY_DISPLAY), eq(types));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index 0806a62..1ac7eaa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -631,6 +631,82 @@
}
@Test
+ public void onBiometricHelp_coEx_fpFailure_faceAlreadyUnlocked() {
+ createController();
+
+ // GIVEN face has already unlocked the device
+ when(mKeyguardUpdateMonitor.getUserUnlockedWithFace(anyInt())).thenReturn(true);
+
+ String message = "A message";
+ mController.setVisible(true);
+
+ // WHEN there's a fingerprint not recognized message
+ mController.getKeyguardCallback().onBiometricHelp(
+ BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED,
+ message,
+ BiometricSourceType.FINGERPRINT);
+
+ // THEN show sequential messages such as: 'Unlocked by face' and
+ // 'Swipe up to open'
+ verifyIndicationMessage(
+ INDICATION_TYPE_BIOMETRIC_MESSAGE,
+ mContext.getString(R.string.keyguard_face_successful_unlock));
+ verifyIndicationMessage(
+ INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+ mContext.getString(R.string.keyguard_unlock));
+ }
+
+ @Test
+ public void onBiometricHelp_coEx_fpFailure_trustAgentAlreadyUnlocked() {
+ createController();
+
+ // GIVEN trust agent has already unlocked the device
+ when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true);
+
+ String message = "A message";
+ mController.setVisible(true);
+
+ // WHEN there's a fingerprint not recognized message
+ mController.getKeyguardCallback().onBiometricHelp(
+ BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED,
+ message,
+ BiometricSourceType.FINGERPRINT);
+
+ // THEN show sequential messages such as: 'Kept unlocked by TrustAgent' and
+ // 'Swipe up to open'
+ verifyIndicationMessage(
+ INDICATION_TYPE_BIOMETRIC_MESSAGE,
+ mContext.getString(R.string.keyguard_indication_trust_unlocked));
+ verifyIndicationMessage(
+ INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+ mContext.getString(R.string.keyguard_unlock));
+ }
+
+ @Test
+ public void onBiometricHelp_coEx_fpFailure_trustAgentUnlocked_emptyTrustGrantedMessage() {
+ createController();
+
+ // GIVEN trust agent has already unlocked the device & trust granted message is empty
+ when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true);
+ mController.showTrustGrantedMessage(false, "");
+
+ String message = "A message";
+ mController.setVisible(true);
+
+ // WHEN there's a fingerprint not recognized message
+ mController.getKeyguardCallback().onBiometricHelp(
+ BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED,
+ message,
+ BiometricSourceType.FINGERPRINT);
+
+ // THEN show action to unlock (ie: 'Swipe up to open')
+ verifyNoMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE);
+ verifyIndicationMessage(
+ INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+ mContext.getString(R.string.keyguard_unlock));
+ }
+
+ @Test
public void transientIndication_visibleWhenDozing_unlessSwipeUp_fromError() {
createController();
String message = mContext.getString(R.string.keyguard_unlock);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
index 2423f13..9c69a6a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
@@ -32,6 +32,7 @@
import android.view.View
import android.widget.FrameLayout
import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
@@ -104,6 +105,9 @@
private lateinit var keyguardBypassController: KeyguardBypassController
@Mock
+ private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+
+ @Mock
private lateinit var deviceProvisionedController: DeviceProvisionedController
@Mock
@@ -113,6 +117,9 @@
private lateinit var handler: Handler
@Mock
+ private lateinit var datePlugin: BcSmartspaceDataPlugin
+
+ @Mock
private lateinit var weatherPlugin: BcSmartspaceDataPlugin
@Mock
@@ -155,6 +162,7 @@
KeyguardBypassController.OnBypassStateChangedListener
private lateinit var deviceProvisionedListener: DeviceProvisionedListener
+ private lateinit var dateSmartspaceView: SmartspaceView
private lateinit var weatherSmartspaceView: SmartspaceView
private lateinit var smartspaceView: SmartspaceView
@@ -190,6 +198,8 @@
`when`(secureSettings.getUriFor(NOTIF_ON_LOCKSCREEN_SETTING))
.thenReturn(fakeNotifOnLockscreenSettingUri)
`when`(smartspaceManager.createSmartspaceSession(any())).thenReturn(smartspaceSession)
+ `when`(datePlugin.getView(any())).thenReturn(
+ createDateSmartspaceView(), createDateSmartspaceView())
`when`(weatherPlugin.getView(any())).thenReturn(
createWeatherSmartspaceView(), createWeatherSmartspaceView())
`when`(plugin.getView(any())).thenReturn(createSmartspaceView(), createSmartspaceView())
@@ -217,10 +227,12 @@
statusBarStateController,
deviceProvisionedController,
keyguardBypassController,
+ keyguardUpdateMonitor,
execution,
executor,
bgExecutor,
handler,
+ Optional.of(datePlugin),
Optional.of(weatherPlugin),
Optional.of(plugin),
Optional.of(configPlugin),
@@ -275,7 +287,8 @@
// THEN the listener is registered to the underlying plugin
verify(plugin).registerListener(controllerListener)
- // The listener is registered only for the plugin, not the weather plugin.
+ // The listener is registered only for the plugin, not the date, or weather plugin.
+ verify(datePlugin, never()).registerListener(any())
verify(weatherPlugin, never()).registerListener(any())
}
@@ -289,7 +302,8 @@
// THEN the listener is subsequently registered
verify(plugin).registerListener(controllerListener)
- // The listener is registered only for the plugin, not the weather plugin.
+ // The listener is registered only for the plugin, not the date, or the weather plugin.
+ verify(datePlugin, never()).registerListener(any())
verify(weatherPlugin, never()).registerListener(any())
}
@@ -308,6 +322,7 @@
verify(plugin).registerSmartspaceEventNotifier(null)
verify(weatherPlugin).onTargetsAvailable(emptyList())
verify(weatherPlugin).registerSmartspaceEventNotifier(null)
+ verify(datePlugin).registerSmartspaceEventNotifier(null)
}
@Test
@@ -357,6 +372,7 @@
configChangeListener.onThemeChanged()
// We update the new text color to match the wallpaper color
+ verify(dateSmartspaceView).setPrimaryTextColor(anyInt())
verify(weatherSmartspaceView).setPrimaryTextColor(anyInt())
verify(smartspaceView).setPrimaryTextColor(anyInt())
}
@@ -384,6 +400,7 @@
statusBarStateListener.onDozeAmountChanged(0.1f, 0.7f)
// We pass that along to the view
+ verify(dateSmartspaceView).setDozeAmount(0.7f)
verify(weatherSmartspaceView).setDozeAmount(0.7f)
verify(smartspaceView).setDozeAmount(0.7f)
}
@@ -502,6 +519,8 @@
verify(plugin).onTargetsAvailable(eq(listOf(targets[0], targets[1], targets[2])))
// No filtering is applied for the weather plugin
verify(weatherPlugin).onTargetsAvailable(eq(targets))
+ // No targets needed for the date plugin
+ verify(datePlugin, never()).onTargetsAvailable(any())
}
@Test
@@ -633,6 +652,18 @@
private fun connectSession() {
if (controller.isDateWeatherDecoupled()) {
+ val dateView = controller.buildAndConnectDateView(fakeParent)
+ dateSmartspaceView = dateView as SmartspaceView
+ fakeParent.addView(dateView)
+ controller.stateChangeListener.onViewAttachedToWindow(dateView)
+
+ verify(dateSmartspaceView).setUiSurface(
+ BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
+ verify(dateSmartspaceView).registerDataProvider(datePlugin)
+
+ verify(dateSmartspaceView).setPrimaryTextColor(anyInt())
+ verify(dateSmartspaceView).setDozeAmount(0.5f)
+
val weatherView = controller.buildAndConnectWeatherView(fakeParent)
weatherSmartspaceView = weatherView as SmartspaceView
fakeParent.addView(weatherView)
@@ -686,6 +717,7 @@
verify(smartspaceView).setDozeAmount(0.5f)
if (controller.isDateWeatherDecoupled()) {
+ clearInvocations(dateSmartspaceView)
clearInvocations(weatherSmartspaceView)
}
clearInvocations(smartspaceView)
@@ -734,7 +766,38 @@
).thenReturn(if (value) 1 else 0)
}
- // Separate function for the weather view, which doesn't implement all functions in interface.
+ // Separate function for the date view, which implements a specific subset of all functions.
+ private fun createDateSmartspaceView(): SmartspaceView {
+ return spy(object : View(context), SmartspaceView {
+ override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {
+ }
+
+ override fun setPrimaryTextColor(color: Int) {
+ }
+
+ override fun setIsDreaming(isDreaming: Boolean) {
+ }
+
+ override fun setUiSurface(uiSurface: String) {
+ }
+
+ override fun setDozeAmount(amount: Float) {
+ }
+
+ override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) {
+ }
+
+ override fun setFalsingManager(falsingManager: FalsingManager?) {
+ }
+
+ override fun setDnd(image: Drawable?, description: String?) {
+ }
+
+ override fun setNextAlarm(image: Drawable?, description: String?) {
+ }
+ })
+ }
+ // Separate function for the weather view, which implements a specific subset of all functions.
private fun createWeatherSmartspaceView(): SmartspaceView {
return spy(object : View(context), SmartspaceView {
override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DeviceProvisionedCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DeviceProvisionedCoordinatorTest.java
index af52459..701cf95 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DeviceProvisionedCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DeviceProvisionedCoordinatorTest.java
@@ -117,6 +117,10 @@
extras.putBoolean(SHOW_WHEN_UNPROVISIONED_FLAG, true);
mNotification.extras = extras;
+ // GIVEN notification has the permission to display during setup
+ when(mIPackageManager.checkUidPermission(SETUP_NOTIF_PERMISSION, NOTIF_UID))
+ .thenReturn(PackageManager.PERMISSION_GRANTED);
+
// THEN don't filter out the notification
assertFalse(mDeviceProvisionedFilter.shouldFilterOut(mEntry, 0));
}
@@ -126,10 +130,15 @@
// GIVEN device is unprovisioned
when(mDeviceProvisionedController.isDeviceProvisioned()).thenReturn(false);
- // GIVEN notification does not have the flag to allow the notification during setup
+ // GIVEN notification has a flag to allow the notification during setup
Bundle extras = new Bundle();
+ extras.putBoolean(SHOW_WHEN_UNPROVISIONED_FLAG, true);
mNotification.extras = extras;
+ // GIVEN notification does NOT have permission to display during setup
+ when(mIPackageManager.checkUidPermission(SETUP_NOTIF_PERMISSION, NOTIF_UID))
+ .thenReturn(PackageManager.PERMISSION_DENIED);
+
// THEN filter out the notification
assertTrue(mDeviceProvisionedFilter.shouldFilterOut(mEntry, 0));
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt
index 33b94e3..bd03903 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt
@@ -32,6 +32,7 @@
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
+import java.lang.RuntimeException
import kotlinx.coroutines.Dispatchers
import org.junit.Before
import org.junit.Test
@@ -113,6 +114,24 @@
assertThat(data).hasSize(2)
}
+ @Test
+ fun onPullAtom_throwsInterruptedException_failsGracefully() {
+ val pipeline: NotifPipeline = mock()
+ whenever(pipeline.allNotifs).thenAnswer { throw InterruptedException("Timeout") }
+ val logger = NotificationMemoryLogger(pipeline, statsManager, immediate, bgExecutor)
+ assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, mutableListOf()))
+ .isEqualTo(StatsManager.PULL_SKIP)
+ }
+
+ @Test
+ fun onPullAtom_throwsRuntimeException_failsGracefully() {
+ val pipeline: NotifPipeline = mock()
+ whenever(pipeline.allNotifs).thenThrow(RuntimeException("Something broke!"))
+ val logger = NotificationMemoryLogger(pipeline, statsManager, immediate, bgExecutor)
+ assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, mutableListOf()))
+ .isEqualTo(StatsManager.PULL_SKIP)
+ }
+
private fun createLoggerWithNotifications(
notifications: List<Notification>
): NotificationMemoryLogger {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
index ed3f710..7e69efa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
@@ -264,6 +264,19 @@
}
@Test
+ public void notifPositionAlignedWithClockAndBurnInOffsetInSplitShadeMode() {
+ setSplitShadeTopMargin(100); // this makes clock to be at 100
+ givenAOD();
+ mIsSplitShade = true;
+ givenMaxBurnInOffset(100);
+ givenHighestBurnInOffset(); // this makes clock to be at 200
+ // WHEN the position algorithm is run
+ positionClock();
+ // THEN the notif padding adjusts for burn-in offset: clock position - burn-in offset
+ assertThat(mClockPosition.stackScrollerPadding).isEqualTo(100);
+ }
+
+ @Test
public void clockPositionedDependingOnMarginInSplitShade() {
setSplitShadeTopMargin(400);
givenLockScreen();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 36e76f4..85e8c34 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -23,10 +23,12 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -45,6 +47,7 @@
import androidx.test.filters.SmallTest;
+import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.R;
import com.android.systemui.SysuiBaseFragmentTest;
import com.android.systemui.dump.DumpManager;
@@ -67,6 +70,8 @@
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateListener;
import com.android.systemui.util.CarrierConfigTracker;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.settings.SecureSettings;
@@ -79,6 +84,9 @@
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import java.util.ArrayList;
+import java.util.List;
+
@RunWith(AndroidTestingRunner.class)
@RunWithLooper(setAsMainLooper = true)
@SmallTest
@@ -118,6 +126,12 @@
private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
@Mock
private DumpManager mDumpManager;
+ @Mock
+ private StatusBarWindowStateController mStatusBarWindowStateController;
+ @Mock
+ private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+
+ private List<StatusBarWindowStateListener> mStatusBarWindowStateListeners = new ArrayList<>();
public CollapsedStatusBarFragmentTest() {
super(CollapsedStatusBarFragment.class);
@@ -127,6 +141,14 @@
public void setup() {
injectLeakCheckedDependencies(ALL_SUPPORTED_CLASSES);
mDependency.injectMockDependency(DarkIconDispatcher.class);
+
+ // Keep the window state listeners so we can dispatch to them to test the status bar
+ // fragment's response.
+ doAnswer(invocation -> {
+ mStatusBarWindowStateListeners.add(invocation.getArgument(0));
+ return null;
+ }).when(mStatusBarWindowStateController).addListener(
+ any(StatusBarWindowStateListener.class));
}
@Test
@@ -414,6 +436,27 @@
assertFalse(contains);
}
+ @Test
+ public void testStatusBarIcons_hiddenThroughoutCameraLaunch() {
+ final CollapsedStatusBarFragment fragment = resumeAndGetFragment();
+
+ mockSecureCameraLaunch(fragment, true /* launched */);
+
+ // Status icons should be invisible or gone, but certainly not VISIBLE.
+ assertNotEquals(View.VISIBLE, getEndSideContentView().getVisibility());
+ assertNotEquals(View.VISIBLE, getClockView().getVisibility());
+
+ mockSecureCameraLaunchFinished();
+
+ assertNotEquals(View.VISIBLE, getEndSideContentView().getVisibility());
+ assertNotEquals(View.VISIBLE, getClockView().getVisibility());
+
+ mockSecureCameraLaunch(fragment, false /* launched */);
+
+ assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
+ assertEquals(View.VISIBLE, getClockView().getVisibility());
+ }
+
@Override
protected Fragment instantiate(Context context, String className, Bundle arguments) {
MockitoAnnotations.initMocks(this);
@@ -455,7 +498,9 @@
mOperatorNameViewControllerFactory,
mSecureSettings,
mExecutor,
- mDumpManager);
+ mDumpManager,
+ mStatusBarWindowStateController,
+ mKeyguardUpdateMonitor);
}
private void setUpDaggerComponent() {
@@ -478,6 +523,35 @@
mNotificationAreaInner);
}
+ /**
+ * Configure mocks to return values consistent with the secure camera animating itself launched
+ * over the keyguard.
+ */
+ private void mockSecureCameraLaunch(CollapsedStatusBarFragment fragment, boolean launched) {
+ when(mKeyguardUpdateMonitor.isSecureCameraLaunchedOverKeyguard()).thenReturn(launched);
+ when(mKeyguardStateController.isOccluded()).thenReturn(launched);
+
+ if (launched) {
+ fragment.onCameraLaunchGestureDetected(0 /* source */);
+ } else {
+ for (StatusBarWindowStateListener listener : mStatusBarWindowStateListeners) {
+ listener.onStatusBarWindowStateChanged(StatusBarManager.WINDOW_STATE_SHOWING);
+ }
+ }
+
+ fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
+ }
+
+ /**
+ * Configure mocks to return values consistent with the secure camera showing over the keyguard
+ * with its launch animation finished.
+ */
+ private void mockSecureCameraLaunchFinished() {
+ for (StatusBarWindowStateListener listener : mStatusBarWindowStateListeners) {
+ listener.onStatusBarWindowStateChanged(StatusBarManager.WINDOW_STATE_HIDDEN);
+ }
+ }
+
private CollapsedStatusBarFragment resumeAndGetFragment() {
mFragments.dispatchResume();
processAllMessages();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt
index f822ba0..45189cf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt
@@ -19,7 +19,8 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.log.table.TableRowLogger
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ACTIVITY_DIRECTION
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ACTIVITY_DIRECTION_IN
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ACTIVITY_DIRECTION_OUT
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CARRIER_NETWORK_CHANGE
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CDMA_LEVEL
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CONNECTION_STATE
@@ -54,7 +55,19 @@
assertThat(logger.changes)
.contains(Pair(COL_CONNECTION_STATE, connection.dataConnectionState.toString()))
assertThat(logger.changes)
- .contains(Pair(COL_ACTIVITY_DIRECTION, connection.dataActivityDirection.toString()))
+ .contains(
+ Pair(
+ COL_ACTIVITY_DIRECTION_IN,
+ connection.dataActivityDirection.hasActivityIn.toString(),
+ )
+ )
+ assertThat(logger.changes)
+ .contains(
+ Pair(
+ COL_ACTIVITY_DIRECTION_OUT,
+ connection.dataActivityDirection.hasActivityOut.toString(),
+ )
+ )
assertThat(logger.changes)
.contains(
Pair(COL_CARRIER_NETWORK_CHANGE, connection.carrierNetworkChangeActive.toString())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index d6b8c0d..314e250 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
@@ -41,6 +41,8 @@
import android.telephony.TelephonyManager.DATA_CONNECTING
import android.telephony.TelephonyManager.DATA_DISCONNECTED
import android.telephony.TelephonyManager.DATA_DISCONNECTING
+import android.telephony.TelephonyManager.DATA_HANDOVER_IN_PROGRESS
+import android.telephony.TelephonyManager.DATA_SUSPENDED
import android.telephony.TelephonyManager.DATA_UNKNOWN
import android.telephony.TelephonyManager.ERI_OFF
import android.telephony.TelephonyManager.ERI_ON
@@ -255,6 +257,37 @@
}
@Test
+ fun testFlowForSubId_dataConnectionState_suspended() =
+ runBlocking(IMMEDIATE) {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ val callback =
+ getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+ callback.onDataConnectionStateChanged(DATA_SUSPENDED, 200 /* unused */)
+
+ assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Suspended)
+
+ job.cancel()
+ }
+
+ @Test
+ fun testFlowForSubId_dataConnectionState_handoverInProgress() =
+ runBlocking(IMMEDIATE) {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ val callback =
+ getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+ callback.onDataConnectionStateChanged(DATA_HANDOVER_IN_PROGRESS, 200 /* unused */)
+
+ assertThat(latest?.dataConnectionState)
+ .isEqualTo(DataConnectionState.HandoverInProgress)
+
+ job.cancel()
+ }
+
+ @Test
fun testFlowForSubId_dataConnectionState_unknown() =
runBlocking(IMMEDIATE) {
var latest: MobileConnectionModel? = null
@@ -270,6 +303,21 @@
}
@Test
+ fun testFlowForSubId_dataConnectionState_invalid() =
+ runBlocking(IMMEDIATE) {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ val callback =
+ getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+ callback.onDataConnectionStateChanged(45, 200 /* unused */)
+
+ assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Invalid)
+
+ job.cancel()
+ }
+
+ @Test
fun testFlowForSubId_dataActivity() =
runBlocking(IMMEDIATE) {
var latest: MobileConnectionModel? = null
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index 1b62d5c..bd24922 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -28,6 +28,7 @@
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.util.CarrierConfigTracker
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
@@ -77,6 +78,7 @@
MobileIconsInteractorImpl(
connectionsRepository,
carrierConfigTracker,
+ logger = mock(),
userSetupRepository,
testScope.backgroundScope,
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
index a24e29ae..b91a4df 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -17,6 +17,8 @@
package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
import androidx.test.filters.SmallTest
+import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH
+import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH_NONE
import com.android.settingslib.graph.SignalDrawable
import com.android.settingslib.mobile.TelephonyIcons.THREE_G
import com.android.systemui.SysuiTestCase
@@ -122,6 +124,39 @@
}
@Test
+ fun contentDescription_notInService_usesNoPhone() =
+ testScope.runTest {
+ var latest: ContentDescription? = null
+ val job = underTest.contentDescription.onEach { latest = it }.launchIn(this)
+
+ interactor.isInService.value = false
+
+ assertThat((latest as ContentDescription.Resource).res)
+ .isEqualTo(PHONE_SIGNAL_STRENGTH_NONE)
+
+ job.cancel()
+ }
+
+ @Test
+ fun contentDescription_inService_usesLevel() =
+ testScope.runTest {
+ var latest: ContentDescription? = null
+ val job = underTest.contentDescription.onEach { latest = it }.launchIn(this)
+
+ interactor.isInService.value = true
+
+ interactor.level.value = 2
+ assertThat((latest as ContentDescription.Resource).res)
+ .isEqualTo(PHONE_SIGNAL_STRENGTH[2])
+
+ interactor.level.value = 0
+ assertThat((latest as ContentDescription.Resource).res)
+ .isEqualTo(PHONE_SIGNAL_STRENGTH[0])
+
+ job.cancel()
+ }
+
+ @Test
fun networkType_dataEnabled_groupIsRepresented() =
testScope.runTest {
val expected =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
index dd04ac4..fc7436a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
@@ -79,6 +79,7 @@
@Mock private lateinit var viewUtil: ViewUtil
@Mock private lateinit var vibratorHelper: VibratorHelper
@Mock private lateinit var swipeGestureHandler: SwipeChipbarAwayGestureHandler
+ private lateinit var chipbarAnimator: TestChipbarAnimator
private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder
private lateinit var fakeWakeLock: WakeLockFake
private lateinit var fakeClock: FakeSystemClock
@@ -98,6 +99,7 @@
fakeWakeLockBuilder.setWakeLock(fakeWakeLock)
uiEventLoggerFake = UiEventLoggerFake()
+ chipbarAnimator = TestChipbarAnimator()
underTest =
ChipbarCoordinator(
@@ -109,6 +111,7 @@
configurationController,
dumpManager,
powerManager,
+ chipbarAnimator,
falsingManager,
falsingCollector,
swipeGestureHandler,
@@ -371,6 +374,26 @@
verify(vibratorHelper).vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK))
}
+ /** Regression test for b/266119467. */
+ @Test
+ fun displayView_animationFailure_viewsStillBecomeVisible() {
+ chipbarAnimator.allowAnimation = false
+
+ underTest.displayView(
+ createChipbarInfo(
+ Icon.Resource(R.id.check_box, null),
+ Text.Loaded("text"),
+ endItem = ChipbarEndItem.Loading,
+ )
+ )
+
+ val view = getChipbarView()
+ assertThat(view.getInnerView().alpha).isEqualTo(1f)
+ assertThat(view.getStartIconView().alpha).isEqualTo(1f)
+ assertThat(view.getLoadingIcon().alpha).isEqualTo(1f)
+ assertThat(view.getChipTextView().alpha).isEqualTo(1f)
+ }
+
@Test
fun updateView_viewUpdated() {
// First, display a view
@@ -453,6 +476,25 @@
verify(windowManager).removeView(chipbarView)
}
+ /** Regression test for b/266209420. */
+ @Test
+ fun removeView_animationFailure_viewStillRemoved() {
+ chipbarAnimator.allowAnimation = false
+
+ underTest.displayView(
+ createChipbarInfo(
+ Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+ Text.Loaded("title text"),
+ endItem = ChipbarEndItem.Error,
+ ),
+ )
+ val chipbarView = getChipbarView()
+
+ underTest.removeView(DEVICE_ID, "test reason")
+
+ verify(windowManager).removeView(chipbarView)
+ }
+
@Test
fun swipeToDismiss_false_neverListensForGesture() {
underTest.displayView(
@@ -560,8 +602,9 @@
private fun ViewGroup.getStartIconView() = this.requireViewById<ImageView>(R.id.start_icon)
- private fun ViewGroup.getChipText(): String =
- (this.requireViewById<TextView>(R.id.text)).text as String
+ private fun ViewGroup.getChipTextView() = this.requireViewById<TextView>(R.id.text)
+
+ private fun ViewGroup.getChipText(): String = this.getChipTextView().text as String
private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading)
@@ -574,6 +617,25 @@
verify(windowManager).addView(viewCaptor.capture(), any())
return viewCaptor.value as ViewGroup
}
+
+ /** Test class that lets us disallow animations. */
+ inner class TestChipbarAnimator : ChipbarAnimator() {
+ var allowAnimation: Boolean = true
+
+ override fun animateViewIn(innerView: ViewGroup, onAnimationEnd: Runnable): Boolean {
+ if (!allowAnimation) {
+ return false
+ }
+ return super.animateViewIn(innerView, onAnimationEnd)
+ }
+
+ override fun animateViewOut(innerView: ViewGroup, onAnimationEnd: Runnable): Boolean {
+ if (!allowAnimation) {
+ return false
+ }
+ return super.animateViewOut(innerView, onAnimationEnd)
+ }
+ }
}
private const val TIMEOUT = 10000
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index 034c618..9312643 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -17,12 +17,17 @@
package com.android.systemui.user.data.repository
+import android.app.IActivityManager
+import android.app.UserSwitchObserver
import android.content.pm.UserInfo
+import android.os.IRemoteCallback
import android.os.UserHandle
import android.os.UserManager
import android.provider.Settings
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.util.settings.FakeSettings
@@ -39,7 +44,14 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@@ -48,6 +60,8 @@
class UserRepositoryImplTest : SysuiTestCase() {
@Mock private lateinit var manager: UserManager
+ @Mock private lateinit var activityManager: IActivityManager
+ @Captor private lateinit var userSwitchObserver: ArgumentCaptor<UserSwitchObserver>
private lateinit var underTest: UserRepositoryImpl
@@ -114,6 +128,11 @@
@Test
fun refreshUsers() = runSelfCancelingTest {
+ val mainUserId = 10
+ val mainUser = mock(UserHandle::class.java)
+ whenever(manager.mainUser).thenReturn(mainUser)
+ whenever(mainUser.identifier).thenReturn(mainUserId)
+
underTest = create(this)
val initialExpectedValue =
setUpUsers(
@@ -152,6 +171,7 @@
assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1])
assertThat(selectedUserInfo?.isGuest).isTrue()
assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId)
+ assertThat(underTest.mainUserId).isEqualTo(mainUserId)
}
@Test
@@ -214,6 +234,34 @@
assertThat(selectedUserInfo?.id).isEqualTo(1)
}
+ @Test
+ fun userSwitchingInProgress_registersOnlyOneUserSwitchObserver() = runSelfCancelingTest {
+ underTest = create(this)
+
+ underTest.userSwitchingInProgress.launchIn(this)
+ underTest.userSwitchingInProgress.launchIn(this)
+ underTest.userSwitchingInProgress.launchIn(this)
+
+ verify(activityManager, times(1)).registerUserSwitchObserver(any(), anyString())
+ }
+
+ @Test
+ fun userSwitchingInProgress_propagatesStateFromActivityManager() = runSelfCancelingTest {
+ underTest = create(this)
+ verify(activityManager)
+ .registerUserSwitchObserver(userSwitchObserver.capture(), anyString())
+
+ userSwitchObserver.value.onUserSwitching(0, mock(IRemoteCallback::class.java))
+
+ var mostRecentSwitchingValue = false
+ underTest.userSwitchingInProgress.onEach { mostRecentSwitchingValue = it }.launchIn(this)
+
+ assertThat(mostRecentSwitchingValue).isTrue()
+
+ userSwitchObserver.value.onUserSwitchComplete(0)
+ assertThat(mostRecentSwitchingValue).isFalse()
+ }
+
private fun createUserInfo(
id: Int,
isGuest: Boolean,
@@ -280,6 +328,8 @@
}
private fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl {
+ val featureFlags = FakeFeatureFlags()
+ featureFlags.set(FACE_AUTH_REFACTOR, true)
return UserRepositoryImpl(
appContext = context,
manager = manager,
@@ -288,6 +338,8 @@
backgroundDispatcher = IMMEDIATE,
globalSettings = globalSettings,
tracker = tracker,
+ activityManager = activityManager,
+ featureFlags = featureFlags,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
index fb781e8..cc23485 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
@@ -204,10 +204,12 @@
}
@Test
- fun `exit - last non-guest was removed - returns to system`() =
+ fun `exit - last non-guest was removed - returns to main user`() =
runBlocking(IMMEDIATE) {
val removedUserId = 310
+ val mainUserId = 10
repository.lastSelectedNonGuestUserId = removedUserId
+ repository.mainUserId = mainUserId
repository.setSelectedUserInfo(GUEST_USER_INFO)
underTest.exit(
@@ -221,7 +223,7 @@
verify(manager, never()).markGuestForDeletion(anyInt())
verify(manager, never()).removeUser(anyInt())
- verify(switchUser).invoke(UserHandle.USER_SYSTEM)
+ verify(switchUser).invoke(mainUserId)
}
@Test
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 9d518ac..c6b4c92 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -74,6 +74,7 @@
import android.testing.TestableLooper;
import android.util.Pair;
import android.util.SparseArray;
+import android.view.IWindowManager;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
@@ -392,7 +393,8 @@
syncExecutor,
mock(Handler.class),
mTaskViewTransitions,
- mock(SyncTransactionQueue.class));
+ mock(SyncTransactionQueue.class),
+ mock(IWindowManager.class));
mBubbleController.setExpandListener(mBubbleExpandListener);
spyOn(mBubbleController);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
index 6357a09..3179285 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
@@ -20,6 +20,7 @@
import android.content.pm.LauncherApps;
import android.os.Handler;
import android.os.UserManager;
+import android.view.IWindowManager;
import android.view.WindowManager;
import com.android.internal.statusbar.IStatusBarService;
@@ -72,13 +73,14 @@
ShellExecutor shellMainExecutor,
Handler shellMainHandler,
TaskViewTransitions taskViewTransitions,
- SyncTransactionQueue syncQueue) {
+ SyncTransactionQueue syncQueue,
+ IWindowManager wmService) {
super(context, shellInit, shellCommandHandler, shellController, data, Runnable::run,
floatingContentCoordinator, dataRepository, statusBarService, windowManager,
windowManagerShellWrapper, userManager, launcherApps, bubbleLogger,
taskStackListener, shellTaskOrganizer, positioner, displayController,
oneHandedOptional, dragAndDropController, shellMainExecutor, shellMainHandler,
- new SyncExecutor(), taskViewTransitions, syncQueue);
+ new SyncExecutor(), taskViewTransitions, syncQueue, wmService);
setInflateSynchronously(true);
onInit();
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
similarity index 95%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
index f3e52de..044679d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
@@ -21,7 +21,7 @@
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
-class FakeBiometricRepository : BiometricRepository {
+class FakeBiometricSettingsRepository : BiometricSettingsRepository {
private val _isFingerprintEnrolled = MutableStateFlow<Boolean>(false)
override val isFingerprintEnrolled: StateFlow<Boolean> = _isFingerprintEnrolled.asStateFlow()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeDisplayTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeDisplayTracker.kt
index 6ae7c34..1403cea 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeDisplayTracker.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeDisplayTracker.kt
@@ -21,7 +21,7 @@
import android.view.Display
import java.util.concurrent.Executor
-class FakeDisplayTracker internal constructor(val context: Context) : DisplayTracker {
+class FakeDisplayTracker constructor(val context: Context) : DisplayTracker {
val displayManager: DisplayManager = context.getSystemService(DisplayManager::class.java)!!
override var defaultDisplayId: Int = Display.DEFAULT_DISPLAY
override var allDisplays: Array<Display> = displayManager.displays
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index ea5a302..53bb340 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -28,6 +28,10 @@
import kotlinx.coroutines.yield
class FakeUserRepository : UserRepository {
+ companion object {
+ // User id to represent a non system (human) user id. We presume this is the main user.
+ private const val MAIN_USER_ID = 10
+ }
private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel())
override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
@@ -39,7 +43,12 @@
private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
- override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
+ private val _userSwitchingInProgress = MutableStateFlow(false)
+ override val userSwitchingInProgress: Flow<Boolean>
+ get() = _userSwitchingInProgress
+
+ override var mainUserId: Int = MAIN_USER_ID
+ override var lastSelectedNonGuestUserId: Int = mainUserId
private var _isGuestUserAutoCreated: Boolean = false
override val isGuestUserAutoCreated: Boolean
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/AlwaysOnMagnificationFeatureFlag.java b/services/accessibility/java/com/android/server/accessibility/magnification/AlwaysOnMagnificationFeatureFlag.java
new file mode 100644
index 0000000..ed45e7b
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/AlwaysOnMagnificationFeatureFlag.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.magnification;
+
+import android.provider.DeviceConfig;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Encapsulates the feature flags for always on magnification. {@see DeviceConfig}
+ *
+ * @hide
+ */
+public class AlwaysOnMagnificationFeatureFlag {
+
+ private static final String NAMESPACE = DeviceConfig.NAMESPACE_WINDOW_MANAGER;
+ private static final String FEATURE_NAME_ENABLE_ALWAYS_ON_MAGNIFICATION =
+ "AlwaysOnMagnifier__enable_always_on_magnifier";
+
+ private AlwaysOnMagnificationFeatureFlag() {}
+
+ /** Returns true if the feature flag is enabled for always on magnification */
+ public static boolean isAlwaysOnMagnificationEnabled() {
+ return DeviceConfig.getBoolean(
+ NAMESPACE,
+ FEATURE_NAME_ENABLE_ALWAYS_ON_MAGNIFICATION,
+ /* defaultValue= */ false);
+ }
+
+ /** Sets the feature flag. Only used for testing; requires shell permissions. */
+ @VisibleForTesting
+ public static boolean setAlwaysOnMagnificationEnabled(boolean isEnabled) {
+ return DeviceConfig.setProperty(
+ NAMESPACE,
+ FEATURE_NAME_ENABLE_ALWAYS_ON_MAGNIFICATION,
+ Boolean.toString(isEnabled),
+ /* makeDefault= */ false);
+ }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
index 37069dc..595cdec 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
@@ -52,6 +52,7 @@
import android.view.animation.DecelerateInterpolator;
import com.android.internal.R;
+import com.android.internal.accessibility.common.MagnificationConstants;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.function.pooled.PooledLambda;
@@ -1157,11 +1158,14 @@
}
/**
- * Persists the default display magnification scale to the current user's settings.
+ * Persists the default display magnification scale to the current user's settings
+ * <strong>if scale is >= {@link MagnificationConstants.PERSISTED_SCALE_MIN_VALUE}</strong>.
+ * We assume if the scale is < {@link MagnificationConstants.PERSISTED_SCALE_MIN_VALUE}, there
+ * will be no obvious magnification effect.
*/
public void persistScale(int displayId) {
final float scale = getScale(Display.DEFAULT_DISPLAY);
- if (scale < 2.0f) {
+ if (scale < MagnificationConstants.PERSISTED_SCALE_MIN_VALUE) {
return;
}
mScaleProvider.putScale(scale, displayId);
@@ -1176,7 +1180,8 @@
*/
public float getPersistedScale(int displayId) {
return MathUtils.constrain(mScaleProvider.getScale(displayId),
- 2.0f, MagnificationScaleProvider.MAX_SCALE);
+ MagnificationConstants.PERSISTED_SCALE_MIN_VALUE,
+ MagnificationScaleProvider.MAX_SCALE);
}
/**
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
index 6bf37a1..9fc9d57 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
@@ -992,9 +992,8 @@
mFullScreenMagnificationController.getPersistedScale(mDisplayId),
MIN_SCALE, MAX_SCALE);
- final float scale = MathUtils.constrain(Math.max(currentScale + 1.0f, persistedScale),
- MIN_SCALE, MAX_SCALE);
-
+ final boolean isActivated = mFullScreenMagnificationController.isActivated(mDisplayId);
+ final float scale = isActivated ? (currentScale + 1.0f) : persistedScale;
zoomToScale(scale, centerX, centerY);
}
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
index 2d5f894..d9391f4 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
@@ -46,6 +46,7 @@
import android.view.accessibility.IWindowMagnificationConnectionCallback;
import android.view.accessibility.MagnificationAnimationCallback;
+import com.android.internal.accessibility.common.MagnificationConstants;
import com.android.internal.accessibility.util.AccessibilityStatsLogUtils;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -716,17 +717,20 @@
*/
float getPersistedScale(int displayId) {
return MathUtils.constrain(mScaleProvider.getScale(displayId),
- 2.0f, MagnificationScaleProvider.MAX_SCALE);
+ MagnificationConstants.PERSISTED_SCALE_MIN_VALUE,
+ MagnificationScaleProvider.MAX_SCALE);
}
/**
* Persists the default display magnification scale to the current user's settings
- * <strong>if scale is >= 2.0</strong>. Only the
- * value of the default display is persisted in user's settings.
+ * <strong>if scale is >= {@link MagnificationConstants.PERSISTED_SCALE_MIN_VALUE}</strong>.
+ * We assume if the scale is < {@link MagnificationConstants.PERSISTED_SCALE_MIN_VALUE}, there
+ * will be no obvious magnification effect.
+ * Only the value of the default display is persisted in user's settings.
*/
void persistScale(int displayId) {
float scale = getScale(displayId);
- if (scale < 2.0f) {
+ if (scale < MagnificationConstants.PERSISTED_SCALE_MIN_VALUE) {
return;
}
mScaleProvider.putScale(scale, displayId);
diff --git a/services/api/current.txt b/services/api/current.txt
index f7d6ee9..a4deed3 100644
--- a/services/api/current.txt
+++ b/services/api/current.txt
@@ -225,6 +225,15 @@
}
+package com.android.server.security {
+
+ public final class FileIntegrity {
+ method public static void setUpFsVerity(@NonNull java.io.File) throws java.io.IOException;
+ method public static void setUpFsVerity(@NonNull android.os.ParcelFileDescriptor) throws java.io.IOException;
+ }
+
+}
+
package com.android.server.stats {
public final class StatsHelper {
diff --git a/services/art-profile b/services/art-profile
index 2bb85a4..132b9ab 100644
--- a/services/art-profile
+++ b/services/art-profile
@@ -20484,12 +20484,12 @@
Lcom/android/server/usage/UsageStatsShellCommand;
Lcom/android/server/usage/UserUsageStatsService$StatsUpdatedListener;
Lcom/android/server/usb/UsbAlsaJackDetector;
+Lcom/android/server/usb/UsbAlsaMidiDevice$2;
+Lcom/android/server/usb/UsbAlsaMidiDevice$3;
+Lcom/android/server/usb/UsbAlsaMidiDevice$InputReceiverProxy;
+Lcom/android/server/usb/UsbAlsaMidiDevice;
Lcom/android/server/usb/UsbDeviceManager;
Lcom/android/server/usb/UsbHostManager;
-Lcom/android/server/usb/UsbMidiDevice$2;
-Lcom/android/server/usb/UsbMidiDevice$3;
-Lcom/android/server/usb/UsbMidiDevice$InputReceiverProxy;
-Lcom/android/server/usb/UsbMidiDevice;
Lcom/android/server/usb/descriptors/UsbDescriptor;
Lcom/android/server/usb/descriptors/UsbInterfaceDescriptor;
Lcom/android/server/utils/AlarmQueue$1;
diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
index 5f1da7b..590f472 100644
--- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
@@ -315,7 +315,12 @@
event.mFillRequestSentTimestampMs,
event.mFillResponseReceivedTimestampMs,
event.mSuggestionSentTimestampMs,
- event.mSuggestionPresentedTimestampMs);
+ event.mSuggestionPresentedTimestampMs,
+ //TODO(b/265051751): add new framework logging.
+ /* selected_dataset_id= */ 0,
+ /* dialog_dismissed= */ false,
+ /* negative_cta_button_clicked= */ false,
+ /* positive_cta_button_clicked= */ false);
mEventInternal = Optional.empty();
}
diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index 998c9c2..7261709 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -551,7 +551,7 @@
mPackageManagerBinder = AppGlobals.getPackageManager();
mActivityManager = ActivityManager.getService();
mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
- mScheduledBackupEligibility = getEligibilityRules(mPackageManager, userId,
+ mScheduledBackupEligibility = getEligibilityRules(mPackageManager, userId, mContext,
BackupDestination.CLOUD);
mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
@@ -4118,13 +4118,14 @@
public BackupEligibilityRules getEligibilityRulesForOperation(
@BackupDestination int backupDestination) {
- return getEligibilityRules(mPackageManager, mUserId, backupDestination);
+ return getEligibilityRules(mPackageManager, mUserId, mContext, backupDestination);
}
private static BackupEligibilityRules getEligibilityRules(PackageManager packageManager,
- int userId, @BackupDestination int backupDestination) {
+ int userId, Context context, @BackupDestination int backupDestination) {
return new BackupEligibilityRules(packageManager,
- LocalServices.getService(PackageManagerInternal.class), userId, backupDestination);
+ LocalServices.getService(PackageManagerInternal.class), userId, context,
+ backupDestination);
}
/** Prints service state for 'dumpsys backup'. */
diff --git a/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java
index 515a172..2374dee 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java
@@ -112,7 +112,9 @@
BackupEligibilityRules eligibilityRules = new BackupEligibilityRules(
mBackupManagerService.getPackageManager(),
LocalServices.getService(PackageManagerInternal.class),
- mBackupManagerService.getUserId(), BackupDestination.ADB_BACKUP);
+ mBackupManagerService.getUserId(),
+ mBackupManagerService.getContext(),
+ BackupDestination.ADB_BACKUP);
FullRestoreEngine mEngine = new FullRestoreEngine(mBackupManagerService,
mOperationStorage, null, mObserver, null, null,
true, 0 /*unused*/, true, eligibilityRules);
diff --git a/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java b/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
index 2ee9174..7c47f1e 100644
--- a/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
+++ b/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
@@ -31,6 +31,7 @@
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.Overridable;
+import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@@ -39,10 +40,12 @@
import android.content.pm.SigningInfo;
import android.os.Build;
import android.os.UserHandle;
+import android.os.UserManager;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
+import com.android.server.backup.SetUtils;
import com.android.server.backup.transport.BackupTransportClient;
import com.android.server.backup.transport.TransportConnection;
@@ -56,13 +59,26 @@
*/
public class BackupEligibilityRules {
private static final boolean DEBUG = false;
- // List of system packages that are eligible for backup in non-system users.
- private static final Set<String> systemPackagesAllowedForAllUsers = Sets.newArraySet(
- PACKAGE_MANAGER_SENTINEL, PLATFORM_PACKAGE_NAME, WALLPAPER_PACKAGE, SETTINGS_PACKAGE);
+
+ /**
+ * List of system packages that are eligible for backup in "profile" users (such as work
+ * profile). See {@link UserManager#isProfile()}. This is a subset of {@link
+ * #systemPackagesAllowedForNonSystemUsers}
+ */
+ private static final Set<String> systemPackagesAllowedForProfileUser =
+ Sets.newArraySet(PACKAGE_MANAGER_SENTINEL, PLATFORM_PACKAGE_NAME);
+
+ /**
+ * List of system packages that are eligible for backup in non-system users.
+ */
+ private static final Set<String> systemPackagesAllowedForNonSystemUsers = SetUtils.union(
+ systemPackagesAllowedForProfileUser,
+ Sets.newArraySet(WALLPAPER_PACKAGE, SETTINGS_PACKAGE));
private final PackageManager mPackageManager;
private final PackageManagerInternal mPackageManagerInternal;
private final int mUserId;
+ private boolean mIsProfileUser = false;
@BackupDestination private final int mBackupDestination;
/**
@@ -85,19 +101,23 @@
public static BackupEligibilityRules forBackup(PackageManager packageManager,
PackageManagerInternal packageManagerInternal,
- int userId) {
- return new BackupEligibilityRules(packageManager, packageManagerInternal, userId,
+ int userId,
+ Context context) {
+ return new BackupEligibilityRules(packageManager, packageManagerInternal, userId, context,
BackupDestination.CLOUD);
}
public BackupEligibilityRules(PackageManager packageManager,
PackageManagerInternal packageManagerInternal,
int userId,
+ Context context,
@BackupDestination int backupDestination) {
mPackageManager = packageManager;
mPackageManagerInternal = packageManagerInternal;
mUserId = userId;
mBackupDestination = backupDestination;
+ UserManager userManager = context.getSystemService(UserManager.class);
+ mIsProfileUser = userManager.isProfile();
}
/**
@@ -125,11 +145,17 @@
// 2. they run as a system-level uid
if (UserHandle.isCore(app.uid)) {
- // and the backup is happening for a non-system user on a package that is not explicitly
- // allowed.
- if (mUserId != UserHandle.USER_SYSTEM
- && !systemPackagesAllowedForAllUsers.contains(app.packageName)) {
- return false;
+ // and the backup is happening for a non-system user or profile on a package that is
+ // not explicitly allowed.
+ if (mUserId != UserHandle.USER_SYSTEM) {
+ if (mIsProfileUser && !systemPackagesAllowedForProfileUser.contains(
+ app.packageName)) {
+ return false;
+ }
+ if (!mIsProfileUser && !systemPackagesAllowedForNonSystemUsers.contains(
+ app.packageName)) {
+ return false;
+ }
}
// or do not supply their own backup agent
diff --git a/services/backup/java/com/android/server/backup/utils/RestoreUtils.java b/services/backup/java/com/android/server/backup/utils/RestoreUtils.java
index 8e8bac4..0accb9f 100644
--- a/services/backup/java/com/android/server/backup/utils/RestoreUtils.java
+++ b/services/backup/java/com/android/server/backup/utils/RestoreUtils.java
@@ -160,7 +160,8 @@
PackageManagerInternal pmi = LocalServices.getService(
PackageManagerInternal.class);
BackupEligibilityRules eligibilityRules =
- BackupEligibilityRules.forBackup(packageManager, pmi, userId);
+ BackupEligibilityRules.forBackup(packageManager, pmi, userId,
+ context);
if (eligibilityRules.signaturesMatch(sigs, pkg)) {
// If this is a system-uid app without a declared backup agent,
// don't restore any of the file data.
diff --git a/services/backup/java/com/android/server/backup/utils/SetUtils.java b/services/backup/java/com/android/server/backup/utils/SetUtils.java
new file mode 100644
index 0000000..ecd628f
--- /dev/null
+++ b/services/backup/java/com/android/server/backup/utils/SetUtils.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.utils;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Helper class containing common operation on {@link java.util.Set}.
+ */
+public final class SetUtils {
+ // Statics only
+ private SetUtils() {}
+
+ /**
+ * Returns union of two sets.
+ */
+ public static <T> Set<T> union(Set<T> set1, Set<T> set2) {
+ Set<T> unionSet = new HashSet<>(set1);
+ unionSet.addAll(set2);
+ return unionSet;
+ }
+}
diff --git a/services/backup/java/com/android/server/backup/utils/TarBackupReader.java b/services/backup/java/com/android/server/backup/utils/TarBackupReader.java
index 6963248..71ca8ca 100644
--- a/services/backup/java/com/android/server/backup/utils/TarBackupReader.java
+++ b/services/backup/java/com/android/server/backup/utils/TarBackupReader.java
@@ -47,6 +47,7 @@
import android.app.backup.BackupManagerMonitor;
import android.app.backup.FullBackup;
import android.app.backup.IBackupManagerMonitor;
+import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@@ -384,13 +385,14 @@
* @param info - file metadata.
* @param signatures - array of signatures parsed from backup file.
* @param userId - ID of the user for which restore is performed.
+ * @param context - Context instance.
* @return a restore policy constant.
*/
public RestorePolicy chooseRestorePolicy(PackageManager packageManager,
boolean allowApks, FileMetadata info, Signature[] signatures,
- PackageManagerInternal pmi, int userId) {
+ PackageManagerInternal pmi, int userId, Context context) {
return chooseRestorePolicy(packageManager, allowApks, info, signatures, pmi, userId,
- BackupEligibilityRules.forBackup(packageManager, pmi, userId));
+ BackupEligibilityRules.forBackup(packageManager, pmi, userId, context));
}
/**
diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java
index f93f504..fc81675 100644
--- a/services/core/java/com/android/server/BinaryTransparencyService.java
+++ b/services/core/java/com/android/server/BinaryTransparencyService.java
@@ -34,6 +34,7 @@
import android.content.IntentFilter;
import android.content.pm.ApexStagedEvent;
import android.content.pm.ApplicationInfo;
+import android.content.pm.Checksum;
import android.content.pm.IBackgroundInstallControlService;
import android.content.pm.IPackageManagerNative;
import android.content.pm.IStagedApexObserver;
@@ -84,6 +85,7 @@
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.pm.ApexManager;
import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.AndroidPackageSplit;
import com.android.server.pm.pkg.PackageState;
import libcore.util.HexEncoding;
@@ -96,7 +98,6 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
/**
@@ -121,15 +122,6 @@
static final long RECORD_MEASUREMENTS_COOLDOWN_MS = 24 * 60 * 60 * 1000;
- @VisibleForTesting
- static final String BUNDLE_PACKAGE_NAME = "package-name";
- @VisibleForTesting
- static final String BUNDLE_PACKAGE_IS_APEX = "package-is-apex";
- @VisibleForTesting
- static final String BUNDLE_CONTENT_DIGEST_ALGORITHM = "content-digest-algo";
- @VisibleForTesting
- static final String BUNDLE_CONTENT_DIGEST = "content-digest";
-
static final String APEX_PRELOAD_LOCATION_ERROR = "could-not-be-determined";
// used for indicating any type of error during MBA measurement
@@ -171,29 +163,6 @@
return mVbmetaDigest;
}
- @Override
- public List getApexInfo() {
- List<Bundle> results = new ArrayList<>();
-
- for (PackageInfo packageInfo : getCurrentInstalledApexs()) {
- PackageState packageState = mPackageManagerInternal.getPackageStateInternal(
- packageInfo.packageName);
- if (packageState == null) {
- Slog.w(TAG, "Package state is unavailable, ignoring the package "
- + packageInfo.packageName);
- continue;
- }
- Bundle apexMeasurement = measurePackage(packageState);
- if (apexMeasurement == null) {
- Slog.w(TAG, "Skipping the missing APEX in " + packageState.getPath());
- continue;
- }
- results.add(apexMeasurement);
- }
-
- return results;
- }
-
/**
* A helper function to compute the SHA256 digest of APK package signer.
* @param signingInfo The signingInfo of a package, usually {@link PackageInfo#signingInfo}.
@@ -218,58 +187,102 @@
return resultList.toArray(new String[1]);
}
- /**
- * Perform basic measurement (i.e. content digest) on a given package.
+ /*
+ * Perform basic measurement (i.e. content digest) on a given app, including the split APKs.
+ *
* @param packageState The package to be measured.
- * @return a {@link android.os.Bundle} that packs the measurement result with the following
- * keys: {@link #BUNDLE_PACKAGE_NAME},
- * {@link #BUNDLE_PACKAGE_IS_APEX}
- * {@link #BUNDLE_CONTENT_DIGEST_ALGORITHM}
- * {@link #BUNDLE_CONTENT_DIGEST}
+ * @param mbaStatus Assign this value of MBA status to the returned elements.
+ * @return a @{@code List<IBinaryTransparencyService.AppInfo>}
*/
- private @Nullable Bundle measurePackage(PackageState packageState) {
- Bundle result = new Bundle();
-
+ private @NonNull List<IBinaryTransparencyService.AppInfo> collectAppInfo(
+ PackageState packageState, int mbaStatus) {
// compute content digest
if (DEBUG) {
Slog.d(TAG, "Computing content digest for " + packageState.getPackageName() + " at "
+ packageState.getPath());
}
+
+ var results = new ArrayList<IBinaryTransparencyService.AppInfo>();
+
+ // Same attributes across base and splits.
+ String packageName = packageState.getPackageName();
+ long versionCode = packageState.getVersionCode();
+ String[] signerDigests =
+ computePackageSignerSha256Digests(packageState.getSigningInfo());
+
AndroidPackage pkg = packageState.getAndroidPackage();
- if (pkg == null) {
- Slog.w(TAG, "Skipping the missing APK in " + packageState.getPath());
- return null;
+ for (AndroidPackageSplit split : pkg.getSplits()) {
+ var appInfo = new IBinaryTransparencyService.AppInfo();
+ appInfo.packageName = packageName;
+ appInfo.longVersion = versionCode;
+ appInfo.splitName = split.getName(); // base's split name is null
+ // Signer digests are consistent between splits, guaranteed by Package Manager.
+ appInfo.signerDigests = signerDigests;
+ appInfo.mbaStatus = mbaStatus;
+
+ // Only digest and split name are different between splits.
+ Checksum checksum = measureApk(split.getPath());
+ appInfo.digest = checksum.getValue();
+ appInfo.digestAlgorithm = checksum.getType();
+
+ results.add(appInfo);
}
- Map<Integer, byte[]> contentDigests = computeApkContentDigest(pkg.getBaseApkPath());
- result.putString(BUNDLE_PACKAGE_NAME, pkg.getPackageName());
+
+ // InstallSourceInfo is only available per package name, so store it only on the base
+ // APK. It's not current currently available in PackageState (there's a TODO), to we
+ // need to extract manually with another call.
+ //
+ // Base APK is already the 0-th split from getSplits() and can't be null.
+ AppInfo base = results.get(0);
+ InstallSourceInfo installSourceInfo = getInstallSourceInfo(
+ packageState.getPackageName());
+ if (installSourceInfo != null) {
+ base.initiator = installSourceInfo.getInitiatingPackageName();
+ SigningInfo initiatorSignerInfo =
+ installSourceInfo.getInitiatingPackageSigningInfo();
+ if (initiatorSignerInfo != null) {
+ base.initiatorSignerDigests =
+ computePackageSignerSha256Digests(initiatorSignerInfo);
+ }
+ base.installer = installSourceInfo.getInstallingPackageName();
+ base.originator = installSourceInfo.getOriginatingPackageName();
+ }
+
+ return results;
+ }
+
+ /**
+ * Perform basic measurement (i.e. content digest) on a given APK.
+ *
+ * @param apkPath The APK (or APEX, since it's also an APK) file to be measured.
+ * @return a {@link android.content.pm.Checksum} with preferred digest algorithm type and
+ * the checksum.
+ */
+ private @Nullable Checksum measureApk(@NonNull String apkPath) {
+ // compute content digest
+ Map<Integer, byte[]> contentDigests = computeApkContentDigest(apkPath);
if (contentDigests == null) {
- Slog.d(TAG, "Failed to compute content digest for " + pkg.getBaseApkPath());
- result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, 0);
- result.putByteArray(BUNDLE_CONTENT_DIGEST, null);
- return result;
+ Slog.d(TAG, "Failed to compute content digest for " + apkPath);
+ return new Checksum(0, new byte[] { -1 });
}
// in this iteration, we'll be supporting only 2 types of digests:
// CHUNKED_SHA256 and CHUNKED_SHA512.
// And only one of them will be available per package.
if (contentDigests.containsKey(ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256)) {
- Integer algorithmId = ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256;
- result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, algorithmId);
- result.putByteArray(BUNDLE_CONTENT_DIGEST, contentDigests.get(algorithmId));
+ return new Checksum(
+ Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256,
+ contentDigests.get(ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256));
} else if (contentDigests.containsKey(
ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512)) {
- Integer algorithmId = ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512;
- result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, algorithmId);
- result.putByteArray(BUNDLE_CONTENT_DIGEST, contentDigests.get(algorithmId));
+ return new Checksum(
+ Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512,
+ contentDigests.get(ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512));
} else {
// TODO(b/259423111): considering putting the raw values for the algorithm & digest
// into the bundle to track potential other digest algorithms that may be in use
- result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, 0);
- result.putByteArray(BUNDLE_CONTENT_DIGEST, null);
+ return new Checksum(0, new byte[] { -1 });
}
- result.putBoolean(BUNDLE_PACKAGE_IS_APEX, packageState.isApex());
-
- return result;
}
@@ -287,7 +300,7 @@
* - dynamically installed mobile bundled apps (MBAs) (new in Android U)
*/
public void recordMeasurementsForAllPackages() {
- // check if we should record the resulting measurements
+ // check if we should measure and record
long currentTimeMs = System.currentTimeMillis();
if ((currentTimeMs - mMeasurementsLastRecordedMs) < RECORD_MEASUREMENTS_COOLDOWN_MS) {
Slog.d(TAG, "Skip measurement since the last measurement was only taken at "
@@ -331,7 +344,7 @@
if (CompatChanges.isChangeEnabled(LOG_MBA_INFO)) {
// lastly measure all newly installed MBAs
List<IBinaryTransparencyService.AppInfo> allMbaInfo =
- collectAllMbaInfo(packagesMeasured);
+ collectAllSilentInstalledMbaInfo(packagesMeasured);
for (IBinaryTransparencyService.AppInfo appInfo : allUpdatedPreloadInfo) {
packagesMeasured.putBoolean(appInfo.packageName, true);
writeAppInfoToLog(appInfo);
@@ -357,18 +370,22 @@
continue;
}
- Bundle apexMeasurement = measurePackage(packageState);
- if (apexMeasurement == null) {
- Slog.w(TAG, "Skipping the missing APEX in " + packageState.getPath());
+ AndroidPackage pkg = packageState.getAndroidPackage();
+ if (pkg == null) {
+ Slog.w(TAG, "Skipping the missing APK in " + pkg.getPath());
+ continue;
+ }
+ Checksum apexChecksum = measureApk(pkg.getPath());
+ if (apexChecksum == null) {
+ Slog.w(TAG, "Skipping the missing APEX in " + pkg.getPath());
continue;
}
var apexInfo = new IBinaryTransparencyService.ApexInfo();
apexInfo.packageName = packageState.getPackageName();
apexInfo.longVersion = packageState.getVersionCode();
- apexInfo.digest = apexMeasurement.getByteArray(BUNDLE_CONTENT_DIGEST);
- apexInfo.digestAlgorithm =
- apexMeasurement.getInt(BUNDLE_CONTENT_DIGEST_ALGORITHM);
+ apexInfo.digest = apexChecksum.getValue();
+ apexInfo.digestAlgorithm = apexChecksum.getType();
apexInfo.signerDigests =
computePackageSignerSha256Digests(packageState.getSigningInfo());
@@ -399,28 +416,16 @@
Slog.d(TAG, "Preload " + packageState.getPackageName() + " at "
+ packageState.getPath() + " has likely been updated.");
- Bundle packageMeasurement = measurePackage(packageState);
- if (packageMeasurement == null) {
- Slog.w(TAG, "Skipping the missing APK in " + packageState.getPath());
- return;
- }
-
- var appInfo = new IBinaryTransparencyService.AppInfo();
- appInfo.packageName = packageState.getPackageName();
- appInfo.longVersion = packageState.getVersionCode();
- appInfo.digest = packageMeasurement.getByteArray(BUNDLE_CONTENT_DIGEST);
- appInfo.digestAlgorithm =
- packageMeasurement.getInt(BUNDLE_CONTENT_DIGEST_ALGORITHM);
- appInfo.signerDigests =
- computePackageSignerSha256Digests(packageState.getSigningInfo());
- appInfo.mbaStatus = MBA_STATUS_UPDATED_PRELOAD;
-
- results.add(appInfo);
+ List<IBinaryTransparencyService.AppInfo> resultsForApp = collectAppInfo(
+ packageState, MBA_STATUS_UPDATED_PRELOAD);
+ results.addAll(resultsForApp);
});
return results;
}
- public List<IBinaryTransparencyService.AppInfo> collectAllMbaInfo(Bundle packagesToSkip) {
+ @Override
+ public List<IBinaryTransparencyService.AppInfo> collectAllSilentInstalledMbaInfo(
+ Bundle packagesToSkip) {
var results = new ArrayList<IBinaryTransparencyService.AppInfo>();
for (PackageInfo packageInfo : getNewlyInstalledMbas()) {
if (packagesToSkip.containsKey(packageInfo.packageName)) {
@@ -434,42 +439,9 @@
continue;
}
- Bundle packageMeasurement = measurePackage(packageState);
- if (packageMeasurement == null) {
- Slog.w(TAG, "Skipping the missing APK in " + packageState.getPath());
- continue;
- }
- if (DEBUG) {
- Slog.d(TAG,
- "Extracting InstallSourceInfo for " + packageState.getPackageName());
- }
- var appInfo = new IBinaryTransparencyService.AppInfo();
- appInfo.packageName = packageState.getPackageName();
- appInfo.longVersion = packageState.getVersionCode();
- appInfo.digest = packageMeasurement.getByteArray(BUNDLE_CONTENT_DIGEST);
- appInfo.digestAlgorithm =
- packageMeasurement.getInt(BUNDLE_CONTENT_DIGEST_ALGORITHM);
- appInfo.signerDigests =
- computePackageSignerSha256Digests(packageState.getSigningInfo());
- appInfo.mbaStatus = MBA_STATUS_NEW_INSTALL;
-
- // Install source isn't currently available in PackageState (there's a TODO).
- // Extract manually with another call.
- InstallSourceInfo installSourceInfo = getInstallSourceInfo(
- packageState.getPackageName());
- if (installSourceInfo != null) {
- appInfo.initiator = installSourceInfo.getInitiatingPackageName();
- SigningInfo initiatorSignerInfo =
- installSourceInfo.getInitiatingPackageSigningInfo();
- if (initiatorSignerInfo != null) {
- appInfo.initiatorSignerDigests =
- computePackageSignerSha256Digests(initiatorSignerInfo);
- }
- appInfo.installer = installSourceInfo.getInstallingPackageName();
- appInfo.originator = installSourceInfo.getOriginatingPackageName();
- }
-
- results.add(appInfo);
+ List<IBinaryTransparencyService.AppInfo> resultsForApp = collectAppInfo(
+ packageState, MBA_STATUS_NEW_INSTALL);
+ results.addAll(resultsForApp);
}
return results;
}
@@ -1230,10 +1202,8 @@
* JobService to measure all covered binaries and record result to Westworld.
*/
public static class UpdateMeasurementsJobService extends JobService {
- private static AtomicBoolean sScheduled = new AtomicBoolean();
private static long sTimeLastRanMs = 0;
- private static final int DO_BINARY_MEASUREMENTS_JOB_ID =
- UpdateMeasurementsJobService.class.hashCode();
+ private static final int DO_BINARY_MEASUREMENTS_JOB_ID = 1740526926;
@Override
public boolean onStartJob(JobParameters params) {
@@ -1256,7 +1226,6 @@
return;
}
sTimeLastRanMs = System.currentTimeMillis();
- sScheduled.set(false);
jobFinished(params, false);
}).start();
@@ -1277,7 +1246,7 @@
return;
}
- if (sScheduled.get()) {
+ if (jobScheduler.getPendingJob(DO_BINARY_MEASUREMENTS_JOB_ID) != null) {
Slog.d(TAG, "A measurement job has already been scheduled.");
return;
}
@@ -1303,7 +1272,6 @@
Slog.e(TAG, "Failed to schedule job to measure binaries.");
return;
}
- sScheduled.set(true);
Slog.d(TAG, TextUtils.formatSimple(
"Job %d to measure binaries was scheduled successfully.",
DO_BINARY_MEASUREMENTS_JOB_ID));
diff --git a/services/core/java/com/android/server/PersistentDataBlockService.java b/services/core/java/com/android/server/PersistentDataBlockService.java
index 5eb0db1..6fd6afe 100644
--- a/services/core/java/com/android/server/PersistentDataBlockService.java
+++ b/services/core/java/com/android/server/PersistentDataBlockService.java
@@ -182,7 +182,6 @@
public void onStart() {
// Do init on a separate thread, will join in PHASE_ACTIVITY_MANAGER_READY
SystemServerInitThreadPool.submit(() -> {
- mAllowedUid = getAllowedUid();
enforceChecksumValidity();
formatIfOemUnlockEnabled();
publishBinderService(Context.PERSISTENT_DATA_BLOCK_SERVICE, mService);
@@ -202,6 +201,8 @@
Thread.currentThread().interrupt();
throw new IllegalStateException("Service " + TAG + " init interrupted", e);
}
+ // The user responsible for FRP should exist by now.
+ mAllowedUid = getAllowedUid();
LocalServices.addService(PersistentDataBlockManagerInternal.class, mInternalService);
}
super.onBootPhase(phase);
diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java
index 4854c37..4b76127 100644
--- a/services/core/java/com/android/server/SystemConfig.java
+++ b/services/core/java/com/android/server/SystemConfig.java
@@ -333,6 +333,11 @@
// Update ownership for system applications and the installers eligible to update them.
private final ArrayMap<String, String> mUpdateOwnersForSystemApps = new ArrayMap<>();
+ // Set of package names that should not be marked as "stopped" during initial device boot
+ // or when adding a new user. A new package not contained in this set will be
+ // marked as stopped by the system
+ @NonNull private final Set<String> mInitialNonStoppedSystemPackages = new ArraySet<>();
+
/**
* Map of system pre-defined, uniquely named actors; keys are namespace,
* value maps actor name to package name.
@@ -527,6 +532,10 @@
? null : mOverlayConfigSignaturePackage;
}
+ public Set<String> getInitialNonStoppedSystemPackages() {
+ return mInitialNonStoppedSystemPackages;
+ }
+
/**
* Only use for testing. Do NOT use in production code.
* @param readPermissions false to create an empty SystemConfig; true to read the permissions.
@@ -1445,6 +1454,19 @@
}
XmlUtils.skipCurrentTag(parser);
} break;
+ case "initial-package-state": {
+ String pkgName = parser.getAttributeValue(null, "package");
+ String stopped = parser.getAttributeValue(null, "stopped");
+ if (TextUtils.isEmpty(pkgName)) {
+ Slog.w(TAG, "<" + name + "> without package in " + permFile
+ + " at " + parser.getPositionDescription());
+ } else if (TextUtils.isEmpty(stopped)) {
+ Slog.w(TAG, "<" + name + "> without stopped in " + permFile
+ + " at " + parser.getPositionDescription());
+ } else if (!Boolean.parseBoolean(stopped)) {
+ mInitialNonStoppedSystemPackages.add(pkgName);
+ }
+ }
default: {
Slog.w(TAG, "Tag " + name + " is unknown in "
+ permFile + " at " + parser.getPositionDescription());
diff --git a/services/core/java/com/android/server/TEST_MAPPING b/services/core/java/com/android/server/TEST_MAPPING
index ae65dcb..a4fd52d 100644
--- a/services/core/java/com/android/server/TEST_MAPPING
+++ b/services/core/java/com/android/server/TEST_MAPPING
@@ -39,6 +39,10 @@
}
],
"file_patterns": ["BinaryTransparencyService\\.java"]
+ },
+ {
+ "name": "BinaryTransparencyHostTest",
+ "file_patterns": ["BinaryTransparencyService\\.java"]
}
],
"presubmit-large": [
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index f0b168d..56d0b59 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -20,6 +20,9 @@
import static android.Manifest.permission.REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND;
import static android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND;
import static android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_BFSL;
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_TOP;
import static android.app.ActivityManager.PROCESS_STATE_HEAVY_WEIGHT;
import static android.app.ActivityManager.PROCESS_STATE_RECEIVER;
import static android.app.ActivityManager.PROCESS_STATE_TOP;
@@ -175,6 +178,8 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
+import android.service.voice.HotwordDetectionService;
+import android.service.voice.VisualQueryDetectionService;
import android.stats.devicepolicy.DevicePolicyEnums;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -3808,6 +3813,17 @@
inSharedIsolatedProcess);
}
+ // TODO(b/265746493): Special case for HotwordDetectionService and
+ // VisualQueryDetectionService. Need a cleaner way to append this seInfo.
+ private String generateAdditionalSeInfoFromService(Intent service) {
+ if (service != null && service.getAction() != null
+ && (service.getAction().equals(HotwordDetectionService.SERVICE_INTERFACE)
+ || service.getAction().equals(VisualQueryDetectionService.SERVICE_INTERFACE))) {
+ return ":isolatedComputeApp";
+ }
+ return "";
+ }
+
private ServiceLookupResult retrieveServiceLocked(Intent service,
String instanceName, boolean isSdkSandboxService, int sdkSandboxClientAppUid,
String sdkSandboxClientAppPackage, String resolvedType,
@@ -3925,6 +3941,7 @@
r.mRecentCallingPackage = callingPackage;
r.mRecentCallingUid = callingUid;
}
+ r.appInfo.seInfo += generateAdditionalSeInfoFromService(service);
return new ServiceLookupResult(r, resolution.getAlias());
}
@@ -4150,6 +4167,7 @@
return null;
}
}
+ r.appInfo.seInfo += generateAdditionalSeInfoFromService(service);
return new ServiceLookupResult(r, resolution.getAlias());
}
return null;
@@ -7380,6 +7398,12 @@
int ret = shouldAllowFgsStartForegroundNoBindingCheckLocked(allowWhileInUse, callingPid,
callingUid, callingPackage, r, backgroundStartPrivileges);
+ // If an app (App 1) is bound by another app (App 2) that could start an FGS, then App 1
+ // is also allowed to start an FGS. We check all the binding
+ // in canBindingClientStartFgsLocked() to do this check.
+ // (Note we won't check more than 1 level of binding.)
+ // [bookmark: 61867f60-007c-408c-a2c4-e19e96056135] -- this code is referred to from
+ // OomAdjuster.
String bindFromPackage = null;
if (ret == REASON_DENIED) {
bindFromPackage = canBindingClientStartFgsLocked(callingUid);
@@ -7395,10 +7419,13 @@
.getTargetSdkVersion(callingPackage);
} catch (PackageManager.NameNotFoundException ignored) {
}
+ final boolean uidBfsl = (mAm.getUidProcessCapabilityLocked(callingUid)
+ & PROCESS_CAPABILITY_BFSL) != 0;
final String debugInfo =
"[callingPackage: " + callingPackage
+ "; callingUid: " + callingUid
+ "; uidState: " + ProcessList.makeProcStateString(uidState)
+ + "; uidBFSL: " + (uidBfsl ? "[BFSL]" : "n/a")
+ "; intent: " + intent
+ "; code:" + reasonCodeToString(ret)
+ "; tempAllowListReason:<"
@@ -7437,11 +7464,15 @@
}
if (ret == REASON_DENIED) {
+ final boolean uidBfsl =
+ (mAm.getUidProcessCapabilityLocked(callingUid) & PROCESS_CAPABILITY_BFSL) != 0;
final Integer allowedType = mAm.mProcessList.searchEachLruProcessesLOSP(false, app -> {
if (app.uid == callingUid) {
final ProcessStateRecord state = app.mState;
- if (state.isAllowedStartFgs()) { // Procstate <= BFGS?
- return getReasonCodeFromProcState(state.getCurProcState());
+ final int procstate = state.getCurProcState();
+ if ((procstate <= PROCESS_STATE_BOUND_TOP)
+ || (uidBfsl && (procstate <= PROCESS_STATE_BOUND_FOREGROUND_SERVICE))) {
+ return getReasonCodeFromProcState(procstate);
} else {
final ActiveInstrumentation instr = app.getActiveInstrumentation();
if (instr != null
@@ -7669,6 +7700,8 @@
}
final int callerTargetSdkVersion = r.mRecentCallerApplicationInfo != null
? r.mRecentCallerApplicationInfo.targetSdkVersion : 0;
+
+ // TODO(short-service): Log BFSL too.
FrameworkStatsLog.write(FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED,
r.appInfo.uid,
r.shortInstanceName,
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index ef79351..cdc5d81 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -770,10 +770,6 @@
// initialized in the constructor.
public int CUR_MAX_EMPTY_PROCESSES;
- /** @see mEnforceReceiverExportedFlagRequirement */
- private static final String KEY_ENFORCE_RECEIVER_EXPORTED_FLAG_REQUIREMENT =
- "enforce_exported_flag_requirement";
-
/** @see #mNoKillCachedProcessesUntilBootCompleted */
private static final String KEY_NO_KILL_CACHED_PROCESSES_UNTIL_BOOT_COMPLETED =
"no_kill_cached_processes_until_boot_completed";
@@ -782,9 +778,6 @@
private static final String KEY_NO_KILL_CACHED_PROCESSES_POST_BOOT_COMPLETED_DURATION_MILLIS =
"no_kill_cached_processes_post_boot_completed_duration_millis";
- /** @see mEnforceReceiverExportedFlagRequirement */
- private static final boolean DEFAULT_ENFORCE_RECEIVER_EXPORTED_FLAG_REQUIREMENT = true;
-
/** @see #mNoKillCachedProcessesUntilBootCompleted */
private static final boolean DEFAULT_NO_KILL_CACHED_PROCESSES_UNTIL_BOOT_COMPLETED = true;
@@ -793,15 +786,6 @@
DEFAULT_NO_KILL_CACHED_PROCESSES_POST_BOOT_COMPLETED_DURATION_MILLIS = 600_000;
/**
- * If true, enforce the requirement that dynamically registered receivers specify one of
- * {@link android.content.Context#RECEIVER_EXPORTED} or
- * {@link android.content.Context#RECEIVER_NOT_EXPORTED} if registering for any non-system
- * broadcasts.
- */
- volatile boolean mEnforceReceiverExportedFlagRequirement =
- DEFAULT_ENFORCE_RECEIVER_EXPORTED_FLAG_REQUIREMENT;
-
- /**
* If true, do not kill excessive cached processes proactively, until user-0 is unlocked.
* @see #mNoKillCachedProcessesPostBootCompletedDurationMillis
*/
@@ -1112,9 +1096,6 @@
case KEY_NO_KILL_CACHED_PROCESSES_UNTIL_BOOT_COMPLETED:
updateNoKillCachedProcessesUntilBootCompleted();
break;
- case KEY_ENFORCE_RECEIVER_EXPORTED_FLAG_REQUIREMENT:
- updateEnforceReceiverExportedFlagRequirement();
- break;
case KEY_NO_KILL_CACHED_PROCESSES_POST_BOOT_COMPLETED_DURATION_MILLIS:
updateNoKillCachedProcessesPostBootCompletedDurationMillis();
break;
@@ -1616,13 +1597,6 @@
DEFAULT_DEFER_BOOT_COMPLETED_BROADCAST);
}
- private void updateEnforceReceiverExportedFlagRequirement() {
- mEnforceReceiverExportedFlagRequirement = DeviceConfig.getBoolean(
- DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
- KEY_ENFORCE_RECEIVER_EXPORTED_FLAG_REQUIREMENT,
- DEFAULT_ENFORCE_RECEIVER_EXPORTED_FLAG_REQUIREMENT);
- }
-
private void updateNoKillCachedProcessesUntilBootCompleted() {
mNoKillCachedProcessesUntilBootCompleted = DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
@@ -2002,8 +1976,6 @@
pw.print("="); pw.println(mPrioritizeAlarmBroadcasts);
pw.print(" "); pw.print(KEY_NO_KILL_CACHED_PROCESSES_UNTIL_BOOT_COMPLETED);
pw.print("="); pw.println(mNoKillCachedProcessesUntilBootCompleted);
- pw.print(" "); pw.print(KEY_ENFORCE_RECEIVER_EXPORTED_FLAG_REQUIREMENT);
- pw.print("="); pw.println(mEnforceReceiverExportedFlagRequirement);
pw.print(" "); pw.print(KEY_NO_KILL_CACHED_PROCESSES_POST_BOOT_COMPLETED_DURATION_MILLIS);
pw.print("="); pw.println(mNoKillCachedProcessesPostBootCompletedDurationMillis);
pw.print(" "); pw.print(KEY_MAX_EMPTY_TIME_MILLIS);
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 57a89e3..aad49c9 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -5874,6 +5874,11 @@
return mProcessList.getUidProcStateLOSP(uid);
}
+ @GuardedBy("this")
+ int getUidProcessCapabilityLocked(int uid) {
+ return mProcessList.getUidProcessCapabilityLOSP(uid);
+ }
+
// =========================================================
// PROCESS INFO
// =========================================================
@@ -13588,8 +13593,7 @@
// broadcasts, or the receiver is null (a sticky broadcast). Sticky broadcasts should
// not be used generally, so we will be marking them as exported by default
boolean requireExplicitFlagForDynamicReceivers = CompatChanges.isChangeEnabled(
- DYNAMIC_RECEIVER_EXPLICIT_EXPORT_REQUIRED, callingUid)
- && mConstants.mEnforceReceiverExportedFlagRequirement;
+ DYNAMIC_RECEIVER_EXPLICIT_EXPORT_REQUIRED, callingUid);
// STOPSHIP(b/259139792): Allow apps that are currently targeting U and in process of
// updating their receivers to be exempt from this requirement until their receivers
// are flagged.
@@ -18974,14 +18978,20 @@
return null;
}
- // Starts with all displays but DEFAULT_DISPLAY
- int[] displayIds = new int[allDisplays.length - 1];
+ boolean allowOnDefaultDisplay = UserManager
+ .isVisibleBackgroundUsersOnDefaultDisplayEnabled();
+ int displaysSize = allDisplays.length;
+ if (!allowOnDefaultDisplay) {
+ displaysSize--;
+ }
+ int[] displayIds = new int[displaysSize];
- // TODO(b/247592632): check for other properties like isSecure or proper display type
int numberValidDisplays = 0;
for (Display display : allDisplays) {
int displayId = display.getDisplayId();
- if (display.isValid() && displayId != Display.DEFAULT_DISPLAY) {
+ // TODO(b/247592632): check other properties like isSecure or proper display type
+ if (display.isValid()
+ && (allowOnDefaultDisplay || displayId != Display.DEFAULT_DISPLAY)) {
displayIds[numberValidDisplays++] = displayId;
}
}
@@ -18993,14 +19003,15 @@
// STOPSHIP: if not removed, it should at least be unit tested
String testingProp = "fw.display_ids_for_starting_users_for_testing_purposes";
int displayId = SystemProperties.getInt(testingProp, Display.DEFAULT_DISPLAY);
- if (displayId != Display.DEFAULT_DISPLAY && displayId > 0) {
- Slogf.w(TAG, "getSecondaryDisplayIdsForStartingBackgroundUsers(): no valid "
+ if (allowOnDefaultDisplay && displayId == Display.DEFAULT_DISPLAY
+ || displayId > 0) {
+ Slogf.w(TAG, "getDisplayIdsForStartingVisibleBackgroundUsers(): no valid "
+ "display found, but returning %d as set by property %s", displayId,
testingProp);
return new int[] { displayId };
}
- Slogf.e(TAG, "getDisplayIdsForStartingBackgroundUsers(): no valid display on %s",
- Arrays.toString(allDisplays));
+ Slogf.e(TAG, "getDisplayIdsForStartingVisibleBackgroundUsers(): no valid display"
+ + " on %s", Arrays.toString(allDisplays));
return null;
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 3ab1cd7..f855c7f 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -16,6 +16,10 @@
package com.android.server.am;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_CAMERA;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_LOCATION;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_MICROPHONE;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_NETWORK;
import static android.app.ActivityManagerInternal.ALLOW_NON_FULL;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.ActivityTaskManager.RESIZE_MODE_SYSTEM;
@@ -1823,16 +1827,20 @@
final InputStream mInput;
final int mUid;
+ final int mMask;
+
static final int STATE_NORMAL = 0;
int mState;
- MyUidObserver(ActivityManagerService service, PrintWriter pw, InputStream input, int uid) {
+ MyUidObserver(ActivityManagerService service, PrintWriter pw, InputStream input, int uid,
+ int mask) {
mInterface = service;
mInternal = service;
mPw = pw;
mInput = input;
mUid = uid;
+ mMask = mask;
}
@Override
@@ -1847,7 +1855,7 @@
mPw.print(" seq ");
mPw.print(procStateSeq);
mPw.print(" capability ");
- mPw.println(capability);
+ mPw.println(capability & mMask);
mPw.flush();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
@@ -1998,9 +2006,19 @@
int runWatchUids(PrintWriter pw) throws RemoteException {
String opt;
int uid = -1;
+
+ // Because a lot of CTS won't ignore capabilities newly added, we report
+ // only the following capabilities -- the ones we had on Android T -- by default.
+ int mask = PROCESS_CAPABILITY_FOREGROUND_LOCATION
+ | PROCESS_CAPABILITY_FOREGROUND_CAMERA
+ | PROCESS_CAPABILITY_FOREGROUND_MICROPHONE
+ | PROCESS_CAPABILITY_NETWORK;
+
while ((opt=getNextOption()) != null) {
if (opt.equals("--oom")) {
uid = Integer.parseInt(getNextArgRequired());
+ } else if (opt.equals("--mask")) {
+ mask = Integer.parseInt(getNextArgRequired());
} else {
getErrPrintWriter().println("Error: Unknown option: " + opt);
return -1;
@@ -2008,7 +2026,7 @@
}
}
- MyUidObserver controller = new MyUidObserver(mInternal, pw, getRawInputStream(), uid);
+ MyUidObserver controller = new MyUidObserver(mInternal, pw, getRawInputStream(), uid, mask);
controller.run();
return 0;
}
@@ -4079,9 +4097,14 @@
pw.println(" -p: only show events related to a specific process / package");
pw.println(" -s: simple mode, only show a summary line for each event");
pw.println(" -c: assume the input is always [c]ontinue");
- pw.println(" watch-uids [--oom <uid>]");
+ pw.println(" watch-uids [--oom <uid>] [--mask <capabilities integer>]");
pw.println(" Start watching for and reporting uid state changes.");
pw.println(" --oom: specify a uid for which to report detailed change messages.");
+ pw.println(" --mask: Specify PROCESS_CAPABILITY_XXX mask to report. ");
+ pw.println(" By default, it only reports FOREGROUND_LOCATION (1)");
+ pw.println(" FOREGROUND_CAMERA (2), FOREGROUND_MICROPHONE (4)");
+ pw.println(" and NETWORK (8). New capabilities added on or after");
+ pw.println(" Android UDC will not be reported by default.");
pw.println(" hang [--allow-restart]");
pw.println(" Hang the system.");
pw.println(" --allow-restart: allow watchdog to perform normal system restart");
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index f09622f..f73594c 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -2880,7 +2880,7 @@
checkinStats.setPowerProfileLocked(mPowerProfile);
checkinStats.readSummaryFromParcel(in);
in.recycle();
- checkinStats.dumpCheckinLocked(mContext, pw, apps, flags,
+ checkinStats.dumpCheckin(mContext, pw, apps, flags,
historyStart);
mStats.mCheckinFile.delete();
return;
@@ -2892,17 +2892,15 @@
}
}
}
- if (DBG) Slog.d(TAG, "begin dumpCheckinLocked from UID " + Binder.getCallingUid());
+ if (DBG) Slog.d(TAG, "begin dumpCheckin from UID " + Binder.getCallingUid());
awaitCompletion();
- synchronized (mStats) {
- mStats.dumpCheckinLocked(mContext, pw, apps, flags, historyStart);
- if (writeData) {
- mStats.writeAsyncLocked();
- }
+ mStats.dumpCheckin(mContext, pw, apps, flags, historyStart);
+ if (writeData) {
+ mStats.writeAsyncLocked();
}
- if (DBG) Slog.d(TAG, "end dumpCheckinLocked");
+ if (DBG) Slog.d(TAG, "end dumpCheckin");
} else {
- if (DBG) Slog.d(TAG, "begin dumpLocked from UID " + Binder.getCallingUid());
+ if (DBG) Slog.d(TAG, "begin dump from UID " + Binder.getCallingUid());
awaitCompletion();
mStats.dump(mContext, pw, flags, reqUid, historyStart);
@@ -2912,7 +2910,7 @@
pw.println();
mCpuWakeupStats.dump(new IndentingPrintWriter(pw, " "), SystemClock.elapsedRealtime());
- if (DBG) Slog.d(TAG, "end dumpLocked");
+ if (DBG) Slog.d(TAG, "end dump");
}
}
diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
index b1a01cc..7290f32 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
@@ -273,7 +273,8 @@
performReceiveLocked(oldRecord.resultToApp, oldRecord.resultTo,
oldRecord.intent,
Activity.RESULT_CANCELED, null, null,
- false, false, oldRecord.userId, oldRecord.callingUid, r.callingUid,
+ false, false, r.shareIdentity, oldRecord.userId,
+ oldRecord.callingUid, r.callingUid, r.callerPackage,
SystemClock.uptimeMillis() - oldRecord.enqueueTime, 0);
} catch (RemoteException e) {
Slog.w(TAG, "Failure ["
@@ -402,7 +403,9 @@
prepareReceiverIntent(r.intent, r.curFilteredExtras),
r.curReceiver, null /* compatInfo (unused but need to keep method signature) */,
r.resultCode, r.resultData, r.resultExtras, r.ordered, assumeDelivered,
- r.userId, app.mState.getReportedProcState()));
+ r.userId, r.shareIdentity ? r.callingUid : Process.INVALID_UID,
+ r.shareIdentity ? r.callerPackage : null,
+ app.mState.getReportedProcState()));
if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST,
"Process cur broadcast " + r + " DELIVERED for app " + app);
started = true;
@@ -726,9 +729,15 @@
public void performReceiveLocked(ProcessRecord app, IIntentReceiver receiver,
Intent intent, int resultCode, String data, Bundle extras,
- boolean ordered, boolean sticky, int sendingUser,
- int receiverUid, int callingUid, long dispatchDelay,
- long receiveDelay) throws RemoteException {
+ boolean ordered, boolean sticky, boolean shareIdentity, int sendingUser,
+ int receiverUid, int callingUid, String callingPackage,
+ long dispatchDelay, long receiveDelay) throws RemoteException {
+ // If the broadcaster opted-in to sharing their identity, then expose package visibility for
+ // the receiver.
+ if (shareIdentity) {
+ mService.mPackageManagerInt.grantImplicitAccess(sendingUser, intent,
+ UserHandle.getAppId(receiverUid), callingUid, true);
+ }
// Send the intent to the receiver asynchronously using one-way binder calls.
if (app != null) {
final IApplicationThread thread = app.getThread();
@@ -740,6 +749,8 @@
thread.scheduleReceiverList(mReceiverBatch.registeredReceiver(
receiver, intent, resultCode,
data, extras, ordered, sticky, assumeDelivered, sendingUser,
+ shareIdentity ? callingUid : Process.INVALID_UID,
+ shareIdentity ? callingPackage : null,
app.mState.getReportedProcState()));
} catch (RemoteException ex) {
// Failed to call into the process. It's either dying or wedged. Kill it gently.
@@ -845,8 +856,8 @@
maybeReportBroadcastDispatchedEventLocked(r, filter.owningUid);
performReceiveLocked(filter.receiverList.app, filter.receiverList.receiver,
prepareReceiverIntent(r.intent, filteredExtras), r.resultCode, r.resultData,
- r.resultExtras, r.ordered, r.initialSticky, r.userId,
- filter.receiverList.uid, r.callingUid,
+ r.resultExtras, r.ordered, r.initialSticky, r.shareIdentity, r.userId,
+ filter.receiverList.uid, r.callingUid, r.callerPackage,
r.dispatchTime - r.enqueueTime,
r.receiverTime - r.dispatchTime);
// parallel broadcasts are fire-and-forget, not bookended by a call to
@@ -1130,8 +1141,8 @@
r.mIsReceiverAppRunning = true;
performReceiveLocked(r.resultToApp, r.resultTo,
new Intent(r.intent), r.resultCode,
- r.resultData, r.resultExtras, false, false, r.userId,
- r.callingUid, r.callingUid,
+ r.resultData, r.resultExtras, false, false, r.shareIdentity,
+ r.userId, r.callingUid, r.callingUid, r.callerPackage,
r.dispatchTime - r.enqueueTime,
now - r.dispatchTime);
logBootCompletedBroadcastCompletionLatencyIfPossible(r);
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index 99e2ac7..b952ce0 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -893,6 +893,8 @@
batch.schedule(((BroadcastFilter) receiver).receiverList.receiver,
receiverIntent, r.resultCode, r.resultData, r.resultExtras,
r.ordered, r.initialSticky, assumeDelivered, r.userId,
+ r.shareIdentity ? r.callingUid : Process.INVALID_UID,
+ r.shareIdentity ? r.callerPackage : null,
app.mState.getReportedProcState(), r, index);
// TODO: consider making registered receivers of unordered
// broadcasts report results to detect ANRs
@@ -903,7 +905,9 @@
} else {
batch.schedule(receiverIntent, ((ResolveInfo) receiver).activityInfo,
null, r.resultCode, r.resultData, r.resultExtras, r.ordered, assumeDelivered,
- r.userId, app.mState.getReportedProcState(), r, index);
+ r.userId, r.shareIdentity ? r.callingUid : Process.INVALID_UID,
+ r.shareIdentity ? r.callerPackage : null,
+ app.mState.getReportedProcState(), r, index);
if (assumeDelivered) {
batch.success(r, index, BroadcastRecord.DELIVERY_DELIVERED, "assuming delivered");
return true;
@@ -946,6 +950,11 @@
final BroadcastRecord r = cookie.r;
final int index = cookie.index;
final Object receiver = r.receivers.get(index);
+
+ if (r.shareIdentity) {
+ mService.mPackageManagerInt.grantImplicitAccess(r.userId, r.intent,
+ UserHandle.getAppId(app.uid), r.callingUid, true);
+ }
if (receiver instanceof BroadcastFilter) {
notifyScheduleRegisteredReceiver(queue.app, r, (BroadcastFilter) receiver);
} else {
@@ -1055,12 +1064,19 @@
if (thread != null) {
mService.mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily(
app, OOM_ADJ_REASON_FINISH_RECEIVER);
+ if (r.shareIdentity) {
+ mService.mPackageManagerInt.grantImplicitAccess(r.userId, r.intent,
+ UserHandle.getAppId(app.uid), r.callingUid, true);
+ }
try {
final boolean assumeDelivered = true;
thread.scheduleReceiverList(mReceiverBatch.registeredReceiver(
r.resultTo, r.intent,
r.resultCode, r.resultData, r.resultExtras, false, r.initialSticky,
- assumeDelivered, r.userId, app.mState.getReportedProcState()));
+ assumeDelivered, r.userId,
+ r.shareIdentity ? r.callingUid : Process.INVALID_UID,
+ r.shareIdentity ? r.callerPackage : null,
+ app.mState.getReportedProcState()));
} catch (RemoteException e) {
final String msg = "Failed to schedule result of " + r + " via " + app + ": " + e;
logw(msg);
diff --git a/services/core/java/com/android/server/am/BroadcastReceiverBatch.java b/services/core/java/com/android/server/am/BroadcastReceiverBatch.java
index 226647c..153403a 100644
--- a/services/core/java/com/android/server/am/BroadcastReceiverBatch.java
+++ b/services/core/java/com/android/server/am/BroadcastReceiverBatch.java
@@ -171,8 +171,8 @@
// Add a ReceiverInfo for a registered receiver.
void schedule(@Nullable IIntentReceiver receiver, Intent intent,
int resultCode, @Nullable String data, @Nullable Bundle extras, boolean ordered,
- boolean sticky, boolean assumeDelivered, int sendingUser, int processState,
- @Nullable BroadcastRecord r, int index) {
+ boolean sticky, boolean assumeDelivered, int sendingUser, int callingUid,
+ String callingPackage, int processState, @Nullable BroadcastRecord r, int index) {
ReceiverInfo ri = new ReceiverInfo();
ri.intent = intent;
ri.data = data;
@@ -185,6 +185,9 @@
ri.receiver = receiver;
ri.ordered = ordered;
ri.sticky = sticky;
+ ri.sentFromUid = callingUid;
+ ri.sentFromPackage = callingPackage;
+
mReceivers.add(ri);
mCookies.add(cookiePool.next().set(r, index));
}
@@ -192,7 +195,8 @@
void schedule(@Nullable Intent intent, @Nullable ActivityInfo activityInfo,
@Nullable CompatibilityInfo compatInfo, int resultCode, @Nullable String data,
@Nullable Bundle extras, boolean sync, boolean assumeDelivered, int sendingUser,
- int processState, @Nullable BroadcastRecord r, int index) {
+ int callingUid, @Nullable String callingPackage, int processState,
+ @Nullable BroadcastRecord r, int index) {
ReceiverInfo ri = new ReceiverInfo();
ri.intent = intent;
ri.data = data;
@@ -205,6 +209,8 @@
ri.activityInfo = activityInfo;
ri.compatInfo = compatInfo;
ri.sync = sync;
+ ri.sentFromUid = callingUid;
+ ri.sentFromPackage = callingPackage;
mReceivers.add(ri);
mCookies.add(cookiePool.next().set(r, index));
}
@@ -217,20 +223,21 @@
ArrayList<ReceiverInfo> registeredReceiver(@Nullable IIntentReceiver receiver,
@Nullable Intent intent, int resultCode, @Nullable String data,
@Nullable Bundle extras, boolean ordered, boolean sticky, boolean assumeDelivered,
- int sendingUser, int processState) {
+ int sendingUser, int callingUid, String callingPackage, int processState) {
reset();
schedule(receiver, intent, resultCode, data, extras, ordered, sticky, assumeDelivered,
- sendingUser, processState, null, 0);
+ sendingUser, callingUid, callingPackage, processState, null, 0);
return receivers();
}
ArrayList<ReceiverInfo> manifestReceiver(@Nullable Intent intent,
@Nullable ActivityInfo activityInfo, @Nullable CompatibilityInfo compatInfo,
int resultCode, @Nullable String data, @Nullable Bundle extras, boolean sync,
- boolean assumeDelivered, int sendingUser, int processState) {
+ boolean assumeDelivered, int sendingUser, int callingUid, String callingPackage,
+ int processState) {
reset();
schedule(intent, activityInfo, compatInfo, resultCode, data, extras, sync, assumeDelivered,
- sendingUser, processState, null, 0);
+ sendingUser, callingUid, callingPackage, processState, null, 0);
return receivers();
}
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index 4304377..6035ad9 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -89,6 +89,7 @@
final boolean initialSticky; // initial broadcast from register to sticky?
final boolean prioritized; // contains more than one priority tranche
final boolean deferUntilActive; // infinitely deferrable broadcast
+ final boolean shareIdentity; // whether the broadcaster's identity should be shared
final int userId; // user id this broadcast was for
final @Nullable String resolvedType; // the resolved data type
final @Nullable String[] requiredPermissions; // permissions the caller has required
@@ -417,6 +418,7 @@
pushMessage = options != null && options.isPushMessagingBroadcast();
pushMessageOverQuota = options != null && options.isPushMessagingOverQuotaBroadcast();
interactive = options != null && options.isInteractive();
+ shareIdentity = options != null && options.isShareIdentityEnabled();
this.filterExtrasForReceiver = filterExtrasForReceiver;
}
@@ -481,6 +483,7 @@
pushMessage = from.pushMessage;
pushMessageOverQuota = from.pushMessageOverQuota;
interactive = from.interactive;
+ shareIdentity = from.shareIdentity;
filterExtrasForReceiver = from.filterExtrasForReceiver;
}
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 2689193..913f151 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -742,9 +742,6 @@
"compactApp " + app.mOptRecord.getReqCompactSource().name() + " "
+ app.mOptRecord.getReqCompactProfile().name() + " " + processName);
}
- Trace.instantForTrack(Trace.TRACE_TAG_ACTIVITY_MANAGER, ATRACE_COMPACTION_TRACK,
- "compactApp " + app.mOptRecord.getReqCompactSource().name() + " "
- + app.mOptRecord.getReqCompactProfile().name() + " " + processName);
app.mOptRecord.setHasPendingCompact(true);
app.mOptRecord.setForceCompact(force);
mPendingCompactionProcesses.add(app);
@@ -1820,7 +1817,8 @@
try {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
"Compact " + resolvedAction.name() + ": " + name
- + " lastOomAdjReason: " + oomAdjReason);
+ + " lastOomAdjReason: " + oomAdjReason
+ + " source: " + compactSource.name());
long zramUsedKbBefore = getUsedZramMemory();
long startCpuTime = threadCpuTimeNs();
mProcessDependencies.performCompaction(resolvedAction, pid);
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index e02dda6..8591973 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -18,6 +18,7 @@
import static android.app.ActivityManager.PROCESS_CAPABILITY_ALL;
import static android.app.ActivityManager.PROCESS_CAPABILITY_ALL_IMPLICIT;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_BFSL;
import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_CAMERA;
import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_LOCATION;
import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_MICROPHONE;
@@ -1706,7 +1707,7 @@
state.setCurRawAdj(state.getMaxAdj());
state.setHasForegroundActivities(false);
state.setCurrentSchedulingGroup(SCHED_GROUP_DEFAULT);
- state.setCurCapability(PROCESS_CAPABILITY_ALL);
+ state.setCurCapability(PROCESS_CAPABILITY_ALL); // BFSL allowed
state.setCurProcState(ActivityManager.PROCESS_STATE_PERSISTENT);
// System processes can do UI, and when they do we want to have
// them trim their memory after the user leaves the UI. To
@@ -1788,6 +1789,7 @@
schedGroup = SCHED_GROUP_DEFAULT;
state.setAdjType("instrumentation");
procState = PROCESS_STATE_FOREGROUND_SERVICE;
+ capability |= PROCESS_CAPABILITY_BFSL;
if (DEBUG_OOM_ADJ_REASON || logUid == appUid) {
reportOomAdjMessageLocked(TAG_OOM_ADJ, "Making instrumentation: " + app);
}
@@ -1880,37 +1882,28 @@
adjType = "fg-service";
newAdj = PERCEPTIBLE_APP_ADJ;
newProcState = PROCESS_STATE_FOREGROUND_SERVICE;
+ capabilityFromFGS |= PROCESS_CAPABILITY_BFSL;
+
+ } else if (hasShortForegroundServices) {
+
+ // For short FGS.
+ adjType = "fg-service-short";
+
+ // We use MEDIUM_APP_ADJ + 1 so we can tell apart EJ
+ // (which uses MEDIUM_APP_ADJ + 1)
+ // from short-FGS.
+ // (We use +1 and +2, not +0 and +1, to be consistent with the following
+ // RECENT_FOREGROUND_APP_ADJ tweak)
+ newAdj = PERCEPTIBLE_MEDIUM_APP_ADJ + 1;
+
+ // We give the FGS procstate, but not PROCESS_CAPABILITY_BFSL, so
+ // short-fgs can't start FGS from the background.
+ newProcState = PROCESS_STATE_FOREGROUND_SERVICE;
} else if (state.hasOverlayUi()) {
adjType = "has-overlay-ui";
newAdj = PERCEPTIBLE_APP_ADJ;
newProcState = PROCESS_STATE_IMPORTANT_FOREGROUND;
-
- } else if (hasForegroundServices) {
- // If we get here, hasNonShortForegroundServices() must be false.
-
- // TODO(short-service): Proactively run OomAjudster when the grace period finish.
- if (!hasShortForegroundServices) {
- // All the short-FGSes within this process are timed out. Don't promote to FGS.
- // TODO(short-service): Should we set some unique oom-adj to make it detectable,
- // in a long trace?
- } else {
- // For short FGS.
- adjType = "fg-service-short";
- // We use MEDIUM_APP_ADJ + 1 so we can tell apart EJ
- // (which uses MEDIUM_APP_ADJ + 1)
- // from short-FGS.
- // (We use +1 and +2, not +0 and +1, to be consistent with the following
- // RECENT_FOREGROUND_APP_ADJ tweak)
- newAdj = PERCEPTIBLE_MEDIUM_APP_ADJ + 1;
-
- // Short-FGS gets a below-BFGS procstate, so it can't start another FGS from it.
- newProcState = PROCESS_STATE_IMPORTANT_FOREGROUND;
-
- // Same as EJ, we explicitly grant network access to short FGS,
- // even when battery saver or data saver is enabled.
- capabilityFromFGS |= PROCESS_CAPABILITY_NETWORK;
- }
}
if (adjType != null) {
@@ -2220,6 +2213,11 @@
app.mOptRecord.setShouldNotFreeze(true);
}
+ // We always propagate PROCESS_CAPABILITY_BFSL over bindings here,
+ // but, right before actually setting it to the process,
+ // we check the final procstate, and remove it if the procsate is below BFGS.
+ capability |= getBfslCapabilityFromClient(client);
+
if ((cr.flags & Context.BIND_WAIVE_PRIORITY) == 0) {
if (cr.hasFlag(Context.BIND_INCLUDE_CAPABILITIES)) {
capability |= cstate.getCurCapability();
@@ -2319,6 +2317,14 @@
&& adj >= (lbAdj = PERCEPTIBLE_LOW_APP_ADJ)) {
newAdj = PERCEPTIBLE_LOW_APP_ADJ;
} else if ((cr.flags & Context.BIND_ALMOST_PERCEPTIBLE) != 0
+ && (cr.flags & Context.BIND_NOT_FOREGROUND) == 0
+ && clientAdj < PERCEPTIBLE_APP_ADJ
+ && adj >= (lbAdj = PERCEPTIBLE_APP_ADJ)) {
+ // This is for user-initiated jobs.
+ // We use APP_ADJ + 1 here, so we can tell them apart from FGS.
+ newAdj = PERCEPTIBLE_APP_ADJ + 1;
+ } else if ((cr.flags & Context.BIND_ALMOST_PERCEPTIBLE) != 0
+ && (cr.flags & Context.BIND_NOT_FOREGROUND) != 0
&& clientAdj < PERCEPTIBLE_APP_ADJ
&& adj >= (lbAdj = (PERCEPTIBLE_MEDIUM_APP_ADJ + 2))) {
// This is for expedited jobs.
@@ -2532,6 +2538,11 @@
int clientAdj = cstate.getCurRawAdj();
int clientProcState = cstate.getCurRawProcState();
+ // We always propagate PROCESS_CAPABILITY_BFSL to providers here,
+ // but, right before actually setting it to the process,
+ // we check the final procstate, and remove it if the procsate is below BFGS.
+ capability |= getBfslCapabilityFromClient(client);
+
if (clientProcState >= PROCESS_STATE_CACHED_ACTIVITY) {
// If the other app is cached for any reason, for purposes here
// we are going to consider it empty.
@@ -2710,6 +2721,11 @@
capability |= getDefaultCapability(app, procState);
+ // Procstates below BFGS should never have this capability.
+ if (procState > PROCESS_STATE_BOUND_FOREGROUND_SERVICE) {
+ capability &= ~PROCESS_CAPABILITY_BFSL;
+ }
+
// Do final modification to adj. Everything we do between here and applying
// the final setAdj must be done in this function, because we will also use
// it when computing the final cached adj later. Note that we don't need to
@@ -2735,9 +2751,9 @@
case PROCESS_STATE_PERSISTENT:
case PROCESS_STATE_PERSISTENT_UI:
case PROCESS_STATE_TOP:
- return PROCESS_CAPABILITY_ALL;
+ return PROCESS_CAPABILITY_ALL; // BFSL allowed
case PROCESS_STATE_BOUND_TOP:
- return PROCESS_CAPABILITY_NETWORK;
+ return PROCESS_CAPABILITY_NETWORK | PROCESS_CAPABILITY_BFSL;
case PROCESS_STATE_FOREGROUND_SERVICE:
if (app.getActiveInstrumentation() != null) {
return PROCESS_CAPABILITY_ALL_IMPLICIT | PROCESS_CAPABILITY_NETWORK ;
@@ -2755,6 +2771,53 @@
}
/**
+ * @return the BFSL capability from a client (of a service binding or provider).
+ */
+ int getBfslCapabilityFromClient(ProcessRecord client) {
+ // Procstates above FGS should always have this flag. We shouldn't need this logic,
+ // but let's do it just in case.
+ if (client.mState.getCurProcState() < PROCESS_STATE_FOREGROUND_SERVICE) {
+ return PROCESS_CAPABILITY_BFSL;
+ }
+ // Otherwise, use the process's cur capability.
+
+ // Note: BFSL is a per-UID check, not per-process, but here, the BFSL capability is still
+ // propagated on a per-process basis.
+ //
+ // For example, consider this case:
+ // - There are App 1 and App 2.
+ // - App 1 has two processes
+ // Proc #1A, procstate BFGS with CAPABILITY_BFSL
+ // Proc #1B, procstate FGS with no CAPABILITY_BFSL (i.e. process has a short FGS)
+ // And this process binds to Proc #2 of App 2.
+ //
+ // (Note because #1A has CAPABILITY_BFSL, App 1's UidRecord has CAPABILITY_BFSL.)
+ //
+ // - App 2 has one process:
+ // Proc #2, procstate FGS due to the above binding, _with no CAPABILITY_BFSL_.
+ //
+ // In this case, #2 will not get CAPABILITY_BFSL because the binding client (#1B)
+ // doesn't have this capability. (Even though App 1's UidRecord has it.)
+ //
+ // This may look weird, because App 2 _is_ still BFSL allowed, because "it's bound by
+ // an app that is BFSL-allowed". (See [bookmark: 61867f60-007c-408c-a2c4-e19e96056135]
+ // in ActiveServices.)
+ //
+ // So why don't we propagate PROCESS_CAPABILITY_BFSL from App 1's UID record?
+ // This is because short-FGS acts like "below BFGS" as far as BFSL is concerned,
+ // similar to how JobScheduler jobs are below BFGS and apps can't start FGS from there.
+ //
+ // If #1B was running a job instead of a short-FGS, then its procstate would be below BFGS.
+ // Then #2's procstate would also be below BFGS. So #2 wouldn't get CAPABILITY_BFSL.
+ // Similarly, if #1B has a short FGS, even though the procstate of #1B and #2 would be FGS,
+ // they both still wouldn't get CAPABILITY_BFSL.
+ //
+ // However, again, because #2 is bound by App 1, which is BFSL-allowed (because of #1A)
+ // App 2 would still BFSL-allowed, due to the aforementioned check in ActiveServices.
+ return client.mState.getCurCapability() & PROCESS_CAPABILITY_BFSL;
+ }
+
+ /**
* Checks if for the given app and client, there's a cycle that should skip over the client
* for now or use partial values to evaluate the effect of the client binding.
* @param app
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 04c0d64..2b2de81e 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -3888,7 +3888,7 @@
return runList;
}
- @GuardedBy(anyOf = {"mService", "mProfileLock"})
+ @GuardedBy(anyOf = {"mService", "mProcLock"})
int getLruSizeLOSP() {
return mLruProcesses.size();
}
@@ -3896,7 +3896,7 @@
/**
* Return the reference to the LRU list, call this function for read-only access
*/
- @GuardedBy(anyOf = {"mService", "mProfileLock"})
+ @GuardedBy(anyOf = {"mService", "mProcLock"})
ArrayList<ProcessRecord> getLruProcessesLOSP() {
return mLruProcesses;
}
@@ -3904,7 +3904,7 @@
/**
* Return the reference to the LRU list, call this function for read/write access
*/
- @GuardedBy({"mService", "mProfileLock"})
+ @GuardedBy({"mService", "mProcLock"})
ArrayList<ProcessRecord> getLruProcessesLSP() {
return mLruProcesses;
}
@@ -3913,12 +3913,12 @@
* For test only
*/
@VisibleForTesting
- @GuardedBy({"mService", "mProfileLock"})
+ @GuardedBy({"mService", "mProcLock"})
void setLruProcessServiceStartLSP(int pos) {
mLruProcessServiceStart = pos;
}
- @GuardedBy(anyOf = {"mService", "mProfileLock"})
+ @GuardedBy(anyOf = {"mService", "mProcLock"})
int getLruProcessServiceStartLOSP() {
return mLruProcessServiceStart;
}
@@ -3931,7 +3931,7 @@
* to most recent used ProcessRecord.
* @param callback The callback interface to accept the current ProcessRecord.
*/
- @GuardedBy(anyOf = {"mService", "mProfileLock"})
+ @GuardedBy(anyOf = {"mService", "mProcLock"})
void forEachLruProcessesLOSP(boolean iterateForward,
@NonNull Consumer<ProcessRecord> callback) {
if (iterateForward) {
@@ -3956,7 +3956,7 @@
* a non-null object, the search will be halted and this object will be used
* as the return value of this search function.
*/
- @GuardedBy(anyOf = {"mService", "mProfileLock"})
+ @GuardedBy(anyOf = {"mService", "mProcLock"})
<R> R searchEachLruProcessesLOSP(boolean iterateForward,
@NonNull Function<ProcessRecord, R> callback) {
if (iterateForward) {
@@ -3977,17 +3977,17 @@
return null;
}
- @GuardedBy(anyOf = {"mService", "mProfileLock"})
+ @GuardedBy(anyOf = {"mService", "mProcLock"})
boolean isInLruListLOSP(ProcessRecord app) {
return mLruProcesses.contains(app);
}
- @GuardedBy(anyOf = {"mService", "mProfileLock"})
+ @GuardedBy(anyOf = {"mService", "mProcLock"})
int getLruSeqLOSP() {
return mLruSeq;
}
- @GuardedBy(anyOf = {"mService", "mProfileLock"})
+ @GuardedBy(anyOf = {"mService", "mProcLock"})
MyProcessMap getProcessNamesLOSP() {
return mProcessNames;
}
diff --git a/services/core/java/com/android/server/am/ProcessStateRecord.java b/services/core/java/com/android/server/am/ProcessStateRecord.java
index 2ad2077..71d5d39 100644
--- a/services/core/java/com/android/server/am/ProcessStateRecord.java
+++ b/services/core/java/com/android/server/am/ProcessStateRecord.java
@@ -17,7 +17,6 @@
package com.android.server.am;
import static android.app.ActivityManager.PROCESS_CAPABILITY_NONE;
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_OOM_ADJ;
@@ -1180,11 +1179,6 @@
}
@GuardedBy("mService")
- boolean isAllowedStartFgs() {
- return mCurProcState <= PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
- }
-
- @GuardedBy("mService")
boolean isBackgroundRestricted() {
return mBackgroundRestricted;
}
diff --git a/services/core/java/com/android/server/am/SameProcessApplicationThread.java b/services/core/java/com/android/server/am/SameProcessApplicationThread.java
index 082e8e0..dcb02ea 100644
--- a/services/core/java/com/android/server/am/SameProcessApplicationThread.java
+++ b/services/core/java/com/android/server/am/SameProcessApplicationThread.java
@@ -48,11 +48,12 @@
@Override
public void scheduleReceiver(Intent intent, ActivityInfo info, CompatibilityInfo compatInfo,
int resultCode, String data, Bundle extras, boolean ordered, boolean assumeDelivered,
- int sendingUser, int processState) {
+ int sendingUser, int processState, int sentFromUid, String sentFromPackage) {
mHandler.post(() -> {
try {
mWrapped.scheduleReceiver(intent, info, compatInfo, resultCode, data, extras,
- ordered, assumeDelivered, sendingUser, processState);
+ ordered, assumeDelivered, sendingUser, processState, sentFromUid,
+ sentFromPackage);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
@@ -62,11 +63,12 @@
@Override
public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent, int resultCode,
String data, Bundle extras, boolean ordered, boolean sticky, boolean assumeDelivered,
- int sendingUser, int processState) {
+ int sendingUser, int processState, int sentFromUid, String sentFromPackage) {
mHandler.post(() -> {
try {
mWrapped.scheduleRegisteredReceiver(receiver, intent, resultCode, data, extras,
- ordered, sticky, assumeDelivered, sendingUser, processState);
+ ordered, sticky, assumeDelivered, sendingUser, processState, sentFromUid,
+ sentFromPackage);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
@@ -80,11 +82,11 @@
if (r.registered) {
scheduleRegisteredReceiver(r.receiver, r.intent,
r.resultCode, r.data, r.extras, r.ordered, r.sticky, r.assumeDelivered,
- r.sendingUser, r.processState);
+ r.sendingUser, r.processState, r.sentFromUid, r.sentFromPackage);
} else {
scheduleReceiver(r.intent, r.activityInfo, r.compatInfo,
r.resultCode, r.data, r.extras, r.sync, r.assumeDelivered,
- r.sendingUser, r.processState);
+ r.sendingUser, r.processState, r.sentFromUid, r.sentFromPackage);
}
}
}
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index f22624c..60a7f93 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -80,6 +80,7 @@
@VisibleForTesting
static final String[] sDeviceConfigScopes = new String[] {
DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
+ DeviceConfig.NAMESPACE_CAMERA_NATIVE,
DeviceConfig.NAMESPACE_CONFIGURATION,
DeviceConfig.NAMESPACE_CONNECTIVITY,
DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT,
@@ -103,6 +104,7 @@
DeviceConfig.NAMESPACE_VENDOR_SYSTEM_NATIVE_BOOT,
DeviceConfig.NAMESPACE_VIRTUALIZATION_FRAMEWORK_NATIVE,
DeviceConfig.NAMESPACE_WINDOW_MANAGER_NATIVE_BOOT,
+ DeviceConfig.NAMESPACE_MEMORY_SAFETY_NATIVE_BOOT,
DeviceConfig.NAMESPACE_MEMORY_SAFETY_NATIVE,
DeviceConfig.NAMESPACE_HDMI_CONTROL
};
diff --git a/services/core/java/com/android/server/am/UidRecord.java b/services/core/java/com/android/server/am/UidRecord.java
index b617582..bfc022b 100644
--- a/services/core/java/com/android/server/am/UidRecord.java
+++ b/services/core/java/com/android/server/am/UidRecord.java
@@ -443,6 +443,8 @@
sb.append(",");
sb.append(lastNetworkUpdatedProcStateSeq);
sb.append(")}");
+ sb.append(" caps=");
+ ActivityManager.printCapabilitiesSummary(sb, mCurCapability);
return sb.toString();
}
}
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index b2e4740..0a019f3 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -2246,7 +2246,7 @@
// If there is no challenge set, dismiss the keyguard right away
if (isUserSwitchUiEnabled && !mInjector.getKeyguardManager().isDeviceSecure(newUserId)) {
// Wait until the keyguard is dismissed to unfreeze
- mInjector.dismissKeyguard(runnable, "User Switch");
+ mInjector.dismissKeyguard(runnable);
} else {
runnable.run();
}
@@ -3714,7 +3714,7 @@
return IStorageManager.Stub.asInterface(ServiceManager.getService("mount"));
}
- protected void dismissKeyguard(Runnable runnable, String reason) {
+ protected void dismissKeyguard(Runnable runnable) {
final AtomicBoolean isFirst = new AtomicBoolean(true);
final Runnable runOnce = () -> {
if (isFirst.getAndSet(false)) {
@@ -3738,7 +3738,7 @@
public void onDismissCancelled() throws RemoteException {
mHandler.post(runOnce);
}
- }, reason);
+ }, /* message= */ null);
}
boolean isUsersOnSecondaryDisplaysEnabled() {
diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java
index ef0de18..704b425 100644
--- a/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java
+++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java
@@ -106,6 +106,21 @@
}
@Override
+ public SparseIntArray getNonDefaultPackageModes(String packageName, int userId) {
+ synchronized (mLock) {
+ ArrayMap<String, SparseIntArray> packageModes = mUserPackageModes.get(userId);
+ if (packageModes == null) {
+ return new SparseIntArray();
+ }
+ SparseIntArray opModes = packageModes.get(packageName);
+ if (opModes == null) {
+ return new SparseIntArray();
+ }
+ return opModes.clone();
+ }
+ }
+
+ @Override
public int getUidMode(int uid, int op) {
synchronized (mLock) {
SparseIntArray opModes = mUidModes.get(uid, null);
diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java
index d8d0d48..9a564fc 100644
--- a/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java
+++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java
@@ -39,6 +39,15 @@
SparseIntArray getNonDefaultUidModes(int uid);
/**
+ * Returns a copy of non-default app-ops with op as keys and their modes as values for a package
+ * and user.
+ * Returns an empty SparseIntArray if nothing is set.
+ * @param packageName for which we need the app-ops and their modes.
+ * @param userId for which the package is installed in.
+ */
+ SparseIntArray getNonDefaultPackageModes(String packageName, int userId);
+
+ /**
* Returns the app-op mode for a particular app-op of a uid.
* Returns default op mode if the op mode for particular uid and op is not set.
* @param uid user id for which we need the mode.
diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceLoggingDecorator.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceLoggingDecorator.java
index ac479b2..b8326ad 100644
--- a/services/core/java/com/android/server/appop/AppOpsCheckingServiceLoggingDecorator.java
+++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceLoggingDecorator.java
@@ -46,6 +46,13 @@
}
@Override
+ public SparseIntArray getNonDefaultPackageModes(String packageName, int userId) {
+ Log.i(LOG_TAG, "getNonDefaultPackageModes("
+ + "packageName = " + packageName + ", userId = " + userId + ") ");
+ return mService.getNonDefaultPackageModes(packageName, userId);
+ }
+
+ @Override
public int getUidMode(int uid, int op) {
Log.i(LOG_TAG, "getUidMode(uid = " + uid + ", op = " + op + ")");
return mService.getUidMode(uid, op);
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index c50f2b7..af6470f 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -98,6 +98,7 @@
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.PermissionInfo;
+import android.content.pm.UserInfo;
import android.database.ContentObserver;
import android.hardware.camera2.CameraDevice.CAMERA_AUDIO_RESTRICTION;
import android.net.Uri;
@@ -162,6 +163,7 @@
import com.android.server.pm.permission.PermissionManagerServiceInternal;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.PackageState;
+import com.android.server.pm.pkg.PackageStateInternal;
import com.android.server.pm.pkg.component.ParsedAttribution;
import com.android.server.policy.AppOpsPolicy;
@@ -383,6 +385,9 @@
/** Package Manager internal. Access via {@link #getPackageManagerInternal()} */
private @Nullable PackageManagerInternal mPackageManagerInternal;
+ /** User Manager internal. Access via {@link #getUserManagerInternal()} */
+ private @Nullable UserManagerInternal mUserManagerInternal;
+
/** Interface for app-op modes.*/
@VisibleForTesting
AppOpsCheckingServiceInterface mAppOpsCheckingService;
@@ -525,22 +530,6 @@
pkgOps = null;
}
- public boolean isDefault() {
- boolean areAllPackageModesDefault = true;
- if (pkgOps != null) {
- for (String packageName : pkgOps.keySet()) {
- if (!mAppOpsCheckingService.arePackageModesDefault(packageName,
- UserHandle.getUserId(uid))) {
- areAllPackageModesDefault = false;
- break;
- }
- }
- }
- return (pkgOps == null || pkgOps.isEmpty())
- && mAppOpsCheckingService.areUidModesDefault(uid)
- && areAllPackageModesDefault;
- }
-
// Functions for uid mode access and manipulation.
public SparseIntArray getNonDefaultUidModes() {
return mAppOpsCheckingService.getNonDefaultUidModes(uid);
@@ -1076,6 +1065,17 @@
synchronized (this) {
upgradeLocked(mVersionAtBoot);
}
+ initializeUidStates();
+
+ getUserManagerInternal().addUserLifecycleListener(
+ new UserManagerInternal.UserLifecycleListener() {
+ @Override
+ public void onUserCreated(UserInfo user, Object token) {
+ initializeUserUidStates(user.id);
+ }
+
+ // onUserRemoved handled by #removeUser
+ });
mConstants.startMonitoring(mContext.getContentResolver());
mHistoricalRegistry.systemReady(mContext.getContentResolver());
@@ -1203,6 +1203,49 @@
}
/**
+ * Initialize uid state objects for state contained in the checking service.
+ */
+ private void initializeUidStates() {
+ UserManagerInternal umi = getUserManagerInternal();
+ int[] userIds = umi.getUserIds();
+ synchronized (this) {
+ for (int i = 0; i < userIds.length; i++) {
+ int userId = userIds[i];
+ initializeUserUidStatesLocked(userId);
+ }
+ }
+ }
+
+ private void initializeUserUidStates(int userId) {
+ synchronized (this) {
+ initializeUserUidStatesLocked(userId);
+ }
+ }
+
+ private void initializeUserUidStatesLocked(int userId) {
+ ArrayMap<String, ? extends PackageStateInternal> packageStates =
+ getPackageManagerInternal().getPackageStates();
+ for (int j = 0; j < packageStates.size(); j++) {
+ PackageStateInternal packageState = packageStates.valueAt(j);
+ int uid = UserHandle.getUid(userId, packageState.getAppId());
+ UidState uidState = getUidStateLocked(uid, true);
+ if (uidState.pkgOps == null) {
+ uidState.pkgOps = new ArrayMap<>();
+ }
+ String packageName = packageStates.keyAt(j);
+ Ops ops = new Ops(packageName, uidState);
+ uidState.pkgOps.put(packageName, ops);
+
+ SparseIntArray packageModes =
+ mAppOpsCheckingService.getNonDefaultPackageModes(packageName, userId);
+ for (int k = 0; k < packageModes.size(); k++) {
+ int code = packageModes.get(k);
+ ops.put(code, new Op(uidState, packageName, code, uid));
+ }
+ }
+ }
+
+ /**
* Sets a policy for handling app ops.
*
* @param policy The policy.
@@ -1687,13 +1730,6 @@
pkgOps.remove(ops.packageName);
mAppOpsCheckingService.removePackage(ops.packageName,
UserHandle.getUserId(uidState.uid));
- if (pkgOps.isEmpty()) {
- uidState.pkgOps = null;
- }
- if (uidState.isDefault()) {
- uidState.clear();
- mUidStates.remove(uid);
- }
}
}
}
@@ -2147,10 +2183,6 @@
UserHandle.getUserId(uidState.uid));
}
}
- if (uidState.isDefault()) {
- uidState.clear();
- mUidStates.remove(uidState.uid);
- }
if (uidChanged) {
uidState.evalForegroundOps();
}
@@ -3588,6 +3620,20 @@
}
/**
+ * @return {@link UserManagerInternal}
+ */
+ private @NonNull UserManagerInternal getUserManagerInternal() {
+ if (mUserManagerInternal == null) {
+ mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
+ }
+ if (mUserManagerInternal == null) {
+ throw new IllegalStateException("UserManagerInternal not loaded");
+ }
+
+ return mUserManagerInternal;
+ }
+
+ /**
* Create a restriction description matching the properties of the package.
*
* @param pkg The package to create the restriction description for
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index ed59a88..c01424d 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -1462,7 +1462,7 @@
mNm = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
- mSoundDoseHelper.configureSafeMediaVolume(/*forced=*/true, TAG);
+ mSoundDoseHelper.configureSafeMedia(/*forced=*/true, TAG);
initA11yMonitoring();
@@ -3647,11 +3647,12 @@
synchronized (VolumeStreamState.class) {
List<Integer> streamsToMute = new ArrayList<>();
for (int stream = 0; stream < mStreamStates.length; stream++) {
- if (streamAlias == mStreamVolumeAlias[stream]) {
+ VolumeStreamState vss = mStreamStates[stream];
+ if (streamAlias == mStreamVolumeAlias[stream] && vss.isMutable()) {
if (!(readCameraSoundForced()
- && (mStreamStates[stream].getStreamType()
+ && (vss.getStreamType()
== AudioSystem.STREAM_SYSTEM_ENFORCED))) {
- boolean changed = mStreamStates[stream].mute(state, /* apply= */ false);
+ boolean changed = vss.mute(state, /* apply= */ false);
if (changed) {
streamsToMute.add(stream);
}
@@ -5329,7 +5330,8 @@
if (!shouldMute) {
// unmute
// ring and notifications volume should never be 0 when not silenced
- if (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_RING) {
+ if (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_RING
+ || mStreamVolumeAlias[streamType] == AudioSystem.STREAM_NOTIFICATION) {
synchronized (VolumeStreamState.class) {
final VolumeStreamState vss = mStreamStates[streamType];
for (int i = 0; i < vss.mIndexMap.size(); i++) {
@@ -6060,6 +6062,8 @@
}
}
+ readVolumeGroupsSettings(userSwitch);
+
// apply new ringer mode before checking volume for alias streams so that streams
// muted by ringer mode have the correct volume
setRingerModeInt(getRingerModeInternal(), false);
@@ -6071,8 +6075,6 @@
mSoundDoseHelper.restoreMusicActiveMs();
mSoundDoseHelper.enforceSafeMediaVolumeIfActive(TAG);
- readVolumeGroupsSettings(userSwitch);
-
if (DEBUG_VOL) {
Log.d(TAG, "Restoring device volume behavior");
}
@@ -8478,9 +8480,10 @@
}
mVolumeGroupState.updateVolumeIndex(groupIndex, device);
// Only propage mute of stream when applicable
- if (mIndexMin == 0 || isCallStream(mStreamType)) {
+ if (isMutable()) {
// For call stream, align mute only when muted, not when index is set to 0
- mVolumeGroupState.mute(forceMuteState ? mIsMuted : groupIndex == 0);
+ mVolumeGroupState.mute(
+ forceMuteState ? mIsMuted : groupIndex == 0 || mIsMuted);
}
}
}
@@ -8529,6 +8532,12 @@
return mIsMuted || mIsMutedInternally;
}
+
+ private boolean isMutable() {
+ return isStreamAffectedByMute(mStreamType)
+ && (mIndexMin == 0 || isCallStream(mStreamType));
+ }
+
/**
* Mute/unmute the stream
* @param state the new mute state
@@ -10236,7 +10245,7 @@
// reading new configuration "safely" (i.e. under try catch) in case anything
// goes wrong.
Configuration config = context.getResources().getConfiguration();
- mSoundDoseHelper.configureSafeMediaVolume(/*forced*/false, TAG);
+ mSoundDoseHelper.configureSafeMedia(/*forced*/false, TAG);
boolean cameraSoundForced = readCameraSoundForced();
synchronized (mSettingsLock) {
diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java
index 42031c6..ac02eb7 100644
--- a/services/core/java/com/android/server/audio/SoundDoseHelper.java
+++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java
@@ -38,6 +38,7 @@
import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.Settings;
+import android.text.TextUtils;
import android.util.Log;
import android.util.MathUtils;
@@ -55,6 +56,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import java.util.stream.Collectors;
/**
* Safe media volume management.
@@ -77,14 +79,16 @@
// SAFE_MEDIA_VOLUME_DISABLED according to country option. If not SAFE_MEDIA_VOLUME_DISABLED, it
// can be set to SAFE_MEDIA_VOLUME_INACTIVE by calling AudioService.disableSafeMediaVolume()
// (when user opts out).
+ // Note: when CSD calculation is enabled the state is set to SAFE_MEDIA_VOLUME_DISABLED
private static final int SAFE_MEDIA_VOLUME_NOT_CONFIGURED = 0;
private static final int SAFE_MEDIA_VOLUME_DISABLED = 1;
private static final int SAFE_MEDIA_VOLUME_INACTIVE = 2; // confirmed
private static final int SAFE_MEDIA_VOLUME_ACTIVE = 3; // unconfirmed
- private static final int MSG_CONFIGURE_SAFE_MEDIA_VOLUME = SAFE_MEDIA_VOLUME_MSG_START + 1;
+ private static final int MSG_CONFIGURE_SAFE_MEDIA = SAFE_MEDIA_VOLUME_MSG_START + 1;
private static final int MSG_PERSIST_SAFE_VOLUME_STATE = SAFE_MEDIA_VOLUME_MSG_START + 2;
private static final int MSG_PERSIST_MUSIC_ACTIVE_MS = SAFE_MEDIA_VOLUME_MSG_START + 3;
+ private static final int MSG_PERSIST_CSD_VALUES = SAFE_MEDIA_VOLUME_MSG_START + 4;
private static final int UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX = (20 * 3600 * 1000); // 20 hours
@@ -100,14 +104,16 @@
private static final int CSD_WARNING_TIMEOUT_MS_ACCUMULATION_START = -1;
private static final int CSD_WARNING_TIMEOUT_MS_MOMENTARY_EXPOSURE = 5000;
+ private static final String PERSIST_CSD_RECORD_FIELD_SEPARATOR = ",";
+ private static final String PERSIST_CSD_RECORD_SEPARATOR_CHAR = "|";
+ private static final String PERSIST_CSD_RECORD_SEPARATOR = "\\|";
+
private final EventLogger mLogger = new EventLogger(AudioService.LOG_NB_EVENTS_SOUND_DOSE,
"CSD updates");
private int mMcc = 0;
- private final boolean mEnableCsd;
-
- final Object mSafeMediaVolumeStateLock = new Object();
+ private final Object mSafeMediaVolumeStateLock = new Object();
private int mSafeMediaVolumeState;
// Used when safe volume warning message display is requested by setStreamVolume(). In this
@@ -148,10 +154,18 @@
@NonNull private final AudioHandler mAudioHandler;
@NonNull private final ISafeHearingVolumeController mVolumeController;
+ private final boolean mEnableCsd;
+
private ISoundDose mSoundDose;
+
+ private final Object mCsdStateLock = new Object();
+
+ @GuardedBy("mCsdStateLock")
private float mCurrentCsd = 0.f;
// dose at which the next dose reached warning occurs
+ @GuardedBy("mCsdStateLock")
private float mNextCsdWarning = 1.0f;
+ @GuardedBy("mCsdStateLock")
private final List<SoundDoseRecord> mDoseRecords = new ArrayList<>();
private final Context mContext;
@@ -169,11 +183,16 @@
}
public void onNewCsdValue(float currentCsd, SoundDoseRecord[] records) {
+ if (!mEnableCsd) {
+ Log.w(TAG, "onNewCsdValue: csd not supported, ignoring value");
+ return;
+ }
+
Log.i(TAG, "onNewCsdValue: " + currentCsd);
- if (mCurrentCsd < currentCsd) {
- // dose increase: going over next threshold?
- if ((mCurrentCsd < mNextCsdWarning) && (currentCsd >= mNextCsdWarning)) {
- if (mEnableCsd) {
+ synchronized (mCsdStateLock) {
+ if (mCurrentCsd < currentCsd) {
+ // dose increase: going over next threshold?
+ if ((mCurrentCsd < mNextCsdWarning) && (currentCsd >= mNextCsdWarning)) {
if (mNextCsdWarning == 5.0f) {
// 500% dose repeat
mVolumeController.postDisplayCsdWarning(
@@ -188,17 +207,18 @@
getTimeoutMsForWarning(
AudioManager.CSD_WARNING_DOSE_REACHED_1X));
}
+ mNextCsdWarning += 1.0f;
}
- mNextCsdWarning += 1.0f;
+ } else {
+ // dose decrease: dropping below previous threshold of warning?
+ if ((currentCsd < mNextCsdWarning - 1.0f) && (
+ mNextCsdWarning >= 2.0f)) {
+ mNextCsdWarning -= 1.0f;
+ }
}
- } else {
- // dose decrease: dropping below previous threshold of warning?
- if ((currentCsd < mNextCsdWarning - 1.0f) && (mNextCsdWarning >= 2.0f)) {
- mNextCsdWarning -= 1.0f;
- }
+ mCurrentCsd = currentCsd;
+ updateSoundDoseRecords_l(records, currentCsd);
}
- mCurrentCsd = currentCsd;
- updateSoundDoseRecords(records, currentCsd);
}
};
@@ -213,20 +233,22 @@
mContext = context;
- mSafeMediaVolumeState = mSettings.getGlobalInt(audioService.getContentResolver(),
- Settings.Global.AUDIO_SAFE_VOLUME_STATE, 0);
-
mEnableCsd = mContext.getResources().getBoolean(R.bool.config_audio_csd_enabled_default);
+ if (mEnableCsd) {
+ mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_DISABLED;
+ } else {
+ mSafeMediaVolumeState = mSettings.getGlobalInt(audioService.getContentResolver(),
+ Settings.Global.AUDIO_SAFE_VOLUME_STATE, 0);
+ }
// The default safe volume index read here will be replaced by the actual value when
- // the mcc is read by onConfigureSafeVolume()
+ // the mcc is read by onConfigureSafeMedia()
+ // For now we use the same index for RS2 initial warning with CSD
mSafeMediaVolumeIndex = mContext.getResources().getInteger(
R.integer.config_safe_media_volume_index) * 10;
mAlarmManager = (AlarmManager) mContext.getSystemService(
Context.ALARM_SERVICE);
-
- initCsd();
}
float getRs2Value() {
@@ -456,8 +478,8 @@
}
}
- /*package*/ void configureSafeMediaVolume(boolean forced, String caller) {
- int msg = MSG_CONFIGURE_SAFE_MEDIA_VOLUME;
+ /*package*/ void configureSafeMedia(boolean forced, String caller) {
+ int msg = MSG_CONFIGURE_SAFE_MEDIA;
mAudioHandler.removeMessages(msg);
long time = 0;
@@ -507,8 +529,8 @@
/*package*/ void handleMessage(Message msg) {
switch (msg.what) {
- case MSG_CONFIGURE_SAFE_MEDIA_VOLUME:
- onConfigureSafeVolume((msg.arg1 == 1), (String) msg.obj);
+ case MSG_CONFIGURE_SAFE_MEDIA:
+ onConfigureSafeMedia((msg.arg1 == 1), (String) msg.obj);
break;
case MSG_PERSIST_SAFE_VOLUME_STATE:
onPersistSafeVolumeState(msg.arg1);
@@ -519,8 +541,13 @@
Settings.Secure.UNSAFE_VOLUME_MUSIC_ACTIVE_MS, musicActiveMs,
UserHandle.USER_CURRENT);
break;
+ case MSG_PERSIST_CSD_VALUES:
+ onPersistSoundDoseRecords();
+ break;
+ default:
+ Log.e(TAG, "Unexpected msg to handle: " + msg.what);
+ break;
}
-
}
/*package*/ void dump(PrintWriter pw) {
@@ -540,33 +567,44 @@
/*package*/void reset() {
Log.d(TAG, "Reset the sound dose helper");
- initCsd();
- }
-
- private void initCsd() {
- synchronized (mSafeMediaVolumeStateLock) {
- if (mEnableCsd) {
- Log.v(TAG, "Initializing sound dose");
-
- mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_DISABLED;
- mSoundDose = AudioSystem.getSoundDoseInterface(mSoundDoseCallback);
- try {
- if (mSoundDose != null && mSoundDose.asBinder().isBinderAlive()) {
- if (mCurrentCsd != 0.f) {
- Log.d(TAG, "Resetting the saved sound dose value " + mCurrentCsd);
- SoundDoseRecord[] records = mDoseRecords.toArray(
- new SoundDoseRecord[0]);
- mSoundDose.resetCsd(mCurrentCsd, records);
- }
+ mSoundDose = AudioSystem.getSoundDoseInterface(mSoundDoseCallback);
+ synchronized (mCsdStateLock) {
+ try {
+ if (mSoundDose != null && mSoundDose.asBinder().isBinderAlive()) {
+ if (mCurrentCsd != 0.f) {
+ Log.d(TAG,
+ "Resetting the saved sound dose value " + mCurrentCsd);
+ SoundDoseRecord[] records = mDoseRecords.toArray(
+ new SoundDoseRecord[0]);
+ mSoundDose.resetCsd(mCurrentCsd, records);
}
- } catch (RemoteException e) {
- // noop
}
+ } catch (RemoteException e) {
+ // noop
}
}
}
- private void onConfigureSafeVolume(boolean force, String caller) {
+ private void initCsd() {
+ if (mEnableCsd) {
+ Log.v(TAG, "Initializing sound dose");
+
+ synchronized (mCsdStateLock) {
+ // Restore persisted values
+ mCurrentCsd = parseGlobalSettingFloat(
+ Settings.Global.AUDIO_SAFE_CSD_CURRENT_VALUE, /* defaultValue= */0.f);
+ mNextCsdWarning = parseGlobalSettingFloat(
+ Settings.Global.AUDIO_SAFE_CSD_NEXT_WARNING, /* defaultValue= */1.f);
+ mDoseRecords.addAll(persistedStringToRecordList(
+ mSettings.getGlobalString(mAudioService.getContentResolver(),
+ Settings.Global.AUDIO_SAFE_CSD_DOSE_RECORDS)));
+ }
+
+ reset();
+ }
+ }
+
+ private void onConfigureSafeMedia(boolean force, String caller) {
synchronized (mSafeMediaVolumeStateLock) {
int mcc = mContext.getResources().getConfiguration().mcc;
if ((mMcc != mcc) || ((mMcc == 0) && force)) {
@@ -612,6 +650,10 @@
/*obj=*/null), /*delay=*/0);
}
}
+
+ if (mEnableCsd) {
+ initCsd();
+ }
}
private int getTimeoutMsForWarning(@AudioManager.CsdWarning int csdWarning) {
@@ -702,7 +744,8 @@
return null;
}
- private void updateSoundDoseRecords(SoundDoseRecord[] newRecords, float currentCsd) {
+ @GuardedBy("mCsdStateLock")
+ private void updateSoundDoseRecords_l(SoundDoseRecord[] newRecords, float currentCsd) {
long totalDuration = 0;
for (SoundDoseRecord record : newRecords) {
Log.i(TAG, " new record: " + record);
@@ -722,9 +765,87 @@
}
}
+ mAudioHandler.sendMessageAtTime(mAudioHandler.obtainMessage(MSG_PERSIST_CSD_VALUES,
+ /* arg1= */0, /* arg2= */0, /* obj= */null), /* delay= */0);
+
mLogger.enqueue(SoundDoseEvent.getDoseUpdateEvent(currentCsd, totalDuration));
}
+ private void onPersistSoundDoseRecords() {
+ synchronized (mCsdStateLock) {
+ mSettings.putGlobalString(mAudioService.getContentResolver(),
+ Settings.Global.AUDIO_SAFE_CSD_CURRENT_VALUE,
+ Float.toString(mCurrentCsd));
+ mSettings.putGlobalString(mAudioService.getContentResolver(),
+ Settings.Global.AUDIO_SAFE_CSD_NEXT_WARNING,
+ Float.toString(mNextCsdWarning));
+ mSettings.putGlobalString(mAudioService.getContentResolver(),
+ Settings.Global.AUDIO_SAFE_CSD_DOSE_RECORDS,
+ mDoseRecords.stream().map(
+ SoundDoseHelper::recordToPersistedString).collect(
+ Collectors.joining(PERSIST_CSD_RECORD_SEPARATOR_CHAR)));
+ }
+ }
+
+ private static String recordToPersistedString(SoundDoseRecord record) {
+ return record.timestamp
+ + PERSIST_CSD_RECORD_FIELD_SEPARATOR + record.duration
+ + PERSIST_CSD_RECORD_FIELD_SEPARATOR + record.value
+ + PERSIST_CSD_RECORD_FIELD_SEPARATOR + record.averageMel;
+ }
+
+ private static List<SoundDoseRecord> persistedStringToRecordList(String records) {
+ if (records == null || records.isEmpty()) {
+ return null;
+ }
+ return Arrays.stream(TextUtils.split(records, PERSIST_CSD_RECORD_SEPARATOR)).map(
+ SoundDoseHelper::persistedStringToRecord).filter(Objects::nonNull).collect(
+ Collectors.toList());
+ }
+
+ private static SoundDoseRecord persistedStringToRecord(String record) {
+ if (record == null || record.isEmpty()) {
+ return null;
+ }
+ final String[] fields = TextUtils.split(record, PERSIST_CSD_RECORD_FIELD_SEPARATOR);
+ if (fields.length != 4) {
+ Log.w(TAG, "Expecting 4 fields for a SoundDoseRecord, parsed " + fields.length);
+ return null;
+ }
+
+ final SoundDoseRecord sdRecord = new SoundDoseRecord();
+ try {
+ sdRecord.timestamp = Long.parseLong(fields[0]);
+ sdRecord.duration = Integer.parseInt(fields[1]);
+ sdRecord.value = Float.parseFloat(fields[2]);
+ sdRecord.averageMel = Float.parseFloat(fields[3]);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Unable to parse persisted SoundDoseRecord: " + record, e);
+ return null;
+ }
+
+ return sdRecord;
+ }
+
+ private float parseGlobalSettingFloat(String audioSafeCsdCurrentValue, float defaultValue) {
+ String stringValue = mSettings.getGlobalString(mAudioService.getContentResolver(),
+ audioSafeCsdCurrentValue);
+ if (stringValue == null || stringValue.isEmpty()) {
+ Log.v(TAG, "No value stored in settings " + audioSafeCsdCurrentValue);
+ return defaultValue;
+ }
+
+ float value;
+ try {
+ value = Float.parseFloat(stringValue);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Error parsing float from settings " + audioSafeCsdCurrentValue, e);
+ value = defaultValue;
+ }
+
+ return value;
+ }
+
// StreamVolumeCommand contains the information needed to defer the process of
// setStreamVolume() in case the user has to acknowledge the safe volume warning message.
private static class StreamVolumeCommand {
diff --git a/services/core/java/com/android/server/backup/SetUtils.java b/services/core/java/com/android/server/backup/SetUtils.java
new file mode 100644
index 0000000..ae70e19
--- /dev/null
+++ b/services/core/java/com/android/server/backup/SetUtils.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Helper class containing common operation on {@link java.util.Set}.
+ */
+public final class SetUtils {
+ // Statics only
+ private SetUtils() {}
+
+ /**
+ * Returns union of two sets.
+ */
+ public static <T> Set<T> union(Set<T> set1, Set<T> set2) {
+ Set<T> unionSet = new HashSet<>(set1);
+ unionSet.addAll(set2);
+ return unionSet;
+ }
+}
diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java
index 35df3ee..224e34d 100644
--- a/services/core/java/com/android/server/backup/SystemBackupAgent.java
+++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java
@@ -30,6 +30,7 @@
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
+import android.os.UserManager;
import android.util.Slog;
import com.google.android.collect.Sets;
@@ -84,29 +85,50 @@
// Use old keys to keep legacy data compatibility and avoid writing two wallpapers
private static final String WALLPAPER_IMAGE_KEY = WallpaperBackupHelper.WALLPAPER_IMAGE_KEY;
- private static final Set<String> sEligibleForMultiUser = Sets.newArraySet(
- PERMISSION_HELPER, NOTIFICATION_HELPER, SYNC_SETTINGS_HELPER, APP_LOCALES_HELPER,
- ACCOUNT_MANAGER_HELPER, USAGE_STATS_HELPER, PREFERRED_HELPER);
+ /**
+ * Helpers that are enabled for "profile" users (such as work profile). See {@link
+ * UserManager#isProfile()}. This is a subset of {@link #sEligibleHelpersForNonSystemUser}.
+ */
+ private static final Set<String> sEligibleHelpersForProfileUser =
+ Sets.newArraySet(
+ PERMISSION_HELPER,
+ NOTIFICATION_HELPER,
+ SYNC_SETTINGS_HELPER,
+ APP_LOCALES_HELPER);
+
+ /** Helpers that are enabled for full, non-system users. */
+ private static final Set<String> sEligibleHelpersForNonSystemUser =
+ SetUtils.union(sEligibleHelpersForProfileUser,
+ Sets.newArraySet(ACCOUNT_MANAGER_HELPER, USAGE_STATS_HELPER, PREFERRED_HELPER,
+ SHORTCUT_MANAGER_HELPER));
private int mUserId = UserHandle.USER_SYSTEM;
+ private boolean mIsProfileUser = false;
@Override
public void onCreate(UserHandle user, @BackupDestination int backupDestination) {
super.onCreate(user, backupDestination);
mUserId = user.getIdentifier();
+ if (mUserId != UserHandle.USER_SYSTEM) {
+ Context context = createContextAsUser(user, /* flags= */ 0);
+ UserManager userManager = context.getSystemService(UserManager.class);
+ mIsProfileUser = userManager.isProfile();
+ }
- addHelper(SYNC_SETTINGS_HELPER, new AccountSyncSettingsBackupHelper(this, mUserId));
- addHelper(PREFERRED_HELPER, new PreferredActivityBackupHelper(mUserId));
- addHelper(NOTIFICATION_HELPER, new NotificationBackupHelper(mUserId));
- addHelper(PERMISSION_HELPER, new PermissionBackupHelper(mUserId));
- addHelper(USAGE_STATS_HELPER, new UsageStatsBackupHelper(mUserId));
- addHelper(SHORTCUT_MANAGER_HELPER, new ShortcutBackupHelper());
- addHelper(ACCOUNT_MANAGER_HELPER, new AccountManagerBackupHelper(mUserId));
- addHelper(SLICES_HELPER, new SliceBackupHelper(this));
- addHelper(PEOPLE_HELPER, new PeopleBackupHelper(mUserId));
- addHelper(APP_LOCALES_HELPER, new AppSpecificLocalesBackupHelper(mUserId));
- addHelper(APP_GENDER_HELPER, new AppGrammaticalGenderBackupHelper(mUserId));
+ addHelperIfEligibleForUser(
+ SYNC_SETTINGS_HELPER, new AccountSyncSettingsBackupHelper(this, mUserId));
+ addHelperIfEligibleForUser(PREFERRED_HELPER, new PreferredActivityBackupHelper(mUserId));
+ addHelperIfEligibleForUser(NOTIFICATION_HELPER, new NotificationBackupHelper(mUserId));
+ addHelperIfEligibleForUser(PERMISSION_HELPER, new PermissionBackupHelper(mUserId));
+ addHelperIfEligibleForUser(USAGE_STATS_HELPER, new UsageStatsBackupHelper(mUserId));
+ addHelperIfEligibleForUser(SHORTCUT_MANAGER_HELPER, new ShortcutBackupHelper(mUserId));
+ addHelperIfEligibleForUser(ACCOUNT_MANAGER_HELPER, new AccountManagerBackupHelper(mUserId));
+ addHelperIfEligibleForUser(SLICES_HELPER, new SliceBackupHelper(this));
+ addHelperIfEligibleForUser(PEOPLE_HELPER, new PeopleBackupHelper(mUserId));
+ addHelperIfEligibleForUser(APP_LOCALES_HELPER, new AppSpecificLocalesBackupHelper(mUserId));
+ addHelperIfEligibleForUser(APP_GENDER_HELPER,
+ new AppGrammaticalGenderBackupHelper(mUserId));
}
@Override
@@ -130,15 +152,6 @@
super.onRestore(data, appVersionCode, newState);
}
- @Override
- public void addHelper(String keyPrefix, BackupHelper helper) {
- if (mUserId != UserHandle.USER_SYSTEM && !sEligibleForMultiUser.contains(keyPrefix)) {
- return;
- }
-
- super.addHelper(keyPrefix, helper);
- }
-
/**
* Support for 'adb restore' of legacy archives
*/
@@ -189,4 +202,25 @@
}
}
}
+
+ private void addHelperIfEligibleForUser(String keyPrefix, BackupHelper helper) {
+ if (isHelperEligibleForUser(keyPrefix)) {
+ addHelper(keyPrefix, helper);
+ }
+ }
+
+ private boolean isHelperEligibleForUser(String keyPrefix) {
+ // All helpers are eligible for the system user.
+ if (mUserId == UserHandle.USER_SYSTEM) {
+ return true;
+ }
+
+ // Profile users (such as work profile) have their own allow list.
+ if (mIsProfileUser) {
+ return sEligibleHelpersForProfileUser.contains(keyPrefix);
+ }
+
+ // Full, non-system users have their own allow list.
+ return sEligibleHelpersForNonSystemUser.contains(keyPrefix);
+ }
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java b/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java
index 969a174..0b5c1c1 100644
--- a/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java
+++ b/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java
@@ -150,6 +150,13 @@
}
/**
+ * Returns if the sensor is side fps.
+ */
+ public boolean isSfps() {
+ return mSidefpsController.isPresent();
+ }
+
+ /**
* Consumer for a biometric overlay controller.
*
* This behaves like a normal {@link Consumer} except that it will trap and log
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index a90679e..932c0b4 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -236,8 +236,14 @@
@Override
public void onError(int errorCode, int vendorCode) {
- super.onError(errorCode, vendorCode);
-
+ if (errorCode == BiometricFingerprintConstants.FINGERPRINT_ERROR_VENDOR
+ && vendorCode == BiometricFingerprintConstants.BIOMETRIC_ERROR_POWER_PRESSED
+ && mSensorOverlays.isSfps()) {
+ super.onError(BiometricFingerprintConstants.BIOMETRIC_ERROR_POWER_PRESSED,
+ 0 /* vendorCode */);
+ } else {
+ super.onError(errorCode, vendorCode);
+ }
if (errorCode == BiometricFingerprintConstants.FINGERPRINT_ERROR_BAD_CALIBRATION) {
BiometricNotificationUtils.showBadCalibrationNotification(getContext());
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
index 513b3e3..cf54662 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
@@ -146,7 +146,14 @@
}
});
mCallback.onBiometricAction(BiometricStateListener.ACTION_SENSOR_TOUCH);
- super.onAcquired(acquiredInfo, vendorCode);
+ if (acquiredInfo == BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR
+ && vendorCode == BiometricFingerprintConstants.BIOMETRIC_ERROR_POWER_PRESSED
+ && mSensorOverlays.isSfps()) {
+ super.onAcquired(BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_POWER_PRESSED,
+ 0 /* vendorCode */);
+ } else {
+ super.onAcquired(acquiredInfo, vendorCode);
+ }
}
@Override
@@ -274,8 +281,5 @@
}
@Override
- public void onPowerPressed() {
- onAcquired(BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_POWER_PRESSED,
- 0 /* vendorCode */);
- }
+ public void onPowerPressed() { }
}
diff --git a/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java b/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java
index dce1c96..39c649b 100644
--- a/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java
+++ b/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java
@@ -111,7 +111,7 @@
bb.order(ByteOrder.LITTLE_ENDIAN);
final int msgLen = bb.getInt();
- if (msgLen <= 0 || msgLen > MAX_CLIPBOARD_BYTES) {
+ if (msgLen < 0 || msgLen > MAX_CLIPBOARD_BYTES) {
throw new ProtocolException("Clipboard message length: " + msgLen + " out of bounds.");
}
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
index 064cd2d..ba96861 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
@@ -25,6 +25,7 @@
import static com.android.server.devicestate.DeviceState.FLAG_CANCEL_OVERRIDE_REQUESTS;
import static com.android.server.devicestate.OverrideRequest.OVERRIDE_REQUEST_TYPE_BASE_STATE;
import static com.android.server.devicestate.OverrideRequest.OVERRIDE_REQUEST_TYPE_EMULATED_STATE;
+import static com.android.server.devicestate.OverrideRequestController.FLAG_THERMAL_CRITICAL;
import static com.android.server.devicestate.OverrideRequestController.STATUS_ACTIVE;
import static com.android.server.devicestate.OverrideRequestController.STATUS_CANCELED;
import static com.android.server.devicestate.OverrideRequestController.STATUS_UNKNOWN;
@@ -183,6 +184,9 @@
ActivityTaskManagerInternal.ScreenObserver mOverrideRequestScreenObserver =
new OverrideRequestScreenObserver();
+ @NonNull
+ private final DeviceStateNotificationController mDeviceStateNotificationController;
+
public DeviceStateManagerService(@NonNull Context context) {
this(context, DeviceStatePolicy.Provider
.fromResources(context.getResources())
@@ -211,6 +215,13 @@
mDeviceStatePolicy.getDeviceStateProvider().setListener(mDeviceStateProviderListener);
mBinderService = new BinderService();
mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
+ mDeviceStateNotificationController = new DeviceStateNotificationController(
+ context, mHandler,
+ () -> {
+ synchronized (mLock) {
+ mActiveOverride.ifPresent(mOverrideRequestController::cancelRequest);
+ }
+ });
}
@Override
@@ -346,7 +357,8 @@
return mBinderService;
}
- private void updateSupportedStates(DeviceState[] supportedDeviceStates) {
+ private void updateSupportedStates(DeviceState[] supportedDeviceStates,
+ @DeviceStateProvider.SupportedStatesUpdatedReason int reason) {
synchronized (mLock) {
final int[] oldStateIdentifiers = getSupportedStateIdentifiersLocked();
@@ -370,7 +382,7 @@
return;
}
- mOverrideRequestController.handleNewSupportedStates(newStateIdentifiers);
+ mOverrideRequestController.handleNewSupportedStates(newStateIdentifiers, reason);
updatePendingStateLocked();
setRearDisplayStateLocked();
@@ -596,7 +608,7 @@
@GuardedBy("mLock")
private void onOverrideRequestStatusChangedLocked(@NonNull OverrideRequest request,
- @OverrideRequestController.RequestStatus int status) {
+ @OverrideRequestController.RequestStatus int status, int flags) {
if (request.getRequestType() == OVERRIDE_REQUEST_TYPE_BASE_STATE) {
switch (status) {
case STATUS_ACTIVE:
@@ -616,10 +628,19 @@
switch (status) {
case STATUS_ACTIVE:
mActiveOverride = Optional.of(request);
+ mDeviceStateNotificationController.showStateActiveNotificationIfNeeded(
+ request.getRequestedState(), request.getUid());
break;
case STATUS_CANCELED:
if (mActiveOverride.isPresent() && mActiveOverride.get() == request) {
mActiveOverride = Optional.empty();
+ mDeviceStateNotificationController.cancelNotification(
+ request.getRequestedState());
+ if ((flags & FLAG_THERMAL_CRITICAL) == FLAG_THERMAL_CRITICAL) {
+ mDeviceStateNotificationController
+ .showThermalCriticalNotificationIfNeeded(
+ request.getRequestedState());
+ }
}
break;
case STATUS_UNKNOWN: // same as default
@@ -700,7 +721,7 @@
}
}
- private void requestStateInternal(int state, int flags, int callingPid,
+ private void requestStateInternal(int state, int flags, int callingPid, int callingUid,
@NonNull IBinder token, boolean hasControlDeviceStatePermission) {
synchronized (mLock) {
final ProcessRecord processRecord = mProcessRecords.get(callingPid);
@@ -721,8 +742,8 @@
+ " is not supported.");
}
- OverrideRequest request = new OverrideRequest(token, callingPid, state, flags,
- OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
+ OverrideRequest request = new OverrideRequest(token, callingPid, callingUid,
+ state, flags, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
// If we don't have the CONTROL_DEVICE_STATE permission, we want to show the overlay
if (!hasControlDeviceStatePermission && mRearDisplayState != null
@@ -762,7 +783,7 @@
}
private void requestBaseStateOverrideInternal(int state, int flags, int callingPid,
- @NonNull IBinder token) {
+ int callingUid, @NonNull IBinder token) {
synchronized (mLock) {
final Optional<DeviceState> deviceState = getStateLocked(state);
if (!deviceState.isPresent()) {
@@ -782,8 +803,8 @@
+ " token: " + token);
}
- OverrideRequest request = new OverrideRequest(token, callingPid, state, flags,
- OVERRIDE_REQUEST_TYPE_BASE_STATE);
+ OverrideRequest request = new OverrideRequest(token, callingPid, callingUid,
+ state, flags, OVERRIDE_REQUEST_TYPE_BASE_STATE);
mOverrideRequestController.addBaseStateRequest(request);
}
}
@@ -953,11 +974,12 @@
@IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int mCurrentBaseState;
@Override
- public void onSupportedDeviceStatesChanged(DeviceState[] newDeviceStates) {
+ public void onSupportedDeviceStatesChanged(DeviceState[] newDeviceStates,
+ @DeviceStateProvider.SupportedStatesUpdatedReason int reason) {
if (newDeviceStates.length == 0) {
throw new IllegalArgumentException("Supported device states must not be empty");
}
- updateSupportedStates(newDeviceStates);
+ updateSupportedStates(newDeviceStates, reason);
}
@Override
@@ -1085,6 +1107,7 @@
@Override // Binder call
public void requestState(IBinder token, int state, int flags) {
final int callingPid = Binder.getCallingPid();
+ final int callingUid = Binder.getCallingUid();
// Allow top processes to request a device state change
// If the calling process ID is not the top app, then we check if this process
// holds a permission to CONTROL_DEVICE_STATE
@@ -1099,7 +1122,8 @@
final long callingIdentity = Binder.clearCallingIdentity();
try {
- requestStateInternal(state, flags, callingPid, token, hasControlStatePermission);
+ requestStateInternal(state, flags, callingPid, callingUid, token,
+ hasControlStatePermission);
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
@@ -1124,6 +1148,7 @@
@Override // Binder call
public void requestBaseStateOverride(IBinder token, int state, int flags) {
final int callingPid = Binder.getCallingPid();
+ final int callingUid = Binder.getCallingUid();
getContext().enforceCallingOrSelfPermission(CONTROL_DEVICE_STATE,
"Permission required to control base state of device.");
@@ -1133,7 +1158,7 @@
final long callingIdentity = Binder.clearCallingIdentity();
try {
- requestBaseStateOverrideInternal(state, flags, callingPid, token);
+ requestBaseStateOverrideInternal(state, flags, callingPid, callingUid, token);
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateNotificationController.java b/services/core/java/com/android/server/devicestate/DeviceStateNotificationController.java
new file mode 100644
index 0000000..9008740
--- /dev/null
+++ b/services/core/java/com/android/server/devicestate/DeviceStateNotificationController.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.devicestate;
+
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.hardware.devicestate.DeviceStateManager;
+import android.os.Handler;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Manages the user-visible device state notifications.
+ */
+class DeviceStateNotificationController extends BroadcastReceiver {
+ private static final String TAG = "DeviceStateNotificationController";
+
+ @VisibleForTesting static final String INTENT_ACTION_CANCEL_STATE =
+ "com.android.server.devicestate.INTENT_ACTION_CANCEL_STATE";
+ @VisibleForTesting static final int NOTIFICATION_ID = 1;
+ @VisibleForTesting static final String CHANNEL_ID = "DeviceStateManager";
+ @VisibleForTesting static final String NOTIFICATION_TAG = "DeviceStateManager";
+
+ private final Context mContext;
+ private final Handler mHandler;
+ private final NotificationManager mNotificationManager;
+ private final PackageManager mPackageManager;
+
+ // Stores the notification title and content indexed with the device state identifier.
+ private final SparseArray<NotificationInfo> mNotificationInfos;
+
+ // The callback when a device state is requested to be canceled.
+ private final Runnable mCancelStateRunnable;
+
+ DeviceStateNotificationController(@NonNull Context context, @NonNull Handler handler,
+ @NonNull Runnable cancelStateRunnable) {
+ this(context, handler, cancelStateRunnable, getNotificationInfos(context),
+ context.getPackageManager(), context.getSystemService(NotificationManager.class));
+ }
+
+ @VisibleForTesting
+ DeviceStateNotificationController(
+ @NonNull Context context, @NonNull Handler handler,
+ @NonNull Runnable cancelStateRunnable,
+ @NonNull SparseArray<NotificationInfo> notificationInfos,
+ @NonNull PackageManager packageManager,
+ @NonNull NotificationManager notificationManager) {
+ mContext = context;
+ mHandler = handler;
+ mCancelStateRunnable = cancelStateRunnable;
+ mNotificationInfos = notificationInfos;
+ mPackageManager = packageManager;
+ mNotificationManager = notificationManager;
+ mContext.registerReceiver(
+ this,
+ new IntentFilter(INTENT_ACTION_CANCEL_STATE),
+ android.Manifest.permission.CONTROL_DEVICE_STATE,
+ mHandler,
+ Context.RECEIVER_NOT_EXPORTED);
+ }
+
+ /**
+ * Displays the ongoing notification indicating that the device state is active. Does nothing if
+ * the state does not have an active notification.
+ *
+ * @param state the active device state identifier.
+ * @param requestingAppUid the uid of the requesting app used to retrieve the app name.
+ */
+ void showStateActiveNotificationIfNeeded(int state, int requestingAppUid) {
+ NotificationInfo info = mNotificationInfos.get(state);
+ if (info == null || !info.hasActiveNotification()) {
+ return;
+ }
+ String requesterApplicationLabel = getApplicationLabel(requestingAppUid);
+ if (requesterApplicationLabel != null) {
+ showNotification(
+ info.name, info.activeNotificationTitle,
+ String.format(info.activeNotificationContent, requesterApplicationLabel),
+ true /* ongoing */, R.drawable.ic_dual_screen
+ );
+ } else {
+ Slog.e(TAG, "Cannot determine the requesting app name when showing state active "
+ + "notification. uid=" + requestingAppUid + ", state=" + state);
+ }
+ }
+
+ /**
+ * Displays the notification indicating that the device state is canceled due to thermal
+ * critical condition. Does nothing if the state does not have a thermal critical notification.
+ *
+ * @param state the identifier of the device state being canceled.
+ */
+ void showThermalCriticalNotificationIfNeeded(int state) {
+ NotificationInfo info = mNotificationInfos.get(state);
+ if (info == null || !info.hasThermalCriticalNotification()) {
+ return;
+ }
+ showNotification(
+ info.name, info.thermalCriticalNotificationTitle,
+ info.thermalCriticalNotificationContent, false /* ongoing */,
+ R.drawable.ic_thermostat
+ );
+ }
+
+ /**
+ * Cancels the notification of the corresponding device state.
+ *
+ * @param state the device state identifier.
+ */
+ void cancelNotification(int state) {
+ if (!mNotificationInfos.contains(state)) {
+ return;
+ }
+ mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
+ }
+
+ @Override
+ public void onReceive(@NonNull Context context, @Nullable Intent intent) {
+ if (intent != null) {
+ if (INTENT_ACTION_CANCEL_STATE.equals(intent.getAction())) {
+ mCancelStateRunnable.run();
+ }
+ }
+ }
+
+ /**
+ * Displays a notification with the specified name, title, and content.
+ *
+ * @param name the name of the notification.
+ * @param title the title of the notification.
+ * @param content the content of the notification.
+ * @param ongoing if true, display an ongoing (sticky) notification with a turn off button.
+ */
+ private void showNotification(
+ @NonNull String name, @NonNull String title, @NonNull String content, boolean ongoing,
+ @DrawableRes int iconRes) {
+ final NotificationChannel channel = new NotificationChannel(
+ CHANNEL_ID, name, NotificationManager.IMPORTANCE_HIGH);
+ final Notification.Builder builder = new Notification.Builder(mContext, CHANNEL_ID)
+ .setSmallIcon(iconRes)
+ .setContentTitle(title)
+ .setContentText(content)
+ .setSubText(name)
+ .setLocalOnly(true)
+ .setOngoing(ongoing)
+ .setCategory(Notification.CATEGORY_SYSTEM);
+
+ if (ongoing) {
+ final Intent intent = new Intent(INTENT_ACTION_CANCEL_STATE)
+ .setPackage(mContext.getPackageName());
+ final PendingIntent pendingIntent = PendingIntent.getBroadcast(
+ mContext, 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE);
+ final Notification.Action action = new Notification.Action.Builder(
+ null /* icon */,
+ mContext.getString(R.string.device_state_notification_turn_off_button),
+ pendingIntent)
+ .build();
+ builder.addAction(action);
+ }
+
+ mNotificationManager.createNotificationChannel(channel);
+ mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, builder.build());
+ }
+
+ /**
+ * Loads the resources for the notifications. The device state identifiers and strings are
+ * stored in arrays. All the string arrays must have the same length and same order as the
+ * identifier array.
+ */
+ private static SparseArray<NotificationInfo> getNotificationInfos(Context context) {
+ final SparseArray<NotificationInfo> notificationInfos = new SparseArray<>();
+
+ final int[] stateIdentifiers =
+ context.getResources().getIntArray(
+ R.array.device_state_notification_state_identifiers);
+ final String[] names =
+ context.getResources().getStringArray(R.array.device_state_notification_names);
+ final String[] activeNotificationTitles =
+ context.getResources().getStringArray(
+ R.array.device_state_notification_active_titles);
+ final String[] activeNotificationContents =
+ context.getResources().getStringArray(
+ R.array.device_state_notification_active_contents);
+ final String[] thermalCriticalNotificationTitles =
+ context.getResources().getStringArray(
+ R.array.device_state_notification_thermal_titles);
+ final String[] thermalCriticalNotificationContents =
+ context.getResources().getStringArray(
+ R.array.device_state_notification_thermal_contents);
+
+ if (stateIdentifiers.length != names.length
+ || stateIdentifiers.length != activeNotificationTitles.length
+ || stateIdentifiers.length != activeNotificationContents.length
+ || stateIdentifiers.length != thermalCriticalNotificationTitles.length
+ || stateIdentifiers.length != thermalCriticalNotificationContents.length
+ ) {
+ throw new IllegalStateException(
+ "The length of state identifiers and notification texts must match!");
+ }
+
+ for (int i = 0; i < stateIdentifiers.length; i++) {
+ int identifier = stateIdentifiers[i];
+ if (identifier == DeviceStateManager.INVALID_DEVICE_STATE) {
+ continue;
+ }
+
+ notificationInfos.put(
+ identifier,
+ new NotificationInfo(
+ names[i], activeNotificationTitles[i], activeNotificationContents[i],
+ thermalCriticalNotificationTitles[i],
+ thermalCriticalNotificationContents[i])
+ );
+ }
+
+ return notificationInfos;
+ }
+
+ /**
+ * A helper function to get app name (label) using the app uid.
+ *
+ * @param uid the uid of the app.
+ * @return app name (label) if found, or null otherwise.
+ */
+ @Nullable
+ private String getApplicationLabel(int uid) {
+ String packageName = mPackageManager.getNameForUid(uid);
+ try {
+ ApplicationInfo appInfo = mPackageManager.getApplicationInfo(
+ packageName, PackageManager.ApplicationInfoFlags.of(0));
+ return appInfo.loadLabel(mPackageManager).toString();
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ /**
+ * A data class storing string resources of the notification of a device state.
+ */
+ @VisibleForTesting
+ static class NotificationInfo {
+ public final String name;
+ public final String activeNotificationTitle;
+ public final String activeNotificationContent;
+ public final String thermalCriticalNotificationTitle;
+ public final String thermalCriticalNotificationContent;
+
+ NotificationInfo(String name, String activeNotificationTitle,
+ String activeNotificationContent, String thermalCriticalNotificationTitle,
+ String thermalCriticalNotificationContent) {
+
+ this.name = name;
+ this.activeNotificationTitle = activeNotificationTitle;
+ this.activeNotificationContent = activeNotificationContent;
+ this.thermalCriticalNotificationTitle = thermalCriticalNotificationTitle;
+ this.thermalCriticalNotificationContent = thermalCriticalNotificationContent;
+ }
+
+ boolean hasActiveNotification() {
+ return activeNotificationTitle != null && activeNotificationTitle.length() > 0;
+ }
+
+ boolean hasThermalCriticalNotification() {
+ return thermalCriticalNotificationTitle != null
+ && thermalCriticalNotificationTitle.length() > 0;
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
index 109bf63..fecc13f 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
@@ -19,8 +19,12 @@
import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
+import android.annotation.IntDef;
import android.annotation.IntRange;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* Responsible for providing the set of supported {@link DeviceState device states} as well as the
* current device state.
@@ -28,12 +32,42 @@
* @see DeviceStatePolicy
*/
public interface DeviceStateProvider {
+ int SUPPORTED_DEVICE_STATES_CHANGED_DEFAULT = 0;
+
+ /**
+ * Indicating that the supported device states changed callback is trigger for initial listener
+ * registration.
+ */
+ int SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED = 1;
+
+ /**
+ * Indicating that the supported device states have changed because the thermal condition
+ * returned to normal status from critical status.
+ */
+ int SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL = 2;
+
+ /**
+ * Indicating that the supported device states have changed because of thermal critical
+ * condition.
+ */
+ int SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL = 3;
+
+ @IntDef(prefix = { "SUPPORTED_DEVICE_STATES_CHANGED_" }, value = {
+ SUPPORTED_DEVICE_STATES_CHANGED_DEFAULT,
+ SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED,
+ SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL,
+ SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface SupportedStatesUpdatedReason {}
+
/**
* Registers a listener for changes in provider state.
* <p>
- * It is <b>required</b> that {@link Listener#onSupportedDeviceStatesChanged(DeviceState[])} be
- * called followed by {@link Listener#onStateChanged(int)} with the initial values on successful
- * registration of the listener.
+ * It is <b>required</b> that
+ * {@link Listener#onSupportedDeviceStatesChanged(DeviceState[], int)} be called followed by
+ * {@link Listener#onStateChanged(int)} with the initial values on successful registration of
+ * the listener.
*/
void setListener(Listener listener);
@@ -53,18 +87,20 @@
* to zero and there must always be at least one supported device state.
*
* @param newDeviceStates array of supported device states.
+ * @param reason the reason for the supported device states change.
*
* @throws IllegalArgumentException if the list of device states is empty or if one of the
* provided states contains an invalid identifier.
*/
- void onSupportedDeviceStatesChanged(DeviceState[] newDeviceStates);
+ void onSupportedDeviceStatesChanged(DeviceState[] newDeviceStates,
+ @SupportedStatesUpdatedReason int reason);
/**
* Called to notify the listener of a change in current device state. Required to be called
* once on successful registration of the listener and then once on every subsequent change
* in device state. Value must have been included in the set of supported device states
* provided in the most recent call to
- * {@link #onSupportedDeviceStatesChanged(DeviceState[])}.
+ * {@link #onSupportedDeviceStatesChanged(DeviceState[], int)}.
*
* @param identifier the identifier of the new device state.
*
diff --git a/services/core/java/com/android/server/devicestate/OverrideRequest.java b/services/core/java/com/android/server/devicestate/OverrideRequest.java
index 325e384..74cf184 100644
--- a/services/core/java/com/android/server/devicestate/OverrideRequest.java
+++ b/services/core/java/com/android/server/devicestate/OverrideRequest.java
@@ -31,6 +31,7 @@
final class OverrideRequest {
private final IBinder mToken;
private final int mPid;
+ private final int mUid;
private final int mRequestedState;
@DeviceStateRequest.RequestFlags
private final int mFlags;
@@ -68,10 +69,11 @@
@Retention(RetentionPolicy.SOURCE)
public @interface OverrideRequestType {}
- OverrideRequest(IBinder token, int pid, int requestedState,
+ OverrideRequest(IBinder token, int pid, int uid, int requestedState,
@DeviceStateRequest.RequestFlags int flags, @OverrideRequestType int requestType) {
mToken = token;
mPid = pid;
+ mUid = uid;
mRequestedState = requestedState;
mFlags = flags;
mRequestType = requestType;
@@ -85,6 +87,10 @@
return mPid;
}
+ int getUid() {
+ return mUid;
+ }
+
int getRequestedState() {
return mRequestedState;
}
diff --git a/services/core/java/com/android/server/devicestate/OverrideRequestController.java b/services/core/java/com/android/server/devicestate/OverrideRequestController.java
index e39c732..2ed4765 100644
--- a/services/core/java/com/android/server/devicestate/OverrideRequestController.java
+++ b/services/core/java/com/android/server/devicestate/OverrideRequestController.java
@@ -59,6 +59,11 @@
@Retention(RetentionPolicy.SOURCE)
@interface RequestStatus {}
+ /**
+ * A flag indicating that the status change was triggered by thermal critical status.
+ */
+ static final int FLAG_THERMAL_CRITICAL = 1 << 0;
+
static String statusToString(@RequestStatus int status) {
switch (status) {
case STATUS_ACTIVE:
@@ -106,7 +111,7 @@
void addRequest(@NonNull OverrideRequest request) {
OverrideRequest previousRequest = mRequest;
mRequest = request;
- mListener.onStatusChanged(request, STATUS_ACTIVE);
+ mListener.onStatusChanged(request, STATUS_ACTIVE, 0 /* flags */);
if (previousRequest != null) {
cancelRequestLocked(previousRequest);
@@ -116,7 +121,7 @@
void addBaseStateRequest(@NonNull OverrideRequest request) {
OverrideRequest previousRequest = mBaseStateRequest;
mBaseStateRequest = request;
- mListener.onStatusChanged(request, STATUS_ACTIVE);
+ mListener.onStatusChanged(request, STATUS_ACTIVE, 0 /* flags */);
if (previousRequest != null) {
cancelRequestLocked(previousRequest);
@@ -219,14 +224,17 @@
* Notifies the controller that the set of supported states has changed. The controller will
* notify the listener of all changes to request status as a result of this change.
*/
- void handleNewSupportedStates(int[] newSupportedStates) {
+ void handleNewSupportedStates(int[] newSupportedStates,
+ @DeviceStateProvider.SupportedStatesUpdatedReason int reason) {
+ boolean isThermalCritical =
+ reason == DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL;
if (mBaseStateRequest != null && !contains(newSupportedStates,
mBaseStateRequest.getRequestedState())) {
- cancelCurrentBaseStateRequestLocked();
+ cancelCurrentBaseStateRequestLocked(isThermalCritical ? FLAG_THERMAL_CRITICAL : 0);
}
if (mRequest != null && !contains(newSupportedStates, mRequest.getRequestedState())) {
- cancelCurrentRequestLocked();
+ cancelCurrentRequestLocked(isThermalCritical ? FLAG_THERMAL_CRITICAL : 0);
}
}
@@ -244,7 +252,11 @@
}
private void cancelRequestLocked(@NonNull OverrideRequest requestToCancel) {
- mListener.onStatusChanged(requestToCancel, STATUS_CANCELED);
+ cancelRequestLocked(requestToCancel, 0 /* flags */);
+ }
+
+ private void cancelRequestLocked(@NonNull OverrideRequest requestToCancel, int flags) {
+ mListener.onStatusChanged(requestToCancel, STATUS_CANCELED, flags);
}
/**
@@ -252,12 +264,16 @@
* Notifies the listener of the canceled status as well.
*/
private void cancelCurrentRequestLocked() {
+ cancelCurrentRequestLocked(0 /* flags */);
+ }
+
+ private void cancelCurrentRequestLocked(int flags) {
if (mRequest == null) {
Slog.w(TAG, "Attempted to cancel a null OverrideRequest");
return;
}
mStickyRequest = false;
- cancelRequestLocked(mRequest);
+ cancelRequestLocked(mRequest, flags);
mRequest = null;
}
@@ -266,11 +282,15 @@
* Notifies the listener of the canceled status as well.
*/
private void cancelCurrentBaseStateRequestLocked() {
+ cancelCurrentBaseStateRequestLocked(0 /* flags */);
+ }
+
+ private void cancelCurrentBaseStateRequestLocked(int flags) {
if (mBaseStateRequest == null) {
Slog.w(TAG, "Attempted to cancel a null OverrideRequest");
return;
}
- cancelRequestLocked(mBaseStateRequest);
+ cancelRequestLocked(mBaseStateRequest, flags);
mBaseStateRequest = null;
}
@@ -284,12 +304,14 @@
}
public interface StatusChangeListener {
+
/**
* Notifies the listener of a change in request status. If a change within the controller
* causes one request to become active and one to become either suspended or cancelled, this
* method is guaranteed to be called with the active request first before the suspended or
* cancelled request.
*/
- void onStatusChanged(@NonNull OverrideRequest request, @RequestStatus int newStatus);
+ void onStatusChanged(@NonNull OverrideRequest request, @RequestStatus int newStatus,
+ int flags);
}
}
diff --git a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
index 188672a..7026529 100644
--- a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
+++ b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
@@ -22,6 +22,7 @@
import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.util.SparseArray;
+import android.view.Display;
import android.view.DisplayAddress;
import com.android.internal.annotations.VisibleForTesting;
@@ -114,15 +115,19 @@
Slog.i(TAG, "Display layout config not found: " + configFile);
return;
}
+ int leadDisplayId = Display.DEFAULT_DISPLAY;
for (com.android.server.display.config.layout.Layout l : layouts.getLayout()) {
final int state = l.getState().intValue();
final Layout layout = createLayout(state);
for (com.android.server.display.config.layout.Display d: l.getDisplay()) {
+ assert layout != null;
Layout.Display display = layout.createDisplayLocked(
DisplayAddress.fromPhysicalDisplayId(d.getAddress().longValue()),
d.isDefaultDisplay(),
d.isEnabled(),
- mIdProducer);
+ mIdProducer,
+ d.getBrightnessThrottlingMapId(),
+ leadDisplayId);
if (FRONT_STRING.equals(d.getPosition())) {
display.setPosition(POSITION_FRONT);
@@ -131,6 +136,7 @@
} else {
display.setPosition(POSITION_UNKNOWN);
}
+ display.setRefreshRateZoneId(d.getRefreshRateZoneId());
}
}
} catch (IOException | DatatypeConfigurationException | XmlPullParserException e) {
diff --git a/services/core/java/com/android/server/display/DisplayControl.java b/services/core/java/com/android/server/display/DisplayControl.java
index fa9a100..f229d0f 100644
--- a/services/core/java/com/android/server/display/DisplayControl.java
+++ b/services/core/java/com/android/server/display/DisplayControl.java
@@ -28,7 +28,8 @@
* Calls into SurfaceFlinger for Display creation and deletion.
*/
public class DisplayControl {
- private static native IBinder nativeCreateDisplay(String name, boolean secure);
+ private static native IBinder nativeCreateDisplay(String name, boolean secure,
+ float requestedRefreshRate);
private static native void nativeDestroyDisplay(IBinder displayToken);
private static native void nativeOverrideHdrTypes(IBinder displayToken, int[] modes);
private static native long[] nativeGetPhysicalDisplayIds();
@@ -46,7 +47,25 @@
*/
public static IBinder createDisplay(String name, boolean secure) {
Objects.requireNonNull(name, "name must not be null");
- return nativeCreateDisplay(name, secure);
+ return nativeCreateDisplay(name, secure, 0.0f);
+ }
+
+ /**
+ * Create a display in SurfaceFlinger.
+ *
+ * @param name The name of the display
+ * @param secure Whether this display is secure.
+ * @param requestedRefreshRate The requested refresh rate in frames per second.
+ * For best results, specify a divisor of the physical refresh rate, e.g., 30 or 60 on
+ * 120hz display. If an arbitrary refresh rate is specified, the rate will be rounded
+ * up or down to a divisor of the physical display. If 0 is specified, the virtual
+ * display is refreshed at the physical display refresh rate.
+ * @return The token reference for the display in SurfaceFlinger.
+ */
+ public static IBinder createDisplay(String name, boolean secure,
+ float requestedRefreshRate) {
+ Objects.requireNonNull(name, "name must not be null");
+ return nativeCreateDisplay(name, secure, requestedRefreshRate);
}
/**
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index e048a0f..a107f33 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -33,6 +33,7 @@
import android.util.Slog;
import android.util.Spline;
import android.view.DisplayAddress;
+import android.view.SurfaceControl;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
@@ -53,6 +54,7 @@
import com.android.server.display.config.Point;
import com.android.server.display.config.RefreshRateConfigs;
import com.android.server.display.config.RefreshRateRange;
+import com.android.server.display.config.RefreshRateZone;
import com.android.server.display.config.SdrHdrRatioMap;
import com.android.server.display.config.SdrHdrRatioPoint;
import com.android.server.display.config.SensorDetails;
@@ -74,8 +76,10 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.HashMap;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import javax.xml.datatype.DatatypeConfigurationException;
@@ -411,6 +415,8 @@
public static final String QUIRK_CAN_SET_BRIGHTNESS_VIA_HWC = "canSetBrightnessViaHwc";
+ static final String DEFAULT_BRIGHTNESS_THROTTLING_DATA_ID = "default";
+
private static final float BRIGHTNESS_DEFAULT = 0.5f;
private static final String ETC_DIR = "etc";
private static final String DISPLAY_CONFIG_DIR = "displayconfig";
@@ -421,6 +427,8 @@
private static final String STABLE_ID_SUFFIX_FORMAT = "id_%d";
private static final String NO_SUFFIX_FORMAT = "%d";
private static final long STABLE_FLAG = 1L << 62;
+ private static final int DEFAULT_PEAK_REFRESH_RATE = 0;
+ private static final int DEFAULT_REFRESH_RATE = 60;
private static final int DEFAULT_LOW_REFRESH_RATE = 60;
private static final int DEFAULT_HIGH_REFRESH_RATE = 0;
private static final int[] DEFAULT_BRIGHTNESS_THRESHOLDS = new int[]{};
@@ -497,10 +505,10 @@
/**
* Array of light sensor lux values to define our levels for auto backlight
* brightness support.
-
+ *
* The N + 1 entries of this array define N control points defined in mBrightnessLevelsNits,
* with first value always being 0 lux
-
+ *
* The control points must be strictly increasing. Each control point
* corresponds to an entry in the brightness backlight values arrays.
* For example, if lux == level[1] (second element of the levels array)
@@ -509,7 +517,6 @@
*
* Spline interpolation is used to determine the auto-brightness
* backlight values for lux levels between these control points.
- *
*/
private float[] mBrightnessLevelsLux;
@@ -589,17 +596,33 @@
* using higher refresh rates, even if display modes with higher refresh rates are available
* from hardware composer. Only has an effect if the value is non-zero.
*/
- private int mDefaultHighRefreshRate = DEFAULT_HIGH_REFRESH_RATE;
+ private int mDefaultPeakRefreshRate = DEFAULT_PEAK_REFRESH_RATE;
/**
* The default refresh rate for a given device. This value sets the higher default
* refresh rate. If the hardware composer on the device supports display modes with
* a higher refresh rate than the default value specified here, the framework may use those
* higher refresh rate modes if an app chooses one by setting preferredDisplayModeId or calling
- * setFrameRate(). We have historically allowed fallback to mDefaultHighRefreshRate if
- * mDefaultLowRefreshRate is set to 0, but this is not supported anymore.
+ * setFrameRate(). We have historically allowed fallback to mDefaultPeakRefreshRate if
+ * mDefaultRefreshRate is set to 0, but this is not supported anymore.
*/
- private int mDefaultLowRefreshRate = DEFAULT_LOW_REFRESH_RATE;
+ private int mDefaultRefreshRate = DEFAULT_REFRESH_RATE;
+
+ /**
+ * Default refresh rate in the high zone defined by brightness and ambient thresholds.
+ * If non-positive, then the refresh rate is unchanged even if thresholds are configured.
+ */
+ private int mDefaultHighBlockingZoneRefreshRate = DEFAULT_HIGH_REFRESH_RATE;
+
+ /**
+ * Default refresh rate in the zone defined by brightness and ambient thresholds.
+ * If non-positive, then the refresh rate is unchanged even if thresholds are configured.
+ */
+ private int mDefaultLowBlockingZoneRefreshRate = DEFAULT_LOW_REFRESH_RATE;
+
+ // Refresh rate profiles, currently only for concurrent mode profile and controlled by Layout
+ private final Map<String, SurfaceControl.RefreshRateRange> mRefreshRateZoneProfiles =
+ new HashMap<>();
/**
* The display uses different gamma curves for different refresh rates. It's hard for panel
@@ -627,13 +650,7 @@
private int[] mHighDisplayBrightnessThresholds = DEFAULT_BRIGHTNESS_THRESHOLDS;
private int[] mHighAmbientBrightnessThresholds = DEFAULT_BRIGHTNESS_THRESHOLDS;
- // Brightness Throttling data may be updated via the DeviceConfig. Here we store the original
- // data, which comes from the ddc, and the current one, which may be the DeviceConfig
- // overwritten value.
- private BrightnessThrottlingData mBrightnessThrottlingData;
- private BrightnessThrottlingData mOriginalBrightnessThrottlingData;
- // The concurrent displays mode might need a stricter throttling policy
- private BrightnessThrottlingData mConcurrentDisplaysBrightnessThrottlingData;
+ private Map<String, BrightnessThrottlingData> mBrightnessThrottlingDataMap = new HashMap();
@Nullable
private HostUsiVersion mHostUsiVersion;
@@ -779,10 +796,6 @@
return config;
}
- void setBrightnessThrottlingData(BrightnessThrottlingData brightnessThrottlingData) {
- mBrightnessThrottlingData = brightnessThrottlingData;
- }
-
/**
* Return the brightness mapping nits array.
*
@@ -1282,18 +1295,11 @@
}
/**
+ * @param id The ID of the throttling data
* @return brightness throttling configuration data for the display.
*/
- public BrightnessThrottlingData getBrightnessThrottlingData() {
- return BrightnessThrottlingData.create(mBrightnessThrottlingData);
- }
-
- /**
- * @return brightness throttling configuration data for the display for the concurrent
- * displays mode.
- */
- public BrightnessThrottlingData getConcurrentDisplaysBrightnessThrottlingData() {
- return BrightnessThrottlingData.create(mConcurrentDisplaysBrightnessThrottlingData);
+ public BrightnessThrottlingData getBrightnessThrottlingData(String id) {
+ return BrightnessThrottlingData.create(mBrightnessThrottlingDataMap.get(id));
}
/**
@@ -1327,15 +1333,46 @@
/**
* @return Default peak refresh rate of the associated display
*/
- public int getDefaultHighRefreshRate() {
- return mDefaultHighRefreshRate;
+ public int getDefaultPeakRefreshRate() {
+ return mDefaultPeakRefreshRate;
}
/**
* @return Default refresh rate of the associated display
*/
- public int getDefaultLowRefreshRate() {
- return mDefaultLowRefreshRate;
+ public int getDefaultRefreshRate() {
+ return mDefaultRefreshRate;
+ }
+
+ /**
+ * @return Default refresh rate in the higher blocking zone of the associated display
+ */
+ public int getDefaultHighBlockingZoneRefreshRate() {
+ return mDefaultHighBlockingZoneRefreshRate;
+ }
+
+ /**
+ * @return Default refresh rate in the lower blocking zone of the associated display
+ */
+ public int getDefaultLowBlockingZoneRefreshRate() {
+ return mDefaultLowBlockingZoneRefreshRate;
+ }
+
+ /**
+ * @return Refresh rate range for specific profile id or null
+ */
+ @Nullable
+ public SurfaceControl.RefreshRateRange getRefreshRange(@Nullable String id) {
+ if (TextUtils.isEmpty(id)) {
+ return null;
+ }
+ return mRefreshRateZoneProfiles.get(id);
+ }
+
+ @NonNull
+ @VisibleForTesting
+ Map<String, SurfaceControl.RefreshRateRange> getRefreshRangeProfiles() {
+ return mRefreshRateZoneProfiles;
}
/**
@@ -1411,8 +1448,7 @@
+ ", isHbmEnabled=" + mIsHighBrightnessModeEnabled
+ ", mHbmData=" + mHbmData
+ ", mSdrToHdrRatioSpline=" + mSdrToHdrRatioSpline
- + ", mBrightnessThrottlingData=" + mBrightnessThrottlingData
- + ", mOriginalBrightnessThrottlingData=" + mOriginalBrightnessThrottlingData
+ + ", mBrightnessThrottlingData=" + mBrightnessThrottlingDataMap
+ "\n"
+ ", mBrightnessRampFastDecrease=" + mBrightnessRampFastDecrease
+ ", mBrightnessRampFastIncrease=" + mBrightnessRampFastIncrease
@@ -1482,8 +1518,11 @@
+ ", mDdcAutoBrightnessAvailable= " + mDdcAutoBrightnessAvailable
+ ", mAutoBrightnessAvailable= " + mAutoBrightnessAvailable
+ "\n"
- + ", mDefaultRefreshRate= " + mDefaultLowRefreshRate
- + ", mDefaultPeakRefreshRate= " + mDefaultHighRefreshRate
+ + ", mDefaultLowBlockingZoneRefreshRate= " + mDefaultLowBlockingZoneRefreshRate
+ + ", mDefaultHighBlockingZoneRefreshRate= " + mDefaultHighBlockingZoneRefreshRate
+ + ", mDefaultPeakRefreshRate= " + mDefaultPeakRefreshRate
+ + ", mDefaultRefreshRate= " + mDefaultRefreshRate
+ + ", mRefreshRateZoneProfiles= " + mRefreshRateZoneProfiles
+ ", mLowDisplayBrightnessThresholds= "
+ Arrays.toString(mLowDisplayBrightnessThresholds)
+ ", mLowAmbientBrightnessThresholds= "
@@ -1545,8 +1584,7 @@
loadBrightnessDefaultFromDdcXml(config);
loadBrightnessConstraintsFromConfigXml();
loadBrightnessMap(config);
- loadBrightnessThrottlingMap(config);
- loadConcurrentDisplaysBrightnessThrottlingMap(config);
+ loadBrightnessThrottlingMaps(config);
loadHighBrightnessModeData(config);
loadQuirks(config);
loadBrightnessRamps(config);
@@ -1756,76 +1794,47 @@
return Spline.createSpline(nits, ratios);
}
- private void loadBrightnessThrottlingMap(DisplayConfiguration config) {
+ private void loadBrightnessThrottlingMaps(DisplayConfiguration config) {
final ThermalThrottling throttlingConfig = config.getThermalThrottling();
if (throttlingConfig == null) {
Slog.i(TAG, "No thermal throttling config found");
return;
}
- final BrightnessThrottlingMap map = throttlingConfig.getBrightnessThrottlingMap();
- if (map == null) {
+ final List<BrightnessThrottlingMap> maps = throttlingConfig.getBrightnessThrottlingMap();
+ if (maps == null || maps.isEmpty()) {
Slog.i(TAG, "No brightness throttling map found");
return;
}
- final List<BrightnessThrottlingPoint> points = map.getBrightnessThrottlingPoint();
- // At least 1 point is guaranteed by the display device config schema
- List<BrightnessThrottlingData.ThrottlingLevel> throttlingLevels =
- new ArrayList<>(points.size());
+ for (BrightnessThrottlingMap map : maps) {
+ final List<BrightnessThrottlingPoint> points = map.getBrightnessThrottlingPoint();
+ // At least 1 point is guaranteed by the display device config schema
+ List<BrightnessThrottlingData.ThrottlingLevel> throttlingLevels =
+ new ArrayList<>(points.size());
- boolean badConfig = false;
- for (BrightnessThrottlingPoint point : points) {
- ThermalStatus status = point.getThermalStatus();
- if (!thermalStatusIsValid(status)) {
- badConfig = true;
- break;
+ boolean badConfig = false;
+ for (BrightnessThrottlingPoint point : points) {
+ ThermalStatus status = point.getThermalStatus();
+ if (!thermalStatusIsValid(status)) {
+ badConfig = true;
+ break;
+ }
+
+ throttlingLevels.add(new BrightnessThrottlingData.ThrottlingLevel(
+ convertThermalStatus(status), point.getBrightness().floatValue()));
}
- throttlingLevels.add(new BrightnessThrottlingData.ThrottlingLevel(
- convertThermalStatus(status), point.getBrightness().floatValue()));
- }
-
- if (!badConfig) {
- mBrightnessThrottlingData = BrightnessThrottlingData.create(throttlingLevels);
- mOriginalBrightnessThrottlingData = mBrightnessThrottlingData;
- }
- }
-
- private void loadConcurrentDisplaysBrightnessThrottlingMap(DisplayConfiguration config) {
- final ThermalThrottling throttlingConfig = config.getThermalThrottling();
- if (throttlingConfig == null) {
- Slog.i(TAG, "No concurrent displays thermal throttling config found");
- return;
- }
-
- final BrightnessThrottlingMap map =
- throttlingConfig.getConcurrentDisplaysBrightnessThrottlingMap();
- if (map == null) {
- Slog.i(TAG, "No concurrent displays brightness throttling map found");
- return;
- }
-
- final List<BrightnessThrottlingPoint> points = map.getBrightnessThrottlingPoint();
- // At least 1 point is guaranteed by the display device config schema
- List<BrightnessThrottlingData.ThrottlingLevel> throttlingLevels =
- new ArrayList<>(points.size());
-
- boolean badConfig = false;
- for (BrightnessThrottlingPoint point : points) {
- ThermalStatus status = point.getThermalStatus();
- if (!thermalStatusIsValid(status)) {
- badConfig = true;
- break;
+ if (!badConfig) {
+ String id = map.getId() == null ? DEFAULT_BRIGHTNESS_THROTTLING_DATA_ID
+ : map.getId();
+ if (mBrightnessThrottlingDataMap.containsKey(id)) {
+ throw new RuntimeException("Brightness throttling data with ID " + id
+ + " already exists");
+ }
+ mBrightnessThrottlingDataMap.put(id,
+ BrightnessThrottlingData.create(throttlingLevels));
}
-
- throttlingLevels.add(new BrightnessThrottlingData.ThrottlingLevel(
- convertThermalStatus(status), point.getBrightness().floatValue()));
- }
-
- if (!badConfig) {
- mConcurrentDisplaysBrightnessThrottlingData =
- BrightnessThrottlingData.create(throttlingLevels);
}
}
@@ -1838,10 +1847,47 @@
BlockingZoneConfig higherBlockingZoneConfig =
(refreshRateConfigs == null) ? null
: refreshRateConfigs.getHigherBlockingZoneConfigs();
+ loadPeakDefaultRefreshRate(refreshRateConfigs);
+ loadDefaultRefreshRate(refreshRateConfigs);
loadLowerRefreshRateBlockingZones(lowerBlockingZoneConfig);
loadHigherRefreshRateBlockingZones(higherBlockingZoneConfig);
+ loadRefreshRateZoneProfiles(refreshRateConfigs);
}
+ private void loadPeakDefaultRefreshRate(RefreshRateConfigs refreshRateConfigs) {
+ if (refreshRateConfigs == null || refreshRateConfigs.getDefaultPeakRefreshRate() == null) {
+ mDefaultPeakRefreshRate = mContext.getResources().getInteger(
+ R.integer.config_defaultPeakRefreshRate);
+ } else {
+ mDefaultPeakRefreshRate =
+ refreshRateConfigs.getDefaultPeakRefreshRate().intValue();
+ }
+ }
+
+ private void loadDefaultRefreshRate(RefreshRateConfigs refreshRateConfigs) {
+ if (refreshRateConfigs == null || refreshRateConfigs.getDefaultRefreshRate() == null) {
+ mDefaultRefreshRate = mContext.getResources().getInteger(
+ R.integer.config_defaultRefreshRate);
+ } else {
+ mDefaultRefreshRate =
+ refreshRateConfigs.getDefaultRefreshRate().intValue();
+ }
+ }
+
+ /** Loads the refresh rate profiles. */
+ private void loadRefreshRateZoneProfiles(RefreshRateConfigs refreshRateConfigs) {
+ if (refreshRateConfigs == null) {
+ return;
+ }
+ for (RefreshRateZone zone :
+ refreshRateConfigs.getRefreshRateZoneProfiles().getRefreshRateZoneProfile()) {
+ RefreshRateRange range = zone.getRefreshRateRange();
+ mRefreshRateZoneProfiles.put(
+ zone.getId(),
+ new SurfaceControl.RefreshRateRange(
+ range.getMinimum().floatValue(), range.getMaximum().floatValue()));
+ }
+ }
/**
* Loads the refresh rate configurations pertaining to the upper blocking zones.
@@ -1866,10 +1912,10 @@
private void loadHigherBlockingZoneDefaultRefreshRate(
BlockingZoneConfig upperBlockingZoneConfig) {
if (upperBlockingZoneConfig == null) {
- mDefaultHighRefreshRate = mContext.getResources().getInteger(
- com.android.internal.R.integer.config_defaultPeakRefreshRate);
+ mDefaultHighBlockingZoneRefreshRate = mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_fixedRefreshRateInHighZone);
} else {
- mDefaultHighRefreshRate =
+ mDefaultHighBlockingZoneRefreshRate =
upperBlockingZoneConfig.getDefaultRefreshRate().intValue();
}
}
@@ -1881,10 +1927,10 @@
private void loadLowerBlockingZoneDefaultRefreshRate(
BlockingZoneConfig lowerBlockingZoneConfig) {
if (lowerBlockingZoneConfig == null) {
- mDefaultLowRefreshRate = mContext.getResources().getInteger(
- com.android.internal.R.integer.config_defaultRefreshRate);
+ mDefaultLowBlockingZoneRefreshRate = mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_defaultRefreshRateInZone);
} else {
- mDefaultLowRefreshRate =
+ mDefaultLowBlockingZoneRefreshRate =
lowerBlockingZoneConfig.getDefaultRefreshRate().intValue();
}
}
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 3759a8b..237e78b 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -142,6 +142,7 @@
import com.android.server.UiThread;
import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
import com.android.server.display.DisplayDeviceConfig.SensorData;
+import com.android.server.display.layout.Layout;
import com.android.server.display.utils.SensorUtils;
import com.android.server.input.InputManagerInternal;
import com.android.server.wm.SurfaceAnimationThread;
@@ -245,7 +246,7 @@
private Display.Mode mUserPreferredMode;
// HDR conversion mode chosen by user
@GuardedBy("mSyncRoot")
- private HdrConversionMode mHdrConversionMode;
+ private HdrConversionMode mHdrConversionMode = null;
// The synchronization root for the display manager.
// This lock guards most of the display manager's state.
@@ -647,6 +648,7 @@
updateSettingsLocked();
updateUserDisabledHdrTypesFromSettingsLocked();
updateUserPreferredDisplayModeSettingsLocked();
+ updateHdrConversionModeSettingsLocked();
}
mDisplayModeDirector.setDesiredDisplayModeSpecsListener(
@@ -1664,12 +1666,37 @@
return;
}
- // TODO (b/265793751): Set this DPC as a follower of the default DPC if needed,
- // clear this DPC's followers if it's not a lead display
+ final int leadDisplayId = display.getLeadDisplayIdLocked();
+ updateDisplayPowerControllerLeaderLocked(dpc, leadDisplayId);
final String uniqueId = device.getUniqueId();
HighBrightnessModeMetadata hbmMetadata = mHighBrightnessModeMetadataMap.get(uniqueId);
- dpc.onDisplayChanged(hbmMetadata);
+ dpc.onDisplayChanged(hbmMetadata, leadDisplayId);
+ }
+ }
+
+ private void updateDisplayPowerControllerLeaderLocked(DisplayPowerControllerInterface dpc,
+ int leadDisplayId) {
+ if (dpc.getLeadDisplayId() == leadDisplayId) {
+ // Lead display hasn't changed, nothing to do.
+ return;
+ }
+
+ // If it has changed, then we need to unregister from the previous leader if there was one.
+ final int prevLeaderId = dpc.getLeadDisplayId();
+ if (prevLeaderId != Layout.NO_LEAD_DISPLAY) {
+ final DisplayPowerControllerInterface prevLeader =
+ mDisplayPowerControllers.get(prevLeaderId);
+ if (prevLeader != null) {
+ prevLeader.removeDisplayBrightnessFollower(dpc);
+ }
+ }
+
+ // And then, if it's following, register it with the new one.
+ if (leadDisplayId != Layout.NO_LEAD_DISPLAY) {
+ final DisplayPowerControllerInterface newLead =
+ mDisplayPowerControllers.get(leadDisplayId);
+ newLead.addDisplayBrightnessFollower(dpc);
}
}
@@ -1733,9 +1760,13 @@
+ display.getDisplayIdLocked());
return;
}
+
+ final int leadDisplayId = display.getLeadDisplayIdLocked();
+ updateDisplayPowerControllerLeaderLocked(dpc, leadDisplayId);
+
final String uniqueId = device.getUniqueId();
HighBrightnessModeMetadata hbmMetadata = mHighBrightnessModeMetadataMap.get(uniqueId);
- dpc.onDisplayChanged(hbmMetadata);
+ dpc.onDisplayChanged(hbmMetadata, leadDisplayId);
}
}
@@ -1806,6 +1837,31 @@
device.setUserPreferredDisplayModeLocked(modeBuilder.build());
}
+ @GuardedBy("mSyncRoot")
+ private void storeHdrConversionModeLocked(HdrConversionMode hdrConversionMode) {
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.HDR_CONVERSION_MODE, hdrConversionMode.getConversionMode());
+ final int preferredHdrOutputType =
+ hdrConversionMode.getConversionMode() == HdrConversionMode.HDR_CONVERSION_FORCE
+ ? hdrConversionMode.getPreferredHdrOutputType()
+ : Display.HdrCapabilities.HDR_TYPE_INVALID;
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.HDR_FORCE_CONVERSION_TYPE, preferredHdrOutputType);
+ }
+
+ @GuardedBy("mSyncRoot")
+ void updateHdrConversionModeSettingsLocked() {
+ final int conversionMode = Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.HDR_CONVERSION_MODE, HdrConversionMode.HDR_CONVERSION_SYSTEM);
+ final int preferredHdrOutputType = conversionMode == HdrConversionMode.HDR_CONVERSION_FORCE
+ ? Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.HDR_FORCE_CONVERSION_TYPE,
+ Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION)
+ : Display.HdrCapabilities.HDR_TYPE_INVALID;
+ mHdrConversionMode = new HdrConversionMode(conversionMode, preferredHdrOutputType);
+ setHdrConversionModeInternal(mHdrConversionMode);
+ }
+
// If we've never recorded stable device stats for this device before and they aren't
// explicitly configured, go ahead and record the stable device stats now based on the status
// of the default display at first boot.
@@ -1962,13 +2018,14 @@
int[] autoHdrOutputTypes = null;
synchronized (mSyncRoot) {
mHdrConversionMode = hdrConversionMode;
+ storeHdrConversionModeLocked(mHdrConversionMode);
// For auto mode, all supported HDR types are allowed except the ones specifically
// disabled by the user.
if (hdrConversionMode.getConversionMode() == HdrConversionMode.HDR_CONVERSION_SYSTEM) {
autoHdrOutputTypes = getEnabledAutoHdrTypesLocked();
}
- DisplayControl.setHdrConversionMode(hdrConversionMode.getConversionMode(),
+ mInjector.setHdrConversionMode(hdrConversionMode.getConversionMode(),
hdrConversionMode.getPreferredHdrOutputType(), autoHdrOutputTypes);
}
}
@@ -1984,7 +2041,7 @@
private @Display.HdrCapabilities.HdrType int[] getSupportedHdrOutputTypesInternal() {
if (mSupportedHdrOutputType == null) {
- mSupportedHdrOutputType = DisplayControl.getSupportedHdrOutputTypes();
+ mSupportedHdrOutputType = mInjector.getSupportedHdrOutputTypes();
}
return mSupportedHdrOutputType;
}
@@ -2604,6 +2661,10 @@
}
}
+ if (mHdrConversionMode != null) {
+ pw.println(" mHdrConversionMode=" + mHdrConversionMode);
+ }
+
pw.println();
final int displayStateCount = mDisplayStates.size();
pw.println("Display States: size=" + displayStateCount);
@@ -2712,6 +2773,16 @@
long getDefaultDisplayDelayTimeout() {
return WAIT_FOR_DEFAULT_DISPLAY_TIMEOUT;
}
+
+ void setHdrConversionMode(int conversionMode, int preferredHdrOutputType,
+ int[] autoHdrTypes) {
+ DisplayControl.setHdrConversionMode(conversionMode, preferredHdrOutputType,
+ autoHdrTypes);
+ }
+
+ int[] getSupportedHdrOutputTypes() {
+ return DisplayControl.getSupportedHdrOutputTypes();
+ }
}
@VisibleForTesting
diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java
index 72c9a4a..91ef167 100644
--- a/services/core/java/com/android/server/display/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/DisplayModeDirector.java
@@ -159,7 +159,6 @@
mSupportedModesByDisplay = new SparseArray<>();
mDefaultModeByDisplay = new SparseArray<>();
mAppRequestObserver = new AppRequestObserver();
- mDisplayObserver = new DisplayObserver(context, handler);
mDeviceConfig = injector.getDeviceConfig();
mDeviceConfigDisplaySettings = new DeviceConfigDisplaySettings();
mSettingsObserver = new SettingsObserver(context, handler);
@@ -170,6 +169,7 @@
updateVoteLocked(displayId, priority, vote);
}
};
+ mDisplayObserver = new DisplayObserver(context, handler, ballotBox);
mSensorObserver = new SensorObserver(context, ballotBox, injector);
mSkinThermalStatusObserver = new SkinThermalStatusObserver(injector, ballotBox);
mHbmObserver = new HbmObserver(injector, ballotBox, BackgroundThread.getHandler(),
@@ -733,7 +733,7 @@
/**
* Sets the display mode switching type.
- * @param newType
+ * @param newType new mode switching type
*/
public void setModeSwitchingType(@DisplayManager.SwitchingType int newType) {
synchronized (mLock) {
@@ -850,6 +850,18 @@
notifyDesiredDisplayModeSpecsChangedLocked();
}
+ @GuardedBy("mLock")
+ private float getMaxRefreshRateLocked(int displayId) {
+ Display.Mode[] modes = mSupportedModesByDisplay.get(displayId);
+ float maxRefreshRate = 0f;
+ for (Display.Mode mode : modes) {
+ if (mode.getRefreshRate() > maxRefreshRate) {
+ maxRefreshRate = mode.getRefreshRate();
+ }
+ }
+ return maxRefreshRate;
+ }
+
private void notifyDesiredDisplayModeSpecsChangedLocked() {
if (mDesiredDisplayModeSpecsListener != null
&& !mHandler.hasMessages(MSG_REFRESH_RATE_RANGE_CHANGED)) {
@@ -1186,26 +1198,33 @@
// rest of low priority voters. It votes [0, max(PEAK, MIN)]
public static final int PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE = 7;
+ // To avoid delay in switching between 60HZ -> 90HZ when activating LHBM, set refresh
+ // rate to max value (same as for PRIORITY_UDFPS) on lock screen
+ public static final int PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE = 8;
+
+ // For concurrent displays we want to limit refresh rate on all displays
+ public static final int PRIORITY_LAYOUT_LIMITED_FRAME_RATE = 9;
+
// LOW_POWER_MODE force the render frame rate to [0, 60HZ] if
// Settings.Global.LOW_POWER_MODE is on.
- public static final int PRIORITY_LOW_POWER_MODE = 8;
+ public static final int PRIORITY_LOW_POWER_MODE = 10;
// PRIORITY_FLICKER_REFRESH_RATE_SWITCH votes for disabling refresh rate switching. If the
// higher priority voters' result is a range, it will fix the rate to a single choice.
// It's used to avoid refresh rate switches in certain conditions which may result in the
// user seeing the display flickering when the switches occur.
- public static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 9;
+ public static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 11;
// Force display to [0, 60HZ] if skin temperature is at or above CRITICAL.
- public static final int PRIORITY_SKIN_TEMPERATURE = 10;
+ public static final int PRIORITY_SKIN_TEMPERATURE = 12;
// The proximity sensor needs the refresh rate to be locked in order to function, so this is
// set to a high priority.
- public static final int PRIORITY_PROXIMITY = 11;
+ public static final int PRIORITY_PROXIMITY = 13;
// The Under-Display Fingerprint Sensor (UDFPS) needs the refresh rate to be locked in order
// to function, so this needs to be the highest priority of all votes.
- public static final int PRIORITY_UDFPS = 12;
+ public static final int PRIORITY_UDFPS = 14;
// Whenever a new priority is added, remember to update MIN_PRIORITY, MAX_PRIORITY, and
// APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF, as well as priorityToString.
@@ -1322,6 +1341,8 @@
return "PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE";
case PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE:
return "PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE";
+ case PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE:
+ return "PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE";
default:
return Integer.toString(priority);
}
@@ -1373,7 +1394,7 @@
mDefaultRefreshRate =
(displayDeviceConfig == null) ? (float) mContext.getResources().getInteger(
R.integer.config_defaultRefreshRate)
- : (float) displayDeviceConfig.getDefaultLowRefreshRate();
+ : (float) displayDeviceConfig.getDefaultRefreshRate();
}
public void observe() {
@@ -1460,7 +1481,7 @@
defaultPeakRefreshRate =
(displayDeviceConfig == null) ? (float) mContext.getResources().getInteger(
R.integer.config_defaultPeakRefreshRate)
- : (float) displayDeviceConfig.getDefaultHighRefreshRate();
+ : (float) displayDeviceConfig.getDefaultPeakRefreshRate();
}
mDefaultPeakRefreshRate = defaultPeakRefreshRate;
}
@@ -1657,10 +1678,12 @@
// calling into us already holding its own lock.
private final Context mContext;
private final Handler mHandler;
+ private final BallotBox mBallotBox;
- DisplayObserver(Context context, Handler handler) {
+ DisplayObserver(Context context, Handler handler, BallotBox ballotBox) {
mContext = context;
mHandler = handler;
+ mBallotBox = ballotBox;
}
public void observe() {
@@ -1689,7 +1712,9 @@
@Override
public void onDisplayAdded(int displayId) {
- updateDisplayModes(displayId);
+ DisplayInfo displayInfo = getDisplayInfo(displayId);
+ updateDisplayModes(displayId, displayInfo);
+ updateLayoutLimitedFrameRate(displayId, displayInfo);
}
@Override
@@ -1698,23 +1723,41 @@
mSupportedModesByDisplay.remove(displayId);
mDefaultModeByDisplay.remove(displayId);
}
+ updateLayoutLimitedFrameRate(displayId, null);
}
@Override
public void onDisplayChanged(int displayId) {
- updateDisplayModes(displayId);
+ DisplayInfo displayInfo = getDisplayInfo(displayId);
+ updateDisplayModes(displayId, displayInfo);
+ updateLayoutLimitedFrameRate(displayId, displayInfo);
}
- private void updateDisplayModes(int displayId) {
+ @Nullable
+ private DisplayInfo getDisplayInfo(int displayId) {
Display d = mContext.getSystemService(DisplayManager.class).getDisplay(displayId);
if (d == null) {
// We can occasionally get a display added or changed event for a display that was
// subsequently removed, which means this returns null. Check this case and bail
// out early; if it gets re-attached we'll eventually get another call back for it.
- return;
+ return null;
}
DisplayInfo info = new DisplayInfo();
d.getDisplayInfo(info);
+ return info;
+ }
+
+ private void updateLayoutLimitedFrameRate(int displayId, @Nullable DisplayInfo info) {
+ Vote vote = info != null && info.layoutLimitedRefreshRate != null
+ ? Vote.forPhysicalRefreshRates(info.layoutLimitedRefreshRate.min,
+ info.layoutLimitedRefreshRate.max) : null;
+ mBallotBox.vote(displayId, Vote.PRIORITY_LAYOUT_LIMITED_FRAME_RATE, vote);
+ }
+
+ private void updateDisplayModes(int displayId, @Nullable DisplayInfo info) {
+ if (info == null) {
+ return;
+ }
boolean changed = false;
synchronized (mLock) {
if (!Arrays.equals(mSupportedModesByDisplay.get(displayId), info.supportedModes)) {
@@ -1819,8 +1862,26 @@
return mHighAmbientBrightnessThresholds;
}
+ /**
+ * @return the refresh rate to lock to when in a high brightness zone
+ */
+ @VisibleForTesting
+ int getRefreshRateInHighZone() {
+ return mRefreshRateInHighZone;
+ }
+
+ /**
+ * @return the refresh rate to lock to when in a low brightness zone
+ */
+ @VisibleForTesting
+ int getRefreshRateInLowZone() {
+ return mRefreshRateInLowZone;
+ }
+
private void loadLowBrightnessThresholds(DisplayDeviceConfig displayDeviceConfig,
boolean attemptLoadingFromDeviceConfig) {
+ loadRefreshRateInHighZone(displayDeviceConfig, attemptLoadingFromDeviceConfig);
+ loadRefreshRateInLowZone(displayDeviceConfig, attemptLoadingFromDeviceConfig);
mLowDisplayBrightnessThresholds = loadBrightnessThresholds(
() -> mDeviceConfigDisplaySettings.getLowDisplayBrightnessThresholds(),
() -> displayDeviceConfig.getLowDisplayBrightnessThresholds(),
@@ -1841,6 +1902,44 @@
}
}
+ private void loadRefreshRateInLowZone(DisplayDeviceConfig displayDeviceConfig,
+ boolean attemptLoadingFromDeviceConfig) {
+ int refreshRateInLowZone =
+ (displayDeviceConfig == null) ? mContext.getResources().getInteger(
+ R.integer.config_defaultRefreshRateInZone)
+ : displayDeviceConfig.getDefaultLowBlockingZoneRefreshRate();
+ if (attemptLoadingFromDeviceConfig) {
+ try {
+ refreshRateInLowZone = mDeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+ DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE,
+ refreshRateInLowZone);
+ } catch (Exception exception) {
+ // Do nothing
+ }
+ }
+ mRefreshRateInLowZone = refreshRateInLowZone;
+ }
+
+ private void loadRefreshRateInHighZone(DisplayDeviceConfig displayDeviceConfig,
+ boolean attemptLoadingFromDeviceConfig) {
+ int refreshRateInHighZone =
+ (displayDeviceConfig == null) ? mContext.getResources().getInteger(
+ R.integer.config_fixedRefreshRateInHighZone) : displayDeviceConfig
+ .getDefaultHighBlockingZoneRefreshRate();
+ if (attemptLoadingFromDeviceConfig) {
+ try {
+ refreshRateInHighZone = mDeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+ DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HIGH_ZONE,
+ refreshRateInHighZone);
+ } catch (Exception exception) {
+ // Do nothing
+ }
+ }
+ mRefreshRateInHighZone = refreshRateInHighZone;
+ }
+
private void loadHighBrightnessThresholds(DisplayDeviceConfig displayDeviceConfig,
boolean attemptLoadingFromDeviceConfig) {
mHighDisplayBrightnessThresholds = loadBrightnessThresholds(
@@ -1894,14 +1993,6 @@
}
/**
- * @return the refresh to lock to when in a low brightness zone
- */
- @VisibleForTesting
- int getRefreshRateInLowZone() {
- return mRefreshRateInLowZone;
- }
-
- /**
* @return the display brightness thresholds for the low brightness zones
*/
@VisibleForTesting
@@ -1946,8 +2037,17 @@
mHighAmbientBrightnessThresholds = highAmbientBrightnessThresholds;
}
- mRefreshRateInLowZone = mDeviceConfigDisplaySettings.getRefreshRateInLowZone();
- mRefreshRateInHighZone = mDeviceConfigDisplaySettings.getRefreshRateInHighZone();
+ final int refreshRateInLowZone = mDeviceConfigDisplaySettings
+ .getRefreshRateInLowZone();
+ if (refreshRateInLowZone != -1) {
+ mRefreshRateInLowZone = refreshRateInLowZone;
+ }
+
+ final int refreshRateInHighZone = mDeviceConfigDisplaySettings
+ .getRefreshRateInHighZone();
+ if (refreshRateInHighZone != -1) {
+ mRefreshRateInHighZone = refreshRateInHighZone;
+ }
restartObserver();
mDeviceConfigDisplaySettings.startListening();
@@ -2001,6 +2101,10 @@
restartObserver();
}
+ /**
+ * Used to reload the lower blocking zone refresh rate in case of changes in the
+ * DeviceConfig properties.
+ */
public void onDeviceConfigRefreshRateInLowZoneChanged(int refreshRate) {
if (refreshRate != mRefreshRateInLowZone) {
mRefreshRateInLowZone = refreshRate;
@@ -2024,6 +2128,10 @@
restartObserver();
}
+ /**
+ * Used to reload the higher blocking zone refresh rate in case of changes in the
+ * DeviceConfig properties.
+ */
public void onDeviceConfigRefreshRateInHighZoneChanged(int refreshRate) {
if (refreshRate != mRefreshRateInHighZone) {
mRefreshRateInHighZone = refreshRate;
@@ -2474,6 +2582,7 @@
private class UdfpsObserver extends IUdfpsRefreshRateRequestCallback.Stub {
private final SparseBooleanArray mUdfpsRefreshRateEnabled = new SparseBooleanArray();
+ private final SparseBooleanArray mAuthenticationPossible = new SparseBooleanArray();
public void observe() {
StatusBarManagerInternal statusBar =
@@ -2486,38 +2595,38 @@
@Override
public void onRequestEnabled(int displayId) {
synchronized (mLock) {
- updateRefreshRateStateLocked(displayId, true /*enabled*/);
+ mUdfpsRefreshRateEnabled.put(displayId, true);
+ updateVoteLocked(displayId, true, Vote.PRIORITY_UDFPS);
}
}
@Override
public void onRequestDisabled(int displayId) {
synchronized (mLock) {
- updateRefreshRateStateLocked(displayId, false /*enabled*/);
+ mUdfpsRefreshRateEnabled.put(displayId, false);
+ updateVoteLocked(displayId, false, Vote.PRIORITY_UDFPS);
}
}
- private void updateRefreshRateStateLocked(int displayId, boolean enabled) {
- mUdfpsRefreshRateEnabled.put(displayId, enabled);
- updateVoteLocked(displayId);
+ @Override
+ public void onAuthenticationPossible(int displayId, boolean isPossible) {
+ synchronized (mLock) {
+ mAuthenticationPossible.put(displayId, isPossible);
+ updateVoteLocked(displayId, isPossible,
+ Vote.PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE);
+ }
}
- private void updateVoteLocked(int displayId) {
+ @GuardedBy("mLock")
+ private void updateVoteLocked(int displayId, boolean enabled, int votePriority) {
final Vote vote;
- if (mUdfpsRefreshRateEnabled.get(displayId)) {
- Display.Mode[] modes = mSupportedModesByDisplay.get(displayId);
- float maxRefreshRate = 0f;
- for (Display.Mode mode : modes) {
- if (mode.getRefreshRate() > maxRefreshRate) {
- maxRefreshRate = mode.getRefreshRate();
- }
- }
+ if (enabled) {
+ float maxRefreshRate = DisplayModeDirector.this.getMaxRefreshRateLocked(displayId);
vote = Vote.forPhysicalRefreshRates(maxRefreshRate, maxRefreshRate);
} else {
vote = null;
}
-
- DisplayModeDirector.this.updateVoteLocked(displayId, Vote.PRIORITY_UDFPS, vote);
+ DisplayModeDirector.this.updateVoteLocked(displayId, votePriority, vote);
}
void dumpLocked(PrintWriter pw) {
@@ -2528,6 +2637,13 @@
final String enabled = mUdfpsRefreshRateEnabled.valueAt(i) ? "enabled" : "disabled";
pw.println(" Display " + displayId + ": " + enabled);
}
+ pw.println(" mAuthenticationPossible: ");
+ for (int i = 0; i < mAuthenticationPossible.size(); i++) {
+ final int displayId = mAuthenticationPossible.keyAt(i);
+ final String isPossible = mAuthenticationPossible.valueAt(i) ? "possible"
+ : "impossible";
+ pw.println(" Display " + displayId + ": " + isPossible);
+ }
}
}
@@ -2874,15 +2990,10 @@
}
public int getRefreshRateInLowZone() {
- int defaultRefreshRateInZone = mContext.getResources().getInteger(
- R.integer.config_defaultRefreshRateInZone);
-
- int refreshRate = mDeviceConfig.getInt(
+ return mDeviceConfig.getInt(
DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
- DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE,
- defaultRefreshRateInZone);
+ DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE, -1);
- return refreshRate;
}
/*
@@ -2904,15 +3015,10 @@
}
public int getRefreshRateInHighZone() {
- int defaultRefreshRateInZone = mContext.getResources().getInteger(
- R.integer.config_fixedRefreshRateInHighZone);
-
- int refreshRate = mDeviceConfig.getInt(
+ return mDeviceConfig.getInt(
DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HIGH_ZONE,
- defaultRefreshRateInZone);
-
- return refreshRate;
+ -1);
}
public int getRefreshRateInHbmSunlight() {
@@ -2960,23 +3066,29 @@
int[] lowDisplayBrightnessThresholds = getLowDisplayBrightnessThresholds();
int[] lowAmbientBrightnessThresholds = getLowAmbientBrightnessThresholds();
- int refreshRateInLowZone = getRefreshRateInLowZone();
+ final int refreshRateInLowZone = getRefreshRateInLowZone();
mHandler.obtainMessage(MSG_LOW_BRIGHTNESS_THRESHOLDS_CHANGED,
new Pair<>(lowDisplayBrightnessThresholds, lowAmbientBrightnessThresholds))
.sendToTarget();
- mHandler.obtainMessage(MSG_REFRESH_RATE_IN_LOW_ZONE_CHANGED, refreshRateInLowZone, 0)
- .sendToTarget();
+
+ if (refreshRateInLowZone != -1) {
+ mHandler.obtainMessage(MSG_REFRESH_RATE_IN_LOW_ZONE_CHANGED, refreshRateInLowZone,
+ 0).sendToTarget();
+ }
int[] highDisplayBrightnessThresholds = getHighDisplayBrightnessThresholds();
int[] highAmbientBrightnessThresholds = getHighAmbientBrightnessThresholds();
- int refreshRateInHighZone = getRefreshRateInHighZone();
+ final int refreshRateInHighZone = getRefreshRateInHighZone();
mHandler.obtainMessage(MSG_HIGH_BRIGHTNESS_THRESHOLDS_CHANGED,
new Pair<>(highDisplayBrightnessThresholds, highAmbientBrightnessThresholds))
.sendToTarget();
- mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HIGH_ZONE_CHANGED, refreshRateInHighZone, 0)
- .sendToTarget();
+
+ if (refreshRateInHighZone != -1) {
+ mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HIGH_ZONE_CHANGED, refreshRateInHighZone,
+ 0).sendToTarget();
+ }
final int refreshRateInHbmSunlight = getRefreshRateInHbmSunlight();
mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HBM_SUNLIGHT_CHANGED,
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index abb0ff6..1305d63 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -18,6 +18,7 @@
import android.animation.Animator;
import android.animation.ObjectAnimator;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
@@ -74,6 +75,7 @@
import com.android.server.display.brightness.BrightnessReason;
import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal;
import com.android.server.display.color.ColorDisplayService.ReduceBrightColorsListener;
+import com.android.server.display.layout.Layout;
import com.android.server.display.utils.SensorUtils;
import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
import com.android.server.display.whitebalance.DisplayWhiteBalanceFactory;
@@ -195,6 +197,9 @@
// The ID of the LogicalDisplay tied to this DisplayPowerController.
private final int mDisplayId;
+ // The ID of the display which this display follows for brightness purposes.
+ private int mLeadDisplayId = Layout.NO_LEAD_DISPLAY;
+
// The unique ID of the primary display device currently tied to this logical display
private String mUniqueDisplayId;
@@ -504,11 +509,13 @@
private boolean mIsEnabled;
private boolean mIsInTransition;
+ private String mBrightnessThrottlingDataId;
+
// DPCs following the brightness of this DPC. This is used in concurrent displays mode - there
// is one lead display, the additional displays follow the brightness value of the lead display.
@GuardedBy("mLock")
- private SparseArray<DisplayPowerControllerInterface> mDisplayBrightnessFollowers =
- new SparseArray();
+ private final SparseArray<DisplayPowerControllerInterface> mDisplayBrightnessFollowers =
+ new SparseArray<>();
/**
* Creates the display power controller.
@@ -539,6 +546,7 @@
mHandler = new DisplayControllerHandler(handler.getLooper());
mLastBrightnessEvent = new BrightnessEvent(mDisplayId);
mTempBrightnessEvent = new BrightnessEvent(mDisplayId);
+ mBrightnessThrottlingDataId = logicalDisplay.getBrightnessThrottlingDataIdLocked();
if (mDisplayId == Display.DEFAULT_DISPLAY) {
mBatteryStats = BatteryStatsService.getService();
@@ -719,6 +727,11 @@
}
@Override
+ public int getLeadDisplayId() {
+ return mLeadDisplayId;
+ }
+
+ @Override
public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux) {
mHbmController.onAmbientLuxChange(ambientLux);
if (mAutomaticBrightnessController == null || nits < 0) {
@@ -736,24 +749,20 @@
}
@Override
- public void addDisplayBrightnessFollower(DisplayPowerControllerInterface follower) {
+ public void addDisplayBrightnessFollower(@NonNull DisplayPowerControllerInterface follower) {
synchronized (mLock) {
mDisplayBrightnessFollowers.append(follower.getDisplayId(), follower);
+ sendUpdatePowerStateLocked();
}
- sendUpdatePowerState();
}
@Override
- public void clearDisplayBrightnessFollowers() {
- SparseArray<DisplayPowerControllerInterface> followers;
+ public void removeDisplayBrightnessFollower(@NonNull DisplayPowerControllerInterface follower) {
synchronized (mLock) {
- followers = mDisplayBrightnessFollowers.clone();
- mDisplayBrightnessFollowers.clear();
- }
- for (int i = 0; i < followers.size(); i++) {
- DisplayPowerControllerInterface follower = followers.valueAt(i);
- follower.setBrightnessToFollow(PowerManager.BRIGHTNESS_INVALID_FLOAT, /* nits= */ -1,
- /* ambientLux= */ 0);
+ mDisplayBrightnessFollowers.remove(follower.getDisplayId());
+ mHandler.postAtTime(() -> follower.setBrightnessToFollow(
+ PowerManager.BRIGHTNESS_INVALID_FLOAT, /* nits= */ -1,
+ /* ambientLux= */ 0), mClock.uptimeMillis());
}
}
@@ -848,7 +857,8 @@
* Make sure DisplayManagerService.mSyncRoot is held when this is called
*/
@Override
- public void onDisplayChanged(HighBrightnessModeMetadata hbmMetadata) {
+ public void onDisplayChanged(HighBrightnessModeMetadata hbmMetadata, int leadDisplayId) {
+ mLeadDisplayId = leadDisplayId;
final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
if (device == null) {
Slog.wtf(mTag, "Display Device is null in DisplayPowerController for display: "
@@ -862,6 +872,8 @@
final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
final boolean isEnabled = mLogicalDisplay.isEnabledLocked();
final boolean isInTransition = mLogicalDisplay.isInTransitionLocked();
+ final String brightnessThrottlingDataId =
+ mLogicalDisplay.getBrightnessThrottlingDataIdLocked();
mHandler.post(() -> {
boolean changed = false;
if (mDisplayDevice != device) {
@@ -870,12 +882,19 @@
mUniqueDisplayId = uniqueId;
mDisplayStatsId = mUniqueDisplayId.hashCode();
mDisplayDeviceConfig = config;
+ mBrightnessThrottlingDataId = brightnessThrottlingDataId;
loadFromDisplayDeviceConfig(token, info, hbmMetadata);
/// Since the underlying display-device changed, we really don't know the
// last command that was sent to change it's state. Lets assume it is unknown so
// that we trigger a change immediately.
mPowerState.resetScreenState();
+ } else if (!mBrightnessThrottlingDataId.equals(brightnessThrottlingDataId)) {
+ changed = true;
+ mBrightnessThrottlingDataId = brightnessThrottlingDataId;
+ mBrightnessThrottler.resetThrottlingData(
+ config.getBrightnessThrottlingData(mBrightnessThrottlingDataId),
+ mUniqueDisplayId);
}
if (mIsEnabled != isEnabled || mIsInTransition != isInTransition) {
changed = true;
@@ -887,9 +906,6 @@
updatePowerState();
}
});
-
- // TODO (b/265793751): Re-create BrightnessTracker if we're enetering/exiting concurrent
- // displays mode
}
/**
@@ -926,7 +942,7 @@
}
private void loadFromDisplayDeviceConfig(IBinder token, DisplayDeviceInfo info,
- HighBrightnessModeMetadata hbmMetadata) {
+ HighBrightnessModeMetadata hbmMetadata) {
// All properties that depend on the associated DisplayDevice and the DDC must be
// updated here.
loadBrightnessRampRates();
@@ -948,10 +964,9 @@
return mDisplayDeviceConfig.getHdrBrightnessFromSdr(sdrBrightness);
}
});
- // TODO (b/265793751): Use the appropriate throttling data if we're in concurrent displays
- // mode
mBrightnessThrottler.resetThrottlingData(
- mDisplayDeviceConfig.getBrightnessThrottlingData(), mUniqueDisplayId);
+ mDisplayDeviceConfig.getBrightnessThrottlingData(mBrightnessThrottlingDataId),
+ mUniqueDisplayId);
}
private void sendUpdatePowerState() {
@@ -1003,7 +1018,7 @@
}
mBrightnessSettingListener = brightnessValue -> {
Message msg = mHandler.obtainMessage(MSG_UPDATE_BRIGHTNESS, brightnessValue);
- mHandler.sendMessage(msg);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
};
mBrightnessSetting.registerListener(mBrightnessSettingListener);
@@ -1032,7 +1047,7 @@
final boolean isIdleScreenBrightnessEnabled = resources.getBoolean(
R.bool.config_enableIdleScreenBrightnessMode);
- mInteractiveModeBrightnessMapper = BrightnessMappingStrategy.create(resources,
+ mInteractiveModeBrightnessMapper = mInjector.getInteractiveModeBrightnessMapper(resources,
mDisplayDeviceConfig, mDisplayWhiteBalanceController);
if (isIdleScreenBrightnessEnabled) {
mIdleModeBrightnessMapper = BrightnessMappingStrategy.createForIdleMode(resources,
@@ -1057,7 +1072,7 @@
mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold();
float ambientBrighteningMinThreshold =
mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold();
- HysteresisLevels ambientBrightnessThresholds = new HysteresisLevels(
+ HysteresisLevels ambientBrightnessThresholds = mInjector.getHysteresisLevels(
ambientBrighteningThresholds, ambientDarkeningThresholds,
ambientBrighteningLevels, ambientDarkeningLevels, ambientDarkeningMinThreshold,
ambientBrighteningMinThreshold);
@@ -1075,7 +1090,7 @@
mDisplayDeviceConfig.getScreenDarkeningMinThreshold();
float screenBrighteningMinThreshold =
mDisplayDeviceConfig.getScreenBrighteningMinThreshold();
- HysteresisLevels screenBrightnessThresholds = new HysteresisLevels(
+ HysteresisLevels screenBrightnessThresholds = mInjector.getHysteresisLevels(
screenBrighteningThresholds, screenDarkeningThresholds,
screenBrighteningLevels, screenDarkeningLevels, screenDarkeningMinThreshold,
screenBrighteningMinThreshold, true);
@@ -1093,7 +1108,7 @@
mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle();
float[] ambientDarkeningLevelsIdle =
mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle();
- HysteresisLevels ambientBrightnessThresholdsIdle = new HysteresisLevels(
+ HysteresisLevels ambientBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
ambientBrighteningThresholdsIdle, ambientDarkeningThresholdsIdle,
ambientBrighteningLevelsIdle, ambientDarkeningLevelsIdle,
ambientDarkeningMinThresholdIdle, ambientBrighteningMinThresholdIdle);
@@ -1111,7 +1126,7 @@
mDisplayDeviceConfig.getScreenBrighteningLevelsIdle();
float[] screenDarkeningLevelsIdle =
mDisplayDeviceConfig.getScreenDarkeningLevelsIdle();
- HysteresisLevels screenBrightnessThresholdsIdle = new HysteresisLevels(
+ HysteresisLevels screenBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
screenBrighteningThresholdsIdle, screenDarkeningThresholdsIdle,
screenBrighteningLevelsIdle, screenDarkeningLevelsIdle,
screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
@@ -1147,8 +1162,8 @@
if (mAutomaticBrightnessController != null) {
mAutomaticBrightnessController.stop();
}
- mAutomaticBrightnessController = new AutomaticBrightnessController(this,
- handler.getLooper(), mSensorManager, mLightSensor,
+ mAutomaticBrightnessController = mInjector.getAutomaticBrightnessController(
+ this, handler.getLooper(), mSensorManager, mLightSensor,
mInteractiveModeBrightnessMapper, lightSensorWarmUpTimeConfig,
PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, dozeScaleFactor,
lightSensorRate, initialLightSensorRate, brighteningLightDebounce,
@@ -1249,7 +1264,7 @@
public void onAnimationEnd() {
sendUpdatePowerState();
Message msg = mHandler.obtainMessage(MSG_BRIGHTNESS_RAMP_DONE);
- mHandler.sendMessage(msg);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
}
};
@@ -2061,11 +2076,8 @@
private BrightnessThrottler createBrightnessThrottlerLocked() {
final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
final DisplayDeviceConfig ddConfig = device.getDisplayDeviceConfig();
- // TODO (b/265793751): Use the appropriate throttling data if we're in concurrent displays
- // mode
- final DisplayDeviceConfig.BrightnessThrottlingData data =
- ddConfig != null ? ddConfig.getBrightnessThrottlingData() : null;
- return new BrightnessThrottler(mHandler, data,
+ return new BrightnessThrottler(mHandler,
+ ddConfig.getBrightnessThrottlingData(mBrightnessThrottlingDataId),
() -> {
sendUpdatePowerState();
postBrightnessChangeRunnable();
@@ -2200,15 +2212,13 @@
}
private void loadProximitySensor() {
- if (DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT) {
+ if (DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT || mDisplayId != Display.DEFAULT_DISPLAY) {
return;
}
final DisplayDeviceConfig.SensorData proxSensor =
mDisplayDeviceConfig.getProximitySensor();
- final int fallbackType = mDisplayId == Display.DEFAULT_DISPLAY
- ? Sensor.TYPE_PROXIMITY : SensorUtils.NO_FALLBACK;
mProximitySensor = SensorUtils.findSensor(mSensorManager, proxSensor.type, proxSensor.name,
- fallbackType);
+ Sensor.TYPE_PROXIMITY);
if (mProximitySensor != null) {
mProximityThreshold = Math.min(mProximitySensor.getMaximumRange(),
TYPICAL_PROXIMITY_THRESHOLD);
@@ -2698,6 +2708,7 @@
pw.println();
pw.println("Display Power Controller:");
pw.println(" mDisplayId=" + mDisplayId);
+ pw.println(" mLeadDisplayId=" + mLeadDisplayId);
pw.println(" mLightSensor=" + mLightSensor);
pw.println();
@@ -2946,7 +2957,8 @@
msg.what = MSG_STATSD_HBM_BRIGHTNESS;
msg.arg1 = Float.floatToIntBits(brightness);
msg.arg2 = mDisplayStatsId;
- mHandler.sendMessageDelayed(msg, BRIGHTNESS_CHANGE_STATSD_REPORT_INTERVAL_MS);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis()
+ + BRIGHTNESS_CHANGE_STATSD_REPORT_INTERVAL_MS);
}
}
}
@@ -3102,7 +3114,7 @@
@Override
public void onScreenOn() {
Message msg = mHandler.obtainMessage(MSG_SCREEN_ON_UNBLOCKED, this);
- mHandler.sendMessage(msg);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
}
}
@@ -3110,7 +3122,7 @@
@Override
public void onScreenOff() {
Message msg = mHandler.obtainMessage(MSG_SCREEN_OFF_UNBLOCKED, this);
- mHandler.sendMessage(msg);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
}
}
@@ -3192,6 +3204,58 @@
FloatProperty<DisplayPowerState> secondProperty) {
return new DualRampAnimator(dps, firstProperty, secondProperty);
}
+
+ AutomaticBrightnessController getAutomaticBrightnessController(
+ AutomaticBrightnessController.Callbacks callbacks, Looper looper,
+ SensorManager sensorManager, Sensor lightSensor,
+ BrightnessMappingStrategy interactiveModeBrightnessMapper,
+ int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
+ float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
+ long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+ boolean resetAmbientLuxAfterWarmUpConfig,
+ HysteresisLevels ambientBrightnessThresholds,
+ HysteresisLevels screenBrightnessThresholds,
+ HysteresisLevels ambientBrightnessThresholdsIdle,
+ HysteresisLevels screenBrightnessThresholdsIdle, Context context,
+ HighBrightnessModeController hbmController, BrightnessThrottler brightnessThrottler,
+ BrightnessMappingStrategy idleModeBrightnessMapper, int ambientLightHorizonShort,
+ int ambientLightHorizonLong, float userLux, float userBrightness) {
+ return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor,
+ interactiveModeBrightnessMapper, lightSensorWarmUpTime, brightnessMin,
+ brightnessMax, dozeScaleFactor, lightSensorRate, initialLightSensorRate,
+ brighteningLightDebounceConfig, darkeningLightDebounceConfig,
+ resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,
+ screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
+ screenBrightnessThresholdsIdle, context, hbmController, brightnessThrottler,
+ idleModeBrightnessMapper, ambientLightHorizonShort, ambientLightHorizonLong,
+ userLux, userBrightness);
+ }
+
+ BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
+ DisplayDeviceConfig displayDeviceConfig,
+ DisplayWhiteBalanceController displayWhiteBalanceController) {
+ return BrightnessMappingStrategy.create(resources,
+ displayDeviceConfig, displayWhiteBalanceController);
+ }
+
+ HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+ float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+ float[] darkeningThresholdLevels, float minDarkeningThreshold,
+ float minBrighteningThreshold) {
+ return new HysteresisLevels(brighteningThresholdsPercentages,
+ darkeningThresholdsPercentages, brighteningThresholdLevels,
+ darkeningThresholdLevels, minDarkeningThreshold, minBrighteningThreshold);
+ }
+
+ HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+ float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+ float[] darkeningThresholdLevels, float minDarkeningThreshold,
+ float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
+ return new HysteresisLevels(brighteningThresholdsPercentages,
+ darkeningThresholdsPercentages, brighteningThresholdLevels,
+ darkeningThresholdLevels, minDarkeningThreshold, minBrighteningThreshold,
+ potentialOldBrightnessRange);
+ }
}
static class CachedBrightnessInfo {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 2e91bdb..82faa12 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -73,6 +73,7 @@
import com.android.server.display.brightness.DisplayBrightnessController;
import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal;
import com.android.server.display.color.ColorDisplayService.ReduceBrightColorsListener;
+import com.android.server.display.layout.Layout;
import com.android.server.display.state.DisplayStateController;
import com.android.server.display.utils.SensorUtils;
import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
@@ -179,6 +180,9 @@
// The ID of the LogicalDisplay tied to this DisplayPowerController2.
private final int mDisplayId;
+ // The ID of the display which this display follows for brightness purposes.
+ private int mLeadDisplayId = Layout.NO_LEAD_DISPLAY;
+
// The unique ID of the primary display device currently tied to this logical display
private String mUniqueDisplayId;
@@ -411,6 +415,8 @@
private boolean mIsEnabled;
private boolean mIsInTransition;
+ private String mBrightnessThrottlingDataId;
+
// DPCs following the brightness of this DPC. This is used in concurrent displays mode - there
// is one lead display, the additional displays follow the brightness value of the lead display.
@GuardedBy("mLock")
@@ -443,6 +449,7 @@
mHighBrightnessModeMetadata = hbmMetadata;
mDisplayStateController = new DisplayStateController(mDisplayPowerProximityStateController);
mTag = "DisplayPowerController2[" + mDisplayId + "]";
+ mBrightnessThrottlingDataId = logicalDisplay.getBrightnessThrottlingDataIdLocked();
mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
mUniqueDisplayId = logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId();
@@ -691,7 +698,8 @@
* Make sure DisplayManagerService.mSyncRoot lock is held when this is called
*/
@Override
- public void onDisplayChanged(HighBrightnessModeMetadata hbmMetadata) {
+ public void onDisplayChanged(HighBrightnessModeMetadata hbmMetadata, int leadDisplayId) {
+ mLeadDisplayId = leadDisplayId;
final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
if (device == null) {
Slog.wtf(mTag, "Display Device is null in DisplayPowerController2 for display: "
@@ -705,6 +713,8 @@
final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
final boolean isEnabled = mLogicalDisplay.isEnabledLocked();
final boolean isInTransition = mLogicalDisplay.isInTransitionLocked();
+ final String brightnessThrottlingDataId =
+ mLogicalDisplay.getBrightnessThrottlingDataIdLocked();
mHandler.post(() -> {
boolean changed = false;
@@ -714,6 +724,7 @@
mUniqueDisplayId = uniqueId;
mDisplayStatsId = mUniqueDisplayId.hashCode();
mDisplayDeviceConfig = config;
+ mBrightnessThrottlingDataId = brightnessThrottlingDataId;
loadFromDisplayDeviceConfig(token, info, hbmMetadata);
mDisplayPowerProximityStateController.notifyDisplayDeviceChanged(config);
@@ -721,6 +732,12 @@
// last command that was sent to change it's state. Lets assume it is unknown so
// that we trigger a change immediately.
mPowerState.resetScreenState();
+ } else if (!mBrightnessThrottlingDataId.equals(brightnessThrottlingDataId)) {
+ changed = true;
+ mBrightnessThrottlingDataId = brightnessThrottlingDataId;
+ mBrightnessThrottler.resetThrottlingData(
+ config.getBrightnessThrottlingData(mBrightnessThrottlingDataId),
+ mUniqueDisplayId);
}
if (mIsEnabled != isEnabled || mIsInTransition != isInTransition) {
changed = true;
@@ -732,9 +749,6 @@
updatePowerState();
}
});
-
- // TODO (b/265793751): Re-create BrightnessTracker if we're enetering/exiting concurrent
- // displays mode
}
/**
@@ -765,7 +779,7 @@
}
private void loadFromDisplayDeviceConfig(IBinder token, DisplayDeviceInfo info,
- HighBrightnessModeMetadata hbmMetadata) {
+ HighBrightnessModeMetadata hbmMetadata) {
// All properties that depend on the associated DisplayDevice and the DDC must be
// updated here.
loadBrightnessRampRates();
@@ -786,10 +800,9 @@
return mDisplayDeviceConfig.getHdrBrightnessFromSdr(sdrBrightness);
}
});
- // TODO (b/265793751): Use the appropriate throttling data if we're in concurrent displays
- // mode
mBrightnessThrottler.resetThrottlingData(
- mDisplayDeviceConfig.getBrightnessThrottlingData(), mUniqueDisplayId);
+ mDisplayDeviceConfig.getBrightnessThrottlingData(mBrightnessThrottlingDataId),
+ mUniqueDisplayId);
}
private void sendUpdatePowerState() {
@@ -842,7 +855,7 @@
BrightnessSetting.BrightnessSettingListener brightnessSettingListener = brightnessValue -> {
Message msg = mHandler.obtainMessage(MSG_UPDATE_BRIGHTNESS, brightnessValue);
- mHandler.sendMessage(msg);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
};
mDisplayBrightnessController
.registerBrightnessSettingChangeListener(brightnessSettingListener);
@@ -872,7 +885,7 @@
final boolean isIdleScreenBrightnessEnabled = resources.getBoolean(
R.bool.config_enableIdleScreenBrightnessMode);
- mInteractiveModeBrightnessMapper = BrightnessMappingStrategy.create(resources,
+ mInteractiveModeBrightnessMapper = mInjector.getInteractiveModeBrightnessMapper(resources,
mDisplayDeviceConfig, mDisplayWhiteBalanceController);
if (isIdleScreenBrightnessEnabled) {
mIdleModeBrightnessMapper = BrightnessMappingStrategy.createForIdleMode(resources,
@@ -897,7 +910,7 @@
mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold();
float ambientBrighteningMinThreshold =
mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold();
- HysteresisLevels ambientBrightnessThresholds = new HysteresisLevels(
+ HysteresisLevels ambientBrightnessThresholds = mInjector.getHysteresisLevels(
ambientBrighteningThresholds, ambientDarkeningThresholds,
ambientBrighteningLevels, ambientDarkeningLevels, ambientDarkeningMinThreshold,
ambientBrighteningMinThreshold);
@@ -915,7 +928,7 @@
mDisplayDeviceConfig.getScreenDarkeningMinThreshold();
float screenBrighteningMinThreshold =
mDisplayDeviceConfig.getScreenBrighteningMinThreshold();
- HysteresisLevels screenBrightnessThresholds = new HysteresisLevels(
+ HysteresisLevels screenBrightnessThresholds = mInjector.getHysteresisLevels(
screenBrighteningThresholds, screenDarkeningThresholds,
screenBrighteningLevels, screenDarkeningLevels, screenDarkeningMinThreshold,
screenBrighteningMinThreshold, true);
@@ -933,7 +946,7 @@
mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle();
float[] ambientDarkeningLevelsIdle =
mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle();
- HysteresisLevels ambientBrightnessThresholdsIdle = new HysteresisLevels(
+ HysteresisLevels ambientBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
ambientBrighteningThresholdsIdle, ambientDarkeningThresholdsIdle,
ambientBrighteningLevelsIdle, ambientDarkeningLevelsIdle,
ambientDarkeningMinThresholdIdle, ambientBrighteningMinThresholdIdle);
@@ -951,7 +964,7 @@
mDisplayDeviceConfig.getScreenBrighteningLevelsIdle();
float[] screenDarkeningLevelsIdle =
mDisplayDeviceConfig.getScreenDarkeningLevelsIdle();
- HysteresisLevels screenBrightnessThresholdsIdle = new HysteresisLevels(
+ HysteresisLevels screenBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
screenBrighteningThresholdsIdle, screenDarkeningThresholdsIdle,
screenBrighteningLevelsIdle, screenDarkeningLevelsIdle,
screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
@@ -987,8 +1000,8 @@
if (mAutomaticBrightnessController != null) {
mAutomaticBrightnessController.stop();
}
- mAutomaticBrightnessController = new AutomaticBrightnessController(this,
- handler.getLooper(), mSensorManager, mLightSensor,
+ mAutomaticBrightnessController = mInjector.getAutomaticBrightnessController(
+ this, handler.getLooper(), mSensorManager, mLightSensor,
mInteractiveModeBrightnessMapper, lightSensorWarmUpTimeConfig,
PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, dozeScaleFactor,
lightSensorRate, initialLightSensorRate, brighteningLightDebounce,
@@ -1086,7 +1099,7 @@
public void onAnimationEnd() {
sendUpdatePowerState();
Message msg = mHandler.obtainMessage(MSG_BRIGHTNESS_RAMP_DONE);
- mHandler.sendMessage(msg);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
}
};
@@ -1768,11 +1781,8 @@
private BrightnessThrottler createBrightnessThrottlerLocked() {
final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
final DisplayDeviceConfig ddConfig = device.getDisplayDeviceConfig();
- // TODO (b/265793751): Use the appropriate throttling data if we're in concurrent displays
- // mode
- final DisplayDeviceConfig.BrightnessThrottlingData data =
- ddConfig != null ? ddConfig.getBrightnessThrottlingData() : null;
- return new BrightnessThrottler(mHandler, data,
+ return new BrightnessThrottler(mHandler,
+ ddConfig.getBrightnessThrottlingData(mBrightnessThrottlingDataId),
() -> {
sendUpdatePowerState();
postBrightnessChangeRunnable();
@@ -2144,6 +2154,11 @@
}
@Override
+ public int getLeadDisplayId() {
+ return mLeadDisplayId;
+ }
+
+ @Override
public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux) {
mHbmController.onAmbientLuxChange(ambientLux);
if (mAutomaticBrightnessController == null || nits < 0) {
@@ -2213,21 +2228,17 @@
public void addDisplayBrightnessFollower(DisplayPowerControllerInterface follower) {
synchronized (mLock) {
mDisplayBrightnessFollowers.append(follower.getDisplayId(), follower);
+ sendUpdatePowerStateLocked();
}
- sendUpdatePowerState();
}
@Override
- public void clearDisplayBrightnessFollowers() {
- SparseArray<DisplayPowerControllerInterface> followers;
+ public void removeDisplayBrightnessFollower(DisplayPowerControllerInterface follower) {
synchronized (mLock) {
- followers = mDisplayBrightnessFollowers.clone();
- mDisplayBrightnessFollowers.clear();
- }
- for (int i = 0; i < followers.size(); i++) {
- DisplayPowerControllerInterface follower = followers.valueAt(i);
- follower.setBrightnessToFollow(PowerManager.BRIGHTNESS_INVALID_FLOAT, /* nits= */ -1,
- /* ambientLux= */ 0);
+ mDisplayBrightnessFollowers.remove(follower.getDisplayId());
+ mHandler.postAtTime(() -> follower.setBrightnessToFollow(
+ PowerManager.BRIGHTNESS_INVALID_FLOAT, /* nits= */ -1,
+ /* ambientLux= */ 0), mClock.uptimeMillis());
}
}
@@ -2237,6 +2248,7 @@
pw.println();
pw.println("Display Power Controller:");
pw.println(" mDisplayId=" + mDisplayId);
+ pw.println(" mLeadDisplayId=" + mLeadDisplayId);
pw.println(" mLightSensor=" + mLightSensor);
pw.println();
@@ -2453,7 +2465,8 @@
msg.what = MSG_STATSD_HBM_BRIGHTNESS;
msg.arg1 = Float.floatToIntBits(brightness);
msg.arg2 = mDisplayStatsId;
- mHandler.sendMessageDelayed(msg, BRIGHTNESS_CHANGE_STATSD_REPORT_INTERVAL_MS);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis()
+ + BRIGHTNESS_CHANGE_STATSD_REPORT_INTERVAL_MS);
}
}
}
@@ -2584,7 +2597,7 @@
@Override
public void onScreenOn() {
Message msg = mHandler.obtainMessage(MSG_SCREEN_ON_UNBLOCKED, this);
- mHandler.sendMessage(msg);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
}
}
@@ -2592,7 +2605,7 @@
@Override
public void onScreenOff() {
Message msg = mHandler.obtainMessage(MSG_SCREEN_OFF_UNBLOCKED, this);
- mHandler.sendMessage(msg);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
}
}
@@ -2666,6 +2679,58 @@
looper, nudgeUpdatePowerState,
displayId, sensorManager, /* injector= */ null);
}
+
+ AutomaticBrightnessController getAutomaticBrightnessController(
+ AutomaticBrightnessController.Callbacks callbacks, Looper looper,
+ SensorManager sensorManager, Sensor lightSensor,
+ BrightnessMappingStrategy interactiveModeBrightnessMapper,
+ int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
+ float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
+ long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+ boolean resetAmbientLuxAfterWarmUpConfig,
+ HysteresisLevels ambientBrightnessThresholds,
+ HysteresisLevels screenBrightnessThresholds,
+ HysteresisLevels ambientBrightnessThresholdsIdle,
+ HysteresisLevels screenBrightnessThresholdsIdle, Context context,
+ HighBrightnessModeController hbmController, BrightnessThrottler brightnessThrottler,
+ BrightnessMappingStrategy idleModeBrightnessMapper, int ambientLightHorizonShort,
+ int ambientLightHorizonLong, float userLux, float userBrightness) {
+ return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor,
+ interactiveModeBrightnessMapper, lightSensorWarmUpTime, brightnessMin,
+ brightnessMax, dozeScaleFactor, lightSensorRate, initialLightSensorRate,
+ brighteningLightDebounceConfig, darkeningLightDebounceConfig,
+ resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,
+ screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
+ screenBrightnessThresholdsIdle, context, hbmController, brightnessThrottler,
+ idleModeBrightnessMapper, ambientLightHorizonShort, ambientLightHorizonLong,
+ userLux, userBrightness);
+ }
+
+ BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
+ DisplayDeviceConfig displayDeviceConfig,
+ DisplayWhiteBalanceController displayWhiteBalanceController) {
+ return BrightnessMappingStrategy.create(resources,
+ displayDeviceConfig, displayWhiteBalanceController);
+ }
+
+ HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+ float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+ float[] darkeningThresholdLevels, float minDarkeningThreshold,
+ float minBrighteningThreshold) {
+ return new HysteresisLevels(brighteningThresholdsPercentages,
+ darkeningThresholdsPercentages, brighteningThresholdLevels,
+ darkeningThresholdLevels, minDarkeningThreshold, minBrighteningThreshold);
+ }
+
+ HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+ float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+ float[] darkeningThresholdLevels, float minDarkeningThreshold,
+ float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
+ return new HysteresisLevels(brighteningThresholdsPercentages,
+ darkeningThresholdsPercentages, brighteningThresholdLevels,
+ darkeningThresholdLevels, minDarkeningThreshold, minBrighteningThreshold,
+ potentialOldBrightnessRange);
+ }
}
static class CachedBrightnessInfo {
diff --git a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
index 4612ec9..0bc8154 100644
--- a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
+++ b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
@@ -32,13 +32,18 @@
/**
* Notified when the display is changed.
- * We use this to apply any changes that might be needed
- * when displays get swapped on foldable devices.
- * We also pass the High brightness mode metadata like
- * remaining time and hbm events for the corresponding
- * physical display, to update the values correctly.
+ *
+ * We use this to apply any changes that might be needed when displays get swapped on foldable
+ * devices, when layouts change, etc.
+ *
+ * Must be called while holding the SyncRoot lock.
+ *
+ * @param hbmInfo The high brightness mode metadata, like
+ * remaining time and hbm events, for the corresponding
+ * physical display, to make sure we stay within the safety margins.
+ * @param leadDisplayId The display who is considered our "leader" for things like brightness.
*/
- void onDisplayChanged(HighBrightnessModeMetadata hbmInfo);
+ void onDisplayChanged(HighBrightnessModeMetadata hbmInfo, int leadDisplayId);
/**
* Unregisters all listeners and interrupts all running threads; halting future work.
@@ -169,6 +174,16 @@
int getDisplayId();
/**
+ * Get the ID of the display that is the leader of this DPC.
+ *
+ * Note that this is different than the display associated with the DPC. The leader is another
+ * display which we follow for things like brightness.
+ *
+ * Must be called while holding the SyncRoot lock.
+ */
+ int getLeadDisplayId();
+
+ /**
* Set the brightness to follow if this is an additional display in a set of concurrent
* displays.
* @param leadDisplayBrightness The brightness of the lead display in the set of concurrent
@@ -187,7 +202,8 @@
void addDisplayBrightnessFollower(DisplayPowerControllerInterface follower);
/**
- * Clear all the additional displays following the brightness value of this display.
+ * Removes the given display from the list of brightness followers.
+ * @param follower The DPC to remove from the followers list
*/
- void clearDisplayBrightnessFollowers();
+ void removeDisplayBrightnessFollower(DisplayPowerControllerInterface follower);
}
diff --git a/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java b/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java
index a3433d9..5bb3e6b 100644
--- a/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java
@@ -346,15 +346,13 @@
}
private void loadProximitySensor() {
- if (DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT) {
+ if (DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT || mDisplayId != Display.DEFAULT_DISPLAY) {
return;
}
final DisplayDeviceConfig.SensorData proxSensor =
mDisplayDeviceConfig.getProximitySensor();
- final int fallbackType = mDisplayId == Display.DEFAULT_DISPLAY
- ? Sensor.TYPE_PROXIMITY : SensorUtils.NO_FALLBACK;
mProximitySensor = SensorUtils.findSensor(mSensorManager, proxSensor.type, proxSensor.name,
- fallbackType);
+ Sensor.TYPE_PROXIMITY);
if (mProximitySensor != null) {
mProximityThreshold = Math.min(mProximitySensor.getMaximumRange(),
TYPICAL_PROXIMITY_THRESHOLD);
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index dfdbce5..1086c55 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -31,6 +31,7 @@
import android.view.Surface;
import android.view.SurfaceControl;
+import com.android.server.display.layout.Layout;
import com.android.server.wm.utils.InsetUtils;
import java.io.PrintWriter;
@@ -76,6 +77,12 @@
private final int mDisplayId;
private final int mLayerStack;
+ // Indicates which display leads this logical display, in terms of brightness or other
+ // properties.
+ // {@link Layout.NO_LEAD_DISPLAY} means that this display is not lead by any others, and could
+ // be a leader itself.
+ private int mLeadDisplayId = Layout.NO_LEAD_DISPLAY;
+
private int mDisplayGroupId = Display.INVALID_DISPLAY_GROUP;
/**
@@ -149,9 +156,24 @@
// Indicates the display is part of a transition from one device-state ({@link
// DeviceStateManager}) to another. Being a "part" of a transition means that either
- // the {@link mIsEnabled} is changing, or the underlying mPrimiaryDisplayDevice is changing.
+ // the {@link mIsEnabled} is changing, or the underlying mPrimaryDisplayDevice is changing.
private boolean mIsInTransition;
+ // Indicates the position of the display, POSITION_UNKNOWN could mean it hasn't been specified,
+ // or this is a virtual display etc.
+ private int mDevicePosition = Layout.Display.POSITION_UNKNOWN;
+
+ // Indicates that something other than the primary display device info has changed and needs to
+ // be handled in the next update.
+ private boolean mDirty = false;
+
+ /**
+ * The ID of the brightness throttling data that should be used. This can change e.g. in
+ * concurrent displays mode in which a stricter brightness throttling policy might need to be
+ * used.
+ */
+ private String mBrightnessThrottlingDataId;
+
public LogicalDisplay(int displayId, int layerStack, DisplayDevice primaryDisplayDevice) {
mDisplayId = displayId;
mLayerStack = layerStack;
@@ -160,6 +182,17 @@
mTempFrameRateOverride = new SparseArray<>();
mIsEnabled = true;
mIsInTransition = false;
+ mBrightnessThrottlingDataId = DisplayDeviceConfig.DEFAULT_BRIGHTNESS_THROTTLING_DATA_ID;
+ }
+
+ public void setDevicePositionLocked(int position) {
+ if (mDevicePosition != position) {
+ mDevicePosition = position;
+ mDirty = true;
+ }
+ }
+ public int getDevicePositionLocked() {
+ return mDevicePosition;
}
/**
@@ -317,9 +350,11 @@
// logical display that they are sharing. (eg. Adjust size for pixel-perfect
// mirroring over HDMI.)
DisplayDeviceInfo deviceInfo = mPrimaryDisplayDevice.getDisplayDeviceInfoLocked();
- if (!Objects.equals(mPrimaryDisplayDeviceInfo, deviceInfo)) {
+ if (!Objects.equals(mPrimaryDisplayDeviceInfo, deviceInfo) || mDirty) {
mBaseDisplayInfo.layerStack = mLayerStack;
mBaseDisplayInfo.flags = 0;
+ // Displays default to moving content to the primary display when removed
+ mBaseDisplayInfo.removeMode = Display.REMOVE_MODE_MOVE_CONTENT_TO_PRIMARY;
if ((deviceInfo.flags & DisplayDeviceInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) != 0) {
mBaseDisplayInfo.flags |= Display.FLAG_SUPPORTS_PROTECTED_BUFFERS;
}
@@ -416,8 +451,21 @@
mBaseDisplayInfo.roundedCorners = deviceInfo.roundedCorners;
mBaseDisplayInfo.installOrientation = deviceInfo.installOrientation;
mBaseDisplayInfo.displayShape = deviceInfo.displayShape;
+
+ if (mDevicePosition == Layout.Display.POSITION_REAR) {
+ // A rear display is meant to host a specific experience that is essentially
+ // a presentation to another user or users other than the main user since they
+ // can't actually see that display. Given that, it's a suitable display for
+ // presentations but the content should be destroyed rather than moved to a non-rear
+ // display when the rear display is removed.
+ mBaseDisplayInfo.flags |= Display.FLAG_REAR;
+ mBaseDisplayInfo.flags |= Display.FLAG_PRESENTATION;
+ mBaseDisplayInfo.removeMode = Display.REMOVE_MODE_DESTROY_CONTENT;
+ }
+
mPrimaryDisplayDeviceInfo = deviceInfo;
mInfo.set(null);
+ mDirty = false;
}
}
@@ -762,7 +810,7 @@
/**
* Sets the display as enabled.
*
- * @param enable True if enabled, false otherwise.
+ * @param enabled True if enabled, false otherwise.
*/
public void setEnabledLocked(boolean enabled) {
mIsEnabled = enabled;
@@ -785,11 +833,48 @@
mIsInTransition = isInTransition;
}
+ /**
+ * @return The ID of the brightness throttling data that this display should use.
+ */
+ public String getBrightnessThrottlingDataIdLocked() {
+ return mBrightnessThrottlingDataId;
+ }
+
+ /**
+ * @param brightnessThrottlingDataId The ID of the brightness throttling data that this
+ * display should use.
+ */
+ public void setBrightnessThrottlingDataIdLocked(String brightnessThrottlingDataId) {
+ mBrightnessThrottlingDataId =
+ brightnessThrottlingDataId;
+ }
+
+ /**
+ * Sets the display of which this display is a follower, regarding brightness or other
+ * properties. If set to {@link Layout#NO_LEAD_DISPLAY}, this display does not follow any
+ * others, and has the potential to be a lead display to others.
+ *
+ * A display cannot be a leader or follower of itself, and there cannot be cycles.
+ * A display cannot be both a leader and a follower, ie, there must not be any chains.
+ *
+ * @param displayId logical display id
+ */
+ public void setLeadDisplayLocked(int displayId) {
+ if (mDisplayId != mLeadDisplayId && mDisplayId != displayId) {
+ mLeadDisplayId = displayId;
+ }
+ }
+
+ public int getLeadDisplayIdLocked() {
+ return mLeadDisplayId;
+ }
+
public void dumpLocked(PrintWriter pw) {
pw.println("mDisplayId=" + mDisplayId);
pw.println("mIsEnabled=" + mIsEnabled);
pw.println("mIsInTransition=" + mIsInTransition);
pw.println("mLayerStack=" + mLayerStack);
+ pw.println("mPosition=" + mDevicePosition);
pw.println("mHasContent=" + mHasContent);
pw.println("mDesiredDisplayModeSpecs={" + mDesiredDisplayModeSpecs + "}");
pw.println("mRequestedColorMode=" + mRequestedColorMode);
@@ -802,6 +887,8 @@
pw.println("mRequestedMinimalPostProcessing=" + mRequestedMinimalPostProcessing);
pw.println("mFrameRateOverrides=" + Arrays.toString(mFrameRateOverrides));
pw.println("mPendingFrameRateOverrideUids=" + mPendingFrameRateOverrideUids);
+ pw.println("mBrightnessThrottlingDataId=" + mBrightnessThrottlingDataId);
+ pw.println("mLeadDisplayId=" + mLeadDisplayId);
}
@Override
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 5087caa..c695862 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -18,6 +18,8 @@
import static android.view.Display.DEFAULT_DISPLAY;
+import static com.android.server.display.layout.Layout.NO_LEAD_DISPLAY;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -638,7 +640,8 @@
& DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY) != 0
&& !nextDeviceInfo.address.equals(deviceInfo.address)) {
layout.createDisplayLocked(nextDeviceInfo.address,
- /* isDefault= */ true, /* isEnabled= */ true, mIdProducer);
+ /* isDefault= */ true, /* isEnabled= */ true, mIdProducer,
+ /* brightnessThrottlingMapId= */ null, DEFAULT_DISPLAY);
applyLayoutLocked();
return;
}
@@ -976,6 +979,7 @@
// Now that we have a display-device, we need a LogicalDisplay to map it to. Find the
// right one, if it doesn't exist, create a new one.
final int logicalDisplayId = displayLayout.getLogicalDisplayId();
+
LogicalDisplay newDisplay = getDisplayLocked(logicalDisplayId);
if (newDisplay == null) {
newDisplay = createNewLogicalDisplayLocked(
@@ -988,11 +992,23 @@
newDisplay.swapDisplaysLocked(oldDisplay);
}
+ newDisplay.setDevicePositionLocked(displayLayout.getPosition());
+ newDisplay.setLeadDisplayLocked(displayLayout.getLeadDisplayId());
+ setLayoutLimitedRefreshRate(newDisplay, device, displayLayout);
setEnabledLocked(newDisplay, displayLayout.isEnabled());
+ newDisplay.setBrightnessThrottlingDataIdLocked(
+ displayLayout.getBrightnessThrottlingMapId() == null
+ ? DisplayDeviceConfig.DEFAULT_BRIGHTNESS_THROTTLING_DATA_ID
+ : displayLayout.getBrightnessThrottlingMapId());
}
-
}
+ private void setLayoutLimitedRefreshRate(@NonNull LogicalDisplay logicalDisplay,
+ @NonNull DisplayDevice device, @NonNull Layout.Display display) {
+ DisplayDeviceConfig config = device.getDisplayDeviceConfig();
+ DisplayInfo info = logicalDisplay.getDisplayInfoLocked();
+ info.layoutLimitedRefreshRate = config.getRefreshRange(display.getRefreshRateZoneId());
+ }
/**
* Creates a new logical display for the specified device and display Id and adds it to the list
@@ -1063,7 +1079,7 @@
}
final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
layout.createDisplayLocked(info.address, /* isDefault= */ true, /* isEnabled= */ true,
- mIdProducer);
+ mIdProducer, /* brightnessThrottlingMapId= */ null, NO_LEAD_DISPLAY);
}
private int assignLayerStackLocked(int displayId) {
diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
index ddeaa1b..364d53b 100644
--- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
@@ -106,7 +106,8 @@
String name = virtualDisplayConfig.getName();
boolean secure = (flags & VIRTUAL_DISPLAY_FLAG_SECURE) != 0;
IBinder appToken = callback.asBinder();
- IBinder displayToken = mSurfaceControlDisplayFactory.createDisplay(name, secure);
+ IBinder displayToken = mSurfaceControlDisplayFactory.createDisplay(name, secure,
+ virtualDisplayConfig.getRequestedRefreshRate());
final String baseUniqueId =
UNIQUE_ID_PREFIX + ownerPackageName + "," + ownerUid + "," + name + ",";
final int uniqueIndex = getNextUniqueIndex(baseUniqueId);
@@ -247,6 +248,7 @@
private int mWidth;
private int mHeight;
private int mDensityDpi;
+ private float mRequestedRefreshRate;
private Surface mSurface;
private DisplayDeviceInfo mInfo;
private int mDisplayState;
@@ -270,8 +272,9 @@
mName = virtualDisplayConfig.getName();
mWidth = virtualDisplayConfig.getWidth();
mHeight = virtualDisplayConfig.getHeight();
- mMode = createMode(mWidth, mHeight, REFRESH_RATE);
mDensityDpi = virtualDisplayConfig.getDensityDpi();
+ mRequestedRefreshRate = virtualDisplayConfig.getRequestedRefreshRate();
+ mMode = createMode(mWidth, mHeight, getRefreshRate());
mSurface = surface;
mFlags = flags;
mCallback = callback;
@@ -410,7 +413,7 @@
sendTraversalRequestLocked();
mWidth = width;
mHeight = height;
- mMode = createMode(width, height, REFRESH_RATE);
+ mMode = createMode(width, height, getRefreshRate());
mDensityDpi = densityDpi;
mInfo = null;
mPendingChanges |= PENDING_RESIZE;
@@ -438,9 +441,9 @@
pw.println("mStopped=" + mStopped);
pw.println("mDisplayIdToMirror=" + mDisplayIdToMirror);
pw.println("mWindowManagerMirroring=" + mIsWindowManagerMirroring);
+ pw.println("mRequestedRefreshRate=" + mRequestedRefreshRate);
}
-
@Override
public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
if (mInfo == null) {
@@ -456,7 +459,7 @@
mInfo.densityDpi = mDensityDpi;
mInfo.xDpi = mDensityDpi;
mInfo.yDpi = mDensityDpi;
- mInfo.presentationDeadlineNanos = 1000000000L / (int) REFRESH_RATE; // 1 frame
+ mInfo.presentationDeadlineNanos = 1000000000L / (int) getRefreshRate(); // 1 frame
mInfo.flags = 0;
if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) {
mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE
@@ -554,6 +557,10 @@
}
return mInfo;
}
+
+ private float getRefreshRate() {
+ return (mRequestedRefreshRate != 0.0f) ? mRequestedRefreshRate : REFRESH_RATE;
+ }
}
private static class Callback extends Handler {
@@ -632,6 +639,19 @@
@VisibleForTesting
public interface SurfaceControlDisplayFactory {
- public IBinder createDisplay(String name, boolean secure);
+ /**
+ * Create a virtual display in SurfaceFlinger.
+ *
+ * @param name The name of the display
+ * @param secure Whether this display is secure.
+ * @param requestedRefreshRate
+ * The refresh rate, frames per second, to request on the virtual display.
+ * It should be a divisor of refresh rate of the leader physical display
+ * that drives VSYNC, e.g. 30/60fps on 120fps display. If an arbitrary refresh
+ * rate is specified, SurfaceFlinger rounds up or down to match a divisor of
+ * the refresh rate of the leader physical display.
+ * @return The token reference for the display in SurfaceFlinger.
+ */
+ IBinder createDisplay(String name, boolean secure, float requestedRefreshRate);
}
}
diff --git a/services/core/java/com/android/server/display/layout/Layout.java b/services/core/java/com/android/server/display/layout/Layout.java
index 4a466fd..59d95a6 100644
--- a/services/core/java/com/android/server/display/layout/Layout.java
+++ b/services/core/java/com/android/server/display/layout/Layout.java
@@ -18,6 +18,8 @@
import static android.view.Display.DEFAULT_DISPLAY;
+import static com.android.server.display.layout.Layout.Display.POSITION_UNKNOWN;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.Slog;
@@ -25,6 +27,7 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
/**
* Holds a collection of {@link Display}s. A single instance of this class describes
@@ -36,6 +39,10 @@
private static final String TAG = "Layout";
private static int sNextNonDefaultDisplayId = DEFAULT_DISPLAY + 1;
+ // Lead display Id is set to this if this is not a follower display, and therefore
+ // has no lead.
+ public static final int NO_LEAD_DISPLAY = -1;
+
private final List<Display> mDisplays = new ArrayList<>(2);
/**
@@ -72,11 +79,34 @@
* @param address Address of the device.
* @param isDefault Indicates if the device is meant to be the default display.
* @param isEnabled Indicates if this display is usable and can be switched on
- * @return The new layout.
+ * @param idProducer Produces the logical display id.
+ * @param brightnessThrottlingMapId Name of which throttling policy should be used.
+ * @param leadDisplayId Display that this one follows (-1 if none).
+ * @return The new Display.
*/
public Display createDisplayLocked(
@NonNull DisplayAddress address, boolean isDefault, boolean isEnabled,
- DisplayIdProducer idProducer) {
+ DisplayIdProducer idProducer, String brightnessThrottlingMapId, int leadDisplayId) {
+ return createDisplayLocked(address, isDefault, isEnabled, idProducer,
+ brightnessThrottlingMapId, POSITION_UNKNOWN, leadDisplayId);
+ }
+
+ /**
+ * Creates a simple 1:1 LogicalDisplay mapping for the specified DisplayDevice.
+ *
+ * @param address Address of the device.
+ * @param isDefault Indicates if the device is meant to be the default display.
+ * @param isEnabled Indicates if this display is usable and can be switched on
+ * @param idProducer Produces the logical display id.
+ * @param brightnessThrottlingMapId Name of which throttling policy should be used.
+ * @param position Indicates the position this display is facing in this layout.
+ * @param leadDisplayId Display that this one follows (-1 if none).
+ * @return The new Display.
+ */
+ public Display createDisplayLocked(
+ @NonNull DisplayAddress address, boolean isDefault, boolean isEnabled,
+ DisplayIdProducer idProducer, String brightnessThrottlingMapId, int position,
+ int leadDisplayId) {
if (contains(address)) {
Slog.w(TAG, "Attempting to add second definition for display-device: " + address);
return null;
@@ -93,7 +123,8 @@
// different layouts, a logical display can be destroyed and later recreated with the
// same logical display ID.
final int logicalDisplayId = idProducer.getId(isDefault);
- final Display display = new Display(address, logicalDisplayId, isEnabled);
+ final Display display = new Display(address, logicalDisplayId, isEnabled,
+ brightnessThrottlingMapId, position, leadDisplayId);
mDisplays.add(display);
return display;
@@ -195,11 +226,33 @@
// {@link DeviceStateToLayoutMap.POSITION_UNKNOWN} is unspecified.
private int mPosition;
- Display(@NonNull DisplayAddress address, int logicalDisplayId, boolean isEnabled) {
+ // The ID of the brightness throttling map that should be used. This can change e.g. in
+ // concurrent displays mode in which a stricter brightness throttling policy might need to
+ // be used.
+ @Nullable
+ private final String mBrightnessThrottlingMapId;
+
+ // The ID of the lead display that this display will follow in a layout. -1 means no lead.
+ private int mLeadDisplayId;
+
+ // Refresh rate zone id for specific layout
+ @Nullable
+ private String mRefreshRateZoneId;
+
+ Display(@NonNull DisplayAddress address, int logicalDisplayId, boolean isEnabled,
+ String brightnessThrottlingMapId, int position, int leadDisplayId) {
mAddress = address;
mLogicalDisplayId = logicalDisplayId;
mIsEnabled = isEnabled;
- mPosition = POSITION_UNKNOWN;
+ mPosition = position;
+ mBrightnessThrottlingMapId = brightnessThrottlingMapId;
+
+ if (leadDisplayId == mLogicalDisplayId) {
+ mLeadDisplayId = NO_LEAD_DISPLAY;
+ } else {
+ mLeadDisplayId = leadDisplayId;
+ }
+
}
@Override
@@ -209,6 +262,9 @@
+ "(" + (mIsEnabled ? "ON" : "OFF") + ")"
+ ", addr: " + mAddress
+ ((mPosition == POSITION_UNKNOWN) ? "" : ", position: " + mPosition)
+ + ", brightnessThrottlingMapId: " + mBrightnessThrottlingMapId
+ + ", mRefreshRateZoneId: " + mRefreshRateZoneId
+ + ", mLeadDisplayId: " + mLeadDisplayId
+ "}";
}
@@ -223,7 +279,11 @@
return otherDisplay.mIsEnabled == this.mIsEnabled
&& otherDisplay.mPosition == this.mPosition
&& otherDisplay.mLogicalDisplayId == this.mLogicalDisplayId
- && this.mAddress.equals(otherDisplay.mAddress);
+ && this.mAddress.equals(otherDisplay.mAddress)
+ && Objects.equals(mBrightnessThrottlingMapId,
+ otherDisplay.mBrightnessThrottlingMapId)
+ && Objects.equals(otherDisplay.mRefreshRateZoneId, this.mRefreshRateZoneId)
+ && this.mLeadDisplayId == otherDisplay.mLeadDisplayId;
}
@Override
@@ -233,6 +293,9 @@
result = 31 * result + mPosition;
result = 31 * result + mLogicalDisplayId;
result = 31 * result + mAddress.hashCode();
+ result = 31 * result + mBrightnessThrottlingMapId.hashCode();
+ result = 31 * result + Objects.hashCode(mRefreshRateZoneId);
+ result = 31 * result + mLeadDisplayId;
return result;
}
@@ -248,8 +311,56 @@
return mIsEnabled;
}
+
+ public void setRefreshRateZoneId(@Nullable String refreshRateZoneId) {
+ mRefreshRateZoneId = refreshRateZoneId;
+ }
+
+ @Nullable
+ public String getRefreshRateZoneId() {
+ return mRefreshRateZoneId;
+ }
+
+ /**
+ * Sets the position that this display is facing.
+ * @param position the display is facing.
+ */
public void setPosition(int position) {
mPosition = position;
}
+
+ /**
+ * @return The ID of the brightness throttling map that this display should use.
+ */
+ public String getBrightnessThrottlingMapId() {
+ return mBrightnessThrottlingMapId;
+ }
+
+ /**
+ *
+ * @return the position that this display is facing.
+ */
+ public int getPosition() {
+ return mPosition;
+ }
+
+ /**
+ * Set the display that this display should follow certain properties of, for example,
+ * brightness
+ * @param displayId of the lead display.
+ */
+ public void setLeadDisplay(int displayId) {
+ if (displayId != mLogicalDisplayId) {
+ mLeadDisplayId = displayId;
+ }
+ }
+
+ /**
+ *
+ * @return logical displayId of the display that this one follows.
+ */
+ public int getLeadDisplayId() {
+ return mLeadDisplayId;
+ }
}
}
diff --git a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java
index 85d5b4f..5b772fc 100644
--- a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java
+++ b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java
@@ -419,16 +419,16 @@
float ambientBrightness = mBrightnessFilter.getEstimate(time);
mLatestAmbientBrightness = ambientBrightness;
- if (ambientColorTemperature != -1.0f &&
- mLowLightAmbientBrightnessToBiasSpline != null) {
+ if (ambientColorTemperature != -1.0f && ambientBrightness != -1.0f
+ && mLowLightAmbientBrightnessToBiasSpline != null) {
float bias = mLowLightAmbientBrightnessToBiasSpline.interpolate(ambientBrightness);
ambientColorTemperature =
bias * ambientColorTemperature + (1.0f - bias)
* mLowLightAmbientColorTemperature;
mLatestLowLightBias = bias;
}
- if (ambientColorTemperature != -1.0f &&
- mHighLightAmbientBrightnessToBiasSpline != null) {
+ if (ambientColorTemperature != -1.0f && ambientBrightness != -1.0f
+ && mHighLightAmbientBrightnessToBiasSpline != null) {
float bias = mHighLightAmbientBrightnessToBiasSpline.interpolate(ambientBrightness);
ambientColorTemperature =
(1.0f - bias) * ambientColorTemperature + bias
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index d9cdba7..caa5036 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -72,6 +72,7 @@
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.input.InputManagerInternal;
+import com.android.server.pm.UserManagerInternal;
import com.android.server.wm.ActivityInterceptorCallback;
import com.android.server.wm.ActivityTaskManagerInternal;
@@ -639,8 +640,10 @@
}
private boolean dreamsEnabledForUser(int userId) {
- // TODO(b/257333623): Support non-system Dock Users in HSUM.
- return !mDreamsOnlyEnabledForDockUser || (userId == UserHandle.USER_SYSTEM);
+ if (!mDreamsOnlyEnabledForDockUser) return true;
+ if (userId < 0) return false;
+ final int mainUserId = LocalServices.getService(UserManagerInternal.class).getMainUserId();
+ return userId == mainUserId;
}
private ServiceInfo getServiceInfo(ComponentName name) {
diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
index 1a357ee..41053e9 100644
--- a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
+++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
@@ -21,11 +21,14 @@
import android.annotation.Nullable;
import android.app.IGrammaticalInflectionManager;
import android.content.Context;
+import android.content.pm.PackageManagerInternal;
import android.os.Binder;
import android.os.IBinder;
import android.os.Process;
import android.os.SystemProperties;
+import android.util.Log;
+import com.android.internal.util.FrameworkStatsLog;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.wm.ActivityTaskManagerInternal;
@@ -36,9 +39,10 @@
* <p>This service is API entry point for storing app-specific grammatical inflection.
*/
public class GrammaticalInflectionService extends SystemService {
-
+ private final String TAG = "GrammaticalInflection";
private final GrammaticalInflectionBackupHelper mBackupHelper;
private final ActivityTaskManagerInternal mActivityTaskManagerInternal;
+ private PackageManagerInternal mPackageManagerInternal;
private static final String GRAMMATICAL_INFLECTION_ENABLED =
"i18n.grammatical_Inflection.enabled";
@@ -55,6 +59,7 @@
public GrammaticalInflectionService(Context context) {
super(context);
mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
+ mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
mBackupHelper = new GrammaticalInflectionBackupHelper(
this, context.getPackageManager());
}
@@ -110,14 +115,26 @@
protected void setRequestedApplicationGrammaticalGender(
String appPackageName, int userId, int gender) {
- if (!SystemProperties.getBoolean(GRAMMATICAL_INFLECTION_ENABLED, true)) {
- return;
- }
-
+ int preValue = getApplicationGrammaticalGender(appPackageName, userId);
final ActivityTaskManagerInternal.PackageConfigurationUpdater updater =
mActivityTaskManagerInternal.createPackageConfigurationUpdater(appPackageName,
userId);
+ if (!SystemProperties.getBoolean(GRAMMATICAL_INFLECTION_ENABLED, true)) {
+ if (preValue != GRAMMATICAL_GENDER_NOT_SPECIFIED) {
+ Log.d(TAG, "Clearing the user's grammatical gender setting");
+ updater.setGrammaticalGender(GRAMMATICAL_GENDER_NOT_SPECIFIED).commit();
+ }
+ return;
+ }
+
+ final int uid = mPackageManagerInternal.getPackageUid(appPackageName, 0, userId);
+ FrameworkStatsLog.write(FrameworkStatsLog.GRAMMATICAL_INFLECTION_CHANGED,
+ FrameworkStatsLog.APPLICATION_GRAMMATICAL_INFLECTION_CHANGED__SOURCE_ID__OTHERS,
+ uid,
+ gender != GRAMMATICAL_GENDER_NOT_SPECIFIED,
+ preValue != GRAMMATICAL_GENDER_NOT_SPECIFIED);
+
updater.setGrammaticalGender(gender).commit();
}
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 14b9121..740d2a3 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -2481,6 +2481,7 @@
Slog.w(TAG, "Local tv device not available to change arc mode.");
return;
}
+ tv.startArcAction(enabled);
}
});
}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index e2caeec..172aa20 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -32,7 +32,6 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
-import android.database.ContentObserver;
import android.graphics.PointF;
import android.hardware.SensorPrivacyManager;
import android.hardware.SensorPrivacyManager.Sensors;
@@ -78,8 +77,6 @@
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
import android.provider.DeviceConfig;
-import android.provider.Settings;
-import android.provider.Settings.SettingNotFoundException;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.IndentingPrintWriter;
@@ -147,8 +144,6 @@
private static final String EXCLUDED_DEVICES_PATH = "etc/excluded-input-devices.xml";
private static final String PORT_ASSOCIATIONS_PATH = "etc/input-port-associations.xml";
- // Feature flag name for the deep press feature
- private static final String DEEP_PRESS_ENABLED = "deep_press_enabled";
// Feature flag name for the strategy to be used in VelocityTracker
private static final String VELOCITYTRACKER_STRATEGY_PROPERTY = "velocitytracker_strategy";
@@ -307,6 +302,9 @@
@GuardedBy("mInputMonitors")
final Map<IBinder, GestureMonitorSpyWindow> mInputMonitors = new HashMap<>();
+ // Watches for settings changes and updates the native side appropriately.
+ private final InputSettingsObserver mSettingsObserver;
+
// Manages Keyboard layouts for Physical keyboards
private final KeyboardLayoutManager mKeyboardLayoutManager;
@@ -428,6 +426,7 @@
mContext = injector.getContext();
mHandler = new InputManagerHandler(injector.getLooper());
mNative = injector.getNativeService(this);
+ mSettingsObserver = new InputSettingsObserver(mContext, mHandler, mNative);
mKeyboardLayoutManager = new KeyboardLayoutManager(mContext, mNative, mDataStore,
injector.getLooper());
mBatteryController = new BatteryController(mContext, mNative, injector.getLooper());
@@ -493,39 +492,7 @@
// Add ourselves to the Watchdog monitors.
Watchdog.getInstance().addMonitor(this);
- registerMousePointerSpeedSettingObserver();
- registerTouchpadPointerSpeedSettingObserver();
- registerTouchpadNaturalScrollingEnabledObserver();
- registerTouchpadTapToClickEnabledObserver();
- registerTouchpadRightClickZoneEnabledObserver();
- registerShowTouchesSettingObserver();
- registerAccessibilityLargePointerSettingObserver();
- registerLongPressTimeoutObserver();
- registerMaximumObscuringOpacityForTouchSettingObserver();
-
- mContext.registerReceiver(new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- updateMousePointerSpeedFromSettings();
- updateTouchpadPointerSpeedFromSettings();
- updateTouchpadNaturalScrollingEnabledFromSettings();
- updateTouchpadTapToClickEnabledFromSettings();
- updateTouchpadRightClickZoneEnabledFromSettings();
- updateShowTouchesFromSettings();
- updateAccessibilityLargePointerFromSettings();
- updateDeepPressStatusFromSettings("user switched");
- }
- }, new IntentFilter(Intent.ACTION_USER_SWITCHED), null, mHandler);
-
- updateMousePointerSpeedFromSettings();
- updateTouchpadPointerSpeedFromSettings();
- updateTouchpadNaturalScrollingEnabledFromSettings();
- updateTouchpadTapToClickEnabledFromSettings();
- updateTouchpadRightClickZoneEnabledFromSettings();
- updateShowTouchesFromSettings();
- updateAccessibilityLargePointerFromSettings();
- updateDeepPressStatusFromSettings("just booted");
- updateMaximumObscuringOpacityForTouchFromSettings();
+ mSettingsObserver.registerAndUpdate();
}
// TODO(BT) Pass in parameter for bluetooth system
@@ -1349,11 +1316,6 @@
setPointerSpeedUnchecked(speed);
}
- private void updateMousePointerSpeedFromSettings() {
- int speed = getMousePointerSpeedSetting();
- setPointerSpeedUnchecked(speed);
- }
-
private void setPointerSpeedUnchecked(int speed) {
speed = Math.min(Math.max(speed, InputManager.MIN_POINTER_SPEED),
InputManager.MAX_POINTER_SPEED);
@@ -1370,194 +1332,6 @@
properties -> properties.pointerIconVisible = visible);
}
- private void registerMousePointerSpeedSettingObserver() {
- mContext.getContentResolver().registerContentObserver(
- Settings.System.getUriFor(Settings.System.POINTER_SPEED), true,
- new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange) {
- updateMousePointerSpeedFromSettings();
- }
- }, UserHandle.USER_ALL);
- }
-
- private int getMousePointerSpeedSetting() {
- int speed = InputManager.DEFAULT_POINTER_SPEED;
- try {
- speed = Settings.System.getIntForUser(mContext.getContentResolver(),
- Settings.System.POINTER_SPEED, UserHandle.USER_CURRENT);
- } catch (SettingNotFoundException ignored) {
- }
- return speed;
- }
-
- private void registerTouchpadPointerSpeedSettingObserver() {
- mContext.getContentResolver().registerContentObserver(
- Settings.System.getUriFor(Settings.System.TOUCHPAD_POINTER_SPEED), true,
- new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange) {
- updateTouchpadPointerSpeedFromSettings();
- }
- }, UserHandle.USER_ALL);
- }
-
- private void updateTouchpadPointerSpeedFromSettings() {
- int speed = Settings.System.getIntForUser(mContext.getContentResolver(),
- Settings.System.TOUCHPAD_POINTER_SPEED, InputManager.DEFAULT_POINTER_SPEED,
- UserHandle.USER_CURRENT);
- speed = Math.min(Math.max(speed, InputManager.MIN_POINTER_SPEED),
- InputManager.MAX_POINTER_SPEED);
- mNative.setTouchpadPointerSpeed(speed);
- }
-
- private void registerTouchpadNaturalScrollingEnabledObserver() {
- mContext.getContentResolver().registerContentObserver(
- Settings.System.getUriFor(Settings.System.TOUCHPAD_NATURAL_SCROLLING), true,
- new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange) {
- updateTouchpadNaturalScrollingEnabledFromSettings();
- }
- }, UserHandle.USER_ALL);
- }
-
- private void updateTouchpadNaturalScrollingEnabledFromSettings() {
- int setting = Settings.System.getIntForUser(mContext.getContentResolver(),
- Settings.System.TOUCHPAD_NATURAL_SCROLLING, 0, UserHandle.USER_CURRENT);
- mNative.setTouchpadNaturalScrollingEnabled(setting != 0);
- }
-
- private void registerTouchpadTapToClickEnabledObserver() {
- mContext.getContentResolver().registerContentObserver(
- Settings.System.getUriFor(Settings.System.TOUCHPAD_TAP_TO_CLICK), true,
- new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange) {
- updateTouchpadTapToClickEnabledFromSettings();
- }
- }, UserHandle.USER_ALL);
- }
-
- private void updateTouchpadTapToClickEnabledFromSettings() {
- int setting = Settings.System.getIntForUser(mContext.getContentResolver(),
- Settings.System.TOUCHPAD_TAP_TO_CLICK, 0, UserHandle.USER_CURRENT);
- mNative.setTouchpadTapToClickEnabled(setting != 0);
- }
-
- private void registerTouchpadRightClickZoneEnabledObserver() {
- mContext.getContentResolver().registerContentObserver(
- Settings.System.getUriFor(Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE), true,
- new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange) {
- updateTouchpadRightClickZoneEnabledFromSettings();
- }
- }, UserHandle.USER_ALL);
- }
-
- private void updateTouchpadRightClickZoneEnabledFromSettings() {
- int setting = Settings.System.getIntForUser(mContext.getContentResolver(),
- Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE, 0, UserHandle.USER_CURRENT);
- mNative.setTouchpadRightClickZoneEnabled(setting != 0);
- }
-
- private void updateShowTouchesFromSettings() {
- int setting = getShowTouchesSetting(0);
- mNative.setShowTouches(setting != 0);
- }
-
- private void registerShowTouchesSettingObserver() {
- mContext.getContentResolver().registerContentObserver(
- Settings.System.getUriFor(Settings.System.SHOW_TOUCHES), true,
- new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange) {
- updateShowTouchesFromSettings();
- }
- }, UserHandle.USER_ALL);
- }
-
- private void updateAccessibilityLargePointerFromSettings() {
- final int accessibilityConfig = Settings.Secure.getIntForUser(
- mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON,
- 0, UserHandle.USER_CURRENT);
- PointerIcon.setUseLargeIcons(accessibilityConfig == 1);
- mNative.reloadPointerIcons();
- }
-
- private void registerAccessibilityLargePointerSettingObserver() {
- mContext.getContentResolver().registerContentObserver(
- Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON), true,
- new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange) {
- updateAccessibilityLargePointerFromSettings();
- }
- }, UserHandle.USER_ALL);
- }
-
- private void updateDeepPressStatusFromSettings(String reason) {
- // Not using ViewConfiguration.getLongPressTimeout here because it may return a stale value
- final int timeout = Settings.Secure.getIntForUser(mContext.getContentResolver(),
- Settings.Secure.LONG_PRESS_TIMEOUT, ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT,
- UserHandle.USER_CURRENT);
- final boolean featureEnabledFlag =
- DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT,
- DEEP_PRESS_ENABLED, true /* default */);
- final boolean enabled =
- featureEnabledFlag && timeout <= ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT;
- Log.i(TAG,
- (enabled ? "Enabling" : "Disabling") + " motion classifier because " + reason
- + ": feature " + (featureEnabledFlag ? "enabled" : "disabled")
- + ", long press timeout = " + timeout);
- mNative.setMotionClassifierEnabled(enabled);
- }
-
- private void registerLongPressTimeoutObserver() {
- mContext.getContentResolver().registerContentObserver(
- Settings.Secure.getUriFor(Settings.Secure.LONG_PRESS_TIMEOUT), true,
- new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange) {
- updateDeepPressStatusFromSettings("timeout changed");
- }
- }, UserHandle.USER_ALL);
- }
-
- private void registerMaximumObscuringOpacityForTouchSettingObserver() {
- mContext.getContentResolver().registerContentObserver(
- Settings.Global.getUriFor(Settings.Global.MAXIMUM_OBSCURING_OPACITY_FOR_TOUCH),
- /* notifyForDescendants */ true,
- new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange) {
- updateMaximumObscuringOpacityForTouchFromSettings();
- }
- }, UserHandle.USER_ALL);
- }
-
- private void updateMaximumObscuringOpacityForTouchFromSettings() {
- InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class));
- final float opacity = im.getMaximumObscuringOpacityForTouch();
- if (opacity < 0 || opacity > 1) {
- Log.e(TAG, "Invalid maximum obscuring opacity " + opacity
- + ", it should be >= 0 and <= 1, rejecting update.");
- return;
- }
- mNative.setMaximumObscuringOpacityForTouch(opacity);
- }
-
- private int getShowTouchesSetting(int defaultValue) {
- int result = defaultValue;
- try {
- result = Settings.System.getIntForUser(mContext.getContentResolver(),
- Settings.System.SHOW_TOUCHES, UserHandle.USER_CURRENT);
- } catch (SettingNotFoundException snfe) {
- }
- return result;
- }
-
/**
* Update the display on which the mouse pointer is shown.
*
@@ -2974,6 +2748,13 @@
return null;
}
+ // Native callback.
+ @SuppressWarnings("unused")
+ private boolean isStylusPointerIconEnabled() {
+ return Objects.requireNonNull(mContext.getSystemService(InputManager.class))
+ .isStylusPointerIconEnabled();
+ }
+
private static class PointerDisplayIdChangedArgs {
final int mPointerDisplayId;
final float mXPosition;
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
new file mode 100644
index 0000000..a113d01
--- /dev/null
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.input;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.hardware.input.InputManager;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.DeviceConfig;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.PointerIcon;
+import android.view.ViewConfiguration;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/** Observes settings changes and propagates them to the native side. */
+class InputSettingsObserver extends ContentObserver {
+ static final String TAG = "InputManager";
+
+ /** Feature flag name for the deep press feature */
+ private static final String DEEP_PRESS_ENABLED = "deep_press_enabled";
+
+ private final Context mContext;
+ private final Handler mHandler;
+ private final NativeInputManagerService mNative;
+ private final Map<Uri, Consumer<String /* reason*/>> mObservers;
+
+ InputSettingsObserver(Context context, Handler handler, NativeInputManagerService nativeIms) {
+ super(handler);
+ mContext = context;
+ mHandler = handler;
+ mNative = nativeIms;
+ mObservers = Map.ofEntries(
+ Map.entry(Settings.System.getUriFor(Settings.System.POINTER_SPEED),
+ (reason) -> updateMousePointerSpeed()),
+ Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_POINTER_SPEED),
+ (reason) -> updateTouchpadPointerSpeed()),
+ Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_NATURAL_SCROLLING),
+ (reason) -> updateTouchpadNaturalScrollingEnabled()),
+ Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_TAP_TO_CLICK),
+ (reason) -> updateTouchpadTapToClickEnabled()),
+ Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE),
+ (reason) -> updateTouchpadRightClickZoneEnabled()),
+ Map.entry(Settings.System.getUriFor(Settings.System.SHOW_TOUCHES),
+ (reason) -> updateShowTouches()),
+ Map.entry(Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON),
+ (reason) -> updateAccessibilityLargePointer()),
+ Map.entry(Settings.Secure.getUriFor(Settings.Secure.LONG_PRESS_TIMEOUT),
+ (reason) -> updateDeepPressStatus(reason)),
+ Map.entry(
+ Settings.Global.getUriFor(Settings.Global.MAXIMUM_OBSCURING_OPACITY_FOR_TOUCH),
+ (reason) -> updateMaximumObscuringOpacityForTouch()));
+ }
+
+ /**
+ * Registers observers for input-related settings and updates the input subsystem with their
+ * current values.
+ */
+ public void registerAndUpdate() {
+ for (Uri uri : mObservers.keySet()) {
+ mContext.getContentResolver().registerContentObserver(
+ uri, true /* notifyForDescendants */, this, UserHandle.USER_ALL);
+ }
+
+ mContext.registerReceiver(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ for (Consumer<String> observer : mObservers.values()) {
+ observer.accept("user switched");
+ }
+ }
+ }, new IntentFilter(Intent.ACTION_USER_SWITCHED), null, mHandler);
+
+ for (Consumer<String> observer : mObservers.values()) {
+ observer.accept("just booted");
+ }
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ mObservers.get(uri).accept("setting changed");
+ }
+
+ private boolean getBoolean(String settingName, boolean defaultValue) {
+ final int setting = Settings.System.getIntForUser(mContext.getContentResolver(),
+ settingName, defaultValue ? 1 : 0, UserHandle.USER_CURRENT);
+ return setting != 0;
+ }
+
+ private int getPointerSpeedValue(String settingName) {
+ int speed = Settings.System.getIntForUser(mContext.getContentResolver(),
+ settingName, InputManager.DEFAULT_POINTER_SPEED, UserHandle.USER_CURRENT);
+ return Math.min(Math.max(speed, InputManager.MIN_POINTER_SPEED),
+ InputManager.MAX_POINTER_SPEED);
+ }
+
+ private void updateMousePointerSpeed() {
+ mNative.setPointerSpeed(getPointerSpeedValue(Settings.System.POINTER_SPEED));
+ }
+
+ private void updateTouchpadPointerSpeed() {
+ mNative.setTouchpadPointerSpeed(
+ getPointerSpeedValue(Settings.System.TOUCHPAD_POINTER_SPEED));
+ }
+
+ private void updateTouchpadNaturalScrollingEnabled() {
+ mNative.setTouchpadNaturalScrollingEnabled(
+ getBoolean(Settings.System.TOUCHPAD_NATURAL_SCROLLING, true));
+ }
+
+ private void updateTouchpadTapToClickEnabled() {
+ mNative.setTouchpadTapToClickEnabled(
+ getBoolean(Settings.System.TOUCHPAD_TAP_TO_CLICK, true));
+ }
+
+ private void updateTouchpadRightClickZoneEnabled() {
+ mNative.setTouchpadRightClickZoneEnabled(
+ getBoolean(Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE, false));
+ }
+
+ private void updateShowTouches() {
+ mNative.setShowTouches(getBoolean(Settings.System.SHOW_TOUCHES, false));
+ }
+
+ private void updateAccessibilityLargePointer() {
+ final int accessibilityConfig = Settings.Secure.getIntForUser(
+ mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON,
+ 0, UserHandle.USER_CURRENT);
+ PointerIcon.setUseLargeIcons(accessibilityConfig == 1);
+ mNative.reloadPointerIcons();
+ }
+
+ private void updateDeepPressStatus(String reason) {
+ // Not using ViewConfiguration.getLongPressTimeout here because it may return a stale value
+ final int timeout = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ Settings.Secure.LONG_PRESS_TIMEOUT, ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT,
+ UserHandle.USER_CURRENT);
+ final boolean featureEnabledFlag =
+ DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT,
+ DEEP_PRESS_ENABLED, true /* default */);
+ final boolean enabled =
+ featureEnabledFlag && timeout <= ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT;
+ Log.i(TAG,
+ (enabled ? "Enabling" : "Disabling") + " motion classifier because " + reason
+ + ": feature " + (featureEnabledFlag ? "enabled" : "disabled")
+ + ", long press timeout = " + timeout);
+ mNative.setMotionClassifierEnabled(enabled);
+ }
+
+ private void updateMaximumObscuringOpacityForTouch() {
+ InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class));
+ final float opacity = im.getMaximumObscuringOpacityForTouch();
+ if (opacity < 0 || opacity > 1) {
+ Log.e(TAG, "Invalid maximum obscuring opacity " + opacity
+ + ", it should be >= 0 and <= 1, rejecting update.");
+ return;
+ }
+ mNative.setMaximumObscuringOpacityForTouch(opacity);
+ }
+}
diff --git a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
index e21895a..298f6ad 100644
--- a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
+++ b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
@@ -27,7 +27,6 @@
import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_SHOW_IME_IMPLICIT;
import android.annotation.Nullable;
-import android.os.Binder;
import android.os.IBinder;
import android.os.ResultReceiver;
import android.util.EventLog;
@@ -65,13 +64,10 @@
@GuardedBy("ImfLock.class")
@Override
- public void performShowIme(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
+ public void performShowIme(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
int showFlags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
final IInputMethodInvoker curMethod = mService.getCurMethodLocked();
if (curMethod != null) {
- // create a placeholder token for IMS so that IMS cannot inject windows into client app.
- final IBinder showInputToken = new Binder();
- mService.setRequestImeTokenToWindow(windowToken, showInputToken);
if (DEBUG) {
Slog.v(TAG, "Calling " + curMethod + ".showSoftInput(" + showInputToken
+ ", " + showFlags + ", " + resultReceiver + ") for reason: "
@@ -86,7 +82,7 @@
InputMethodDebug.softInputModeToString(
mService.mCurFocusedWindowSoftInputMode));
}
- mService.onShowHideSoftInputRequested(true /* show */, windowToken, reason,
+ mService.onShowHideSoftInputRequested(true /* show */, showInputToken, reason,
statsToken);
}
}
@@ -94,12 +90,10 @@
@GuardedBy("ImfLock.class")
@Override
- public void performHideIme(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
+ public void performHideIme(IBinder hideInputToken, @Nullable ImeTracker.Token statsToken,
ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
final IInputMethodInvoker curMethod = mService.getCurMethodLocked();
if (curMethod != null) {
- final Binder hideInputToken = new Binder();
- mService.setRequestImeTokenToWindow(windowToken, hideInputToken);
// The IME will report its visible state again after the following message finally
// delivered to the IME process as an IPC. Hence the inconsistency between
// IMMS#mInputShown and IMMS#mImeWindowVis should be resolved spontaneously in
@@ -118,7 +112,7 @@
InputMethodDebug.softInputModeToString(
mService.mCurFocusedWindowSoftInputMode));
}
- mService.onShowHideSoftInputRequested(false /* show */, windowToken, reason,
+ mService.onShowHideSoftInputRequested(false /* show */, hideInputToken, reason,
statsToken);
}
}
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
index e97ec93..f03e985 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
@@ -31,26 +31,26 @@
/**
* Performs showing IME on top of the given window.
*
- * @param windowToken The token of a window that currently has focus.
+ * @param showInputToken A token that represents the requester to show IME.
* @param statsToken A token that tracks the progress of an IME request.
* @param showFlags Provides additional operating flags to show IME.
* @param resultReceiver If non-null, this will be called back to the caller when
* it has processed request to tell what it has done.
* @param reason The reason for requesting to show IME.
*/
- default void performShowIme(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
+ default void performShowIme(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
int showFlags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {}
/**
* Performs hiding IME to the given window
*
- * @param windowToken The token of a window that currently has focus.
+ * @param hideInputToken A token that represents the requester to hide IME.
* @param statsToken A token that tracks the progress of an IME request.
* @param resultReceiver If non-null, this will be called back to the caller when
* it has processed request to tell what it has done.
* @param reason The reason for requesting to hide IME.
*/
- default void performHideIme(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
+ default void performHideIme(IBinder hideInputToken, @Nullable ImeTracker.Token statsToken,
ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {}
/**
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
index db61e954..eaca842 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
@@ -35,6 +35,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.content.res.Configuration;
+import android.os.Binder;
import android.os.IBinder;
import android.util.PrintWriterPrinter;
import android.util.Printer;
@@ -263,6 +264,8 @@
// policy.
mPolicy.mPendingA11yRequestingHideKeyboard = false;
}
+ // create a placeholder token for IMS so that IMS cannot inject windows into client app.
+ state.setRequestImeToken(new Binder());
setWindowStateInner(windowToken, state);
}
@@ -279,14 +282,6 @@
return state;
}
- void setRequestImeTokenToWindow(IBinder windowToken, IBinder token) {
- ImeTargetWindowState state = getWindowStateOrNull(windowToken);
- if (state != null) {
- state.setRequestImeToken(token);
- setWindowStateInner(windowToken, state);
- }
- }
-
void setWindowState(IBinder windowToken, @NonNull ImeTargetWindowState newState) {
final ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken);
if (state != null && newState.hasEdiorFocused()) {
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 2ced988..27daceb 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -3344,11 +3344,6 @@
}
}
- @GuardedBy("ImfLock.class")
- void setRequestImeTokenToWindow(IBinder windowToken, IBinder token) {
- mVisibilityStateComputer.setRequestImeTokenToWindow(windowToken, token);
- }
-
@BinderThread
@Override
public void reportPerceptibleAsync(IBinder windowToken, boolean perceptible) {
@@ -4603,10 +4598,13 @@
}
}
- /** Called right after {@link com.android.internal.inputmethod.IInputMethod#showSoftInput}. */
+ /**
+ * Called right after {@link IInputMethod#showSoftInput} or {@link IInputMethod#hideSoftInput}.
+ */
@GuardedBy("ImfLock.class")
- void onShowHideSoftInputRequested(boolean show, IBinder requestToken,
+ void onShowHideSoftInputRequested(boolean show, IBinder requestImeToken,
@SoftInputShowHideReason int reason, @Nullable ImeTracker.Token statsToken) {
+ final IBinder requestToken = mVisibilityStateComputer.getWindowTokenFrom(requestImeToken);
final WindowManagerInternal.ImeTargetInfo info =
mWindowManagerInternal.onToggleImeRequested(
show, mCurFocusedWindow, requestToken, mCurTokenDisplayId);
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index ebc18bc..b4fc195 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -29,7 +29,7 @@
import static android.content.Context.KEYGUARD_SERVICE;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.UserHandle.USER_ALL;
-import static android.provider.DeviceConfig.NAMESPACE_AUTO_PIN_CONFIRMATION;
+import static android.os.UserHandle.USER_SYSTEM;
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
@@ -57,6 +57,8 @@
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
+import android.app.RemoteLockscreenValidationResult;
+import android.app.StartLockscreenValidationRequest;
import android.app.admin.DevicePolicyManager;
import android.app.admin.DevicePolicyManagerInternal;
import android.app.admin.DeviceStateCache;
@@ -70,6 +72,7 @@
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
+import android.content.res.Resources;
import android.database.ContentObserver;
import android.database.sqlite.SQLiteDatabase;
import android.hardware.authsecret.IAuthSecret;
@@ -110,12 +113,12 @@
import android.security.keystore2.AndroidKeyStoreLoadStoreParameter;
import android.security.keystore2.AndroidKeyStoreProvider;
import android.service.gatekeeper.IGateKeeperService;
+import android.service.notification.StatusBarNotification;
import android.system.keystore2.Domain;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.EventLog;
-import android.util.FeatureFlagUtils;
import android.util.LongSparseArray;
import android.util.Slog;
import android.util.SparseArray;
@@ -218,6 +221,8 @@
private static final String PROFILE_KEY_NAME_ENCRYPT = "profile_key_name_encrypt_";
private static final String PROFILE_KEY_NAME_DECRYPT = "profile_key_name_decrypt_";
+ private static final int HEADLESS_VENDOR_AUTH_SECRET_LENGTH = 32;
+
// Order of holding lock: mSeparateChallengeLock -> mSpManager -> this
// Do not call into ActivityManager while holding mSpManager lock.
private final Object mSeparateChallengeLock = new Object();
@@ -266,6 +271,13 @@
@VisibleForTesting
protected boolean mHasSecureLockScreen;
+ @VisibleForTesting
+ protected final Object mHeadlessAuthSecretLock = new Object();
+
+ @VisibleForTesting
+ @GuardedBy("mHeadlessAuthSecretLock")
+ protected byte[] mAuthSecret;
+
protected IGateKeeperService mGateKeeperService;
protected IAuthSecret mAuthSecretService;
@@ -562,6 +574,15 @@
java.security.KeyStore ks) {
return new ManagedProfilePasswordCache(ks, getUserManager());
}
+
+ public boolean isHeadlessSystemUserMode() {
+ return UserManager.isHeadlessSystemUserMode();
+ }
+
+ public boolean isMainUserPermanentAdmin() {
+ return Resources.getSystem()
+ .getBoolean(com.android.internal.R.bool.config_isMainUserPermanentAdmin);
+ }
}
public LockSettingsService(Context context) {
@@ -581,6 +602,7 @@
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_USER_ADDED);
filter.addAction(Intent.ACTION_USER_STARTING);
+ filter.addAction(Intent.ACTION_LOCALE_CHANGED);
injector.getContext().registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter,
null, null);
@@ -602,6 +624,20 @@
LocalServices.addService(LockSettingsInternal.class, new LocalService());
}
+ private void updateActivatedEncryptionNotifications(String reason) {
+ for (UserInfo userInfo : mUserManager.getUsers()) {
+ Context userContext = mContext.createContextAsUser(UserHandle.of(userInfo.id), 0);
+ NotificationManager nm = (NotificationManager)
+ userContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ for (StatusBarNotification notification : nm.getActiveNotifications()) {
+ if (notification.getId() == SystemMessage.NOTE_FBE_ENCRYPTED_NOTIFICATION) {
+ maybeShowEncryptionNotificationForUser(userInfo.id, reason);
+ break;
+ }
+ }
+ }
+ }
+
/**
* If the account is credential-encrypted, show notification requesting the user to unlock the
* device.
@@ -799,6 +835,8 @@
} else if (Intent.ACTION_USER_STARTING.equals(intent.getAction())) {
final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
mStorage.prefetchUser(userHandle);
+ } else if (Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())) {
+ updateActivatedEncryptionNotifications("locale changed");
}
}
};
@@ -1679,7 +1717,7 @@
throw new IllegalStateException("password change failed");
}
- onSyntheticPasswordKnown(userId, sp);
+ onSyntheticPasswordUnlocked(userId, sp);
setLockCredentialWithSpLocked(credential, sp, userId);
sendCredentialsOnChangeIfRequired(credential, userId, isLockTiedToParent);
return true;
@@ -1690,8 +1728,7 @@
if (newCredential.isPattern()) {
setBoolean(LockPatternUtils.PATTERN_EVER_CHOSEN_KEY, true, userHandle);
}
- if (DeviceConfig.getBoolean(NAMESPACE_AUTO_PIN_CONFIRMATION,
- "enable_auto_pin_confirmation", /* defaultValue= */ false)) {
+ if (LockPatternUtils.isAutoPinConfirmFeatureAvailable()) {
if (newCredential.isPin()) {
setLong(LockPatternUtils.PIN_LENGTH, newCredential.size(), userHandle);
}
@@ -1991,7 +2028,7 @@
Slogf.wtf(TAG, "Failed to unwrap synthetic password for unsecured user %d", userId);
return;
}
- onSyntheticPasswordKnown(userId, result.syntheticPassword);
+ onSyntheticPasswordUnlocked(userId, result.syntheticPassword);
unlockUserKey(userId, result.syntheticPassword);
}
}
@@ -2515,23 +2552,18 @@
/**
* Starts a session to verify lock screen credentials provided by a remote device.
*/
- public void startRemoteLockscreenValidation() {
- if (!FeatureFlagUtils.isEnabled(mContext,
- FeatureFlagUtils.SETTINGS_ENABLE_LOCKSCREEN_TRANSFER_API)) {
- throw new UnsupportedOperationException("Under development");
- }
- mRecoverableKeyStoreManager.startRemoteLockscreenValidation();
+ @NonNull
+ public StartLockscreenValidationRequest startRemoteLockscreenValidation() {
+ return mRecoverableKeyStoreManager.startRemoteLockscreenValidation(this);
}
/**
- * Verifies credentials guess from a remote device.
+ * Verifies encrypted credentials guess from a remote device.
*/
- public void validateRemoteLockscreen(@NonNull byte[] encryptedCredential) {
- if (!FeatureFlagUtils.isEnabled(mContext,
- FeatureFlagUtils.SETTINGS_ENABLE_LOCKSCREEN_TRANSFER_API)) {
- throw new UnsupportedOperationException("Under development");
- }
- mRecoverableKeyStoreManager.validateRemoteLockscreen(encryptedCredential);
+ @NonNull
+ public RemoteLockscreenValidationResult
+ validateRemoteLockScreen2(@NonNull byte[] encryptedCredential) {
+ return mRecoverableKeyStoreManager.validateRemoteLockscreen(encryptedCredential, this);
}
// Reading these settings needs the contacts permission
@@ -2584,43 +2616,112 @@
}
}
- private void onSyntheticPasswordKnown(@UserIdInt int userId, SyntheticPassword sp) {
+ private void onSyntheticPasswordCreated(@UserIdInt int userId, SyntheticPassword sp) {
+ onSyntheticPasswordKnown(userId, sp, true);
+ }
+
+ private void onSyntheticPasswordUnlocked(@UserIdInt int userId, SyntheticPassword sp) {
+ onSyntheticPasswordKnown(userId, sp, false);
+ }
+
+ private void onSyntheticPasswordKnown(
+ @UserIdInt int userId, SyntheticPassword sp, boolean justCreated) {
if (mInjector.isGsiRunning()) {
Slog.w(TAG, "Running in GSI; skipping calls to AuthSecret and RebootEscrow");
return;
}
- mRebootEscrowManager.callToRebootEscrowIfNeeded(userId, sp.getVersion(),
- sp.getSyntheticPassword());
-
- callToAuthSecretIfNeeded(userId, sp);
+ mRebootEscrowManager.callToRebootEscrowIfNeeded(
+ userId, sp.getVersion(), sp.getSyntheticPassword());
+ callToAuthSecretIfNeeded(userId, sp, justCreated);
}
- private void callToAuthSecretIfNeeded(@UserIdInt int userId, SyntheticPassword sp) {
- // If the given user is the primary user, pass the auth secret to the HAL. Only the system
- // user can be primary. Check for the system user ID before calling getUserInfo(), as other
- // users may still be under construction.
+ /**
+ * Handles generation, storage, and sending of the vendor auth secret. Here we try to retrieve
+ * the auth secret to send it to the auth secret HAL, generate a fresh secret if need be, store
+ * it encrypted on disk so that the given user can unlock it in future, and stash it in memory
+ * so that when future users are created they can also unlock it.
+ *
+ * <p>Called whenever the SP of a user is available, except in GSI.
+ */
+ private void callToAuthSecretIfNeeded(
+ @UserIdInt int userId, SyntheticPassword sp, boolean justCreated) {
if (mAuthSecretService == null) {
+ // If there's no IAuthSecret service, we don't need to maintain a auth secret
return;
}
- if (userId == UserHandle.USER_SYSTEM &&
- mUserManager.getUserInfo(userId).isPrimary()) {
- final byte[] secret = sp.deriveVendorAuthSecret();
- try {
- mAuthSecretService.setPrimaryUserCredential(secret);
- } catch (RemoteException e) {
- Slog.w(TAG, "Failed to pass primary user secret to AuthSecret HAL", e);
+ // User may be partially created, so use the internal user manager interface
+ final UserManagerInternal userManagerInternal = mInjector.getUserManagerInternal();
+ final UserInfo userInfo = userManagerInternal.getUserInfo(userId);
+ if (userInfo == null) {
+ // User may be partially deleted, skip this.
+ return;
+ }
+ final byte[] authSecret;
+ if (!mInjector.isHeadlessSystemUserMode()) {
+ // On non-headless systems, the auth secret is derived from user 0's
+ // SP, and only user 0 passes it to the HAL.
+ if (userId != USER_SYSTEM) {
+ return;
}
+ authSecret = sp.deriveVendorAuthSecret();
+ } else if (!mInjector.isMainUserPermanentAdmin() || !userInfo.isFull()) {
+ // Only full users can receive or pass on the auth secret.
+ // If there is no main permanent admin user, we don't try to create or send
+ // an auth secret, since there may sometimes be no full users.
+ return;
+ } else if (justCreated) {
+ if (userInfo.isMain()) {
+ // The first user is just being created, so we create a new auth secret
+ // at the same time.
+ Slog.i(TAG, "Generating new vendor auth secret and storing for user: " + userId);
+ authSecret = SecureRandomUtils.randomBytes(HEADLESS_VENDOR_AUTH_SECRET_LENGTH);
+ // Store it in memory, for when new users are created.
+ synchronized (mHeadlessAuthSecretLock) {
+ mAuthSecret = authSecret;
+ }
+ } else {
+ // A new user is being created. Another user should already have logged in at
+ // this point, and therefore the auth secret should be stored in memory.
+ synchronized (mHeadlessAuthSecretLock) {
+ authSecret = mAuthSecret;
+ }
+ if (authSecret == null) {
+ Slog.e(TAG, "Creating non-main user " + userId
+ + " but vendor auth secret is not in memory");
+ return;
+ }
+ }
+ // Store the auth secret encrypted using the user's SP (which was just created).
+ mSpManager.writeVendorAuthSecret(authSecret, sp, userId);
+ } else {
+ // The user already exists, so the auth secret should be stored encrypted
+ // with that user's SP.
+ authSecret = mSpManager.readVendorAuthSecret(sp, userId);
+ if (authSecret == null) {
+ Slog.e(TAG, "Unable to read vendor auth secret for user: " + userId);
+ return;
+ }
+ // Store it in memory, for when new users are created.
+ synchronized (mHeadlessAuthSecretLock) {
+ mAuthSecret = authSecret;
+ }
+ }
+ Slog.i(TAG, "Sending vendor auth secret to IAuthSecret HAL as user: " + userId);
+ try {
+ mAuthSecretService.setPrimaryUserCredential(authSecret);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed to send vendor auth secret to IAuthSecret HAL", e);
}
}
/**
* Creates the synthetic password (SP) for the given user, protects it with an empty LSKF, and
* protects the user's CE key with a key derived from the SP.
- * <p>
- * This is called just once in the lifetime of the user: at user creation time (possibly delayed
- * until the time when Weaver is guaranteed to be available), or when upgrading from Android 13
- * or earlier where users with no LSKF didn't necessarily have an SP.
+ *
+ * <p>This is called just once in the lifetime of the user: at user creation time (possibly
+ * delayed until the time when Weaver is guaranteed to be available), or when upgrading from
+ * Android 13 or earlier where users with no LSKF didn't necessarily have an SP.
*/
@VisibleForTesting
SyntheticPassword initializeSyntheticPassword(int userId) {
@@ -2635,7 +2736,7 @@
LockscreenCredential.createNone(), sp, userId);
setCurrentLskfBasedProtectorId(protectorId, userId);
setUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey());
- onSyntheticPasswordKnown(userId, sp);
+ onSyntheticPasswordCreated(userId, sp);
return sp;
}
}
@@ -2702,7 +2803,7 @@
}
mStrongAuth.reportSuccessfulStrongAuthUnlock(userId);
- onSyntheticPasswordKnown(userId, sp);
+ onSyntheticPasswordUnlocked(userId, sp);
}
private void setDeviceUnlockedForUser(int userId) {
@@ -2990,7 +3091,7 @@
+ "verification.");
return false;
}
- onSyntheticPasswordKnown(userId, result.syntheticPassword);
+ onSyntheticPasswordUnlocked(userId, result.syntheticPassword);
setLockCredentialWithSpLocked(credential, result.syntheticPassword, userId);
return true;
}
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index cd972dc..c21c945 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -21,6 +21,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.app.admin.PasswordMetrics;
import android.content.Context;
import android.content.pm.UserInfo;
@@ -93,6 +94,9 @@
* while the LSKF is nonempty.
* SP_E0_NAME, SP_P1_NAME: Information needed to create and use escrow token-based protectors.
* Deleted when escrow token support is disabled for the user.
+ * VENDOR_AUTH_SECRET_NAME: A copy of the secret passed using the IAuthSecret interface,
+ * encrypted using a secret derived from the SP using
+ * PERSONALIZATION_AUTHSECRET_ENCRYPTION_KEY.
*
* For each protector, stored under the corresponding protector ID:
* SP_BLOB_NAME: The encrypted SP secret (the SP itself or the P0 value). Always exists.
@@ -120,6 +124,7 @@
private static final String PASSWORD_DATA_NAME = "pwd";
private static final String WEAVER_SLOT_NAME = "weaver";
private static final String PASSWORD_METRICS_NAME = "metrics";
+ private static final String VENDOR_AUTH_SECRET_NAME = "vendor_auth_secret";
// used for files associated with the SP itself, not with a particular protector
public static final long NULL_PROTECTOR_ID = 0L;
@@ -158,6 +163,8 @@
private static final byte[] PERSONALIZATION_SP_GK_AUTH = "sp-gk-authentication".getBytes();
private static final byte[] PERSONALIZATION_FBE_KEY = "fbe-key".getBytes();
private static final byte[] PERSONALIZATION_AUTHSECRET_KEY = "authsecret-hal".getBytes();
+ private static final byte[] PERSONALIZATION_AUTHSECRET_ENCRYPTION_KEY =
+ "vendor-authsecret-encryption-key".getBytes();
private static final byte[] PERSONALIZATION_SP_SPLIT = "sp-split".getBytes();
private static final byte[] PERSONALIZATION_PASSWORD_HASH = "pw-hash".getBytes();
private static final byte[] PERSONALIZATION_E0 = "e0-encryption".getBytes();
@@ -249,6 +256,10 @@
return deriveSubkey(PERSONALIZATION_PASSWORD_METRICS);
}
+ public byte[] deriveVendorAuthSecretEncryptionKey() {
+ return deriveSubkey(PERSONALIZATION_AUTHSECRET_ENCRYPTION_KEY);
+ }
+
/**
* Assigns escrow data to this synthetic password. This is a prerequisite to call
* {@link SyntheticPassword#recreateFromEscrow}.
@@ -513,7 +524,7 @@
WeaverConfig weaverConfig = null;
try {
weaverConfig = weaver.getConfig();
- } catch (RemoteException e) {
+ } catch (RemoteException | ServiceSpecificException e) {
Slog.e(TAG, "Failed to get weaver config", e);
}
if (weaverConfig == null || weaverConfig.slots <= 0) {
@@ -1737,4 +1748,25 @@
mListeners.finishBroadcast();
}
}
+
+ public void writeVendorAuthSecret(
+ @NonNull final byte[] vendorAuthSecret,
+ @NonNull final SyntheticPassword sp,
+ @UserIdInt final int userId) {
+ final byte[] encrypted =
+ SyntheticPasswordCrypto.encrypt(
+ sp.deriveVendorAuthSecretEncryptionKey(), new byte[0], vendorAuthSecret);
+ saveState(VENDOR_AUTH_SECRET_NAME, encrypted, NULL_PROTECTOR_ID, userId);
+ syncState(userId);
+ }
+
+ public @Nullable byte[] readVendorAuthSecret(
+ @NonNull final SyntheticPassword sp, @UserIdInt final int userId) {
+ final byte[] encrypted = loadState(VENDOR_AUTH_SECRET_NAME, NULL_PROTECTOR_ID, userId);
+ if (encrypted == null) {
+ return null;
+ }
+ return SyntheticPasswordCrypto.decrypt(
+ sp.deriveVendorAuthSecretEncryptionKey(), new byte[0], encrypted);
+ }
}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
index b437421..d981569 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
@@ -28,7 +28,10 @@
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.KeyguardManager;
import android.app.PendingIntent;
+import android.app.RemoteLockscreenValidationResult;
+import android.app.StartLockscreenValidationRequest;
import android.content.Context;
import android.os.Binder;
import android.os.RemoteException;
@@ -40,11 +43,14 @@
import android.security.keystore.recovery.RecoveryController;
import android.security.keystore.recovery.WrappedApplicationKey;
import android.util.ArrayMap;
+import android.util.FeatureFlagUtils;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.HexDump;
+import com.android.internal.widget.LockPatternUtils;
import com.android.security.SecureBox;
+import com.android.server.locksettings.LockSettingsService;
import com.android.server.locksettings.recoverablekeystore.certificate.CertParsingException;
import com.android.server.locksettings.recoverablekeystore.certificate.CertUtils;
import com.android.server.locksettings.recoverablekeystore.certificate.CertValidationException;
@@ -55,6 +61,8 @@
import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage;
+import com.android.server.locksettings.recoverablekeystore.storage.RemoteLockscreenValidationSessionStorage;
+import com.android.server.locksettings.recoverablekeystore.storage.RemoteLockscreenValidationSessionStorage.LockscreenVerificationSession;
import java.io.IOException;
import java.security.InvalidKeyException;
@@ -103,6 +111,9 @@
private final ApplicationKeyStorage mApplicationKeyStorage;
private final TestOnlyInsecureCertificateHelper mTestCertHelper;
private final CleanupManager mCleanupManager;
+ // only set if SETTINGS_ENABLE_LOCKSCREEN_TRANSFER_API is enabled.
+ @Nullable private final RemoteLockscreenValidationSessionStorage
+ mRemoteLockscreenValidationSessionStorage;
/**
* Returns a new or existing instance.
@@ -112,7 +123,18 @@
public static synchronized RecoverableKeyStoreManager
getInstance(Context context) {
if (mInstance == null) {
- RecoverableKeyStoreDb db = RecoverableKeyStoreDb.newInstance(context);
+ RecoverableKeyStoreDb db;
+ RemoteLockscreenValidationSessionStorage lockscreenCheckSessions;
+ if (FeatureFlagUtils.isEnabled(context,
+ FeatureFlagUtils.SETTINGS_ENABLE_LOCKSCREEN_TRANSFER_API)) {
+ // TODO(b/254335492): Remove flag check when feature is launched.
+ db = RecoverableKeyStoreDb.newInstance(context, 7);
+ lockscreenCheckSessions = new RemoteLockscreenValidationSessionStorage();
+ } else {
+ db = RecoverableKeyStoreDb.newInstance(context);
+ lockscreenCheckSessions = null;
+ }
+
PlatformKeyManager platformKeyManager;
ApplicationKeyStorage applicationKeyStorage;
try {
@@ -142,7 +164,8 @@
platformKeyManager,
applicationKeyStorage,
new TestOnlyInsecureCertificateHelper(),
- cleanupManager);
+ cleanupManager,
+ lockscreenCheckSessions);
}
return mInstance;
}
@@ -158,7 +181,8 @@
PlatformKeyManager platformKeyManager,
ApplicationKeyStorage applicationKeyStorage,
TestOnlyInsecureCertificateHelper testOnlyInsecureCertificateHelper,
- CleanupManager cleanupManager) {
+ CleanupManager cleanupManager,
+ RemoteLockscreenValidationSessionStorage remoteLockscreenValidationSessionStorage) {
mContext = context;
mDatabase = recoverableKeyStoreDb;
mRecoverySessionStorage = recoverySessionStorage;
@@ -177,6 +201,7 @@
Log.wtf(TAG, "AES keygen algorithm not available. AOSP must support this.", e);
throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
}
+ mRemoteLockscreenValidationSessionStorage = remoteLockscreenValidationSessionStorage;
}
/**
@@ -969,19 +994,50 @@
/**
* Starts a session to verify lock screen credentials provided by a remote device.
*/
- public void startRemoteLockscreenValidation() {
+ public StartLockscreenValidationRequest startRemoteLockscreenValidation(
+ LockSettingsService lockSettingService) {
+ if (mRemoteLockscreenValidationSessionStorage == null) {
+ throw new UnsupportedOperationException("Under development");
+ }
checkVerifyRemoteLockscreenPermission();
- // TODO(b/254335492): Create session in memory
- return;
+ int userId = UserHandle.getCallingUserId();
+ int savedCredentialType;
+ final long token = Binder.clearCallingIdentity();
+ try {
+ savedCredentialType = lockSettingService.getCredentialType(userId);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ int keyguardCredentailsType = lockPatternUtilsToKeyguardType(savedCredentialType);
+ LockscreenVerificationSession session =
+ mRemoteLockscreenValidationSessionStorage.startSession(userId);
+ PublicKey publicKey = session.getKeyPair().getPublic();
+ byte[] encodedPublicKey = SecureBox.encodePublicKey(publicKey);
+ int badGuesses = mDatabase.getBadRemoteGuessCounter(userId);
+ return new StartLockscreenValidationRequest.Builder()
+ .setLockscreenUiType(keyguardCredentailsType)
+ .setSourcePublicKey(new byte[]{})
+ .build();
}
/**
* Verifies encrypted credentials guess from a remote device.
*/
- public void validateRemoteLockscreen(@NonNull byte[] encryptedCredential) {
+ public RemoteLockscreenValidationResult validateRemoteLockscreen(
+ @NonNull byte[] encryptedCredential,
+ LockSettingsService lockSettingService) {
+ if (mRemoteLockscreenValidationSessionStorage == null) {
+ throw new UnsupportedOperationException("Under development");
+ }
checkVerifyRemoteLockscreenPermission();
- // TODO(b/254335492): Decrypt and verify credentials
- return;
+ int userId = UserHandle.getCallingUserId();
+ LockscreenVerificationSession session =
+ mRemoteLockscreenValidationSessionStorage.get(userId);
+ if (session == null) {
+ throw new IllegalStateException("There is no active lock screen check session");
+ }
+ // TODO(b/254335492): Call lockSettingService.verifyCredential
+ return new RemoteLockscreenValidationResult.Builder().build();
}
private void checkVerifyRemoteLockscreenPermission() {
@@ -995,6 +1051,21 @@
mCleanupManager.registerRecoveryAgent(userId, uid);
}
+ private int lockPatternUtilsToKeyguardType(int credentialsType) {
+ switch(credentialsType) {
+ case LockPatternUtils.CREDENTIAL_TYPE_NONE:
+ throw new IllegalStateException("Screen lock is not set");
+ case LockPatternUtils.CREDENTIAL_TYPE_PATTERN:
+ return KeyguardManager.PATTERN;
+ case LockPatternUtils.CREDENTIAL_TYPE_PIN:
+ return KeyguardManager.PIN;
+ case LockPatternUtils.CREDENTIAL_TYPE_PASSWORD:
+ return KeyguardManager.PASSWORD;
+ default:
+ throw new IllegalStateException("Screen lock is not set");
+ }
+ }
+
private void checkRecoverKeyStorePermission() {
mContext.enforceCallingOrSelfPermission(
Manifest.permission.RECOVER_KEYSTORE,
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index ffc309e..3c97aaf8 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -280,13 +280,13 @@
public void setRouteListingPreference(
@NonNull IMediaRouter2 router,
@Nullable RouteListingPreference routeListingPreference) {
- ComponentName inAppOnlyItemRoutingReceiver =
+ ComponentName linkedItemLandingComponent =
routeListingPreference != null
- ? routeListingPreference.getInAppOnlyItemRoutingReceiver()
+ ? routeListingPreference.getLinkedItemComponentName()
: null;
- if (inAppOnlyItemRoutingReceiver != null) {
+ if (linkedItemLandingComponent != null) {
MediaServerUtils.enforcePackageName(
- inAppOnlyItemRoutingReceiver.getPackageName(), Binder.getCallingUid());
+ linkedItemLandingComponent.getPackageName(), Binder.getCallingUid());
}
final long token = Binder.clearCallingIdentity();
diff --git a/services/core/java/com/android/server/media/MediaSessionStack.java b/services/core/java/com/android/server/media/MediaSessionStack.java
index f2a39b8..26e8fe6 100644
--- a/services/core/java/com/android/server/media/MediaSessionStack.java
+++ b/services/core/java/com/android/server/media/MediaSessionStack.java
@@ -21,11 +21,11 @@
import android.media.Session2Token;
import android.media.session.MediaSession;
import android.os.UserHandle;
+import android.text.TextUtils;
import android.util.Log;
+import android.util.Slog;
import android.util.SparseArray;
-import com.android.server.utils.EventLogger;
-
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
@@ -61,8 +61,6 @@
private final AudioPlayerStateMonitor mAudioPlayerStateMonitor;
private final OnMediaButtonSessionChangedListener mOnMediaButtonSessionChangedListener;
- private final EventLogger mEventLogger = new EventLogger(DUMP_EVENTS_MAX_COUNT, TAG);
-
/**
* The media button session which receives media key events.
* It could be null if the previous media button session is released.
@@ -86,9 +84,8 @@
* @param record The record to add.
*/
public void addSession(MediaSessionRecordImpl record) {
- mEventLogger.enqueue(EventLogger.StringEvent.from(
- "addSession() (to bottom of stack)",
- "record: %s",
+ Slog.i(TAG, TextUtils.formatSimple(
+ "addSession to bottom of stack | record: %s",
record
));
mSessions.add(record);
@@ -106,9 +103,8 @@
* @param record The record to remove.
*/
public void removeSession(MediaSessionRecordImpl record) {
- mEventLogger.enqueue(EventLogger.StringEvent.from(
- "removeSession()",
- "record: %s",
+ Slog.i(TAG, TextUtils.formatSimple(
+ "removeSession | record: %s",
record
));
mSessions.remove(record);
@@ -156,9 +152,8 @@
public void onPlaybackStateChanged(
MediaSessionRecordImpl record, boolean shouldUpdatePriority) {
if (shouldUpdatePriority) {
- mEventLogger.enqueue(EventLogger.StringEvent.from(
- "onPlaybackStateChanged() - Pushing session to top",
- "record: %s",
+ Slog.i(TAG, TextUtils.formatSimple(
+ "onPlaybackStateChanged - Pushing session to top | record: %s",
record
));
mSessions.remove(record);
@@ -365,8 +360,6 @@
for (MediaSessionRecordImpl record : mSessions) {
record.dump(pw, indent);
}
- pw.println(prefix + "Session stack events:");
- mEventLogger.dump(pw, indent);
}
/**
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 5ab4a42..ce921d6 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -24,6 +24,7 @@
import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
import static android.app.Notification.FLAG_INSISTENT;
import static android.app.Notification.FLAG_NO_CLEAR;
+import static android.app.Notification.FLAG_NO_DISMISS;
import static android.app.Notification.FLAG_ONGOING_EVENT;
import static android.app.Notification.FLAG_ONLY_ALERT_ONCE;
import static android.app.NotificationChannel.CONVERSATION_CHANNEL_ID_FORMAT;
@@ -521,8 +522,6 @@
private PackageManager mPackageManagerClient;
PackageManagerInternal mPackageManagerInternal;
private PermissionPolicyInternal mPermissionPolicyInternal;
-
- private PermissionManagerServiceInternal mPermissionInternal;
AudioManager mAudioManager;
AudioManagerInternal mAudioManagerInternal;
// Can be null for wear
@@ -578,6 +577,9 @@
private float mInCallNotificationVolume;
private Binder mCallNotificationToken = null;
+ private static final boolean ONGOING_DISMISSAL = SystemProperties.getBoolean(
+ "persist.sysui.notification.ongoing_dismissal", true);
+
// used as a mutex for access to all active notifications & listeners
final Object mNotificationLock = new Object();
@GuardedBy("mNotificationLock")
@@ -1203,10 +1205,13 @@
id = r.getSbn().getId();
}
}
- int mustNotHaveFlags = FLAG_ONGOING_EVENT;
- cancelNotification(callingUid, callingPid, pkg, tag, id, 0,
- mustNotHaveFlags,
- true, userId, REASON_CANCEL, nv.rank, nv.count,null);
+
+ int mustNotHaveFlags = ONGOING_DISMISSAL ? FLAG_NO_DISMISS : FLAG_ONGOING_EVENT;
+ cancelNotification(callingUid, callingPid, pkg, tag, id,
+ /* mustHaveFlags= */ 0,
+ /* mustNotHaveFlags= */ mustNotHaveFlags,
+ /* sendDelete= */ true,
+ userId, REASON_CANCEL, nv.rank, nv.count, /* listener= */ null);
nv.recycle();
}
@@ -2212,8 +2217,7 @@
TelephonyManager telephonyManager, ActivityManagerInternal ami,
MultiRateLimiter toastRateLimiter, PermissionHelper permissionHelper,
UsageStatsManagerInternal usageStatsManagerInternal,
- TelecomManager telecomManager, NotificationChannelLogger channelLogger,
- PermissionManagerServiceInternal permInternal) {
+ TelecomManager telecomManager, NotificationChannelLogger channelLogger) {
mHandler = handler;
Resources resources = getContext().getResources();
mMaxPackageEnqueueRate = Settings.Global.getFloat(getContext().getContentResolver(),
@@ -2231,7 +2235,6 @@
mPackageManagerClient = packageManagerClient;
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
mPermissionPolicyInternal = LocalServices.getService(PermissionPolicyInternal.class);
- mPermissionInternal = permInternal;
mUmInternal = LocalServices.getService(UserManagerInternal.class);
mUsageStatsManagerInternal = usageStatsManagerInternal;
mAppOps = appOps;
@@ -2543,8 +2546,7 @@
AppGlobals.getPermissionManager()),
LocalServices.getService(UsageStatsManagerInternal.class),
getContext().getSystemService(TelecomManager.class),
- new NotificationChannelLoggerImpl(),
- LocalServices.getService(PermissionManagerServiceInternal.class));
+ new NotificationChannelLoggerImpl());
publishBinderService(Context.NOTIFICATION_SERVICE, mService, /* allowIsolated= */ false,
DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_NORMAL);
@@ -6694,30 +6696,27 @@
(userId == UserHandle.USER_ALL) ? USER_SYSTEM : userId);
Notification.addFieldsFromContext(ai, notification);
- int canColorize = mPermissionInternal.checkPermission(
- android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, pkg, userId);
+ // Only notifications that can be non-dismissible can have the flag FLAG_NO_DISMISS
+ if (ONGOING_DISMISSAL) {
+ if (((notification.flags & FLAG_ONGOING_EVENT) > 0)
+ && canBeNonDismissible(ai, notification)) {
+ notification.flags |= FLAG_NO_DISMISS;
+ } else {
+ notification.flags &= ~FLAG_NO_DISMISS;
+ }
+ }
+
+ int canColorize = mPackageManagerClient.checkPermission(
+ android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, pkg);
if (canColorize == PERMISSION_GRANTED) {
notification.flags |= Notification.FLAG_CAN_COLORIZE;
} else {
notification.flags &= ~Notification.FLAG_CAN_COLORIZE;
}
- if (notification.extras.getBoolean(Notification.EXTRA_ALLOW_DURING_SETUP, false)) {
- int hasShowDuringSetupPerm = mPermissionInternal.checkPermission(
- android.Manifest.permission.NOTIFICATION_DURING_SETUP, pkg, userId);
- if (hasShowDuringSetupPerm != PERMISSION_GRANTED) {
- notification.extras.remove(Notification.EXTRA_ALLOW_DURING_SETUP);
- if (DBG) {
- Slog.w(TAG, "warning: pkg " + pkg + " attempting to show during setup"
- + " without holding perm "
- + Manifest.permission.NOTIFICATION_DURING_SETUP);
- }
- }
- }
-
if (notification.fullScreenIntent != null && ai.targetSdkVersion >= Build.VERSION_CODES.Q) {
- int fullscreenIntentPermission = mPermissionInternal.checkPermission(
- android.Manifest.permission.USE_FULL_SCREEN_INTENT, pkg, userId);
+ int fullscreenIntentPermission = mPackageManagerClient.checkPermission(
+ android.Manifest.permission.USE_FULL_SCREEN_INTENT, pkg);
if (fullscreenIntentPermission != PERMISSION_GRANTED) {
notification.fullScreenIntent = null;
Slog.w(TAG, "Package " + pkg +
@@ -6725,6 +6724,31 @@
}
}
+ // Ensure all actions are present
+ if (notification.actions != null) {
+ boolean hasNullActions = false;
+ int nActions = notification.actions.length;
+ for (int i = 0; i < nActions; i++) {
+ if (notification.actions[i] == null) {
+ hasNullActions = true;
+ break;
+ }
+ }
+ if (hasNullActions) {
+ ArrayList<Notification.Action> nonNullActions = new ArrayList<>();
+ for (int i = 0; i < nActions; i++) {
+ if (notification.actions[i] != null) {
+ nonNullActions.add(notification.actions[i]);
+ }
+ }
+ if (nonNullActions.size() != 0) {
+ notification.actions = nonNullActions.toArray(new Notification.Action[0]);
+ } else {
+ notification.actions = null;
+ }
+ }
+ }
+
// Ensure CallStyle has all the correct actions
if (notification.isStyle(Notification.CallStyle.class)) {
Notification.Builder builder =
@@ -6737,7 +6761,7 @@
// Ensure MediaStyle has correct permissions for remote device extras
if (notification.isStyle(Notification.MediaStyle.class)) {
- int hasMediaContentControlPermission = mPermissionInternal.checkPermission(
+ int hasMediaContentControlPermission = mPackageManager.checkPermission(
android.Manifest.permission.MEDIA_CONTENT_CONTROL, pkg, userId);
if (hasMediaContentControlPermission != PERMISSION_GRANTED) {
notification.extras.remove(Notification.EXTRA_MEDIA_REMOTE_DEVICE);
@@ -6752,7 +6776,7 @@
// Ensure only allowed packages have a substitute app name
if (notification.extras.containsKey(Notification.EXTRA_SUBSTITUTE_APP_NAME)) {
- int hasSubstituteAppNamePermission = mPermissionInternal.checkPermission(
+ int hasSubstituteAppNamePermission = mPackageManager.checkPermission(
permission.SUBSTITUTE_NOTIFICATION_APP_NAME, pkg, userId);
if (hasSubstituteAppNamePermission != PERMISSION_GRANTED) {
notification.extras.remove(Notification.EXTRA_SUBSTITUTE_APP_NAME);
@@ -6768,6 +6792,23 @@
checkRemoteViews(pkg, tag, id, notification);
}
+ /**
+ * Whether a notification can be non-dismissible.
+ * A notification should be dismissible, unless it's exempted for some reason.
+ */
+ private boolean canBeNonDismissible(ApplicationInfo ai, Notification notification) {
+ // Check if the app is on the system partition, or an update to an app on the system
+ // partition.
+ boolean isSystemAppExempt = (ai.flags
+ & (ApplicationInfo.FLAG_UPDATED_SYSTEM_APP | ApplicationInfo.FLAG_SYSTEM)) > 0;
+ return isSystemAppExempt || notification.isMediaNotification() || isEnterpriseExempted();
+ }
+
+ // TODO: b/266237746 Enterprise app exemptions
+ private boolean isEnterpriseExempted() {
+ return false;
+ }
+
private void checkRemoteViews(String pkg, String tag, int id, Notification notification) {
if (removeRemoteView(pkg, tag, id, notification.contentView)) {
notification.contentView = null;
@@ -8356,9 +8397,15 @@
}
private boolean isExemptFromRateLimiting(String pkg, int userId) {
- return mPermissionInternal.checkPermission(
- android.Manifest.permission.UNLIMITED_TOASTS, pkg, userId)
- == PackageManager.PERMISSION_GRANTED;
+ boolean isExemptFromRateLimiting = false;
+ try {
+ isExemptFromRateLimiting = mPackageManager.checkPermission(
+ android.Manifest.permission.UNLIMITED_TOASTS, pkg, userId)
+ == PackageManager.PERMISSION_GRANTED;
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to connect with package manager");
+ }
+ return isExemptFromRateLimiting;
}
/** Reports rate limiting toasts compat change (used when the toast was blocked). */
@@ -10005,9 +10052,13 @@
boolean canUseManagedServices(String pkg, Integer userId, String requiredPermission) {
boolean canUseManagedServices = true;
if (requiredPermission != null) {
- if (mPermissionInternal.checkPermission(requiredPermission, pkg, userId)
- != PackageManager.PERMISSION_GRANTED) {
- canUseManagedServices = false;
+ try {
+ if (mPackageManager.checkPermission(requiredPermission, pkg, userId)
+ != PackageManager.PERMISSION_GRANTED) {
+ canUseManagedServices = false;
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "can't talk to pm", e);
}
}
diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
index 2fdc4cd..e698c4b 100644
--- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
+++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
@@ -16,6 +16,7 @@
package com.android.server.os;
+import android.Manifest;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.app.ActivityManager;
@@ -25,6 +26,7 @@
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.os.Binder;
+import android.os.BugreportManager.BugreportCallback;
import android.os.BugreportParams;
import android.os.IDumpstate;
import android.os.IDumpstateListener;
@@ -35,10 +37,13 @@
import android.os.UserHandle;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.Pair;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.SystemConfig;
import java.io.FileDescriptor;
@@ -60,18 +65,105 @@
private final AppOpsManager mAppOps;
private final TelephonyManager mTelephonyManager;
private final ArraySet<String> mBugreportWhitelistedPackages;
+ private final BugreportFileManager mBugreportFileManager;
@GuardedBy("mLock")
private OptionalInt mPreDumpedDataUid = OptionalInt.empty();
- BugreportManagerServiceImpl(Context context) {
- mContext = context;
- mAppOps = context.getSystemService(AppOpsManager.class);
- mTelephonyManager = context.getSystemService(TelephonyManager.class);
- mBugreportWhitelistedPackages =
- SystemConfig.getInstance().getBugreportWhitelistedPackages();
+ /** Helper class for associating previously generated bugreports with their callers. */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ static class BugreportFileManager {
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private final ArrayMap<Pair<Integer, String>, ArraySet<String>> mBugreportFiles;
+
+ BugreportFileManager() {
+ mBugreportFiles = new ArrayMap<>();
+ }
+
+ /**
+ * Checks that a given file was generated on behalf of the given caller. If the file was
+ * generated on behalf of the caller, it is removed from the bugreport mapping so that it
+ * may not be retrieved again. If the file was not generated on behalf of the caller, an
+ * {@link IllegalArgumentException} is thrown.
+ *
+ * @param callingInfo a (uid, package name) pair identifying the caller
+ * @param bugreportFile the file name which was previously given to the caller in the
+ * {@link BugreportCallback#onFinished(String)} callback.
+ *
+ * @throws IllegalArgumentException if {@code bugreportFile} is not associated with
+ * {@code callingInfo}.
+ */
+ void ensureCallerPreviouslyGeneratedFile(
+ Pair<Integer, String> callingInfo, String bugreportFile) {
+ synchronized (mLock) {
+ ArraySet<String> bugreportFilesForCaller = mBugreportFiles.get(callingInfo);
+ if (bugreportFilesForCaller != null
+ && bugreportFilesForCaller.contains(bugreportFile)) {
+ bugreportFilesForCaller.remove(bugreportFile);
+ if (bugreportFilesForCaller.isEmpty()) {
+ mBugreportFiles.remove(callingInfo);
+ }
+ } else {
+ throw new IllegalArgumentException(
+ "File " + bugreportFile + " was not generated"
+ + " on behalf of calling package " + callingInfo.second);
+ }
+ }
+ }
+
+ /**
+ * Associates a bugreport file with a caller, which is identified as a
+ * (uid, package name) pair.
+ */
+ void addBugreportFileForCaller(Pair<Integer, String> caller, String bugreportFile) {
+ synchronized (mLock) {
+ if (!mBugreportFiles.containsKey(caller)) {
+ mBugreportFiles.put(caller, new ArraySet<>());
+ }
+ ArraySet<String> bugreportFilesForCaller = mBugreportFiles.get(caller);
+ bugreportFilesForCaller.add(bugreportFile);
+ }
+ }
}
+ static class Injector {
+ Context mContext;
+ ArraySet<String> mAllowlistedPackages;
+
+ Injector(Context context, ArraySet<String> allowlistedPackages) {
+ mContext = context;
+ mAllowlistedPackages = allowlistedPackages;
+ }
+
+ Context getContext() {
+ return mContext;
+ }
+
+ ArraySet<String> getAllowlistedPackages() {
+ return mAllowlistedPackages;
+ }
+
+ }
+
+ BugreportManagerServiceImpl(Context context) {
+ this(new Injector(context, SystemConfig.getInstance().getBugreportWhitelistedPackages()));
+
+ }
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ BugreportManagerServiceImpl(Injector injector) {
+ mContext = injector.getContext();
+ mAppOps = mContext.getSystemService(AppOpsManager.class);
+ mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
+ mBugreportFileManager = new BugreportFileManager();
+ mBugreportWhitelistedPackages =
+ injector.getAllowlistedPackages();
+ }
+
+
@Override
@RequiresPermission(android.Manifest.permission.DUMP)
public void preDumpUiData(String callingPackage) {
@@ -135,6 +227,50 @@
}
}
+ @Override
+ @RequiresPermission(Manifest.permission.DUMP)
+ public void retrieveBugreport(int callingUidUnused, String callingPackage,
+ FileDescriptor bugreportFd, String bugreportFile, IDumpstateListener listener) {
+ int callingUid = Binder.getCallingUid();
+ enforcePermission(callingPackage, callingUid, false);
+
+ try {
+ mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
+ new Pair<>(callingUid, callingPackage), bugreportFile);
+ } catch (IllegalArgumentException e) {
+ Slog.e(TAG, e.getMessage());
+ reportError(listener, IDumpstateListener.BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE);
+ return;
+ }
+
+ synchronized (mLock) {
+ if (isDumpstateBinderServiceRunningLocked()) {
+ Slog.w(TAG, "'dumpstate' is already running. Cannot retrieve a bugreport"
+ + " while another one is currently in progress.");
+ reportError(listener,
+ IDumpstateListener.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS);
+ return;
+ }
+
+ IDumpstate ds = startAndGetDumpstateBinderServiceLocked();
+ if (ds == null) {
+ Slog.w(TAG, "Unable to get bugreport service");
+ reportError(listener, IDumpstateListener.BUGREPORT_ERROR_RUNTIME_ERROR);
+ return;
+ }
+
+ // Wrap the listener so we can intercept binder events directly.
+ IDumpstateListener myListener = new DumpstateListener(listener, ds,
+ new Pair<>(callingUid, callingPackage));
+ try {
+ ds.retrieveBugreport(callingUid, callingPackage, bugreportFd,
+ bugreportFile, myListener);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "RemoteException in retrieveBugreport", e);
+ }
+ }
+ }
+
private void validateBugreportMode(@BugreportParams.BugreportMode int mode) {
if (mode != BugreportParams.BUGREPORT_MODE_FULL
&& mode != BugreportParams.BUGREPORT_MODE_INTERACTIVE
@@ -148,7 +284,9 @@
}
private void validateBugreportFlags(int flags) {
- flags = clearBugreportFlag(flags, BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA);
+ flags = clearBugreportFlag(flags,
+ BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA
+ | BugreportParams.BUGREPORT_FLAG_DEFER_CONSENT);
if (flags != 0) {
Slog.w(TAG, "Unknown bugreport flags: " + flags);
throw new IllegalArgumentException("Unknown bugreport flags: " + flags);
@@ -298,6 +436,9 @@
}
}
+ boolean isConsentDeferred =
+ (bugreportFlags & BugreportParams.BUGREPORT_FLAG_DEFER_CONSENT) != 0;
+
IDumpstate ds = startAndGetDumpstateBinderServiceLocked();
if (ds == null) {
Slog.w(TAG, "Unable to get bugreport service");
@@ -306,7 +447,8 @@
}
// Wrap the listener so we can intercept binder events directly.
- IDumpstateListener myListener = new DumpstateListener(listener, ds);
+ IDumpstateListener myListener = new DumpstateListener(listener, ds,
+ isConsentDeferred ? new Pair<>(callingUid, callingPackage) : null);
try {
ds.startBugreport(callingUid, callingPackage, bugreportFd, screenshotFd, bugreportMode,
bugreportFlags, myListener, isScreenshotRequested);
@@ -405,10 +547,13 @@
private final IDumpstateListener mListener;
private final IDumpstate mDs;
private boolean mDone = false;
+ private final Pair<Integer, String> mCaller;
- DumpstateListener(IDumpstateListener listener, IDumpstate ds) {
+ DumpstateListener(IDumpstateListener listener, IDumpstate ds,
+ @Nullable Pair<Integer, String> caller) {
mListener = listener;
mDs = ds;
+ mCaller = caller;
try {
mDs.asBinder().linkToDeath(this, 0);
} catch (RemoteException e) {
@@ -430,11 +575,14 @@
}
@Override
- public void onFinished() throws RemoteException {
+ public void onFinished(String bugreportFile) throws RemoteException {
synchronized (mLock) {
mDone = true;
}
- mListener.onFinished();
+ if (mCaller != null) {
+ mBugreportFileManager.addBugreportFileForCaller(mCaller, bugreportFile);
+ }
+ mListener.onFinished(bugreportFile);
}
@Override
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
index 69436da..8d40adf 100644
--- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
@@ -27,12 +27,15 @@
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ParceledListSlice;
+import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
+import android.os.SystemProperties;
import android.os.UserHandle;
+import android.text.TextUtils;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.Slog;
@@ -51,6 +54,7 @@
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
@@ -122,7 +126,18 @@
@Override
public ParceledListSlice<PackageInfo> getBackgroundInstalledPackages(
@PackageManager.PackageInfoFlagsBits long flags, int userId) {
- return mService.getBackgroundInstalledPackages(flags, userId);
+ if (!Build.IS_DEBUGGABLE) {
+ return mService.getBackgroundInstalledPackages(flags, userId);
+ }
+ // The debug.transparency.bg-install-apps (only works for debuggable builds)
+ // is used to set mock list of background installed apps for testing.
+ // The list of apps' names is delimited by ",".
+ String propertyString = SystemProperties.get("debug.transparency.bg-install-apps");
+ if (TextUtils.isEmpty(propertyString)) {
+ return mService.getBackgroundInstalledPackages(flags, userId);
+ } else {
+ return mService.getMockBackgroundInstalledPackages(propertyString);
+ }
}
}
@@ -145,6 +160,27 @@
return new ParceledListSlice<>(packages);
}
+ /**
+ * Mock a list of background installed packages based on the property string.
+ */
+ @NonNull
+ ParceledListSlice<PackageInfo> getMockBackgroundInstalledPackages(
+ @NonNull String propertyString) {
+ String[] mockPackageNames = propertyString.split(",");
+ List<PackageInfo> mockPackages = new ArrayList<>();
+ for (String name : mockPackageNames) {
+ try {
+ PackageInfo packageInfo = mPackageManager.getPackageInfo(name,
+ PackageManager.PackageInfoFlags.of(PackageManager.MATCH_ALL));
+ mockPackages.add(packageInfo);
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.w(TAG, "Package's PackageInfo not found " + name);
+ continue;
+ }
+ }
+ return new ParceledListSlice<>(mockPackages);
+ }
+
private static class EventHandler extends Handler {
private final BackgroundInstallControlService mService;
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 094b182..58a609e 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -1856,8 +1856,7 @@
// Figure out which lib versions the caller can see
LongSparseLongArray versionsCallerCanSee = null;
final int callingAppId = UserHandle.getAppId(callingUid);
- if (callingAppId != Process.SYSTEM_UID && callingAppId != Process.SHELL_UID
- && callingAppId != Process.ROOT_UID) {
+ if (!PackageManagerServiceUtils.isSystemOrRootOrShell(callingAppId)) {
versionsCallerCanSee = new LongSparseLongArray();
String libName = versionedLib.valueAt(0).getName();
String[] uidPackages = getPackagesForUidInternal(callingUid, callingUid);
@@ -2034,8 +2033,7 @@
if ((flags & PackageManager.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES) != 0) {
// System/shell/root get to see all static libs
final int appId = UserHandle.getAppId(uid);
- if (appId == Process.SYSTEM_UID || appId == Process.SHELL_UID
- || appId == Process.ROOT_UID) {
+ if (PackageManagerServiceUtils.isSystemOrRootOrShell(appId)) {
return false;
}
// Installer gets to see all static libs.
@@ -2091,8 +2089,7 @@
if ((flags & PackageManager.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES) != 0) {
// System/shell/root get to see all SDK libs.
final int appId = UserHandle.getAppId(uid);
- if (appId == Process.SYSTEM_UID || appId == Process.SHELL_UID
- || appId == Process.ROOT_UID) {
+ if (PackageManagerServiceUtils.isSystemOrRootOrShell(appId)) {
return false;
}
// Installer gets to see all SDK libs.
@@ -2152,7 +2149,7 @@
if (!requirePermissionWhenSameUser && userId == callingUserId) {
return true;
}
- if (callingUid == Process.SYSTEM_UID || callingUid == Process.ROOT_UID) {
+ if (PackageManagerServiceUtils.isSystemOrRoot(callingUid)) {
return true;
}
if (requireFullPermission) {
@@ -3813,8 +3810,7 @@
public boolean canRequestPackageInstalls(@NonNull String packageName, int callingUid,
int userId, boolean throwIfPermNotDeclared) {
int uid = getPackageUidInternal(packageName, 0, userId, callingUid);
- if (callingUid != uid && callingUid != Process.ROOT_UID
- && callingUid != Process.SYSTEM_UID) {
+ if (callingUid != uid && !PackageManagerServiceUtils.isSystemOrRoot(callingUid)) {
throw new SecurityException(
"Caller uid " + callingUid + " does not own package " + packageName);
}
@@ -5540,8 +5536,8 @@
enforceCrossUserPermission(callingUid, userId, true /*requireFullPermission*/,
true /*checkShell*/, "getHarmfulAppInfo");
- if (callingAppId != Process.SYSTEM_UID && callingAppId != Process.ROOT_UID &&
- checkUidPermission(SET_HARMFUL_APP_WARNINGS, callingUid) != PERMISSION_GRANTED) {
+ if (!PackageManagerServiceUtils.isSystemOrRoot(callingAppId)
+ && checkUidPermission(SET_HARMFUL_APP_WARNINGS, callingUid) != PERMISSION_GRANTED) {
throw new SecurityException("Caller must have the "
+ SET_HARMFUL_APP_WARNINGS + " permission.");
}
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index e0de294..3a21c34 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -262,7 +262,7 @@
final boolean killApp = (deleteFlags & PackageManager.DELETE_DONT_KILL_APP) == 0;
info.sendPackageRemovedBroadcasts(killApp, removedBySystem);
info.sendSystemPackageUpdatedBroadcasts();
- PackageMetrics.onUninstallSucceeded(info, deleteFlags, userId);
+ PackageMetrics.onUninstallSucceeded(info, deleteFlags, removeUser);
}
// Force a gc to clear up things.
@@ -840,7 +840,7 @@
private boolean isCallerAllowedToSilentlyUninstall(@NonNull Computer snapshot, int callingUid,
String pkgName, int userId) {
- if (callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID
+ if (PackageManagerServiceUtils.isRootOrShell(callingUid)
|| UserHandle.getAppId(callingUid) == Process.SYSTEM_UID) {
return true;
}
diff --git a/services/core/java/com/android/server/pm/GentleUpdateHelper.java b/services/core/java/com/android/server/pm/GentleUpdateHelper.java
index ae80bab..babcd33 100644
--- a/services/core/java/com/android/server/pm/GentleUpdateHelper.java
+++ b/services/core/java/com/android/server/pm/GentleUpdateHelper.java
@@ -145,7 +145,7 @@
mHandler.post(() -> {
var pendingCheck = new PendingInstallConstraintsCheck(
packageNames, constraints, resultFuture, timeoutMillis);
- var deviceIdleFuture = constraints.isRequireDeviceIdle()
+ var deviceIdleFuture = constraints.isDeviceIdleRequired()
? checkDeviceIdle() : CompletableFuture.completedFuture(false);
deviceIdleFuture.thenAccept(isIdle -> {
Preconditions.checkState(mHandler.getLooper().isCurrentThread());
@@ -217,14 +217,14 @@
@WorkerThread
private boolean areConstraintsSatisfied(List<String> packageNames,
InstallConstraints constraints, boolean isIdle) {
- return (!constraints.isRequireDeviceIdle() || isIdle)
- && (!constraints.isRequireAppNotForeground()
+ return (!constraints.isDeviceIdleRequired() || isIdle)
+ && (!constraints.isAppNotForegroundRequired()
|| !mAppStateHelper.hasForegroundApp(packageNames))
- && (!constraints.isRequireAppNotInteracting()
+ && (!constraints.isAppNotInteractingRequired()
|| !mAppStateHelper.hasInteractingApp(packageNames))
- && (!constraints.isRequireAppNotTopVisible()
+ && (!constraints.isAppNotTopVisibleRequired()
|| !mAppStateHelper.hasTopVisibleApp(packageNames))
- && (!constraints.isRequireNotInCall()
+ && (!constraints.isNotInCallRequired()
|| !mAppStateHelper.isInCall());
}
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index bd6bd75..9384300 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -70,6 +70,7 @@
import static com.android.server.pm.PackageManagerService.SCAN_AS_OEM;
import static com.android.server.pm.PackageManagerService.SCAN_AS_PRIVILEGED;
import static com.android.server.pm.PackageManagerService.SCAN_AS_PRODUCT;
+import static com.android.server.pm.PackageManagerService.SCAN_AS_STOPPED_SYSTEM_APP;
import static com.android.server.pm.PackageManagerService.SCAN_AS_SYSTEM;
import static com.android.server.pm.PackageManagerService.SCAN_AS_SYSTEM_EXT;
import static com.android.server.pm.PackageManagerService.SCAN_AS_VENDOR;
@@ -100,6 +101,7 @@
import android.apex.ApexInfo;
import android.app.AppOpsManager;
import android.app.ApplicationPackageManager;
+import android.app.BroadcastOptions;
import android.app.backup.IBackupManager;
import android.content.ContentResolver;
import android.content.Context;
@@ -686,7 +688,10 @@
fillIn.putExtra(PackageInstaller.EXTRA_STATUS,
PackageManager.installStatusToPublicStatus(returnCode));
try {
- target.sendIntent(context, 0, fillIn, null, null);
+ final BroadcastOptions options = BroadcastOptions.makeBasic();
+ options.setPendingIntentBackgroundActivityLaunchAllowed(false);
+ target.sendIntent(context, 0, fillIn, null /* onFinished*/, null /* handler */,
+ null /* requiredPermission */, options.toBundle());
} catch (IntentSender.SendIntentException ignored) {
}
}
@@ -2565,15 +2570,19 @@
if (disabledPs != null) {
dataOwnerPkg = disabledPs.getPkg();
}
- try {
- PackageManagerServiceUtils.checkDowngrade(dataOwnerPkg, pkgLite);
- } catch (PackageManagerException e) {
- String errorMsg = "System app: " + packageName + " cannot be downgraded to"
- + " older than its preloaded version on the system image. "
- + e.getMessage();
- Slog.w(TAG, errorMsg);
- return Pair.create(
- PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE, errorMsg);
+ if (!Build.IS_DEBUGGABLE && !dataOwnerPkg.isDebuggable()) {
+ // Only restrict non-debuggable builds and non-debuggable version of the app
+ try {
+ PackageManagerServiceUtils.checkDowngrade(dataOwnerPkg, pkgLite);
+ } catch (PackageManagerException e) {
+ String errorMsg =
+ "System app: " + packageName + " cannot be downgraded to"
+ + " older than its preloaded version on the system"
+ + " image. " + e.getMessage();
+ Slog.w(TAG, errorMsg);
+ return Pair.create(
+ PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE, errorMsg);
+ }
}
}
}
@@ -4237,6 +4246,18 @@
}
}
+ // A new application appeared on /system, and we are seeing it for the first time.
+ // Its also not updated as we don't have a copy of it on /data. So, scan it in a
+ // STOPPED state. Ignore if it's an APEX package since stopped state does not affect them.
+ final boolean isApexPkg = (scanFlags & SCAN_AS_APEX) != 0;
+ if (mPm.mShouldStopSystemPackagesByDefault && scanSystemPartition
+ && !pkgAlreadyExists && !isApexPkg) {
+ String packageName = parsedPackage.getPackageName();
+ if (!mPm.mInitialNonStoppedSystemPackages.contains(packageName)) {
+ scanFlags |= SCAN_AS_STOPPED_SYSTEM_APP;
+ }
+ }
+
final ScanResult scanResult = scanPackageNewLI(parsedPackage, parseFlags,
scanFlags | SCAN_UPDATE_SIGNATURE, 0 /* currentTime */, user, null);
return new Pair<>(scanResult, shouldHideSystemApp);
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index c5bcddc..6b9be25 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -1116,12 +1116,16 @@
// Flag for bubble
ActivityOptions options = ActivityOptions.fromBundle(startActivityOptions);
- if (options != null && options.isApplyActivityFlagsForBubbles()) {
- // Flag for bubble to make behaviour match documentLaunchMode=always.
- intents[0].addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
- intents[0].addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ if (options != null) {
+ if (options.isApplyActivityFlagsForBubbles()) {
+ // Flag for bubble to make behaviour match documentLaunchMode=always.
+ intents[0].addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
+ intents[0].addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ }
+ if (options.isApplyMultipleTaskFlagForShortcut()) {
+ intents[0].addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ }
}
-
intents[0].addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intents[0].setSourceBounds(sourceBounds);
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 617561a..8823714 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -28,6 +28,7 @@
import android.app.ActivityManager;
import android.app.AppGlobals;
import android.app.AppOpsManager;
+import android.app.BroadcastOptions;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PackageDeleteObserver;
@@ -663,7 +664,7 @@
&& params.installerPackageName.length() < SessionParams.MAX_PACKAGE_NAME_LENGTH)
? params.installerPackageName : installerPackageName;
- if ((callingUid == Process.SHELL_UID) || (callingUid == Process.ROOT_UID)
+ if (PackageManagerServiceUtils.isRootOrShell(callingUid)
|| PackageInstallerSession.isSystemDataLoaderInstallation(params)) {
params.installFlags |= PackageManager.INSTALL_FROM_ADB;
// adb installs can override the installingPackageName, but not the
@@ -706,7 +707,7 @@
}
}
- if (Build.IS_DEBUGGABLE || isCalledBySystemOrShell(callingUid)) {
+ if (Build.IS_DEBUGGABLE || PackageManagerServiceUtils.isSystemOrRoot(callingUid)) {
params.installFlags |= PackageManager.INSTALL_ALLOW_DOWNGRADE;
} else {
params.installFlags &= ~PackageManager.INSTALL_ALLOW_DOWNGRADE;
@@ -748,7 +749,8 @@
if (params.isMultiPackage) {
throw new IllegalArgumentException("A multi-session can't be set as APEX.");
}
- if (isCalledBySystemOrShell(callingUid) || mBypassNextAllowedApexUpdateCheck) {
+ if (PackageManagerServiceUtils.isSystemOrRootOrShell(callingUid)
+ || mBypassNextAllowedApexUpdateCheck) {
params.installFlags |= PackageManager.INSTALL_DISABLE_ALLOWED_APEX_UPDATE_CHECK;
} else {
// Only specific APEX updates (installed through ADB, or for CTS tests) can disable
@@ -758,20 +760,20 @@
}
if ((params.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0
- && !isCalledBySystemOrShell(callingUid)
+ && !PackageManagerServiceUtils.isSystemOrRootOrShell(callingUid)
&& (snapshot.getFlagsForUid(callingUid) & ApplicationInfo.FLAG_SYSTEM)
== 0) {
throw new SecurityException(
"Only system apps could use the PackageManager.INSTALL_INSTANT_APP flag.");
}
- if (params.isStaged && !isCalledBySystemOrShell(callingUid)) {
+ if (params.isStaged && !PackageManagerServiceUtils.isSystemOrRootOrShell(callingUid)) {
if (!mBypassNextStagedInstallerCheck
&& !isStagedInstallerAllowed(requestedInstallerPackageName)) {
throw new SecurityException("Installer not allowed to commit staged install");
}
}
- if (isApex && !isCalledBySystemOrShell(callingUid)) {
+ if (isApex && !PackageManagerServiceUtils.isSystemOrRootOrShell(callingUid)) {
if (!mBypassNextStagedInstallerCheck
&& !isStagedInstallerAllowed(requestedInstallerPackageName)) {
throw new SecurityException(
@@ -874,7 +876,7 @@
// reset the force queryable param if it's not called by an approved caller.
if (params.forceQueryableOverride) {
- if (callingUid != Process.SHELL_UID && callingUid != Process.ROOT_UID) {
+ if (!PackageManagerServiceUtils.isRootOrShell(callingUid)) {
params.forceQueryableOverride = false;
}
}
@@ -916,11 +918,6 @@
return sessionId;
}
- private boolean isCalledBySystemOrShell(int callingUid) {
- return callingUid == Process.SYSTEM_UID || callingUid == Process.ROOT_UID
- || callingUid == Process.SHELL_UID;
- }
-
private boolean isStagedInstallerAllowed(String installerName) {
return SystemConfig.getInstance().getWhitelistedStagedInstallers().contains(installerName);
}
@@ -1185,7 +1182,7 @@
final Computer snapshot = mPm.snapshotComputer();
final int callingUid = Binder.getCallingUid();
snapshot.enforceCrossUserPermission(callingUid, userId, true, true, "uninstall");
- if ((callingUid != Process.SHELL_UID) && (callingUid != Process.ROOT_UID)) {
+ if (!PackageManagerServiceUtils.isRootOrShell(callingUid)) {
mAppOps.checkPackage(callingUid, callerPackageName);
}
@@ -1239,7 +1236,7 @@
mContext.enforceCallingOrSelfPermission(Manifest.permission.DELETE_PACKAGES, null);
final Computer snapshot = mPm.snapshotComputer();
snapshot.enforceCrossUserPermission(callingUid, userId, true, true, "uninstall");
- if ((callingUid != Process.SHELL_UID) && (callingUid != Process.ROOT_UID)) {
+ if (!PackageManagerServiceUtils.isRootOrShell(callingUid)) {
mAppOps.checkPackage(callingUid, callerPackageName);
}
@@ -1276,7 +1273,7 @@
final var snapshot = mPm.snapshotComputer();
final int callingUid = Binder.getCallingUid();
- if (!isCalledBySystemOrShell(callingUid)) {
+ if (!PackageManagerServiceUtils.isSystemOrRootOrShell(callingUid)) {
for (var packageName : packageNames) {
var ps = snapshot.getPackageStateInternal(packageName);
if (ps == null || !TextUtils.equals(
@@ -1318,7 +1315,10 @@
intent.putExtra(PackageInstaller.EXTRA_INSTALL_CONSTRAINTS, constraints);
intent.putExtra(PackageInstaller.EXTRA_INSTALL_CONSTRAINTS_RESULT, result);
try {
- callback.sendIntent(mContext, 0, intent, null, null);
+ final BroadcastOptions options = BroadcastOptions.makeBasic();
+ options.setPendingIntentBackgroundActivityLaunchAllowed(false);
+ callback.sendIntent(mContext, 0, intent, null /* onFinished*/,
+ null /* handler */, null /* requiredPermission */, options.toBundle());
} catch (SendIntentException ignore) {
}
});
@@ -1363,7 +1363,7 @@
@Override
public void bypassNextStagedInstallerCheck(boolean value) {
- if (!isCalledBySystemOrShell(Binder.getCallingUid())) {
+ if (!PackageManagerServiceUtils.isSystemOrRootOrShell(Binder.getCallingUid())) {
throw new SecurityException("Caller not allowed to bypass staged installer check");
}
mBypassNextStagedInstallerCheck = value;
@@ -1371,7 +1371,7 @@
@Override
public void bypassNextAllowedApexUpdateCheck(boolean value) {
- if (!isCalledBySystemOrShell(Binder.getCallingUid())) {
+ if (!PackageManagerServiceUtils.isSystemOrRootOrShell(Binder.getCallingUid())) {
throw new SecurityException("Caller not allowed to bypass allowed apex update check");
}
mBypassNextAllowedApexUpdateCheck = value;
@@ -1379,7 +1379,7 @@
@Override
public void disableVerificationForUid(int uid) {
- if (!isCalledBySystemOrShell(Binder.getCallingUid())) {
+ if (!PackageManagerServiceUtils.isSystemOrRootOrShell(Binder.getCallingUid())) {
throw new SecurityException("Operation not allowed for caller");
}
mDisableVerificationForUid = uid;
@@ -1390,7 +1390,7 @@
*/
@Override
public void setAllowUnlimitedSilentUpdates(@Nullable String installerPackageName) {
- if (!isCalledBySystemOrShell(Binder.getCallingUid())) {
+ if (!PackageManagerServiceUtils.isSystemOrRootOrShell(Binder.getCallingUid())) {
throw new SecurityException("Caller not allowed to unlimite silent updates");
}
mSilentUpdatePolicy.setAllowUnlimitedSilentUpdates(installerPackageName);
@@ -1401,7 +1401,7 @@
*/
@Override
public void setSilentUpdatesThrottleTime(long throttleTimeInSeconds) {
- if (!isCalledBySystemOrShell(Binder.getCallingUid())) {
+ if (!PackageManagerServiceUtils.isSystemOrRootOrShell(Binder.getCallingUid())) {
throw new SecurityException("Caller not allowed to set silent updates throttle time");
}
mSilentUpdatePolicy.setSilentUpdatesThrottleTime(throttleTimeInSeconds);
@@ -1481,7 +1481,10 @@
PackageInstaller.STATUS_PENDING_USER_ACTION);
fillIn.putExtra(Intent.EXTRA_INTENT, intent);
try {
- mTarget.sendIntent(mContext, 0, fillIn, null, null);
+ final BroadcastOptions options = BroadcastOptions.makeBasic();
+ options.setPendingIntentBackgroundActivityLaunchAllowed(false);
+ mTarget.sendIntent(mContext, 0, fillIn, null /* onFinished*/,
+ null /* handler */, null /* requiredPermission */, options.toBundle());
} catch (SendIntentException ignored) {
}
}
@@ -1506,7 +1509,10 @@
PackageManager.deleteStatusToString(returnCode, msg));
fillIn.putExtra(PackageInstaller.EXTRA_LEGACY_STATUS, returnCode);
try {
- mTarget.sendIntent(mContext, 0, fillIn, null, null);
+ final BroadcastOptions options = BroadcastOptions.makeBasic();
+ options.setPendingIntentBackgroundActivityLaunchAllowed(false);
+ mTarget.sendIntent(mContext, 0, fillIn, null /* onFinished*/,
+ null /* handler */, null /* requiredPermission */, options.toBundle());
} catch (SendIntentException ignored) {
}
}
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 47e18f1..55dcaf6 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -59,6 +59,7 @@
import android.annotation.Nullable;
import android.annotation.WorkerThread;
import android.app.AppOpsManager;
+import android.app.BroadcastOptions;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.admin.DevicePolicyEventLogger;
@@ -3995,7 +3996,14 @@
return;
}
}
- r.run();
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ // This will call into StagingManager which might trigger external callbacks
+ r.run();
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
@Override
@@ -4495,8 +4503,10 @@
intent.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_SUCCESS);
intent.putExtra(PackageInstaller.EXTRA_PRE_APPROVAL, true);
try {
+ final BroadcastOptions options = BroadcastOptions.makeBasic();
+ options.setPendingIntentBackgroundActivityLaunchAllowed(false);
target.sendIntent(mContext, 0 /* code */, intent, null /* onFinished */,
- null /* handler */);
+ null /* handler */, null /* requiredPermission */, options.toBundle());
} catch (IntentSender.SendIntentException ignored) {
}
}
@@ -4763,7 +4773,10 @@
PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(intent.getAction()));
fillIn.putExtra(Intent.EXTRA_INTENT, intent);
try {
- target.sendIntent(context, 0, fillIn, null, null);
+ final BroadcastOptions options = BroadcastOptions.makeBasic();
+ options.setPendingIntentBackgroundActivityLaunchAllowed(false);
+ target.sendIntent(context, 0, fillIn, null /* onFinished */,
+ null /* handler */, null /* requiredPermission */, options.toBundle());
} catch (IntentSender.SendIntentException ignored) {
}
}
@@ -4805,7 +4818,10 @@
}
}
try {
- target.sendIntent(context, 0, fillIn, null, null);
+ final BroadcastOptions options = BroadcastOptions.makeBasic();
+ options.setPendingIntentBackgroundActivityLaunchAllowed(false);
+ target.sendIntent(context, 0, fillIn, null /* onFinished */,
+ null /* handler */, null /* requiredPermission */, options.toBundle());
} catch (IntentSender.SendIntentException ignored) {
}
}
@@ -4839,7 +4855,10 @@
intent.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, "Staging Image Not Ready");
}
try {
- target.sendIntent(context, 0, intent, null, null);
+ final BroadcastOptions options = BroadcastOptions.makeBasic();
+ options.setPendingIntentBackgroundActivityLaunchAllowed(false);
+ target.sendIntent(context, 0, intent, null /* onFinished */,
+ null /* handler */, null /* requiredPermission */, options.toBundle());
} catch (IntentSender.SendIntentException ignored) {
}
}
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index a02a419..d0a0558 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -56,6 +56,7 @@
import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.app.ApplicationPackageManager;
+import android.app.BroadcastOptions;
import android.app.IActivityManager;
import android.app.admin.IDevicePolicyManager;
import android.app.admin.SecurityLog;
@@ -387,6 +388,7 @@
static final int SCAN_DROP_CACHE = 1 << 24;
static final int SCAN_AS_FACTORY = 1 << 25;
static final int SCAN_AS_APEX = 1 << 26;
+ static final int SCAN_AS_STOPPED_SYSTEM_APP = 1 << 27;
@IntDef(flag = true, prefix = { "SCAN_" }, value = {
SCAN_NO_DEX,
@@ -403,6 +405,7 @@
SCAN_AS_INSTANT_APP,
SCAN_AS_FULL_APP,
SCAN_AS_VIRTUAL_PRELOAD,
+ SCAN_AS_STOPPED_SYSTEM_APP,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ScanFlags {}
@@ -964,6 +967,8 @@
final @Nullable String mRecentsPackage;
final @Nullable String mAmbientContextDetectionPackage;
final @Nullable String mWearableSensingPackage;
+ final @NonNull Set<String> mInitialNonStoppedSystemPackages;
+ final boolean mShouldStopSystemPackagesByDefault;
private final @NonNull String mRequiredSdkSandboxPackage;
@GuardedBy("mLock")
@@ -1771,6 +1776,8 @@
mOverlayConfigSignaturePackage = testParams.overlayConfigSignaturePackage;
mResolveComponentName = testParams.resolveComponentName;
mRequiredSdkSandboxPackage = testParams.requiredSdkSandboxPackage;
+ mInitialNonStoppedSystemPackages = testParams.initialNonStoppedSystemPackages;
+ mShouldStopSystemPackagesByDefault = testParams.shouldStopSystemPackagesByDefault;
mLiveComputer = createLiveComputer();
mSnapshotStatistics = null;
@@ -2097,6 +2104,11 @@
mCacheDir = PackageManagerServiceUtils.preparePackageParserCache(
mIsEngBuild, mIsUserDebugBuild, mIncrementalVersion);
+ mInitialNonStoppedSystemPackages = mInjector.getSystemConfig()
+ .getInitialNonStoppedSystemPackages();
+ mShouldStopSystemPackagesByDefault = mContext.getResources()
+ .getBoolean(R.bool.config_stopSystemPackagesByDefault);
+
final int[] userIds = mUserManager.getUserIds();
PackageParser2 packageParser = mInjector.getScanningCachingPackageParser();
mOverlayConfig = mInitAppsHelper.initSystemApps(packageParser, packageSettings, userIds,
@@ -3201,7 +3213,7 @@
mContext.enforceCallingOrSelfPermission(Manifest.permission.SUSPEND_APPS,
callingMethod);
- if (callingUid != Process.ROOT_UID && callingUid != Process.SYSTEM_UID
+ if (!PackageManagerServiceUtils.isSystemOrRoot(callingUid)
&& UserHandle.getUserId(callingUid) != userId) {
throw new SecurityException("Calling uid " + callingUid + " cannot call for user "
+ userId);
@@ -4937,7 +4949,11 @@
}
if (pi != null) {
try {
- pi.sendIntent(null, success ? 1 : 0, null, null, null);
+ final BroadcastOptions options = BroadcastOptions.makeBasic();
+ options.setPendingIntentBackgroundActivityLaunchAllowed(false);
+ pi.sendIntent(null, success ? 1 : 0, null /* intent */,
+ null /* onFinished*/, null /* handler */,
+ null /* requiredPermission */, options.toBundle());
} catch (SendIntentException e) {
Slog.w(TAG, e);
}
@@ -5312,7 +5328,7 @@
snapshot.enforceCrossUserPermission(callingUid, userId, false /*requireFullPermission*/,
true /*checkShell*/, "isPackageStateProtected");
- if (callingAppId != Process.SYSTEM_UID && callingAppId != Process.ROOT_UID
+ if (!PackageManagerServiceUtils.isSystemOrRoot(callingAppId)
&& snapshot.checkUidPermission(MANAGE_DEVICE_ADMINS, callingUid)
!= PERMISSION_GRANTED) {
throw new SecurityException("Caller must have the "
@@ -5847,8 +5863,8 @@
snapshot.enforceCrossUserPermission(callingUid, userId, true /*requireFullPermission*/,
true /*checkShell*/, "setHarmfulAppInfo");
- if (callingAppId != Process.SYSTEM_UID && callingAppId != Process.ROOT_UID &&
- snapshot.checkUidPermission(SET_HARMFUL_APP_WARNINGS, callingUid)
+ if (!PackageManagerServiceUtils.isSystemOrRoot(callingAppId)
+ && snapshot.checkUidPermission(SET_HARMFUL_APP_WARNINGS, callingUid)
!= PERMISSION_GRANTED) {
throw new SecurityException("Caller must have the "
+ SET_HARMFUL_APP_WARNINGS + " permission.");
@@ -6602,7 +6618,7 @@
public void uninstallApex(String packageName, long versionCode, int userId,
IntentSender intentSender, int flags) {
final int callerUid = Binder.getCallingUid();
- if (callerUid != Process.ROOT_UID && callerUid != Process.SHELL_UID) {
+ if (!PackageManagerServiceUtils.isRootOrShell(callerUid)) {
throw new SecurityException("Not allowed to uninstall apexes");
}
PackageInstallerService.PackageDeleteObserverAdapter adapter =
@@ -6647,7 +6663,7 @@
final int callingUid = Binder.getCallingUid();
final Computer snapshot = snapshotComputer();
final String[] callerPackageNames = snapshot.getPackagesForUid(callingUid);
- if (callingUid != Process.SHELL_UID && callingUid != Process.ROOT_UID
+ if (!PackageManagerServiceUtils.isRootOrShell(callingUid)
&& !ArrayUtils.contains(callerPackageNames, packageName)) {
throw new SecurityException("dumpProfiles");
}
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java b/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java
index e5cfa67..0ed90e4 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java
@@ -26,6 +26,7 @@
import android.os.Handler;
import android.os.incremental.IncrementalManager;
import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.DisplayMetrics;
import com.android.internal.annotations.VisibleForTesting;
@@ -40,6 +41,7 @@
import java.io.File;
import java.util.List;
+import java.util.Set;
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
public final class PackageManagerServiceTestParams {
@@ -119,4 +121,6 @@
public SuspendPackageHelper suspendPackageHelper;
public DistractingPackageHelper distractingPackageHelper;
public StorageEventHelper storageEventHelper;
+ public Set<String> initialNonStoppedSystemPackages = new ArraySet<>();
+ public boolean shouldStopSystemPackagesByDefault;
}
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index b919330..928ffa7 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -1372,10 +1372,24 @@
*/
public static boolean isSystemOrRoot() {
final int uid = Binder.getCallingUid();
+ return isSystemOrRoot(uid);
+ }
+
+ /**
+ * Check if a UID is system UID or root's UID.
+ */
+ public static boolean isSystemOrRoot(int uid) {
return uid == Process.SYSTEM_UID || uid == Process.ROOT_UID;
}
/**
+ * Check if a UID is system UID or shell's UID.
+ */
+ public static boolean isRootOrShell(int uid) {
+ return uid == Process.ROOT_UID || uid == Process.SHELL_UID;
+ }
+
+ /**
* Enforces that only the system UID or root's UID can call a method exposed
* via Binder.
*
diff --git a/services/core/java/com/android/server/pm/ScanPackageUtils.java b/services/core/java/com/android/server/pm/ScanPackageUtils.java
index 830b096c..2538871 100644
--- a/services/core/java/com/android/server/pm/ScanPackageUtils.java
+++ b/services/core/java/com/android/server/pm/ScanPackageUtils.java
@@ -31,6 +31,7 @@
import static com.android.server.pm.PackageManagerService.SCAN_AS_OEM;
import static com.android.server.pm.PackageManagerService.SCAN_AS_PRIVILEGED;
import static com.android.server.pm.PackageManagerService.SCAN_AS_PRODUCT;
+import static com.android.server.pm.PackageManagerService.SCAN_AS_STOPPED_SYSTEM_APP;
import static com.android.server.pm.PackageManagerService.SCAN_AS_SYSTEM;
import static com.android.server.pm.PackageManagerService.SCAN_AS_SYSTEM_EXT;
import static com.android.server.pm.PackageManagerService.SCAN_AS_VENDOR;
@@ -198,6 +199,7 @@
if (createNewPackage) {
final boolean instantApp = (scanFlags & SCAN_AS_INSTANT_APP) != 0;
final boolean virtualPreload = (scanFlags & SCAN_AS_VIRTUAL_PRELOAD) != 0;
+ final boolean isStoppedSystemApp = (scanFlags & SCAN_AS_STOPPED_SYSTEM_APP) != 0;
// Flags contain system values stored in the server variant of AndroidPackage,
// and so the server-side PackageInfoUtils is still called, even without a
@@ -212,7 +214,7 @@
AndroidPackageUtils.getRawPrimaryCpuAbi(parsedPackage),
AndroidPackageUtils.getRawSecondaryCpuAbi(parsedPackage),
parsedPackage.getLongVersionCode(), pkgFlags, pkgPrivateFlags, user,
- true /*allowInstall*/, instantApp, virtualPreload,
+ true /*allowInstall*/, instantApp, virtualPreload, isStoppedSystemApp,
UserManagerService.getInstance(), usesSdkLibraries,
parsedPackage.getUsesSdkLibrariesVersionsMajor(), usesStaticLibraries,
parsedPackage.getUsesStaticLibrariesVersions(), parsedPackage.getMimeGroups(),
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 165e476..7e7205d 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -89,7 +89,6 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.BackgroundThread;
-import com.android.internal.security.VerityUtils;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.CollectionUtils;
import com.android.internal.util.IndentingPrintWriter;
@@ -121,6 +120,7 @@
import com.android.server.pm.verify.domain.DomainVerificationLegacySettings;
import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
import com.android.server.pm.verify.domain.DomainVerificationPersistence;
+import com.android.server.security.FileIntegrity;
import com.android.server.utils.Slogf;
import com.android.server.utils.Snappable;
import com.android.server.utils.SnapshotCache;
@@ -1041,7 +1041,7 @@
File codePath, String legacyNativeLibraryPath, String primaryCpuAbi,
String secondaryCpuAbi, long versionCode, int pkgFlags, int pkgPrivateFlags,
UserHandle installUser, boolean allowInstall, boolean instantApp,
- boolean virtualPreload, UserManagerService userManager,
+ boolean virtualPreload, boolean isStoppedSystemApp, UserManagerService userManager,
String[] usesSdkLibraries, long[] usesSdkLibrariesVersions,
String[] usesStaticLibraries, long[] usesStaticLibrariesVersions,
Set<String> mimeGroupNames, @NonNull UUID domainSetId) {
@@ -1068,6 +1068,9 @@
pkgSetting.setFlags(pkgFlags)
.setPrivateFlags(pkgPrivateFlags);
} else {
+ int installUserId = installUser != null ? installUser.getIdentifier()
+ : UserHandle.USER_SYSTEM;
+
pkgSetting = new PackageSetting(pkgName, realPkgName, codePath,
legacyNativeLibraryPath, primaryCpuAbi, secondaryCpuAbi,
null /*cpuAbiOverrideString*/, versionCode, pkgFlags, pkgPrivateFlags,
@@ -1086,8 +1089,6 @@
Slog.i(PackageManagerService.TAG, "Stopping package " + pkgName, e);
}
List<UserInfo> users = getAllUsers(userManager);
- int installUserId = installUser != null ? installUser.getIdentifier()
- : UserHandle.USER_SYSTEM;
if (users != null && allowInstall) {
for (UserInfo user : users) {
// By default we consider this app to be installed
@@ -1126,6 +1127,13 @@
);
}
}
+ } else if (isStoppedSystemApp) {
+ if (DEBUG_STOPPED) {
+ RuntimeException e = new RuntimeException("here");
+ e.fillInStackTrace();
+ Slog.i(PackageManagerService.TAG, "Stopping system package " + pkgName, e);
+ }
+ pkgSetting.setStopped(true, installUserId);
}
if (sharedUser != null) {
pkgSetting.setAppId(sharedUser.mAppId);
@@ -2706,8 +2714,8 @@
}
try {
- VerityUtils.setUpFsverity(mSettingsFilename.getAbsolutePath());
- VerityUtils.setUpFsverity(mSettingsReserveCopyFilename.getAbsolutePath());
+ FileIntegrity.setUpFsVerity(mSettingsFilename);
+ FileIntegrity.setUpFsVerity(mSettingsReserveCopyFilename);
} catch (IOException e) {
Slog.e(TAG, "Failed to verity-protect settings", e);
}
@@ -4422,6 +4430,15 @@
ps.getPackageName()));
// Only system apps are initially installed.
ps.setInstalled(shouldReallyInstall, userHandle);
+
+ // Non-Apex system apps, that are not included in the allowlist in
+ // initialNonStoppedSystemPackages, should be marked as stopped by default.
+ final boolean shouldBeStopped = service.mShouldStopSystemPackagesByDefault
+ && ps.isSystem()
+ && !ps.isApex()
+ && !service.mInitialNonStoppedSystemPackages.contains(ps.getPackageName());
+ ps.setStopped(shouldBeStopped, userHandle);
+
// If userTypeInstallablePackages is the *only* reason why we're not installing,
// then uninstallReason is USER_TYPE. If there's a different reason, or if we
// actually are installing, put UNKNOWN.
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 0362ddd..6d5e2b0 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -885,7 +885,12 @@
* available ShareTarget definitions in this package.
*/
public List<ShortcutManager.ShareShortcutInfo> getMatchingShareTargets(
- @NonNull IntentFilter filter) {
+ @NonNull final IntentFilter filter) {
+ return getMatchingShareTargets(filter, null);
+ }
+
+ List<ShortcutManager.ShareShortcutInfo> getMatchingShareTargets(
+ @NonNull final IntentFilter filter, @Nullable final String pkgName) {
synchronized (mLock) {
final List<ShareTargetInfo> matchedTargets = new ArrayList<>();
for (int i = 0; i < mShareTargets.size(); i++) {
@@ -909,8 +914,7 @@
// included in the result
findAll(shortcuts, ShortcutInfo::isNonManifestVisible,
ShortcutInfo.CLONE_REMOVE_FOR_APP_PREDICTION,
- mShortcutUser.mService.mContext.getPackageName(),
- 0, /*getPinnedByAnyLauncher=*/ false);
+ pkgName, 0, /*getPinnedByAnyLauncher=*/ false);
final List<ShortcutManager.ShareShortcutInfo> result = new ArrayList<>();
for (int i = 0; i < shortcuts.size(); i++) {
@@ -1108,7 +1112,7 @@
// Now prepare to publish manifest shortcuts.
List<ShortcutInfo> newManifestShortcutList = null;
- final int shareTargetSize;
+ int shareTargetSize = 0;
synchronized (mLock) {
try {
shareTargetSize = mShareTargets.size();
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 83720f1..1cd9ec6 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -2512,11 +2512,17 @@
}
enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APP_PREDICTIONS,
"getShareTargets");
+ final ComponentName chooser = injectChooserActivity();
+ final String pkg = (chooser != null
+ && mPackageManagerInternal.getComponentEnabledSetting(chooser,
+ injectBinderCallingUid(), userId) == PackageManager.COMPONENT_ENABLED_STATE_ENABLED)
+ ? chooser.getPackageName() : mContext.getPackageName();
synchronized (mLock) {
throwIfUserLockedL(userId);
final List<ShortcutManager.ShareShortcutInfo> shortcutInfoList = new ArrayList<>();
final ShortcutUser user = getUserShortcutsLocked(userId);
- user.forAllPackages(p -> shortcutInfoList.addAll(p.getMatchingShareTargets(filter)));
+ user.forAllPackages(p -> shortcutInfoList.addAll(
+ p.getMatchingShareTargets(filter, pkg)));
return new ParceledListSlice<>(shortcutInfoList);
}
}
diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java
index eb3be54..3cbaebe 100644
--- a/services/core/java/com/android/server/pm/UserManagerInternal.java
+++ b/services/core/java/com/android/server/pm/UserManagerInternal.java
@@ -52,21 +52,24 @@
// TODO(b/248408342): Move keep annotation to the method referencing these fields reflectively.
@Keep public static final int USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE = 1;
@Keep public static final int USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE = 2;
+ @Keep public static final int USER_ASSIGNMENT_RESULT_SUCCESS_ALREADY_VISIBLE = 3;
@Keep public static final int USER_ASSIGNMENT_RESULT_FAILURE = -1;
private static final String PREFIX_USER_ASSIGNMENT_RESULT = "USER_ASSIGNMENT_RESULT_";
@IntDef(flag = false, prefix = {PREFIX_USER_ASSIGNMENT_RESULT}, value = {
USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE,
USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE,
+ USER_ASSIGNMENT_RESULT_SUCCESS_ALREADY_VISIBLE,
USER_ASSIGNMENT_RESULT_FAILURE
})
public @interface UserAssignmentResult {}
- private static final String PREFIX_USER_START_MODE = "USER_START_MODE_";
+ // TODO(b/248408342): Move keep annotation to the method referencing these fields reflectively.
+ @Keep public static final int USER_START_MODE_FOREGROUND = 1;
+ @Keep public static final int USER_START_MODE_BACKGROUND = 2;
+ @Keep public static final int USER_START_MODE_BACKGROUND_VISIBLE = 3;
- /**
- * Type used to indicate how a user started.
- */
+ private static final String PREFIX_USER_START_MODE = "USER_START_MODE_";
@IntDef(flag = false, prefix = {PREFIX_USER_START_MODE}, value = {
USER_START_MODE_FOREGROUND,
USER_START_MODE_BACKGROUND,
@@ -74,32 +77,6 @@
})
public @interface UserStartMode {}
- // TODO(b/248408342): Move keep annotations below to the method referencing these fields
- // reflectively.
-
- /** (Full) user started on foreground (a.k.a. "current user"). */
- @Keep public static final int USER_START_MODE_FOREGROUND = 1;
-
- /**
- * User (full or profile) started on background and is
- * {@link UserManager#isUserVisible() invisible}.
- *
- * <p>This is the "traditional" way of starting a background user, and can be used to start
- * profiles as well, although starting an invisible profile is not common from the System UI
- * (it could be done through APIs or adb, though).
- */
- @Keep public static final int USER_START_MODE_BACKGROUND = 2;
-
- /**
- * User (full or profile) started on background and is
- * {@link UserManager#isUserVisible() visible}.
- *
- * <p>This is the "traditional" way of starting a profile (i.e., when the profile of the current
- * user is the current foreground user), but it can also be used to start a full user associated
- * with a display (which is the case on automotives with passenger displays).
- */
- @Keep public static final int USER_START_MODE_BACKGROUND_VISIBLE = 3;
-
public interface UserRestrictionsListener {
/**
* Called when a user restriction changes.
@@ -551,11 +528,12 @@
* switched to.
*
* <p>Otherwise, in {@link UserManager#isHeadlessSystemUserMode() headless system user mode},
- * this will be the user who was last in the foreground on this device. If there is no
- * switchable user on the device, a new user will be created and its id will be returned.
+ * this will be the user who was last in the foreground on this device.
*
- * <p>In non-headless system user mode, the return value will be {@link UserHandle#USER_SYSTEM}.
+ * <p>In non-headless system user mode, the return value will be
+ * {@link android.os.UserHandle#USER_SYSTEM}.
+
+ * @throws UserManager.CheckedUserOperationException if no switchable user can be found
*/
- public abstract @UserIdInt int getBootUser()
- throws UserManager.CheckedUserOperationException;
+ public abstract @UserIdInt int getBootUser() throws UserManager.CheckedUserOperationException;
}
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 762d1f6..ce7dc5b 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -5049,6 +5049,8 @@
//...then external ones
Intent addedIntent = new Intent(Intent.ACTION_USER_ADDED);
addedIntent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+ // In HSUM, MainUser might be created before PHASE_ACTIVITY_MANAGER_READY has been sent.
+ addedIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
addedIntent.putExtra(Intent.EXTRA_USER_HANDLE, userInfo.id);
// Also, add the UserHandle for mainline modules which can't use the @hide
// EXTRA_USER_HANDLE.
@@ -6758,18 +6760,6 @@
return mLocalService.isUserInitialized(userId);
}
- /**
- * Creates a new user, intended to be the initial user on a device in headless system user mode.
- */
- private UserInfo createInitialUserForHsum() throws UserManager.CheckedUserOperationException {
- final int flags = UserInfo.FLAG_ADMIN | UserInfo.FLAG_MAIN;
-
- // Null name will be replaced with "Owner" on-demand to allow for localisation.
- return createUserInternalUnchecked(/* name= */ null, UserManager.USER_TYPE_FULL_SECONDARY,
- flags, UserHandle.USER_NULL, /* preCreate= */ false,
- /* disallowedPackages= */ null, /* token= */ null);
- }
-
private class LocalService extends UserManagerInternal {
@Override
public void setDevicePolicyUserRestrictions(@UserIdInt int originatingUserId,
@@ -7249,15 +7239,9 @@
}
}
}
- // No switchable users. Create the initial user.
- final UserInfo newInitialUser = createInitialUserForHsum();
- if (newInitialUser == null) {
- throw new UserManager.CheckedUserOperationException(
- "Initial user creation failed", USER_OPERATION_ERROR_UNKNOWN);
- }
- Slogf.i(LOG_TAG,
- "No switchable users. Boot user is new user %d", newInitialUser.id);
- return newInitialUser.id;
+ // No switchable users found. Uh oh!
+ throw new UserManager.CheckedUserOperationException(
+ "No switchable users found", USER_OPERATION_ERROR_UNKNOWN);
}
// Not HSUM, return system user.
return UserHandle.USER_SYSTEM;
@@ -7437,14 +7421,14 @@
/**
* Returns true, when user has {@link UserInfo#FLAG_MAIN} and system property
- * {@link com.android.internal.R.bool.isMainUserPermanentAdmin} is true.
+ * {@link com.android.internal.R.bool#config_isMainUserPermanentAdmin} is true.
*/
private boolean isNonRemovableMainUser(UserInfo userInfo) {
return userInfo.isMain() && isMainUserPermanentAdmin();
}
/**
- * Returns true, when {@link com.android.internal.R.bool.isMainUserPermanentAdmin} is true.
+ * Returns true if {@link com.android.internal.R.bool#config_isMainUserPermanentAdmin} is true.
* If the main user is a permanent admin user it can't be deleted
* or downgraded to non-admin status.
*/
diff --git a/services/core/java/com/android/server/pm/UserManagerServiceShellCommand.java b/services/core/java/com/android/server/pm/UserManagerServiceShellCommand.java
index 5bdcbb9..50a1d90 100644
--- a/services/core/java/com/android/server/pm/UserManagerServiceShellCommand.java
+++ b/services/core/java/com/android/server/pm/UserManagerServiceShellCommand.java
@@ -25,6 +25,7 @@
import android.content.Context;
import android.content.pm.PackageManagerInternal;
import android.content.pm.UserInfo;
+import android.content.res.Resources;
import android.os.Binder;
import android.os.Build;
import android.os.Process;
@@ -36,6 +37,7 @@
import android.util.IndentingPrintWriter;
import android.util.Slog;
+import com.android.internal.R;
import com.android.internal.os.RoSystemProperties;
import com.android.internal.widget.LockPatternUtils;
import com.android.server.LocalServices;
@@ -103,6 +105,9 @@
pw.println();
pw.println(" is-headless-system-user-mode [-v | --verbose]");
pw.println(" Checks whether the device uses headless system user mode.");
+ pw.println(" is-visible-background-users-on-default-display-supported [-v | --verbose]");
+ pw.println(" Checks whether the device allows users to be start visible on background "
+ + "in the default display.");
pw.println(" It returns the effective mode, even when using emulation");
pw.println(" (to get the real mode as well, use -v or --verbose)");
pw.println();
@@ -129,6 +134,8 @@
return runSetSystemUserModeEmulation();
case "is-headless-system-user-mode":
return runIsHeadlessSystemUserMode();
+ case "is-visible-background-users-on-default-display-supported":
+ return runIsVisibleBackgroundUserOnDefaultDisplaySupported();
case "is-user-visible":
return runIsUserVisible();
default:
@@ -431,19 +438,47 @@
return -1;
}
}
-
- boolean isHsum = mService.isHeadlessSystemUserMode();
+ boolean effective = mService.isHeadlessSystemUserMode();
if (!verbose) {
// NOTE: do not change output below, as it's used by ITestDevice
// (it's ok to change the verbose option though)
- pw.println(isHsum);
+ pw.println(effective);
} else {
- pw.printf("effective=%b real=%b\n", isHsum,
+ pw.printf("effective=%b real=%b\n", effective,
RoSystemProperties.MULTIUSER_HEADLESS_SYSTEM_USER);
}
return 0;
}
+ private int runIsVisibleBackgroundUserOnDefaultDisplaySupported() {
+ PrintWriter pw = getOutPrintWriter();
+
+ boolean verbose = false;
+ String opt;
+ while ((opt = getNextOption()) != null) {
+ switch (opt) {
+ case "-v":
+ case "--verbose":
+ verbose = true;
+ break;
+ default:
+ pw.println("Invalid option: " + opt);
+ return -1;
+ }
+ }
+
+ boolean effective = UserManager.isVisibleBackgroundUsersOnDefaultDisplayEnabled();
+ if (!verbose) {
+ // NOTE: do not change output below, as it's used by ITestDevice
+ // (it's ok to change the verbose option though)
+ pw.println(effective);
+ } else {
+ pw.printf("effective=%b real=%b\n", effective, Resources.getSystem()
+ .getBoolean(R.bool.config_multiuserVisibleBackgroundUsersOnDefaultDisplay));
+ }
+ return 0;
+ }
+
/**
* Gets the {@link UserManager} associated with the context of the given user.
*/
diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index 77c32ae..4cf8c09 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -121,6 +121,16 @@
.setBaseType(FLAG_PROFILE)
.setMaxAllowedPerParent(1)
.setLabel(0)
+ .setIconBadge(com.android.internal.R.drawable.ic_clone_icon_badge)
+ .setBadgePlain(com.android.internal.R.drawable.ic_clone_badge)
+ // Clone doesn't use BadgeNoBackground, so just set to BadgePlain as a placeholder.
+ .setBadgeNoBackground(com.android.internal.R.drawable.ic_clone_badge)
+ .setBadgeLabels(
+ com.android.internal.R.string.clone_profile_label_badge)
+ .setBadgeColors(
+ com.android.internal.R.color.system_neutral2_800)
+ .setDarkThemeBadgeColors(
+ com.android.internal.R.color.system_neutral2_900)
.setDefaultRestrictions(null)
.setDefaultCrossProfileIntentFilters(getDefaultCloneCrossProfileIntentFilter())
.setDefaultUserProperties(new UserProperties.Builder()
diff --git a/services/core/java/com/android/server/pm/UserVisibilityMediator.java b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
index 0a181a4..d5cc7ca 100644
--- a/services/core/java/com/android/server/pm/UserVisibilityMediator.java
+++ b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
@@ -22,6 +22,7 @@
import static android.view.Display.INVALID_DISPLAY;
import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE;
+import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_ALREADY_VISIBLE;
import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE;
import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE;
import static com.android.server.pm.UserManagerInternal.USER_START_MODE_BACKGROUND;
@@ -41,7 +42,6 @@
import android.util.EventLog;
import android.util.IndentingPrintWriter;
import android.util.IntArray;
-import android.util.Log;
import android.util.SparseIntArray;
import android.view.Display;
@@ -55,8 +55,6 @@
import com.android.server.utils.Slogf;
import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
@@ -79,11 +77,11 @@
*/
public final class UserVisibilityMediator implements Dumpable {
- private static final String TAG = UserVisibilityMediator.class.getSimpleName();
-
- private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+ private static final boolean DBG = false; // DO NOT SUBMIT WITH TRUE
private static final boolean VERBOSE = false; // DO NOT SUBMIT WITH TRUE
+ private static final String TAG = UserVisibilityMediator.class.getSimpleName();
+
private static final String PREFIX_SECONDARY_DISPLAY_MAPPING = "SECONDARY_DISPLAY_MAPPING_";
public static final int SECONDARY_DISPLAY_MAPPING_NEEDED = 1;
public static final int SECONDARY_DISPLAY_MAPPING_NOT_NEEDED = 2;
@@ -100,7 +98,7 @@
})
public @interface SecondaryDisplayMappingStatus {}
- // TODO(b/266158156): might need to change this if boot logic is refactored for HSUM devices
+ // TODO(b/242195409): might need to change this if boot logic is refactored for HSUM devices
@VisibleForTesting
static final int INITIAL_CURRENT_USER_ID = USER_SYSTEM;
@@ -134,23 +132,10 @@
private final SparseIntArray mExtraDisplaysAssignedToUsers;
/**
- * Mapping of each user that started visible (key) to its profile group id (value).
- *
- * <p>It's used to determine not just if the user is visible, but also
- * {@link #isProfile(int, int) if it's a profile}.
+ * Mapping from each started user to its profile group.
*/
@GuardedBy("mLock")
- private final SparseIntArray mStartedVisibleProfileGroupIds = new SparseIntArray();
-
- /**
- * List of profiles that have explicitly started invisible.
- *
- * <p>Only used for debugging purposes (and set when {@link #DBG} is {@code true}), hence we
- * don't care about autoboxing.
- */
- @GuardedBy("mLock")
- @Nullable
- private final List<Integer> mStartedInvisibleProfileUserIds;
+ private final SparseIntArray mStartedProfileGroupIds = new SparseIntArray();
/**
* Handler user to call listeners
@@ -163,8 +148,7 @@
UserVisibilityMediator(Handler handler) {
this(UserManager.isVisibleBackgroundUsersEnabled(),
- // TODO(b/261538232): get visibleBackgroundUserOnDefaultDisplayAllowed from UM
- /* visibleBackgroundUserOnDefaultDisplayAllowed= */ false, handler);
+ UserManager.isVisibleBackgroundUsersOnDefaultDisplayEnabled(), handler);
}
@VisibleForTesting
@@ -180,14 +164,9 @@
mUsersAssignedToDisplayOnStart = null;
mExtraDisplaysAssignedToUsers = null;
}
- mStartedInvisibleProfileUserIds = DBG ? new ArrayList<>(4) : null;
mHandler = handler;
- // TODO(b/266158156): might need to change this if boot logic is refactored for HSUM devices
- mStartedVisibleProfileGroupIds.put(INITIAL_CURRENT_USER_ID, INITIAL_CURRENT_USER_ID);
-
- if (DBG) {
- Slogf.i(TAG, "UserVisibilityMediator created with DBG on");
- }
+ // TODO(b/242195409): might need to change this if boot logic is refactored for HSUM devices
+ mStartedProfileGroupIds.put(INITIAL_CURRENT_USER_ID, INITIAL_CURRENT_USER_ID);
}
/**
@@ -198,8 +177,6 @@
int displayId) {
Preconditions.checkArgument(!isSpecialUserId(userId), "user id cannot be generic: %d",
userId);
- validateUserStartMode(userStartMode);
-
// This method needs to perform 4 actions:
//
// 1. Check if the user can be started given the provided arguments
@@ -230,7 +207,8 @@
Slogf.d(TAG, "result of getUserVisibilityOnStartLocked(%s)",
userAssignmentResultToString(result));
}
- if (result == USER_ASSIGNMENT_RESULT_FAILURE) {
+ if (result == USER_ASSIGNMENT_RESULT_FAILURE
+ || result == USER_ASSIGNMENT_RESULT_SUCCESS_ALREADY_VISIBLE) {
return result;
}
@@ -246,29 +224,14 @@
visibleUsersBefore = getVisibleUsers();
- // Set current user / started users state
- switch (userStartMode) {
- case USER_START_MODE_FOREGROUND:
- mCurrentUserId = userId;
- // Fallthrough
- case USER_START_MODE_BACKGROUND_VISIBLE:
- if (DBG) {
- Slogf.d(TAG, "adding visible user / profile group id mapping (%d -> %d)",
- userId, profileGroupId);
- }
- mStartedVisibleProfileGroupIds.put(userId, profileGroupId);
- break;
- case USER_START_MODE_BACKGROUND:
- if (mStartedInvisibleProfileUserIds != null
- && isProfile(userId, profileGroupId)) {
- Slogf.d(TAG, "adding user %d to list of invisible profiles", userId);
- mStartedInvisibleProfileUserIds.add(userId);
- }
- break;
- default:
- Slogf.wtf(TAG, "invalid userStartMode passed to assignUserToDisplayOnStart: "
- + "%d", userStartMode);
+ // Set current user / profiles state
+ if (userStartMode == USER_START_MODE_FOREGROUND) {
+ mCurrentUserId = userId;
}
+ if (DBG) {
+ Slogf.d(TAG, "adding user / profile mapping (%d -> %d)", userId, profileGroupId);
+ }
+ mStartedProfileGroupIds.put(userId, profileGroupId);
// Set user / display state
switch (mappingResult) {
@@ -316,56 +279,57 @@
}
boolean visibleBackground = userStartMode == USER_START_MODE_BACKGROUND_VISIBLE;
- if (displayId == DEFAULT_DISPLAY && visibleBackground
- && !mVisibleBackgroundUserOnDefaultDisplayAllowed
- && !isProfile(userId, profileGroupId)) {
- Slogf.wtf(TAG, "cannot start full user (%d) visible on default display", userId);
- return USER_ASSIGNMENT_RESULT_FAILURE;
+ if (displayId == DEFAULT_DISPLAY && visibleBackground) {
+ if (mVisibleBackgroundUserOnDefaultDisplayAllowed && isCurrentUserLocked(userId)) {
+ // Shouldn't happen - UserController returns before calling this method
+ Slogf.wtf(TAG, "trying to start current user (%d) visible in background on default"
+ + " display", userId);
+ return USER_ASSIGNMENT_RESULT_SUCCESS_ALREADY_VISIBLE;
+
+ }
+ if (!mVisibleBackgroundUserOnDefaultDisplayAllowed
+ && !isProfile(userId, profileGroupId)) {
+ Slogf.wtf(TAG, "cannot start full user (%d) visible on default display", userId);
+ return USER_ASSIGNMENT_RESULT_FAILURE;
+ }
}
boolean foreground = userStartMode == USER_START_MODE_FOREGROUND;
if (displayId != DEFAULT_DISPLAY) {
if (foreground) {
- Slogf.w(TAG, "getUserVisibilityOnStartLocked(%d, %d, %s, %d) failed: cannot start "
+ Slogf.w(TAG, "getUserVisibilityOnStartLocked(%d, %d, %b, %d) failed: cannot start "
+ "foreground user on secondary display", userId, profileGroupId,
- userStartModeToString(userStartMode), displayId);
+ foreground, displayId);
return USER_ASSIGNMENT_RESULT_FAILURE;
}
if (!mVisibleBackgroundUsersEnabled) {
- Slogf.w(TAG, "getUserVisibilityOnStartLocked(%d, %d, %s, %d) failed: called on "
+ Slogf.w(TAG, "getUserVisibilityOnStartLocked(%d, %d, %b, %d) failed: called on "
+ "device that doesn't support multiple users on multiple displays",
- userId, profileGroupId, userStartModeToString(userStartMode), displayId);
+ userId, profileGroupId, foreground, displayId);
return USER_ASSIGNMENT_RESULT_FAILURE;
}
}
if (isProfile(userId, profileGroupId)) {
if (displayId != DEFAULT_DISPLAY) {
- Slogf.w(TAG, "canStartUserLocked(%d, %d, %s, %d) failed: cannot start profile user "
- + "on secondary display", userId, profileGroupId,
- userStartModeToString(userStartMode), displayId);
+ Slogf.w(TAG, "canStartUserLocked(%d, %d, %b, %d) failed: cannot start profile user "
+ + "on secondary display", userId, profileGroupId, foreground,
+ displayId);
return USER_ASSIGNMENT_RESULT_FAILURE;
}
- switch (userStartMode) {
- case USER_START_MODE_FOREGROUND:
- Slogf.w(TAG, "startUser(%d, %d, %s, %d) failed: cannot start profile user in "
- + "foreground", userId, profileGroupId,
- userStartModeToString(userStartMode), displayId);
- return USER_ASSIGNMENT_RESULT_FAILURE;
- case USER_START_MODE_BACKGROUND_VISIBLE:
- boolean isParentVisibleOnDisplay = isUserVisible(profileGroupId, displayId);
- if (!isParentVisibleOnDisplay) {
- Slogf.w(TAG, "getUserVisibilityOnStartLocked(%d, %d, %s, %d) failed: cannot"
- + " start profile user visible when its parent is not visible in "
- + "that display", userId, profileGroupId,
- userStartModeToString(userStartMode), displayId);
- return USER_ASSIGNMENT_RESULT_FAILURE;
- }
- return USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE;
- case USER_START_MODE_BACKGROUND:
- return USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE;
+ if (foreground) {
+ Slogf.w(TAG, "startUser(%d, %d, %b, %d) failed: cannot start profile user in "
+ + "foreground", userId, profileGroupId, foreground, displayId);
+ return USER_ASSIGNMENT_RESULT_FAILURE;
+ } else {
+ boolean isParentVisibleOnDisplay = isUserVisible(profileGroupId, displayId);
+ if (DBG) {
+ Slogf.d(TAG, "parent visible on display: %b", isParentVisibleOnDisplay);
+ }
+ return isParentVisibleOnDisplay
+ ? USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE
+ : USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE;
}
-
}
return foreground || displayId != DEFAULT_DISPLAY
@@ -383,9 +347,8 @@
if (mVisibleBackgroundUserOnDefaultDisplayAllowed
&& userStartMode == USER_START_MODE_BACKGROUND_VISIBLE) {
int userStartedOnDefaultDisplay = getUserStartedOnDisplay(DEFAULT_DISPLAY);
- if (userStartedOnDefaultDisplay != USER_NULL
- && userStartedOnDefaultDisplay != profileGroupId) {
- Slogf.w(TAG, "canAssignUserToDisplayLocked(): cannot start user %d visible on"
+ if (userStartedOnDefaultDisplay != USER_NULL) {
+ Slogf.w(TAG, "getUserVisibilityOnStartLocked(): cannot start user %d visible on"
+ " default display because user %d already did so", userId,
userStartedOnDefaultDisplay);
return SECONDARY_DISPLAY_MAPPING_FAILED;
@@ -406,7 +369,7 @@
// parent is the current user, as the current user is always assigned to the
// DEFAULT_DISPLAY).
if (DBG) {
- Slogf.d(TAG, "ignoring mapping for default display for user %d starting as %s",
+ Slogf.d(TAG, "Ignoring mapping for default display for user %d starting as %s",
userId, userStartModeToString(userStartMode));
}
return SECONDARY_DISPLAY_MAPPING_NOT_NEEDED;
@@ -491,7 +454,7 @@
userId, displayId);
return false;
}
- if (isStartedVisibleProfileLocked(userId)) {
+ if (isStartedProfile(userId)) {
Slogf.w(TAG, "assignUserToExtraDisplay(%d, %d): failed because user is a profile",
userId, displayId);
return false;
@@ -579,14 +542,10 @@
@GuardedBy("mLock")
private void unassignUserFromAllDisplaysOnStopLocked(@UserIdInt int userId) {
if (DBG) {
- Slogf.d(TAG, "Removing %d from mStartedVisibleProfileGroupIds (%s)", userId,
- mStartedVisibleProfileGroupIds);
+ Slogf.d(TAG, "Removing %d from mStartedProfileGroupIds (%s)", userId,
+ mStartedProfileGroupIds);
}
- mStartedVisibleProfileGroupIds.delete(userId);
- if (mStartedInvisibleProfileUserIds != null) {
- Slogf.d(TAG, "Removing %d from list of invisible profiles", userId);
- mStartedInvisibleProfileUserIds.remove(Integer.valueOf(userId));
- }
+ mStartedProfileGroupIds.delete(userId);
if (!mVisibleBackgroundUsersEnabled) {
// Don't need to update mUsersAssignedToDisplayOnStart because methods (such as
@@ -616,8 +575,7 @@
* See {@link UserManagerInternal#isUserVisible(int)}.
*/
public boolean isUserVisible(@UserIdInt int userId) {
- // For optimization (as most devices don't support visible background users), check for
- // current foreground user and their profiles first
+ // First check current foreground user and their profiles (on main display)
if (isCurrentUserOrRunningProfileOfCurrentUser(userId)) {
if (VERBOSE) {
Slogf.v(TAG, "isUserVisible(%d): true to current user or profile", userId);
@@ -626,31 +584,19 @@
}
if (!mVisibleBackgroundUsersEnabled) {
- if (VERBOSE) {
- Slogf.v(TAG, "isUserVisible(%d): false for non-current user (or its profiles) when"
+ if (DBG) {
+ Slogf.d(TAG, "isUserVisible(%d): false for non-current user (or its profiles) when"
+ " device doesn't support visible background users", userId);
}
return false;
}
-
+ boolean visible;
synchronized (mLock) {
- int profileGroupId;
- synchronized (mLock) {
- profileGroupId = mStartedVisibleProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID);
- }
- if (isProfile(userId, profileGroupId)) {
- return isUserAssignedToDisplayOnStartLocked(profileGroupId);
- }
- return isUserAssignedToDisplayOnStartLocked(userId);
+ visible = mUsersAssignedToDisplayOnStart.indexOfKey(userId) >= 0;
}
- }
-
- @GuardedBy("mLock")
- private boolean isUserAssignedToDisplayOnStartLocked(@UserIdInt int userId) {
- boolean visible = mUsersAssignedToDisplayOnStart.indexOfKey(userId) >= 0;
- if (VERBOSE) {
- Slogf.v(TAG, "isUserAssignedToDisplayOnStartLocked(%d): %b", userId, visible);
+ if (DBG) {
+ Slogf.d(TAG, "isUserVisible(%d): %b from mapping", userId, visible);
}
return visible;
}
@@ -663,8 +609,7 @@
return false;
}
- // For optimization (as most devices don't support visible background users), check for
- // current user and profile first. Current user is always visible on:
+ // Current user is always visible on:
// - Default display
// - Secondary displays when device doesn't support visible bg users
// - Or when explicitly added (which is checked below)
@@ -686,26 +631,14 @@
}
synchronized (mLock) {
- int profileGroupId;
- synchronized (mLock) {
- profileGroupId = mStartedVisibleProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID);
+ if (mUsersAssignedToDisplayOnStart.get(userId, Display.INVALID_DISPLAY) == displayId) {
+ // User assigned to display on start
+ return true;
}
- if (isProfile(userId, profileGroupId)) {
- return isFullUserVisibleOnBackgroundLocked(profileGroupId, displayId);
- }
- return isFullUserVisibleOnBackgroundLocked(userId, displayId);
- }
- }
- // NOTE: it doesn't check if the userId is a full user, it's up to the caller to check that
- @GuardedBy("mLock")
- private boolean isFullUserVisibleOnBackgroundLocked(@UserIdInt int userId, int displayId) {
- if (mUsersAssignedToDisplayOnStart.get(userId, Display.INVALID_DISPLAY) == displayId) {
- // User assigned to display on start
- return true;
+ // Check for extra display assignment
+ return mExtraDisplaysAssignedToUsers.get(displayId, USER_NULL) == userId;
}
- // Check for extra display assignment
- return mExtraDisplaysAssignedToUsers.get(displayId, USER_NULL) == userId;
}
/**
@@ -773,7 +706,7 @@
continue;
}
int userId = mUsersAssignedToDisplayOnStart.keyAt(i);
- if (!isStartedVisibleProfileLocked(userId)) {
+ if (!isStartedProfile(userId)) {
return userId;
} else if (DBG) {
Slogf.d(TAG, "getUserAssignedToDisplay(%d): skipping user %d because it's "
@@ -806,8 +739,8 @@
// number of users is too small, the gain is probably not worth the increase on complexity.
IntArray visibleUsers = new IntArray();
synchronized (mLock) {
- for (int i = 0; i < mStartedVisibleProfileGroupIds.size(); i++) {
- int userId = mStartedVisibleProfileGroupIds.keyAt(i);
+ for (int i = 0; i < mStartedProfileGroupIds.size(); i++) {
+ int userId = mStartedProfileGroupIds.keyAt(i);
if (isUserVisible(userId)) {
visibleUsers.add(userId);
}
@@ -840,7 +773,7 @@
}
}
- // TODO(b/266158156): remove this method if not needed anymore
+ // TODO(b/242195409): remove this method if not needed anymore
/**
* Nofify all listeners that the system user visibility changed.
*/
@@ -902,9 +835,6 @@
ipw.println("UserVisibilityMediator");
ipw.increaseIndent();
- ipw.print("DBG: ");
- ipw.println(DBG);
-
synchronized (mLock) {
ipw.print("Current user id: ");
ipw.println(mCurrentUserId);
@@ -912,12 +842,8 @@
ipw.print("Visible users: ");
ipw.println(getVisibleUsers());
- dumpSparseIntArray(ipw, mStartedVisibleProfileGroupIds,
- "started visible user / profile group", "u", "pg");
- if (mStartedInvisibleProfileUserIds != null) {
- ipw.print("Profiles started invisible: ");
- ipw.println(mStartedInvisibleProfileUserIds);
- }
+ dumpSparseIntArray(ipw, mStartedProfileGroupIds, "started user / profile group",
+ "u", "pg");
ipw.print("Supports visible background users on displays: ");
ipw.println(mVisibleBackgroundUsersEnabled);
@@ -1007,6 +933,15 @@
}
}
+ @GuardedBy("mLock")
+ private boolean isCurrentUserLocked(@UserIdInt int userId) {
+ // Special case as NO_PROFILE_GROUP_ID == USER_NULL
+ if (userId == USER_NULL || mCurrentUserId == USER_NULL) {
+ return false;
+ }
+ return mCurrentUserId == userId;
+ }
+
private boolean isCurrentUserOrRunningProfileOfCurrentUser(@UserIdInt int userId) {
synchronized (mLock) {
// Special case as NO_PROFILE_GROUP_ID == USER_NULL
@@ -1016,25 +951,22 @@
if (mCurrentUserId == userId) {
return true;
}
- return mStartedVisibleProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID)
- == mCurrentUserId;
+ return mStartedProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID) == mCurrentUserId;
}
}
- @GuardedBy("mLock")
- private boolean isStartedVisibleProfileLocked(@UserIdInt int userId) {
- int profileGroupId = mStartedVisibleProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID);
+ private boolean isStartedProfile(@UserIdInt int userId) {
+ int profileGroupId;
+ synchronized (mLock) {
+ profileGroupId = mStartedProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID);
+ }
return isProfile(userId, profileGroupId);
}
- private void validateUserStartMode(@UserStartMode int userStartMode) {
- switch (userStartMode) {
- case USER_START_MODE_FOREGROUND:
- case USER_START_MODE_BACKGROUND:
- case USER_START_MODE_BACKGROUND_VISIBLE:
- return;
+ private @UserIdInt int getStartedProfileGroupId(@UserIdInt int userId) {
+ synchronized (mLock) {
+ return mStartedProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID);
}
- throw new IllegalArgumentException("Invalid user start mode: " + userStartMode);
}
private static String secondaryDisplayMappingStatusToString(
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index 287cc29..42a8cb1 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -51,7 +51,7 @@
import android.content.pm.PermissionGroupInfo;
import android.content.pm.PermissionInfo;
import android.content.pm.permission.SplitPermissionInfoParcelable;
-import android.healthconnect.HealthConnectManager;
+import android.health.connect.HealthConnectManager;
import android.os.Binder;
import android.os.IBinder;
import android.os.Process;
diff --git a/services/core/java/com/android/server/pm/resolution/ComponentResolver.java b/services/core/java/com/android/server/pm/resolution/ComponentResolver.java
index 35c42a3..fac681a 100644
--- a/services/core/java/com/android/server/pm/resolution/ComponentResolver.java
+++ b/services/core/java/com/android/server/pm/resolution/ComponentResolver.java
@@ -943,11 +943,7 @@
return false;
}
- // System apps are never considered stopped for purposes of
- // filtering, because there may be no way for the user to
- // actually re-launch them.
- return !packageState.isSystem()
- && packageState.getUserStateOrDefault(userId).isStopped();
+ return packageState.getUserStateOrDefault(userId).isStopped();
}
}
diff --git a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
index 7f733ef..8d7f782 100644
--- a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
+++ b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
@@ -366,12 +366,12 @@
}
mListener = listener;
}
- notifySupportedStatesChanged();
+ notifySupportedStatesChanged(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED);
notifyDeviceStateChangedIfNeeded();
}
/** Notifies the listener that the set of supported device states has changed. */
- private void notifySupportedStatesChanged() {
+ private void notifySupportedStatesChanged(@SupportedStatesUpdatedReason int reason) {
List<DeviceState> supportedStates = new ArrayList<>();
Listener listener;
synchronized (mLock) {
@@ -390,7 +390,7 @@
}
listener.onSupportedDeviceStatesChanged(
- supportedStates.toArray(new DeviceState[supportedStates.size()]));
+ supportedStates.toArray(new DeviceState[supportedStates.size()]), reason);
}
/** Computes the current device state and notifies the listener of a change, if needed. */
@@ -688,7 +688,10 @@
if (isThermalStatusCriticalOrAbove != isPreviousThermalStatusCriticalOrAbove) {
Slog.i(TAG, "Updating supported device states due to thermal status change."
+ " isThermalStatusCriticalOrAbove: " + isThermalStatusCriticalOrAbove);
- notifySupportedStatesChanged();
+ notifySupportedStatesChanged(
+ isThermalStatusCriticalOrAbove
+ ? SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL
+ : SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL);
}
}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index db939d9..5ca3333 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -334,6 +334,8 @@
static public final String SYSTEM_DIALOG_REASON_SCREENSHOT = "screenshot";
static public final String SYSTEM_DIALOG_REASON_GESTURE_NAV = "gestureNav";
+ public static final String TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD = "waitForAllWindowsDrawn";
+
private static final String TALKBACK_LABEL = "TalkBack";
private static final int POWER_BUTTON_SUPPRESSION_DELAY_DEFAULT_MILLIS = 800;
@@ -4902,10 +4904,15 @@
// ... eventually calls finishWindowsDrawn which will finalize our screen turn on
// as well as enabling the orientation change logic/sensor.
+ Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER,
+ TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD, /* cookie= */ 0);
mWindowManagerInternal.waitForAllWindowsDrawn(() -> {
if (DEBUG_WAKEUP) Slog.i(TAG, "All windows ready for every display");
mHandler.sendMessage(mHandler.obtainMessage(MSG_WINDOW_MANAGER_DRAWN_COMPLETE,
INVALID_DISPLAY, 0));
+
+ Trace.asyncTraceEnd(Trace.TRACE_TAG_WINDOW_MANAGER,
+ TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD, /* cookie= */ 0);
}, WAITING_FOR_DRAWN_TIMEOUT, INVALID_DISPLAY);
}
@@ -4961,10 +4968,16 @@
}
} else {
mScreenOnListeners.put(displayId, screenOnListener);
+
+ Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER,
+ TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD, /* cookie= */ 0);
mWindowManagerInternal.waitForAllWindowsDrawn(() -> {
if (DEBUG_WAKEUP) Slog.i(TAG, "All windows ready for display: " + displayId);
mHandler.sendMessage(mHandler.obtainMessage(MSG_WINDOW_MANAGER_DRAWN_COMPLETE,
displayId, 0));
+
+ Trace.asyncTraceEnd(Trace.TRACE_TAG_WINDOW_MANAGER,
+ TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD, /* cookie= */ 0);
}, WAITING_FOR_DRAWN_TIMEOUT, displayId);
}
}
diff --git a/services/core/java/com/android/server/security/FileIntegrity.java b/services/core/java/com/android/server/security/FileIntegrity.java
new file mode 100644
index 0000000..7b87d99
--- /dev/null
+++ b/services/core/java/com/android/server/security/FileIntegrity.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.security;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.ParcelFileDescriptor;
+
+import com.android.internal.security.VerityUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+
+/**
+ * In-process API for server side FileIntegrity related infrastructure.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
+public final class FileIntegrity {
+ private FileIntegrity() {}
+
+ /**
+ * Enables fs-verity, if supported by the filesystem.
+ * @see <a href="https://www.kernel.org/doc/html/latest/filesystems/fsverity.html">
+ * @hide
+ */
+ @SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
+ public static void setUpFsVerity(@NonNull File file) throws IOException {
+ VerityUtils.setUpFsverity(file.getAbsolutePath());
+ }
+
+ /**
+ * Enables fs-verity, if supported by the filesystem.
+ * @see <a href="https://www.kernel.org/doc/html/latest/filesystems/fsverity.html">
+ * @hide
+ */
+ @SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
+ public static void setUpFsVerity(@NonNull ParcelFileDescriptor parcelFileDescriptor)
+ throws IOException {
+ VerityUtils.setUpFsverity(parcelFileDescriptor.getFd());
+ }
+}
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
index 6d39177..a694f5f 100644
--- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
@@ -863,8 +863,9 @@
*/
private void enforceObserveSensorPrivacyPermission() {
String systemUIPackage = mContext.getString(R.string.config_systemUi);
- if (Binder.getCallingUid() == mPackageManagerInternal
- .getPackageUid(systemUIPackage, MATCH_SYSTEM_ONLY, UserHandle.USER_SYSTEM)) {
+ int systemUIAppId = UserHandle.getAppId(mPackageManagerInternal
+ .getPackageUid(systemUIPackage, MATCH_SYSTEM_ONLY, UserHandle.USER_SYSTEM));
+ if (UserHandle.getCallingAppId() == systemUIAppId) {
// b/221782106, possible race condition with role grant might bootloop device.
return;
}
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index 5521384..ec052ec 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -21,7 +21,6 @@
import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
import android.os.Bundle;
import android.os.IBinder;
-import android.view.InsetsState.InternalInsetsType;
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsController.Appearance;
import android.view.WindowInsetsController.Behavior;
@@ -162,11 +161,10 @@
LetterboxDetails[] letterboxDetails);
/** @see com.android.internal.statusbar.IStatusBar#showTransient */
- void showTransient(int displayId, @InternalInsetsType int[] types,
- boolean isGestureOnSystemBar);
+ void showTransient(int displayId, @InsetsType int types, boolean isGestureOnSystemBar);
/** @see com.android.internal.statusbar.IStatusBar#abortTransient */
- void abortTransient(int displayId, @InternalInsetsType int[] types);
+ void abortTransient(int displayId, @InsetsType int types);
/**
* @see com.android.internal.statusbar.IStatusBar#showToast(String, IBinder, CharSequence,
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 83f4805..4489ba9 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -79,12 +79,10 @@
import android.service.quicksettings.TileService;
import android.text.TextUtils;
import android.util.ArrayMap;
-import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
-import android.view.InsetsState.InternalInsetsType;
import android.view.WindowInsets;
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsController.Appearance;
@@ -645,7 +643,7 @@
}
@Override
- public void showTransient(int displayId, @InternalInsetsType int[] types,
+ public void showTransient(int displayId, @InsetsType int types,
boolean isGestureOnSystemBar) {
getUiState(displayId).showTransient(types);
if (mBar != null) {
@@ -656,7 +654,7 @@
}
@Override
- public void abortTransient(int displayId, @InternalInsetsType int[] types) {
+ public void abortTransient(int displayId, @InsetsType int types) {
getUiState(displayId).clearTransient(types);
if (mBar != null) {
try {
@@ -1258,7 +1256,7 @@
private static class UiState {
private @Appearance int mAppearance = 0;
private AppearanceRegion[] mAppearanceRegions = new AppearanceRegion[0];
- private final ArraySet<Integer> mTransientBarTypes = new ArraySet<>();
+ private @InsetsType int mTransientBarTypes;
private boolean mNavbarColorManagedByIme = false;
private @Behavior int mBehavior;
private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
@@ -1285,16 +1283,12 @@
mLetterboxDetails = letterboxDetails;
}
- private void showTransient(@InternalInsetsType int[] types) {
- for (int type : types) {
- mTransientBarTypes.add(type);
- }
+ private void showTransient(@InsetsType int types) {
+ mTransientBarTypes |= types;
}
- private void clearTransient(@InternalInsetsType int[] types) {
- for (int type : types) {
- mTransientBarTypes.remove(type);
- }
+ private void clearTransient(@InsetsType int types) {
+ mTransientBarTypes &= ~types;
}
private int getDisabled1() {
@@ -1410,16 +1404,12 @@
// TODO(b/118592525): Currently, status bar only works on the default display.
// Make it aware of multi-display if needed.
final UiState state = mDisplayUiState.get(DEFAULT_DISPLAY);
- final int[] transientBarTypes = new int[state.mTransientBarTypes.size()];
- for (int i = 0; i < transientBarTypes.length; i++) {
- transientBarTypes[i] = state.mTransientBarTypes.valueAt(i);
- }
return new RegisterStatusBarResult(icons, gatherDisableActionsLocked(mCurrentUserId, 1),
state.mAppearance, state.mAppearanceRegions, state.mImeWindowVis,
state.mImeBackDisposition, state.mShowImeSwitcher,
gatherDisableActionsLocked(mCurrentUserId, 2), state.mImeToken,
state.mNavbarColorManagedByIme, state.mBehavior, state.mRequestedVisibleTypes,
- state.mPackageName, transientBarTypes, state.mLetterboxDetails);
+ state.mPackageName, state.mTransientBarTypes, state.mLetterboxDetails);
}
}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperData.java b/services/core/java/com/android/server/wallpaper/WallpaperData.java
index 625e7d9..6763514 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperData.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperData.java
@@ -30,7 +30,7 @@
import android.content.ComponentName;
import android.graphics.Rect;
import android.os.RemoteCallbackList;
-import android.util.ArrayMap;
+import android.util.SparseArray;
import java.io.File;
@@ -115,7 +115,7 @@
* A map to keep track of the dimming set by different applications. The key is the calling
* UID and the value is the dim amount.
*/
- ArrayMap<Integer, Float> mUidToDimAmount = new ArrayMap<>();
+ SparseArray<Float> mUidToDimAmount = new SparseArray<>();
/**
* Whether we need to extract the wallpaper colors again to calculate the dark hints
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
new file mode 100644
index 0000000..07a7837
--- /dev/null
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
@@ -0,0 +1,550 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wallpaper;
+
+import static android.app.WallpaperManager.FLAG_LOCK;
+import static android.app.WallpaperManager.FLAG_SYSTEM;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.server.wallpaper.WallpaperDisplayHelper.DisplayData;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_CROP;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_INFO;
+import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir;
+import static com.android.server.wallpaper.WallpaperUtils.makeWallpaperIdLocked;
+
+import android.annotation.Nullable;
+import android.app.WallpaperColors;
+import android.app.backup.WallpaperBackupHelper;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.os.FileUtils;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.Xml;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.JournaledFile;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import libcore.io.IoUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Helper for the wallpaper loading / saving / xml parsing
+ * Only meant to be used lock held by WallpaperManagerService
+ * Only meant to be instantiated once by WallpaperManagerService
+ */
+class WallpaperDataParser {
+
+ private static final String TAG = WallpaperDataParser.class.getSimpleName();
+ private static final boolean DEBUG = false;
+ private final ComponentName mImageWallpaper;
+ private final WallpaperDisplayHelper mWallpaperDisplayHelper;
+ private final WallpaperCropper mWallpaperCropper;
+ private final Context mContext;
+
+ WallpaperDataParser(Context context, WallpaperDisplayHelper wallpaperDisplayHelper,
+ WallpaperCropper wallpaperCropper) {
+ mContext = context;
+ mWallpaperDisplayHelper = wallpaperDisplayHelper;
+ mWallpaperCropper = wallpaperCropper;
+ mImageWallpaper = ComponentName.unflattenFromString(
+ context.getResources().getString(R.string.image_wallpaper_component));
+ }
+
+ private JournaledFile makeJournaledFile(int userId) {
+ final String base = new File(getWallpaperDir(userId), WALLPAPER_INFO).getAbsolutePath();
+ return new JournaledFile(new File(base), new File(base + ".tmp"));
+ }
+
+ static class WallpaperLoadingResult {
+
+ private final WallpaperData mSystemWallpaperData;
+
+ @Nullable
+ private final WallpaperData mLockWallpaperData;
+
+ private final boolean mSuccess;
+
+ private WallpaperLoadingResult(
+ WallpaperData systemWallpaperData,
+ WallpaperData lockWallpaperData,
+ boolean success) {
+ mSystemWallpaperData = systemWallpaperData;
+ mLockWallpaperData = lockWallpaperData;
+ mSuccess = success;
+ }
+
+ public WallpaperData getSystemWallpaperData() {
+ return mSystemWallpaperData;
+ }
+
+ public WallpaperData getLockWallpaperData() {
+ return mLockWallpaperData;
+ }
+
+ public boolean success() {
+ return mSuccess;
+ }
+ }
+
+ /**
+ * Load the system wallpaper (and the lock wallpaper, if it exists) from disk
+ * @param userId the id of the user for which the wallpaper should be loaded
+ * @param keepDimensionHints if false, parse and set the
+ * {@link DisplayData} width and height for the specified userId
+ * @param wallpaper the wallpaper object to reuse to do the modifications.
+ * If null, a new object will be created.
+ * @param lockWallpaper the lock wallpaper object to reuse to do the modifications.
+ * If null, a new object will be created.
+ * @return a {@link WallpaperLoadingResult} object containing the wallpaper data.
+ * This object will contain the {@code wallpaper} and
+ * {@code lockWallpaper} provided as parameters, if they are not null.
+ */
+ public WallpaperLoadingResult loadSettingsLocked(int userId, boolean keepDimensionHints,
+ WallpaperData wallpaper, WallpaperData lockWallpaper) {
+ JournaledFile journal = makeJournaledFile(userId);
+ FileInputStream stream = null;
+ File file = journal.chooseForRead();
+
+ if (wallpaper == null) {
+ // Do this once per boot
+ migrateFromOld();
+ wallpaper = new WallpaperData(userId, FLAG_SYSTEM);
+ wallpaper.allowBackup = true;
+ if (!wallpaper.cropExists()) {
+ if (wallpaper.sourceExists()) {
+ mWallpaperCropper.generateCrop(wallpaper);
+ } else {
+ Slog.i(TAG, "No static wallpaper imagery; defaults will be shown");
+ }
+ }
+ }
+
+ final DisplayData wpdData = mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY);
+ boolean success = false;
+
+ try {
+ stream = new FileInputStream(file);
+ TypedXmlPullParser parser = Xml.resolvePullParser(stream);
+
+ int type;
+ do {
+ type = parser.next();
+ if (type == XmlPullParser.START_TAG) {
+ String tag = parser.getName();
+ if ("wp".equals(tag)) {
+ // Common to system + lock wallpapers
+ parseWallpaperAttributes(parser, wallpaper, keepDimensionHints);
+
+ // A system wallpaper might also be a live wallpaper
+ String comp = parser.getAttributeValue(null, "component");
+ wallpaper.nextWallpaperComponent = comp != null
+ ? ComponentName.unflattenFromString(comp)
+ : null;
+ if (wallpaper.nextWallpaperComponent == null
+ || "android".equals(wallpaper.nextWallpaperComponent
+ .getPackageName())) {
+ wallpaper.nextWallpaperComponent = mImageWallpaper;
+ }
+
+ if (DEBUG) {
+ Slog.v(TAG, "mWidth:" + wpdData.mWidth);
+ Slog.v(TAG, "mHeight:" + wpdData.mHeight);
+ Slog.v(TAG, "cropRect:" + wallpaper.cropHint);
+ Slog.v(TAG, "primaryColors:" + wallpaper.primaryColors);
+ Slog.v(TAG, "mName:" + wallpaper.name);
+ Slog.v(TAG, "mNextWallpaperComponent:"
+ + wallpaper.nextWallpaperComponent);
+ }
+ } else if ("kwp".equals(tag)) {
+ // keyguard-specific wallpaper for this user
+
+ if (lockWallpaper == null) {
+ lockWallpaper = new WallpaperData(userId, FLAG_LOCK);
+ }
+ parseWallpaperAttributes(parser, lockWallpaper, false);
+ }
+ }
+ } while (type != XmlPullParser.END_DOCUMENT);
+ success = true;
+ } catch (FileNotFoundException e) {
+ Slog.w(TAG, "no current wallpaper -- first boot?");
+ } catch (NullPointerException e) {
+ Slog.w(TAG, "failed parsing " + file + " " + e);
+ } catch (NumberFormatException e) {
+ Slog.w(TAG, "failed parsing " + file + " " + e);
+ } catch (XmlPullParserException e) {
+ Slog.w(TAG, "failed parsing " + file + " " + e);
+ } catch (IOException e) {
+ Slog.w(TAG, "failed parsing " + file + " " + e);
+ } catch (IndexOutOfBoundsException e) {
+ Slog.w(TAG, "failed parsing " + file + " " + e);
+ }
+ IoUtils.closeQuietly(stream);
+
+ if (!success) {
+ wallpaper.cropHint.set(0, 0, 0, 0);
+ wpdData.mPadding.set(0, 0, 0, 0);
+ wallpaper.name = "";
+ lockWallpaper = null;
+ } else {
+ if (wallpaper.wallpaperId <= 0) {
+ wallpaper.wallpaperId = makeWallpaperIdLocked();
+ if (DEBUG) {
+ Slog.w(TAG, "Didn't set wallpaper id in loadSettingsLocked(" + userId
+ + "); now " + wallpaper.wallpaperId);
+ }
+ }
+ }
+
+ mWallpaperDisplayHelper.ensureSaneWallpaperDisplaySize(wpdData, DEFAULT_DISPLAY);
+ ensureSaneWallpaperData(wallpaper);
+ if (lockWallpaper != null) {
+ ensureSaneWallpaperData(lockWallpaper);
+ lockWallpaper.mWhich = FLAG_LOCK;
+ wallpaper.mWhich = FLAG_SYSTEM;
+ } else {
+ wallpaper.mWhich = FLAG_SYSTEM | FLAG_LOCK;
+ }
+
+ return new WallpaperLoadingResult(wallpaper, lockWallpaper, success);
+ }
+
+ private void ensureSaneWallpaperData(WallpaperData wallpaper) {
+ // Only overwrite cropHint if the rectangle is invalid.
+ if (wallpaper.cropHint.width() < 0
+ || wallpaper.cropHint.height() < 0) {
+ wallpaper.cropHint.set(0, 0, 0, 0);
+ }
+ }
+
+
+ private void migrateFromOld() {
+ // Pre-N, what existed is the one we're now using as the display crop
+ File preNWallpaper = new File(getWallpaperDir(0), WALLPAPER_CROP);
+ // In the very-long-ago, imagery lived with the settings app
+ File originalWallpaper = new File(WallpaperBackupHelper.WALLPAPER_IMAGE_KEY);
+ File newWallpaper = new File(getWallpaperDir(0), WALLPAPER);
+
+ // Migrations from earlier wallpaper image storage schemas
+ if (preNWallpaper.exists()) {
+ if (!newWallpaper.exists()) {
+ // we've got the 'wallpaper' crop file but not the nominal source image,
+ // so do the simple "just take everything" straight copy of legacy data
+ if (DEBUG) {
+ Slog.i(TAG, "Migrating wallpaper schema");
+ }
+ FileUtils.copyFile(preNWallpaper, newWallpaper);
+ } // else we're in the usual modern case: both source & crop exist
+ } else if (originalWallpaper.exists()) {
+ // VERY old schema; make sure things exist and are in the right place
+ if (DEBUG) {
+ Slog.i(TAG, "Migrating antique wallpaper schema");
+ }
+ File oldInfo = new File(WallpaperBackupHelper.WALLPAPER_INFO_KEY);
+ if (oldInfo.exists()) {
+ File newInfo = new File(getWallpaperDir(0), WALLPAPER_INFO);
+ oldInfo.renameTo(newInfo);
+ }
+
+ FileUtils.copyFile(originalWallpaper, preNWallpaper);
+ originalWallpaper.renameTo(newWallpaper);
+ }
+ }
+
+ @VisibleForTesting
+ void parseWallpaperAttributes(TypedXmlPullParser parser, WallpaperData wallpaper,
+ boolean keepDimensionHints) throws XmlPullParserException {
+ final int id = parser.getAttributeInt(null, "id", -1);
+ if (id != -1) {
+ wallpaper.wallpaperId = id;
+ if (id > WallpaperUtils.getCurrentWallpaperId()) {
+ WallpaperUtils.setCurrentWallpaperId(id);
+ }
+ } else {
+ wallpaper.wallpaperId = makeWallpaperIdLocked();
+ }
+
+ final DisplayData wpData = mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY);
+
+ if (!keepDimensionHints) {
+ wpData.mWidth = parser.getAttributeInt(null, "width");
+ wpData.mHeight = parser.getAttributeInt(null, "height");
+ }
+ wallpaper.cropHint.left = getAttributeInt(parser, "cropLeft", 0);
+ wallpaper.cropHint.top = getAttributeInt(parser, "cropTop", 0);
+ wallpaper.cropHint.right = getAttributeInt(parser, "cropRight", 0);
+ wallpaper.cropHint.bottom = getAttributeInt(parser, "cropBottom", 0);
+ wpData.mPadding.left = getAttributeInt(parser, "paddingLeft", 0);
+ wpData.mPadding.top = getAttributeInt(parser, "paddingTop", 0);
+ wpData.mPadding.right = getAttributeInt(parser, "paddingRight", 0);
+ wpData.mPadding.bottom = getAttributeInt(parser, "paddingBottom", 0);
+ wallpaper.mWallpaperDimAmount = getAttributeFloat(parser, "dimAmount", 0f);
+ int dimAmountsCount = getAttributeInt(parser, "dimAmountsCount", 0);
+ if (dimAmountsCount > 0) {
+ SparseArray<Float> allDimAmounts = new SparseArray<>(dimAmountsCount);
+ for (int i = 0; i < dimAmountsCount; i++) {
+ int uid = getAttributeInt(parser, "dimUID" + i, 0);
+ float dimValue = getAttributeFloat(parser, "dimValue" + i, 0f);
+ allDimAmounts.put(uid, dimValue);
+ }
+ wallpaper.mUidToDimAmount = allDimAmounts;
+ }
+ int colorsCount = getAttributeInt(parser, "colorsCount", 0);
+ int allColorsCount = getAttributeInt(parser, "allColorsCount", 0);
+ if (allColorsCount > 0) {
+ Map<Integer, Integer> allColors = new HashMap<>(allColorsCount);
+ for (int i = 0; i < allColorsCount; i++) {
+ int colorInt = getAttributeInt(parser, "allColorsValue" + i, 0);
+ int population = getAttributeInt(parser, "allColorsPopulation" + i, 0);
+ allColors.put(colorInt, population);
+ }
+ int colorHints = getAttributeInt(parser, "colorHints", 0);
+ wallpaper.primaryColors = new WallpaperColors(allColors, colorHints);
+ } else if (colorsCount > 0) {
+ Color primary = null, secondary = null, tertiary = null;
+ for (int i = 0; i < colorsCount; i++) {
+ Color color = Color.valueOf(getAttributeInt(parser, "colorValue" + i, 0));
+ if (i == 0) {
+ primary = color;
+ } else if (i == 1) {
+ secondary = color;
+ } else if (i == 2) {
+ tertiary = color;
+ } else {
+ break;
+ }
+ }
+ int colorHints = getAttributeInt(parser, "colorHints", 0);
+ wallpaper.primaryColors = new WallpaperColors(primary, secondary, tertiary, colorHints);
+ }
+ wallpaper.name = parser.getAttributeValue(null, "name");
+ wallpaper.allowBackup = parser.getAttributeBoolean(null, "backup", false);
+ }
+
+ private int getAttributeInt(TypedXmlPullParser parser, String name, int defValue) {
+ return parser.getAttributeInt(null, name, defValue);
+ }
+
+ private float getAttributeFloat(TypedXmlPullParser parser, String name, float defValue) {
+ return parser.getAttributeFloat(null, name, defValue);
+ }
+
+ void saveSettingsLocked(int userId, WallpaperData wallpaper, WallpaperData lockWallpaper) {
+ JournaledFile journal = makeJournaledFile(userId);
+ FileOutputStream fstream = null;
+ try {
+ fstream = new FileOutputStream(journal.chooseForWrite(), false);
+ TypedXmlSerializer out = Xml.resolveSerializer(fstream);
+ out.startDocument(null, true);
+
+ if (wallpaper != null) {
+ writeWallpaperAttributes(out, "wp", wallpaper);
+ }
+
+ if (lockWallpaper != null) {
+ writeWallpaperAttributes(out, "kwp", lockWallpaper);
+ }
+
+ out.endDocument();
+
+ fstream.flush();
+ FileUtils.sync(fstream);
+ fstream.close();
+ journal.commit();
+ } catch (IOException e) {
+ IoUtils.closeQuietly(fstream);
+ journal.rollback();
+ }
+ }
+
+ @VisibleForTesting
+ void writeWallpaperAttributes(TypedXmlSerializer out, String tag, WallpaperData wallpaper)
+ throws IllegalArgumentException, IllegalStateException, IOException {
+ if (DEBUG) {
+ Slog.v(TAG, "writeWallpaperAttributes id=" + wallpaper.wallpaperId);
+ }
+ final DisplayData wpdData = mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY);
+ out.startTag(null, tag);
+ out.attributeInt(null, "id", wallpaper.wallpaperId);
+ out.attributeInt(null, "width", wpdData.mWidth);
+ out.attributeInt(null, "height", wpdData.mHeight);
+
+ out.attributeInt(null, "cropLeft", wallpaper.cropHint.left);
+ out.attributeInt(null, "cropTop", wallpaper.cropHint.top);
+ out.attributeInt(null, "cropRight", wallpaper.cropHint.right);
+ out.attributeInt(null, "cropBottom", wallpaper.cropHint.bottom);
+
+ if (wpdData.mPadding.left != 0) {
+ out.attributeInt(null, "paddingLeft", wpdData.mPadding.left);
+ }
+ if (wpdData.mPadding.top != 0) {
+ out.attributeInt(null, "paddingTop", wpdData.mPadding.top);
+ }
+ if (wpdData.mPadding.right != 0) {
+ out.attributeInt(null, "paddingRight", wpdData.mPadding.right);
+ }
+ if (wpdData.mPadding.bottom != 0) {
+ out.attributeInt(null, "paddingBottom", wpdData.mPadding.bottom);
+ }
+
+ out.attributeFloat(null, "dimAmount", wallpaper.mWallpaperDimAmount);
+ int dimAmountsCount = wallpaper.mUidToDimAmount.size();
+ out.attributeInt(null, "dimAmountsCount", dimAmountsCount);
+ if (dimAmountsCount > 0) {
+ int index = 0;
+ for (int i = 0; i < wallpaper.mUidToDimAmount.size(); i++) {
+ out.attributeInt(null, "dimUID" + index, wallpaper.mUidToDimAmount.keyAt(i));
+ out.attributeFloat(null, "dimValue" + index, wallpaper.mUidToDimAmount.valueAt(i));
+ index++;
+ }
+ }
+
+ if (wallpaper.primaryColors != null) {
+ int colorsCount = wallpaper.primaryColors.getMainColors().size();
+ out.attributeInt(null, "colorsCount", colorsCount);
+ if (colorsCount > 0) {
+ for (int i = 0; i < colorsCount; i++) {
+ final Color wc = wallpaper.primaryColors.getMainColors().get(i);
+ out.attributeInt(null, "colorValue" + i, wc.toArgb());
+ }
+ }
+
+ int allColorsCount = wallpaper.primaryColors.getAllColors().size();
+ out.attributeInt(null, "allColorsCount", allColorsCount);
+ if (allColorsCount > 0) {
+ int index = 0;
+ for (Map.Entry<Integer, Integer> entry : wallpaper.primaryColors.getAllColors()
+ .entrySet()) {
+ out.attributeInt(null, "allColorsValue" + index, entry.getKey());
+ out.attributeInt(null, "allColorsPopulation" + index, entry.getValue());
+ index++;
+ }
+ }
+
+ out.attributeInt(null, "colorHints", wallpaper.primaryColors.getColorHints());
+ }
+
+ out.attribute(null, "name", wallpaper.name);
+ if (wallpaper.wallpaperComponent != null
+ && !wallpaper.wallpaperComponent.equals(mImageWallpaper)) {
+ out.attribute(null, "component",
+ wallpaper.wallpaperComponent.flattenToShortString());
+ }
+
+ if (wallpaper.allowBackup) {
+ out.attributeBoolean(null, "backup", true);
+ }
+
+ out.endTag(null, tag);
+ }
+
+ // Restore the named resource bitmap to both source + crop files
+ boolean restoreNamedResourceLocked(WallpaperData wallpaper) {
+ if (wallpaper.name.length() > 4 && "res:".equals(wallpaper.name.substring(0, 4))) {
+ String resName = wallpaper.name.substring(4);
+
+ String pkg = null;
+ int colon = resName.indexOf(':');
+ if (colon > 0) {
+ pkg = resName.substring(0, colon);
+ }
+
+ String ident = null;
+ int slash = resName.lastIndexOf('/');
+ if (slash > 0) {
+ ident = resName.substring(slash + 1);
+ }
+
+ String type = null;
+ if (colon > 0 && slash > 0 && (slash - colon) > 1) {
+ type = resName.substring(colon + 1, slash);
+ }
+
+ if (pkg != null && ident != null && type != null) {
+ int resId = -1;
+ InputStream res = null;
+ FileOutputStream fos = null;
+ FileOutputStream cos = null;
+ try {
+ Context c = mContext.createPackageContext(pkg, Context.CONTEXT_RESTRICTED);
+ Resources r = c.getResources();
+ resId = r.getIdentifier(resName, null, null);
+ if (resId == 0) {
+ Slog.e(TAG, "couldn't resolve identifier pkg=" + pkg + " type=" + type
+ + " ident=" + ident);
+ return false;
+ }
+
+ res = r.openRawResource(resId);
+ if (wallpaper.wallpaperFile.exists()) {
+ wallpaper.wallpaperFile.delete();
+ wallpaper.cropFile.delete();
+ }
+ fos = new FileOutputStream(wallpaper.wallpaperFile);
+ cos = new FileOutputStream(wallpaper.cropFile);
+
+ byte[] buffer = new byte[32768];
+ int amt;
+ while ((amt = res.read(buffer)) > 0) {
+ fos.write(buffer, 0, amt);
+ cos.write(buffer, 0, amt);
+ }
+ // mWallpaperObserver will notice the close and send the change broadcast
+
+ Slog.v(TAG, "Restored wallpaper: " + resName);
+ return true;
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.e(TAG, "Package name " + pkg + " not found");
+ } catch (Resources.NotFoundException e) {
+ Slog.e(TAG, "Resource not found: " + resId);
+ } catch (IOException e) {
+ Slog.e(TAG, "IOException while restoring wallpaper ", e);
+ } finally {
+ IoUtils.closeQuietly(res);
+ if (fos != null) {
+ FileUtils.sync(fos);
+ }
+ if (cos != null) {
+ FileUtils.sync(cos);
+ }
+ IoUtils.closeQuietly(fos);
+ IoUtils.closeQuietly(cos);
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
index f02ee66..3f02266 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
@@ -30,6 +30,7 @@
import com.android.server.wm.WindowManagerInternal;
import java.util.function.Consumer;
+
/**
* Internal class used to store all the display data relevant to the wallpapers
*/
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 262b964..198c339 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -17,7 +17,6 @@
package com.android.server.wallpaper;
import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
-import static android.Manifest.permission.QUERY_ALL_PACKAGES;
import static android.Manifest.permission.READ_WALLPAPER_INTERNAL;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
import static android.app.WallpaperManager.COMMAND_REAPPLY;
@@ -36,7 +35,6 @@
import static com.android.server.wallpaper.WallpaperUtils.RECORD_FILE;
import static com.android.server.wallpaper.WallpaperUtils.RECORD_LOCK_FILE;
import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER;
-import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_CROP;
import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_INFO;
import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_LOCK_ORIG;
import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir;
@@ -57,7 +55,6 @@
import android.app.WallpaperManager;
import android.app.WallpaperManager.SetWallpaperFlags;
import android.app.admin.DevicePolicyManagerInternal;
-import android.app.backup.WallpaperBackupHelper;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -67,13 +64,12 @@
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
-import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
-import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.RectF;
import android.hardware.display.DisplayManager;
@@ -101,12 +97,10 @@
import android.service.wallpaper.WallpaperService;
import android.system.ErrnoException;
import android.system.Os;
-import android.util.ArrayMap;
import android.util.EventLog;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
-import android.util.Xml;
import android.view.Display;
import com.android.internal.R;
@@ -114,9 +108,6 @@
import com.android.internal.content.PackageMonitor;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.DumpUtils;
-import com.android.internal.util.JournaledFile;
-import com.android.modules.utils.TypedXmlPullParser;
-import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.EventLogTags;
import com.android.server.FgThread;
import com.android.server.LocalServices;
@@ -125,22 +116,16 @@
import com.android.server.utils.TimingsTraceAndSlog;
import com.android.server.wm.WindowManagerInternal;
-import libcore.io.IoUtils;
-
-import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.FileDescriptor;
-import java.io.FileInputStream;
import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -152,7 +137,6 @@
private static final String TAG = "WallpaperManagerService";
private static final boolean DEBUG = false;
private static final boolean DEBUG_LIVE = true;
- private static final boolean DEBUG_CROP = true;
private static final @NonNull RectF LOCAL_COLOR_BOUNDS =
new RectF(0, 0, 1, 1);
@@ -759,6 +743,7 @@
private final Context mContext;
private final WindowManagerInternal mWindowManagerInternal;
+ private final PackageManagerInternal mPackageManagerInternal;
private final IPackageManager mIPackageManager;
private final ActivityManager mActivityManager;
private final MyPackageMonitor mMonitor;
@@ -848,6 +833,9 @@
private LocalColorRepository mLocalColorRepo = new LocalColorRepository();
@VisibleForTesting
+ final WallpaperDataParser mWallpaperDataParser;
+
+ @VisibleForTesting
final WallpaperDisplayHelper mWallpaperDisplayHelper;
final WallpaperCropper mWallpaperCropper;
@@ -1604,12 +1592,15 @@
context.getResources().getString(R.string.image_wallpaper_component));
mDefaultWallpaperComponent = WallpaperManager.getDefaultWallpaperComponent(context);
mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
+ mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
mIPackageManager = AppGlobals.getPackageManager();
mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
DisplayManager dm = mContext.getSystemService(DisplayManager.class);
dm.registerDisplayListener(mDisplayListener, null /* handler */);
mWallpaperDisplayHelper = new WallpaperDisplayHelper(dm, mWindowManagerInternal);
mWallpaperCropper = new WallpaperCropper(mWallpaperDisplayHelper);
+ mWallpaperDataParser = new WallpaperDataParser(
+ mContext, mWallpaperDisplayHelper, mWallpaperCropper);
mActivityManager = mContext.getSystemService(ActivityManager.class);
mMonitor = new MyPackageMonitor();
mColorsChangedListeners = new SparseArray<>();
@@ -1897,7 +1888,6 @@
// bound into place
wallpaper.wallpaperComponent = wallpaper.nextWallpaperComponent;
final WallpaperData fallback = new WallpaperData(wallpaper.userId, FLAG_LOCK);
- ensureSaneWallpaperData(fallback);
bindWallpaperComponentLocked(mImageWallpaper, true, false, fallback, reply);
mWaitingForUnlock = true;
}
@@ -2283,20 +2273,23 @@
@Override
public WallpaperInfo getWallpaperInfoWithFlags(@SetWallpaperFlags int which, int userId) {
- final boolean allow =
- hasPermission(READ_WALLPAPER_INTERNAL) || hasPermission(QUERY_ALL_PACKAGES);
- if (allow) {
- userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
- Binder.getCallingUid(), userId, false, true, "getWallpaperInfo", null);
- synchronized (mLock) {
- WallpaperData wallpaper = (which == FLAG_LOCK) ? mLockWallpaperMap.get(userId)
- : mWallpaperMap.get(userId);
- if (wallpaper != null && wallpaper.connection != null) {
- return wallpaper.connection.mInfo;
- }
+
+ userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
+ Binder.getCallingUid(), userId, false, true, "getWallpaperInfo", null);
+ synchronized (mLock) {
+ WallpaperData wallpaper = (which == FLAG_LOCK) ? mLockWallpaperMap.get(userId)
+ : mWallpaperMap.get(userId);
+ if (wallpaper == null
+ || wallpaper.connection == null
+ || wallpaper.connection.mInfo == null) return null;
+
+ WallpaperInfo info = wallpaper.connection.mInfo;
+ if (hasPermission(READ_WALLPAPER_INTERNAL)
+ || mPackageManagerInternal.canQueryPackage(
+ Binder.getCallingUid(), info.getComponent().getPackageName())) {
+ return info;
}
}
-
return null;
}
@@ -2625,12 +2618,10 @@
*
* @param uidToDimAmountMap Map of UIDs to dim amounts
*/
- private float getHighestDimAmountFromMap(ArrayMap<Integer, Float> uidToDimAmountMap) {
+ private float getHighestDimAmountFromMap(SparseArray<Float> uidToDimAmountMap) {
float maxDimAmount = 0.0f;
- for (Map.Entry<Integer, Float> entry : uidToDimAmountMap.entrySet()) {
- if (entry.getValue() > maxDimAmount) {
- maxDimAmount = entry.getValue();
- }
+ for (int i = 0; i < uidToDimAmountMap.size(); i++) {
+ maxDimAmount = Math.max(maxDimAmount, uidToDimAmountMap.valueAt(i));
}
return maxDimAmount;
}
@@ -3427,169 +3418,14 @@
}
}
- private JournaledFile makeJournaledFile(int userId) {
- final String base = new File(getWallpaperDir(userId), WALLPAPER_INFO).getAbsolutePath();
- return new JournaledFile(new File(base), new File(base + ".tmp"));
- }
-
void saveSettingsLocked(int userId) {
TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
t.traceBegin("WPMS.saveSettingsLocked-" + userId);
- JournaledFile journal = makeJournaledFile(userId);
- FileOutputStream fstream = null;
- try {
- fstream = new FileOutputStream(journal.chooseForWrite(), false);
- TypedXmlSerializer out = Xml.resolveSerializer(fstream);
- out.startDocument(null, true);
-
- WallpaperData wallpaper;
-
- wallpaper = mWallpaperMap.get(userId);
- if (wallpaper != null) {
- writeWallpaperAttributes(out, "wp", wallpaper);
- }
- wallpaper = mLockWallpaperMap.get(userId);
- if (wallpaper != null) {
- writeWallpaperAttributes(out, "kwp", wallpaper);
- }
-
- out.endDocument();
-
- fstream.flush();
- FileUtils.sync(fstream);
- fstream.close();
- journal.commit();
- } catch (IOException e) {
- IoUtils.closeQuietly(fstream);
- journal.rollback();
- }
+ mWallpaperDataParser.saveSettingsLocked(
+ userId, mWallpaperMap.get(userId), mLockWallpaperMap.get(userId));
t.traceEnd();
}
-
- @VisibleForTesting
- void writeWallpaperAttributes(TypedXmlSerializer out, String tag,
- WallpaperData wallpaper)
- throws IllegalArgumentException, IllegalStateException, IOException {
- if (DEBUG) {
- Slog.v(TAG, "writeWallpaperAttributes id=" + wallpaper.wallpaperId);
- }
- final DisplayData wpdData = mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY);
- out.startTag(null, tag);
- out.attributeInt(null, "id", wallpaper.wallpaperId);
- out.attributeInt(null, "width", wpdData.mWidth);
- out.attributeInt(null, "height", wpdData.mHeight);
-
- out.attributeInt(null, "cropLeft", wallpaper.cropHint.left);
- out.attributeInt(null, "cropTop", wallpaper.cropHint.top);
- out.attributeInt(null, "cropRight", wallpaper.cropHint.right);
- out.attributeInt(null, "cropBottom", wallpaper.cropHint.bottom);
-
- if (wpdData.mPadding.left != 0) {
- out.attributeInt(null, "paddingLeft", wpdData.mPadding.left);
- }
- if (wpdData.mPadding.top != 0) {
- out.attributeInt(null, "paddingTop", wpdData.mPadding.top);
- }
- if (wpdData.mPadding.right != 0) {
- out.attributeInt(null, "paddingRight", wpdData.mPadding.right);
- }
- if (wpdData.mPadding.bottom != 0) {
- out.attributeInt(null, "paddingBottom", wpdData.mPadding.bottom);
- }
-
- out.attributeFloat(null, "dimAmount", wallpaper.mWallpaperDimAmount);
- int dimAmountsCount = wallpaper.mUidToDimAmount.size();
- out.attributeInt(null, "dimAmountsCount", dimAmountsCount);
- if (dimAmountsCount > 0) {
- int index = 0;
- for (Map.Entry<Integer, Float> entry : wallpaper.mUidToDimAmount.entrySet()) {
- out.attributeInt(null, "dimUID" + index, entry.getKey());
- out.attributeFloat(null, "dimValue" + index, entry.getValue());
- index++;
- }
- }
-
- if (wallpaper.primaryColors != null) {
- int colorsCount = wallpaper.primaryColors.getMainColors().size();
- out.attributeInt(null, "colorsCount", colorsCount);
- if (colorsCount > 0) {
- for (int i = 0; i < colorsCount; i++) {
- final Color wc = wallpaper.primaryColors.getMainColors().get(i);
- out.attributeInt(null, "colorValue" + i, wc.toArgb());
- }
- }
-
- int allColorsCount = wallpaper.primaryColors.getAllColors().size();
- out.attributeInt(null, "allColorsCount", allColorsCount);
- if (allColorsCount > 0) {
- int index = 0;
- for (Map.Entry<Integer, Integer> entry : wallpaper.primaryColors.getAllColors()
- .entrySet()) {
- out.attributeInt(null, "allColorsValue" + index, entry.getKey());
- out.attributeInt(null, "allColorsPopulation" + index, entry.getValue());
- index++;
- }
- }
-
- out.attributeInt(null, "colorHints", wallpaper.primaryColors.getColorHints());
- }
-
- out.attribute(null, "name", wallpaper.name);
- if (wallpaper.wallpaperComponent != null
- && !wallpaper.wallpaperComponent.equals(mImageWallpaper)) {
- out.attribute(null, "component",
- wallpaper.wallpaperComponent.flattenToShortString());
- }
-
- if (wallpaper.allowBackup) {
- out.attributeBoolean(null, "backup", true);
- }
-
- out.endTag(null, tag);
- }
-
- private void migrateFromOld() {
- // Pre-N, what existed is the one we're now using as the display crop
- File preNWallpaper = new File(getWallpaperDir(0), WALLPAPER_CROP);
- // In the very-long-ago, imagery lived with the settings app
- File originalWallpaper = new File(WallpaperBackupHelper.WALLPAPER_IMAGE_KEY);
- File newWallpaper = new File(getWallpaperDir(0), WALLPAPER);
-
- // Migrations from earlier wallpaper image storage schemas
- if (preNWallpaper.exists()) {
- if (!newWallpaper.exists()) {
- // we've got the 'wallpaper' crop file but not the nominal source image,
- // so do the simple "just take everything" straight copy of legacy data
- if (DEBUG) {
- Slog.i(TAG, "Migrating wallpaper schema");
- }
- FileUtils.copyFile(preNWallpaper, newWallpaper);
- } // else we're in the usual modern case: both source & crop exist
- } else if (originalWallpaper.exists()) {
- // VERY old schema; make sure things exist and are in the right place
- if (DEBUG) {
- Slog.i(TAG, "Migrating antique wallpaper schema");
- }
- File oldInfo = new File(WallpaperBackupHelper.WALLPAPER_INFO_KEY);
- if (oldInfo.exists()) {
- File newInfo = new File(getWallpaperDir(0), WALLPAPER_INFO);
- oldInfo.renameTo(newInfo);
- }
-
- FileUtils.copyFile(originalWallpaper, preNWallpaper);
- originalWallpaper.renameTo(newWallpaper);
- }
- }
-
- private int getAttributeInt(TypedXmlPullParser parser, String name, int defValue) {
- return parser.getAttributeInt(null, name, defValue);
- }
-
- private float getAttributeFloat(TypedXmlPullParser parser, String name, float defValue) {
- return parser.getAttributeFloat(null, name, defValue);
- }
-
/**
* Determines and returns the current wallpaper for the given user and destination, creating
* a valid entry if it does not already exist and adding it to the appropriate wallpaper map.
@@ -3624,14 +3460,12 @@
if (which == FLAG_LOCK) {
wallpaper = new WallpaperData(userId, FLAG_LOCK);
mLockWallpaperMap.put(userId, wallpaper);
- ensureSaneWallpaperData(wallpaper);
} else {
// rationality fallback: we're in bad shape, but establishing a known
// valid system+lock WallpaperData will keep us from dying.
Slog.wtf(TAG, "Didn't find wallpaper in non-lock case!");
wallpaper = new WallpaperData(userId, FLAG_SYSTEM);
mWallpaperMap.put(userId, wallpaper);
- ensureSaneWallpaperData(wallpaper);
}
}
}
@@ -3639,114 +3473,17 @@
}
private void loadSettingsLocked(int userId, boolean keepDimensionHints) {
- JournaledFile journal = makeJournaledFile(userId);
- FileInputStream stream = null;
- File file = journal.chooseForRead();
+ initializeFallbackWallpaper();
+ WallpaperData wallpaperData = mWallpaperMap.get(userId);
+ WallpaperData lockWallpaperData = mLockWallpaperMap.get(userId);
+ WallpaperDataParser.WallpaperLoadingResult result = mWallpaperDataParser.loadSettingsLocked(
+ userId, keepDimensionHints, wallpaperData, lockWallpaperData);
- WallpaperData wallpaper = mWallpaperMap.get(userId);
- if (wallpaper == null) {
- // Do this once per boot
- migrateFromOld();
-
- wallpaper = new WallpaperData(userId, FLAG_SYSTEM);
- wallpaper.allowBackup = true;
- mWallpaperMap.put(userId, wallpaper);
- if (!wallpaper.cropExists()) {
- if (wallpaper.sourceExists()) {
- mWallpaperCropper.generateCrop(wallpaper);
- } else {
- Slog.i(TAG, "No static wallpaper imagery; defaults will be shown");
- }
- }
- initializeFallbackWallpaper();
- }
- boolean success = false;
- final DisplayData wpdData = mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY);
- try {
- stream = new FileInputStream(file);
- TypedXmlPullParser parser = Xml.resolvePullParser(stream);
-
- int type;
- do {
- type = parser.next();
- if (type == XmlPullParser.START_TAG) {
- String tag = parser.getName();
- if ("wp".equals(tag)) {
- // Common to system + lock wallpapers
- parseWallpaperAttributes(parser, wallpaper, keepDimensionHints);
-
- // A system wallpaper might also be a live wallpaper
- String comp = parser.getAttributeValue(null, "component");
- wallpaper.nextWallpaperComponent = comp != null
- ? ComponentName.unflattenFromString(comp)
- : null;
- if (wallpaper.nextWallpaperComponent == null
- || "android".equals(wallpaper.nextWallpaperComponent
- .getPackageName())) {
- wallpaper.nextWallpaperComponent = mImageWallpaper;
- }
-
- if (DEBUG) {
- Slog.v(TAG, "mWidth:" + wpdData.mWidth);
- Slog.v(TAG, "mHeight:" + wpdData.mHeight);
- Slog.v(TAG, "cropRect:" + wallpaper.cropHint);
- Slog.v(TAG, "primaryColors:" + wallpaper.primaryColors);
- Slog.v(TAG, "mName:" + wallpaper.name);
- Slog.v(TAG, "mNextWallpaperComponent:"
- + wallpaper.nextWallpaperComponent);
- }
- } else if ("kwp".equals(tag)) {
- // keyguard-specific wallpaper for this user
- WallpaperData lockWallpaper = mLockWallpaperMap.get(userId);
- if (lockWallpaper == null) {
- lockWallpaper = new WallpaperData(userId, FLAG_LOCK);
- mLockWallpaperMap.put(userId, lockWallpaper);
- }
- parseWallpaperAttributes(parser, lockWallpaper, false);
- }
- }
- } while (type != XmlPullParser.END_DOCUMENT);
- success = true;
- } catch (FileNotFoundException e) {
- Slog.w(TAG, "no current wallpaper -- first boot?");
- } catch (NullPointerException e) {
- Slog.w(TAG, "failed parsing " + file + " " + e);
- } catch (NumberFormatException e) {
- Slog.w(TAG, "failed parsing " + file + " " + e);
- } catch (XmlPullParserException e) {
- Slog.w(TAG, "failed parsing " + file + " " + e);
- } catch (IOException e) {
- Slog.w(TAG, "failed parsing " + file + " " + e);
- } catch (IndexOutOfBoundsException e) {
- Slog.w(TAG, "failed parsing " + file + " " + e);
- }
- IoUtils.closeQuietly(stream);
-
- if (!success) {
- wallpaper.cropHint.set(0, 0, 0, 0);
- wpdData.mPadding.set(0, 0, 0, 0);
- wallpaper.name = "";
-
+ mWallpaperMap.put(userId, result.getSystemWallpaperData());
+ if (result.success()) {
+ mLockWallpaperMap.put(userId, result.getLockWallpaperData());
+ } else {
mLockWallpaperMap.remove(userId);
- } else {
- if (wallpaper.wallpaperId <= 0) {
- wallpaper.wallpaperId = makeWallpaperIdLocked();
- if (DEBUG) {
- Slog.w(TAG, "Didn't set wallpaper id in loadSettingsLocked(" + userId
- + "); now " + wallpaper.wallpaperId);
- }
- }
- }
-
- mWallpaperDisplayHelper.ensureSaneWallpaperDisplaySize(wpdData, DEFAULT_DISPLAY);
- ensureSaneWallpaperData(wallpaper);
- WallpaperData lockWallpaper = mLockWallpaperMap.get(userId);
- if (lockWallpaper != null) {
- ensureSaneWallpaperData(lockWallpaper);
- lockWallpaper.mWhich = FLAG_LOCK;
- wallpaper.mWhich = FLAG_SYSTEM;
- } else {
- wallpaper.mWhich = FLAG_SYSTEM | FLAG_LOCK;
}
}
@@ -3761,84 +3498,6 @@
}
}
- private void ensureSaneWallpaperData(WallpaperData wallpaper) {
- // Only overwrite cropHint if the rectangle is invalid.
- if (wallpaper.cropHint.width() < 0
- || wallpaper.cropHint.height() < 0) {
- wallpaper.cropHint.set(0, 0, 0, 0);
- }
- }
-
- @VisibleForTesting
- void parseWallpaperAttributes(TypedXmlPullParser parser, WallpaperData wallpaper,
- boolean keepDimensionHints) throws XmlPullParserException {
- final int id = parser.getAttributeInt(null, "id", -1);
- if (id != -1) {
- wallpaper.wallpaperId = id;
- if (id > WallpaperUtils.getCurrentWallpaperId()) {
- WallpaperUtils.setCurrentWallpaperId(id);
- }
- } else {
- wallpaper.wallpaperId = makeWallpaperIdLocked();
- }
-
- final DisplayData wpData = mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY);
-
- if (!keepDimensionHints) {
- wpData.mWidth = parser.getAttributeInt(null, "width");
- wpData.mHeight = parser.getAttributeInt(null, "height");
- }
- wallpaper.cropHint.left = getAttributeInt(parser, "cropLeft", 0);
- wallpaper.cropHint.top = getAttributeInt(parser, "cropTop", 0);
- wallpaper.cropHint.right = getAttributeInt(parser, "cropRight", 0);
- wallpaper.cropHint.bottom = getAttributeInt(parser, "cropBottom", 0);
- wpData.mPadding.left = getAttributeInt(parser, "paddingLeft", 0);
- wpData.mPadding.top = getAttributeInt(parser, "paddingTop", 0);
- wpData.mPadding.right = getAttributeInt(parser, "paddingRight", 0);
- wpData.mPadding.bottom = getAttributeInt(parser, "paddingBottom", 0);
- wallpaper.mWallpaperDimAmount = getAttributeFloat(parser, "dimAmount", 0f);
- int dimAmountsCount = getAttributeInt(parser, "dimAmountsCount", 0);
- if (dimAmountsCount > 0) {
- ArrayMap<Integer, Float> allDimAmounts = new ArrayMap<>(dimAmountsCount);
- for (int i = 0; i < dimAmountsCount; i++) {
- int uid = getAttributeInt(parser, "dimUID" + i, 0);
- float dimValue = getAttributeFloat(parser, "dimValue" + i, 0f);
- allDimAmounts.put(uid, dimValue);
- }
- wallpaper.mUidToDimAmount = allDimAmounts;
- }
- int colorsCount = getAttributeInt(parser, "colorsCount", 0);
- int allColorsCount = getAttributeInt(parser, "allColorsCount", 0);
- if (allColorsCount > 0) {
- Map<Integer, Integer> allColors = new HashMap<>(allColorsCount);
- for (int i = 0; i < allColorsCount; i++) {
- int colorInt = getAttributeInt(parser, "allColorsValue" + i, 0);
- int population = getAttributeInt(parser, "allColorsPopulation" + i, 0);
- allColors.put(colorInt, population);
- }
- int colorHints = getAttributeInt(parser, "colorHints", 0);
- wallpaper.primaryColors = new WallpaperColors(allColors, colorHints);
- } else if (colorsCount > 0) {
- Color primary = null, secondary = null, tertiary = null;
- for (int i = 0; i < colorsCount; i++) {
- Color color = Color.valueOf(getAttributeInt(parser, "colorValue" + i, 0));
- if (i == 0) {
- primary = color;
- } else if (i == 1) {
- secondary = color;
- } else if (i == 2) {
- tertiary = color;
- } else {
- break;
- }
- }
- int colorHints = getAttributeInt(parser, "colorHints", 0);
- wallpaper.primaryColors = new WallpaperColors(primary, secondary, tertiary, colorHints);
- }
- wallpaper.name = parser.getAttributeValue(null, "name");
- wallpaper.allowBackup = parser.getAttributeBoolean(null, "backup", false);
- }
-
// Called by SystemBackupAgent after files are restored to disk.
public void settingsRestored() {
// Verify caller is the system
@@ -3873,7 +3532,7 @@
success = true;
} else {
if (DEBUG) Slog.v(TAG, "settingsRestored: attempting to restore named resource");
- success = restoreNamedResourceLocked(wallpaper);
+ success = mWallpaperDataParser.restoreNamedResourceLocked(wallpaper);
}
if (DEBUG) Slog.v(TAG, "settingsRestored: success=" + success
+ " id=" + wallpaper.wallpaperId);
@@ -3896,83 +3555,6 @@
}
}
- // Restore the named resource bitmap to both source + crop files
- private boolean restoreNamedResourceLocked(WallpaperData wallpaper) {
- if (wallpaper.name.length() > 4 && "res:".equals(wallpaper.name.substring(0, 4))) {
- String resName = wallpaper.name.substring(4);
-
- String pkg = null;
- int colon = resName.indexOf(':');
- if (colon > 0) {
- pkg = resName.substring(0, colon);
- }
-
- String ident = null;
- int slash = resName.lastIndexOf('/');
- if (slash > 0) {
- ident = resName.substring(slash+1);
- }
-
- String type = null;
- if (colon > 0 && slash > 0 && (slash-colon) > 1) {
- type = resName.substring(colon+1, slash);
- }
-
- if (pkg != null && ident != null && type != null) {
- int resId = -1;
- InputStream res = null;
- FileOutputStream fos = null;
- FileOutputStream cos = null;
- try {
- Context c = mContext.createPackageContext(pkg, Context.CONTEXT_RESTRICTED);
- Resources r = c.getResources();
- resId = r.getIdentifier(resName, null, null);
- if (resId == 0) {
- Slog.e(TAG, "couldn't resolve identifier pkg=" + pkg + " type=" + type
- + " ident=" + ident);
- return false;
- }
-
- res = r.openRawResource(resId);
- if (wallpaper.wallpaperFile.exists()) {
- wallpaper.wallpaperFile.delete();
- wallpaper.cropFile.delete();
- }
- fos = new FileOutputStream(wallpaper.wallpaperFile);
- cos = new FileOutputStream(wallpaper.cropFile);
-
- byte[] buffer = new byte[32768];
- int amt;
- while ((amt=res.read(buffer)) > 0) {
- fos.write(buffer, 0, amt);
- cos.write(buffer, 0, amt);
- }
- // mWallpaperObserver will notice the close and send the change broadcast
-
- Slog.v(TAG, "Restored wallpaper: " + resName);
- return true;
- } catch (NameNotFoundException e) {
- Slog.e(TAG, "Package name " + pkg + " not found");
- } catch (Resources.NotFoundException e) {
- Slog.e(TAG, "Resource not found: " + resId);
- } catch (IOException e) {
- Slog.e(TAG, "IOException while restoring wallpaper ", e);
- } finally {
- IoUtils.closeQuietly(res);
- if (fos != null) {
- FileUtils.sync(fos);
- }
- if (cos != null) {
- FileUtils.sync(cos);
- }
- IoUtils.closeQuietly(fos);
- IoUtils.closeQuietly(cos);
- }
- }
- }
- return false;
- }
-
@Override // Binder call
public void onShellCommand(FileDescriptor in, FileDescriptor out,
FileDescriptor err, String[] args, ShellCallback callback,
@@ -3981,6 +3563,56 @@
args, callback, resultReceiver);
}
+ private void dumpWallpaper(WallpaperData wallpaper, PrintWriter pw) {
+ pw.print(" User "); pw.print(wallpaper.userId);
+ pw.print(": id="); pw.print(wallpaper.wallpaperId);
+ pw.print(": mWhich="); pw.print(wallpaper.mWhich);
+ pw.print(": mSystemWasBoth="); pw.println(wallpaper.mSystemWasBoth);
+ pw.println(" Display state:");
+ mWallpaperDisplayHelper.forEachDisplayData(wpSize -> {
+ pw.print(" displayId=");
+ pw.println(wpSize.mDisplayId);
+ pw.print(" mWidth=");
+ pw.print(wpSize.mWidth);
+ pw.print(" mHeight=");
+ pw.println(wpSize.mHeight);
+ pw.print(" mPadding="); pw.println(wpSize.mPadding);
+ });
+ pw.print(" mCropHint="); pw.println(wallpaper.cropHint);
+ pw.print(" mName="); pw.println(wallpaper.name);
+ pw.print(" mAllowBackup="); pw.println(wallpaper.allowBackup);
+ pw.print(" mWallpaperComponent="); pw.println(wallpaper.wallpaperComponent);
+ pw.print(" mWallpaperDimAmount="); pw.println(wallpaper.mWallpaperDimAmount);
+ pw.print(" isColorExtracted="); pw.println(wallpaper.mIsColorExtractedFromDim);
+ pw.println(" mUidToDimAmount:");
+ for (int j = 0; j < wallpaper.mUidToDimAmount.size(); j++) {
+ pw.print(" UID="); pw.print(wallpaper.mUidToDimAmount.keyAt(j));
+ pw.print(" dimAmount="); pw.println(wallpaper.mUidToDimAmount.valueAt(j));
+ }
+ if (wallpaper.connection != null) {
+ WallpaperConnection conn = wallpaper.connection;
+ pw.print(" Wallpaper connection ");
+ pw.print(conn);
+ pw.println(":");
+ if (conn.mInfo != null) {
+ pw.print(" mInfo.component=");
+ pw.println(conn.mInfo.getComponent());
+ }
+ conn.forEachDisplayConnector(connector -> {
+ pw.print(" mDisplayId=");
+ pw.println(connector.mDisplayId);
+ pw.print(" mToken=");
+ pw.println(connector.mToken);
+ pw.print(" mEngine=");
+ pw.println(connector.mEngine);
+ });
+ pw.print(" mService=");
+ pw.println(conn.mService);
+ pw.print(" mLastDiedTime=");
+ pw.println(wallpaper.lastDiedTime - SystemClock.uptimeMillis());
+ }
+ }
+
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
@@ -3991,90 +3623,15 @@
synchronized (mLock) {
pw.println("System wallpaper state:");
for (int i = 0; i < mWallpaperMap.size(); i++) {
- WallpaperData wallpaper = mWallpaperMap.valueAt(i);
- pw.print(" User "); pw.print(wallpaper.userId);
- pw.print(": id="); pw.println(wallpaper.wallpaperId);
- pw.println(" Display state:");
- mWallpaperDisplayHelper.forEachDisplayData(wpSize -> {
- pw.print(" displayId=");
- pw.println(wpSize.mDisplayId);
- pw.print(" mWidth=");
- pw.print(wpSize.mWidth);
- pw.print(" mHeight=");
- pw.println(wpSize.mHeight);
- pw.print(" mPadding="); pw.println(wpSize.mPadding);
- });
- pw.print(" mCropHint="); pw.println(wallpaper.cropHint);
- pw.print(" mName="); pw.println(wallpaper.name);
- pw.print(" mAllowBackup="); pw.println(wallpaper.allowBackup);
- pw.print(" mWallpaperComponent="); pw.println(wallpaper.wallpaperComponent);
- pw.print(" mWallpaperDimAmount="); pw.println(wallpaper.mWallpaperDimAmount);
- pw.print(" isColorExtracted="); pw.println(wallpaper.mIsColorExtractedFromDim);
- pw.println(" mUidToDimAmount:");
- for (Map.Entry<Integer, Float> entry : wallpaper.mUidToDimAmount.entrySet()) {
- pw.print(" UID="); pw.print(entry.getKey());
- pw.print(" dimAmount="); pw.println(entry.getValue());
- }
- if (wallpaper.connection != null) {
- WallpaperConnection conn = wallpaper.connection;
- pw.print(" Wallpaper connection ");
- pw.print(conn);
- pw.println(":");
- if (conn.mInfo != null) {
- pw.print(" mInfo.component=");
- pw.println(conn.mInfo.getComponent());
- }
- conn.forEachDisplayConnector(connector -> {
- pw.print(" mDisplayId=");
- pw.println(connector.mDisplayId);
- pw.print(" mToken=");
- pw.println(connector.mToken);
- pw.print(" mEngine=");
- pw.println(connector.mEngine);
- });
- pw.print(" mService=");
- pw.println(conn.mService);
- pw.print(" mLastDiedTime=");
- pw.println(wallpaper.lastDiedTime - SystemClock.uptimeMillis());
- }
+ dumpWallpaper(mWallpaperMap.valueAt(i), pw);
}
pw.println("Lock wallpaper state:");
for (int i = 0; i < mLockWallpaperMap.size(); i++) {
- WallpaperData wallpaper = mLockWallpaperMap.valueAt(i);
- pw.print(" User "); pw.print(wallpaper.userId);
- pw.print(": id="); pw.println(wallpaper.wallpaperId);
- pw.print(" mCropHint="); pw.println(wallpaper.cropHint);
- pw.print(" mName="); pw.println(wallpaper.name);
- pw.print(" mAllowBackup="); pw.println(wallpaper.allowBackup);
- pw.print(" mWallpaperDimAmount="); pw.println(wallpaper.mWallpaperDimAmount);
+ dumpWallpaper(mLockWallpaperMap.valueAt(i), pw);
}
pw.println("Fallback wallpaper state:");
- pw.print(" User "); pw.print(mFallbackWallpaper.userId);
- pw.print(": id="); pw.println(mFallbackWallpaper.wallpaperId);
- pw.print(" mCropHint="); pw.println(mFallbackWallpaper.cropHint);
- pw.print(" mName="); pw.println(mFallbackWallpaper.name);
- pw.print(" mAllowBackup="); pw.println(mFallbackWallpaper.allowBackup);
- if (mFallbackWallpaper.connection != null) {
- WallpaperConnection conn = mFallbackWallpaper.connection;
- pw.print(" Fallback Wallpaper connection ");
- pw.print(conn);
- pw.println(":");
- if (conn.mInfo != null) {
- pw.print(" mInfo.component=");
- pw.println(conn.mInfo.getComponent());
- }
- conn.forEachDisplayConnector(connector -> {
- pw.print(" mDisplayId=");
- pw.println(connector.mDisplayId);
- pw.print(" mToken=");
- pw.println(connector.mToken);
- pw.print(" mEngine=");
- pw.println(connector.mEngine);
- });
- pw.print(" mService=");
- pw.println(conn.mService);
- pw.print(" mLastDiedTime=");
- pw.println(mFallbackWallpaper.lastDiedTime - SystemClock.uptimeMillis());
+ if (mFallbackWallpaper != null) {
+ dumpWallpaper(mFallbackWallpaper, pw);
}
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index 316b12a..4507637 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -749,6 +749,8 @@
synchronized (mGlobalLock) {
final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token);
if (r != null) {
+ EventLogTags.writeWmSetRequestedOrientation(requestedOrientation,
+ r.shortComponentName);
r.setRequestedOrientation(requestedOrientation);
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 0c97d7d..c289153 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -10333,8 +10333,8 @@
// TODO(b/263592337): Explore enabling compat fake focus for fullscreen, e.g. for when
// covered with bubbles.
boolean shouldSendCompatFakeFocus() {
- return mWmService.mLetterboxConfiguration.isCompatFakeFocusEnabled(info)
- && inMultiWindowMode() && !inPinnedWindowingMode() && !inFreeformWindowingMode();
+ return mLetterboxUiController.shouldSendFakeFocus() && inMultiWindowMode()
+ && !inPinnedWindowingMode() && !inFreeformWindowingMode();
}
static class Builder {
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 50eb356..c37a3d7 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1033,10 +1033,33 @@
return err;
}
- boolean abort = !mSupervisor.checkStartAnyActivityPermission(intent, aInfo, resultWho,
- requestCode, callingPid, callingUid, callingPackage, callingFeatureId,
- request.ignoreTargetSecurity, inTask != null, callerApp, resultRecord,
- resultRootTask);
+ boolean abort;
+ try {
+ abort = !mSupervisor.checkStartAnyActivityPermission(intent, aInfo, resultWho,
+ requestCode, callingPid, callingUid, callingPackage, callingFeatureId,
+ request.ignoreTargetSecurity, inTask != null, callerApp, resultRecord,
+ resultRootTask);
+ } catch (SecurityException e) {
+ // Return activity not found for the explicit intent if the caller can't see the target
+ // to prevent the disclosure of package existence.
+ final Intent originalIntent = request.ephemeralIntent;
+ if (originalIntent != null && (originalIntent.getComponent() != null
+ || originalIntent.getPackage() != null)) {
+ final String targetPackageName = originalIntent.getComponent() != null
+ ? originalIntent.getComponent().getPackageName()
+ : originalIntent.getPackage();
+ if (mService.getPackageManagerInternalLocked()
+ .filterAppAccess(targetPackageName, callingUid, userId)) {
+ if (resultRecord != null) {
+ resultRecord.sendResult(INVALID_UID, resultWho, requestCode,
+ RESULT_CANCELED, null /* data */, null /* dataGrants */);
+ }
+ SafeActivityOptions.abort(options);
+ return ActivityManager.START_CLASS_NOT_FOUND;
+ }
+ }
+ throw e;
+ }
abort |= !mService.mIntentFirewall.checkStartActivity(intent, callingUid,
callingPid, resolvedType, aInfo.applicationInfo);
abort |= !mService.getPermissionPolicyInternal().checkStartActivity(intent, callingUid,
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 2f82167..80f660e 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -4748,16 +4748,27 @@
updateResumedAppTrace(r);
mLastResumedActivity = r;
- final boolean changed = r.mDisplayContent.setFocusedApp(r);
- if (changed) {
- mWindowManager.updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL,
- true /*updateInputWindows*/);
- }
- if (prevTask == null || task != prevTask) {
- if (prevTask != null) {
- mTaskChangeNotificationController.notifyTaskFocusChanged(prevTask.mTaskId, false);
+ // Don't take focus when transient launching. We don't want the app to know anything
+ // until we've committed to the gesture. The focus will be transferred at the end of
+ // the transition (if the transient launch is committed) or early if explicitly requested
+ // via `setFocused*`.
+ if (!getTransitionController().isTransientCollect(r)) {
+ final Task prevFocusTask = r.mDisplayContent.mFocusedApp != null
+ ? r.mDisplayContent.mFocusedApp.getTask() : null;
+ final boolean changed = r.mDisplayContent.setFocusedApp(r);
+ if (changed) {
+ mWindowManager.updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL,
+ true /*updateInputWindows*/);
}
- mTaskChangeNotificationController.notifyTaskFocusChanged(task.mTaskId, true);
+ if (task != prevFocusTask) {
+ if (prevFocusTask != null) {
+ mTaskChangeNotificationController.notifyTaskFocusChanged(
+ prevFocusTask.mTaskId, false);
+ }
+ mTaskChangeNotificationController.notifyTaskFocusChanged(task.mTaskId, true);
+ }
+ }
+ if (task != prevTask) {
mTaskSupervisor.mRecentTasks.add(task);
}
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index cd79f2e..e1fdeca1 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -32,6 +32,7 @@
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.BackgroundStartPrivileges;
+import android.app.ComponentOptions;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -166,7 +167,8 @@
final boolean useCallingUidState =
originatingPendingIntent == null
|| checkedOptions == null
- || !checkedOptions.getIgnorePendingIntentCreatorForegroundState();
+ || checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
+ != ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
if (useCallingUidState) {
if (callingUid == Process.ROOT_UID
|| callingAppId == Process.SYSTEM_UID
diff --git a/services/core/java/com/android/server/wm/DeviceStateController.java b/services/core/java/com/android/server/wm/DeviceStateController.java
index 7d9a4ec..3f28522 100644
--- a/services/core/java/com/android/server/wm/DeviceStateController.java
+++ b/services/core/java/com/android/server/wm/DeviceStateController.java
@@ -24,6 +24,7 @@
import android.os.HandlerExecutor;
import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.ArrayUtils;
import java.util.ArrayList;
@@ -48,9 +49,12 @@
private final int[] mRearDisplayDeviceStates;
@NonNull
private final int[] mReverseRotationAroundZAxisStates;
+ @GuardedBy("this")
@NonNull
private final List<Consumer<DeviceState>> mDeviceStateCallbacks = new ArrayList<>();
+ private final boolean mMatchBuiltInDisplayOrientationToDefaultDisplay;
+
@Nullable
private DeviceState mLastDeviceState;
private int mCurrentState;
@@ -72,20 +76,19 @@
.getIntArray(R.array.config_rearDisplayDeviceStates);
mReverseRotationAroundZAxisStates = context.getResources()
.getIntArray(R.array.config_deviceStatesToReverseDefaultDisplayRotationAroundZAxis);
+ mMatchBuiltInDisplayOrientationToDefaultDisplay = context.getResources()
+ .getBoolean(R.bool
+ .config_matchSecondaryInternalDisplaysOrientationToReverseDefaultDisplay);
if (mDeviceStateManager != null) {
mDeviceStateManager.registerCallback(new HandlerExecutor(handler), this);
}
}
- void unregisterFromDeviceStateManager() {
- if (mDeviceStateManager != null) {
- mDeviceStateManager.unregisterCallback(this);
- }
- }
-
void registerDeviceStateCallback(@NonNull Consumer<DeviceState> callback) {
- mDeviceStateCallbacks.add(callback);
+ synchronized (this) {
+ mDeviceStateCallbacks.add(callback);
+ }
}
/**
@@ -95,6 +98,15 @@
return ArrayUtils.contains(mReverseRotationAroundZAxisStates, mCurrentState);
}
+ /**
+ * @return true if non-default built-in displays should match the default display's rotation.
+ */
+ boolean shouldMatchBuiltInDisplayOrientationToReverseDefaultDisplay() {
+ // TODO(b/265991392): This should come from display_settings.xml once it's easier to
+ // extend with complex configurations.
+ return mMatchBuiltInDisplayOrientationToDefaultDisplay;
+ }
+
@Override
public void onStateChanged(int state) {
mCurrentState = state;
@@ -115,8 +127,10 @@
if (mLastDeviceState == null || !mLastDeviceState.equals(deviceState)) {
mLastDeviceState = deviceState;
- for (Consumer<DeviceState> callback : mDeviceStateCallbacks) {
- callback.accept(mLastDeviceState);
+ synchronized (this) {
+ for (Consumer<DeviceState> callback : mDeviceStateCallbacks) {
+ callback.accept(mLastDeviceState);
+ }
}
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java
index de63191..c2ddb41 100644
--- a/services/core/java/com/android/server/wm/DisplayArea.java
+++ b/services/core/java/com/android/server/wm/DisplayArea.java
@@ -16,6 +16,7 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
@@ -246,7 +247,22 @@
|| orientation == ActivityInfo.SCREEN_ORIENTATION_NOSENSOR) {
return false;
}
- return getIgnoreOrientationRequest();
+ return getIgnoreOrientationRequest()
+ && !shouldRespectOrientationRequestDueToPerAppOverride();
+ }
+
+ private boolean shouldRespectOrientationRequestDueToPerAppOverride() {
+ if (mDisplayContent == null) {
+ return false;
+ }
+ ActivityRecord activity = mDisplayContent.topRunningActivity(
+ /* considerKeyguardState= */ true);
+ return activity != null && activity.getTaskFragment() != null
+ // Checking TaskFragment rather than ActivityRecord to ensure that transition
+ // between fullscreen and PiP would work well. Checking TaskFragment rather than
+ // Task to ensure that Activity Embedding is excluded.
+ && activity.getTaskFragment().getWindowingMode() == WINDOWING_MODE_FULLSCREEN
+ && activity.mLetterboxUiController.isOverrideRespectRequestedOrientationEnabled();
}
boolean getIgnoreOrientationRequest() {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index f592f9a..9b643c5 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -46,9 +46,7 @@
import static android.view.Display.REMOVE_MODE_DESTROY_CONTENT;
import static android.view.Display.STATE_UNKNOWN;
import static android.view.Display.isSuspendedState;
-import static android.view.InsetsState.ITYPE_IME;
-import static android.view.InsetsState.ITYPE_LEFT_GESTURES;
-import static android.view.InsetsState.ITYPE_RIGHT_GESTURES;
+import static android.view.InsetsSource.ID_IME;
import static android.view.Surface.ROTATION_0;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
@@ -57,6 +55,7 @@
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.navigationBars;
import static android.view.WindowInsets.Type.systemBars;
+import static android.view.WindowInsets.Type.systemGestures;
import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
@@ -120,12 +119,10 @@
import static com.android.server.wm.DisplayContentProto.FOCUSED_APP;
import static com.android.server.wm.DisplayContentProto.FOCUSED_ROOT_TASK_ID;
import static com.android.server.wm.DisplayContentProto.ID;
-import static com.android.server.wm.DisplayContentProto.IME_INSETS_SOURCE_PROVIDER;
import static com.android.server.wm.DisplayContentProto.IME_POLICY;
import static com.android.server.wm.DisplayContentProto.INPUT_METHOD_CONTROL_TARGET;
import static com.android.server.wm.DisplayContentProto.INPUT_METHOD_INPUT_TARGET;
import static com.android.server.wm.DisplayContentProto.INPUT_METHOD_TARGET;
-import static com.android.server.wm.DisplayContentProto.INSETS_SOURCE_PROVIDERS;
import static com.android.server.wm.DisplayContentProto.IS_SLEEPING;
import static com.android.server.wm.DisplayContentProto.KEEP_CLEAR_AREAS;
import static com.android.server.wm.DisplayContentProto.MIN_SIZE_OF_RESIZEABLE_TASK_DP;
@@ -176,6 +173,7 @@
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.ColorSpace;
+import android.graphics.Insets;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
@@ -223,7 +221,6 @@
import android.view.InputDevice;
import android.view.InsetsSource;
import android.view.InsetsState;
-import android.view.InsetsState.InternalInsetsType;
import android.view.MagnificationSpec;
import android.view.PrivacyIndicatorBounds;
import android.view.RemoteAnimationDefinition;
@@ -250,7 +247,6 @@
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.ToBooleanFunction;
-import com.android.internal.util.function.TriConsumer;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.internal.util.function.pooled.PooledPredicate;
import com.android.server.inputmethod.InputMethodManagerInternal;
@@ -293,6 +289,13 @@
@Retention(RetentionPolicy.SOURCE)
@interface ForceScalingMode {}
+ private static final InsetsState.OnTraverseCallbacks COPY_SOURCE_VISIBILITY =
+ new InsetsState.OnTraverseCallbacks() {
+ public void onIdMatch(InsetsSource source1, InsetsSource source2) {
+ source1.setVisible(source2.isVisible());
+ }
+ };
+
final ActivityTaskManagerService mAtmService;
/**
@@ -461,6 +464,8 @@
private boolean mSystemGestureExclusionWasRestricted = false;
private final Region mSystemGestureExclusionUnrestricted = new Region();
private int mSystemGestureExclusionLimit;
+ private final Rect mSystemGestureFrameLeft = new Rect();
+ private final Rect mSystemGestureFrameRight = new Rect();
private Set<Rect> mRestrictedKeepClearAreas = new ArraySet<>();
private Set<Rect> mUnrestrictedKeepClearAreas = new ArraySet<>();
@@ -590,7 +595,8 @@
final FixedRotationTransitionListener mFixedRotationTransitionListener =
new FixedRotationTransitionListener();
- private final DeviceStateController mDeviceStateController;
+ @VisibleForTesting
+ final DeviceStateController mDeviceStateController;
private final PhysicalDisplaySwitchTransitionLauncher mDisplaySwitchTransitionLauncher;
final RemoteDisplayChangeController mRemoteDisplayChangeController;
@@ -1086,7 +1092,8 @@
* @param display May not be null.
* @param root {@link RootWindowContainer}
*/
- DisplayContent(Display display, RootWindowContainer root) {
+ DisplayContent(Display display, RootWindowContainer root,
+ @NonNull DeviceStateController deviceStateController) {
super(root.mWindowManager, "DisplayContent", FEATURE_ROOT);
if (mWmService.mRoot.getDisplayContent(display.getDisplayId()) != null) {
throw new IllegalArgumentException("Display with ID=" + display.getDisplayId()
@@ -1146,11 +1153,11 @@
mWmService.mAtmService.getRecentTasks().getInputListener());
}
- mDeviceStateController = new DeviceStateController(mWmService.mContext, mWmService.mH);
+ mDeviceStateController = deviceStateController;
mDisplayPolicy = new DisplayPolicy(mWmService, this);
mDisplayRotation = new DisplayRotation(mWmService, this, mDisplayInfo.address,
- mDeviceStateController);
+ mDeviceStateController, root.getDisplayRotationCoordinator());
final Consumer<DeviceStateController.DeviceState> deviceStateConsumer =
(@NonNull DeviceStateController.DeviceState newFoldState) -> {
@@ -1512,31 +1519,6 @@
return mDisplayRotation;
}
- void setInsetProvider(@InternalInsetsType int type, WindowContainer win,
- @Nullable TriConsumer<DisplayFrames, WindowContainer, Rect> frameProvider) {
- setInsetProvider(type, win, frameProvider, null /* overrideFrameProviders */);
- }
-
- /**
- * Marks a window as providing insets for the rest of the windows in the system.
- *
- * @param type The type of inset this window provides.
- * @param win The window.
- * @param frameProvider Function to compute the frame, or {@code null} if the just the frame of
- * the window should be taken. Only for non-WindowState providers, nav bar
- * and status bar.
- * @param overrideFrameProviders Functions to compute the frame when dispatching insets to the
- * given window types, or {@code null} if the normal frame should
- * be taken.
- */
- void setInsetProvider(@InternalInsetsType int type, WindowContainer win,
- @Nullable TriConsumer<DisplayFrames, WindowContainer, Rect> frameProvider,
- @Nullable SparseArray<TriConsumer<DisplayFrames, WindowContainer, Rect>>
- overrideFrameProviders) {
- mInsetsStateController.getSourceProvider(type).setWindowContainer(win, frameProvider,
- overrideFrameProviders);
- }
-
InsetsStateController getInsetsStateController() {
return mInsetsStateController;
}
@@ -1725,7 +1707,7 @@
}
// The orientation source may not be the top if it uses SCREEN_ORIENTATION_BEHIND.
final ActivityRecord topCandidate = !r.isVisibleRequested() ? topRunningActivity() : r;
- if (handleTopActivityLaunchingInDifferentOrientation(
+ if (topCandidate != null && handleTopActivityLaunchingInDifferentOrientation(
topCandidate, r, true /* checkOpening */)) {
// Display orientation should be deferred until the top fixed rotation is finished.
return false;
@@ -2088,12 +2070,7 @@
mFixedRotationLaunchingApp.getFixedRotationTransformInsetsState();
if (rotatedState != null) {
final InsetsState state = mInsetsStateController.getRawInsetsState();
- for (int i = 0; i < InsetsState.SIZE; i++) {
- final InsetsSource source = state.peekSource(i);
- if (source != null) {
- rotatedState.setSourceVisible(i, source.isVisible());
- }
- }
+ InsetsState.traverse(rotatedState, state, COPY_SOURCE_VISIBILITY);
}
}
forAllWindows(dispatchInsetsChanged, true /* traverseTopToBottom */);
@@ -2163,6 +2140,10 @@
w.seamlesslyRotateIfAllowed(transaction, oldRotation, rotation, rotateSeamlessly);
}, true /* traverseTopToBottom */);
mPinnedTaskController.startSeamlessRotationIfNeeded(transaction, oldRotation, rotation);
+ if (!mDisplayRotation.hasSeamlessRotatingWindow()) {
+ // Make sure DisplayRotation#isRotatingSeamlessly() will return false.
+ mDisplayRotation.cancelSeamlessRotation();
+ }
}
mWmService.mDisplayManagerInternal.performTraversal(transaction);
@@ -3322,7 +3303,7 @@
mTransitionController.unregisterLegacyListener(mFixedRotationTransitionListener);
handleAnimatingStoppedAndTransition();
mWmService.stopFreezingDisplayLocked();
- mDeviceStateController.unregisterFromDeviceStateManager();
+ mDisplayRotation.removeDefaultDisplayRotationChangedCallback();
super.removeImmediately();
if (DEBUG_DISPLAY) Slog.v(TAG_WM, "Removing display=" + this);
mPointerEventDispatcher.dispose();
@@ -3377,7 +3358,7 @@
int getInputMethodWindowVisibleHeight() {
final InsetsState state = getInsetsStateController().getRawInsetsState();
- final InsetsSource imeSource = state.peekSource(ITYPE_IME);
+ final InsetsSource imeSource = state.peekSource(ID_IME);
if (imeSource == null || !imeSource.isVisible()) {
return 0;
}
@@ -3544,14 +3525,7 @@
mCurrentFocus.dumpDebug(proto, CURRENT_FOCUS, logLevel);
}
if (mInsetsStateController != null) {
- for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) {
- final WindowContainerInsetsSourceProvider provider = mInsetsStateController
- .peekSourceProvider(type);
- if (provider != null) {
- provider.dumpDebug(proto, type == ITYPE_IME ? IME_INSETS_SOURCE_PROVIDER :
- INSETS_SOURCE_PROVIDERS, logLevel);
- }
- }
+ mInsetsStateController.dumpDebug(proto, logLevel);
}
proto.write(IME_POLICY, getImePolicy());
for (Rect r : getKeepClearAreas()) {
@@ -3811,6 +3785,17 @@
*/
boolean updateFocusedWindowLocked(int mode, boolean updateInputWindows,
int topFocusedDisplayId) {
+ // Don't re-assign focus automatically away from a should-keep-focus app window.
+ // `findFocusedWindow` will always grab the transient-launch app since it is "on top" which
+ // would create a mismatch, so just early-out here.
+ if (mCurrentFocus != null && mTransitionController.shouldKeepFocus(mCurrentFocus)
+ // This is only keeping focus, so don't early-out if the focused-app has been
+ // explicitly changed (eg. via setFocusedTask).
+ && mFocusedApp != null && mCurrentFocus.isDescendantOf(mFocusedApp)
+ && mCurrentFocus.isVisible() && mCurrentFocus.isFocusable()) {
+ ProtoLog.v(WM_DEBUG_FOCUS, "Current transition prevents automatic focus change");
+ return false;
+ }
WindowState newFocus = findFocusedWindowIfNeeded(topFocusedDisplayId);
if (mCurrentFocus == newFocus) {
return false;
@@ -4038,7 +4023,7 @@
final int imePid = mInputMethodWindow.mSession.mPid;
mAtmService.onImeWindowSetOnDisplayArea(imePid, mImeWindowsContainer);
}
- mInsetsStateController.getSourceProvider(ITYPE_IME).setWindowContainer(win,
+ mInsetsStateController.getImeSourceProvider().setWindowContainer(win,
mDisplayPolicy.getImeSourceFrameProvider(), null);
computeImeTarget(true /* updateImeTarget */);
updateImeControlTarget();
@@ -4287,7 +4272,8 @@
assignWindowLayers(true /* setLayoutNeeded */);
// 3. The z-order of IME might have been changed. Update the above insets state.
mInsetsStateController.updateAboveInsetsState(
- mInsetsStateController.getRawInsetsState().getSourceOrDefaultVisibility(ITYPE_IME));
+ mInsetsStateController.getRawInsetsState().isSourceOrDefaultVisible(
+ ID_IME, ime()));
// 4. Update the IME control target to apply any inset change and animation.
// 5. Reparent the IME container surface to either the input target app, or the IME window
// parent.
@@ -4513,7 +4499,7 @@
ProtoLog.i(WM_DEBUG_IME, "setInputMethodInputTarget %s", target);
setImeInputTarget(target);
mInsetsStateController.updateAboveInsetsState(mInsetsStateController
- .getRawInsetsState().getSourceOrDefaultVisibility(ITYPE_IME));
+ .getRawInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
// Force updating the IME parent when the IME control target has been updated to the
// remote target but updateImeParent not happen because ImeLayeringTarget and
// ImeInputTarget are different. Then later updateImeParent would be ignored when there
@@ -4933,7 +4919,7 @@
mInsetsStateController.getImeSourceProvider().checkShowImePostLayout();
mLastHasContent = mTmpApplySurfaceChangesTransactionState.displayHasContent;
- if (!mWmService.mDisplayFrozen) {
+ if (!mWmService.mDisplayFrozen && !mDisplayRotation.isRotatingSeamlessly()) {
mWmService.mDisplayManagerInternal.setDisplayProperties(mDisplayId,
mLastHasContent,
mTmpApplySurfaceChangesTransactionState.preferredRefreshRate,
@@ -5708,10 +5694,12 @@
final Region unhandled = Region.obtain();
unhandled.set(0, 0, mDisplayFrames.mWidth, mDisplayFrames.mHeight);
- final Rect leftEdge = mInsetsStateController.getSourceProvider(ITYPE_LEFT_GESTURES)
- .getSource().getFrame();
- final Rect rightEdge = mInsetsStateController.getSourceProvider(ITYPE_RIGHT_GESTURES)
- .getSource().getFrame();
+ final InsetsState state = mInsetsStateController.getRawInsetsState();
+ final Rect df = state.getDisplayFrame();
+ final Insets gestureInsets = state.calculateInsets(df, systemGestures(),
+ false /* ignoreVisibility */);
+ mSystemGestureFrameLeft.set(df.left, df.top, gestureInsets.left, df.bottom);
+ mSystemGestureFrameRight.set(gestureInsets.right, df.top, df.right, df.bottom);
final Region touchableRegion = Region.obtain();
final Region local = Region.obtain();
@@ -5755,25 +5743,25 @@
if (needsGestureExclusionRestrictions(w, false /* ignoreRequest */)) {
// Processes the region along the left edge.
- remainingLeftRight[0] = addToGlobalAndConsumeLimit(local, outExclusion, leftEdge,
- remainingLeftRight[0], w, EXCLUSION_LEFT);
+ remainingLeftRight[0] = addToGlobalAndConsumeLimit(local, outExclusion,
+ mSystemGestureFrameLeft, remainingLeftRight[0], w, EXCLUSION_LEFT);
// Processes the region along the right edge.
- remainingLeftRight[1] = addToGlobalAndConsumeLimit(local, outExclusion, rightEdge,
- remainingLeftRight[1], w, EXCLUSION_RIGHT);
+ remainingLeftRight[1] = addToGlobalAndConsumeLimit(local, outExclusion,
+ mSystemGestureFrameRight, remainingLeftRight[1], w, EXCLUSION_RIGHT);
// Adds the middle (unrestricted area)
final Region middle = Region.obtain(local);
- middle.op(leftEdge, Op.DIFFERENCE);
- middle.op(rightEdge, Op.DIFFERENCE);
+ middle.op(mSystemGestureFrameLeft, Op.DIFFERENCE);
+ middle.op(mSystemGestureFrameRight, Op.DIFFERENCE);
outExclusion.op(middle, Op.UNION);
middle.recycle();
} else {
boolean loggable = needsGestureExclusionRestrictions(w, true /* ignoreRequest */);
if (loggable) {
- addToGlobalAndConsumeLimit(local, outExclusion, leftEdge,
+ addToGlobalAndConsumeLimit(local, outExclusion, mSystemGestureFrameLeft,
Integer.MAX_VALUE, w, EXCLUSION_LEFT);
- addToGlobalAndConsumeLimit(local, outExclusion, rightEdge,
+ addToGlobalAndConsumeLimit(local, outExclusion, mSystemGestureFrameRight,
Integer.MAX_VALUE, w, EXCLUSION_RIGHT);
}
outExclusion.op(local, Op.UNION);
diff --git a/services/core/java/com/android/server/wm/DisplayFrames.java b/services/core/java/com/android/server/wm/DisplayFrames.java
index e984456..7f785af 100644
--- a/services/core/java/com/android/server/wm/DisplayFrames.java
+++ b/services/core/java/com/android/server/wm/DisplayFrames.java
@@ -16,10 +16,8 @@
package com.android.server.wm;
-import static android.view.InsetsState.ITYPE_BOTTOM_DISPLAY_CUTOUT;
-import static android.view.InsetsState.ITYPE_LEFT_DISPLAY_CUTOUT;
-import static android.view.InsetsState.ITYPE_RIGHT_DISPLAY_CUTOUT;
-import static android.view.InsetsState.ITYPE_TOP_DISPLAY_CUTOUT;
+import static android.view.InsetsSource.createId;
+import static android.view.WindowInsets.Type.displayCutout;
import android.annotation.NonNull;
import android.graphics.Rect;
@@ -38,6 +36,12 @@
* @hide
*/
public class DisplayFrames {
+
+ private static final int ID_DISPLAY_CUTOUT_LEFT = createId(null, 0, displayCutout());
+ private static final int ID_DISPLAY_CUTOUT_TOP = createId(null, 1, displayCutout());
+ private static final int ID_DISPLAY_CUTOUT_RIGHT = createId(null, 2, displayCutout());
+ private static final int ID_DISPLAY_CUTOUT_BOTTOM = createId(null, 3, displayCutout());
+
public final InsetsState mInsetsState;
/**
@@ -97,28 +101,28 @@
state.setDisplayShape(displayShape);
state.getDisplayCutoutSafe(safe);
if (safe.left > unrestricted.left) {
- state.getSource(ITYPE_LEFT_DISPLAY_CUTOUT).setFrame(
+ state.getOrCreateSource(ID_DISPLAY_CUTOUT_LEFT, displayCutout()).setFrame(
unrestricted.left, unrestricted.top, safe.left, unrestricted.bottom);
} else {
- state.removeSource(ITYPE_LEFT_DISPLAY_CUTOUT);
+ state.removeSource(ID_DISPLAY_CUTOUT_LEFT);
}
if (safe.top > unrestricted.top) {
- state.getSource(ITYPE_TOP_DISPLAY_CUTOUT).setFrame(
+ state.getOrCreateSource(ID_DISPLAY_CUTOUT_TOP, displayCutout()).setFrame(
unrestricted.left, unrestricted.top, unrestricted.right, safe.top);
} else {
- state.removeSource(ITYPE_TOP_DISPLAY_CUTOUT);
+ state.removeSource(ID_DISPLAY_CUTOUT_TOP);
}
if (safe.right < unrestricted.right) {
- state.getSource(ITYPE_RIGHT_DISPLAY_CUTOUT).setFrame(
+ state.getOrCreateSource(ID_DISPLAY_CUTOUT_RIGHT, displayCutout()).setFrame(
safe.right, unrestricted.top, unrestricted.right, unrestricted.bottom);
} else {
- state.removeSource(ITYPE_RIGHT_DISPLAY_CUTOUT);
+ state.removeSource(ID_DISPLAY_CUTOUT_RIGHT);
}
if (safe.bottom < unrestricted.bottom) {
- state.getSource(ITYPE_BOTTOM_DISPLAY_CUTOUT).setFrame(
+ state.getOrCreateSource(ID_DISPLAY_CUTOUT_BOTTOM, displayCutout()).setFrame(
unrestricted.left, safe.bottom, unrestricted.right, unrestricted.bottom);
} else {
- state.removeSource(ITYPE_BOTTOM_DISPLAY_CUTOUT);
+ state.removeSource(ID_DISPLAY_CUTOUT_BOTTOM);
}
return true;
}
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index d759ff5..3c4d706 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -171,9 +171,8 @@
/** Use the transit animation in style resource (see {@link #selectAnimation}). */
static final int ANIMATION_STYLEABLE = 0;
- private static final int[] SHOW_TYPES_FOR_SWIPE = {ITYPE_NAVIGATION_BAR, ITYPE_STATUS_BAR,
- ITYPE_CLIMATE_BAR, ITYPE_EXTRA_NAVIGATION_BAR};
- private static final int[] SHOW_TYPES_FOR_PANIC = {ITYPE_NAVIGATION_BAR};
+ private static final int SHOW_TYPES_FOR_SWIPE = Type.statusBars() | Type.navigationBars();
+ private static final int SHOW_TYPES_FOR_PANIC = Type.navigationBars();
private final WindowManagerService mService;
private final Context mContext;
@@ -251,7 +250,7 @@
private boolean mIsFreeformWindowOverlappingWithNavBar;
- private boolean mLastImmersiveMode;
+ private boolean mIsImmersiveMode;
// The windows we were told about in focusChanged.
private WindowState mFocusedWindow;
@@ -1077,8 +1076,16 @@
} else {
overrideProviders = null;
}
- mDisplayContent.setInsetProvider(provider.type, win, frameProvider,
- overrideProviders);
+ // TODO (b/234093736): Let InsetsFrameProvider have the following fields:
+ // - IBinder owner.
+ // - int index.
+ // - @InsetsType int type.
+ // So we can create the id by using InsetsSource#createId.
+ // And we won't need toPublicType anymore.
+ final int id = provider.type;
+ final @InsetsType int type = InsetsState.toPublicType(id);
+ mDisplayContent.getInsetsStateController().getOrCreateSourceProvider(id, type)
+ .setWindowContainer(win, frameProvider, overrideProviders);
mInsetsSourceWindowsExceptIme.add(win);
}
}
@@ -1171,10 +1178,17 @@
mLastFocusedWindow = null;
}
- final SparseArray<InsetsSource> sources = win.getProvidedInsetsSources();
- for (int index = sources.size() - 1; index >= 0; index--) {
- final @InternalInsetsType int type = sources.keyAt(index);
- mDisplayContent.setInsetProvider(type, null /* win */, null /* frameProvider */);
+ if (win.hasInsetsSourceProvider()) {
+ final SparseArray<InsetsSourceProvider> providers = win.getInsetsSourceProviders();
+ final InsetsStateController controller = mDisplayContent.getInsetsStateController();
+ for (int index = providers.size() - 1; index >= 0; index--) {
+ final InsetsSourceProvider provider = providers.valueAt(index);
+ provider.setWindowContainer(
+ null /* windowContainer */,
+ null /* frameProvider */,
+ null /* overrideFrameProviders */);
+ controller.removeSourceProvider(provider.getSource().getId());
+ }
}
mInsetsSourceWindowsExceptIme.remove(win);
}
@@ -1243,7 +1257,6 @@
* some temporal states, but doesn't change the window frames used to show on screen.
*/
void simulateLayoutDisplay(DisplayFrames displayFrames) {
- final InsetsStateController controller = mDisplayContent.getInsetsStateController();
sTmpClientFrames.attachedFrame = null;
for (int i = mInsetsSourceWindowsExceptIme.size() - 1; i >= 0; i--) {
final WindowState win = mInsetsSourceWindowsExceptIme.valueAt(i);
@@ -1252,11 +1265,10 @@
displayFrames.mUnrestricted, win.getWindowingMode(), UNSPECIFIED_LENGTH,
UNSPECIFIED_LENGTH, win.getRequestedVisibleTypes(), win.mGlobalScale,
sTmpClientFrames);
- final SparseArray<InsetsSource> sources = win.getProvidedInsetsSources();
+ final SparseArray<InsetsSourceProvider> providers = win.getInsetsSourceProviders();
final InsetsState state = displayFrames.mInsetsState;
- for (int index = sources.size() - 1; index >= 0; index--) {
- final int type = sources.keyAt(index);
- state.addSource(controller.getSourceProvider(type).createSimulatedSource(
+ for (int index = providers.size() - 1; index >= 0; index--) {
+ state.addSource(providers.valueAt(index).createSimulatedSource(
displayFrames, sTmpClientFrames.frame));
}
}
@@ -1359,30 +1371,33 @@
mIsFreeformWindowOverlappingWithNavBar = true;
}
- final SparseArray<InsetsSource> sources = win.getProvidedInsetsSources();
- final Rect bounds = win.getBounds();
- for (int index = sources.size() - 1; index >= 0; index--) {
- final InsetsSource source = sources.valueAt(index);
- if ((source.getType()
- & (Type.systemGestures() | Type.mandatorySystemGestures())) == 0) {
- continue;
- }
- if (mLeftGestureHost != null && mTopGestureHost != null
- && mRightGestureHost != null && mBottomGestureHost != null) {
- continue;
- }
- final Insets insets = source.calculateInsets(bounds, false /* ignoreVisibility */);
- if (mLeftGestureHost == null && insets.left > 0) {
- mLeftGestureHost = win;
- }
- if (mTopGestureHost == null && insets.top > 0) {
- mTopGestureHost = win;
- }
- if (mRightGestureHost == null && insets.right > 0) {
- mRightGestureHost = win;
- }
- if (mBottomGestureHost == null && insets.bottom > 0) {
- mBottomGestureHost = win;
+ if (win.hasInsetsSourceProvider()) {
+ final SparseArray<InsetsSourceProvider> providers = win.getInsetsSourceProviders();
+ final Rect bounds = win.getBounds();
+ for (int index = providers.size() - 1; index >= 0; index--) {
+ final InsetsSourceProvider provider = providers.valueAt(index);
+ final InsetsSource source = provider.getSource();
+ if ((source.getType()
+ & (Type.systemGestures() | Type.mandatorySystemGestures())) == 0) {
+ continue;
+ }
+ if (mLeftGestureHost != null && mTopGestureHost != null
+ && mRightGestureHost != null && mBottomGestureHost != null) {
+ continue;
+ }
+ final Insets insets = source.calculateInsets(bounds, false /* ignoreVisibility */);
+ if (mLeftGestureHost == null && insets.left > 0) {
+ mLeftGestureHost = win;
+ }
+ if (mTopGestureHost == null && insets.top > 0) {
+ mTopGestureHost = win;
+ }
+ if (mRightGestureHost == null && insets.right > 0) {
+ mRightGestureHost = win;
+ }
+ if (mBottomGestureHost == null && insets.bottom > 0) {
+ mBottomGestureHost = win;
+ }
}
}
@@ -1741,21 +1756,6 @@
}
/**
- * Get the Navigation Bar Frame height. This dimension is the height of the navigation bar that
- * is used for spacing to show additional buttons on the navigation bar (such as the ime
- * switcher when ime is visible).
- *
- * @param rotation specifies rotation to return dimension from
- * @return navigation bar frame height
- */
- private int getNavigationBarFrameHeight(int rotation) {
- if (mNavigationBar == null) {
- return 0;
- }
- return mNavigationBar.mAttrs.forRotation(rotation).height;
- }
-
- /**
* Return corner radius in pixels that should be used on windows in order to cover the display.
*
* The radius is only valid for internal displays, since the corner radius of external displays
@@ -2186,14 +2186,27 @@
appearance = configureNavBarOpacity(appearance, multiWindowTaskVisible,
freeformRootTaskVisible);
+ // Show immersive mode confirmation if needed.
+ final boolean wasImmersiveMode = mIsImmersiveMode;
+ final boolean isImmersiveMode = isImmersiveMode(win);
+ if (wasImmersiveMode != isImmersiveMode) {
+ mIsImmersiveMode = isImmersiveMode;
+ // The immersive confirmation window should be attached to the immersive window root.
+ final RootDisplayArea root = win.getRootDisplayArea();
+ final int rootDisplayAreaId = root == null ? FEATURE_UNDEFINED : root.mFeatureId;
+ mImmersiveModeConfirmation.immersiveModeChangedLw(rootDisplayAreaId, isImmersiveMode,
+ mService.mPolicy.isUserSetupComplete(),
+ isNavBarEmpty(disableFlags));
+ }
+
+ // Show transient bars for panic if needed.
final boolean requestHideNavBar = !win.isRequestedVisible(Type.navigationBars());
final long now = SystemClock.uptimeMillis();
final boolean pendingPanic = mPendingPanicGestureUptime != 0
&& now - mPendingPanicGestureUptime <= PANIC_GESTURE_EXPIRATION;
final DisplayPolicy defaultDisplayPolicy =
mService.getDefaultDisplayContentLocked().getDisplayPolicy();
- if (pendingPanic && requestHideNavBar && win != mNotificationShade
- && getInsetsPolicy().isHidden(ITYPE_NAVIGATION_BAR)
+ if (pendingPanic && requestHideNavBar && isImmersiveMode
// TODO (b/111955725): Show keyguard presentation on all external displays
&& defaultDisplayPolicy.isKeyguardDrawComplete()) {
// The user performed the panic gesture recently, we're about to hide the bars,
@@ -2205,19 +2218,6 @@
}
}
- // update navigation bar
- boolean oldImmersiveMode = mLastImmersiveMode;
- boolean newImmersiveMode = isImmersiveMode(win);
- if (oldImmersiveMode != newImmersiveMode) {
- mLastImmersiveMode = newImmersiveMode;
- // The immersive confirmation window should be attached to the immersive window root.
- final RootDisplayArea root = win.getRootDisplayArea();
- final int rootDisplayAreaId = root == null ? FEATURE_UNDEFINED : root.mFeatureId;
- mImmersiveModeConfirmation.immersiveModeChangedLw(rootDisplayAreaId, newImmersiveMode,
- mService.mPolicy.isUserSetupComplete(),
- isNavBarEmpty(disableFlags));
- }
-
return appearance;
}
@@ -2339,18 +2339,10 @@
if (win == null) {
return false;
}
- return getNavigationBar() != null
- && canHideNavigationBar()
- && getInsetsPolicy().isHidden(ITYPE_NAVIGATION_BAR)
- && win != getNotificationShade()
- && !win.isActivityTypeDream();
- }
-
- /**
- * @return whether the navigation bar can be hidden, e.g. the device has a navigation bar
- */
- private boolean canHideNavigationBar() {
- return hasNavigationBar();
+ if (win == getNotificationShade() || win.isActivityTypeDream()) {
+ return false;
+ }
+ return getInsetsPolicy().hasHiddenSources(Type.navigationBars());
}
private static boolean isNavBarEmpty(int systemUiFlags) {
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index 3404279..7071aa7 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -58,6 +58,7 @@
import android.os.UserHandle;
import android.provider.Settings;
import android.util.ArraySet;
+import android.util.RotationUtils;
import android.util.Slog;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
@@ -120,6 +121,11 @@
private FoldController mFoldController;
@NonNull
private final DeviceStateController mDeviceStateController;
+ @NonNull
+ private final DisplayRotationCoordinator mDisplayRotationCoordinator;
+ @NonNull
+ @VisibleForTesting
+ final Runnable mDefaultDisplayRotationChangedCallback;
@ScreenOrientation
private int mCurrentAppOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
@@ -221,17 +227,19 @@
private boolean mDemoRotationLock;
DisplayRotation(WindowManagerService service, DisplayContent displayContent,
- DisplayAddress displayAddress, @NonNull DeviceStateController deviceStateController) {
+ DisplayAddress displayAddress, @NonNull DeviceStateController deviceStateController,
+ @NonNull DisplayRotationCoordinator displayRotationCoordinator) {
this(service, displayContent, displayAddress, displayContent.getDisplayPolicy(),
service.mDisplayWindowSettings, service.mContext, service.getWindowManagerLock(),
- deviceStateController);
+ deviceStateController, displayRotationCoordinator);
}
@VisibleForTesting
DisplayRotation(WindowManagerService service, DisplayContent displayContent,
DisplayAddress displayAddress, DisplayPolicy displayPolicy,
DisplayWindowSettings displayWindowSettings, Context context, Object lock,
- @NonNull DeviceStateController deviceStateController) {
+ @NonNull DeviceStateController deviceStateController,
+ @NonNull DisplayRotationCoordinator displayRotationCoordinator) {
mService = service;
mDisplayContent = displayContent;
mDisplayPolicy = displayPolicy;
@@ -252,6 +260,19 @@
int defaultRotation = readDefaultDisplayRotation(displayAddress);
mRotation = defaultRotation;
+ mDisplayRotationCoordinator = displayRotationCoordinator;
+ if (isDefaultDisplay) {
+ mDisplayRotationCoordinator.setDefaultDisplayDefaultRotation(mRotation);
+ }
+ mDefaultDisplayRotationChangedCallback = this::updateRotationAndSendNewConfigIfChanged;
+
+ if (DisplayRotationCoordinator.isSecondaryInternalDisplay(displayContent)
+ && mDeviceStateController
+ .shouldMatchBuiltInDisplayOrientationToReverseDefaultDisplay()) {
+ mDisplayRotationCoordinator.setDefaultDisplayRotationChangedCallback(
+ mDefaultDisplayRotationChangedCallback);
+ }
+
if (isDefaultDisplay) {
final Handler uiHandler = UiThread.getHandler();
mOrientationListener =
@@ -494,8 +515,11 @@
return false;
}
+ @Surface.Rotation
final int oldRotation = mRotation;
+ @ScreenOrientation
final int lastOrientation = mLastOrientation;
+ @Surface.Rotation
int rotation = rotationForOrientation(lastOrientation, oldRotation);
// Use the saved rotation for tabletop mode, if set.
if (mFoldController != null && mFoldController.shouldRevertOverriddenRotation()) {
@@ -507,6 +531,14 @@
Surface.rotationToString(oldRotation),
Surface.rotationToString(prevRotation));
}
+
+ if (DisplayRotationCoordinator.isSecondaryInternalDisplay(mDisplayContent)
+ && mDeviceStateController
+ .shouldMatchBuiltInDisplayOrientationToReverseDefaultDisplay()) {
+ rotation = RotationUtils.reverseRotationDirectionAroundZAxis(
+ mDisplayRotationCoordinator.getDefaultDisplayCurrentRotation());
+ }
+
ProtoLog.v(WM_DEBUG_ORIENTATION,
"Computed rotation=%s (%d) for display id=%d based on lastOrientation=%s (%d) and "
+ "oldRotation=%s (%d)",
@@ -525,6 +557,10 @@
return false;
}
+ if (isDefaultDisplay) {
+ mDisplayRotationCoordinator.onDefaultDisplayRotationChanged(rotation);
+ }
+
// Preemptively cancel the running recents animation -- SysUI can't currently handle this
// case properly since the signals it receives all happen post-change. We do this earlier
// in the rotation flow, since DisplayContent.updateDisplayOverrideConfigurationLocked seems
@@ -1142,17 +1178,12 @@
return mUserRotation;
}
+ @Surface.Rotation
int sensorRotation = mOrientationListener != null
? mOrientationListener.getProposedRotation() // may be -1
: -1;
if (mDeviceStateController.shouldReverseRotationDirectionAroundZAxis()) {
- // Flipping 270 and 90 has the same effect as changing the direction which rotation is
- // applied.
- if (sensorRotation == Surface.ROTATION_90) {
- sensorRotation = Surface.ROTATION_270;
- } else if (sensorRotation == Surface.ROTATION_270) {
- sensorRotation = Surface.ROTATION_90;
- }
+ sensorRotation = RotationUtils.reverseRotationDirectionAroundZAxis(sensorRotation);
}
mLastSensorRotation = sensorRotation;
if (sensorRotation < 0) {
@@ -1167,6 +1198,7 @@
final boolean deskDockEnablesAccelerometer =
mDisplayPolicy.isDeskDockEnablesAccelerometer();
+ @Surface.Rotation
final int preferredRotation;
if (!isDefaultDisplay) {
// For secondary displays we ignore things like displays sensors, docking mode and
@@ -1536,6 +1568,12 @@
return shouldUpdateRotation;
}
+ void removeDefaultDisplayRotationChangedCallback() {
+ if (DisplayRotationCoordinator.isSecondaryInternalDisplay(mDisplayContent)) {
+ mDisplayRotationCoordinator.removeDefaultDisplayRotationChangedCallback();
+ }
+ }
+
void dump(String prefix, PrintWriter pw) {
pw.println(prefix + "DisplayRotation");
pw.println(prefix + " mCurrentAppOrientation="
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index 3ffb2fa..e04900c 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -16,6 +16,8 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
@@ -34,6 +36,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.StringRes;
import android.app.servertransaction.ClientTransaction;
import android.app.servertransaction.RefreshCallbackItem;
import android.app.servertransaction.ResumeActivityItem;
@@ -44,10 +47,13 @@
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.widget.Toast;
+import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
+import com.android.server.UiThread;
import java.util.Map;
import java.util.Set;
@@ -232,6 +238,27 @@
activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(false);
}
+ /**
+ * Notifies that animation in {@link ScreenAnimationRotation} has finished.
+ *
+ * <p>This class uses this signal as a trigger for notifying the user about forced rotation
+ * reason with the {@link Toast}.
+ */
+ void onScreenRotationAnimationFinished() {
+ if (!isTreatmentEnabledForDisplay() || mCameraIdPackageBiMap.isEmpty()) {
+ return;
+ }
+ ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+ /* considerKeyguardState= */ true);
+ if (topActivity == null
+ // Checking windowing mode on activity level because we don't want to
+ // show toast in case of activity embedding.
+ || topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
+ return;
+ }
+ showToast(R.string.display_rotation_camera_compat_toast_after_rotation);
+ }
+
String getSummaryForDisplayRotationHistoryRecord() {
String summaryIfEnabled = "";
if (isTreatmentEnabledForDisplay()) {
@@ -281,6 +308,10 @@
&& mDisplayContent.getDisplay().getType() == TYPE_INTERNAL;
}
+ boolean isActivityEligibleForOrientationOverride(@NonNull ActivityRecord activity) {
+ return isTreatmentEnabledForDisplay() && isCameraActiveInFullscreen(activity);
+ }
+
/**
* Whether camera compat treatment is applicable for the given activity.
*
@@ -292,12 +323,16 @@
* </ul>
*/
boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) {
- return activity != null && !activity.inMultiWindowMode()
+ return activity != null && isCameraActiveInFullscreen(activity)
&& activity.getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED
// "locked" and "nosensor" values are often used by camera apps that can't
// handle dynamic changes so we shouldn't force rotate them.
&& activity.getOverrideOrientation() != SCREEN_ORIENTATION_NOSENSOR
- && activity.getOverrideOrientation() != SCREEN_ORIENTATION_LOCKED
+ && activity.getOverrideOrientation() != SCREEN_ORIENTATION_LOCKED;
+ }
+
+ private boolean isCameraActiveInFullscreen(@NonNull ActivityRecord activity) {
+ return !activity.inMultiWindowMode()
&& mCameraIdPackageBiMap.containsPackageName(activity.packageName)
&& activity.mLetterboxUiController.shouldForceRotateForCameraCompat();
}
@@ -334,7 +369,31 @@
}
mCameraIdPackageBiMap.put(packageName, cameraId);
}
- updateOrientationWithWmLock();
+ ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+ /* considerKeyguardState= */ true);
+ if (topActivity == null || topActivity.getTask() == null) {
+ return;
+ }
+ // Checking whether an activity in fullscreen rather than the task as this camera compat
+ // treatment doesn't cover activity embedding.
+ if (topActivity.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
+ if (topActivity.mLetterboxUiController.isOverrideOrientationOnlyForCameraEnabled()) {
+ topActivity.recomputeConfiguration();
+ }
+ updateOrientationWithWmLock();
+ return;
+ }
+ // Checking that the whole app is in multi-window mode as we shouldn't show toast
+ // for the activity embedding case.
+ if (topActivity.getTask().getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) {
+ showToast(R.string.display_rotation_camera_compat_toast_in_split_screen);
+ }
+ }
+
+ @VisibleForTesting
+ void showToast(@StringRes int stringRes) {
+ UiThread.getHandler().post(
+ () -> Toast.makeText(mWmService.mContext, stringRes, Toast.LENGTH_LONG).show());
}
private synchronized void notifyCameraClosed(@NonNull String cameraId) {
@@ -375,6 +434,17 @@
ProtoLog.v(WM_DEBUG_ORIENTATION,
"Display id=%d is notified that Camera %s is closed, updating rotation.",
mDisplayContent.mDisplayId, cameraId);
+ ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+ /* considerKeyguardState= */ true);
+ if (topActivity == null
+ // Checking whether an activity in fullscreen rather than the task as this camera
+ // compat treatment doesn't cover activity embedding.
+ || topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
+ return;
+ }
+ if (topActivity.mLetterboxUiController.isOverrideOrientationOnlyForCameraEnabled()) {
+ topActivity.recomputeConfiguration();
+ }
updateOrientationWithWmLock();
}
@@ -396,6 +466,10 @@
private final Map<String, String> mPackageToCameraIdMap = new ArrayMap<>();
private final Map<String, String> mCameraIdToPackageMap = new ArrayMap<>();
+ boolean isEmpty() {
+ return mCameraIdToPackageMap.isEmpty();
+ }
+
void put(String packageName, String cameraId) {
// Always using the last connected camera ID for the package even for the concurrent
// camera use case since we can't guess which camera is more important anyway.
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCoordinator.java b/services/core/java/com/android/server/wm/DisplayRotationCoordinator.java
new file mode 100644
index 0000000..ae3787c
--- /dev/null
+++ b/services/core/java/com/android/server/wm/DisplayRotationCoordinator.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.view.Display;
+import android.view.Surface;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Singleton for coordinating rotation across multiple displays. Used to notify non-default
+ * displays when the default display rotates.
+ *
+ * Note that this class does not need locking because it is always protected by WindowManagerService
+ * mGlobalLock.
+ */
+class DisplayRotationCoordinator {
+
+ private static final String TAG = "DisplayRotationCoordinator";
+
+ @Surface.Rotation
+ private int mDefaultDisplayDefaultRotation;
+
+ @Nullable
+ @VisibleForTesting
+ Runnable mDefaultDisplayRotationChangedCallback;
+
+ @Surface.Rotation
+ private int mDefaultDisplayCurrentRotation;
+
+ /**
+ * Notifies clients when the default display rotation changes.
+ */
+ void onDefaultDisplayRotationChanged(@Surface.Rotation int rotation) {
+ mDefaultDisplayCurrentRotation = rotation;
+
+ if (mDefaultDisplayRotationChangedCallback != null) {
+ mDefaultDisplayRotationChangedCallback.run();
+ }
+ }
+
+ void setDefaultDisplayDefaultRotation(@Surface.Rotation int rotation) {
+ mDefaultDisplayDefaultRotation = rotation;
+ }
+
+ @Surface.Rotation
+ int getDefaultDisplayCurrentRotation() {
+ return mDefaultDisplayCurrentRotation;
+ }
+
+ /**
+ * Register a callback to be notified when the default display's rotation changes. Clients can
+ * query the default display's current rotation via {@link #getDefaultDisplayCurrentRotation()}.
+ */
+ void setDefaultDisplayRotationChangedCallback(@NonNull Runnable callback) {
+ if (mDefaultDisplayRotationChangedCallback != null) {
+ throw new UnsupportedOperationException("Multiple clients unsupported");
+ }
+
+ mDefaultDisplayRotationChangedCallback = callback;
+
+ if (mDefaultDisplayCurrentRotation != mDefaultDisplayDefaultRotation) {
+ callback.run();
+ }
+ }
+
+ /**
+ * Removes the callback that was added via
+ * {@link #setDefaultDisplayRotationChangedCallback(Runnable)}.
+ */
+ void removeDefaultDisplayRotationChangedCallback() {
+ mDefaultDisplayRotationChangedCallback = null;
+ }
+
+ static boolean isSecondaryInternalDisplay(@NonNull DisplayContent displayContent) {
+ if (displayContent.isDefaultDisplay) {
+ return false;
+ } else if (displayContent.mDisplay == null) {
+ return false;
+ }
+ return displayContent.mDisplay.getType() == Display.TYPE_INTERNAL;
+ }
+}
diff --git a/services/core/java/com/android/server/wm/EventLogTags.logtags b/services/core/java/com/android/server/wm/EventLogTags.logtags
index b36f835..031e022 100644
--- a/services/core/java/com/android/server/wm/EventLogTags.logtags
+++ b/services/core/java/com/android/server/wm/EventLogTags.logtags
@@ -1,4 +1,4 @@
-# See system/core/logcat/event.logtags for a description of the format of this file.
+# See system/logging/logcat/event.logtags for a description of the format of this file.
option java_package com.android.server.wm
@@ -59,6 +59,10 @@
31002 wm_task_moved (TaskId|1|5),(Root Task ID|1|5),(Display Id|1|5),(ToTop|1),(Index|1)
# Task removed with source explanation.
31003 wm_task_removed (TaskId|1|5),(Root Task ID|1|5),(Display Id|1|5),(Reason|3)
+
+# Set the requested orientation of an activity.
+31006 wm_set_requested_orientation (Orientation|1|5),(Component Name|3)
+
# bootanim finished:
31007 wm_boot_animation_done (time|2|3)
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index 85938e3..f38ae3f 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -17,7 +17,7 @@
package com.android.server.wm;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
-import static android.view.InsetsState.ITYPE_IME;
+import static android.view.InsetsSource.ID_IME;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_IME;
import static com.android.server.wm.DisplayContent.IME_TARGET_CONTROL;
@@ -57,7 +57,7 @@
private Runnable mShowImeRunner;
private boolean mIsImeLayoutDrawn;
private boolean mImeShowing;
- private final InsetsSource mLastSource = new InsetsSource(ITYPE_IME, WindowInsets.Type.ime());
+ private final InsetsSource mLastSource = new InsetsSource(ID_IME, WindowInsets.Type.ime());
/** @see #setFrozen(boolean) */
private boolean mFrozen;
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index 3e1105b..8c59548 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -405,8 +405,12 @@
// Apply recents input consumer when the focusing window is in recents animation.
final boolean shouldApplyRecentsInputConsumer = (recentsAnimationController != null
&& recentsAnimationController.shouldApplyInputConsumer(focus.mActivityRecord))
- // Shell transitions doesn't use RecentsAnimationController
- || getWeak(mActiveRecentsActivity) != null && focus.inTransition();
+ // Shell transitions doesn't use RecentsAnimationController but we still
+ // have carryover legacy logic that relies on the consumer.
+ || (getWeak(mActiveRecentsActivity) != null && focus.inTransition()
+ // only take focus from the recents activity to avoid intercepting
+ // events before the gesture officially starts.
+ && focus.isActivityTypeHomeOrRecents());
if (shouldApplyRecentsInputConsumer) {
if (mInputFocus != recentsAnimationInputConsumer.mWindowHandle.token) {
requestFocus(recentsAnimationInputConsumer.mWindowHandle.token,
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 1df534f..97dd291 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -25,9 +25,7 @@
import static android.view.InsetsController.ANIMATION_TYPE_SHOW;
import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_HIDDEN;
import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_SHOWN;
-import static android.view.InsetsState.ITYPE_IME;
-import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_STATUS_BAR;
+import static android.view.InsetsSource.ID_IME;
import static android.view.SyncRtSurfaceTransactionApplier.applyParams;
import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR;
@@ -41,8 +39,6 @@
import android.app.WindowConfiguration;
import android.content.ComponentName;
import android.content.res.Resources;
-import android.util.ArrayMap;
-import android.util.IntArray;
import android.util.SparseArray;
import android.view.InsetsAnimationControlCallbacks;
import android.view.InsetsAnimationControlImpl;
@@ -52,7 +48,6 @@
import android.view.InsetsSource;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
-import android.view.InsetsState.InternalInsetsType;
import android.view.InternalInsetsAnimationController;
import android.view.SurfaceControl;
import android.view.SyncRtSurfaceTransactionApplier;
@@ -81,7 +76,6 @@
private final InsetsStateController mStateController;
private final DisplayContent mDisplayContent;
private final DisplayPolicy mPolicy;
- private final IntArray mShowingTransientTypes = new IntArray();
/** For resetting visibilities of insets sources. */
private final InsetsControlTarget mDummyControlTarget = new InsetsControlTarget() {
@@ -95,7 +89,7 @@
return;
}
for (InsetsSourceControl control : controls) {
- if (mShowingTransientTypes.indexOf(control.getId()) != -1) {
+ if (isTransient(control.getType())) {
// The visibilities of transient bars will be handled with animations.
continue;
}
@@ -117,13 +111,16 @@
};
private WindowState mFocusedWin;
- private BarWindow mStatusBar = new BarWindow(StatusBarManager.WINDOW_STATUS_BAR);
- private BarWindow mNavBar = new BarWindow(StatusBarManager.WINDOW_NAVIGATION_BAR);
+ private final BarWindow mStatusBar = new BarWindow(StatusBarManager.WINDOW_STATUS_BAR);
+ private final BarWindow mNavBar = new BarWindow(StatusBarManager.WINDOW_NAVIGATION_BAR);
+ private @InsetsType int mShowingTransientTypes;
private boolean mAnimatingShown;
+
/**
* Let remote insets controller control system bars regardless of other settings.
*/
private boolean mRemoteInsetsControllerControlsSystemBars;
+
private final boolean mHideNavBarForKeyboard;
private final float[] mTmpFloat9 = new float[9];
@@ -178,37 +175,46 @@
mNavBar.updateVisibility(navControlTarget, Type.navigationBars());
}
- boolean isHidden(@InternalInsetsType int type) {
- final WindowContainerInsetsSourceProvider provider = mStateController
- .peekSourceProvider(type);
- return provider != null && provider.hasWindowContainer()
- && !provider.getSource().isVisible();
+ boolean hasHiddenSources(@InsetsType int types) {
+ final InsetsState state = mStateController.getRawInsetsState();
+ for (int i = state.sourceSize() - 1; i >= 0; i--) {
+ final InsetsSource source = state.sourceAt(i);
+ if ((source.getType() & types) == 0) {
+ continue;
+ }
+ if (!source.getFrame().isEmpty() && !source.isVisible()) {
+ return true;
+ }
+ }
+ return false;
}
- void showTransient(@InternalInsetsType int[] types, boolean isGestureOnSystemBar) {
- boolean changed = false;
- for (int i = types.length - 1; i >= 0; i--) {
- final @InternalInsetsType int type = types[i];
- if (!isHidden(type)) {
+ void showTransient(@InsetsType int types, boolean isGestureOnSystemBar) {
+ @InsetsType int showingTransientTypes = mShowingTransientTypes;
+ final InsetsState rawState = mStateController.getRawInsetsState();
+ for (int i = rawState.sourceSize() - 1; i >= 0; i--) {
+ final InsetsSource source = rawState.sourceAt(i);
+ if (source.isVisible()) {
continue;
}
- if (mShowingTransientTypes.indexOf(type) != -1) {
+ final @InsetsType int type = source.getType();
+ if ((source.getType() & types) == 0) {
continue;
}
- mShowingTransientTypes.add(type);
- changed = true;
+ showingTransientTypes |= type;
}
- if (changed) {
+ if (mShowingTransientTypes != showingTransientTypes) {
+ mShowingTransientTypes = showingTransientTypes;
StatusBarManagerInternal statusBarManagerInternal =
mPolicy.getStatusBarManagerInternal();
if (statusBarManagerInternal != null) {
statusBarManagerInternal.showTransient(mDisplayContent.getDisplayId(),
- mShowingTransientTypes.toArray(), isGestureOnSystemBar);
+ showingTransientTypes, isGestureOnSystemBar);
}
updateBarControlTarget(mFocusedWin);
dispatchTransientSystemBarsVisibilityChanged(
mFocusedWin,
- isTransient(ITYPE_STATUS_BAR) || isTransient(ITYPE_NAVIGATION_BAR),
+ (showingTransientTypes & (Type.statusBars() | Type.navigationBars())) != 0,
isGestureOnSystemBar);
// The leashes can be created while updating bar control target. The surface transaction
@@ -224,7 +230,7 @@
}
void hideTransient() {
- if (mShowingTransientTypes.size() == 0) {
+ if (mShowingTransientTypes == 0) {
return;
}
@@ -235,20 +241,25 @@
startAnimation(false /* show */, () -> {
synchronized (mDisplayContent.mWmService.mGlobalLock) {
- for (int i = mShowingTransientTypes.size() - 1; i >= 0; i--) {
+ final SparseArray<WindowContainerInsetsSourceProvider> providers =
+ mStateController.getSourceProviders();
+ for (int i = providers.size() - 1; i >= 0; i--) {
+ final WindowContainerInsetsSourceProvider provider = providers.valueAt(i);
+ if (!isTransient(provider.getSource().getType())) {
+ continue;
+ }
// We are about to clear mShowingTransientTypes, we don't want the transient bar
// can cause insets on the client. Restore the client visibility.
- final @InternalInsetsType int type = mShowingTransientTypes.get(i);
- mStateController.getSourceProvider(type).setClientVisible(false);
+ provider.setClientVisible(false);
}
- mShowingTransientTypes.clear();
+ mShowingTransientTypes = 0;
updateBarControlTarget(mFocusedWin);
}
});
}
- boolean isTransient(@InternalInsetsType int type) {
- return mShowingTransientTypes.indexOf(type) != -1;
+ boolean isTransient(@InsetsType int type) {
+ return (mShowingTransientTypes & type) != 0;
}
/**
@@ -280,9 +291,9 @@
? token.getFixedRotationTransformInsetsState()
: mStateController.getRawInsetsState();
outInsetsState.set(srcState, true /* copySources */);
- for (int i = mShowingTransientTypes.size() - 1; i >= 0; i--) {
- final InsetsSource source = outInsetsState.peekSource(mShowingTransientTypes.get(i));
- if (source != null) {
+ for (int i = outInsetsState.sourceSize() - 1; i >= 0; i--) {
+ final InsetsSource source = outInsetsState.sourceAt(i);
+ if (isTransient(source.getType())) {
source.setVisible(false);
}
}
@@ -315,12 +326,14 @@
// The caller should not receive the visible insets provided by itself.
if (attrs.type == TYPE_INPUT_METHOD) {
state = new InsetsState(state);
- state.removeSource(ITYPE_IME);
+ state.removeSource(ID_IME);
} else if (attrs.providedInsets != null) {
for (InsetsFrameProvider provider : attrs.providedInsets) {
// TODO(b/234093736): Let InsetsFrameProvider return the public type and the ID.
final int sourceId = provider.type;
- final @InsetsType int type = InsetsState.toPublicType(sourceId);
+ final @InsetsType int type = sourceId == ID_IME
+ ? WindowInsets.Type.ime()
+ : InsetsState.toPublicType(sourceId);
if ((type & WindowInsets.Type.systemBars()) == 0) {
continue;
}
@@ -331,8 +344,8 @@
}
}
- final ArrayMap<Integer, WindowContainerInsetsSourceProvider> providers = mStateController
- .getSourceProviders();
+ final SparseArray<WindowContainerInsetsSourceProvider> providers =
+ mStateController.getSourceProviders();
final int windowType = attrs.type;
for (int i = providers.size() - 1; i >= 0; i--) {
final WindowContainerInsetsSourceProvider otherProvider = providers.valueAt(i);
@@ -340,8 +353,7 @@
if (state == originalState) {
state = new InsetsState(state);
}
- final InsetsSource override =
- new InsetsSource(state.getSource(otherProvider.getSource().getId()));
+ final InsetsSource override = new InsetsSource(otherProvider.getSource());
override.setFrame(otherProvider.getOverriddenFrame(windowType));
state.addSource(override);
}
@@ -364,18 +376,17 @@
private InsetsState adjustVisibilityForTransientTypes(InsetsState originalState) {
InsetsState state = originalState;
- for (int i = mShowingTransientTypes.size() - 1; i >= 0; i--) {
- final @InternalInsetsType int type = mShowingTransientTypes.get(i);
- final InsetsSource originalSource = state.peekSource(type);
- if (originalSource != null && originalSource.isVisible()) {
+ for (int i = state.sourceSize() - 1; i >= 0; i--) {
+ final InsetsSource source = state.sourceAt(i);
+ if (isTransient(source.getType()) && source.isVisible()) {
if (state == originalState) {
// The source will be modified, create a non-deep copy to store the new one.
state = new InsetsState(originalState);
}
// Replace the source with a copy in invisible state.
- final InsetsSource source = new InsetsSource(originalSource);
- source.setVisible(false);
- state.addSource(source);
+ final InsetsSource outSource = new InsetsSource(source);
+ outSource.setVisible(false);
+ state.addSource(outSource);
}
}
return state;
@@ -384,23 +395,28 @@
private InsetsState adjustVisibilityForIme(WindowState w, InsetsState originalState,
boolean copyState) {
if (w.mIsImWindow) {
+ InsetsState state = originalState;
// If navigation bar is not hidden by IME, IME should always receive visible
// navigation bar insets.
final boolean navVisible = !mHideNavBarForKeyboard;
- final InsetsSource originalNavSource = originalState.peekSource(ITYPE_NAVIGATION_BAR);
- if (originalNavSource != null && originalNavSource.isVisible() != navVisible) {
- final InsetsState state = copyState ? new InsetsState(originalState)
- : originalState;
- final InsetsSource navSource = new InsetsSource(originalNavSource);
+ for (int i = originalState.sourceSize() - 1; i >= 0; i--) {
+ final InsetsSource source = originalState.sourceAt(i);
+ if (source.getType() != Type.navigationBars() || source.isVisible() == navVisible) {
+ continue;
+ }
+ if (state == originalState && copyState) {
+ state = new InsetsState(originalState);
+ }
+ final InsetsSource navSource = new InsetsSource(source);
navSource.setVisible(navVisible);
state.addSource(navSource);
- return state;
}
+ return state;
} else if (w.mActivityRecord != null && w.mActivityRecord.mImeInsetsFrozenUntilStartInput) {
// During switching tasks with gestural navigation, before the next IME input target
// starts the input, we should adjust and freeze the last IME visibility of the window
// in case delivering obsoleted IME insets state during transitioning.
- final InsetsSource originalImeSource = originalState.peekSource(ITYPE_IME);
+ final InsetsSource originalImeSource = originalState.peekSource(ID_IME);
if (originalImeSource != null) {
final boolean imeVisibility = w.isRequestedVisible(Type.ime());
@@ -446,23 +462,22 @@
* @param caller who changed the insets state.
*/
private void checkAbortTransient(InsetsControlTarget caller) {
- if (mShowingTransientTypes.size() != 0) {
- final IntArray abortTypes = new IntArray();
- final boolean imeRequestedVisible = caller.isRequestedVisible(Type.ime());
- for (int i = mShowingTransientTypes.size() - 1; i >= 0; i--) {
- final @InternalInsetsType int type = mShowingTransientTypes.get(i);
- if ((mStateController.isFakeTarget(type, caller)
- && caller.isRequestedVisible(InsetsState.toPublicType(type)))
- || (type == ITYPE_NAVIGATION_BAR && imeRequestedVisible)) {
- mShowingTransientTypes.remove(i);
- abortTypes.add(type);
- }
- }
- StatusBarManagerInternal statusBarManagerInternal =
- mPolicy.getStatusBarManagerInternal();
- if (abortTypes.size() > 0 && statusBarManagerInternal != null) {
- statusBarManagerInternal.abortTransient(
- mDisplayContent.getDisplayId(), abortTypes.toArray());
+ if (mShowingTransientTypes == 0) {
+ return;
+ }
+ final boolean isImeVisible = mStateController.getImeSourceProvider().isClientVisible();
+ final @InsetsType int fakeControllingTypes =
+ mStateController.getFakeControllingTypes(caller);
+ final @InsetsType int abortTypes =
+ (fakeControllingTypes & caller.getRequestedVisibleTypes())
+ | (isImeVisible ? Type.navigationBars() : 0);
+ mShowingTransientTypes &= ~abortTypes;
+ if (abortTypes != 0) {
+ mDisplayContent.setLayoutNeeded();
+ mDisplayContent.mWmService.requestTraversal();
+ final StatusBarManagerInternal statusBarManager = mPolicy.getStatusBarManagerInternal();
+ if (statusBarManager != null) {
+ statusBarManager.abortTransient(mDisplayContent.getDisplayId(), abortTypes);
}
}
}
@@ -472,12 +487,16 @@
* updateBarControlTarget(mFocusedWin) after this invocation.
*/
private void abortTransient() {
- StatusBarManagerInternal statusBarManagerInternal = mPolicy.getStatusBarManagerInternal();
- if (statusBarManagerInternal != null) {
- statusBarManagerInternal.abortTransient(
- mDisplayContent.getDisplayId(), mShowingTransientTypes.toArray());
+ if (mShowingTransientTypes == 0) {
+ return;
}
- mShowingTransientTypes.clear();
+ final StatusBarManagerInternal statusBarManager = mPolicy.getStatusBarManagerInternal();
+ if (statusBarManager != null) {
+ statusBarManager.abortTransient(mDisplayContent.getDisplayId(), mShowingTransientTypes);
+ }
+ mShowingTransientTypes = 0;
+ mDisplayContent.setLayoutNeeded();
+ mDisplayContent.mWmService.requestTraversal();
dispatchTransientSystemBarsVisibilityChanged(
mFocusedWin,
@@ -487,7 +506,7 @@
private @Nullable InsetsControlTarget getStatusControlTarget(@Nullable WindowState focusedWin,
boolean fake) {
- if (!fake && isShowingTransientTypes(Type.statusBars())) {
+ if (!fake && isTransient(Type.statusBars())) {
return mDummyControlTarget;
}
final WindowState notificationShade = mPolicy.getNotificationShade();
@@ -540,7 +559,7 @@
// configured to be hidden by the IME.
return null;
}
- if (!fake && isShowingTransientTypes(Type.navigationBars())) {
+ if (!fake && isTransient(Type.navigationBars())) {
return mDummyControlTarget;
}
if (focusedWin == mPolicy.getNotificationShade()) {
@@ -576,16 +595,6 @@
return focusedWin;
}
- private boolean isShowingTransientTypes(@InsetsType int types) {
- final IntArray showingTransientTypes = mShowingTransientTypes;
- for (int i = showingTransientTypes.size() - 1; i >= 0; i--) {
- if ((InsetsState.toPublicType(showingTransientTypes.get(i)) & types) != 0) {
- return true;
- }
- }
- return false;
- }
-
/**
* Determines whether the remote insets controller should take control of system bars for all
* windows.
@@ -621,21 +630,17 @@
@VisibleForTesting
void startAnimation(boolean show, Runnable callback) {
- int typesReady = 0;
- final SparseArray<InsetsSourceControl> controls = new SparseArray<>();
- final IntArray showingTransientTypes = mShowingTransientTypes;
- for (int i = showingTransientTypes.size() - 1; i >= 0; i--) {
- final int sourceId = showingTransientTypes.get(i);
- final WindowContainerInsetsSourceProvider provider =
- mStateController.getSourceProvider(sourceId);
- final InsetsSourceControl control = provider.getControl(mDummyControlTarget);
- if (control == null || control.getLeash() == null) {
- continue;
+ @InsetsType int typesReady = 0;
+ final SparseArray<InsetsSourceControl> controlsReady = new SparseArray<>();
+ final InsetsSourceControl[] controls =
+ mStateController.getControlsForDispatch(mDummyControlTarget);
+ for (InsetsSourceControl control : controls) {
+ if (isTransient(control.getType()) && control.getLeash() != null) {
+ typesReady |= control.getType();
+ controlsReady.put(control.getId(), new InsetsSourceControl(control));
}
- typesReady |= control.getType();
- controls.put(sourceId, new InsetsSourceControl(control));
}
- controlAnimationUnchecked(typesReady, controls, show, callback);
+ controlAnimationUnchecked(typesReady, controlsReady, show, callback);
}
private void controlAnimationUnchecked(int typesReady,
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index 49eaea2..2b7a451 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -16,7 +16,7 @@
package com.android.server.wm;
-import static android.view.InsetsState.ITYPE_IME;
+import static android.view.InsetsSource.ID_IME;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_INSETS;
import static com.android.server.wm.InsetsSourceProviderProto.CAPTURED_LEASH;
@@ -164,7 +164,7 @@
// TODO: Ideally, we should wait for the animation to finish so previous window can
// animate-out as new one animates-in.
mWindowContainer.cancelAnimation();
- mWindowContainer.getProvidedInsetsSources().remove(mSource.getId());
+ mWindowContainer.getInsetsSourceProviders().remove(mSource.getId());
mSeamlessRotating = false;
}
ProtoLog.d(WM_DEBUG_WINDOW_INSETS, "InsetsSource setWin %s for type %s",
@@ -180,7 +180,7 @@
mSource.setInsetsRoundedCornerFrame(false);
mSourceFrame.setEmpty();
} else {
- mWindowContainer.getProvidedInsetsSources().put(mSource.getId(), mSource);
+ mWindowContainer.getInsetsSourceProviders().put(mSource.getId(), this);
if (mControllable) {
mWindowContainer.setControllableInsetProvider(this);
if (mPendingControlTarget != null) {
@@ -192,13 +192,6 @@
}
/**
- * @return Whether there is a window container which backs this source.
- */
- boolean hasWindowContainer() {
- return mWindowContainer != null;
- }
-
- /**
* The source frame can affect the layout of other windows, so this should be called once the
* window container gets laid out.
*/
@@ -363,9 +356,9 @@
}
/**
- * @see InsetsStateController#onControlFakeTargetChanged(int, InsetsControlTarget)
+ * @see InsetsStateController#onControlTargetChanged
*/
- void updateControlForFakeTarget(@Nullable InsetsControlTarget fakeTarget) {
+ void updateFakeControlTarget(@Nullable InsetsControlTarget fakeTarget) {
if (fakeTarget == mFakeControlTarget) {
return;
}
@@ -541,7 +534,7 @@
return false;
}
for (int i = 0; i < providers.length; i++) {
- if (providers[i].type == ITYPE_IME) {
+ if (providers[i].type == ID_IME) {
return true;
}
}
@@ -570,6 +563,10 @@
return mControlTarget;
}
+ InsetsControlTarget getFakeControlTarget() {
+ return mFakeControlTarget;
+ }
+
boolean isClientVisible() {
return mClientVisible;
}
@@ -609,15 +606,15 @@
}
if (mControlTarget != null) {
pw.print(prefix + "mControlTarget=");
- pw.println(mControlTarget.getWindow());
+ pw.println(mControlTarget);
}
if (mPendingControlTarget != null) {
pw.print(prefix + "mPendingControlTarget=");
- pw.println(mPendingControlTarget.getWindow());
+ pw.println(mPendingControlTarget);
}
if (mFakeControlTarget != null) {
pw.print(prefix + "mFakeControlTarget=");
- pw.println(mFakeControlTarget.getWindow());
+ pw.println(mFakeControlTarget);
}
}
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 455cd48..0e1e63e 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -17,16 +17,15 @@
package com.android.server.wm;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
-import static android.view.InsetsState.ITYPE_CLIMATE_BAR;
-import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_IME;
-import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_STATUS_BAR;
+import static android.view.InsetsSource.ID_IME;
import static android.view.WindowInsets.Type.displayCutout;
+import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.mandatorySystemGestures;
import static android.view.WindowInsets.Type.systemGestures;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_IME;
+import static com.android.server.wm.DisplayContentProto.IME_INSETS_SOURCE_PROVIDER;
+import static com.android.server.wm.DisplayContentProto.INSETS_SOURCE_PROVIDERS;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -34,11 +33,12 @@
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.SparseArray;
+import android.util.proto.ProtoOutputStream;
import android.view.InsetsSource;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
-import android.view.InsetsState.InternalInsetsType;
import android.view.WindowInsets;
+import android.view.WindowInsets.Type.InsetsType;
import com.android.internal.protolog.common.ProtoLog;
import com.android.server.inputmethod.InputMethodManagerInternal;
@@ -46,7 +46,6 @@
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.function.Consumer;
-import java.util.function.Function;
/**
* Manages global window inset state in the system represented by {@link InsetsState}.
@@ -57,14 +56,11 @@
private final InsetsState mState = new InsetsState();
private final DisplayContent mDisplayContent;
- private final ArrayMap<Integer, WindowContainerInsetsSourceProvider> mProviders =
- new ArrayMap<>();
- private final ArrayMap<InsetsControlTarget, ArrayList<Integer>> mControlTargetTypeMap =
- new ArrayMap<>();
- private final SparseArray<InsetsControlTarget> mTypeControlTargetMap = new SparseArray<>();
-
- /** @see #onControlFakeTargetChanged */
- private final SparseArray<InsetsControlTarget> mTypeFakeControlTargetMap = new SparseArray<>();
+ private final SparseArray<WindowContainerInsetsSourceProvider> mProviders = new SparseArray<>();
+ private final ArrayMap<InsetsControlTarget, ArrayList<InsetsSourceProvider>>
+ mControlTargetProvidersMap = new ArrayMap<>();
+ private final SparseArray<InsetsControlTarget> mIdControlTargetMap = new SparseArray<>();
+ private final SparseArray<InsetsControlTarget> mIdFakeControlTargetMap = new SparseArray<>();
private final ArraySet<InsetsControlTarget> mPendingControlChanged = new ArraySet<>();
@@ -89,17 +85,8 @@
}
};
- private final Function<Integer, WindowContainerInsetsSourceProvider> mSourceProviderFunc;
-
InsetsStateController(DisplayContent displayContent) {
mDisplayContent = displayContent;
- mSourceProviderFunc = type -> {
- final InsetsSource source = mState.getSource(type);
- if (type == ITYPE_IME) {
- return new ImeInsetsSourceProvider(source, this, mDisplayContent);
- }
- return new WindowContainerInsetsSourceProvider(source, this, mDisplayContent);
- };
}
InsetsState getRawInsetsState() {
@@ -107,39 +94,55 @@
}
@Nullable InsetsSourceControl[] getControlsForDispatch(InsetsControlTarget target) {
- ArrayList<Integer> controlled = mControlTargetTypeMap.get(target);
+ final ArrayList<InsetsSourceProvider> controlled = mControlTargetProvidersMap.get(target);
if (controlled == null) {
return null;
}
final int size = controlled.size();
final InsetsSourceControl[] result = new InsetsSourceControl[size];
for (int i = 0; i < size; i++) {
- result[i] = mProviders.get(controlled.get(i)).getControl(target);
+ result[i] = controlled.get(i).getControl(target);
}
return result;
}
- ArrayMap<Integer, WindowContainerInsetsSourceProvider> getSourceProviders() {
+ SparseArray<WindowContainerInsetsSourceProvider> getSourceProviders() {
return mProviders;
}
/**
- * @return The provider of a specific type.
+ * @return The provider of a specific source ID.
*/
- WindowContainerInsetsSourceProvider getSourceProvider(@InternalInsetsType int type) {
- return mProviders.computeIfAbsent(type, mSourceProviderFunc);
+ WindowContainerInsetsSourceProvider getOrCreateSourceProvider(int id, @InsetsType int type) {
+ WindowContainerInsetsSourceProvider provider = mProviders.get(id);
+ if (provider != null) {
+ return provider;
+ }
+ final InsetsSource source = mState.getOrCreateSource(id, type);
+ provider = id == ID_IME
+ ? new ImeInsetsSourceProvider(source, this, mDisplayContent)
+ : new WindowContainerInsetsSourceProvider(source, this, mDisplayContent);
+ mProviders.put(id, provider);
+ return provider;
}
ImeInsetsSourceProvider getImeSourceProvider() {
- return (ImeInsetsSourceProvider) getSourceProvider(ITYPE_IME);
+ return (ImeInsetsSourceProvider) getOrCreateSourceProvider(ID_IME, ime());
+ }
+
+ void removeSourceProvider(int id) {
+ if (id != ID_IME) {
+ mState.removeSource(id);
+ mProviders.remove(id);
+ }
}
/**
- * @return The provider of a specific type or null if we don't have it.
+ * @return The provider of a source ID or null if we don't have it.
*/
@Nullable
- WindowContainerInsetsSourceProvider peekSourceProvider(@InternalInsetsType int type) {
- return mProviders.get(type);
+ WindowContainerInsetsSourceProvider peekSourceProvider(int id) {
+ return mProviders.get(id);
}
/**
@@ -207,8 +210,16 @@
}
}
- boolean isFakeTarget(@InternalInsetsType int type, InsetsControlTarget target) {
- return mTypeFakeControlTargetMap.get(type) == target;
+ @InsetsType int getFakeControllingTypes(InsetsControlTarget target) {
+ @InsetsType int types = 0;
+ for (int i = mProviders.size() - 1; i >= 0; i--) {
+ final InsetsSourceProvider provider = mProviders.valueAt(i);
+ final InsetsControlTarget fakeControlTarget = provider.getFakeControlTarget();
+ if (target == fakeControlTarget) {
+ types |= provider.getSource().getType();
+ }
+ }
+ return types;
}
void onImeControlTargetChanged(@Nullable InsetsControlTarget imeTarget) {
@@ -216,7 +227,7 @@
// Make sure that we always have a control target for the IME, even if the IME target is
// null. Otherwise there is no leash that will hide it and IME becomes "randomly" visible.
InsetsControlTarget target = imeTarget != null ? imeTarget : mEmptyImeControlTarget;
- onControlChanged(ITYPE_IME, target);
+ onControlTargetChanged(getImeSourceProvider(), target, false /* fake */);
ProtoLog.d(WM_DEBUG_IME, "onImeControlTargetChanged %s",
target != null ? target.getWindow() : "null");
notifyPendingInsetsControlChanged();
@@ -234,101 +245,88 @@
@Nullable InsetsControlTarget fakeStatusControlling,
@Nullable InsetsControlTarget navControlling,
@Nullable InsetsControlTarget fakeNavControlling) {
- onControlChanged(ITYPE_STATUS_BAR, statusControlling);
- onControlChanged(ITYPE_NAVIGATION_BAR, navControlling);
- onControlChanged(ITYPE_CLIMATE_BAR, statusControlling);
- onControlChanged(ITYPE_EXTRA_NAVIGATION_BAR, navControlling);
- onControlFakeTargetChanged(ITYPE_STATUS_BAR, fakeStatusControlling);
- onControlFakeTargetChanged(ITYPE_NAVIGATION_BAR, fakeNavControlling);
- onControlFakeTargetChanged(ITYPE_CLIMATE_BAR, fakeStatusControlling);
- onControlFakeTargetChanged(ITYPE_EXTRA_NAVIGATION_BAR, fakeNavControlling);
+ for (int i = mProviders.size() - 1; i >= 0; i--) {
+ final InsetsSourceProvider provider = mProviders.valueAt(i);
+ final @InsetsType int type = provider.getSource().getType();
+ if (type == WindowInsets.Type.statusBars()) {
+ onControlTargetChanged(provider, statusControlling, false /* fake */);
+ onControlTargetChanged(provider, fakeStatusControlling, true /* fake */);
+ } else if (type == WindowInsets.Type.navigationBars()) {
+ onControlTargetChanged(provider, navControlling, false /* fake */);
+ onControlTargetChanged(provider, fakeNavControlling, true /* fake */);
+ }
+ }
notifyPendingInsetsControlChanged();
}
void notifyControlRevoked(@NonNull InsetsControlTarget previousControlTarget,
InsetsSourceProvider provider) {
- removeFromControlMaps(previousControlTarget, provider.getSource().getId(),
- false /* fake */);
+ removeFromControlMaps(previousControlTarget, provider, false /* fake */);
}
- private void onControlChanged(@InternalInsetsType int type,
- @Nullable InsetsControlTarget target) {
- final InsetsControlTarget previous = mTypeControlTargetMap.get(type);
- if (target == previous) {
- return;
- }
- final WindowContainerInsetsSourceProvider provider = mProviders.get(type);
- if (provider == null) {
+ private void onControlTargetChanged(InsetsSourceProvider provider,
+ @Nullable InsetsControlTarget target, boolean fake) {
+ final InsetsControlTarget lastTarget = fake
+ ? mIdFakeControlTargetMap.get(provider.getSource().getId())
+ : mIdControlTargetMap.get(provider.getSource().getId());
+ if (target == lastTarget) {
return;
}
if (!provider.isControllable()) {
return;
}
- provider.updateControlForTarget(target, false /* force */);
- target = provider.getControlTarget();
- if (previous != null) {
- removeFromControlMaps(previous, type, false /* fake */);
- mPendingControlChanged.add(previous);
+ if (fake) {
+ // The fake target updated here will be used to pretend to the app that it's still under
+ // control of the bars while it's not really, but we still need to find out the apps
+ // intentions around showing/hiding. For example, when the transient bars are showing,
+ // and the fake target requests to show system bars, the transient state will be
+ // aborted.
+ provider.updateFakeControlTarget(target);
+ } else {
+ provider.updateControlForTarget(target, false /* force */);
+
+ // Get control target again in case the provider didn't accept the one we passed to it.
+ target = provider.getControlTarget();
+ if (target == lastTarget) {
+ return;
+ }
+ }
+ if (lastTarget != null) {
+ removeFromControlMaps(lastTarget, provider, fake);
+ mPendingControlChanged.add(lastTarget);
}
if (target != null) {
- addToControlMaps(target, type, false /* fake */);
+ addToControlMaps(target, provider, fake);
mPendingControlChanged.add(target);
}
}
- /**
- * The fake target saved here will be used to pretend to the app that it's still under control
- * of the bars while it's not really, but we still need to find out the apps intentions around
- * showing/hiding. For example, when the transient bars are showing, and the fake target
- * requests to show system bars, the transient state will be aborted.
- */
- void onControlFakeTargetChanged(@InternalInsetsType int type,
- @Nullable InsetsControlTarget fakeTarget) {
- final InsetsControlTarget previous = mTypeFakeControlTargetMap.get(type);
- if (fakeTarget == previous) {
- return;
- }
- final WindowContainerInsetsSourceProvider provider = mProviders.get(type);
- if (provider == null) {
- return;
- }
- provider.updateControlForFakeTarget(fakeTarget);
- if (previous != null) {
- removeFromControlMaps(previous, type, true /* fake */);
- mPendingControlChanged.add(previous);
- }
- if (fakeTarget != null) {
- addToControlMaps(fakeTarget, type, true /* fake */);
- mPendingControlChanged.add(fakeTarget);
- }
- }
-
private void removeFromControlMaps(@NonNull InsetsControlTarget target,
- @InternalInsetsType int type, boolean fake) {
- final ArrayList<Integer> array = mControlTargetTypeMap.get(target);
+ InsetsSourceProvider provider, boolean fake) {
+ final ArrayList<InsetsSourceProvider> array = mControlTargetProvidersMap.get(target);
if (array == null) {
return;
}
- array.remove((Integer) type);
+ array.remove(provider);
if (array.isEmpty()) {
- mControlTargetTypeMap.remove(target);
+ mControlTargetProvidersMap.remove(target);
}
if (fake) {
- mTypeFakeControlTargetMap.remove(type);
+ mIdFakeControlTargetMap.remove(provider.getSource().getId());
} else {
- mTypeControlTargetMap.remove(type);
+ mIdControlTargetMap.remove(provider.getSource().getId());
}
}
private void addToControlMaps(@NonNull InsetsControlTarget target,
- @InternalInsetsType int type, boolean fake) {
- final ArrayList<Integer> array = mControlTargetTypeMap.computeIfAbsent(target,
- key -> new ArrayList<>());
- array.add(type);
+ InsetsSourceProvider provider, boolean fake) {
+ final ArrayList<InsetsSourceProvider> array = mControlTargetProvidersMap.computeIfAbsent(
+ target, key -> new ArrayList<>());
+ array.add(provider);
if (fake) {
- mTypeFakeControlTargetMap.put(type, target);
+ mIdFakeControlTargetMap.put(provider.getSource().getId(), target);
} else {
- mTypeControlTargetMap.put(type, target);
+ mIdControlTargetMap.put(provider.getSource().getId(), target);
}
}
@@ -350,7 +348,7 @@
for (int i = mPendingControlChanged.size() - 1; i >= 0; i--) {
final InsetsControlTarget controlTarget = mPendingControlChanged.valueAt(i);
controlTarget.notifyInsetsControlChanged();
- if (mControlTargetTypeMap.containsKey(controlTarget)) {
+ if (mControlTargetProvidersMap.containsKey(controlTarget)) {
// We only collect targets who get controls, not lose controls.
newControlTargets.add(controlTarget);
}
@@ -376,14 +374,40 @@
prefix = prefix + " ";
mState.dump(prefix, pw);
pw.println(prefix + "Control map:");
- for (int i = mTypeControlTargetMap.size() - 1; i >= 0; i--) {
+ for (int i = mControlTargetProvidersMap.size() - 1; i >= 0; i--) {
+ final InsetsControlTarget controlTarget = mControlTargetProvidersMap.keyAt(i);
pw.print(prefix + " ");
- pw.println(InsetsState.typeToString(mTypeControlTargetMap.keyAt(i)) + " -> "
- + mTypeControlTargetMap.valueAt(i));
+ pw.print(controlTarget);
+ pw.println(":");
+ final ArrayList<InsetsSourceProvider> providers = mControlTargetProvidersMap.valueAt(i);
+ for (int j = providers.size() - 1; j >= 0; j--) {
+ final InsetsSourceProvider provider = providers.get(j);
+ if (provider != null) {
+ pw.print(prefix + " ");
+ if (controlTarget == provider.getFakeControlTarget()) {
+ pw.print("(fake) ");
+ }
+ pw.println(provider.getControl(controlTarget));
+ }
+ }
+ }
+ if (mControlTargetProvidersMap.isEmpty()) {
+ pw.print(prefix + " none");
}
pw.println(prefix + "InsetsSourceProviders:");
for (int i = mProviders.size() - 1; i >= 0; i--) {
mProviders.valueAt(i).dump(pw, prefix + " ");
}
}
+
+ void dumpDebug(ProtoOutputStream proto, @WindowTraceLogLevel int logLevel) {
+ for (int i = mProviders.size() - 1; i >= 0; i--) {
+ final InsetsSourceProvider provider = mProviders.valueAt(i);
+ provider.dumpDebug(proto,
+ provider.getSource().getType() == ime()
+ ? IME_INSETS_SOURCE_PROVIDER
+ : INSETS_SOURCE_PROVIDERS,
+ logLevel);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index b64420a..7066a33 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -19,14 +19,13 @@
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ALLOW_IGNORE_ORIENTATION_REQUEST;
+import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ENABLE_CAMERA_COMPAT_TREATMENT;
import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
import android.graphics.Color;
import android.provider.DeviceConfig;
import android.util.Slog;
@@ -310,6 +309,9 @@
mIsDisplayRotationImmersiveAppCompatPolicyEnabled = mContext.getResources().getBoolean(
R.bool.config_letterboxIsDisplayRotationImmersiveAppCompatPolicyEnabled);
mDeviceConfig.updateFlagActiveStatus(
+ /* isActive */ mIsCameraCompatTreatmentEnabled,
+ /* key */ KEY_ENABLE_CAMERA_COMPAT_TREATMENT);
+ mDeviceConfig.updateFlagActiveStatus(
/* isActive */ mIsDisplayRotationImmersiveAppCompatPolicyEnabled,
/* key */ KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY);
mDeviceConfig.updateFlagActiveStatus(
@@ -1062,38 +1064,8 @@
"enable_translucent_activity_letterbox", false);
}
- @VisibleForTesting
- boolean getPackageManagerProperty(PackageManager pm, String property) {
- boolean enabled = false;
- try {
- final PackageManager.Property p = pm.getProperty(property, mContext.getPackageName());
- enabled = p.getBoolean();
- } catch (PackageManager.NameNotFoundException e) {
- // Property not found
- }
- return enabled;
- }
-
- @VisibleForTesting
- boolean isCompatFakeFocusEnabled(ActivityInfo info) {
- if (!isCompatFakeFocusEnabledOnDevice()) {
- return false;
- }
- // See if the developer has chosen to opt in / out of treatment
- PackageManager pm = mContext.getPackageManager();
- if (getPackageManagerProperty(pm, PROPERTY_COMPAT_FAKE_FOCUS_OPT_OUT)) {
- return false;
- } else if (getPackageManagerProperty(pm, PROPERTY_COMPAT_FAKE_FOCUS_OPT_IN)) {
- return true;
- }
- if (info.isChangeEnabled(ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS)) {
- return true;
- }
- return false;
- }
-
/** Whether fake sending focus is enabled for unfocused apps in splitscreen */
- boolean isCompatFakeFocusEnabledOnDevice() {
+ boolean isCompatFakeFocusEnabled() {
return mIsCompatFakeFocusEnabled
&& DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
DEVICE_CONFIG_KEY_ENABLE_COMPAT_FAKE_FOCUS, true);
@@ -1118,15 +1090,8 @@
/** Whether camera compatibility treatment is enabled. */
boolean isCameraCompatTreatmentEnabled(boolean checkDeviceConfig) {
- return mIsCameraCompatTreatmentEnabled
- && (!checkDeviceConfig || isCameraCompatTreatmentAllowed());
- }
-
- // TODO(b/262977416): Cache a runtime flag and implement
- // DeviceConfig.OnPropertiesChangedListener
- private static boolean isCameraCompatTreatmentAllowed() {
- return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
- "enable_compat_camera_treatment", true);
+ return mIsCameraCompatTreatmentEnabled && (!checkDeviceConfig
+ || mDeviceConfig.getFlag(KEY_ENABLE_CAMERA_COMPAT_TREATMENT));
}
/** Whether camera compatibility refresh is enabled. */
diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java b/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java
index 3f067e3..d004fa6 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java
@@ -33,6 +33,9 @@
final class LetterboxConfigurationDeviceConfig
implements DeviceConfig.OnPropertiesChangedListener {
+ static final String KEY_ENABLE_CAMERA_COMPAT_TREATMENT = "enable_compat_camera_treatment";
+ private static final boolean DEFAULT_VALUE_ENABLE_CAMERA_COMPAT_TREATMENT = true;
+
static final String KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY =
"enable_display_rotation_immersive_app_compat_policy";
private static final boolean DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY =
@@ -44,12 +47,19 @@
@VisibleForTesting
static final Map<String, Boolean> sKeyToDefaultValueMap = Map.of(
+ KEY_ENABLE_CAMERA_COMPAT_TREATMENT,
+ DEFAULT_VALUE_ENABLE_CAMERA_COMPAT_TREATMENT,
KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY,
DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY,
KEY_ALLOW_IGNORE_ORIENTATION_REQUEST,
DEFAULT_VALUE_ALLOW_IGNORE_ORIENTATION_REQUEST
);
+ // Whether camera compatibility treatment is enabled.
+ // See DisplayRotationCompatPolicy for context.
+ private boolean mIsCameraCompatTreatmentEnabled =
+ DEFAULT_VALUE_ENABLE_CAMERA_COMPAT_TREATMENT;
+
// Whether enabling rotation compat policy for immersive apps that prevents auto rotation
// into non-optimal screen orientation while in fullscreen. This is needed because immersive
// apps, such as games, are often not optimized for all orientations and can have a poor UX
@@ -101,6 +111,8 @@
*/
boolean getFlag(String key) {
switch (key) {
+ case KEY_ENABLE_CAMERA_COMPAT_TREATMENT:
+ return mIsCameraCompatTreatmentEnabled;
case KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY:
return mIsDisplayRotationImmersiveAppCompatPolicyEnabled;
case KEY_ALLOW_IGNORE_ORIENTATION_REQUEST:
@@ -116,6 +128,10 @@
throw new AssertionError("Haven't found default value for flag: " + key);
}
switch (key) {
+ case KEY_ENABLE_CAMERA_COMPAT_TREATMENT:
+ mIsCameraCompatTreatmentEnabled =
+ getDeviceConfig(key, defaultValue);
+ break;
case KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY:
mIsDisplayRotationImmersiveAppCompatPolicyEnabled =
getDeviceConfig(key, defaultValue);
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index c5a50ca..9681789 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -21,8 +21,11 @@
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS;
import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
+import static android.content.pm.ActivityInfo.OVERRIDE_RESPECT_REQUESTED_ORIENTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT;
import static android.content.pm.ActivityInfo.OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION;
@@ -41,6 +44,7 @@
import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS;
import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
@@ -136,8 +140,12 @@
private final boolean mIsOverrideToNosensorOrientationEnabled;
// Corresponds to OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE
private final boolean mIsOverrideToReverseLandscapeOrientationEnabled;
+ // Corresponds to OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA
+ private final boolean mIsOverrideOrientationOnlyForCameraEnabled;
// Corresponds to OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION
private final boolean mIsOverrideUseDisplayLandscapeNaturalOrientationEnabled;
+ // Corresponds to OVERRIDE_RESPECT_REQUESTED_ORIENTATION
+ private final boolean mIsOverrideRespectRequestedOrientationEnabled;
// Corresponds to OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION
private final boolean mIsOverrideCameraCompatDisableForceRotationEnabled;
@@ -149,6 +157,9 @@
// Corresponds to OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION
private final boolean mIsOverrideEnableCompatIgnoreRequestedOrientationEnabled;
+ // Corresponds to OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS
+ private final boolean mIsOverrideEnableCompatFakeFocusEnabled;
+
@Nullable
private final Boolean mBooleanPropertyAllowOrientationOverride;
@Nullable
@@ -204,6 +215,9 @@
@Nullable
private final Boolean mBooleanPropertyIgnoreRequestedOrientation;
+ @Nullable
+ private final Boolean mBooleanPropertyFakeFocus;
+
private boolean mIsRelauchingAfterRequestedOrientationChanged;
LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) {
@@ -218,6 +232,10 @@
readComponentProperty(packageManager, mActivityRecord.packageName,
mLetterboxConfiguration::isPolicyForIgnoringRequestedOrientationEnabled,
PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION);
+ mBooleanPropertyFakeFocus =
+ readComponentProperty(packageManager, mActivityRecord.packageName,
+ mLetterboxConfiguration::isCompatFakeFocusEnabled,
+ PROPERTY_COMPAT_ENABLE_FAKE_FOCUS);
mBooleanPropertyCameraCompatAllowForceRotation =
readComponentProperty(packageManager, mActivityRecord.packageName,
() -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
@@ -253,8 +271,12 @@
isCompatChangeEnabled(OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE);
mIsOverrideToNosensorOrientationEnabled =
isCompatChangeEnabled(OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR);
+ mIsOverrideOrientationOnlyForCameraEnabled =
+ isCompatChangeEnabled(OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA);
mIsOverrideUseDisplayLandscapeNaturalOrientationEnabled =
isCompatChangeEnabled(OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION);
+ mIsOverrideRespectRequestedOrientationEnabled =
+ isCompatChangeEnabled(OVERRIDE_RESPECT_REQUESTED_ORIENTATION);
mIsOverrideCameraCompatDisableForceRotationEnabled =
isCompatChangeEnabled(OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION);
@@ -265,6 +287,9 @@
mIsOverrideEnableCompatIgnoreRequestedOrientationEnabled =
isCompatChangeEnabled(OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION);
+
+ mIsOverrideEnableCompatFakeFocusEnabled =
+ isCompatChangeEnabled(OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS);
}
/**
@@ -360,6 +385,25 @@
}
/**
+ * Whether sending compat fake focus for split screen resumed activities is enabled. Needed
+ * because some game engines wait to get focus before drawing the content of the app which isn't
+ * guaranteed by default in multi-window modes.
+ *
+ * <p>This treatment is enabled when the following conditions are met:
+ * <ul>
+ * <li>Flag gating the treatment is enabled
+ * <li>Component property is NOT set to false
+ * <li>Component property is set to true or per-app override is enabled
+ * </ul>
+ */
+ boolean shouldSendFakeFocus() {
+ return shouldEnableWithOverrideAndProperty(
+ /* gatingCondition */ mLetterboxConfiguration::isCompatFakeFocusEnabled,
+ mIsOverrideEnableCompatFakeFocusEnabled,
+ mBooleanPropertyFakeFocus);
+ }
+
+ /**
* Sets whether an activity is relaunching after the app has called {@link
* android.app.Activity#setRequestedOrientation}.
*/
@@ -379,6 +423,10 @@
mIsRefreshAfterRotationRequested = isRequested;
}
+ boolean isOverrideRespectRequestedOrientationEnabled() {
+ return mIsOverrideRespectRequestedOrientationEnabled;
+ }
+
/**
* Whether should fix display orientation to landscape natural orientation when a task is
* fullscreen and the display is ignoring orientation requests.
@@ -410,6 +458,14 @@
return candidate;
}
+ DisplayContent displayContent = mActivityRecord.mDisplayContent;
+ if (mIsOverrideOrientationOnlyForCameraEnabled && displayContent != null
+ && (displayContent.mDisplayRotationCompatPolicy == null
+ || !displayContent.mDisplayRotationCompatPolicy
+ .isActivityEligibleForOrientationOverride(mActivityRecord))) {
+ return candidate;
+ }
+
if (mIsOverrideToReverseLandscapeOrientationEnabled
&& (isFixedOrientationLandscape(candidate) || mIsOverrideAnyOrientationEnabled)) {
Slog.w(TAG, "Requested orientation " + screenOrientationToString(candidate) + " for "
@@ -439,6 +495,10 @@
return candidate;
}
+ boolean isOverrideOrientationOnlyForCameraEnabled() {
+ return mIsOverrideOrientationOnlyForCameraEnabled;
+ }
+
/**
* Whether activity is eligible for activity "refresh" after camera compat force rotation
* treatment. See {@link DisplayRotationCompatPolicy} for context.
diff --git a/services/core/java/com/android/server/wm/RefreshRatePolicy.java b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
index de42c55..f952adb 100644
--- a/services/core/java/com/android/server/wm/RefreshRatePolicy.java
+++ b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
@@ -59,6 +59,8 @@
}
}
+ private final DisplayInfo mDisplayInfo;
+ private final Mode mDefaultMode;
private final Mode mLowRefreshRateMode;
private final PackageRefreshRate mNonHighRefreshRatePackages = new PackageRefreshRate();
private final HighRefreshRateDenylist mHighRefreshRateDenylist;
@@ -89,7 +91,9 @@
RefreshRatePolicy(WindowManagerService wmService, DisplayInfo displayInfo,
HighRefreshRateDenylist denylist) {
- mLowRefreshRateMode = findLowRefreshRateMode(displayInfo);
+ mDisplayInfo = displayInfo;
+ mDefaultMode = displayInfo.getDefaultMode();
+ mLowRefreshRateMode = findLowRefreshRateMode(displayInfo, mDefaultMode);
mHighRefreshRateDenylist = denylist;
mWmService = wmService;
}
@@ -98,10 +102,9 @@
* Finds the mode id with the lowest refresh rate which is >= 60hz and same resolution as the
* default mode.
*/
- private Mode findLowRefreshRateMode(DisplayInfo displayInfo) {
- Mode mode = displayInfo.getDefaultMode();
+ private Mode findLowRefreshRateMode(DisplayInfo displayInfo, Mode defaultMode) {
float[] refreshRates = displayInfo.getDefaultRefreshRates();
- float bestRefreshRate = mode.getRefreshRate();
+ float bestRefreshRate = defaultMode.getRefreshRate();
mMinSupportedRefreshRate = bestRefreshRate;
mMaxSupportedRefreshRate = bestRefreshRate;
for (int i = refreshRates.length - 1; i >= 0; i--) {
@@ -127,13 +130,39 @@
}
int getPreferredModeId(WindowState w) {
- // If app is animating, it's not able to control refresh rate because we want the animation
- // to run in default refresh rate.
- if (w.isAnimating(TRANSITION | PARENTS)) {
+ final int preferredDisplayModeId = w.mAttrs.preferredDisplayModeId;
+ if (preferredDisplayModeId <= 0) {
+ // Unspecified, use default mode.
return 0;
}
- return w.mAttrs.preferredDisplayModeId;
+ // If app is animating, it's not able to control refresh rate because we want the animation
+ // to run in default refresh rate. But if the display size of default mode is different
+ // from the using preferred mode, then still keep the preferred mode to avoid disturbing
+ // the animation.
+ if (w.isAnimating(TRANSITION | PARENTS)) {
+ Display.Mode preferredMode = null;
+ for (Display.Mode mode : mDisplayInfo.supportedModes) {
+ if (preferredDisplayModeId == mode.getModeId()) {
+ preferredMode = mode;
+ break;
+ }
+ }
+ if (preferredMode != null) {
+ final int pW = preferredMode.getPhysicalWidth();
+ final int pH = preferredMode.getPhysicalHeight();
+ if ((pW != mDefaultMode.getPhysicalWidth()
+ || pH != mDefaultMode.getPhysicalHeight())
+ && pW == mDisplayInfo.getNaturalWidth()
+ && pH == mDisplayInfo.getNaturalHeight()) {
+ // Prefer not to change display size when animating.
+ return preferredDisplayModeId;
+ }
+ }
+ return 0;
+ }
+
+ return preferredDisplayModeId;
}
/**
@@ -234,14 +263,10 @@
if (refreshRateSwitchingType != SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY) {
final int preferredModeId = w.mAttrs.preferredDisplayModeId;
if (preferredModeId > 0) {
- DisplayInfo info = w.getDisplayInfo();
- if (info != null) {
- for (Display.Mode mode : info.supportedModes) {
- if (preferredModeId == mode.getModeId()) {
- return w.mFrameRateVote.update(mode.getRefreshRate(),
- Surface.FRAME_RATE_COMPATIBILITY_EXACT);
-
- }
+ for (Display.Mode mode : mDisplayInfo.supportedModes) {
+ if (preferredModeId == mode.getModeId()) {
+ return w.mFrameRateVote.update(mode.getRefreshRate(),
+ Surface.FRAME_RATE_COMPATIBILITY_EXACT);
}
}
}
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index e8aa2c8..110cce2 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -136,6 +136,7 @@
import android.view.DisplayInfo;
import android.view.SurfaceControl;
import android.view.WindowManager;
+import android.window.TaskFragmentAnimationParams;
import android.window.WindowContainerToken;
import com.android.internal.annotations.VisibleForTesting;
@@ -234,6 +235,10 @@
WindowManagerService mWindowManager;
DisplayManager mDisplayManager;
private DisplayManagerInternal mDisplayManagerInternal;
+ @NonNull
+ private final DeviceStateController mDeviceStateController;
+ @NonNull
+ private final DisplayRotationCoordinator mDisplayRotationCoordinator;
/** Reference to default display so we can quickly look it up. */
private DisplayContent mDefaultDisplay;
@@ -380,9 +385,9 @@
}
ProtoLog.d(WM_DEBUG_TASKS, "Comparing existing cls=%s /aff=%s to new cls=%s /aff=%s",
- r.getTask().rootAffinity, mIntent.getComponent().flattenToShortString(),
- mInfo.taskAffinity, (task.realActivity != null
- ? task.realActivity.flattenToShortString() : ""));
+ (task.realActivity != null ? task.realActivity.flattenToShortString() : ""),
+ task.rootAffinity, mIntent.getComponent().flattenToShortString(),
+ mTaskAffinity);
// TODO Refactor to remove duplications. Check if logic can be simplified.
if (task.realActivity != null && task.realActivity.compareTo(cls) == 0
&& Objects.equals(documentData, taskDocumentData)) {
@@ -440,6 +445,8 @@
mTaskSupervisor = mService.mTaskSupervisor;
mTaskSupervisor.mRootWindowContainer = this;
mDisplayOffTokenAcquirer = mService.new SleepTokenAcquirerImpl(DISPLAY_OFF_SLEEP_TOKEN_TAG);
+ mDeviceStateController = new DeviceStateController(service.mContext, service.mH);
+ mDisplayRotationCoordinator = new DisplayRotationCoordinator();
}
/**
@@ -1279,7 +1286,8 @@
final Display[] displays = mDisplayManager.getDisplays();
for (int displayNdx = 0; displayNdx < displays.length; ++displayNdx) {
final Display display = displays[displayNdx];
- final DisplayContent displayContent = new DisplayContent(display, this);
+ final DisplayContent displayContent =
+ new DisplayContent(display, this, mDeviceStateController);
addChild(displayContent, POSITION_BOTTOM);
if (displayContent.mDisplayId == DEFAULT_DISPLAY) {
mDefaultDisplay = displayContent;
@@ -1297,6 +1305,10 @@
return mDefaultDisplay;
}
+ DisplayRotationCoordinator getDisplayRotationCoordinator() {
+ return mDisplayRotationCoordinator;
+ }
+
/**
* Get the default display area on the device dedicated to app windows. This one should be used
* only as a fallback location for activity launches when no target display area is specified,
@@ -1358,7 +1370,7 @@
return null;
}
// The display hasn't been added to ActivityManager yet, create a new record now.
- displayContent = new DisplayContent(display, this);
+ displayContent = new DisplayContent(display, this, mDeviceStateController);
addChild(displayContent, POSITION_BOTTOM);
return displayContent;
}
@@ -2082,6 +2094,8 @@
return;
}
tf.resetAdjacentTaskFragment();
+ tf.setCompanionTaskFragment(null /* companionTaskFragment */);
+ tf.setAnimationParams(TaskFragmentAnimationParams.DEFAULT);
if (tf.getTopNonFinishingActivity() != null) {
// When the Task is entering picture-in-picture, we should clear all override
// from the client organizer, so the PIP activity can get the correct config
diff --git a/services/core/java/com/android/server/wm/SafeActivityOptions.java b/services/core/java/com/android/server/wm/SafeActivityOptions.java
index 98ca9ae..5860776 100644
--- a/services/core/java/com/android/server/wm/SafeActivityOptions.java
+++ b/services/core/java/com/android/server/wm/SafeActivityOptions.java
@@ -146,8 +146,8 @@
.setLaunchRootTask(options.getLaunchRootTask())
.setPendingIntentBackgroundActivityStartMode(
options.getPendingIntentBackgroundActivityStartMode())
- .setIgnorePendingIntentCreatorForegroundState(
- options.getIgnorePendingIntentCreatorForegroundState());
+ .setPendingIntentCreatorBackgroundActivityStartMode(
+ options.getPendingIntentCreatorBackgroundActivityStartMode());
}
/**
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index db88f0f..b5df3e0 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -799,6 +799,10 @@
if (mDisplayContent.getRotationAnimation() == ScreenRotationAnimation.this) {
// It also invokes kill().
mDisplayContent.setRotationAnimation(null);
+ if (mDisplayContent.mDisplayRotationCompatPolicy != null) {
+ mDisplayContent.mDisplayRotationCompatPolicy
+ .onScreenRotationAnimationFinished();
+ }
} else {
kill();
}
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index ee11f68..1e53cc3 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -149,7 +149,6 @@
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
-import android.graphics.Insets;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
@@ -171,6 +170,7 @@
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import android.view.DisplayInfo;
+import android.view.InsetsSource;
import android.view.InsetsState;
import android.view.RemoteAnimationAdapter;
import android.view.Surface;
@@ -2833,14 +2833,13 @@
void adjustAnimationBoundsForTransition(Rect animationBounds) {
TaskTransitionSpec spec = mWmService.mTaskTransitionSpec;
if (spec != null) {
- for (@InsetsState.InternalInsetsType int insetType : spec.animationBoundInsets) {
- WindowContainerInsetsSourceProvider insetProvider = getDisplayContent()
- .getInsetsStateController()
- .getSourceProvider(insetType);
-
- Insets insets = insetProvider.getSource().calculateVisibleInsets(
- animationBounds);
- animationBounds.inset(insets);
+ final InsetsState state =
+ getDisplayContent().getInsetsStateController().getRawInsetsState();
+ for (int id : spec.animationBoundInsets) {
+ final InsetsSource source = state.peekSource(id);
+ if (source != null) {
+ animationBounds.inset(source.calculateVisibleInsets(animationBounds));
+ }
}
}
}
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index eb06b91..294e90b 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2813,7 +2813,7 @@
void removeImmediately() {
mIsRemovalRequested = false;
resetAdjacentTaskFragment();
- cleanUp();
+ cleanUpEmbeddedTaskFragment();
final boolean shouldExecuteAppTransition =
mClearedTaskFragmentForPip && isTaskVisibleRequested();
super.removeImmediately();
@@ -2830,10 +2830,20 @@
}
/** Called on remove to cleanup. */
- private void cleanUp() {
- if (mIsEmbedded) {
- mAtmService.mWindowOrganizerController.cleanUpEmbeddedTaskFragment(this);
+ private void cleanUpEmbeddedTaskFragment() {
+ if (!mIsEmbedded) {
+ return;
}
+ mAtmService.mWindowOrganizerController.cleanUpEmbeddedTaskFragment(this);
+ final Task task = getTask();
+ if (task == null) {
+ return;
+ }
+ task.forAllLeafTaskFragments(taskFragment -> {
+ if (taskFragment.getCompanionTaskFragment() == this) {
+ taskFragment.setCompanionTaskFragment(null /* companionTaskFragment */);
+ }
+ }, false /* traverseTopToBottom */);
}
@Override
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index d2f30ce..04a2761 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -216,6 +216,12 @@
final TransitionController.Logger mLogger = new TransitionController.Logger();
+ /**
+ * {@code false} if this transition runs purely in WMCore (meaning Shell is completely unaware
+ * of it). Currently, this happens before the display is ready since nothing can be seen yet.
+ */
+ boolean mIsPlayerEnabled = true;
+
Transition(@TransitionType int type, @TransitionFlags int flags,
TransitionController controller, BLASTSyncEngine syncEngine) {
mType = type;
@@ -277,6 +283,17 @@
return false;
}
+ /** @return whether `wc` is a descendent of a transient-hide window. */
+ boolean isInTransientHide(@NonNull WindowContainer wc) {
+ if (mTransientLaunches == null) return false;
+ for (int i = 0; i < mTransientLaunches.size(); ++i) {
+ if (wc.isDescendantOf(mTransientLaunches.valueAt(i))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
boolean isTransientLaunch(@NonNull ActivityRecord activity) {
return mTransientLaunches != null && mTransientLaunches.containsKey(activity);
}
@@ -766,7 +783,7 @@
* be called directly; use {@link TransitionController#finishTransition} instead.
*/
void finishTransition() {
- if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+ if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER) && mIsPlayerEnabled) {
Trace.asyncTraceEnd(TRACE_TAG_WINDOW_MANAGER, TRACE_NAME_PLAY_TRANSITION,
System.identityHashCode(this));
}
@@ -825,6 +842,11 @@
&& ar.isVisible()) {
// Transient launch was committed, so report enteringAnimation
ar.mEnteringAnimation = true;
+ // Since transient launches don't automatically take focus, make sure we
+ // synchronize focus since we committed to the launch.
+ if (ar.isTopRunningActivity()) {
+ ar.moveFocusableActivityToTop("transitionFinished");
+ }
}
continue;
}
@@ -1096,7 +1118,7 @@
controller.setupStartTransaction(transaction);
}
buildFinishTransaction(mFinishTransaction, info.getRootLeash());
- if (mController.getTransitionPlayer() != null) {
+ if (mController.getTransitionPlayer() != null && mIsPlayerEnabled) {
mController.dispatchLegacyAppTransitionStarting(info, mStatusBarTransitionDelay);
try {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
@@ -1112,11 +1134,16 @@
} catch (RemoteException e) {
// If there's an exception when trying to send the mergedTransaction to the
// client, we should finish and apply it here so the transactions aren't lost.
- cleanUpOnFailure();
+ postCleanupOnFailure();
}
} else {
- // No player registered, so just finish/apply immediately
- cleanUpOnFailure();
+ // No player registered or it's not enabled, so just finish/apply immediately
+ if (!mIsPlayerEnabled) {
+ mLogger.mSendTimeNs = SystemClock.uptimeNanos();
+ ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Apply and finish immediately"
+ + " because player is disabled for transition #%d .", mSyncId);
+ }
+ postCleanupOnFailure();
}
mController.mLoggerHandler.post(mLogger::logOnSend);
mOverrideOptions = null;
@@ -1127,6 +1154,14 @@
info.releaseAnimSurfaces();
}
+ private void postCleanupOnFailure() {
+ mController.mAtm.mH.post(() -> {
+ synchronized (mController.mAtm.mGlobalLock) {
+ cleanUpOnFailure();
+ }
+ });
+ }
+
/**
* If the remote failed for any reason, use this to do any appropriate clean-up. Do not call
* this directly, it's designed to by called by {@link TransitionController} only.
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 5e116ba..2c23b5d 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -126,6 +126,13 @@
final Handler mLoggerHandler = FgThread.getHandler();
+ /**
+ * {@code true} While this waits for the display to become enabled (during boot). While waiting
+ * for the display, all core-initiated transitions will be "local".
+ * Note: This defaults to false so that it doesn't interfere with unit tests.
+ */
+ boolean mIsWaitingForDisplayEnabled = false;
+
TransitionController(ActivityTaskManagerService atm,
TaskSnapshotController taskSnapshotController,
TransitionTracer transitionTracer) {
@@ -353,11 +360,37 @@
}
/**
+ * During transient-launch, the "behind" app should retain focus during the transition unless
+ * something takes focus from it explicitly (eg. by calling ATMS.setFocusedTask or by another
+ * transition interrupting this one.
+ *
+ * The rules around this are basically: if there is exactly one active transition and `wc` is
+ * the "behind" of a transient launch, then it can retain focus.
+ */
+ boolean shouldKeepFocus(@NonNull WindowContainer wc) {
+ if (mCollectingTransition != null) {
+ if (!mPlayingTransitions.isEmpty()) return false;
+ return mCollectingTransition.isInTransientHide(wc);
+ } else if (mPlayingTransitions.size() == 1) {
+ return mPlayingTransitions.get(0).isInTransientHide(wc);
+ }
+ return false;
+ }
+
+ /**
+ * @return {@code true} if {@param ar} is part of a transient-launch activity in the
+ * collecting transition.
+ */
+ boolean isTransientCollect(@NonNull ActivityRecord ar) {
+ return mCollectingTransition != null && mCollectingTransition.isTransientLaunch(ar);
+ }
+
+ /**
* @return {@code true} if {@param ar} is part of a transient-launch activity in an active
* transition.
*/
boolean isTransientLaunch(@NonNull ActivityRecord ar) {
- if (mCollectingTransition != null && mCollectingTransition.isTransientLaunch(ar)) {
+ if (isTransientCollect(ar)) {
return true;
}
for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
@@ -460,6 +493,15 @@
Transition requestStartTransition(@NonNull Transition transition, @Nullable Task startTask,
@Nullable RemoteTransition remoteTransition,
@Nullable TransitionRequestInfo.DisplayChange displayChange) {
+ if (mIsWaitingForDisplayEnabled) {
+ ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Disabling player for transition"
+ + " #%d because display isn't enabled yet", transition.getSyncId());
+ transition.mIsPlayerEnabled = false;
+ transition.mLogger.mRequestTimeNs = SystemClock.uptimeNanos();
+ mAtm.mH.post(() -> mAtm.mWindowOrganizerController.startTransition(
+ transition.getToken(), null));
+ return transition;
+ }
try {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
"Requesting StartTransition: %s", transition);
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index b06bdb1..7173980 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -170,9 +170,9 @@
protected InsetsSourceProvider mControllableInsetProvider;
/**
- * The insets sources provided by this windowContainer.
+ * The {@link InsetsSourceProvider}s provided by this window container.
*/
- protected SparseArray<InsetsSource> mProvidedInsetsSources = null;
+ protected SparseArray<InsetsSourceProvider> mInsetsSourceProviders = null;
// List of children for this window container. List is in z-order as the children appear on
// screen with the top-most window container at the tail of the list.
@@ -366,7 +366,7 @@
* {@link WindowState#mMergedLocalInsetsSources} by visiting the entire hierarchy.
*
* {@link WindowState#mAboveInsetsState} is updated by visiting all the windows in z-order
- * top-to-bottom manner and considering the {@link WindowContainer#mProvidedInsetsSources}
+ * top-to-bottom manner and considering the {@link WindowContainer#mInsetsSourceProviders}
* provided by the {@link WindowState}s at the top.
* {@link WindowState#updateAboveInsetsState(InsetsState, SparseArray, ArraySet)} visits the
* IME container in the correct order to make sure the IME insets are passed correctly to the
@@ -1035,11 +1035,21 @@
}
}
- public SparseArray<InsetsSource> getProvidedInsetsSources() {
- if (mProvidedInsetsSources == null) {
- mProvidedInsetsSources = new SparseArray<>();
+ /**
+ * Returns {@code true} if this node provides insets.
+ */
+ public boolean hasInsetsSourceProvider() {
+ return mInsetsSourceProviders != null;
+ }
+
+ /**
+ * Returns {@link InsetsSourceProvider}s provided by this node.
+ */
+ public SparseArray<InsetsSourceProvider> getInsetsSourceProviders() {
+ if (mInsetsSourceProviders == null) {
+ mInsetsSourceProviders = new SparseArray<>();
}
- return mProvidedInsetsSources;
+ return mInsetsSourceProviders;
}
public DisplayContent getDisplayContent() {
@@ -4102,13 +4112,13 @@
}
}
- private void hideInsetSourceViewOverflows(Set<Integer> insetTypes) {
- final ArrayList<SurfaceControl> surfaceControls =
- new ArrayList<>(insetTypes.size());
-
- for (int insetType : insetTypes) {
- WindowContainerInsetsSourceProvider insetProvider = getDisplayContent()
- .getInsetsStateController().getSourceProvider(insetType);
+ private void hideInsetSourceViewOverflows(Set<Integer> sourceIds) {
+ final InsetsStateController controller = getDisplayContent().getInsetsStateController();
+ for (int id : sourceIds) {
+ final InsetsSourceProvider insetProvider = controller.peekSourceProvider(id);
+ if (insetProvider == null) {
+ return;
+ }
// Will apply it immediately to current leash and to all future inset animations
// until we disable it.
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 6eab745..06a8869 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -113,6 +113,7 @@
import static com.android.internal.util.LatencyTracker.ACTION_ROTATE_SCREEN;
import static com.android.server.LockGuard.INDEX_WINDOW;
import static com.android.server.LockGuard.installLock;
+import static com.android.server.policy.PhoneWindowManager.TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD;
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY;
import static com.android.server.wm.DisplayContent.IME_TARGET_CONTROL;
@@ -309,6 +310,7 @@
import com.android.internal.policy.IKeyguardLockedStateListener;
import com.android.internal.policy.IShortcutService;
import com.android.internal.policy.KeyInterceptionInfo;
+import com.android.internal.protolog.ProtoLogGroup;
import com.android.internal.protolog.ProtoLogImpl;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.DumpUtils;
@@ -365,6 +367,7 @@
public class WindowManagerService extends IWindowManager.Stub
implements Watchdog.Monitor, WindowManagerPolicy.WindowManagerFuncs {
private static final String TAG = TAG_WITH_CLASS_NAME ? "WindowManagerService" : TAG_WM;
+ private static final int TRACE_MAX_SECTION_NAME_LENGTH = 127;
static final int LAYOUT_REPEAT_THRESHOLD = 4;
@@ -3774,6 +3777,12 @@
// Make sure the last requested orientation has been applied.
updateRotationUnchecked(false, false);
+
+ synchronized (mGlobalLock) {
+ mAtmService.getTransitionController().mIsWaitingForDisplayEnabled = false;
+ ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Notified TransitionController "
+ + "that the display is ready.");
+ }
}
private boolean checkBootAnimationCompleteLocked() {
@@ -5461,10 +5470,15 @@
case WAITING_FOR_DRAWN_TIMEOUT: {
Runnable callback = null;
- final WindowContainer container = (WindowContainer) msg.obj;
+ final WindowContainer<?> container = (WindowContainer<?>) msg.obj;
synchronized (mGlobalLock) {
ProtoLog.w(WM_ERROR, "Timeout waiting for drawn: undrawn=%s",
container.mWaitingForDrawn);
+ if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+ for (int i = 0; i < container.mWaitingForDrawn.size(); i++) {
+ traceEndWaitingForWindowDrawn(container.mWaitingForDrawn.get(i));
+ }
+ }
container.mWaitingForDrawn.clear();
callback = mWaitingForDrawnCallbacks.remove(container);
}
@@ -6080,10 +6094,16 @@
// Window has been removed or hidden; no draw will now happen, so stop waiting.
ProtoLog.w(WM_DEBUG_SCREEN_ON, "Aborted waiting for drawn: %s", win);
container.mWaitingForDrawn.remove(win);
+ if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+ traceEndWaitingForWindowDrawn(win);
+ }
} else if (win.hasDrawn()) {
// Window is now drawn (and shown).
ProtoLog.d(WM_DEBUG_SCREEN_ON, "Window drawn win=%s", win);
container.mWaitingForDrawn.remove(win);
+ if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+ traceEndWaitingForWindowDrawn(win);
+ }
}
}
if (container.mWaitingForDrawn.isEmpty()) {
@@ -6094,6 +6114,22 @@
});
}
+ private void traceStartWaitingForWindowDrawn(WindowState window) {
+ final String traceName = TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD + "#"
+ + window.getWindowTag();
+ final String shortenedTraceName = traceName.substring(0, Math.min(
+ TRACE_MAX_SECTION_NAME_LENGTH, traceName.length()));
+ Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, shortenedTraceName, /* cookie= */ 0);
+ }
+
+ private void traceEndWaitingForWindowDrawn(WindowState window) {
+ final String traceName = TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD + "#"
+ + window.getWindowTag();
+ final String shortenedTraceName = traceName.substring(0, Math.min(
+ TRACE_MAX_SECTION_NAME_LENGTH, traceName.length()));
+ Trace.asyncTraceEnd(Trace.TRACE_TAG_WINDOW_MANAGER, shortenedTraceName, /* cookie= */ 0);
+ }
+
void requestTraversal() {
mWindowPlacerLocked.requestTraversal();
}
@@ -7812,7 +7848,7 @@
@Override
public void waitForAllWindowsDrawn(Runnable callback, long timeout, int displayId) {
- final WindowContainer container = displayId == INVALID_DISPLAY
+ final WindowContainer<?> container = displayId == INVALID_DISPLAY
? mRoot : mRoot.getDisplayContent(displayId);
if (container == null) {
// The waiting container doesn't exist, no need to wait to run the callback. Run and
@@ -7828,6 +7864,12 @@
if (container.mWaitingForDrawn.isEmpty()) {
allWindowsDrawn = true;
} else {
+ if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+ for (int i = 0; i < container.mWaitingForDrawn.size(); i++) {
+ traceStartWaitingForWindowDrawn(container.mWaitingForDrawn.get(i));
+ }
+ }
+
mWaitingForDrawnCallbacks.put(container, callback);
mH.sendNewMessageDelayed(H.WAITING_FOR_DRAWN_TIMEOUT, container, timeout);
checkDrawnWindowsLocked();
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 29d3462..73e417e 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -168,6 +168,7 @@
void setWindowManager(WindowManagerService wms) {
mTransitionController = new TransitionController(mService, wms.mTaskSnapshotController,
wms.mTransitionTracer);
+ mTransitionController.mIsWaitingForDisplayEnabled = !wms.mDisplayEnabled;
mTransitionController.registerLegacyListener(wms.mActivityManagerAppTransitionNotifier);
}
@@ -431,6 +432,7 @@
applyTransaction(wct, -1 /* syncId */, transition, caller);
mTransitionController.requestStartTransition(transition, null /* startTask */,
null /* remoteTransition */, null /* displayChange */);
+ transition.setAllReady();
return;
}
@@ -469,6 +471,7 @@
mTransitionController.requestStartTransition(nextTransition,
null /* startTask */, null /* remoteTransition */,
null /* displayChange */);
+ nextTransition.setAllReady();
} else {
nextTransition.abort();
}
@@ -1036,17 +1039,30 @@
taskDisplayArea.moveRootTaskBehindRootTask(thisTask.getRootTask(), restoreAt);
break;
}
- case HIERARCHY_OP_TYPE_ADD_RECT_INSETS_PROVIDER:
+ case HIERARCHY_OP_TYPE_ADD_RECT_INSETS_PROVIDER: {
final Rect insetsProviderWindowContainer = hop.getInsetsProviderFrame();
- final WindowContainer receiverWindowContainer =
+ final WindowContainer container =
WindowContainer.fromBinder(hop.getContainer());
- receiverWindowContainer.addLocalRectInsetsSourceProvider(
+ if (container == null) {
+ Slog.e(TAG, "Attempt to add local insets source provider on unknown: "
+ + container);
+ break;
+ }
+ container.addLocalRectInsetsSourceProvider(
insetsProviderWindowContainer, hop.getInsetsTypes());
break;
- case HIERARCHY_OP_TYPE_REMOVE_INSETS_PROVIDER:
- WindowContainer.fromBinder(hop.getContainer())
- .removeLocalInsetsSourceProvider(hop.getInsetsTypes());
+ }
+ case HIERARCHY_OP_TYPE_REMOVE_INSETS_PROVIDER: {
+ final WindowContainer container =
+ WindowContainer.fromBinder(hop.getContainer());
+ if (container == null) {
+ Slog.e(TAG, "Attempt to remove local insets source provider from unknown: "
+ + container);
+ break;
+ }
+ container.removeLocalInsetsSourceProvider(hop.getInsetsTypes());
break;
+ }
case HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP: {
final WindowContainer container = WindowContainer.fromBinder(hop.getContainer());
if (container == null || container.asDisplayArea() == null
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 31f30af..e8aa38e 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -225,7 +225,6 @@
import android.view.InputWindowHandle;
import android.view.InsetsSource;
import android.view.InsetsState;
-import android.view.InsetsState.InternalInsetsType;
import android.view.Surface;
import android.view.Surface.Rotation;
import android.view.SurfaceControl;
@@ -1393,15 +1392,18 @@
}
void updateSourceFrame(Rect winFrame) {
+ if (!hasInsetsSourceProvider()) {
+ // This window doesn't provide any insets.
+ return;
+ }
if (mGivenInsetsPending) {
// The given insets are pending, and they are not reliable for now. The source frame
// should be updated after the new given insets are sent to window manager.
return;
}
- final SparseArray<InsetsSource> providedSources = getProvidedInsetsSources();
- final InsetsStateController controller = getDisplayContent().getInsetsStateController();
- for (int i = providedSources.size() - 1; i >= 0; i--) {
- controller.getSourceProvider(providedSources.keyAt(i)).updateSourceFrame(winFrame);
+ final SparseArray<InsetsSourceProvider> providers = getInsetsSourceProviders();
+ for (int i = providers.size() - 1; i >= 0; i--) {
+ providers.valueAt(i).updateSourceFrame(winFrame);
}
}
@@ -1531,6 +1533,10 @@
winAnimator.mDrawState = DRAW_PENDING;
if (mActivityRecord != null) {
mActivityRecord.clearAllDrawn();
+ if (mAttrs.type == TYPE_APPLICATION_STARTING
+ && mActivityRecord.mStartingData != null) {
+ mActivityRecord.mStartingData.mIsDisplayed = false;
+ }
}
}
if (!mWmService.mResizingWindows.contains(this)) {
@@ -1696,14 +1702,12 @@
* Returns the insets state for the window and applies the requested visibility.
*/
InsetsState getInsetsStateWithVisibilityOverride() {
- final InsetsState state = new InsetsState(getInsetsState());
- for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) {
- final boolean requestedVisible = isRequestedVisible(InsetsState.toPublicType(type));
- InsetsSource source = state.peekSource(type);
- if (source != null && source.isVisible() != requestedVisible) {
- source = new InsetsSource(source);
+ final InsetsState state = new InsetsState(getInsetsState(), true /* copySources */);
+ for (int i = state.sourceSize() - 1; i >= 0; i--) {
+ final InsetsSource source = state.sourceAt(i);
+ final boolean requestedVisible = isRequestedVisible(source.getType());
+ if (source.isVisible() != requestedVisible) {
source.setVisible(requestedVisible);
- state.addSource(source);
}
}
return state;
@@ -1878,12 +1882,12 @@
}
boolean providesNonDecorInsets() {
- if (mProvidedInsetsSources == null) {
+ if (mInsetsSourceProviders == null) {
return false;
}
- for (int i = mProvidedInsetsSources.size() - 1; i >= 0; i--) {
- final int type = mProvidedInsetsSources.keyAt(i);
- if ((InsetsState.toPublicType(type) & WindowInsets.Type.navigationBars()) != 0) {
+ for (int i = mInsetsSourceProviders.size() - 1; i >= 0; i--) {
+ final InsetsSource source = mInsetsSourceProviders.valueAt(i).getSource();
+ if (source.getType() == WindowInsets.Type.navigationBars()) {
return true;
}
}
@@ -3159,15 +3163,25 @@
+ (mActivityRecord == null || mActivityRecord.windowsAreFocusable(fromUserTouch))
+ " canReceiveTouchInput=" + canReceiveTouchInput()
+ " displayIsOnTop=" + getDisplayContent().isOnTop()
- + " displayIsTrusted=" + getDisplayContent().isTrusted();
+ + " displayIsTrusted=" + getDisplayContent().isTrusted()
+ + " transitShouldKeepFocus=" + (mActivityRecord != null
+ && mTransitionController.shouldKeepFocus(mActivityRecord));
}
public boolean canReceiveKeys(boolean fromUserTouch) {
+ if (mActivityRecord != null && mTransitionController.shouldKeepFocus(mActivityRecord)) {
+ // During transient launch, the transient-hide windows are not visibleRequested
+ // or on-top but are kept focusable and thus can receive keys.
+ return true;
+ }
final boolean canReceiveKeys = isVisibleRequestedOrAdding()
&& (mViewVisibility == View.VISIBLE) && !mRemoveOnExit
&& ((mAttrs.flags & WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) == 0)
&& (mActivityRecord == null || mActivityRecord.windowsAreFocusable(fromUserTouch))
- && canReceiveTouchInput();
+ // can it receive touches
+ && (mActivityRecord == null || mActivityRecord.getTask() == null
+ || !mActivityRecord.getTask().getRootTask().shouldIgnoreInput());
+
if (!canReceiveKeys) {
return false;
}
@@ -3193,6 +3207,11 @@
if (mActivityRecord == null || mActivityRecord.getTask() == null) {
return true;
}
+ // During transient launch, the transient-hide windows are not visibleRequested
+ // or on-top but are kept focusable and thus can receive touch input.
+ if (mTransitionController.shouldKeepFocus(mActivityRecord)) {
+ return true;
+ }
return !mActivityRecord.getTask().getRootTask().shouldIgnoreInput()
&& mActivityRecord.isVisibleRequested();
@@ -4824,10 +4843,10 @@
insetsChangedWindows.add(w);
}
- final SparseArray<InsetsSource> providedSources = w.mProvidedInsetsSources;
- if (providedSources != null) {
- for (int i = providedSources.size() - 1; i >= 0; i--) {
- aboveInsetsState.addSource(providedSources.valueAt(i));
+ final SparseArray<InsetsSourceProvider> providers = w.mInsetsSourceProviders;
+ if (providers != null) {
+ for (int i = providers.size() - 1; i >= 0; i--) {
+ aboveInsetsState.addSource(providers.valueAt(i).getSource());
}
}
}, true /* traverseTopToBottom */);
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index e661688..e2bdcdd 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -62,9 +62,9 @@
"com_android_server_tv_TvInputHal.cpp",
"com_android_server_vr_VrManagerService.cpp",
"com_android_server_UsbAlsaJackDetector.cpp",
+ "com_android_server_UsbAlsaMidiDevice.cpp",
"com_android_server_UsbDeviceManager.cpp",
"com_android_server_UsbDescriptorParser.cpp",
- "com_android_server_UsbMidiDevice.cpp",
"com_android_server_UsbHostManager.cpp",
"com_android_server_vibrator_VibratorController.cpp",
"com_android_server_vibrator_VibratorManagerService.cpp",
diff --git a/services/core/jni/com_android_server_UsbMidiDevice.cpp b/services/core/jni/com_android_server_UsbAlsaMidiDevice.cpp
similarity index 83%
rename from services/core/jni/com_android_server_UsbMidiDevice.cpp
rename to services/core/jni/com_android_server_UsbAlsaMidiDevice.cpp
index c8e7698..93938b1 100644
--- a/services/core/jni/com_android_server_UsbMidiDevice.cpp
+++ b/services/core/jni/com_android_server_UsbAlsaMidiDevice.cpp
@@ -14,27 +14,25 @@
* limitations under the License.
*/
-#define LOG_TAG "UsbMidiDeviceJNI"
+#define LOG_TAG "UsbAlsaMidiDeviceJNI"
#define LOG_NDEBUG 0
-#include "utils/Log.h"
-
-#include "jni.h"
-#include <nativehelper/JNIPlatformHelp.h>
-#include <nativehelper/ScopedLocalRef.h>
-#include "android_runtime/AndroidRuntime.h"
-#include "android_runtime/Log.h"
-
#include <asm/byteorder.h>
#include <errno.h>
#include <fcntl.h>
+#include <nativehelper/JNIPlatformHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
#include <sound/asound.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
-namespace android
-{
+#include "android_runtime/AndroidRuntime.h"
+#include "android_runtime/Log.h"
+#include "jni.h"
+#include "utils/Log.h"
+
+namespace android {
static jclass sFileDescriptorClass;
static jfieldID sPipeFDField;
@@ -46,10 +44,10 @@
// 1. Input O_RDONLY file descriptor
// 2. Special input file descriptor to block the input thread
// 3. Output O_WRONLY file descriptor
-static jobjectArray android_server_UsbMidiDevice_open(JNIEnv *env, jobject thiz, jint card,
- jint device, jint numInputs,
- jint numOutputs) {
- char path[100];
+static jobjectArray android_server_UsbAlsaMidiDevice_open(JNIEnv *env, jobject thiz, jint card,
+ jint device, jint numInputs,
+ jint numOutputs) {
+ char path[100];
int fd;
snprintf(path, sizeof(path), "/dev/snd/midiC%dD%d", card, device);
@@ -126,9 +124,7 @@
return NULL;
}
-static void
-android_server_UsbMidiDevice_close(JNIEnv *env, jobject thiz, jobjectArray fds)
-{
+static void android_server_UsbAlsaMidiDevice_close(JNIEnv *env, jobject thiz, jobjectArray fds) {
// write to mPipeFD to unblock input thread
jint pipeFD = env->GetIntField(thiz, sPipeFDField);
write(pipeFD, &pipeFD, sizeof(pipeFD));
@@ -144,12 +140,12 @@
static JNINativeMethod method_table[] = {
{"nativeOpen", "(IIII)[Ljava/io/FileDescriptor;",
- (void *)android_server_UsbMidiDevice_open},
- {"nativeClose", "([Ljava/io/FileDescriptor;)V", (void *)android_server_UsbMidiDevice_close},
+ (void *)android_server_UsbAlsaMidiDevice_open},
+ {"nativeClose", "([Ljava/io/FileDescriptor;)V",
+ (void *)android_server_UsbAlsaMidiDevice_close},
};
-int register_android_server_UsbMidiDevice(JNIEnv *env)
-{
+int register_android_server_UsbAlsaMidiDevice(JNIEnv *env) {
jclass clazz = env->FindClass("java/io/FileDescriptor");
if (clazz == NULL) {
ALOGE("Can't find java/io/FileDescriptor");
@@ -157,19 +153,19 @@
}
sFileDescriptorClass = (jclass)env->NewGlobalRef(clazz);
- clazz = env->FindClass("com/android/server/usb/UsbMidiDevice");
+ clazz = env->FindClass("com/android/server/usb/UsbAlsaMidiDevice");
if (clazz == NULL) {
- ALOGE("Can't find com/android/server/usb/UsbMidiDevice");
+ ALOGE("Can't find com/android/server/usb/UsbAlsaMidiDevice");
return -1;
}
sPipeFDField = env->GetFieldID(clazz, "mPipeFD", "I");
if (sPipeFDField == NULL) {
- ALOGE("Can't find UsbMidiDevice.mPipeFD");
+ ALOGE("Can't find UsbAlsaMidiDevice.mPipeFD");
return -1;
}
- return jniRegisterNativeMethods(env, "com/android/server/usb/UsbMidiDevice",
- method_table, NELEM(method_table));
+ return jniRegisterNativeMethods(env, "com/android/server/usb/UsbAlsaMidiDevice", method_table,
+ NELEM(method_table));
}
-};
+}; // namespace android
diff --git a/services/core/jni/com_android_server_companion_virtual_InputController.cpp b/services/core/jni/com_android_server_companion_virtual_InputController.cpp
index 4cb7a8f..dd757bc 100644
--- a/services/core/jni/com_android_server_companion_virtual_InputController.cpp
+++ b/services/core/jni/com_android_server_companion_virtual_InputController.cpp
@@ -29,8 +29,17 @@
#include <utils/Log.h>
#include <map>
+#include <set>
#include <string>
+/**
+ * Log debug messages about native virtual input devices.
+ * Enable this via "adb shell setprop log.tag.InputController DEBUG"
+ */
+static bool isDebug() {
+ return __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG, ANDROID_LOG_INFO);
+}
+
namespace android {
enum class DeviceType {
@@ -194,6 +203,15 @@
{AKEYCODE_NUMPAD_COMMA, KEY_KPCOMMA},
};
+/*
+ * Map from the uinput touchscreen fd to the pointers present in the previous touch events that
+ * hasn't been lifted.
+ * We only allow pointer id to go up to MAX_POINTERS because the maximum slots of virtual
+ * touchscreen is set up with MAX_POINTERS. Note that in other cases Android allows pointer id to go
+ * up to MAX_POINTERS_ID.
+ */
+static std::map<int32_t, std::bitset<MAX_POINTERS>> unreleasedTouches;
+
/** Creates a new uinput device and assigns a file descriptor. */
static int openUinput(const char* readableName, jint vendorId, jint productId, const char* phys,
DeviceType deviceType, jint screenHeight, jint screenWidth) {
@@ -366,6 +384,12 @@
static bool nativeCloseUinput(JNIEnv* env, jobject thiz, jint fd) {
ioctl(fd, UI_DEV_DESTROY);
+ if (auto touchesOnFd = unreleasedTouches.find(fd); touchesOnFd != unreleasedTouches.end()) {
+ const size_t remainingPointers = touchesOnFd->second.size();
+ unreleasedTouches.erase(touchesOnFd);
+ ALOGW_IF(remainingPointers > 0, "Closing touchscreen %d, erased %zu unreleased pointers.",
+ fd, remainingPointers);
+ }
return close(fd);
}
@@ -425,6 +449,69 @@
return true;
}
+static bool handleTouchUp(int fd, int pointerId) {
+ if (!writeInputEvent(fd, EV_ABS, ABS_MT_TRACKING_ID, static_cast<int32_t>(-1))) {
+ return false;
+ }
+ auto touchesOnFd = unreleasedTouches.find(fd);
+ if (touchesOnFd == unreleasedTouches.end()) {
+ ALOGE("PointerId %d action UP received with no prior events on touchscreen %d.", pointerId,
+ fd);
+ return false;
+ }
+ ALOGD_IF(isDebug(), "Unreleased touches found for touchscreen %d in the map", fd);
+
+ // When a pointer is no longer in touch, remove the pointer id from the corresponding
+ // entry in the unreleased touches map.
+ if (pointerId < 0 || pointerId >= MAX_POINTERS) {
+ ALOGE("Virtual touch event has invalid pointer id %d; value must be between 0 and %zu",
+ pointerId, MAX_POINTERS - 1);
+ return false;
+ }
+ if (!touchesOnFd->second.test(pointerId)) {
+ ALOGE("PointerId %d action UP received with no prior action DOWN on touchscreen %d.",
+ pointerId, fd);
+ return false;
+ }
+ touchesOnFd->second.reset(pointerId);
+ ALOGD_IF(isDebug(), "Pointer %d erased from the touchscreen %d", pointerId, fd);
+
+ // Only sends the BTN UP event when there's no pointers on the touchscreen.
+ if (touchesOnFd->second.none()) {
+ unreleasedTouches.erase(touchesOnFd);
+ if (!writeInputEvent(fd, EV_KEY, BTN_TOUCH, static_cast<int32_t>(UinputAction::RELEASE))) {
+ return false;
+ }
+ ALOGD_IF(isDebug(), "No pointers on touchscreen %d, BTN UP event sent.", fd);
+ }
+ return true;
+}
+
+static bool handleTouchDown(int fd, int pointerId) {
+ // When a new pointer is down on the touchscreen, add the pointer id in the corresponding
+ // entry in the unreleased touches map.
+ auto touchesOnFd = unreleasedTouches.find(fd);
+ if (touchesOnFd == unreleasedTouches.end()) {
+ // Only sends the BTN Down event when the first pointer on the touchscreen is down.
+ if (!writeInputEvent(fd, EV_KEY, BTN_TOUCH, static_cast<int32_t>(UinputAction::PRESS))) {
+ return false;
+ }
+ touchesOnFd = unreleasedTouches.insert({fd, {}}).first;
+ ALOGD_IF(isDebug(), "New touchscreen with fd %d added in the unreleased touches map.", fd);
+ }
+ if (touchesOnFd->second.test(pointerId)) {
+ ALOGE("Repetitive action DOWN event received on a pointer %d that is already down.",
+ pointerId);
+ return false;
+ }
+ touchesOnFd->second.set(pointerId);
+ ALOGD_IF(isDebug(), "Added pointer %d under touchscreen %d in the map", pointerId, fd);
+ if (!writeInputEvent(fd, EV_ABS, ABS_MT_TRACKING_ID, static_cast<int32_t>(pointerId))) {
+ return false;
+ }
+ return true;
+}
+
static bool nativeWriteTouchEvent(JNIEnv* env, jobject thiz, jint fd, jint pointerId, jint toolType,
jint action, jfloat locationX, jfloat locationY, jfloat pressure,
jfloat majorAxisSize) {
@@ -446,15 +533,10 @@
return false;
}
UinputAction uinputAction = actionIterator->second;
- if (uinputAction == UinputAction::PRESS || uinputAction == UinputAction::RELEASE) {
- if (!writeInputEvent(fd, EV_KEY, BTN_TOUCH, static_cast<int32_t>(uinputAction))) {
- return false;
- }
- if (!writeInputEvent(fd, EV_ABS, ABS_MT_TRACKING_ID,
- static_cast<int32_t>(uinputAction == UinputAction::PRESS ? pointerId
- : -1))) {
- return false;
- }
+ if (uinputAction == UinputAction::PRESS && !handleTouchDown(fd, pointerId)) {
+ return false;
+ } else if (uinputAction == UinputAction::RELEASE && !handleTouchUp(fd, pointerId)) {
+ return false;
}
if (!writeInputEvent(fd, EV_ABS, ABS_MT_POSITION_X, locationX)) {
return false;
diff --git a/services/core/jni/com_android_server_display_DisplayControl.cpp b/services/core/jni/com_android_server_display_DisplayControl.cpp
index ec42324..db7fced 100644
--- a/services/core/jni/com_android_server_display_DisplayControl.cpp
+++ b/services/core/jni/com_android_server_display_DisplayControl.cpp
@@ -23,9 +23,11 @@
namespace android {
-static jobject nativeCreateDisplay(JNIEnv* env, jclass clazz, jstring nameObj, jboolean secure) {
+static jobject nativeCreateDisplay(JNIEnv* env, jclass clazz, jstring nameObj, jboolean secure,
+ jfloat requestedRefreshRate) {
ScopedUtfChars name(env, nameObj);
- sp<IBinder> token(SurfaceComposerClient::createDisplay(String8(name.c_str()), bool(secure)));
+ sp<IBinder> token(SurfaceComposerClient::createDisplay(String8(name.c_str()), bool(secure),
+ requestedRefreshRate));
return javaObjectForIBinder(env, token);
}
@@ -134,7 +136,7 @@
static const JNINativeMethod sDisplayMethods[] = {
// clang-format off
- {"nativeCreateDisplay", "(Ljava/lang/String;Z)Landroid/os/IBinder;",
+ {"nativeCreateDisplay", "(Ljava/lang/String;ZF)Landroid/os/IBinder;",
(void*)nativeCreateDisplay },
{"nativeDestroyDisplay", "(Landroid/os/IBinder;)V",
(void*)nativeDestroyDisplay },
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index f4d1d1e..b4e2fb6 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -137,6 +137,7 @@
jmethodID notifyDropWindow;
jmethodID getParentSurfaceForPointers;
jmethodID isPerDisplayTouchModeEnabled;
+ jmethodID isStylusPointerIconEnabled;
} gServiceClassInfo;
static struct {
@@ -363,6 +364,7 @@
std::map<PointerIconStyle, SpriteIcon>* outResources,
std::map<PointerIconStyle, PointerAnimation>* outAnimationResources, int32_t displayId);
virtual PointerIconStyle getDefaultPointerIconId();
+ virtual PointerIconStyle getDefaultStylusIconId();
virtual PointerIconStyle getCustomPointerIconId();
virtual void onPointerDisplayIdChanged(int32_t displayId, float xPos, float yPos);
@@ -659,6 +661,12 @@
outConfig->pointerGestureTapSlop = hoverTapSlop;
}
+ jboolean stylusPointerIconEnabled =
+ env->CallBooleanMethod(mServiceObj, gServiceClassInfo.isStylusPointerIconEnabled);
+ if (!checkAndClearExceptionFromCallback(env, "isStylusPointerIconEnabled")) {
+ outConfig->stylusPointerIconEnabled = stylusPointerIconEnabled;
+ }
+
{ // acquire lock
AutoMutex _l(mLock);
@@ -1605,6 +1613,11 @@
return PointerIconStyle::TYPE_ARROW;
}
+PointerIconStyle NativeInputManager::getDefaultStylusIconId() {
+ // TODO: add resource for default stylus icon and change this
+ return PointerIconStyle::TYPE_CROSSHAIR;
+}
+
PointerIconStyle NativeInputManager::getCustomPointerIconId() {
return PointerIconStyle::TYPE_CUSTOM;
}
@@ -2787,6 +2800,9 @@
GET_METHOD_ID(gServiceClassInfo.isPerDisplayTouchModeEnabled, clazz,
"isPerDisplayTouchModeEnabled", "()Z");
+ GET_METHOD_ID(gServiceClassInfo.isStylusPointerIconEnabled, clazz, "isStylusPointerIconEnabled",
+ "()Z");
+
// InputDevice
FIND_CLASS(gInputDeviceClassInfo.clazz, "android/view/InputDevice");
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 00f851f..290ad8d 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -35,8 +35,8 @@
int register_android_server_SerialService(JNIEnv* env);
int register_android_server_SystemServer(JNIEnv* env);
int register_android_server_UsbAlsaJackDetector(JNIEnv* env);
+int register_android_server_UsbAlsaMidiDevice(JNIEnv* env);
int register_android_server_UsbDeviceManager(JNIEnv* env);
-int register_android_server_UsbMidiDevice(JNIEnv* env);
int register_android_server_UsbHostManager(JNIEnv* env);
int register_android_server_vr_VrManagerService(JNIEnv* env);
int register_android_server_vibrator_VibratorController(JavaVM* vm, JNIEnv* env);
@@ -90,8 +90,8 @@
register_android_server_InputManager(env);
register_android_server_LightsService(env);
register_android_server_UsbDeviceManager(env);
- register_android_server_UsbMidiDevice(env);
register_android_server_UsbAlsaJackDetector(env);
+ register_android_server_UsbAlsaMidiDevice(env);
register_android_server_UsbHostManager(env);
register_android_server_vr_VrManagerService(env);
register_android_server_vibrator_VibratorController(vm, env);
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index 97dbe04..ef5aa60 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -208,11 +208,7 @@
<xs:complexType name="thermalThrottling">
<xs:sequence>
- <xs:element type="brightnessThrottlingMap" name="brightnessThrottlingMap">
- <xs:annotation name="nonnull"/>
- <xs:annotation name="final"/>
- </xs:element>
- <xs:element type="brightnessThrottlingMap" name="concurrentDisplaysBrightnessThrottlingMap">
+ <xs:element type="brightnessThrottlingMap" name="brightnessThrottlingMap" maxOccurs="unbounded">
<xs:annotation name="final"/>
</xs:element>
</xs:sequence>
@@ -225,6 +221,7 @@
<xs:annotation name="final"/>
</xs:element>
</xs:sequence>
+ <xs:attribute name="id" type="xs:string"/>
</xs:complexType>
<xs:complexType name="brightnessThrottlingPoint">
@@ -472,6 +469,18 @@
</xs:complexType>
<xs:complexType name="refreshRateConfigs">
+ <xs:element name="defaultRefreshRate" type="xs:nonNegativeInteger"
+ minOccurs="0" maxOccurs="1">
+ <xs:annotation name="final"/>
+ </xs:element>
+ <xs:element name="defaultPeakRefreshRate" type="xs:nonNegativeInteger"
+ minOccurs="0" maxOccurs="1">
+ <xs:annotation name="final"/>
+ </xs:element>
+ <xs:element name="refreshRateZoneProfiles" type="refreshRateZoneProfiles"
+ minOccurs="0" maxOccurs="1">
+ <xs:annotation name="final"/>
+ </xs:element>
<xs:element name="lowerBlockingZoneConfigs" type="blockingZoneConfig"
minOccurs="0" maxOccurs="1">
<xs:annotation name="final"/>
@@ -482,6 +491,22 @@
</xs:element>
</xs:complexType>
+ <xs:complexType name="refreshRateZoneProfiles">
+ <xs:sequence>
+ <xs:element name="refreshRateZoneProfile" type="refreshRateZone"
+ minOccurs="0" maxOccurs="unbounded">
+ <xs:annotation name="final"/>
+ </xs:element>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="refreshRateZone">
+ <xs:attribute name="id" use="required" type="xs:string" />
+ <xs:element name="refreshRateRange" type="refreshRateRange">
+ <xs:annotation name="final"/>
+ </xs:element>
+ </xs:complexType>
+
<xs:complexType name="blockingZoneConfig">
<xs:element name="defaultRefreshRate" type="xs:nonNegativeInteger"
minOccurs="1" maxOccurs="1">
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index aba8a2c..ed9f959 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -37,6 +37,8 @@
public class BrightnessThrottlingMap {
ctor public BrightnessThrottlingMap();
method @NonNull public final java.util.List<com.android.server.display.config.BrightnessThrottlingPoint> getBrightnessThrottlingPoint();
+ method public String getId();
+ method public void setId(String);
}
public class BrightnessThrottlingPoint {
@@ -188,10 +190,16 @@
public class RefreshRateConfigs {
ctor public RefreshRateConfigs();
+ method public final java.math.BigInteger getDefaultPeakRefreshRate();
+ method public final java.math.BigInteger getDefaultRefreshRate();
method public final com.android.server.display.config.BlockingZoneConfig getHigherBlockingZoneConfigs();
method public final com.android.server.display.config.BlockingZoneConfig getLowerBlockingZoneConfigs();
+ method public final com.android.server.display.config.RefreshRateZoneProfiles getRefreshRateZoneProfiles();
+ method public final void setDefaultPeakRefreshRate(java.math.BigInteger);
+ method public final void setDefaultRefreshRate(java.math.BigInteger);
method public final void setHigherBlockingZoneConfigs(com.android.server.display.config.BlockingZoneConfig);
method public final void setLowerBlockingZoneConfigs(com.android.server.display.config.BlockingZoneConfig);
+ method public final void setRefreshRateZoneProfiles(com.android.server.display.config.RefreshRateZoneProfiles);
}
public class RefreshRateRange {
@@ -202,6 +210,19 @@
method public final void setMinimum(java.math.BigInteger);
}
+ public class RefreshRateZone {
+ ctor public RefreshRateZone();
+ method public String getId();
+ method public final com.android.server.display.config.RefreshRateRange getRefreshRateRange();
+ method public void setId(String);
+ method public final void setRefreshRateRange(com.android.server.display.config.RefreshRateRange);
+ }
+
+ public class RefreshRateZoneProfiles {
+ ctor public RefreshRateZoneProfiles();
+ method public final java.util.List<com.android.server.display.config.RefreshRateZone> getRefreshRateZoneProfile();
+ }
+
public class SdrHdrRatioMap {
ctor public SdrHdrRatioMap();
method @NonNull public final java.util.List<com.android.server.display.config.SdrHdrRatioPoint> getPoint();
@@ -238,10 +259,7 @@
public class ThermalThrottling {
ctor public ThermalThrottling();
- method @NonNull public final com.android.server.display.config.BrightnessThrottlingMap getBrightnessThrottlingMap();
- method public final com.android.server.display.config.BrightnessThrottlingMap getConcurrentDisplaysBrightnessThrottlingMap();
- method public final void setBrightnessThrottlingMap(@NonNull com.android.server.display.config.BrightnessThrottlingMap);
- method public final void setConcurrentDisplaysBrightnessThrottlingMap(com.android.server.display.config.BrightnessThrottlingMap);
+ method public final java.util.List<com.android.server.display.config.BrightnessThrottlingMap> getBrightnessThrottlingMap();
}
public class ThresholdPoint {
diff --git a/services/core/xsd/display-layout-config/display-layout-config.xsd b/services/core/xsd/display-layout-config/display-layout-config.xsd
index 842d97a..45e10a8 100644
--- a/services/core/xsd/display-layout-config/display-layout-config.xsd
+++ b/services/core/xsd/display-layout-config/display-layout-config.xsd
@@ -51,8 +51,10 @@
<xs:sequence>
<xs:element name="address" type="xs:nonNegativeInteger"/>
<xs:element name="position" type="xs:string" minOccurs="0" maxOccurs="1" />
+ <xs:element name="brightnessThrottlingMapId" type="xs:string" minOccurs="0" maxOccurs="1" />
</xs:sequence>
<xs:attribute name="enabled" type="xs:boolean" use="optional" />
<xs:attribute name="defaultDisplay" type="xs:boolean" use="optional" />
+ <xs:attribute name="refreshRateZoneId" type="xs:string" use="optional" />
</xs:complexType>
</xs:schema>
diff --git a/services/core/xsd/display-layout-config/schema/current.txt b/services/core/xsd/display-layout-config/schema/current.txt
index 55f866c..2c16c37 100644
--- a/services/core/xsd/display-layout-config/schema/current.txt
+++ b/services/core/xsd/display-layout-config/schema/current.txt
@@ -4,13 +4,17 @@
public class Display {
ctor public Display();
method public java.math.BigInteger getAddress();
+ method public String getBrightnessThrottlingMapId();
method public String getPosition();
+ method public String getRefreshRateZoneId();
method public boolean isDefaultDisplay();
method public boolean isEnabled();
method public void setAddress(java.math.BigInteger);
+ method public void setBrightnessThrottlingMapId(String);
method public void setDefaultDisplay(boolean);
method public void setEnabled(boolean);
method public void setPosition(String);
+ method public void setRefreshRateZoneId(String);
}
public class Layout {
diff --git a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
index be60946..447c67f 100644
--- a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
@@ -23,6 +23,7 @@
import android.credentials.IClearCredentialStateCallback;
import android.credentials.ui.ProviderData;
import android.credentials.ui.RequestInfo;
+import android.os.CancellationSignal;
import android.os.RemoteException;
import android.service.credentials.CallingAppInfo;
import android.service.credentials.CredentialProviderInfo;
@@ -41,9 +42,9 @@
public ClearRequestSession(Context context, int userId, int callingUid,
IClearCredentialStateCallback callback, ClearCredentialStateRequest request,
- CallingAppInfo callingAppInfo) {
+ CallingAppInfo callingAppInfo, CancellationSignal cancellationSignal) {
super(context, userId, callingUid, request, callback, RequestInfo.TYPE_UNDEFINED,
- callingAppInfo);
+ callingAppInfo, cancellationSignal);
}
/**
@@ -111,6 +112,12 @@
private void respondToClientWithResponseAndFinish() {
Log.i(TAG, "respondToClientWithResponseAndFinish");
+ if (isSessionCancelled()) {
+ // TODO: Differentiate btw cancelled and false
+ logApiCalled(RequestType.CLEAR_CREDENTIALS, /* isSuccessful */ true);
+ finishSession(/*propagateCancellation=*/true);
+ return;
+ }
try {
mClientCallback.onSuccess();
logApiCalled(RequestType.CLEAR_CREDENTIALS, /* isSuccessful */ true);
@@ -118,18 +125,24 @@
Log.i(TAG, "Issue while propagating the response to the client");
logApiCalled(RequestType.CLEAR_CREDENTIALS, /* isSuccessful */ false);
}
- finishSession();
+ finishSession(/*propagateCancellation=*/false);
}
private void respondToClientWithErrorAndFinish(String errorType, String errorMsg) {
Log.i(TAG, "respondToClientWithErrorAndFinish");
+ if (isSessionCancelled()) {
+ // TODO: Differentiate btw cancelled and false
+ logApiCalled(RequestType.CLEAR_CREDENTIALS, /* isSuccessful */ true);
+ finishSession(/*propagateCancellation=*/true);
+ return;
+ }
try {
mClientCallback.onError(errorType, errorMsg);
} catch (RemoteException e) {
e.printStackTrace();
}
logApiCalled(RequestType.CLEAR_CREDENTIALS, /* isSuccessful */ false);
- finishSession();
+ finishSession(/*propagateCancellation=*/false);
}
private void processResponses() {
diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
index 351afb9..2345e3f 100644
--- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
@@ -27,6 +27,7 @@
import android.credentials.ICreateCredentialCallback;
import android.credentials.ui.ProviderData;
import android.credentials.ui.RequestInfo;
+import android.os.CancellationSignal;
import android.os.RemoteException;
import android.service.credentials.CallingAppInfo;
import android.service.credentials.CredentialProviderInfo;
@@ -47,9 +48,10 @@
CreateRequestSession(@NonNull Context context, int userId, int callingUid,
CreateCredentialRequest request,
ICreateCredentialCallback callback,
- CallingAppInfo callingAppInfo) {
+ CallingAppInfo callingAppInfo,
+ CancellationSignal cancellationSignal) {
super(context, userId, callingUid, request, callback, RequestInfo.TYPE_CREATE,
- callingAppInfo);
+ callingAppInfo, cancellationSignal);
}
/**
@@ -119,6 +121,12 @@
private void respondToClientWithResponseAndFinish(CreateCredentialResponse response) {
Log.i(TAG, "respondToClientWithResponseAndFinish");
+ if (isSessionCancelled()) {
+ // TODO: Differentiate btw cancelled and false
+ logApiCalled(RequestType.CREATE_CREDENTIALS, /* isSuccessful */ true);
+ finishSession(/*propagateCancellation=*/true);
+ return;
+ }
try {
mClientCallback.onResponse(response);
logApiCalled(RequestType.CREATE_CREDENTIALS, /* isSuccessful */ true);
@@ -126,18 +134,24 @@
Log.i(TAG, "Issue while responding to client: " + e.getMessage());
logApiCalled(RequestType.CREATE_CREDENTIALS, /* isSuccessful */ false);
}
- finishSession();
+ finishSession(/*propagateCancellation=*/false);
}
private void respondToClientWithErrorAndFinish(String errorType, String errorMsg) {
Log.i(TAG, "respondToClientWithErrorAndFinish");
+ if (isSessionCancelled()) {
+ // TODO: Differentiate btw cancelled and false
+ logApiCalled(RequestType.CREATE_CREDENTIALS, /* isSuccessful */ true);
+ finishSession(/*propagateCancellation=*/true);
+ return;
+ }
try {
mClientCallback.onError(errorType, errorMsg);
} catch (RemoteException e) {
Log.i(TAG, "Issue while responding to client: " + e.getMessage());
}
logApiCalled(RequestType.CREATE_CREDENTIALS, /* isSuccessful */ false);
- finishSession();
+ finishSession(/*propagateCancellation=*/false);
}
@Override
diff --git a/services/credentials/java/com/android/server/credentials/CredentialDescriptionRegistry.java b/services/credentials/java/com/android/server/credentials/CredentialDescriptionRegistry.java
index fbdcc44..3d504ef 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialDescriptionRegistry.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialDescriptionRegistry.java
@@ -19,35 +19,72 @@
import android.credentials.CredentialDescription;
import android.credentials.RegisterCredentialDescriptionRequest;
import android.credentials.UnregisterCredentialDescriptionRequest;
+import android.service.credentials.CredentialEntry;
import android.util.SparseArray;
+import com.android.internal.annotations.GuardedBy;
+
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.locks.ReentrantLock;
/** Contains information on what CredentialProvider has what provisioned Credential. */
-public class CredentialDescriptionRegistry {
+public final class CredentialDescriptionRegistry {
private static final int MAX_ALLOWED_CREDENTIAL_DESCRIPTIONS = 128;
private static final int MAX_ALLOWED_ENTRIES_PER_PROVIDER = 16;
- private static SparseArray<CredentialDescriptionRegistry> sCredentialDescriptionSessionPerUser;
+ @GuardedBy("sLock")
+ private static final SparseArray<CredentialDescriptionRegistry>
+ sCredentialDescriptionSessionPerUser;
+ private static final ReentrantLock sLock;
static {
sCredentialDescriptionSessionPerUser = new SparseArray<>();
+ sLock = new ReentrantLock();
}
- // TODO(b/265992655): add a way to update CredentialRegistry when a user is removed.
- /** Get and/or create a {@link CredentialDescription} for the given user id. */
- public static CredentialDescriptionRegistry forUser(int userId) {
- CredentialDescriptionRegistry session =
- sCredentialDescriptionSessionPerUser.get(userId, null);
+ /** Represents the results of a given query into the registry. */
+ public static final class FilterResult {
+ final String mPackageName;
+ final List<CredentialEntry> mCredentialEntries;
- if (session == null) {
- session = new CredentialDescriptionRegistry();
- sCredentialDescriptionSessionPerUser.put(userId, session);
+ private FilterResult(String packageName,
+ List<CredentialEntry> credentialEntries) {
+ mPackageName = packageName;
+ mCredentialEntries = credentialEntries;
}
- return session;
+ }
+
+ /** Get and/or create a {@link CredentialDescription} for the given user id. */
+ @GuardedBy("sLock")
+ public static CredentialDescriptionRegistry forUser(int userId) {
+ sLock.lock();
+ try {
+ CredentialDescriptionRegistry session =
+ sCredentialDescriptionSessionPerUser.get(userId, null);
+
+ if (session == null) {
+ session = new CredentialDescriptionRegistry();
+ sCredentialDescriptionSessionPerUser.put(userId, session);
+ }
+ return session;
+ } finally {
+ sLock.unlock();
+ }
+ }
+
+ /** Clears an existing session for a given user identifier. */
+ @GuardedBy("sLock")
+ public static void clearUserSession(int userId) {
+ sLock.lock();
+ try {
+ sCredentialDescriptionSessionPerUser.remove(userId);
+ } finally {
+ sLock.unlock();
+ }
}
private Map<String, Set<CredentialDescription>> mCredentialDescriptions;
@@ -74,7 +111,7 @@
int size = mCredentialDescriptions.get(callingPackageName).size();
mCredentialDescriptions.get(callingPackageName)
.addAll(descriptions);
- mTotalDescriptionCount += size - mCredentialDescriptions.get(callingPackageName).size();
+ mTotalDescriptionCount += mCredentialDescriptions.get(callingPackageName).size() - size;
}
}
@@ -93,21 +130,33 @@
}
}
+ /** Returns package names and entries of a CredentialProviders that can satisfy a given
+ * {@link CredentialDescription}. */
+ public Set<FilterResult> getFilteredResultForProvider(String packageName,
+ List<String> flatRequestStrings) {
+ Set<FilterResult> result = new HashSet<>();
+ Set<CredentialDescription> currentSet = mCredentialDescriptions.get(packageName);
+ for (CredentialDescription containedDescription: currentSet) {
+ if (flatRequestStrings.contains(containedDescription.getFlattenedRequestString())) {
+ result.add(new FilterResult(packageName, containedDescription
+ .getCredentialEntries()));
+ }
+ }
+ return result;
+ }
+
/** Returns package names of CredentialProviders that can satisfy a given
* {@link CredentialDescription}. */
- public Set<String> filterCredentials(String flatRequestString) {
-
+ public Set<String> getMatchingProviders(Set<String> flatRequestString) {
Set<String> result = new HashSet<>();
-
- for (String componentName: mCredentialDescriptions.keySet()) {
- Set<CredentialDescription> currentSet = mCredentialDescriptions.get(componentName);
- for (CredentialDescription containedDescription: currentSet) {
- if (flatRequestString.equals(containedDescription.getFlattenedRequestString())) {
- result.add(componentName);
+ for (String packageName: mCredentialDescriptions.keySet()) {
+ Set<CredentialDescription> currentSet = mCredentialDescriptions.get(packageName);
+ for (CredentialDescription containedDescription : currentSet) {
+ if (flatRequestString.contains(containedDescription.getFlattenedRequestString())) {
+ result.add(packageName);
}
}
}
-
return result;
}
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index bb26fa9..ea63c30 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
@@ -29,8 +29,8 @@
import android.credentials.CreateCredentialException;
import android.credentials.CreateCredentialRequest;
import android.credentials.CredentialDescription;
+import android.credentials.CredentialOption;
import android.credentials.GetCredentialException;
-import android.credentials.GetCredentialOption;
import android.credentials.GetCredentialRequest;
import android.credentials.IClearCredentialStateCallback;
import android.credentials.ICreateCredentialCallback;
@@ -49,8 +49,6 @@
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.provider.Settings;
-import android.service.credentials.BeginCreateCredentialRequest;
-import android.service.credentials.BeginGetCredentialRequest;
import android.service.credentials.CallingAppInfo;
import android.service.credentials.CredentialProviderInfo;
import android.text.TextUtils;
@@ -63,13 +61,11 @@
import com.android.server.infra.SecureSettingsServiceNameResolver;
import java.util.ArrayList;
-import java.util.Collection;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
-import java.util.function.Function;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
/**
* Entry point service for credential management.
@@ -198,7 +194,6 @@
}
-
@GuardedBy("mLock")
private List<CredentialManagerServiceImpl> getOrConstructSystemServiceListLock(
int resolvedUserId) {
@@ -238,6 +233,7 @@
concatenatedServices.addAll(getOrConstructSystemServiceListLock(userId));
return concatenatedServices;
}
+
public static boolean isCredentialDescriptionApiEnabled() {
return DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_CREDENTIAL, DEVICE_CONFIG_ENABLE_CREDENTIAL_DESC_API, false);
@@ -246,44 +242,38 @@
@SuppressWarnings("GuardedBy") // ErrorProne requires initiateProviderSessionForRequestLocked
// to be guarded by 'service.mLock', which is the same as mLock.
private List<ProviderSession> initiateProviderSessionsWithActiveContainers(
- RequestSession session,
- List<String> requestOptions, Set<ComponentName> activeCredentialContainers) {
+ GetRequestSession session,
+ List<String> requestOptions, Set<String> activeCredentialContainers) {
List<ProviderSession> providerSessions = new ArrayList<>();
// Invoke all services of a user to initiate a provider session
- runForUser((service) -> {
- if (activeCredentialContainers.contains(service.getComponentName())) {
- ProviderSession providerSession = service
- .initiateProviderSessionForRequestLocked(session, requestOptions);
- if (providerSession != null) {
- providerSessions.add(providerSession);
- }
- }
- });
+ for (String packageName: activeCredentialContainers) {
+ providerSessions.add(ProviderRegistryGetSession.createNewSession(
+ mContext,
+ UserHandle.getCallingUserId(),
+ session,
+ packageName,
+ requestOptions));
+ }
return providerSessions;
}
@NonNull
- private Set<String> getMatchingProviders(GetCredentialRequest request) {
+ private Set<String> getFilteredResultFromRegistry(List<CredentialOption> options) {
// Session for active/provisioned credential descriptions;
CredentialDescriptionRegistry registry = CredentialDescriptionRegistry
.forUser(UserHandle.getCallingUserId());
// All requested credential descriptions based on the given request.
Set<String> requestedCredentialDescriptions =
- request.getGetCredentialOptions().stream().map(
+ options.stream().map(
getCredentialOption -> getCredentialOption
.getCredentialRetrievalData()
- .getString(GetCredentialOption
+ .getString(CredentialOption
.FLATTENED_REQUEST))
.collect(Collectors.toSet());
// All requested credential descriptions based on the given request.
- return requestedCredentialDescriptions.stream()
- .map(registry::filterCredentials)
- .flatMap(
- (Function<Set<String>, Stream<String>>)
- Collection::stream)
- .collect(Collectors.toSet());
+ return registry.getMatchingProviders(requestedCredentialDescriptions);
}
@SuppressWarnings("GuardedBy") // ErrorProne requires initiateProviderSessionForRequestLocked
@@ -306,6 +296,13 @@
return providerSessions;
}
+ @Override
+ @GuardedBy("CredentialDescriptionRegistry.sLock")
+ public void onUserStopped(@NonNull TargetUser user) {
+ super.onUserStopped(user);
+ CredentialDescriptionRegistry.clearUserSession(user.getUserIdentifier());
+ }
+
private CallingAppInfo constructCallingAppInfo(String packageName, int userId) {
final PackageInfo packageInfo;
try {
@@ -340,15 +337,60 @@
callingUid,
callback,
request,
- constructCallingAppInfo(callingPackage, userId));
+ constructCallingAppInfo(callingPackage, userId),
+ CancellationSignal.fromTransport(cancelTransport));
- // Initiate all provider sessions
- List<ProviderSession> providerSessions =
+ List<ProviderSession> providerSessions;
+
+ if (isCredentialDescriptionApiEnabled()) {
+ List<CredentialOption> optionsThatRequireActiveCredentials =
+ request.getCredentialOptions().stream()
+ .filter(getCredentialOption ->
+ !TextUtils.isEmpty(getCredentialOption
+ .getCredentialRetrievalData().getString(
+ CredentialOption
+ .FLATTENED_REQUEST, null)))
+ .toList();
+
+ List<CredentialOption> optionsThatDoNotRequireActiveCredentials =
+ request.getCredentialOptions().stream()
+ .filter(getCredentialOption ->
+ TextUtils.isEmpty(getCredentialOption
+ .getCredentialRetrievalData().getString(
+ CredentialOption
+ .FLATTENED_REQUEST, null)))
+ .toList();
+
+ List<ProviderSession> sessionsWithoutRemoteService =
+ initiateProviderSessionsWithActiveContainers(session,
+ optionsThatRequireActiveCredentials
+ .stream().map(getCredentialOption ->
+ getCredentialOption.getCredentialRetrievalData()
+ .getString(CredentialOption
+ .FLATTENED_REQUEST))
+ .collect(Collectors.toList()),
+ getFilteredResultFromRegistry(optionsThatRequireActiveCredentials));
+
+ List<ProviderSession> sessionsWithRemoteService = initiateProviderSessions(
+ session,
+ optionsThatDoNotRequireActiveCredentials.stream()
+ .map(CredentialOption::getType)
+ .collect(Collectors.toList()));
+
+ Set<ProviderSession> all = new LinkedHashSet<>();
+ all.addAll(sessionsWithRemoteService);
+ all.addAll(sessionsWithoutRemoteService);
+
+ providerSessions = new ArrayList<>(all);
+ } else {
+ // Initiate all provider sessions
+ providerSessions =
initiateProviderSessions(
session,
- request.getGetCredentialOptions().stream()
- .map(GetCredentialOption::getType)
+ request.getCredentialOptions().stream()
+ .map(CredentialOption::getType)
.collect(Collectors.toList()));
+ }
if (providerSessions.isEmpty()) {
try {
@@ -362,13 +404,8 @@
+ e.getMessage());
}
}
+ providerSessions.forEach(ProviderSession::invokeSession);
- // Iterate over all provider sessions and invoke the request
- providerSessions.forEach(
- providerGetSession -> providerGetSession
- .getRemoteCredentialService().onBeginGetCredential(
- (BeginGetCredentialRequest) providerGetSession.getProviderRequest(),
- /*callback=*/providerGetSession));
return cancelTransport;
}
@@ -391,7 +428,8 @@
callingUid,
request,
callback,
- constructCallingAppInfo(callingPackage, userId));
+ constructCallingAppInfo(callingPackage, userId),
+ CancellationSignal.fromTransport(cancelTransport));
// Initiate all provider sessions
List<ProviderSession> providerSessions =
@@ -411,13 +449,7 @@
}
// Iterate over all provider sessions and invoke the request
- providerSessions.forEach(
- providerCreateSession -> providerCreateSession
- .getRemoteCredentialService()
- .onCreateCredential(
- (BeginCreateCredentialRequest)
- providerCreateSession.getProviderRequest(),
- /* callback= */ providerCreateSession));
+ providerSessions.forEach(ProviderSession::invokeSession);
return cancelTransport;
}
@@ -508,7 +540,8 @@
callingUid,
callback,
request,
- constructCallingAppInfo(callingPackage, userId));
+ constructCallingAppInfo(callingPackage, userId),
+ CancellationSignal.fromTransport(cancelTransport));
// Initiate all provider sessions
// TODO: Determine if provider needs to have clear capability in their manifest
@@ -529,15 +562,7 @@
}
// Iterate over all provider sessions and invoke the request
- providerSessions.forEach(
- providerClearSession -> {
- providerClearSession
- .getRemoteCredentialService()
- .onClearCredentialState(
- (android.service.credentials.ClearCredentialStateRequest)
- providerClearSession.getProviderRequest(),
- /* callback= */ providerClearSession);
- });
+ providerSessions.forEach(ProviderSession::invokeSession);
return cancelTransport;
}
diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
index e3a27ec..e732c23 100644
--- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
@@ -25,6 +25,7 @@
import android.credentials.IGetCredentialCallback;
import android.credentials.ui.ProviderData;
import android.credentials.ui.RequestInfo;
+import android.os.CancellationSignal;
import android.os.RemoteException;
import android.service.credentials.CallingAppInfo;
import android.service.credentials.CredentialProviderInfo;
@@ -43,8 +44,9 @@
public GetRequestSession(Context context, int userId, int callingUid,
IGetCredentialCallback callback, GetCredentialRequest request,
- CallingAppInfo callingAppInfo) {
- super(context, userId, callingUid, request, callback, RequestInfo.TYPE_GET, callingAppInfo);
+ CallingAppInfo callingAppInfo, CancellationSignal cancellationSignal) {
+ super(context, userId, callingUid, request, callback, RequestInfo.TYPE_GET,
+ callingAppInfo, cancellationSignal);
}
/**
@@ -102,6 +104,12 @@
}
private void respondToClientWithResponseAndFinish(GetCredentialResponse response) {
+ if (isSessionCancelled()) {
+ // TODO: Differentiate btw cancelled and false
+ logApiCalled(RequestType.GET_CREDENTIALS, /* isSuccessful */ false);
+ finishSession(/*propagateCancellation=*/true);
+ return;
+ }
try {
mClientCallback.onResponse(response);
logApiCalled(RequestType.GET_CREDENTIALS, /* isSuccessful */ true);
@@ -109,18 +117,22 @@
Log.i(TAG, "Issue while responding to client with a response : " + e.getMessage());
logApiCalled(RequestType.GET_CREDENTIALS, /* isSuccessful */ false);
}
- finishSession();
+ finishSession(/*propagateCancellation=*/false);
}
private void respondToClientWithErrorAndFinish(String errorType, String errorMsg) {
+ if (isSessionCancelled()) {
+ logApiCalled(RequestType.GET_CREDENTIALS, /* isSuccessful */ false);
+ finishSession(/*propagateCancellation=*/true);
+ return;
+ }
try {
mClientCallback.onError(errorType, errorMsg);
} catch (RemoteException e) {
Log.i(TAG, "Issue while responding to client with error : " + e.getMessage());
-
}
logApiCalled(RequestType.GET_CREDENTIALS, /* isSuccessful */ false);
- finishSession();
+ finishSession(/*propagateCancellation=*/false);
}
@Override
diff --git a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java
index 48e35b2..b20f0cd 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java
@@ -115,4 +115,11 @@
ProviderPendingIntentResponse providerPendingIntentResponse) {
//Not applicable for clearCredential as response is not picked by the user
}
+
+ @Override
+ protected void invokeSession() {
+ if (mRemoteCredentialService != null) {
+ mRemoteCredentialService.onClearCredentialState(mProviderRequest, this);
+ }
+ }
}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
index 7a24a22..ade40ad 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
@@ -51,6 +51,8 @@
// Key to be used as an entry key for a save entry
private static final String SAVE_ENTRY_KEY = "save_entry_key";
+ // Key to be used as an entry key for a remote entry
+ private static final String REMOTE_ENTRY_KEY = "remote_entry_key";
@NonNull
private final Map<String, CreateEntry> mUiSaveEntries = new HashMap<>();
@@ -197,6 +199,13 @@
}
}
+ @Override
+ protected void invokeSession() {
+ if (mRemoteCredentialService != null) {
+ mRemoteCredentialService.onCreateCredential(mProviderRequest, this);
+ }
+ }
+
private List<Entry> prepareUiSaveEntries(@NonNull List<CreateEntry> saveEntries) {
Log.i(TAG, "in populateUiSaveEntries");
List<Entry> uiSaveEntries = new ArrayList<>();
diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
index 95f2313..3ccead1 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
@@ -21,8 +21,8 @@
import android.annotation.UserIdInt;
import android.content.Context;
import android.content.Intent;
+import android.credentials.CredentialOption;
import android.credentials.GetCredentialException;
-import android.credentials.GetCredentialOption;
import android.credentials.GetCredentialResponse;
import android.credentials.ui.Entry;
import android.credentials.ui.GetCredentialProviderData;
@@ -59,14 +59,14 @@
implements
RemoteCredentialService.ProviderCallbacks<BeginGetCredentialResponse> {
private static final String TAG = "ProviderGetSession";
-
- // Key to be used as an entry key for a credential entry
- private static final String CREDENTIAL_ENTRY_KEY = "credential_key";
-
// Key to be used as the entry key for an action entry
private static final String ACTION_ENTRY_KEY = "action_key";
// Key to be used as the entry key for the authentication entry
private static final String AUTHENTICATION_ACTION_ENTRY_KEY = "authentication_action_key";
+ // Key to be used as an entry key for a remote entry
+ private static final String REMOTE_ENTRY_KEY = "remote_entry_key";
+ // Key to be used as an entry key for a credential entry
+ private static final String CREDENTIAL_ENTRY_KEY = "credential_key";
@NonNull
private final Map<String, CredentialEntry> mUiCredentialEntries = new HashMap<>();
@@ -101,28 +101,13 @@
return null;
}
- private static BeginGetCredentialRequest constructQueryPhaseRequest(
- android.credentials.GetCredentialRequest filteredRequest,
- CallingAppInfo callingAppInfo
- ) {
- return new BeginGetCredentialRequest.Builder(callingAppInfo)
- .setBeginGetCredentialOptions(
- filteredRequest.getGetCredentialOptions().stream().map(
- option -> {
- return new BeginGetCredentialOption(
- option.getType(),
- option.getCandidateQueryData());
- }).collect(Collectors.toList()))
- .build();
- }
-
@Nullable
- private static android.credentials.GetCredentialRequest filterOptions(
+ protected static android.credentials.GetCredentialRequest filterOptions(
List<String> providerCapabilities,
android.credentials.GetCredentialRequest clientRequest
) {
- List<GetCredentialOption> filteredOptions = new ArrayList<>();
- for (GetCredentialOption option : clientRequest.getGetCredentialOptions()) {
+ List<CredentialOption> filteredOptions = new ArrayList<>();
+ for (CredentialOption option : clientRequest.getCredentialOptions()) {
if (providerCapabilities.contains(option.getType())) {
Log.i(TAG, "In createProviderRequest - capability found : "
+ option.getType());
@@ -135,13 +120,28 @@
if (!filteredOptions.isEmpty()) {
return new android.credentials.GetCredentialRequest
.Builder(clientRequest.getData())
- .setGetCredentialOptions(
+ .setCredentialOptions(
filteredOptions).build();
}
Log.i(TAG, "In createProviderRequest - returning null");
return null;
}
+ private static BeginGetCredentialRequest constructQueryPhaseRequest(
+ android.credentials.GetCredentialRequest filteredRequest,
+ CallingAppInfo callingAppInfo
+ ) {
+ return new BeginGetCredentialRequest.Builder(callingAppInfo)
+ .setBeginGetCredentialOptions(
+ filteredRequest.getCredentialOptions().stream().map(
+ option -> {
+ return new BeginGetCredentialOption(
+ option.getType(),
+ option.getCandidateQueryData());
+ }).collect(Collectors.toList()))
+ .build();
+ }
+
public ProviderGetSession(Context context,
CredentialProviderInfo info,
ProviderInternalCallback<GetCredentialResponse> callbacks,
@@ -230,6 +230,13 @@
}
}
+ @Override
+ protected void invokeSession() {
+ if (mRemoteCredentialService != null) {
+ mRemoteCredentialService.onBeginGetCredential(mProviderRequest, this);
+ }
+ }
+
@Override // Call from request session to data to be shown on the UI
@Nullable protected GetCredentialProviderData prepareUiData() throws IllegalArgumentException {
Log.i(TAG, "In prepareUiData");
@@ -300,7 +307,7 @@
private Intent setUpFillInIntent(String type) {
Intent intent = new Intent();
- for (GetCredentialOption option : mCompleteRequest.getGetCredentialOptions()) {
+ for (CredentialOption option : mCompleteRequest.getCredentialOptions()) {
if (option.getType().equals(type)) {
intent.putExtra(
CredentialProviderService
@@ -372,6 +379,28 @@
invokeCallbackOnInternalInvalidState();
}
+ @Nullable
+ protected GetCredentialException maybeGetPendingIntentException(
+ ProviderPendingIntentResponse pendingIntentResponse) {
+ if (pendingIntentResponse == null) {
+ Log.i(TAG, "pendingIntentResponse is null");
+ return null;
+ }
+ if (PendingIntentResultHandler.isValidResponse(pendingIntentResponse)) {
+ GetCredentialException exception = PendingIntentResultHandler
+ .extractGetCredentialException(pendingIntentResponse.getResultData());
+ if (exception != null) {
+ Log.i(TAG, "Pending intent contains provider exception");
+ return exception;
+ }
+ } else if (PendingIntentResultHandler.isCancelledResponse(pendingIntentResponse)) {
+ return new GetCredentialException(GetCredentialException.TYPE_USER_CANCELED);
+ } else {
+ return new GetCredentialException(GetCredentialException.TYPE_NO_CREDENTIAL);
+ }
+ return null;
+ }
+
private void onAuthenticationEntrySelected(
@Nullable ProviderPendingIntentResponse providerPendingIntentResponse) {
//TODO: Other provider intent statuses
@@ -424,28 +453,6 @@
updateStatusAndInvokeCallback(Status.NO_CREDENTIALS);
}
- @Nullable
- private GetCredentialException maybeGetPendingIntentException(
- ProviderPendingIntentResponse pendingIntentResponse) {
- if (pendingIntentResponse == null) {
- Log.i(TAG, "pendingIntentResponse is null");
- return null;
- }
- if (PendingIntentResultHandler.isValidResponse(pendingIntentResponse)) {
- GetCredentialException exception = PendingIntentResultHandler
- .extractGetCredentialException(pendingIntentResponse.getResultData());
- if (exception != null) {
- Log.i(TAG, "Pending intent contains provider exception");
- return exception;
- }
- } else if (PendingIntentResultHandler.isCancelledResponse(pendingIntentResponse)) {
- return new GetCredentialException(GetCredentialException.TYPE_USER_CANCELED);
- } else {
- return new GetCredentialException(GetCredentialException.TYPE_NO_CREDENTIAL);
- }
- return null;
- }
-
/**
* When an invalid state occurs, e.g. entry mismatch or no response from provider,
* we send back a TYPE_UNKNOWN error as to the developer.
diff --git a/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java
new file mode 100644
index 0000000..461f447
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.credentials;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.Context;
+import android.content.Intent;
+import android.credentials.CredentialOption;
+import android.credentials.GetCredentialException;
+import android.credentials.GetCredentialRequest;
+import android.credentials.GetCredentialResponse;
+import android.credentials.ui.Entry;
+import android.credentials.ui.GetCredentialProviderData;
+import android.credentials.ui.ProviderData;
+import android.credentials.ui.ProviderPendingIntentResponse;
+import android.service.credentials.CallingAppInfo;
+import android.service.credentials.CredentialEntry;
+import android.service.credentials.CredentialProviderService;
+import android.telecom.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Central provider session that utilizes {@link CredentialDescriptionRegistry} and therefor is able
+ * to bypass having to use a {@link RemoteCredentialService}.
+ *
+ * @hide
+ */
+public class ProviderRegistryGetSession extends ProviderSession<GetCredentialRequest,
+ Set<CredentialDescriptionRegistry.FilterResult>> {
+
+ private static final String TAG = "ProviderRegistryGetSession";
+ private static final String CREDENTIAL_ENTRY_KEY = "credential_key";
+
+ /** Creates a new provider session to be used by the request session. */
+ @Nullable
+ public static ProviderRegistryGetSession createNewSession(
+ @NonNull Context context,
+ @UserIdInt int userId,
+ @NonNull GetRequestSession getRequestSession,
+ @NonNull String credentialProviderPackageName,
+ @NonNull List<String> requestOptions) {
+ return new ProviderRegistryGetSession(
+ context,
+ userId,
+ getRequestSession,
+ getRequestSession.mClientRequest,
+ getRequestSession.mClientAppInfo,
+ credentialProviderPackageName,
+ requestOptions);
+ }
+
+ @NonNull
+ private final Map<String, CredentialEntry> mUiCredentialEntries = new HashMap<>();
+ @NonNull
+ private final CredentialDescriptionRegistry mCredentialDescriptionRegistry;
+ @NonNull
+ private final CallingAppInfo mCallingAppInfo;
+ @NonNull
+ private final String mCredentialProviderPackageName;
+ @NonNull
+ private final GetRequestSession mGetRequestSession;
+ @NonNull
+ private final List<String> mRequestOptions;
+ private List<CredentialEntry> mCredentialEntries;
+
+ protected ProviderRegistryGetSession(@NonNull Context context,
+ @NonNull int userId,
+ @NonNull GetRequestSession session,
+ @NonNull GetCredentialRequest request,
+ @NonNull CallingAppInfo callingAppInfo,
+ @NonNull String servicePackageName,
+ @NonNull List<String> requestOptions) {
+ super(context, null, request, session, userId, null);
+ mGetRequestSession = session;
+ mCredentialDescriptionRegistry = CredentialDescriptionRegistry.forUser(userId);
+ mCallingAppInfo = callingAppInfo;
+ mCredentialProviderPackageName = servicePackageName;
+ mRequestOptions = requestOptions;
+ }
+
+ private List<Entry> prepareUiCredentialEntries(
+ @NonNull List<CredentialEntry> credentialEntries) {
+ Log.i(TAG, "in prepareUiProviderDataWithCredentials");
+ List<Entry> credentialUiEntries = new ArrayList<>();
+
+ // Populate the credential entries
+ for (CredentialEntry credentialEntry : credentialEntries) {
+ String entryId = generateEntryId();
+ mUiCredentialEntries.put(entryId, credentialEntry);
+ Log.i(TAG, "in prepareUiProviderData creating ui entry with id " + entryId);
+ credentialUiEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId,
+ credentialEntry.getSlice(),
+ setUpFillInIntent(credentialEntry.getType())));
+ }
+ return credentialUiEntries;
+ }
+
+ private Intent setUpFillInIntent(String type) {
+ Intent intent = new Intent();
+ for (CredentialOption option : mProviderRequest.getCredentialOptions()) {
+ if (option.getType().equals(type)) {
+ intent.putExtra(
+ CredentialProviderService
+ .EXTRA_GET_CREDENTIAL_REQUEST,
+ new android.service.credentials.GetCredentialRequest(
+ mCallingAppInfo, option));
+ return intent;
+ }
+ }
+ return intent;
+ }
+
+ @Override
+ protected ProviderData prepareUiData() {
+ Log.i(TAG, "In prepareUiData");
+ if (!ProviderSession.isUiInvokingStatus(getStatus())) {
+ Log.i(TAG, "In prepareUiData - provider does not want to show UI: "
+ + mComponentName.flattenToString());
+ return null;
+ }
+ if (mProviderResponse == null) {
+ Log.i(TAG, "In prepareUiData response null");
+ throw new IllegalStateException("Response must be in completion mode");
+ }
+ return new GetCredentialProviderData.Builder(
+ mComponentName.flattenToString()).setActionChips(null)
+ .setCredentialEntries(prepareUiCredentialEntries(
+ mProviderResponse.stream().flatMap((Function<CredentialDescriptionRegistry
+ .FilterResult,
+ Stream<CredentialEntry>>) filterResult ->
+ filterResult.mCredentialEntries.stream())
+ .collect(Collectors.toList())))
+ .build();
+ }
+
+ @Override // Selection call from the request provider
+ protected void onUiEntrySelected(String entryType, String entryKey,
+ ProviderPendingIntentResponse providerPendingIntentResponse) {
+ switch (entryType) {
+ case CREDENTIAL_ENTRY_KEY:
+ CredentialEntry credentialEntry = mUiCredentialEntries.get(entryKey);
+ if (credentialEntry == null) {
+ Log.i(TAG, "Unexpected credential entry key");
+ return;
+ }
+ onCredentialEntrySelected(credentialEntry, providerPendingIntentResponse);
+ break;
+ default:
+ Log.i(TAG, "Unsupported entry type selected");
+ }
+ }
+
+ private void onCredentialEntrySelected(CredentialEntry credentialEntry,
+ ProviderPendingIntentResponse providerPendingIntentResponse) {
+ if (!mCredentialEntries.contains(credentialEntry)) {
+ invokeCallbackWithError("",
+ "");
+ }
+
+ if (providerPendingIntentResponse != null) {
+ // Check if pending intent has an error
+ GetCredentialException exception = maybeGetPendingIntentException(
+ providerPendingIntentResponse);
+ if (exception != null) {
+ invokeCallbackWithError(exception.getType(),
+ exception.getMessage());
+ return;
+ }
+
+ // Check if pending intent has a credential
+ GetCredentialResponse getCredentialResponse = PendingIntentResultHandler
+ .extractGetCredentialResponse(
+ providerPendingIntentResponse.getResultData());
+ if (getCredentialResponse != null) {
+ if (mCallbacks != null) {
+ mCallbacks.onFinalResponseReceived(mComponentName,
+ getCredentialResponse);
+ }
+ return;
+ }
+
+ Log.i(TAG, "Pending intent response contains no credential, or error");
+ }
+ Log.i(TAG, "CredentialEntry does not have a credential or a pending intent result");
+ }
+
+ @Override
+ public void onProviderResponseSuccess(
+ @Nullable Set<CredentialDescriptionRegistry.FilterResult> response) {
+ // No need to do anything since this class does not rely on a remote service.
+ }
+
+ @Override
+ public void onProviderResponseFailure(int internalErrorCode, @Nullable Exception e) {
+ // No need to do anything since this class does not rely on a remote service.
+ }
+
+ @Override
+ public void onProviderServiceDied(RemoteCredentialService service) {
+ // No need to do anything since this class does not rely on a remote service.
+ }
+
+ @Override
+ protected void invokeSession() {
+ mProviderResponse = mCredentialDescriptionRegistry
+ .getFilteredResultForProvider(mCredentialProviderPackageName,
+ mRequestOptions);
+ mCredentialEntries = mProviderResponse.stream().flatMap(
+ (Function<CredentialDescriptionRegistry.FilterResult,
+ Stream<CredentialEntry>>) filterResult
+ -> filterResult.mCredentialEntries.stream())
+ .collect(Collectors.toList());
+ setStatus(Status.CREDENTIALS_RECEIVED);
+ }
+
+ @Nullable
+ protected GetCredentialException maybeGetPendingIntentException(
+ ProviderPendingIntentResponse pendingIntentResponse) {
+ if (pendingIntentResponse == null) {
+ android.util.Log.i(TAG, "pendingIntentResponse is null");
+ return null;
+ }
+ if (PendingIntentResultHandler.isValidResponse(pendingIntentResponse)) {
+ GetCredentialException exception = PendingIntentResultHandler
+ .extractGetCredentialException(pendingIntentResponse.getResultData());
+ if (exception != null) {
+ android.util.Log.i(TAG, "Pending intent contains provider exception");
+ return exception;
+ }
+ } else if (PendingIntentResultHandler.isCancelledResponse(pendingIntentResponse)) {
+ return new GetCredentialException(GetCredentialException.TYPE_USER_CANCELED);
+ } else {
+ return new GetCredentialException(GetCredentialException.TYPE_NO_CREDENTIAL);
+ }
+ return null;
+ }
+}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java
index 678c752..d6f97ff 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java
@@ -23,8 +23,11 @@
import android.credentials.Credential;
import android.credentials.ui.ProviderData;
import android.credentials.ui.ProviderPendingIntentResponse;
+import android.os.ICancellationSignal;
+import android.os.RemoteException;
import android.service.credentials.CredentialEntry;
import android.service.credentials.CredentialProviderInfo;
+import android.util.Log;
import android.util.Pair;
import java.util.UUID;
@@ -38,17 +41,16 @@
implements RemoteCredentialService.ProviderCallbacks<R> {
private static final String TAG = "ProviderSession";
- // Key to be used as an entry key for a remote entry
- protected static final String REMOTE_ENTRY_KEY = "remote_entry_key";
@NonNull protected final Context mContext;
@NonNull protected final ComponentName mComponentName;
- @NonNull protected final CredentialProviderInfo mProviderInfo;
- @NonNull protected final RemoteCredentialService mRemoteCredentialService;
+ @Nullable protected final CredentialProviderInfo mProviderInfo;
+ @Nullable protected final RemoteCredentialService mRemoteCredentialService;
@NonNull protected final int mUserId;
@NonNull protected Status mStatus = Status.NOT_STARTED;
- @NonNull protected final ProviderInternalCallback mCallbacks;
+ @Nullable protected final ProviderInternalCallback mCallbacks;
@Nullable protected Credential mFinalCredentialResponse;
+ @Nullable protected ICancellationSignal mProviderCancellationSignal;
@NonNull protected final T mProviderRequest;
@Nullable protected R mProviderResponse;
@NonNull protected Boolean mProviderResponseSet = false;
@@ -109,9 +111,9 @@
protected ProviderSession(@NonNull Context context, @NonNull CredentialProviderInfo info,
@NonNull T providerRequest,
- @NonNull ProviderInternalCallback callbacks,
+ @Nullable ProviderInternalCallback callbacks,
@NonNull int userId,
- @NonNull RemoteCredentialService remoteCredentialService) {
+ @Nullable RemoteCredentialService remoteCredentialService) {
mContext = context;
mProviderInfo = info;
mProviderRequest = providerRequest;
@@ -151,6 +153,18 @@
return mFinalCredentialResponse;
}
+ /** Propagates cancellation signal to the remote provider service. */
+ public void cancelProviderRemoteSession() {
+ try {
+ if (mProviderCancellationSignal != null) {
+ mProviderCancellationSignal.cancel();
+ }
+ setStatus(Status.CANCELED);
+ } catch (RemoteException e) {
+ Log.i(TAG, "Issue while cancelling provider session: " + e.getMessage());
+ }
+ }
+
protected void setStatus(@NonNull Status status) {
mStatus = status;
}
@@ -165,7 +179,7 @@
return mComponentName;
}
- @NonNull
+ @Nullable
protected RemoteCredentialService getRemoteCredentialService() {
return mRemoteCredentialService;
}
@@ -208,4 +222,8 @@
/** Should be overridden to handle the selected entry from the UI. */
protected abstract void onUiEntrySelected(String entryType, String entryId,
ProviderPendingIntentResponse providerPendingIntentResponse);
+
+ /** Should be overridden to invoke the provider at a defined location. Helpful for
+ * situations such as metric generation. */
+ protected abstract void invokeSession();
}
diff --git a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java
index 8cad6ac..2dea8bd 100644
--- a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java
+++ b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java
@@ -110,7 +110,7 @@
* @param callback the callback to be used to send back the provider response to the
* {@link ProviderGetSession} class that maintains provider state
*/
- public void onBeginGetCredential(@NonNull BeginGetCredentialRequest request,
+ public ICancellationSignal onBeginGetCredential(@NonNull BeginGetCredentialRequest request,
ProviderCallbacks<BeginGetCredentialResponse> callback) {
Log.i(TAG, "In onGetCredentials in RemoteCredentialService");
AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>();
@@ -149,6 +149,8 @@
futureRef.set(connectThenExecute);
connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() ->
handleExecutionResponse(result, error, cancellationSink, callback)));
+
+ return cancellationSink.get();
}
/** Main entry point to be called for executing a beginCreateCredential call on the remote
@@ -157,7 +159,7 @@
* @param callback the callback to be used to send back the provider response to the
* {@link ProviderCreateSession} class that maintains provider state
*/
- public void onCreateCredential(@NonNull BeginCreateCredentialRequest request,
+ public ICancellationSignal onCreateCredential(@NonNull BeginCreateCredentialRequest request,
ProviderCallbacks<BeginCreateCredentialResponse> callback) {
Log.i(TAG, "In onCreateCredential in RemoteCredentialService");
AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>();
@@ -196,6 +198,8 @@
futureRef.set(connectThenExecute);
connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() ->
handleExecutionResponse(result, error, cancellationSink, callback)));
+
+ return cancellationSink.get();
}
/** Main entry point to be called for executing a clearCredentialState call on the remote
@@ -204,7 +208,7 @@
* @param callback the callback to be used to send back the provider response to the
* {@link ProviderClearSession} class that maintains provider state
*/
- public void onClearCredentialState(@NonNull ClearCredentialStateRequest request,
+ public ICancellationSignal onClearCredentialState(@NonNull ClearCredentialStateRequest request,
ProviderCallbacks<Void> callback) {
Log.i(TAG, "In onClearCredentialState in RemoteCredentialService");
AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>();
@@ -243,6 +247,8 @@
futureRef.set(connectThenExecute);
connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() ->
handleExecutionResponse(result, error, cancellationSink, callback)));
+
+ return cancellationSink.get();
}
private <T> void handleExecutionResponse(T result,
diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java
index f92ffe2..9f1bd8f 100644
--- a/services/credentials/java/com/android/server/credentials/RequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/RequestSession.java
@@ -29,6 +29,7 @@
import android.credentials.ui.ProviderData;
import android.credentials.ui.UserSelectionDialogResult;
import android.os.Binder;
+import android.os.CancellationSignal;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -83,13 +84,16 @@
private final int mCallingUid;
@NonNull
protected final CallingAppInfo mClientAppInfo;
+ @NonNull
+ protected final CancellationSignal mCancellationSignal;
protected final Map<String, ProviderSession> mProviders = new HashMap<>();
protected RequestSession(@NonNull Context context,
@UserIdInt int userId, int callingUid, @NonNull T clientRequest, U clientCallback,
@NonNull String requestType,
- CallingAppInfo callingAppInfo) {
+ CallingAppInfo callingAppInfo,
+ CancellationSignal cancellationSignal) {
mContext = context;
mUserId = userId;
mCallingUid = callingUid;
@@ -97,6 +101,7 @@
mClientCallback = clientCallback;
mRequestType = requestType;
mClientAppInfo = callingAppInfo;
+ mCancellationSignal = cancellationSignal;
mHandler = new Handler(Looper.getMainLooper(), null, true);
mRequestId = new Binder();
mCredentialManagerUi = new CredentialManagerUi(mContext,
@@ -112,6 +117,10 @@
@Override // from CredentialManagerUiCallbacks
public void onUiSelection(UserSelectionDialogResult selection) {
+ if (isSessionCancelled()) {
+ finishSession(/*propagateCancellation=*/true);
+ return;
+ }
String providerId = selection.getProviderId();
Log.i(TAG, "onUiSelection, providerId: " + providerId);
ProviderSession providerSession = mProviders.get(providerId);
@@ -127,18 +136,19 @@
@Override // from CredentialManagerUiCallbacks
public void onUiCancellation(boolean isUserCancellation) {
Log.i(TAG, "Ui canceled. Canceled by user: " + isUserCancellation);
+ if (isSessionCancelled()) {
+ finishSession(/*propagateCancellation=*/true);
+ return;
+ }
// User canceled the activity
- finishSession();
+ finishSession(/*propagateCancellation=*/false);
}
- protected void finishSession() {
+ protected void finishSession(boolean propagateCancellation) {
Log.i(TAG, "finishing session");
- clearProviderSessions();
- }
-
- protected void clearProviderSessions() {
- Log.i(TAG, "Clearing sessions");
- //TODO: Implement
+ if (propagateCancellation) {
+ mProviders.values().forEach(ProviderSession::cancelProviderRemoteSession);
+ }
mProviders.clear();
}
@@ -178,6 +188,10 @@
isSuccessful ? METRICS_API_STATUS_SUCCESS : METRICS_API_STATUS_FAILURE);
}
+ protected boolean isSessionCancelled() {
+ return mCancellationSignal.isCanceled();
+ }
+
/**
* Returns true if at least one provider is ready for UI invocation, and no
* provider is pending a response.
@@ -197,6 +211,11 @@
Log.i(TAG, "In getProviderDataAndInitiateUi");
Log.i(TAG, "In getProviderDataAndInitiateUi providers size: " + mProviders.size());
+ if (isSessionCancelled()) {
+ finishSession(/*propagateCancellation=*/true);
+ return;
+ }
+
ArrayList<ProviderData> providerDataList = new ArrayList<>();
for (ProviderSession session : mProviders.values()) {
Log.i(TAG, "preparing data for : " + session.getComponentName());
@@ -208,7 +227,11 @@
}
if (!providerDataList.isEmpty()) {
Log.i(TAG, "provider list not empty about to initiate ui");
- launchUiWithProviderData(providerDataList);
+ if (isSessionCancelled()) {
+ Log.i(TAG, "In getProviderDataAndInitiateUi but session has been cancelled");
+ } else {
+ launchUiWithProviderData(providerDataList);
+ }
}
}
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index c67ffd5..c6b2207 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -18435,9 +18435,9 @@
.addExtras(extras)
.build();
- mInjector.getNotificationManager().notifyAsUser(
+ mHandler.post(() -> mInjector.getNotificationManager().notifyAsUser(
null, SystemMessage.NOTE_PERSONAL_APPS_SUSPENDED, notification,
- UserHandle.of(getProfileParentId(profileUserId)));
+ UserHandle.of(getProfileParentId(profileUserId))));
}
private String getPersonalAppSuspensionButtonText() {
diff --git a/services/java/com/android/server/BootUserInitializer.java b/services/java/com/android/server/BootUserInitializer.java
deleted file mode 100644
index 3d71739..0000000
--- a/services/java/com/android/server/BootUserInitializer.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2021 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;
-
-import android.annotation.UserIdInt;
-import android.content.ContentResolver;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.provider.Settings;
-
-import com.android.server.am.ActivityManagerService;
-import com.android.server.pm.UserManagerInternal;
-import com.android.server.utils.Slogf;
-import com.android.server.utils.TimingsTraceAndSlog;
-
-/**
- * Class responsible for booting the device in the proper user on headless system user mode.
- *
- */
-// TODO(b/204091126): STOPSHIP - provide proper APIs
-final class BootUserInitializer {
-
- private static final String TAG = BootUserInitializer.class.getSimpleName();
-
- // TODO(b/204091126): STOPSHIP - set to false or dynamic value
- private static final boolean DEBUG = true;
-
- private final ActivityManagerService mAms;
- private final ContentResolver mContentResolver;
-
- BootUserInitializer(ActivityManagerService am, ContentResolver contentResolver) {
- mAms = am;
- mContentResolver = contentResolver;
- }
-
- public void init(TimingsTraceAndSlog t) {
- Slogf.i(TAG, "init())");
-
- // TODO(b/204091126): in the long term, we need to decide who's reponsible for that,
- // this class or the setup wizard app
- provisionHeadlessSystemUser();
-
- unlockSystemUser(t);
-
- try {
- t.traceBegin("getBootUser");
- int bootUser = LocalServices.getService(UserManagerInternal.class).getBootUser();
- t.traceEnd();
- t.traceBegin("switchToBootUser-" + bootUser);
- switchToBootUser(bootUser);
- t.traceEnd();
- } catch (UserManager.CheckedUserOperationException e) {
- Slogf.wtf(TAG, "Failed to created boot user", e);
- }
- }
-
- /* TODO(b/261791491): STOPSHIP - SUW should be responsible for this. */
- private void provisionHeadlessSystemUser() {
- if (isDeviceProvisioned()) {
- Slogf.d(TAG, "provisionHeadlessSystemUser(): already provisioned");
- return;
- }
-
- Slogf.i(TAG, "Marking USER_SETUP_COMPLETE for system user");
- Settings.Secure.putInt(mContentResolver, Settings.Secure.USER_SETUP_COMPLETE, 1);
- Slogf.i(TAG, "Marking DEVICE_PROVISIONED for system user");
- Settings.Global.putInt(mContentResolver, Settings.Global.DEVICE_PROVISIONED, 1);
- }
-
- private boolean isDeviceProvisioned() {
- try {
- return Settings.Global.getInt(mContentResolver,
- Settings.Global.DEVICE_PROVISIONED) == 1;
- } catch (Exception e) {
- Slogf.wtf(TAG, "DEVICE_PROVISIONED setting not found.", e);
- return false;
- }
- }
-
- // NOTE: Mostly copied from Automotive's InitialUserSetter
- private void unlockSystemUser(TimingsTraceAndSlog t) {
- Slogf.i(TAG, "Unlocking system user");
- t.traceBegin("unlock-system-user");
- try {
- // This is for force changing state into RUNNING_LOCKED. Otherwise unlock does not
- // update the state and USER_SYSTEM unlock happens twice.
- t.traceBegin("am.startUser");
- boolean started = mAms.startUserInBackgroundWithListener(UserHandle.USER_SYSTEM,
- /* listener= */ null);
- t.traceEnd();
- if (!started) {
- Slogf.w(TAG, "could not restart system user in background; trying unlock instead");
- t.traceBegin("am.unlockUser");
- boolean unlocked = mAms.unlockUser(UserHandle.USER_SYSTEM, /* token= */ null,
- /* secret= */ null, /* listener= */ null);
- t.traceEnd();
- if (!unlocked) {
- Slogf.w(TAG, "could not unlock system user either");
- return;
- }
- }
- } finally {
- t.traceEnd();
- }
- }
-
- private void switchToBootUser(@UserIdInt int bootUserId) {
- Slogf.i(TAG, "Switching to boot user %d", bootUserId);
- boolean started = mAms.startUserInForegroundWithListener(bootUserId,
- /* unlockListener= */ null);
- if (!started) {
- Slogf.wtf(TAG, "Failed to start user %d in foreground", bootUserId);
- }
- }
-}
diff --git a/services/java/com/android/server/HsumBootUserInitializer.java b/services/java/com/android/server/HsumBootUserInitializer.java
new file mode 100644
index 0000000..c4ad80e
--- /dev/null
+++ b/services/java/com/android/server/HsumBootUserInitializer.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2021 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;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.ContentResolver;
+import android.content.pm.UserInfo;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+
+import com.android.server.am.ActivityManagerService;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.utils.Slogf;
+import com.android.server.utils.TimingsTraceAndSlog;
+
+/**
+ * Class responsible for booting the device in the proper user on headless system user mode.
+ *
+ */
+final class HsumBootUserInitializer {
+
+ private static final String TAG = HsumBootUserInitializer.class.getSimpleName();
+
+ private final UserManagerInternal mUmi;
+ private final ActivityManagerService mAms;
+ private final ContentResolver mContentResolver;
+
+ private final ContentObserver mDeviceProvisionedObserver =
+ new ContentObserver(new Handler(Looper.getMainLooper())) {
+ @Override
+ public void onChange(boolean selfChange) {
+ // Set USER_SETUP_COMPLETE for the (headless) system user only when the device
+ // has been set up at least once.
+ if (isDeviceProvisioned()) {
+ Slogf.i(TAG, "Marking USER_SETUP_COMPLETE for system user");
+ Settings.Secure.putInt(mContentResolver,
+ Settings.Secure.USER_SETUP_COMPLETE, 1);
+ mContentResolver.unregisterContentObserver(mDeviceProvisionedObserver);
+ }
+ }
+ };
+
+ /** Whether this device should always have a non-removable MainUser, including at first boot. */
+ private final boolean mShouldAlwaysHaveMainUser;
+
+ /** Static factory method for creating a {@link HsumBootUserInitializer} instance. */
+ public static @Nullable HsumBootUserInitializer createInstance(ActivityManagerService am,
+ ContentResolver contentResolver, boolean shouldAlwaysHaveMainUser) {
+
+ if (!UserManager.isHeadlessSystemUserMode()) {
+ return null;
+ }
+ return new HsumBootUserInitializer(
+ LocalServices.getService(UserManagerInternal.class),
+ am, contentResolver, shouldAlwaysHaveMainUser);
+ }
+
+ private HsumBootUserInitializer(UserManagerInternal umi, ActivityManagerService am,
+ ContentResolver contentResolver, boolean shouldAlwaysHaveMainUser) {
+ mUmi = umi;
+ mAms = am;
+ mContentResolver = contentResolver;
+ this.mShouldAlwaysHaveMainUser = shouldAlwaysHaveMainUser;
+ }
+
+ /**
+ * Initialize this object, and create MainUser if needed.
+ *
+ * Should be called before PHASE_SYSTEM_SERVICES_READY as services' setups may require MainUser,
+ * but probably after PHASE_LOCK_SETTINGS_READY since that may be needed for user creation.
+ */
+ public void init(TimingsTraceAndSlog t) {
+ Slogf.i(TAG, "init())");
+
+ if (mShouldAlwaysHaveMainUser) {
+ t.traceBegin("createMainUserIfNeeded");
+ createMainUserIfNeeded();
+ t.traceEnd();
+ }
+ }
+
+ private void createMainUserIfNeeded() {
+ int mainUser = mUmi.getMainUserId();
+ if (mainUser != UserHandle.USER_NULL) {
+ Slogf.d(TAG, "Found existing MainUser, userId=%d", mainUser);
+ return;
+ }
+
+ Slogf.d(TAG, "Creating a new MainUser");
+ try {
+ final UserInfo newInitialUser = mUmi.createUserEvenWhenDisallowed(
+ /* name= */ null, // null will appear as "Owner" in on-demand localisation
+ UserManager.USER_TYPE_FULL_SECONDARY,
+ UserInfo.FLAG_ADMIN | UserInfo.FLAG_MAIN,
+ /* disallowedPackages= */ null,
+ /* token= */ null);
+ if (newInitialUser == null) {
+ Slogf.wtf(TAG, "Initial bootable MainUser creation failed: returned null");
+ } else {
+ Slogf.i(TAG, "Successfully created MainUser, userId=%d", newInitialUser.id);
+ }
+ } catch (UserManager.CheckedUserOperationException e) {
+ Slogf.wtf(TAG, "Initial bootable MainUser creation failed", e);
+ }
+ }
+
+ /**
+ * Put the device into the correct user state: unlock the system and switch to the boot user.
+ *
+ * Should only call once PHASE_THIRD_PARTY_APPS_CAN_START is reached to ensure that privileged
+ * apps have had the chance to set the boot user, if applicable.
+ */
+ public void systemRunning(TimingsTraceAndSlog t) {
+ observeDeviceProvisioning();
+ unlockSystemUser(t);
+
+ try {
+ t.traceBegin("getBootUser");
+ final int bootUser = mUmi.getBootUser();
+ t.traceEnd();
+ t.traceBegin("switchToBootUser-" + bootUser);
+ switchToBootUser(bootUser);
+ t.traceEnd();
+ } catch (UserManager.CheckedUserOperationException e) {
+ Slogf.wtf(TAG, "Failed to switch to boot user since there isn't one.");
+ }
+ }
+
+ private void observeDeviceProvisioning() {
+ if (isDeviceProvisioned()) {
+ return;
+ }
+
+ mContentResolver.registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
+ false,
+ mDeviceProvisionedObserver
+ );
+ }
+
+ private boolean isDeviceProvisioned() {
+ try {
+ return Settings.Global.getInt(mContentResolver,
+ Settings.Global.DEVICE_PROVISIONED) == 1;
+ } catch (Exception e) {
+ Slogf.wtf(TAG, "DEVICE_PROVISIONED setting not found.", e);
+ return false;
+ }
+ }
+
+ // NOTE: Mostly copied from Automotive's InitialUserSetter
+ private void unlockSystemUser(TimingsTraceAndSlog t) {
+ Slogf.i(TAG, "Unlocking system user");
+ t.traceBegin("unlock-system-user");
+ try {
+ // This is for force changing state into RUNNING_LOCKED. Otherwise unlock does not
+ // update the state and USER_SYSTEM unlock happens twice.
+ t.traceBegin("am.startUser");
+ boolean started = mAms.startUserInBackgroundWithListener(UserHandle.USER_SYSTEM,
+ /* listener= */ null);
+ t.traceEnd();
+ if (!started) {
+ Slogf.w(TAG, "could not restart system user in background; trying unlock instead");
+ t.traceBegin("am.unlockUser");
+ boolean unlocked = mAms.unlockUser(UserHandle.USER_SYSTEM, /* token= */ null,
+ /* secret= */ null, /* listener= */ null);
+ t.traceEnd();
+ if (!unlocked) {
+ Slogf.w(TAG, "could not unlock system user either");
+ return;
+ }
+ }
+ } finally {
+ t.traceEnd();
+ }
+ }
+
+ private void switchToBootUser(@UserIdInt int bootUserId) {
+ Slogf.i(TAG, "Switching to boot user %d", bootUserId);
+ boolean started = mAms.startUserInForegroundWithListener(bootUserId,
+ /* unlockListener= */ null);
+ if (!started) {
+ Slogf.wtf(TAG, "Failed to start user %d in foreground", bootUserId);
+ }
+ }
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index a15c6d2..5c6cc9f 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -38,6 +38,7 @@
import android.app.SystemServiceRegistry;
import android.app.admin.DevicePolicySafetyChecker;
import android.app.usage.UsageStatsManagerInternal;
+import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
@@ -75,7 +76,6 @@
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
-import android.os.UserManager;
import android.os.storage.IStorageManager;
import android.provider.DeviceConfig;
import android.provider.Settings;
@@ -2694,6 +2694,18 @@
mSystemServiceManager.startBootPhase(t, SystemService.PHASE_LOCK_SETTINGS_READY);
t.traceEnd();
+ // Create initial user if needed, which should be done early since some system services rely
+ // on it in their setup, but likely needs to be done after LockSettingsService is ready.
+ final HsumBootUserInitializer hsumBootUserInitializer =
+ HsumBootUserInitializer.createInstance(
+ mActivityManagerService, mContentResolver,
+ context.getResources().getBoolean(R.bool.config_isMainUserPermanentAdmin));
+ if (hsumBootUserInitializer != null) {
+ t.traceBegin("HsumBootUserInitializer.init");
+ hsumBootUserInitializer.init(t);
+ t.traceEnd();
+ }
+
t.traceBegin("StartBootPhaseSystemServicesReady");
mSystemServiceManager.startBootPhase(t, SystemService.PHASE_SYSTEM_SERVICES_READY);
t.traceEnd();
@@ -2883,6 +2895,27 @@
t.traceEnd();
}
+ if (isWatch) {
+ t.traceBegin("StartWearService");
+ String wearServiceComponentNameString =
+ context.getString(R.string.config_wearServiceComponent);
+
+ if (!TextUtils.isEmpty(wearServiceComponentNameString)) {
+ ComponentName wearServiceComponentName = ComponentName.unflattenFromString(
+ wearServiceComponentNameString);
+
+ if (wearServiceComponentName != null) {
+ Intent intent = new Intent();
+ intent.setComponent(wearServiceComponentName);
+ intent.addFlags(Intent.FLAG_DIRECT_BOOT_AUTO);
+ context.startServiceAsUser(intent, UserHandle.SYSTEM);
+ } else {
+ Slog.d(TAG, "Null wear service component name.");
+ }
+ }
+ t.traceEnd();
+ }
+
// Enable airplane mode in safe mode. setAirplaneMode() cannot be called
// earlier as it sends broadcasts to other services.
// TODO: This may actually be too late if radio firmware already started leaking
@@ -2961,10 +2994,10 @@
mSystemServiceManager.startBootPhase(t, SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
t.traceEnd();
- if (UserManager.isHeadlessSystemUserMode() && !isAutomotive) {
- // TODO(b/204091126): remove isAutomotive check once the workflow is finalized
- t.traceBegin("BootUserInitializer");
- new BootUserInitializer(mActivityManagerService, mContentResolver).init(t);
+ if (hsumBootUserInitializer != null && !isAutomotive) {
+ // TODO(b/261924826): remove isAutomotive check once the workflow is finalized
+ t.traceBegin("HsumBootUserInitializer.systemRunning");
+ hsumBootUserInitializer.systemRunning(t);
t.traceEnd();
}
diff --git a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
index f2cff62..a26b2ac 100644
--- a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
+++ b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
@@ -71,6 +71,10 @@
return opNameMapToOpIntMap(getUidModes(uid))
}
+ override fun getNonDefaultPackageModes(packageName: String, userId: Int): SparseIntArray {
+ return opNameMapToOpIntMap(getPackageModes(packageName, userId))
+ }
+
override fun getUidMode(uid: Int, op: Int): Int {
val appId = UserHandle.getAppId(uid)
val userId = UserHandle.getUserId(uid)
diff --git a/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt
index a9884dd..59551a3 100644
--- a/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt
@@ -196,7 +196,9 @@
val changedPermissionNames = IndexedSet<String>()
trimPermissions(packageName, changedPermissionNames)
- trimPermissionStates(appId)
+ if (appId in newState.systemState.appIds) {
+ trimPermissionStates(appId)
+ }
changedPermissionNames.forEachIndexed { _, permissionName ->
evaluatePermissionStateForAllPackages(permissionName, null)
}
@@ -782,6 +784,7 @@
}
}
} else {
+ val wasGrantedByLegacy = newFlags.hasBits(PermissionFlags.LEGACY_GRANTED)
newFlags = newFlags andInv PermissionFlags.LEGACY_GRANTED
val wasGrantedByImplicit = newFlags.hasBits(PermissionFlags.IMPLICIT_GRANTED)
val isLeanbackNotificationsPermission = newState.systemState.isLeanback &&
@@ -805,10 +808,16 @@
} else {
newFlags = newFlags andInv PermissionFlags.IMPLICIT_GRANTED
}
+ if ((wasGrantedByLegacy || wasGrantedByImplicit) && !shouldGrantByImplicit) {
+ // The permission was granted from a compatibility grant or an implicit grant,
+ // however this flag might still be set if the user denied this permission in
+ // the settings. Hence upon app upgrade and when this permission is no longer
+ // LEGACY_GRANTED or IMPLICIT_GRANTED and we revoke the permission, we want to
+ // remove this flag so that the app can request the permission again.
+ newFlags = newFlags andInv PermissionFlags.APP_OP_REVOKED
+ }
val hasImplicitFlag = newFlags.hasBits(PermissionFlags.IMPLICIT)
if (!isImplicitPermission && hasImplicitFlag) {
- // TODO: We might not want to remove the IMPLICIT flag
- // for NOTIFICATION_PERMISSIONS
newFlags = newFlags andInv PermissionFlags.IMPLICIT
var shouldRetainAsNearbyDevices = false
if (permissionName in NEARBY_DEVICES_PERMISSIONS) {
@@ -994,12 +1003,7 @@
permissionName: String
): Boolean? {
val permissionAllowlist = newState.systemState.permissionAllowlist
- // TODO(b/261913353): STOPSHIP: Add AndroidPackage.apexModuleName.
- // val apexModuleName = androidPackage.apexModuleName
- val apexModuleName = permissionAllowlist.apexPrivilegedAppAllowlists
- .firstNotNullOfOrNullIndexed { _, apexModuleName, apexAllowlist ->
- if (packageState.packageName in apexAllowlist) apexModuleName else null
- }
+ val apexModuleName = packageState.apexModuleName
val packageName = packageState.packageName
return when {
packageState.isVendor -> permissionAllowlist.getVendorPrivilegedAppAllowlistState(
@@ -1066,7 +1070,7 @@
state: AccessState = newState,
action: (PackageState) -> Unit
) {
- val packageNames = state.systemState.appIds[appId]
+ val packageNames = state.systemState.appIds[appId]!!
packageNames.forEachIndexed { _, packageName ->
val packageState = state.systemState.packageStates[packageName]!!
if (packageState.androidPackage != null) {
@@ -1190,9 +1194,7 @@
// Special permission for the recents app.
return true
}
- // TODO(b/261913353): STOPSHIP: Add AndroidPackage.apexModuleName.
- // This should be androidPackage.apexModuleName instead
- if (permission.isModule && androidPackage.packageName != null) {
+ if (permission.isModule && packageState.apexModuleName != null) {
// Special permission granted for APKs inside APEX modules.
return true
}
@@ -1397,11 +1399,11 @@
Manifest.permission.READ_MEDIA_VIDEO,
)
- // TODO: also add the permission NEARBY_WIFI_DEVICES to this set
private val NEARBY_DEVICES_PERMISSIONS = indexedSetOf(
Manifest.permission.BLUETOOTH_ADVERTISE,
Manifest.permission.BLUETOOTH_CONNECT,
- Manifest.permission.BLUETOOTH_SCAN
+ Manifest.permission.BLUETOOTH_SCAN,
+ Manifest.permission.NEARBY_WIFI_DEVICES
)
private val NOTIFICATIONS_PERMISSIONS = indexedSetOf(
diff --git a/services/proguard.flags b/services/proguard.flags
index ba4560f..c133044 100644
--- a/services/proguard.flags
+++ b/services/proguard.flags
@@ -103,7 +103,7 @@
-keep,allowoptimization,allowaccessmodification class com.android.server.storage.AppFuseBridge { *; }
-keep,allowoptimization,allowaccessmodification class com.android.server.tv.TvInputHal { *; }
-keep,allowoptimization,allowaccessmodification class com.android.server.usb.UsbAlsaJackDetector { *; }
--keep,allowoptimization,allowaccessmodification class com.android.server.usb.UsbMidiDevice { *; }
+-keep,allowoptimization,allowaccessmodification class com.android.server.usb.UsbAlsaMidiDevice { *; }
-keep,allowoptimization,allowaccessmodification class com.android.server.vibrator.VibratorController$OnVibrationCompleteListener { *; }
-keep,allowoptimization,allowaccessmodification class com.android.server.vibrator.VibratorManagerService$OnSyncedVibrationCompleteListener { *; }
-keepclasseswithmembers,allowoptimization,allowaccessmodification class com.android.server.** {
diff --git a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
index 1abcf38..bfbc0f5 100644
--- a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
@@ -267,7 +267,7 @@
LocalServices.removeServiceForTest(PackageManagerInternal.class);
LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternal);
mBackupEligibilityRules = new BackupEligibilityRules(mPackageManager,
- LocalServices.getService(PackageManagerInternal.class), USER_ID,
+ LocalServices.getService(PackageManagerInternal.class), USER_ID, mContext,
BACKUP_DESTINATION);
}
diff --git a/services/searchui/java/com/android/server/searchui/SearchUiManagerService.java b/services/searchui/java/com/android/server/searchui/SearchUiManagerService.java
index 524002a..29ad537 100644
--- a/services/searchui/java/com/android/server/searchui/SearchUiManagerService.java
+++ b/services/searchui/java/com/android/server/searchui/SearchUiManagerService.java
@@ -125,6 +125,26 @@
(service) -> service.queryLocked(sessionId, query, callback));
}
+ public void registerEmptyQueryResultUpdateCallback(@NonNull SearchSessionId sessionId,
+ @NonNull ISearchCallback callback) {
+ runForUserLocked("registerEmptyQueryResultUpdateCallback", sessionId,
+ (service) -> service.registerEmptyQueryResultUpdateCallbackLocked(sessionId,
+ callback));
+ }
+
+ public void unregisterEmptyQueryResultUpdateCallback(@NonNull SearchSessionId sessionId,
+ @NonNull ISearchCallback callback) {
+ runForUserLocked("unregisterEmptyQueryResultUpdateCallback", sessionId,
+ (service) -> service.unregisterEmptyQueryResultUpdateCallbackLocked(sessionId,
+ callback));
+ }
+
+ @Override
+ public void requestEmptyQueryResultUpdate(@NonNull SearchSessionId sessionId) {
+ runForUserLocked("requestEmptyQueryResultUpdate", sessionId,
+ (service) -> service.requestEmptyQueryResultUpdateLocked(sessionId));
+ }
+
@Override
public void destroySearchSession(@NonNull SearchSessionId sessionId) {
runForUserLocked("destroySearchSession", sessionId,
diff --git a/services/searchui/java/com/android/server/searchui/SearchUiPerUserService.java b/services/searchui/java/com/android/server/searchui/SearchUiPerUserService.java
index 4c31978..0d70fff 100644
--- a/services/searchui/java/com/android/server/searchui/SearchUiPerUserService.java
+++ b/services/searchui/java/com/android/server/searchui/SearchUiPerUserService.java
@@ -149,6 +149,48 @@
}
/**
+ * Registers a callback for continuous updates of search targets for empty query result used for
+ * zero state.
+ */
+ @GuardedBy("mLock")
+ public void registerEmptyQueryResultUpdateCallbackLocked(@NonNull SearchSessionId sessionId,
+ @NonNull ISearchCallback callback) {
+ final SearchSessionInfo sessionInfo = mSessionInfos.get(sessionId);
+ if (sessionInfo == null) return;
+ final boolean serviceExists = resolveService(sessionId,
+ s -> s.onRegisterEmptyQueryResultUpdateCallback(sessionId, callback));
+ if (serviceExists) {
+ sessionInfo.addCallbackLocked(callback);
+ }
+ }
+
+ /**
+ * Unregisters a callback for continuous updates of search targets for empty query result
+ * used for zero state.
+ */
+ @GuardedBy("mLock")
+ public void unregisterEmptyQueryResultUpdateCallbackLocked(@NonNull SearchSessionId sessionId,
+ @NonNull ISearchCallback callback) {
+ final SearchSessionInfo sessionInfo = mSessionInfos.get(sessionId);
+ if (sessionInfo == null) return;
+ final boolean serviceExists = resolveService(sessionId,
+ s -> s.onUnregisterEmptyQueryResultUpdateCallback(sessionId, callback));
+ if (serviceExists) {
+ sessionInfo.removeCallbackLocked(callback);
+ }
+ }
+
+ /**
+ * Requests a new set of search targets for empty query result used for zero state.
+ */
+ @GuardedBy("mLock")
+ public void requestEmptyQueryResultUpdateLocked(@NonNull SearchSessionId sessionId) {
+ final SearchSessionInfo sessionInfo = mSessionInfos.get(sessionId);
+ if (sessionInfo == null) return;
+ resolveService(sessionId, s->s.onRequestEmptyQueryResultUpdate(sessionId));
+ }
+
+ /**
* Notifies the service of the end of an existing search session.
*/
@GuardedBy("mLock")
@@ -310,18 +352,7 @@
final IBinder.DeathRecipient mDeathRecipient;
private final RemoteCallbackList<ISearchCallback> mCallbacks =
- new RemoteCallbackList<ISearchCallback>() {
- @Override
- public void onCallbackDied(ISearchCallback callback) {
- if (DEBUG) {
- Slog.d(TAG, "Binder died for session Id=" + mSessionId
- + " and callback=" + callback.asBinder());
- }
- if (mCallbacks.getRegisteredCallbackCount() == 0) {
- destroy();
- }
- }
- };
+ new RemoteCallbackList<>();
SearchSessionInfo(
@NonNull final SearchSessionId id,
@@ -337,6 +368,22 @@
mDeathRecipient = deathRecipient;
}
+ void addCallbackLocked(ISearchCallback callback) {
+ if (DEBUG) {
+ Slog.d(TAG, "Storing callback for session Id=" + mSessionId
+ + " and callback=" + callback.asBinder());
+ }
+ mCallbacks.register(callback);
+ }
+
+ void removeCallbackLocked(ISearchCallback callback) {
+ if (DEBUG) {
+ Slog.d(TAG, "Removing callback for session Id=" + mSessionId
+ + " and callback=" + callback.asBinder());
+ }
+ mCallbacks.unregister(callback);
+ }
+
boolean linkToDeath() {
try {
mToken.linkToDeath(mDeathRecipient, 0);
@@ -369,6 +416,9 @@
+ callbackCount + " callbacks.");
}
service.onCreateSearchSessionLocked(mSearchContext, mSessionId, token);
+ mCallbacks.broadcast(
+ callback -> service.registerEmptyQueryResultUpdateCallbackLocked(mSessionId,
+ callback));
}
}
}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
index 720f486..7e44049 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -34,6 +34,7 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
+import android.os.Binder;
import android.os.RemoteException;
import android.view.inputmethod.InputMethodManager;
@@ -66,8 +67,8 @@
@Test
public void testPerformShowIme() throws Exception {
synchronized (ImfLock.class) {
- mVisibilityApplier.performShowIme(mWindowToken, null /* statsToken */,
- InputMethodManager.SHOW_IMPLICIT, null, SHOW_SOFT_INPUT);
+ mVisibilityApplier.performShowIme(new Binder() /* showInputToken */,
+ null /* statsToken */, InputMethodManager.SHOW_IMPLICIT, null, SHOW_SOFT_INPUT);
}
verifyShowSoftInput(false, true, InputMethodManager.SHOW_IMPLICIT);
}
@@ -75,8 +76,8 @@
@Test
public void testPerformHideIme() throws Exception {
synchronized (ImfLock.class) {
- mVisibilityApplier.performHideIme(mWindowToken, null /* statsToken */, null,
- HIDE_SOFT_INPUT);
+ mVisibilityApplier.performHideIme(new Binder() /* hideInputToken */,
+ null /* statsToken */, null, HIDE_SOFT_INPUT);
}
verifyHideSoftInput(false, true);
}
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
index 75484d1..a39e021 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
@@ -1129,6 +1129,7 @@
false /*allowInstall*/,
false /*instantApp*/,
false /*virtualPreload*/,
+ false /* stopped */,
UserManagerService.getInstance(),
null /*usesSdkLibraries*/,
null /*usesSdkLibrariesVersions*/,
@@ -1173,6 +1174,7 @@
true /*allowInstall*/,
false /*instantApp*/,
false /*virtualPreload*/,
+ false /* stopped */,
UserManagerService.getInstance(),
null /*usesSdkLibraries*/,
null /*usesSdkLibrariesVersions*/,
@@ -1217,6 +1219,7 @@
false /*allowInstall*/,
false /*instantApp*/,
false /*virtualPreload*/,
+ false /* stopped */,
UserManagerService.getInstance(),
null /*usesSdkLibraries*/,
null /*usesSdkLibrariesVersions*/,
@@ -1262,6 +1265,7 @@
false /*allowInstall*/,
false /*instantApp*/,
false /*virtualPreload*/,
+ false /* stopped */,
UserManagerService.getInstance(),
null /*usesSdkLibraries*/,
null /*usesSdkLibrariesVersions*/,
@@ -1284,6 +1288,48 @@
verifyUserState(userState, false /*notLaunched*/, false /*stopped*/, true /*installed*/);
}
+ /** Create a new stopped system PackageSetting */
+ @Test
+ public void testCreateNewSetting05() {
+ final PackageSetting testPkgSetting01 = Settings.createNewSetting(
+ PACKAGE_NAME,
+ null /*originalPkg*/,
+ null /*disabledPkg*/,
+ null /*realPkgName*/,
+ null /*sharedUser*/,
+ UPDATED_CODE_PATH /*codePath*/,
+ null /*legacyNativeLibraryPath*/,
+ "arm64-v8a" /*primaryCpuAbi*/,
+ "armeabi" /*secondaryCpuAbi*/,
+ UPDATED_VERSION_CODE /*versionCode*/,
+ ApplicationInfo.FLAG_SYSTEM /*pkgFlags*/,
+ 0 /*pkgPrivateFlags*/,
+ UserHandle.SYSTEM /*installUser*/,
+ false /*allowInstall*/,
+ false /*instantApp*/,
+ false /*virtualPreload*/,
+ true /* stopped */,
+ UserManagerService.getInstance(),
+ null /*usesSdkLibraries*/,
+ null /*usesSdkLibrariesVersions*/,
+ null /*usesStaticLibraries*/,
+ null /*usesStaticLibrariesVersions*/,
+ null /*mimeGroups*/,
+ UUID.randomUUID());
+ assertThat(testPkgSetting01.getAppId(), is(0));
+ assertThat(testPkgSetting01.getPath(), is(UPDATED_CODE_PATH));
+ assertThat(testPkgSetting01.getPackageName(), is(PACKAGE_NAME));
+ assertThat(testPkgSetting01.getFlags(), is(ApplicationInfo.FLAG_SYSTEM));
+ assertThat(testPkgSetting01.getPrivateFlags(), is(0));
+ assertThat(testPkgSetting01.getPrimaryCpuAbi(), is("arm64-v8a"));
+ assertThat(testPkgSetting01.getPrimaryCpuAbiLegacy(), is("arm64-v8a"));
+ assertThat(testPkgSetting01.getSecondaryCpuAbi(), is("armeabi"));
+ assertThat(testPkgSetting01.getSecondaryCpuAbiLegacy(), is("armeabi"));
+ assertThat(testPkgSetting01.getVersionCode(), is(UPDATED_VERSION_CODE));
+ final PackageUserState userState = testPkgSetting01.readUserState(0);
+ verifyUserState(userState, false /*notLaunched*/, true /*stopped*/, true /*installed*/);
+ }
+
@Test
public void testSetPkgStateLibraryFiles_addNewFiles() {
final PackageSetting packageSetting = createPackageSetting("com.foo");
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index ef470fe..3b20735 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -112,7 +112,6 @@
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
import android.Manifest;
import android.app.ActivityManager;
@@ -1265,7 +1264,30 @@
mService.interactiveStateChangedLocked(false);
mService.interactiveStateChangedLocked(true);
runnableCaptor.getValue().run();
- verify(mMockContext).sendBroadcastAsUser(mService.mTimeTickIntent, UserHandle.ALL);
+ final ArgumentCaptor<Bundle> optionsCaptor = ArgumentCaptor.forClass(Bundle.class);
+ verify(mMockContext).sendBroadcastAsUser(eq(mService.mTimeTickIntent), eq(UserHandle.ALL),
+ isNull(), optionsCaptor.capture());
+ verifyTimeTickBroadcastOptions(optionsCaptor.getValue());
+ }
+
+ @Test
+ public void sendsTimeTickOnAlarmTrigger() throws Exception {
+ final ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+ // Stubbing so the handler doesn't actually run the runnable.
+ doReturn(true).when(mService.mHandler).post(runnableCaptor.capture());
+ mService.mTimeTickTrigger.doAlarm(mock(IAlarmCompleteListener.class));
+ runnableCaptor.getValue().run();
+ final ArgumentCaptor<Bundle> optionsCaptor = ArgumentCaptor.forClass(Bundle.class);
+ verify(mMockContext).sendBroadcastAsUser(eq(mService.mTimeTickIntent), eq(UserHandle.ALL),
+ isNull(), optionsCaptor.capture());
+ verifyTimeTickBroadcastOptions(optionsCaptor.getValue());
+ }
+
+ private void verifyTimeTickBroadcastOptions(Bundle actualOptionsBundle) {
+ final BroadcastOptions actualOptions = new BroadcastOptions(actualOptionsBundle);
+ assertEquals(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT,
+ actualOptions.getDeliveryGroupPolicy());
+ assertTrue(actualOptions.isDeferUntilActive());
}
@Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index 41ae50b..5e4ba88 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -1401,10 +1401,10 @@
// Confirm that we saw no registered receiver traffic
final IApplicationThread oldThread = oldApp.getThread();
verify(oldThread, never()).scheduleRegisteredReceiver(any(), any(), anyInt(), any(), any(),
- anyBoolean(), anyBoolean(), anyBoolean(), anyInt(), anyInt());
+ anyBoolean(), anyBoolean(), anyBoolean(), anyInt(), anyInt(), anyInt(), any());
final IApplicationThread newThread = newApp.getThread();
verify(newThread, never()).scheduleRegisteredReceiver(any(), any(), anyInt(), any(), any(),
- anyBoolean(), anyBoolean(), anyBoolean(), anyInt(), anyInt());
+ anyBoolean(), anyBoolean(), anyBoolean(), anyInt(), anyInt(), anyInt(), any());
// Confirm that we saw final manifest broadcast
verifyScheduleReceiver(times(1), newApp, airplane,
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index c40017a..fb05699 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -17,7 +17,7 @@
package com.android.server.am;
import static android.app.ActivityManager.PROCESS_CAPABILITY_ALL;
-import static android.app.ActivityManager.PROCESS_CAPABILITY_NETWORK;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_BFSL;
import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
import static android.app.ActivityManager.PROCESS_STATE_BOUND_TOP;
import static android.app.ActivityManager.PROCESS_STATE_CACHED_ACTIVITY;
@@ -38,6 +38,7 @@
import static android.app.ActivityManager.PROCESS_STATE_TOP;
import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
import static android.app.ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
@@ -110,6 +111,7 @@
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -225,6 +227,40 @@
}
}
+ private static void assertBfsl(ProcessRecord app) {
+ assertEquals(PROCESS_CAPABILITY_BFSL,
+ app.mState.getSetCapability() & PROCESS_CAPABILITY_BFSL);
+ }
+
+ private static void assertNoBfsl(ProcessRecord app) {
+ assertEquals(0, app.mState.getSetCapability() & PROCESS_CAPABILITY_BFSL);
+ }
+
+ /**
+ * Replace the process LRU with the given processes.
+ * @param apps
+ */
+ private void setProcessesToLru(ProcessRecord... apps) {
+ ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
+ lru.clear();
+ Collections.addAll(lru, apps);
+ }
+
+ /**
+ * Run updateOomAdjLocked().
+ * - If there's only one process, then it calls updateOomAdjLocked(ProcessRecord, int).
+ * - Otherwise, sets the processes to the LRU and run updateOomAdjLocked(int).
+ */
+ private void updateOomAdj(ProcessRecord... apps) {
+ if (apps.length == 1) {
+ sService.mOomAdjuster.updateOomAdjLocked(apps[0], OomAdjuster.OOM_ADJ_REASON_NONE);
+ } else {
+ setProcessesToLru(apps);
+ sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
+ sService.mProcessList.getLruProcessesLOSP().clear();
+ }
+ }
+
@SuppressWarnings("GuardedBy")
@Test
public void testUpdateOomAdj_DoOne_Persistent_TopUi_Sleeping() {
@@ -233,11 +269,12 @@
app.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
app.mState.setHasTopUi(true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_ASLEEP);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERSISTENT_PROC_ADJ,
SCHED_GROUP_RESTRICTED);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -248,7 +285,7 @@
app.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
app.mState.setHasTopUi(true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertProcStates(app, PROCESS_STATE_PERSISTENT_UI, PERSISTENT_PROC_ADJ,
SCHED_GROUP_TOP_APP);
@@ -262,7 +299,7 @@
app.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
doReturn(app).when(sService).getTopApp();
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
doReturn(null).when(sService).getTopApp();
assertProcStates(app, PROCESS_STATE_PERSISTENT_UI, PERSISTENT_PROC_ADJ,
@@ -277,7 +314,7 @@
doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
doReturn(app).when(sService).getTopApp();
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
doReturn(null).when(sService).getTopApp();
assertProcStates(app, PROCESS_STATE_TOP, FOREGROUND_APP_ADJ, SCHED_GROUP_TOP_APP);
@@ -291,7 +328,7 @@
doReturn(PROCESS_STATE_TOP_SLEEPING).when(sService.mAtmInternal).getTopProcessState();
app.mState.setRunningRemoteAnimation(true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
assertProcStates(app, PROCESS_STATE_TOP_SLEEPING, VISIBLE_APP_ADJ, SCHED_GROUP_TOP_APP);
@@ -304,11 +341,12 @@
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
doReturn(mock(ActiveInstrumentation.class)).when(app).getActiveInstrumentation();
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
doCallRealMethod().when(app).getActiveInstrumentation();
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, FOREGROUND_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -319,7 +357,7 @@
doReturn(true).when(sService).isReceivingBroadcastLocked(any(ProcessRecord.class),
any(int[].class));
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
doReturn(false).when(sService).isReceivingBroadcastLocked(any(ProcessRecord.class),
any(int[].class));
@@ -333,7 +371,7 @@
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
app.mServices.startExecutingService(mock(ServiceRecord.class));
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertProcStates(app, PROCESS_STATE_SERVICE, FOREGROUND_APP_ADJ, SCHED_GROUP_BACKGROUND);
}
@@ -346,7 +384,7 @@
doReturn(PROCESS_STATE_TOP_SLEEPING).when(sService.mAtmInternal).getTopProcessState();
doReturn(app).when(sService).getTopApp();
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_ASLEEP);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
doReturn(null).when(sService).getTopApp();
doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
@@ -363,7 +401,7 @@
app.mState.setCurRawAdj(CACHED_APP_MIN_ADJ);
doReturn(null).when(sService).getTopApp();
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertProcStates(app, PROCESS_STATE_CACHED_EMPTY, CACHED_APP_MIN_ADJ,
SCHED_GROUP_BACKGROUND);
@@ -389,7 +427,7 @@
})).when(wpc).computeOomAdjFromActivities(
any(WindowProcessController.ComputeOomAdjCallback.class));
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertProcStates(app, PROCESS_STATE_TOP, VISIBLE_APP_ADJ, SCHED_GROUP_TOP_APP);
}
@@ -403,7 +441,7 @@
doReturn(true).when(wpc).hasRecentTasks();
app.mState.setLastTopTime(SystemClock.uptimeMillis());
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
doCallRealMethod().when(wpc).hasRecentTasks();
assertEquals(PROCESS_STATE_CACHED_RECENT, app.mState.getSetProcState());
@@ -417,10 +455,11 @@
app.mServices.setHasForegroundServices(true, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION,
/* hasNoneType=*/false);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -430,10 +469,11 @@
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
app.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -445,7 +485,7 @@
ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(sService);
s.startRequested = true;
s.isForeground = true;
- s.foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
+ s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
s.setShortFgsInfo(SystemClock.uptimeMillis());
// SHORT_SERVICE FGS will get IMP_FG and a slightly different recent-adjustment.
@@ -454,16 +494,15 @@
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
app.mServices.startService(s);
app.mServices.setHasForegroundServices(true,
- ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, /* hasNoneType=*/false);
+ FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, /* hasNoneType=*/false);
app.mState.setLastTopTime(SystemClock.uptimeMillis());
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
- assertProcStates(app, PROCESS_STATE_IMPORTANT_FOREGROUND,
+ assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE,
PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ + 1, SCHED_GROUP_DEFAULT);
- // Should get network access.
- assertTrue((app.mState.getSetCapability() & PROCESS_CAPABILITY_NETWORK) != 0);
+ assertNoBfsl(app);
}
// SHORT_SERVICE, but no longer recent.
@@ -471,25 +510,24 @@
ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
app.mServices.setHasForegroundServices(true,
- ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, /* hasNoneType=*/false);
+ FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, /* hasNoneType=*/false);
app.mServices.startService(s);
app.mState.setLastTopTime(SystemClock.uptimeMillis()
- sService.mConstants.TOP_TO_FGS_GRACE_DURATION);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
- assertProcStates(app, PROCESS_STATE_IMPORTANT_FOREGROUND,
+ assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE,
PERCEPTIBLE_MEDIUM_APP_ADJ + 1, SCHED_GROUP_DEFAULT);
- // Still should get network access.
- assertTrue((app.mState.getSetCapability() & PROCESS_CAPABILITY_NETWORK) != 0);
+ assertNoBfsl(app);
}
// SHORT_SERVICE, timed out already.
s = ServiceRecord.newEmptyInstanceForTest(sService);
s.startRequested = true;
s.isForeground = true;
- s.foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
+ s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
s.setShortFgsInfo(SystemClock.uptimeMillis()
- sService.mConstants.mShortFgsTimeoutDuration
- sService.mConstants.mShortFgsProcStateExtraWaitDuration);
@@ -497,19 +535,17 @@
ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
app.mServices.setHasForegroundServices(true,
- ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, /* hasNoneType=*/false);
+ FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, /* hasNoneType=*/false);
app.mServices.startService(s);
app.mState.setLastTopTime(SystemClock.uptimeMillis()
- sService.mConstants.TOP_TO_FGS_GRACE_DURATION);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
// Procstate should be lower than FGS. (It should be SERVICE)
assertEquals(app.mState.getSetProcState(), PROCESS_STATE_SERVICE);
-
- // Shouldn't have the network capability now.
- assertTrue((app.mState.getSetCapability() & PROCESS_CAPABILITY_NETWORK) == 0);
+ assertNoBfsl(app);
}
}
@@ -520,7 +556,7 @@
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
app.mState.setHasOverlayUi(true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertProcStates(app, PROCESS_STATE_IMPORTANT_FOREGROUND, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
@@ -534,10 +570,11 @@
app.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
app.mState.setLastTopTime(SystemClock.uptimeMillis());
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE,
PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ, SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -558,7 +595,7 @@
s.getConnections().clear();
app.mServices.updateHasTopStartedAlmostPerceptibleServices();
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertEquals(PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ + 2, app.mState.getSetAdj());
}
@@ -578,7 +615,7 @@
nowUptime - 2 * sService.mConstants.mServiceBindAlmostPerceptibleTimeoutMs;
app.mServices.updateHasTopStartedAlmostPerceptibleServices();
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertEquals(PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ + 2, app.mState.getSetAdj());
}
@@ -599,11 +636,31 @@
s.getConnections().clear();
app.mServices.updateHasTopStartedAlmostPerceptibleServices();
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertNotEquals(PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ + 2, app.mState.getSetAdj());
}
}
+
+ @SuppressWarnings("GuardedBy")
+ @Test
+ public void testUpdateOomAdj_DoOne_ImpFg_AlmostPerceptibleService() {
+ ProcessRecord system = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+ MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
+ ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID,
+ MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, true));
+ system.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
+ system.mState.setHasTopUi(true);
+ // Simulate the system starting and binding to a service in the app.
+ ServiceRecord s = bindService(app, system,
+ null, Context.BIND_ALMOST_PERCEPTIBLE, mock(IBinder.class));
+ sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
+ sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+
+ assertProcStates(app, PROCESS_STATE_IMPORTANT_FOREGROUND,
+ PERCEPTIBLE_APP_ADJ + 1, SCHED_GROUP_DEFAULT);
+ }
+
@SuppressWarnings("GuardedBy")
@Test
public void testUpdateOomAdj_DoOne_Toast() {
@@ -611,7 +668,7 @@
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
app.mState.setForcingToImportant(new Object());
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertProcStates(app, PROCESS_STATE_TRANSIENT_BACKGROUND, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
@@ -625,7 +682,7 @@
WindowProcessController wpc = app.getWindowProcessController();
doReturn(true).when(wpc).isHeavyWeightProcess();
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
doReturn(false).when(wpc).isHeavyWeightProcess();
assertProcStates(app, PROCESS_STATE_HEAVY_WEIGHT, HEAVY_WEIGHT_APP_ADJ,
@@ -640,7 +697,7 @@
WindowProcessController wpc = app.getWindowProcessController();
doReturn(true).when(wpc).isHomeProcess();
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertProcStates(app, PROCESS_STATE_HOME, HOME_APP_ADJ, SCHED_GROUP_BACKGROUND);
}
@@ -654,7 +711,7 @@
doReturn(true).when(wpc).isPreviousProcess();
doReturn(true).when(wpc).hasActivities();
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertProcStates(app, PROCESS_STATE_LAST_ACTIVITY, PREVIOUS_APP_ADJ,
SCHED_GROUP_BACKGROUND);
@@ -669,7 +726,7 @@
backupTarget.app = app;
doReturn(backupTarget).when(sService.mBackupTargets).get(anyInt());
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
doReturn(null).when(sService.mBackupTargets).get(anyInt());
assertProcStates(app, PROCESS_STATE_TRANSIENT_BACKGROUND, BACKUP_APP_ADJ,
@@ -683,7 +740,7 @@
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
app.mServices.setHasClientActivities(true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertEquals(PROCESS_STATE_CACHED_ACTIVITY_CLIENT, app.mState.getSetProcState());
}
@@ -695,7 +752,7 @@
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
app.mServices.setTreatLikeActivity(true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertEquals(PROCESS_STATE_CACHED_ACTIVITY, app.mState.getSetProcState());
}
@@ -712,7 +769,7 @@
s.lastActivity = SystemClock.uptimeMillis();
app.mServices.startService(s);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertProcStates(app, PROCESS_STATE_SERVICE, SERVICE_B_ADJ, SCHED_GROUP_BACKGROUND);
}
@@ -724,7 +781,7 @@
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
app.mState.setMaxAdj(PERCEPTIBLE_LOW_APP_ADJ);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertProcStates(app, PROCESS_STATE_CACHED_EMPTY, PERCEPTIBLE_LOW_APP_ADJ,
SCHED_GROUP_DEFAULT);
@@ -739,7 +796,7 @@
app.mState.setCurRawAdj(SERVICE_ADJ);
doReturn(null).when(sService).getTopApp();
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertTrue(ProcessList.CACHED_APP_MIN_ADJ <= app.mState.getSetAdj());
assertTrue(ProcessList.CACHED_APP_MAX_ADJ >= app.mState.getSetAdj());
@@ -756,7 +813,7 @@
s.lastActivity = SystemClock.uptimeMillis();
app.mServices.startService(s);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertProcStates(app, PROCESS_STATE_SERVICE, SERVICE_ADJ, SCHED_GROUP_BACKGROUND);
}
@@ -774,7 +831,7 @@
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
doReturn(client).when(sService).getTopApp();
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
doReturn(null).when(sService).getTopApp();
assertProcStates(app, PROCESS_STATE_SERVICE, FIRST_CACHED_ADJ, SCHED_GROUP_BACKGROUND);
@@ -790,7 +847,7 @@
bindService(app, client, null, Context.BIND_WAIVE_PRIORITY
| Context.BIND_TREAT_LIKE_ACTIVITY, mock(IBinder.class));
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
assertEquals(PROCESS_STATE_CACHED_ACTIVITY, app.mState.getSetProcState());
}
@@ -810,7 +867,7 @@
mock(ActivityServiceConnectionsHolder.class));
doReturn(true).when(cr.activity).isActivityVisible();
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
assertEquals(FOREGROUND_APP_ADJ, app.mState.getSetAdj());
assertEquals(SCHED_GROUP_TOP_APP_BOUND, app.mState.getSetSchedGroup());
@@ -822,11 +879,8 @@
ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
bindService(app, app, null, 0, mock(IBinder.class));
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertProcStates(app, PROCESS_STATE_CACHED_EMPTY, FIRST_CACHED_ADJ, SCHED_GROUP_BACKGROUND);
}
@@ -841,7 +895,7 @@
client.mServices.setTreatLikeActivity(true);
bindService(app, client, null, 0, mock(IBinder.class));
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
assertEquals(PROCESS_STATE_CACHED_EMPTY, app.mState.getSetProcState());
}
@@ -861,7 +915,7 @@
doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
doReturn(client).when(sService).getTopApp();
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
doReturn(null).when(sService).getTopApp();
assertEquals(PREVIOUS_APP_ADJ, app.mState.getSetAdj());
@@ -878,10 +932,11 @@
client.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
client.mState.setHasTopUi(true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, VISIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -894,9 +949,10 @@
bindService(app, client, null, Context.BIND_IMPORTANT, mock(IBinder.class));
client.mServices.startExecutingService(mock(ServiceRecord.class));
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
assertEquals(FOREGROUND_APP_ADJ, app.mState.getSetAdj());
+ assertNoBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -910,7 +966,7 @@
doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
doReturn(client).when(sService).getTopApp();
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
doReturn(null).when(sService).getTopApp();
assertProcStates(app, PROCESS_STATE_BOUND_TOP, VISIBLE_APP_ADJ, SCHED_GROUP_DEFAULT);
@@ -926,9 +982,12 @@
bindService(app, client, null, Context.BIND_FOREGROUND_SERVICE, mock(IBinder.class));
client.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
assertEquals(PROCESS_STATE_BOUND_FOREGROUND_SERVICE, app.mState.getSetProcState());
+ assertEquals(PROCESS_STATE_PERSISTENT, client.mState.getSetProcState());
+ assertBfsl(client);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -941,9 +1000,10 @@
bindService(app, client, null, Context.BIND_NOT_FOREGROUND, mock(IBinder.class));
client.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertEquals(PROCESS_STATE_TRANSIENT_BACKGROUND, app.mState.getSetProcState());
+ assertNoBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -956,9 +1016,94 @@
bindService(app, client, null, 0, mock(IBinder.class));
client.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
+ assertEquals(PROCESS_STATE_FOREGROUND_SERVICE, client.mState.getSetProcState());
+ assertBfsl(client);
assertEquals(PROCESS_STATE_FOREGROUND_SERVICE, app.mState.getSetProcState());
+ assertBfsl(app);
+ }
+
+ @SuppressWarnings("GuardedBy")
+ @Test
+ public void testUpdateOomAdj_DoOne_Service_ImportantFgService_ShortFgs() {
+ // Client has a SHORT_SERVICE FGS, which isn't allowed BFSL.
+ ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+ MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
+ ProcessRecord client = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID,
+ MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
+ bindService(app, client, null, 0, mock(IBinder.class));
+
+ // In order to trick OomAdjuster to think it has a short-service, we need this logic.
+ ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(sService);
+ s.startRequested = true;
+ s.isForeground = true;
+ s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
+ s.setShortFgsInfo(SystemClock.uptimeMillis());
+ client.mServices.startService(s);
+ client.mState.setLastTopTime(SystemClock.uptimeMillis());
+
+ client.mServices.setHasForegroundServices(true, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
+ /* hasNoneType=*/false);
+ sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
+ updateOomAdj(client, app);
+
+ // Client only has a SHORT_FGS, so it doesn't have BFSL, and that's propagated.
+ assertEquals(PROCESS_STATE_FOREGROUND_SERVICE, client.mState.getSetProcState());
+ assertNoBfsl(client);
+ assertEquals(PROCESS_STATE_FOREGROUND_SERVICE, app.mState.getSetProcState());
+ assertNoBfsl(app);
+ }
+
+ @SuppressWarnings("GuardedBy")
+ @Test
+ public void testUpdateOomAdj_DoOne_Service_BoundForegroundService_with_ShortFgs() {
+
+ // app2, which is bound by app1 (which makes it BFGS)
+ // but it also has a short-fgs.
+ ProcessRecord app2 = spy(makeDefaultProcessRecord(MOCKAPP3_PID, MOCKAPP3_UID,
+ MOCKAPP3_PROCESSNAME, MOCKAPP3_PACKAGENAME, false));
+
+ // In order to trick OomAdjuster to think it has a short-service, we need this logic.
+ ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(sService);
+ s.startRequested = true;
+ s.isForeground = true;
+ s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
+ s.setShortFgsInfo(SystemClock.uptimeMillis());
+ app2.mServices.startService(s);
+ app2.mState.setLastTopTime(SystemClock.uptimeMillis());
+
+ app2.mServices.setHasForegroundServices(true, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
+ /* hasNoneType=*/false);
+ sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
+ updateOomAdj(app2);
+
+ // Client only has a SHORT_FGS, so it doesn't have BFSL, and that's propagated.
+ assertEquals(PROCESS_STATE_FOREGROUND_SERVICE, app2.mState.getSetProcState());
+ assertNoBfsl(app2);
+
+ // Now, create a BFGS process (app1), and make it bind to app 2
+
+ // Persistent process
+ ProcessRecord pers = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+ MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
+ pers.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
+
+ // app1, which is bound by pers (which makes it BFGS)
+ ProcessRecord app1 = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID,
+ MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
+
+ bindService(app1, pers, null, Context.BIND_FOREGROUND_SERVICE, mock(IBinder.class));
+ bindService(app2, app1, null, 0, mock(IBinder.class));
+
+ updateOomAdj(pers, app1, app2);
+
+ assertEquals(PROCESS_STATE_BOUND_FOREGROUND_SERVICE, app1.mState.getSetProcState());
+ assertBfsl(app1);
+
+ // Now, app2 gets BFSL from app1.
+ assertEquals(PROCESS_STATE_FOREGROUND_SERVICE, app2.mState.getSetProcState());
+ assertBfsl(app2);
}
@SuppressWarnings("GuardedBy")
@@ -973,15 +1118,18 @@
backupTarget.app = client;
doReturn(backupTarget).when(sService.mBackupTargets).get(anyInt());
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
+
doReturn(null).when(sService.mBackupTargets).get(anyInt());
assertEquals(BACKUP_APP_ADJ, app.mState.getSetAdj());
+ assertNoBfsl(app);
client.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertEquals(PERSISTENT_SERVICE_ADJ, app.mState.getSetAdj());
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -994,7 +1142,7 @@
bindService(app, client, null, Context.BIND_NOT_PERCEPTIBLE, mock(IBinder.class));
client.mState.setRunningRemoteAnimation(true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertEquals(PERCEPTIBLE_LOW_APP_ADJ, app.mState.getSetAdj());
}
@@ -1009,7 +1157,7 @@
bindService(app, client, null, Context.BIND_NOT_VISIBLE, mock(IBinder.class));
client.mState.setRunningRemoteAnimation(true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
assertEquals(PERCEPTIBLE_APP_ADJ, app.mState.getSetAdj());
}
@@ -1024,23 +1172,25 @@
bindService(app, client, null, 0, mock(IBinder.class));
client.mState.setHasOverlayUi(true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
assertEquals(PERCEPTIBLE_APP_ADJ, app.mState.getSetAdj());
}
@SuppressWarnings("GuardedBy")
@Test
- public void testUpdateOomAdj_DoOne_Service_MediumPerceptible() {
+ public void testUpdateOomAdj_DoOne_Service_AlmostPerceptible() {
{
ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
ProcessRecord client = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID,
MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
- bindService(app, client, null, Context.BIND_ALMOST_PERCEPTIBLE, mock(IBinder.class));
+ bindService(app, client, null,
+ Context.BIND_ALMOST_PERCEPTIBLE | Context.BIND_NOT_FOREGROUND,
+ mock(IBinder.class));
client.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
assertEquals(PERCEPTIBLE_MEDIUM_APP_ADJ + 2, app.mState.getSetAdj());
}
@@ -1052,13 +1202,48 @@
MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
WindowProcessController wpc = client.getWindowProcessController();
doReturn(true).when(wpc).isHeavyWeightProcess();
- bindService(app, client, null, Context.BIND_ALMOST_PERCEPTIBLE, mock(IBinder.class));
+ bindService(app, client, null,
+ Context.BIND_ALMOST_PERCEPTIBLE | Context.BIND_NOT_FOREGROUND,
+ mock(IBinder.class));
+ client.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
+ sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
+ updateOomAdj(client, app);
+ doReturn(false).when(wpc).isHeavyWeightProcess();
+
+ assertEquals(PERCEPTIBLE_MEDIUM_APP_ADJ + 2, app.mState.getSetAdj());
+ }
+
+ {
+ ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+ MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
+ ProcessRecord client = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID,
+ MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
+ bindService(app, client, null,
+ Context.BIND_ALMOST_PERCEPTIBLE,
+ mock(IBinder.class));
+ client.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
+ sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
+ sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+
+ assertEquals(PERCEPTIBLE_APP_ADJ + 1, app.mState.getSetAdj());
+ }
+
+ {
+ ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+ MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
+ ProcessRecord client = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID,
+ MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
+ WindowProcessController wpc = client.getWindowProcessController();
+ doReturn(true).when(wpc).isHeavyWeightProcess();
+ bindService(app, client, null,
+ Context.BIND_ALMOST_PERCEPTIBLE,
+ mock(IBinder.class));
client.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
doReturn(false).when(wpc).isHeavyWeightProcess();
- assertEquals(PERCEPTIBLE_MEDIUM_APP_ADJ + 2, app.mState.getSetAdj());
+ assertEquals(PERCEPTIBLE_APP_ADJ + 1, app.mState.getSetAdj());
}
}
@@ -1072,7 +1257,7 @@
bindService(app, client, null, 0, mock(IBinder.class));
client.mState.setRunningRemoteAnimation(true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
assertEquals(VISIBLE_APP_ADJ, app.mState.getSetAdj());
}
@@ -1087,9 +1272,10 @@
bindService(app, client, null, Context.BIND_IMPORTANT_BACKGROUND, mock(IBinder.class));
client.mState.setHasOverlayUi(true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
assertEquals(PROCESS_STATE_IMPORTANT_BACKGROUND, app.mState.getSetProcState());
+ assertNoBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1098,7 +1284,7 @@
ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
bindProvider(app, app, null, null, false);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertProcStates(app, PROCESS_STATE_CACHED_EMPTY, FIRST_CACHED_ADJ, SCHED_GROUP_BACKGROUND);
}
@@ -1112,12 +1298,8 @@
MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
bindProvider(app, client, null, null, false);
client.mServices.setTreatLikeActivity(true);
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app);
- lru.add(client);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app, client);
assertProcStates(app, PROCESS_STATE_CACHED_EMPTY, FIRST_CACHED_ADJ, SCHED_GROUP_BACKGROUND);
}
@@ -1133,7 +1315,7 @@
doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
doReturn(client).when(sService).getTopApp();
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
doReturn(null).when(sService).getTopApp();
assertProcStates(app, PROCESS_STATE_BOUND_TOP, FOREGROUND_APP_ADJ, SCHED_GROUP_DEFAULT);
@@ -1149,10 +1331,46 @@
client.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
bindProvider(app, client, null, null, false);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ }
+
+ @SuppressWarnings("GuardedBy")
+ @Test
+ public void testUpdateOomAdj_DoOne_Provider_FgService_ShortFgs() {
+ // Client has a SHORT_SERVICE FGS, which isn't allowed BFSL.
+ ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+ MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
+ ProcessRecord client = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID,
+ MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
+
+ // In order to trick OomAdjuster to think it has a short-service, we need this logic.
+ ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(sService);
+ s.startRequested = true;
+ s.isForeground = true;
+ s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
+ s.setShortFgsInfo(SystemClock.uptimeMillis());
+ client.mServices.startService(s);
+ client.mState.setLastTopTime(SystemClock.uptimeMillis());
+
+ client.mServices.setHasForegroundServices(true, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
+ /* hasNoneType=*/false);
+ bindProvider(app, client, null, null, false);
+ sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
+ updateOomAdj(client, app);
+
+ // Client only has a SHORT_FGS, so it doesn't have BFSL, and that's propagated.
+ assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE,
+ PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ + 1,
+ SCHED_GROUP_DEFAULT);
+ assertNoBfsl(client);
+ assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE,
+ PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ + 1,
+ SCHED_GROUP_DEFAULT);
+ assertNoBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1164,7 +1382,7 @@
MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
bindProvider(app, client, null, null, true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, app);
assertProcStates(app, PROCESS_STATE_IMPORTANT_FOREGROUND, FOREGROUND_APP_ADJ,
SCHED_GROUP_DEFAULT);
@@ -1177,7 +1395,7 @@
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
app.mProviders.setLastProviderTime(SystemClock.uptimeMillis());
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertProcStates(app, PROCESS_STATE_LAST_ACTIVITY, PREVIOUS_APP_ADJ,
SCHED_GROUP_BACKGROUND);
@@ -1197,7 +1415,7 @@
doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
doReturn(client2).when(sService).getTopApp();
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, client2, app);
doReturn(null).when(sService).getTopApp();
assertProcStates(app, PROCESS_STATE_BOUND_TOP, VISIBLE_APP_ADJ,
@@ -1217,10 +1435,11 @@
bindService(app, client2, null, 0, mock(IBinder.class));
client2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, client2, app);
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1236,10 +1455,11 @@
bindService(client, client2, null, 0, mock(IBinder.class));
client2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, client2, app);
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1255,13 +1475,12 @@
bindService(client, client2, null, 0, mock(IBinder.class));
client2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
bindService(client2, app, null, 0, mock(IBinder.class));
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app);
- lru.add(client);
- lru.add(client2);
+
+ // Note: We add processes to LRU but still call updateOomAdjLocked() with a specific
+ // processes.
+ setProcessesToLru(app, client, client2);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app);
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
@@ -1269,6 +1488,9 @@
SCHED_GROUP_DEFAULT);
assertProcStates(client2, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ assertBfsl(client);
+ assertBfsl(client2);
client2.mServices.setHasForegroundServices(false, 0, /* hasNoneType=*/false);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
@@ -1277,6 +1499,9 @@
assertEquals(PROCESS_STATE_CACHED_EMPTY, client2.mState.getSetProcState());
assertEquals(PROCESS_STATE_CACHED_EMPTY, client.mState.getSetProcState());
assertEquals(PROCESS_STATE_CACHED_EMPTY, app.mState.getSetProcState());
+ assertNoBfsl(app);
+ assertNoBfsl(client);
+ assertNoBfsl(client2);
}
@SuppressWarnings("GuardedBy")
@@ -1292,13 +1517,8 @@
MOCKAPP3_PROCESSNAME, MOCKAPP3_PACKAGENAME, false));
bindService(client2, client, null, 0, mock(IBinder.class));
client.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app);
- lru.add(client);
- lru.add(client2);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app, client, client2);
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
@@ -1306,6 +1526,9 @@
SCHED_GROUP_DEFAULT);
assertProcStates(client2, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ assertBfsl(client);
+ assertBfsl(client2);
}
@SuppressWarnings("GuardedBy")
@@ -1321,13 +1544,8 @@
bindService(client, client2, null, 0, mock(IBinder.class));
bindService(client2, client, null, 0, mock(IBinder.class));
client.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app);
- lru.add(client);
- lru.add(client2);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app, client, client2);
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
@@ -1335,6 +1553,9 @@
SCHED_GROUP_DEFAULT);
assertProcStates(client2, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ assertBfsl(client);
+ assertBfsl(client2);
}
@SuppressWarnings("GuardedBy")
@@ -1357,15 +1578,8 @@
bindService(client3, client4, null, 0, mock(IBinder.class));
bindService(client4, client3, null, 0, mock(IBinder.class));
client.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app);
- lru.add(client);
- lru.add(client2);
- lru.add(client3);
- lru.add(client4);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app, client, client2, client3, client4);
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
@@ -1377,6 +1591,11 @@
SCHED_GROUP_DEFAULT);
assertProcStates(client4, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ assertBfsl(client);
+ assertBfsl(client2);
+ assertBfsl(client3);
+ assertBfsl(client4);
}
@SuppressWarnings("GuardedBy")
@@ -1396,17 +1615,12 @@
MOCKAPP4_PROCESSNAME, MOCKAPP4_PACKAGENAME, false));
client3.mState.setForcingToImportant(new Object());
bindService(app, client3, null, 0, mock(IBinder.class));
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app);
- lru.add(client);
- lru.add(client2);
- lru.add(client3);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app, client, client2, client3);
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1427,14 +1641,8 @@
MOCKAPP4_PROCESSNAME, MOCKAPP4_PACKAGENAME, false));
client3.mState.setForcingToImportant(new Object());
bindService(app, client3, null, 0, mock(IBinder.class));
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app);
- lru.add(client);
- lru.add(client2);
- lru.add(client3);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app, client, client2, client3);
assertProcStates(app, PROCESS_STATE_TRANSIENT_BACKGROUND, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
@@ -1460,15 +1668,8 @@
MOCKAPP5_PROCESSNAME, MOCKAPP5_PACKAGENAME, false));
client4.mState.setForcingToImportant(new Object());
bindService(app, client4, null, 0, mock(IBinder.class));
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app);
- lru.add(client);
- lru.add(client2);
- lru.add(client3);
- lru.add(client4);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app, client, client2, client3, client4);
assertProcStates(app, PROCESS_STATE_TRANSIENT_BACKGROUND, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
@@ -1496,18 +1697,12 @@
MOCKAPP5_PROCESSNAME, MOCKAPP5_PACKAGENAME, false));
client4.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
bindService(app, client4, null, 0, mock(IBinder.class));
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app);
- lru.add(client);
- lru.add(client2);
- lru.add(client3);
- lru.add(client4);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app, client, client2, client3, client4);
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1529,10 +1724,11 @@
client3.mState.setForcingToImportant(new Object());
bindService(app, client3, null, 0, mock(IBinder.class));
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, client2, client3, app);
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1548,10 +1744,11 @@
bindProvider(client, client2, null, null, false);
client2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, client2, app);
assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1567,16 +1764,12 @@
bindProvider(client, client2, null, null, false);
client2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
bindService(client2, app, null, 0, mock(IBinder.class));
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app);
- lru.add(client);
- lru.add(client2);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app, client, client2);
assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1592,10 +1785,11 @@
bindProvider(client, client2, null, null, false);
client2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client, client2, app);
assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1611,17 +1805,12 @@
bindProvider(client, client2, null, null, false);
client2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
bindProvider(client2, app, null, null, false);
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app);
- lru.add(client);
- lru.add(client2);
-
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app, client, client2);
assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
}
@SuppressWarnings("GuardedBy")
@@ -1643,18 +1832,18 @@
client2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app1, OomAdjuster.OOM_ADJ_REASON_NONE);
- sService.mOomAdjuster.updateOomAdjLocked(app2, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client1, client2, app1, app2);
assertProcStates(app1, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, VISIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
assertProcStates(app2, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app1);
+ assertBfsl(app2);
bindService(app1, client1, null, Context.BIND_SCHEDULE_LIKE_TOP_APP, mock(IBinder.class));
bindService(app2, client2, null, Context.BIND_SCHEDULE_LIKE_TOP_APP, mock(IBinder.class));
- sService.mOomAdjuster.updateOomAdjLocked(app1, OomAdjuster.OOM_ADJ_REASON_NONE);
- sService.mOomAdjuster.updateOomAdjLocked(app2, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app1, app2);
assertProcStates(app1, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, VISIBLE_APP_ADJ,
SCHED_GROUP_TOP_APP);
@@ -1662,12 +1851,12 @@
SCHED_GROUP_DEFAULT);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_ASLEEP);
- sService.mOomAdjuster.updateOomAdjLocked(app1, OomAdjuster.OOM_ADJ_REASON_NONE);
- sService.mOomAdjuster.updateOomAdjLocked(app2, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client1, client2, app1, app2);
assertProcStates(app1, PROCESS_STATE_IMPORTANT_FOREGROUND, VISIBLE_APP_ADJ,
SCHED_GROUP_TOP_APP);
assertProcStates(app2, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app2);
}
@SuppressWarnings("GuardedBy")
@@ -1689,8 +1878,7 @@
final ServiceRecord s2 = bindService(app2, client2, null,
Context.BIND_IMPORTANT, mock(IBinder.class));
- sService.mOomAdjuster.updateOomAdjLocked(app1, OomAdjuster.OOM_ADJ_REASON_NONE);
- sService.mOomAdjuster.updateOomAdjLocked(app2, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client1, client2, app1, app2);
assertProcStates(app1, PROCESS_STATE_FOREGROUND_SERVICE, VISIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
@@ -1699,7 +1887,7 @@
bindService(app2, client1, s2, Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE,
mock(IBinder.class));
- sService.mOomAdjuster.updateOomAdjLocked(app2, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app2);
assertProcStates(app2, PROCESS_STATE_PERSISTENT, PERSISTENT_SERVICE_ADJ,
SCHED_GROUP_DEFAULT);
@@ -1715,10 +1903,10 @@
bindService(app2, client2, s2, Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE,
mock(IBinder.class));
- sService.mOomAdjuster.updateOomAdjLocked(app1, OomAdjuster.OOM_ADJ_REASON_NONE);
- sService.mOomAdjuster.updateOomAdjLocked(app2, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app1, app2);
- assertProcStates(app1, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
+ // VISIBLE_APP_ADJ is the max oom-adj for BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE.
+ assertProcStates(app1, PROCESS_STATE_FOREGROUND_SERVICE, VISIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
assertProcStates(app2, PROCESS_STATE_IMPORTANT_FOREGROUND, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
@@ -1747,10 +1935,11 @@
bindService(app1, client1, null, Context.BIND_NOT_PERCEPTIBLE, mock(IBinder.class));
- sService.mOomAdjuster.updateOomAdjLocked(app1, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client1, app1);
assertProcStates(app1, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app1);
}
@SuppressWarnings("GuardedBy")
@@ -1767,10 +1956,11 @@
bindService(app1, client1, null, Context.BIND_ALMOST_PERCEPTIBLE, mock(IBinder.class));
- sService.mOomAdjuster.updateOomAdjLocked(app1, OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(client1, app1);
assertProcStates(app1, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app1);
}
@SuppressWarnings("GuardedBy")
@@ -1798,13 +1988,7 @@
client1.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
client2.mState.setForcingToImportant(new Object());
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app1);
- lru.add(app2);
- lru.add(app3);
- lru.add(client1);
- lru.add(client2);
+ setProcessesToLru(app1, app2, app3, client1, client2);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
final ComponentName cn1 = ComponentName.unflattenFromString(
@@ -1891,18 +2075,15 @@
ProcessRecord app2 = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID,
MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
app2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app);
- lru.add(app2);
+
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
- lru.clear();
+ updateOomAdj(app, app2);
assertProcStates(app, PROCESS_STATE_TRANSIENT_BACKGROUND, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
assertProcStates(app2, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app2);
}
@SuppressWarnings("GuardedBy")
@@ -1915,18 +2096,15 @@
MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
app2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
bindService(app, app2, null, 0, mock(IBinder.class));
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app);
- lru.add(app2);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
- lru.clear();
+ updateOomAdj(app, app2);
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
assertProcStates(app2, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ assertBfsl(app2);
}
@SuppressWarnings("GuardedBy")
@@ -1942,14 +2120,8 @@
bindService(app2, app3, null, 0, mock(IBinder.class));
app3.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
bindService(app3, app, null, 0, mock(IBinder.class));
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app);
- lru.add(app2);
- lru.add(app3);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
- lru.clear();
+ updateOomAdj(app, app2, app3);
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
@@ -1966,6 +2138,9 @@
assertEquals(false, app.mState.isEmpty());
assertEquals(false, app2.mState.isEmpty());
assertEquals(false, app3.mState.isEmpty());
+ assertBfsl(app);
+ assertBfsl(app2);
+ assertBfsl(app3);
}
@SuppressWarnings("GuardedBy")
@@ -1990,16 +2165,8 @@
MOCKAPP5_PROCESSNAME, MOCKAPP5_PACKAGENAME, false));
app5.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
bindService(app, app5, s, 0, mock(IBinder.class));
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app);
- lru.add(app2);
- lru.add(app3);
- lru.add(app4);
- lru.add(app5);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
- lru.clear();
+ updateOomAdj(app, app2, app3, app4, app5);
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
@@ -2011,6 +2178,11 @@
SCHED_GROUP_DEFAULT);
assertProcStates(app5, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ assertBfsl(app2);
+ assertBfsl(app3);
+ // 4 is IMP_FG
+ assertBfsl(app5);
}
@SuppressWarnings("GuardedBy")
@@ -2035,16 +2207,8 @@
MOCKAPP5_PROCESSNAME, MOCKAPP5_PACKAGENAME, false));
app5.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
bindService(app, app5, s, 0, mock(IBinder.class));
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app5);
- lru.add(app4);
- lru.add(app3);
- lru.add(app2);
- lru.add(app);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
- lru.clear();
+ updateOomAdj(app5, app4, app3, app2, app);
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
@@ -2056,6 +2220,11 @@
SCHED_GROUP_DEFAULT);
assertProcStates(app5, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ assertBfsl(app2);
+ assertBfsl(app3);
+ // 4 is IMP_FG
+ assertBfsl(app5);
}
@SuppressWarnings("GuardedBy")
@@ -2080,16 +2249,8 @@
MOCKAPP5_PROCESSNAME, MOCKAPP5_PACKAGENAME, false));
app5.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
bindService(app, app5, s, 0, mock(IBinder.class));
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app3);
- lru.add(app4);
- lru.add(app2);
- lru.add(app);
- lru.add(app5);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
- lru.clear();
+ updateOomAdj(app3, app4, app2, app, app5);
assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
@@ -2101,6 +2262,11 @@
SCHED_GROUP_DEFAULT);
assertProcStates(app5, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ assertBfsl(app2);
+ assertBfsl(app3);
+ // 4 is IMP_FG
+ assertBfsl(app5);
}
@SuppressWarnings("GuardedBy")
@@ -2119,18 +2285,13 @@
MOCKAPP4_PROCESSNAME, MOCKAPP4_PACKAGENAME, false));
client3.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
bindService(app, client3, null, Context.BIND_INCLUDE_CAPABILITIES, mock(IBinder.class));
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app);
- lru.add(client);
- lru.add(client2);
- lru.add(client3);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
+ updateOomAdj(app, client, client2, client3);
- assertEquals(PROCESS_CAPABILITY_ALL, client.mState.getSetCapability());
- assertEquals(PROCESS_CAPABILITY_ALL, client2.mState.getSetCapability());
- assertEquals(PROCESS_CAPABILITY_ALL, app.mState.getSetCapability());
+ final int expected = PROCESS_CAPABILITY_ALL & ~PROCESS_CAPABILITY_BFSL;
+ assertEquals(expected, client.mState.getSetCapability());
+ assertEquals(expected, client2.mState.getSetCapability());
+ assertEquals(expected, app.mState.getSetCapability());
}
@SuppressWarnings("GuardedBy")
@@ -2155,16 +2316,8 @@
MOCKAPP5_PROCESSNAME, MOCKAPP5_PACKAGENAME, false));
app5.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
bindProvider(app, app5, cr, null, false);
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app);
- lru.add(app2);
- lru.add(app3);
- lru.add(app4);
- lru.add(app5);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
- lru.clear();
+ updateOomAdj(app, app2, app3, app4, app5);
assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
@@ -2176,6 +2329,11 @@
SCHED_GROUP_DEFAULT);
assertProcStates(app5, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
SCHED_GROUP_DEFAULT);
+ assertBfsl(app);
+ assertBfsl(app2);
+ assertBfsl(app3);
+ // 4 is IMP_FG
+ assertBfsl(app5);
}
@SuppressWarnings("GuardedBy")
@@ -2202,15 +2360,9 @@
doCallRealMethod().when(s).getConnections();
s.startRequested = true;
s.lastActivity = now;
- ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
- lru.clear();
- lru.add(app3);
- lru.add(app2);
- lru.add(app);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
sService.mOomAdjuster.mNumServiceProcs = 3;
- sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
- lru.clear();
+ updateOomAdj(app3, app2, app);
assertEquals(SERVICE_B_ADJ, app3.mState.getSetAdj());
assertEquals(SERVICE_ADJ, app2.mState.getSetAdj());
@@ -2489,6 +2641,15 @@
assertEquals(expectedProcState, state.getSetProcState());
assertEquals(expectedAdj, state.getSetAdj());
assertEquals(expectedSchedGroup, state.getSetSchedGroup());
+
+ // Below BFGS should never have BFSL.
+ if (expectedProcState > PROCESS_STATE_BOUND_FOREGROUND_SERVICE) {
+ assertNoBfsl(app);
+ }
+ // Above FGS should always have BFSL.
+ if (expectedProcState < PROCESS_STATE_FOREGROUND_SERVICE) {
+ assertBfsl(app);
+ }
}
private void assertProcStates(ProcessRecord app, boolean expectedCached,
@@ -2498,5 +2659,14 @@
assertEquals(expectedProcState, state.getSetProcState());
assertEquals(expectedAdj, state.getSetAdj());
assertEquals(expectedAdjType, state.getAdjType());
+
+ // Below BFGS should never have BFSL.
+ if (expectedProcState > PROCESS_STATE_BOUND_FOREGROUND_SERVICE) {
+ assertNoBfsl(app);
+ }
+ // Above FGS should always have BFSL.
+ if (expectedProcState < PROCESS_STATE_FOREGROUND_SERVICE) {
+ assertBfsl(app);
+ }
}
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java
index 9eed6ad..d1f7f93 100644
--- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java
@@ -42,6 +42,7 @@
import android.content.res.AssetManager;
import android.os.Handler;
import android.os.UserHandle;
+import android.util.ArrayMap;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
@@ -51,12 +52,14 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.internal.util.ArrayUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.server.LocalServices;
import com.android.server.SystemServerInitThreadPool;
import com.android.server.pm.UserManagerInternal;
import com.android.server.pm.permission.PermissionManagerServiceInternal;
+import com.android.server.pm.pkg.PackageStateInternal;
import org.junit.After;
import org.junit.Before;
@@ -188,6 +191,11 @@
// Stub out package calls to disable AppOpsService#updatePermissionRevokedCompat
doReturn(null).when(mPackageManager).getPackagesForUid(anyInt());
+
+ doReturn(new ArrayMap<String, PackageStateInternal>()).when(mPackageManagerInternal)
+ .getPackageStates();
+
+ doReturn(new int[] {0}).when(mUserManagerInternal).getUserIds();
}
@After
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
new file mode 100644
index 0000000..327fc19
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.annotation.NonNull;
+import android.app.backup.BackupHelper;
+import android.content.Context;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.platform.test.annotations.Presubmit;
+import android.util.ArraySet;
+
+import static org.mockito.Mockito.when;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Set;
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class SystemBackupAgentTest {
+ private static final int NON_SYSTEM_USER_ID = 10;
+
+ private TestableSystemBackupAgent mSystemBackupAgent;
+
+ @Mock private Context mContextMock;
+ @Mock private UserManager mUserManagerMock;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mSystemBackupAgent = new TestableSystemBackupAgent();
+ when(mContextMock.getSystemService(UserManager.class)).thenReturn(mUserManagerMock);
+ }
+
+ @Test
+ public void onCreate_systemUser_addsAllHelpers() {
+ UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM);
+ when(mUserManagerMock.isProfile()).thenReturn(false);
+
+ mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0);
+
+ assertThat(mSystemBackupAgent.mAddedHelpers)
+ .containsExactly(
+ "account_sync_settings",
+ "preferred_activities",
+ "notifications",
+ "permissions",
+ "usage_stats",
+ "shortcut_manager",
+ "account_manager",
+ "slices",
+ "people",
+ "app_locales",
+ "app_gender");
+ }
+
+ @Test
+ public void onCreate_profileUser_addsProfileEligibleHelpers() {
+ UserHandle userHandle = new UserHandle(NON_SYSTEM_USER_ID);
+ when(mUserManagerMock.isProfile()).thenReturn(true);
+
+ mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0);
+
+ assertThat(mSystemBackupAgent.mAddedHelpers)
+ .containsExactly(
+ "account_sync_settings",
+ "notifications",
+ "permissions",
+ "app_locales");
+ }
+
+ @Test
+ public void onCreate_nonSystemUser_addsNonSystemEligibleHelpers() {
+ UserHandle userHandle = new UserHandle(NON_SYSTEM_USER_ID);
+ when(mUserManagerMock.isProfile()).thenReturn(false);
+
+ mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0);
+
+ assertThat(mSystemBackupAgent.mAddedHelpers)
+ .containsExactly(
+ "account_sync_settings",
+ "preferred_activities",
+ "notifications",
+ "permissions",
+ "app_locales",
+ "account_manager",
+ "usage_stats",
+ "shortcut_manager");
+ }
+
+ private class TestableSystemBackupAgent extends SystemBackupAgent {
+ final Set<String> mAddedHelpers = new ArraySet<>();
+
+ @Override
+ public void addHelper(String keyPrefix, BackupHelper helper) {
+ mAddedHelpers.add(keyPrefix);
+ }
+
+ @Override
+ public Context createContextAsUser(UserHandle user, @CreatePackageOptions int flags) {
+ return mContextMock;
+ }
+
+ @Override
+ public Object getSystemService(@ServiceName @NonNull String name) {
+ return null;
+ }
+ }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupEligibilityRulesTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupEligibilityRulesTest.java
index 6093f4b..0306655 100644
--- a/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupEligibilityRulesTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupEligibilityRulesTest.java
@@ -25,6 +25,7 @@
import android.app.backup.BackupAnnotations.BackupDestination;
import android.compat.testing.PlatformCompatChangeRule;
+import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@@ -35,6 +36,7 @@
import android.content.pm.SigningInfo;
import android.os.Process;
import android.os.UserHandle;
+import android.os.UserManager;
import android.platform.test.annotations.Presubmit;
import androidx.test.filters.SmallTest;
@@ -64,11 +66,17 @@
private static final Signature SIGNATURE_2 = generateSignature((byte) 2);
private static final Signature SIGNATURE_3 = generateSignature((byte) 3);
private static final Signature SIGNATURE_4 = generateSignature((byte) 4);
+ private static final int NON_SYSTEM_USER = 10;
@Rule public TestRule compatChangeRule = new PlatformCompatChangeRule();
@Mock private PackageManagerInternal mMockPackageManagerInternal;
- @Mock private PackageManager mPackageManager;
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private Context mContext;
+ @Mock
+ private UserManager mUserManager;
private BackupEligibilityRules mBackupEligibilityRules;
private int mUserId;
@@ -78,6 +86,7 @@
MockitoAnnotations.initMocks(this);
mUserId = UserHandle.USER_SYSTEM;
+ mockContextForFullUser();
mBackupEligibilityRules = getBackupEligibilityRules(BackupDestination.CLOUD);
}
@@ -95,6 +104,70 @@
}
@Test
+ public void appIsEligibleForBackup_systemUid_nonSystemUser_notAllowedPackage_returnsFalse()
+ throws Exception {
+ setUpForNonSystemUser();
+
+ ApplicationInfo applicationInfo = new ApplicationInfo();
+ applicationInfo.flags |= ApplicationInfo.FLAG_ALLOW_BACKUP;
+ applicationInfo.uid = Process.SYSTEM_UID;
+ applicationInfo.backupAgentName = CUSTOM_BACKUP_AGENT_NAME;
+ applicationInfo.packageName = TEST_PACKAGE_NAME;
+
+ boolean isEligible = mBackupEligibilityRules.appIsEligibleForBackup(applicationInfo);
+
+ assertThat(isEligible).isFalse();
+ }
+
+ @Test
+ public void appIsEligibleForBackup_systemUid_nonSystemUser_allowedPackage_returnsTrue()
+ throws Exception {
+ setUpForNonSystemUser();
+
+ ApplicationInfo applicationInfo = new ApplicationInfo();
+ applicationInfo.flags |= ApplicationInfo.FLAG_ALLOW_BACKUP;
+ applicationInfo.uid = Process.SYSTEM_UID;
+ applicationInfo.backupAgentName = CUSTOM_BACKUP_AGENT_NAME;
+ applicationInfo.packageName = UserBackupManagerService.WALLPAPER_PACKAGE;
+
+ boolean isEligible = mBackupEligibilityRules.appIsEligibleForBackup(applicationInfo);
+
+ assertThat(isEligible).isTrue();
+ }
+
+ @Test
+ public void appIsEligibleForBackup_systemUid_profileUser_notAllowedPackage_returnsFalse()
+ throws Exception {
+ setUpForProfileUser();
+
+ ApplicationInfo applicationInfo = new ApplicationInfo();
+ applicationInfo.flags |= ApplicationInfo.FLAG_ALLOW_BACKUP;
+ applicationInfo.uid = Process.SYSTEM_UID;
+ applicationInfo.backupAgentName = CUSTOM_BACKUP_AGENT_NAME;
+ applicationInfo.packageName = TEST_PACKAGE_NAME;
+
+ boolean isEligible = mBackupEligibilityRules.appIsEligibleForBackup(applicationInfo);
+
+ assertThat(isEligible).isFalse();
+ }
+
+ @Test
+ public void appIsEligibleForBackup_systemUid_profileUser_allowedPackage_returnsTrue()
+ throws Exception {
+ setUpForProfileUser();
+
+ ApplicationInfo applicationInfo = new ApplicationInfo();
+ applicationInfo.flags |= ApplicationInfo.FLAG_ALLOW_BACKUP;
+ applicationInfo.uid = Process.SYSTEM_UID;
+ applicationInfo.backupAgentName = CUSTOM_BACKUP_AGENT_NAME;
+ applicationInfo.packageName = UserBackupManagerService.PACKAGE_MANAGER_SENTINEL;
+
+ boolean isEligible = mBackupEligibilityRules.appIsEligibleForBackup(applicationInfo);
+
+ assertThat(isEligible).isTrue();
+ }
+
+ @Test
public void appIsEligibleForBackup_systemAppWithoutCustomBackupAgent_returnsFalse()
throws Exception {
ApplicationInfo applicationInfo = new ApplicationInfo();
@@ -790,7 +863,7 @@
private BackupEligibilityRules getBackupEligibilityRules(
@BackupDestination int backupDestination) {
return new BackupEligibilityRules(mPackageManager, mMockPackageManagerInternal, mUserId,
- backupDestination);
+ mContext, backupDestination);
}
private static Signature generateSignature(byte i) {
@@ -813,4 +886,26 @@
return new Property(PackageManager.PROPERTY_ALLOW_ADB_BACKUP, allowAdbBackup,
TEST_PACKAGE_NAME, /* className */ "");
}
+
+ private void setUpForNonSystemUser() {
+ mUserId = NON_SYSTEM_USER;
+ mockContextForFullUser();
+ mBackupEligibilityRules = getBackupEligibilityRules(BackupDestination.CLOUD);
+ }
+
+ private void setUpForProfileUser() {
+ mUserId = NON_SYSTEM_USER;
+ mockContextForProfile();
+ mBackupEligibilityRules = getBackupEligibilityRules(BackupDestination.CLOUD);
+ }
+
+ private void mockContextForProfile() {
+ when(mUserManager.isProfile()).thenReturn(true);
+ when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
+ }
+
+ private void mockContextForFullUser() {
+ when(mUserManager.isProfile()).thenReturn(false);
+ when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
+ }
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/utils/TarBackupReaderTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/utils/TarBackupReaderTest.java
index 30c6975..3399565 100644
--- a/services/tests/mockingservicestests/src/com/android/server/backup/utils/TarBackupReaderTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/utils/TarBackupReaderTest.java
@@ -149,7 +149,7 @@
fileMetadata);
RestorePolicy restorePolicy = tarBackupReader.chooseRestorePolicy(
mPackageManagerStub, false /* allowApks */, fileMetadata, signatures,
- mMockPackageManagerInternal, mUserId);
+ mMockPackageManagerInternal, mUserId, mContext);
assertThat(restorePolicy).isEqualTo(RestorePolicy.IGNORE);
assertThat(fileMetadata.packageName).isEqualTo(TEST_PACKAGE_NAME);
@@ -163,7 +163,7 @@
fileMetadata);
restorePolicy = tarBackupReader.chooseRestorePolicy(
mPackageManagerStub, false /* allowApks */, fileMetadata, signatures,
- mMockPackageManagerInternal, mUserId);
+ mMockPackageManagerInternal, mUserId, mContext);
assertThat(restorePolicy).isEqualTo(RestorePolicy.IGNORE);
assertThat(fileMetadata.packageName).isEqualTo(TEST_PACKAGE_NAME);
@@ -226,7 +226,7 @@
RestorePolicy policy = tarBackupReader.chooseRestorePolicy(mPackageManagerStub,
true /* allowApks */, new FileMetadata(), null /* signatures */,
- mMockPackageManagerInternal, mUserId);
+ mMockPackageManagerInternal, mUserId, mContext);
assertThat(policy).isEqualTo(RestorePolicy.IGNORE);
verifyZeroInteractions(mBackupManagerMonitorMock);
@@ -247,7 +247,7 @@
RestorePolicy policy = tarBackupReader.chooseRestorePolicy(mPackageManagerStub,
true /* allowApks */, info, new Signature[0] /* signatures */,
- mMockPackageManagerInternal, mUserId);
+ mMockPackageManagerInternal, mUserId, mContext);
assertThat(policy).isEqualTo(RestorePolicy.ACCEPT_IF_APK);
ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
@@ -272,7 +272,7 @@
RestorePolicy policy = tarBackupReader.chooseRestorePolicy(mPackageManagerStub,
true /* allowApks */, info, new Signature[0] /* signatures */,
- mMockPackageManagerInternal, mUserId);
+ mMockPackageManagerInternal, mUserId, mContext);
assertThat(policy).isEqualTo(RestorePolicy.ACCEPT_IF_APK);
ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
@@ -298,7 +298,7 @@
RestorePolicy policy = tarBackupReader.chooseRestorePolicy(mPackageManagerStub,
false /* allowApks */, new FileMetadata(), new Signature[0] /* signatures */,
- mMockPackageManagerInternal, mUserId);
+ mMockPackageManagerInternal, mUserId, mContext);
assertThat(policy).isEqualTo(RestorePolicy.IGNORE);
ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
@@ -323,7 +323,7 @@
RestorePolicy policy = tarBackupReader.chooseRestorePolicy(mPackageManagerStub,
false /* allowApks */, new FileMetadata(), new Signature[0] /* signatures */,
- mMockPackageManagerInternal, mUserId);
+ mMockPackageManagerInternal, mUserId, mContext);
assertThat(policy).isEqualTo(RestorePolicy.IGNORE);
ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
@@ -350,7 +350,7 @@
RestorePolicy policy = tarBackupReader.chooseRestorePolicy(mPackageManagerStub,
false /* allowApks */, new FileMetadata(), new Signature[0] /* signatures */,
- mMockPackageManagerInternal, mUserId);
+ mMockPackageManagerInternal, mUserId, mContext);
assertThat(policy).isEqualTo(RestorePolicy.IGNORE);
ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
@@ -385,7 +385,7 @@
RestorePolicy policy = tarBackupReader.chooseRestorePolicy(mPackageManagerStub,
false /* allowApks */, new FileMetadata(), signatures,
- mMockPackageManagerInternal, mUserId);
+ mMockPackageManagerInternal, mUserId, mContext);
assertThat(policy).isEqualTo(RestorePolicy.IGNORE);
ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
@@ -424,7 +424,7 @@
packageInfo.packageName);
RestorePolicy policy = tarBackupReader.chooseRestorePolicy(mPackageManagerStub,
false /* allowApks */, new FileMetadata(), signatures,
- mMockPackageManagerInternal, mUserId);
+ mMockPackageManagerInternal, mUserId, mContext);
assertThat(policy).isEqualTo(RestorePolicy.ACCEPT);
ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
@@ -462,7 +462,7 @@
packageInfo.packageName);
RestorePolicy policy = tarBackupReader.chooseRestorePolicy(mPackageManagerStub,
false /* allowApks */, new FileMetadata(), signatures,
- mMockPackageManagerInternal, mUserId);
+ mMockPackageManagerInternal, mUserId, mContext);
assertThat(policy).isEqualTo(RestorePolicy.ACCEPT);
ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
@@ -504,7 +504,7 @@
packageInfo.packageName);
RestorePolicy policy = tarBackupReader.chooseRestorePolicy(mPackageManagerStub,
false /* allowApks */, info, signatures, mMockPackageManagerInternal,
- mUserId);
+ mUserId, mContext);
assertThat(policy).isEqualTo(RestorePolicy.ACCEPT);
ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
@@ -548,7 +548,7 @@
packageInfo.packageName);
RestorePolicy policy = tarBackupReader.chooseRestorePolicy(mPackageManagerStub,
true /* allowApks */, info, signatures, mMockPackageManagerInternal,
- mUserId);
+ mUserId, mContext);
assertThat(policy).isEqualTo(RestorePolicy.ACCEPT_IF_APK);
verifyNoMoreInteractions(mBackupManagerMonitorMock);
@@ -588,7 +588,7 @@
packageInfo.packageName);
RestorePolicy policy = tarBackupReader.chooseRestorePolicy(mPackageManagerStub,
false /* allowApks */, info, signatures, mMockPackageManagerInternal,
- mUserId);
+ mUserId, mContext);
assertThat(policy).isEqualTo(RestorePolicy.IGNORE);
ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
index 3d36c1c..6e63315 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
@@ -16,13 +16,19 @@
package com.android.server.display;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
@@ -37,6 +43,7 @@
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
+import android.os.SystemProperties;
import android.os.test.TestLooper;
import android.util.FloatProperty;
import android.view.Display;
@@ -46,8 +53,12 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.server.LocalServices;
+import com.android.server.am.BatteryStatsService;
import com.android.server.display.RampAnimator.DualRampAnimator;
+import com.android.server.display.color.ColorDisplayService;
+import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
import com.android.server.policy.WindowManagerPolicy;
import com.android.server.testutils.OffsettableClock;
@@ -58,7 +69,9 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+import org.mockito.stubbing.Answer;
import java.util.List;
@@ -66,14 +79,20 @@
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class DisplayPowerController2Test {
- private static final String UNIQUE_DISPLAY_ID = "unique_id_test123";
- private static final int DISPLAY_ID = 42;
+ private static final int DISPLAY_ID = Display.DEFAULT_DISPLAY;
+ private static final String UNIQUE_ID = "unique_id_test123";
+ private static final int FOLLOWER_DISPLAY_ID = DISPLAY_ID + 1;
+ private static final String FOLLOWER_UNIQUE_ID = "unique_id_456";
+ private static final int SECOND_FOLLOWER_DISPLAY_ID = FOLLOWER_DISPLAY_ID + 1;
+ private static final String SECOND_FOLLOWER_UNIQUE_DISPLAY_ID = "unique_id_789";
+ private MockitoSession mSession;
private OffsettableClock mClock;
private TestLooper mTestLooper;
private Handler mHandler;
- private DisplayPowerController2.Injector mInjector;
private Context mContextSpy;
+ private DisplayPowerControllerHolder mHolder;
+ private Sensor mProxSensor;
@Mock
private DisplayPowerCallbacks mDisplayPowerCallbacksMock;
@@ -82,131 +101,106 @@
@Mock
private DisplayBlanker mDisplayBlankerMock;
@Mock
- private HighBrightnessModeMetadata mHighBrightnessModeMetadataMock;
- @Mock
- private LogicalDisplay mLogicalDisplayMock;
- @Mock
- private DisplayDevice mDisplayDeviceMock;
- @Mock
private BrightnessTracker mBrightnessTrackerMock;
@Mock
- private BrightnessSetting mBrightnessSettingMock;
- @Mock
private WindowManagerPolicy mWindowManagerPolicyMock;
@Mock
private PowerManager mPowerManagerMock;
@Mock
private Resources mResourcesMock;
@Mock
- private DisplayDeviceConfig mDisplayDeviceConfigMock;
- @Mock
- private DisplayPowerState mDisplayPowerStateMock;
- @Mock
- private DualRampAnimator<DisplayPowerState> mDualRampAnimatorMock;
- @Mock
- private WakelockController mWakelockController;
+ private ColorDisplayService.ColorDisplayServiceInternal mCdsiMock;
@Captor
private ArgumentCaptor<SensorEventListener> mSensorEventListenerCaptor;
@Before
public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
+ mSession = ExtendedMockito.mockitoSession()
+ .initMocks(this)
+ .strictness(Strictness.LENIENT)
+ .spyStatic(SystemProperties.class)
+ .spyStatic(LocalServices.class)
+ .spyStatic(BatteryStatsService.class)
+ .startMocking();
mContextSpy = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
mClock = new OffsettableClock.Stopped();
mTestLooper = new TestLooper(mClock::now);
mHandler = new Handler(mTestLooper.getLooper());
- mInjector = new DisplayPowerController2.Injector() {
- @Override
- DisplayPowerController2.Clock getClock() {
- return mClock::now;
- }
-
- @Override
- DisplayPowerState getDisplayPowerState(DisplayBlanker blanker, ColorFade colorFade,
- int displayId, int displayState) {
- return mDisplayPowerStateMock;
- }
-
- @Override
- DualRampAnimator<DisplayPowerState> getDualRampAnimator(DisplayPowerState dps,
- FloatProperty<DisplayPowerState> firstProperty,
- FloatProperty<DisplayPowerState> secondProperty) {
- return mDualRampAnimatorMock;
- }
-
- @Override
- WakelockController getWakelockController(int displayId,
- DisplayPowerCallbacks displayPowerCallbacks) {
- return mWakelockController;
- }
-
- @Override
- DisplayPowerProximityStateController getDisplayPowerProximityStateController(
- WakelockController wakelockController, DisplayDeviceConfig displayDeviceConfig,
- Looper looper, Runnable nudgeUpdatePowerState, int displayId,
- SensorManager sensorManager) {
- return new DisplayPowerProximityStateController(wakelockController,
- displayDeviceConfig, looper, nudgeUpdatePowerState, displayId,
- sensorManager, /* injector= */ null);
- }
- };
-
addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock);
when(mContextSpy.getSystemService(eq(PowerManager.class))).thenReturn(mPowerManagerMock);
when(mContextSpy.getResources()).thenReturn(mResourcesMock);
+
+ doAnswer((Answer<Void>) invocationOnMock -> null).when(() ->
+ SystemProperties.set(anyString(), any()));
+ doAnswer((Answer<ColorDisplayService.ColorDisplayServiceInternal>) invocationOnMock ->
+ mCdsiMock).when(() -> LocalServices.getService(
+ ColorDisplayService.ColorDisplayServiceInternal.class));
+ doAnswer((Answer<Void>) invocationOnMock -> null).when(BatteryStatsService::getService);
+
+ mProxSensor = setUpProxSensor();
+
+ mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
}
@After
public void tearDown() {
+ mSession.finishMocking();
LocalServices.removeServiceForTest(WindowManagerPolicy.class);
}
@Test
public void testReleaseProxSuspendBlockersOnExit() throws Exception {
- setUpDisplay(DISPLAY_ID, UNIQUE_DISPLAY_ID);
-
- Sensor proxSensor = setUpProxSensor();
-
- DisplayPowerController2 dpc = new DisplayPowerController2(
- mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
- mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
- mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
- }, mHighBrightnessModeMetadataMock);
-
- when(mDisplayPowerStateMock.getScreenState()).thenReturn(Display.STATE_ON);
+ when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
// send a display power request
DisplayPowerRequest dpr = new DisplayPowerRequest();
dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
dpr.useProximitySensor = true;
- dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
+ mHolder.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
// Run updatePowerState to start listener for the prox sensor
advanceTime(1);
- SensorEventListener listener = getSensorEventListener(proxSensor);
+ SensorEventListener listener = getSensorEventListener(mProxSensor);
assertNotNull(listener);
- listener.onSensorChanged(TestUtils.createSensorEvent(proxSensor, 5 /* lux */));
+ listener.onSensorChanged(TestUtils.createSensorEvent(mProxSensor, 5 /* lux */));
advanceTime(1);
// two times, one for unfinished business and one for proximity
- verify(mWakelockController).acquireWakelock(
+ verify(mHolder.wakelockController).acquireWakelock(
WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
- verify(mWakelockController).acquireWakelock(
+ verify(mHolder.wakelockController).acquireWakelock(
WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
-
- dpc.stop();
+ mHolder.dpc.stop();
advanceTime(1);
// two times, one for unfinished business and one for proximity
- verify(mWakelockController).acquireWakelock(
+ verify(mHolder.wakelockController).acquireWakelock(
WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
- verify(mWakelockController).acquireWakelock(
+ verify(mHolder.wakelockController).acquireWakelock(
WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
}
+ @Test
+ public void testProximitySensorListenerNotRegisteredForNonDefaultDisplay() {
+ when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+ // send a display power request
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
+ dpr.useProximitySensor = true;
+ final DisplayPowerControllerHolder followerDpc =
+ createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
+ followerDpc.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
+
+ // Run updatePowerState
+ advanceTime(1);
+
+ verify(mSensorManagerMock, never()).registerListener(any(SensorEventListener.class),
+ eq(mProxSensor), anyInt(), any(Handler.class));
+ }
+
/**
* Creates a mock and registers it to {@link LocalServices}.
*/
@@ -234,56 +228,394 @@
return mSensorEventListenerCaptor.getValue();
}
- private void setUpDisplay(int displayId, String uniqueId) {
+ private void setUpDisplay(int displayId, String uniqueId, LogicalDisplay logicalDisplayMock,
+ DisplayDevice displayDeviceMock, DisplayDeviceConfig displayDeviceConfigMock) {
DisplayInfo info = new DisplayInfo();
DisplayDeviceInfo deviceInfo = new DisplayDeviceInfo();
- when(mLogicalDisplayMock.getDisplayIdLocked()).thenReturn(displayId);
- when(mLogicalDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(mDisplayDeviceMock);
- when(mLogicalDisplayMock.getDisplayInfoLocked()).thenReturn(info);
- when(mLogicalDisplayMock.isEnabledLocked()).thenReturn(true);
- when(mLogicalDisplayMock.isInTransitionLocked()).thenReturn(false);
- when(mDisplayDeviceMock.getDisplayDeviceInfoLocked()).thenReturn(deviceInfo);
- when(mDisplayDeviceMock.getUniqueId()).thenReturn(uniqueId);
- when(mDisplayDeviceMock.getDisplayDeviceConfig()).thenReturn(mDisplayDeviceConfigMock);
- when(mDisplayDeviceConfigMock.getProximitySensor()).thenReturn(
+ when(logicalDisplayMock.getDisplayIdLocked()).thenReturn(displayId);
+ when(logicalDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(displayDeviceMock);
+ when(logicalDisplayMock.getDisplayInfoLocked()).thenReturn(info);
+ when(logicalDisplayMock.isEnabledLocked()).thenReturn(true);
+ when(logicalDisplayMock.isInTransitionLocked()).thenReturn(false);
+ when(displayDeviceMock.getDisplayDeviceInfoLocked()).thenReturn(deviceInfo);
+ when(displayDeviceMock.getUniqueId()).thenReturn(uniqueId);
+ when(displayDeviceMock.getDisplayDeviceConfig()).thenReturn(displayDeviceConfigMock);
+ when(displayDeviceConfigMock.getProximitySensor()).thenReturn(
new DisplayDeviceConfig.SensorData() {
{
type = Sensor.STRING_TYPE_PROXIMITY;
name = null;
}
});
- when(mDisplayDeviceConfigMock.getNits()).thenReturn(new float[]{2, 500});
+ when(displayDeviceConfigMock.getNits()).thenReturn(new float[]{2, 500});
+ when(displayDeviceConfigMock.isAutoBrightnessAvailable()).thenReturn(true);
+ when(displayDeviceConfigMock.getAmbientLightSensor()).thenReturn(
+ new DisplayDeviceConfig.SensorData());
+ when(displayDeviceConfigMock.getScreenOffBrightnessSensor()).thenReturn(
+ new DisplayDeviceConfig.SensorData());
}
@Test
- public void testDisplayBrightnessFollowers() {
- setUpDisplay(DISPLAY_ID, UNIQUE_DISPLAY_ID);
+ public void testDisplayBrightnessFollowers_BothDpcsSupportNits() {
+ DisplayPowerControllerHolder followerDpc =
+ createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
- DisplayPowerController2 defaultDpc = new DisplayPowerController2(
- mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
- mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
- mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
- }, mHighBrightnessModeMetadataMock);
- DisplayPowerController2 followerDpc = new DisplayPowerController2(
- mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
- mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
- mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
- }, mHighBrightnessModeMetadataMock);
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
- defaultDpc.addDisplayBrightnessFollower(followerDpc);
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
- defaultDpc.setBrightness(0.3f);
- assertEquals(defaultDpc.getBrightnessInfo().brightness,
- followerDpc.getBrightnessInfo().brightness, 0);
+ mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
- defaultDpc.setBrightness(0.6f);
- assertEquals(defaultDpc.getBrightnessInfo().brightness,
- followerDpc.getBrightnessInfo().brightness, 0);
+ // Test different float scale values
+ float leadBrightness = 0.3f;
+ float followerBrightness = 0.4f;
+ float nits = 300;
+ when(mHolder.automaticBrightnessController.convertToNits(leadBrightness)).thenReturn(nits);
+ when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+ .thenReturn(followerBrightness);
+ when(mHolder.brightnessSetting.getBrightness()).thenReturn(leadBrightness);
+ listener.onBrightnessChanged(leadBrightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(), anyFloat());
+ verify(followerDpc.animator).animateTo(eq(followerBrightness), anyFloat(),
+ anyFloat());
- float brightness = 0.1f;
- defaultDpc.clearDisplayBrightnessFollowers();
- defaultDpc.setBrightness(brightness);
- assertNotEquals(brightness, followerDpc.getBrightnessInfo().brightness, 0);
+ clearInvocations(mHolder.animator, followerDpc.animator);
+
+ // Test the same float scale value
+ float brightness = 0.6f;
+ nits = 600;
+ when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
+ when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+ .thenReturn(brightness);
+ when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ }
+
+ @Test
+ public void testDisplayBrightnessFollowers_FollowerDoesNotSupportNits() {
+ DisplayPowerControllerHolder followerDpc =
+ createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
+
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+ mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
+
+ float brightness = 0.3f;
+ when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(300f);
+ when(followerDpc.automaticBrightnessController.convertToFloatScale(anyFloat()))
+ .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
+ when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ }
+
+ @Test
+ public void testDisplayBrightnessFollowers_LeadDpcDoesNotSupportNits() {
+ DisplayPowerControllerHolder followerDpc =
+ createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
+
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+ mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
+
+ float brightness = 0.3f;
+ when(mHolder.automaticBrightnessController.convertToNits(anyFloat())).thenReturn(-1f);
+ when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ }
+
+ @Test
+ public void testDisplayBrightnessFollowers_NeitherDpcSupportsNits() {
+ DisplayPowerControllerHolder followerDpc =
+ createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
+
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+ mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
+
+ float brightness = 0.3f;
+ when(mHolder.automaticBrightnessController.convertToNits(anyFloat())).thenReturn(-1f);
+ when(followerDpc.automaticBrightnessController.convertToFloatScale(anyFloat()))
+ .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
+ when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ }
+
+
+ @Test
+ public void testDisplayBrightnessFollowersRemoval() {
+ DisplayPowerControllerHolder followerDpc = createDisplayPowerController(FOLLOWER_DISPLAY_ID,
+ FOLLOWER_UNIQUE_ID);
+ DisplayPowerControllerHolder secondFollowerDpc = createDisplayPowerController(
+ SECOND_FOLLOWER_DISPLAY_ID, SECOND_FOLLOWER_UNIQUE_DISPLAY_ID);
+
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ secondFollowerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+ // Set the initial brightness on the DPC we're going to remove so we have a fixed value for
+ // it to return to.
+ listenerCaptor = ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(followerDpc.brightnessSetting).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener followerListener = listenerCaptor.getValue();
+ final float initialFollowerBrightness = 0.3f;
+ when(followerDpc.brightnessSetting.getBrightness()).thenReturn(initialFollowerBrightness);
+ followerListener.onBrightnessChanged(initialFollowerBrightness);
+ advanceTime(1);
+ verify(followerDpc.animator).animateTo(eq(initialFollowerBrightness),
+ anyFloat(), anyFloat());
+
+
+ mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
+ mHolder.dpc.addDisplayBrightnessFollower(secondFollowerDpc.dpc);
+ clearInvocations(followerDpc.animator);
+
+ // Validate both followers are correctly registered and receiving brightness updates
+ float brightness = 0.6f;
+ float nits = 600;
+ when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
+ when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+ .thenReturn(brightness);
+ when(secondFollowerDpc.automaticBrightnessController.convertToFloatScale(nits))
+ .thenReturn(brightness);
+ when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(secondFollowerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+
+ clearInvocations(mHolder.animator, followerDpc.animator, secondFollowerDpc.animator);
+
+ // Remove the first follower and validate it goes back to its original brightness.
+ mHolder.dpc.removeDisplayBrightnessFollower(followerDpc.dpc);
+ advanceTime(1);
+ verify(followerDpc.animator).animateTo(eq(initialFollowerBrightness),
+ anyFloat(), anyFloat());
+ clearInvocations(followerDpc.animator);
+
+ // Change the brightness of the lead display and validate only the second follower responds
+ brightness = 0.7f;
+ nits = 700;
+ when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
+ when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+ .thenReturn(brightness);
+ when(secondFollowerDpc.automaticBrightnessController.convertToFloatScale(nits))
+ .thenReturn(brightness);
+ when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(followerDpc.animator, never()).animateTo(anyFloat(), anyFloat(), anyFloat());
+ verify(secondFollowerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ }
+
+ private DisplayPowerControllerHolder createDisplayPowerController(int displayId,
+ String uniqueId) {
+ final DisplayPowerState displayPowerState = mock(DisplayPowerState.class);
+ final DualRampAnimator<DisplayPowerState> animator = mock(DualRampAnimator.class);
+ final AutomaticBrightnessController automaticBrightnessController =
+ mock(AutomaticBrightnessController.class);
+ final WakelockController wakelockController = mock(WakelockController.class);
+ final BrightnessMappingStrategy brightnessMappingStrategy =
+ mock(BrightnessMappingStrategy.class);
+ final HysteresisLevels hysteresisLevels = mock(HysteresisLevels.class);
+
+ TestInjector injector = new TestInjector(displayPowerState, animator,
+ automaticBrightnessController, wakelockController, brightnessMappingStrategy,
+ hysteresisLevels);
+
+ final LogicalDisplay display = mock(LogicalDisplay.class);
+ final DisplayDevice device = mock(DisplayDevice.class);
+ final HighBrightnessModeMetadata hbmMetadata = mock(HighBrightnessModeMetadata.class);
+ final BrightnessSetting brightnessSetting = mock(BrightnessSetting.class);
+ final DisplayDeviceConfig config = mock(DisplayDeviceConfig.class);
+
+ setUpDisplay(displayId, uniqueId, display, device, config);
+
+ final DisplayPowerController2 dpc = new DisplayPowerController2(
+ mContextSpy, injector, mDisplayPowerCallbacksMock, mHandler,
+ mSensorManagerMock, mDisplayBlankerMock, display,
+ mBrightnessTrackerMock, brightnessSetting, () -> {},
+ hbmMetadata);
+
+ return new DisplayPowerControllerHolder(dpc, displayPowerState, brightnessSetting, animator,
+ automaticBrightnessController, wakelockController);
+ }
+
+ /**
+ * A class for holding a DisplayPowerController under test and all the mocks specifically
+ * related to it.
+ */
+ private static class DisplayPowerControllerHolder {
+ public final DisplayPowerController2 dpc;
+ public final DisplayPowerState displayPowerState;
+ public final BrightnessSetting brightnessSetting;
+ public final DualRampAnimator<DisplayPowerState> animator;
+ public final AutomaticBrightnessController automaticBrightnessController;
+ public final WakelockController wakelockController;
+
+ DisplayPowerControllerHolder(DisplayPowerController2 dpc,
+ DisplayPowerState displayPowerState, BrightnessSetting brightnessSetting,
+ DualRampAnimator<DisplayPowerState> animator,
+ AutomaticBrightnessController automaticBrightnessController,
+ WakelockController wakelockController) {
+ this.dpc = dpc;
+ this.displayPowerState = displayPowerState;
+ this.brightnessSetting = brightnessSetting;
+ this.animator = animator;
+ this.automaticBrightnessController = automaticBrightnessController;
+ this.wakelockController = wakelockController;
+ }
+ }
+
+ private class TestInjector extends DisplayPowerController2.Injector {
+ private final DisplayPowerState mDisplayPowerState;
+ private final DualRampAnimator<DisplayPowerState> mAnimator;
+ private final AutomaticBrightnessController mAutomaticBrightnessController;
+ private final WakelockController mWakelockController;
+ private final BrightnessMappingStrategy mBrightnessMappingStrategy;
+ private final HysteresisLevels mHysteresisLevels;
+
+ TestInjector(DisplayPowerState dps, DualRampAnimator<DisplayPowerState> animator,
+ AutomaticBrightnessController automaticBrightnessController,
+ WakelockController wakelockController,
+ BrightnessMappingStrategy brightnessMappingStrategy,
+ HysteresisLevels hysteresisLevels) {
+ mDisplayPowerState = dps;
+ mAnimator = animator;
+ mAutomaticBrightnessController = automaticBrightnessController;
+ mWakelockController = wakelockController;
+ mBrightnessMappingStrategy = brightnessMappingStrategy;
+ mHysteresisLevels = hysteresisLevels;
+ }
+
+ @Override
+ DisplayPowerController2.Clock getClock() {
+ return mClock::now;
+ }
+
+ @Override
+ DisplayPowerState getDisplayPowerState(DisplayBlanker blanker, ColorFade colorFade,
+ int displayId, int displayState) {
+ return mDisplayPowerState;
+ }
+
+ @Override
+ DualRampAnimator<DisplayPowerState> getDualRampAnimator(DisplayPowerState dps,
+ FloatProperty<DisplayPowerState> firstProperty,
+ FloatProperty<DisplayPowerState> secondProperty) {
+ return mAnimator;
+ }
+
+ @Override
+ WakelockController getWakelockController(int displayId,
+ DisplayPowerCallbacks displayPowerCallbacks) {
+ return mWakelockController;
+ }
+
+ @Override
+ DisplayPowerProximityStateController getDisplayPowerProximityStateController(
+ WakelockController wakelockController, DisplayDeviceConfig displayDeviceConfig,
+ Looper looper, Runnable nudgeUpdatePowerState, int displayId,
+ SensorManager sensorManager) {
+ return new DisplayPowerProximityStateController(wakelockController,
+ displayDeviceConfig, looper, nudgeUpdatePowerState, displayId,
+ sensorManager, /* injector= */ null);
+ }
+
+ @Override
+ AutomaticBrightnessController getAutomaticBrightnessController(
+ AutomaticBrightnessController.Callbacks callbacks, Looper looper,
+ SensorManager sensorManager, Sensor lightSensor,
+ BrightnessMappingStrategy interactiveModeBrightnessMapper,
+ int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
+ float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
+ long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+ boolean resetAmbientLuxAfterWarmUpConfig,
+ HysteresisLevels ambientBrightnessThresholds,
+ HysteresisLevels screenBrightnessThresholds,
+ HysteresisLevels ambientBrightnessThresholdsIdle,
+ HysteresisLevels screenBrightnessThresholdsIdle, Context context,
+ HighBrightnessModeController hbmController,
+ BrightnessThrottler brightnessThrottler,
+ BrightnessMappingStrategy idleModeBrightnessMapper,
+ int ambientLightHorizonShort, int ambientLightHorizonLong, float userLux,
+ float userBrightness) {
+ return mAutomaticBrightnessController;
+ }
+
+ @Override
+ BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
+ DisplayDeviceConfig displayDeviceConfig,
+ DisplayWhiteBalanceController displayWhiteBalanceController) {
+ return mBrightnessMappingStrategy;
+ }
+
+ @Override
+ HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+ float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+ float[] darkeningThresholdLevels, float minDarkeningThreshold,
+ float minBrighteningThreshold) {
+ return mHysteresisLevels;
+ }
+
+ @Override
+ HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+ float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+ float[] darkeningThresholdLevels, float minDarkeningThreshold,
+ float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
+ return mHysteresisLevels;
+ }
}
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java
index b6388cc..a8c3e4e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -16,14 +16,19 @@
package com.android.server.display;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
@@ -36,7 +41,9 @@
import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks;
import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
import android.os.Handler;
+import android.os.Looper;
import android.os.PowerManager;
+import android.os.SystemProperties;
import android.os.test.TestLooper;
import android.util.FloatProperty;
import android.view.Display;
@@ -46,8 +53,12 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.server.LocalServices;
+import com.android.server.am.BatteryStatsService;
import com.android.server.display.RampAnimator.DualRampAnimator;
+import com.android.server.display.color.ColorDisplayService;
+import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
import com.android.server.policy.WindowManagerPolicy;
import com.android.server.testutils.OffsettableClock;
@@ -58,7 +69,9 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+import org.mockito.stubbing.Answer;
import java.util.List;
@@ -66,14 +79,20 @@
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class DisplayPowerControllerTest {
- private static final String UNIQUE_DISPLAY_ID = "unique_id_test123";
- private static final int DISPLAY_ID = 42;
+ private static final int DISPLAY_ID = Display.DEFAULT_DISPLAY;
+ private static final String UNIQUE_ID = "unique_id_test123";
+ private static final int FOLLOWER_DISPLAY_ID = DISPLAY_ID + 1;
+ private static final String FOLLOWER_UNIQUE_DISPLAY_ID = "unique_id_456";
+ private static final int SECOND_FOLLOWER_DISPLAY_ID = FOLLOWER_DISPLAY_ID + 1;
+ private static final String SECOND_FOLLOWER_UNIQUE_DISPLAY_ID = "unique_id_789";
+ private MockitoSession mSession;
private OffsettableClock mClock;
private TestLooper mTestLooper;
private Handler mHandler;
- private DisplayPowerController.Injector mInjector;
private Context mContextSpy;
+ private DisplayPowerControllerHolder mHolder;
+ private Sensor mProxSensor;
@Mock
private DisplayPowerCallbacks mDisplayPowerCallbacksMock;
@@ -82,111 +101,107 @@
@Mock
private DisplayBlanker mDisplayBlankerMock;
@Mock
- private LogicalDisplay mLogicalDisplayMock;
- @Mock
- private DisplayDevice mDisplayDeviceMock;
- @Mock
- private HighBrightnessModeMetadata mHighBrightnessModeMetadataMock;
- @Mock
private BrightnessTracker mBrightnessTrackerMock;
@Mock
- private BrightnessSetting mBrightnessSettingMock;
- @Mock
private WindowManagerPolicy mWindowManagerPolicyMock;
@Mock
private PowerManager mPowerManagerMock;
@Mock
private Resources mResourcesMock;
@Mock
- private DisplayDeviceConfig mDisplayDeviceConfigMock;
- @Mock
- private DisplayPowerState mDisplayPowerStateMock;
- @Mock
- private DualRampAnimator<DisplayPowerState> mDualRampAnimatorMock;
+ private ColorDisplayService.ColorDisplayServiceInternal mCdsiMock;
@Captor
private ArgumentCaptor<SensorEventListener> mSensorEventListenerCaptor;
@Before
public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
+ mSession = ExtendedMockito.mockitoSession()
+ .initMocks(this)
+ .strictness(Strictness.LENIENT)
+ .spyStatic(SystemProperties.class)
+ .spyStatic(LocalServices.class)
+ .spyStatic(BatteryStatsService.class)
+ .startMocking();
mContextSpy = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
mClock = new OffsettableClock.Stopped();
mTestLooper = new TestLooper(mClock::now);
mHandler = new Handler(mTestLooper.getLooper());
- mInjector = new DisplayPowerController.Injector() {
- @Override
- DisplayPowerController.Clock getClock() {
- return mClock::now;
- }
-
- @Override
- DisplayPowerState getDisplayPowerState(DisplayBlanker blanker, ColorFade colorFade,
- int displayId, int displayState) {
- return mDisplayPowerStateMock;
- }
-
- @Override
- DualRampAnimator<DisplayPowerState> getDualRampAnimator(DisplayPowerState dps,
- FloatProperty<DisplayPowerState> firstProperty,
- FloatProperty<DisplayPowerState> secondProperty) {
- return mDualRampAnimatorMock;
- }
- };
addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock);
when(mContextSpy.getSystemService(eq(PowerManager.class))).thenReturn(mPowerManagerMock);
when(mContextSpy.getResources()).thenReturn(mResourcesMock);
+
+ doAnswer((Answer<Void>) invocationOnMock -> null).when(() ->
+ SystemProperties.set(anyString(), any()));
+ doAnswer((Answer<ColorDisplayService.ColorDisplayServiceInternal>) invocationOnMock ->
+ mCdsiMock).when(() -> LocalServices.getService(
+ ColorDisplayService.ColorDisplayServiceInternal.class));
+ doAnswer((Answer<Void>) invocationOnMock -> null).when(BatteryStatsService::getService);
+
+ mProxSensor = setUpProxSensor();
+
+ mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
}
@After
public void tearDown() {
+ mSession.finishMocking();
LocalServices.removeServiceForTest(WindowManagerPolicy.class);
}
@Test
public void testReleaseProxSuspendBlockersOnExit() throws Exception {
- setUpDisplay(DISPLAY_ID, UNIQUE_DISPLAY_ID);
-
- Sensor proxSensor = setUpProxSensor();
-
- DisplayPowerController dpc = new DisplayPowerController(
- mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
- mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
- mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
- }, mHighBrightnessModeMetadataMock);
-
- when(mDisplayPowerStateMock.getScreenState()).thenReturn(Display.STATE_ON);
+ when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
// send a display power request
DisplayPowerRequest dpr = new DisplayPowerRequest();
dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
dpr.useProximitySensor = true;
- dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
+ mHolder.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
// Run updatePowerState to start listener for the prox sensor
advanceTime(1);
- SensorEventListener listener = getSensorEventListener(proxSensor);
+ SensorEventListener listener = getSensorEventListener(mProxSensor);
assertNotNull(listener);
- listener.onSensorChanged(TestUtils.createSensorEvent(proxSensor, 5 /* lux */));
+ listener.onSensorChanged(TestUtils.createSensorEvent(mProxSensor, 5 /* lux */));
advanceTime(1);
// two times, one for unfinished business and one for proximity
verify(mDisplayPowerCallbacksMock).acquireSuspendBlocker(
- dpc.getSuspendBlockerUnfinishedBusinessId(DISPLAY_ID));
+ mHolder.dpc.getSuspendBlockerUnfinishedBusinessId(DISPLAY_ID));
verify(mDisplayPowerCallbacksMock).acquireSuspendBlocker(
- dpc.getSuspendBlockerProxDebounceId(DISPLAY_ID));
+ mHolder.dpc.getSuspendBlockerProxDebounceId(DISPLAY_ID));
- dpc.stop();
+ mHolder.dpc.stop();
advanceTime(1);
// two times, one for unfinished business and one for proximity
verify(mDisplayPowerCallbacksMock).releaseSuspendBlocker(
- dpc.getSuspendBlockerUnfinishedBusinessId(DISPLAY_ID));
+ mHolder.dpc.getSuspendBlockerUnfinishedBusinessId(DISPLAY_ID));
verify(mDisplayPowerCallbacksMock).releaseSuspendBlocker(
- dpc.getSuspendBlockerProxDebounceId(DISPLAY_ID));
+ mHolder.dpc.getSuspendBlockerProxDebounceId(DISPLAY_ID));
+ }
+
+ @Test
+ public void testProximitySensorListenerNotRegisteredForNonDefaultDisplay() {
+ DisplayPowerControllerHolder followerDpc =
+ createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_DISPLAY_ID);
+
+ when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+ // send a display power request
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
+ dpr.useProximitySensor = true;
+ followerDpc.dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
+
+ // Run updatePowerState
+ advanceTime(1);
+
+ verify(mSensorManagerMock, never()).registerListener(any(SensorEventListener.class),
+ eq(mProxSensor), anyInt(), any(Handler.class));
}
/**
@@ -216,56 +231,371 @@
return mSensorEventListenerCaptor.getValue();
}
- private void setUpDisplay(int displayId, String uniqueId) {
+ private void setUpDisplay(int displayId, String uniqueId, LogicalDisplay logicalDisplayMock,
+ DisplayDevice displayDeviceMock, DisplayDeviceConfig displayDeviceConfigMock) {
DisplayInfo info = new DisplayInfo();
DisplayDeviceInfo deviceInfo = new DisplayDeviceInfo();
- when(mLogicalDisplayMock.getDisplayIdLocked()).thenReturn(displayId);
- when(mLogicalDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(mDisplayDeviceMock);
- when(mLogicalDisplayMock.getDisplayInfoLocked()).thenReturn(info);
- when(mLogicalDisplayMock.isEnabledLocked()).thenReturn(true);
- when(mLogicalDisplayMock.isInTransitionLocked()).thenReturn(false);
- when(mDisplayDeviceMock.getDisplayDeviceInfoLocked()).thenReturn(deviceInfo);
- when(mDisplayDeviceMock.getUniqueId()).thenReturn(uniqueId);
- when(mDisplayDeviceMock.getDisplayDeviceConfig()).thenReturn(mDisplayDeviceConfigMock);
- when(mDisplayDeviceConfigMock.getProximitySensor()).thenReturn(
+ when(logicalDisplayMock.getDisplayIdLocked()).thenReturn(displayId);
+ when(logicalDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(displayDeviceMock);
+ when(logicalDisplayMock.getDisplayInfoLocked()).thenReturn(info);
+ when(logicalDisplayMock.isEnabledLocked()).thenReturn(true);
+ when(logicalDisplayMock.isInTransitionLocked()).thenReturn(false);
+ when(displayDeviceMock.getDisplayDeviceInfoLocked()).thenReturn(deviceInfo);
+ when(displayDeviceMock.getUniqueId()).thenReturn(uniqueId);
+ when(displayDeviceMock.getDisplayDeviceConfig()).thenReturn(displayDeviceConfigMock);
+ when(displayDeviceConfigMock.getProximitySensor()).thenReturn(
new DisplayDeviceConfig.SensorData() {
{
type = Sensor.STRING_TYPE_PROXIMITY;
name = null;
}
});
- when(mDisplayDeviceConfigMock.getNits()).thenReturn(new float[]{2, 500});
+ when(displayDeviceConfigMock.getNits()).thenReturn(new float[]{2, 500});
+ when(displayDeviceConfigMock.isAutoBrightnessAvailable()).thenReturn(true);
+ when(displayDeviceConfigMock.getAmbientLightSensor()).thenReturn(
+ new DisplayDeviceConfig.SensorData());
+ when(displayDeviceConfigMock.getScreenOffBrightnessSensor()).thenReturn(
+ new DisplayDeviceConfig.SensorData());
}
@Test
- public void testDisplayBrightnessFollowers() {
- setUpDisplay(DISPLAY_ID, UNIQUE_DISPLAY_ID);
+ public void testDisplayBrightnessFollowers_BothDpcsSupportNits() {
+ DisplayPowerControllerHolder followerDpc =
+ createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_DISPLAY_ID);
- DisplayPowerController defaultDpc = new DisplayPowerController(
- mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
- mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
- mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
- }, mHighBrightnessModeMetadataMock);
- DisplayPowerController followerDpc = new DisplayPowerController(
- mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
- mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
- mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
- }, mHighBrightnessModeMetadataMock);
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
- defaultDpc.addDisplayBrightnessFollower(followerDpc);
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
- defaultDpc.setBrightness(0.3f);
- assertEquals(defaultDpc.getBrightnessInfo().brightness,
- followerDpc.getBrightnessInfo().brightness, 0);
+ mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
- defaultDpc.setBrightness(0.6f);
- assertEquals(defaultDpc.getBrightnessInfo().brightness,
- followerDpc.getBrightnessInfo().brightness, 0);
+ // Test different float scale values
+ float leadBrightness = 0.3f;
+ float followerBrightness = 0.4f;
+ float nits = 300;
+ when(mHolder.automaticBrightnessController.convertToNits(leadBrightness)).thenReturn(nits);
+ when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+ .thenReturn(followerBrightness);
+ when(mHolder.brightnessSetting.getBrightness()).thenReturn(leadBrightness);
+ listener.onBrightnessChanged(leadBrightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(), anyFloat());
+ verify(followerDpc.animator).animateTo(eq(followerBrightness), anyFloat(),
+ anyFloat());
- float brightness = 0.1f;
- defaultDpc.clearDisplayBrightnessFollowers();
- defaultDpc.setBrightness(brightness);
- assertNotEquals(brightness, followerDpc.getBrightnessInfo().brightness, 0);
+ clearInvocations(mHolder.animator, followerDpc.animator);
+
+ // Test the same float scale value
+ float brightness = 0.6f;
+ nits = 600;
+ when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
+ when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+ .thenReturn(brightness);
+ when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ }
+
+ @Test
+ public void testDisplayBrightnessFollowers_FollowerDoesNotSupportNits() {
+ DisplayPowerControllerHolder followerDpc =
+ createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_DISPLAY_ID);
+
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+ mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
+
+ float brightness = 0.3f;
+ when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(300f);
+ when(followerDpc.automaticBrightnessController.convertToFloatScale(anyFloat()))
+ .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
+ when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ }
+
+ @Test
+ public void testDisplayBrightnessFollowers_LeadDpcDoesNotSupportNits() {
+ DisplayPowerControllerHolder followerDpc =
+ createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_DISPLAY_ID);
+
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+ mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
+
+ float brightness = 0.3f;
+ when(mHolder.automaticBrightnessController.convertToNits(anyFloat())).thenReturn(-1f);
+ when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ }
+
+ @Test
+ public void testDisplayBrightnessFollowers_NeitherDpcSupportsNits() {
+ DisplayPowerControllerHolder followerDpc =
+ createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_DISPLAY_ID);
+
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+ mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
+
+ float brightness = 0.3f;
+ when(mHolder.automaticBrightnessController.convertToNits(anyFloat())).thenReturn(-1f);
+ when(followerDpc.automaticBrightnessController.convertToFloatScale(anyFloat()))
+ .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
+ when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ }
+
+ @Test
+ public void testDisplayBrightnessFollowersRemoval() {
+ DisplayPowerControllerHolder followerHolder =
+ createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_DISPLAY_ID);
+ DisplayPowerControllerHolder secondFollowerHolder =
+ createDisplayPowerController(SECOND_FOLLOWER_DISPLAY_ID,
+ SECOND_FOLLOWER_UNIQUE_DISPLAY_ID);
+
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ followerHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ secondFollowerHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+ // Set the initial brightness on the DPC we're going to remove so we have a fixed value for
+ // it to return to.
+ listenerCaptor = ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(followerHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener followerListener = listenerCaptor.getValue();
+ final float initialFollowerBrightness = 0.3f;
+ when(followerHolder.brightnessSetting.getBrightness()).thenReturn(
+ initialFollowerBrightness);
+ followerListener.onBrightnessChanged(initialFollowerBrightness);
+ advanceTime(1);
+ verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness),
+ anyFloat(), anyFloat());
+
+
+ mHolder.dpc.addDisplayBrightnessFollower(followerHolder.dpc);
+ mHolder.dpc.addDisplayBrightnessFollower(secondFollowerHolder.dpc);
+ clearInvocations(followerHolder.animator);
+
+ // Validate both followers are correctly registered and receiving brightness updates
+ float brightness = 0.6f;
+ float nits = 600;
+ when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
+ when(followerHolder.automaticBrightnessController.convertToFloatScale(nits))
+ .thenReturn(brightness);
+ when(secondFollowerHolder.automaticBrightnessController.convertToFloatScale(nits))
+ .thenReturn(brightness);
+ when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(followerHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(secondFollowerHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+
+ clearInvocations(mHolder.animator, followerHolder.animator, secondFollowerHolder.animator);
+
+ // Remove the first follower and validate it goes back to its original brightness.
+ mHolder.dpc.removeDisplayBrightnessFollower(followerHolder.dpc);
+ advanceTime(1);
+ verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness),
+ anyFloat(), anyFloat());
+ clearInvocations(followerHolder.animator);
+
+ // Change the brightness of the lead display and validate only the second follower responds
+ brightness = 0.7f;
+ nits = 700;
+ when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
+ when(followerHolder.automaticBrightnessController.convertToFloatScale(nits))
+ .thenReturn(brightness);
+ when(secondFollowerHolder.automaticBrightnessController.convertToFloatScale(nits))
+ .thenReturn(brightness);
+ when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(followerHolder.animator, never()).animateTo(anyFloat(), anyFloat(), anyFloat());
+ verify(secondFollowerHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+ }
+
+ private DisplayPowerControllerHolder createDisplayPowerController(int displayId,
+ String uniqueId) {
+ final DisplayPowerState displayPowerState = mock(DisplayPowerState.class);
+ final DualRampAnimator<DisplayPowerState> animator = mock(DualRampAnimator.class);
+ final AutomaticBrightnessController automaticBrightnessController =
+ mock(AutomaticBrightnessController.class);
+ final BrightnessMappingStrategy brightnessMappingStrategy =
+ mock(BrightnessMappingStrategy.class);
+ final HysteresisLevels hysteresisLevels = mock(HysteresisLevels.class);
+
+ DisplayPowerController.Injector injector = new TestInjector(displayPowerState, animator,
+ automaticBrightnessController, brightnessMappingStrategy, hysteresisLevels);
+
+ final LogicalDisplay display = mock(LogicalDisplay.class);
+ final DisplayDevice device = mock(DisplayDevice.class);
+ final HighBrightnessModeMetadata hbmMetadata = mock(HighBrightnessModeMetadata.class);
+ final BrightnessSetting brightnessSetting = mock(BrightnessSetting.class);
+ final DisplayDeviceConfig config = mock(DisplayDeviceConfig.class);
+
+ setUpDisplay(displayId, uniqueId, display, device, config);
+
+ final DisplayPowerController dpc = new DisplayPowerController(
+ mContextSpy, injector, mDisplayPowerCallbacksMock, mHandler,
+ mSensorManagerMock, mDisplayBlankerMock, display,
+ mBrightnessTrackerMock, brightnessSetting, () -> {},
+ hbmMetadata);
+
+ return new DisplayPowerControllerHolder(dpc, displayPowerState, brightnessSetting, animator,
+ automaticBrightnessController);
+ }
+
+ /**
+ * A class for holding a DisplayPowerController under test and all the mocks specifically
+ * related to it.
+ */
+ private static class DisplayPowerControllerHolder {
+ public final DisplayPowerController dpc;
+ public final DisplayPowerState displayPowerState;
+ public final BrightnessSetting brightnessSetting;
+ public final DualRampAnimator<DisplayPowerState> animator;
+ public final AutomaticBrightnessController automaticBrightnessController;
+
+ DisplayPowerControllerHolder(DisplayPowerController dpc,
+ DisplayPowerState displayPowerState, BrightnessSetting brightnessSetting,
+ DualRampAnimator<DisplayPowerState> animator,
+ AutomaticBrightnessController automaticBrightnessController) {
+ this.dpc = dpc;
+ this.displayPowerState = displayPowerState;
+ this.brightnessSetting = brightnessSetting;
+ this.animator = animator;
+ this.automaticBrightnessController = automaticBrightnessController;
+ }
+ }
+
+ private class TestInjector extends DisplayPowerController.Injector {
+ private final DisplayPowerState mDisplayPowerState;
+ private final DualRampAnimator<DisplayPowerState> mAnimator;
+ private final AutomaticBrightnessController mAutomaticBrightnessController;
+ private final BrightnessMappingStrategy mBrightnessMappingStrategy;
+ private final HysteresisLevels mHysteresisLevels;
+
+ TestInjector(DisplayPowerState dps, DualRampAnimator<DisplayPowerState> animator,
+ AutomaticBrightnessController automaticBrightnessController,
+ BrightnessMappingStrategy brightnessMappingStrategy,
+ HysteresisLevels hysteresisLevels) {
+ mDisplayPowerState = dps;
+ mAnimator = animator;
+ mAutomaticBrightnessController = automaticBrightnessController;
+ mBrightnessMappingStrategy = brightnessMappingStrategy;
+ mHysteresisLevels = hysteresisLevels;
+ }
+
+ @Override
+ DisplayPowerController.Clock getClock() {
+ return mClock::now;
+ }
+
+ @Override
+ DisplayPowerState getDisplayPowerState(DisplayBlanker blanker, ColorFade colorFade,
+ int displayId, int displayState) {
+ return mDisplayPowerState;
+ }
+
+ @Override
+ DualRampAnimator<DisplayPowerState> getDualRampAnimator(DisplayPowerState dps,
+ FloatProperty<DisplayPowerState> firstProperty,
+ FloatProperty<DisplayPowerState> secondProperty) {
+ return mAnimator;
+ }
+
+ @Override
+ AutomaticBrightnessController getAutomaticBrightnessController(
+ AutomaticBrightnessController.Callbacks callbacks, Looper looper,
+ SensorManager sensorManager, Sensor lightSensor,
+ BrightnessMappingStrategy interactiveModeBrightnessMapper,
+ int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
+ float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
+ long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+ boolean resetAmbientLuxAfterWarmUpConfig,
+ HysteresisLevels ambientBrightnessThresholds,
+ HysteresisLevels screenBrightnessThresholds,
+ HysteresisLevels ambientBrightnessThresholdsIdle,
+ HysteresisLevels screenBrightnessThresholdsIdle, Context context,
+ HighBrightnessModeController hbmController,
+ BrightnessThrottler brightnessThrottler,
+ BrightnessMappingStrategy idleModeBrightnessMapper,
+ int ambientLightHorizonShort, int ambientLightHorizonLong, float userLux,
+ float userBrightness) {
+ return mAutomaticBrightnessController;
+ }
+
+ @Override
+ BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
+ DisplayDeviceConfig displayDeviceConfig,
+ DisplayWhiteBalanceController displayWhiteBalanceController) {
+ return mBrightnessMappingStrategy;
+ }
+
+ @Override
+ HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+ float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+ float[] darkeningThresholdLevels, float minDarkeningThreshold,
+ float minBrighteningThreshold) {
+ return mHysteresisLevels;
+ }
+
+ @Override
+ HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+ float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+ float[] darkeningThresholdLevels, float minDarkeningThreshold,
+ float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
+ return mHysteresisLevels;
+ }
}
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
index ed78e72..c7bacbc 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
@@ -32,6 +32,8 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
@@ -470,6 +472,50 @@
}
/**
+ * Confirm that
+ * returns {@code null} when for user-visible jobs stopped by the user.
+ */
+ @Test
+ public void testGetRescheduleJobForFailure_userStopped() {
+ JobStatus uiJob = createJobStatus("testGetRescheduleJobForFailure",
+ createJobInfo().setUserInitiated(true));
+ JobStatus uvJob = createJobStatus("testGetRescheduleJobForFailure", createJobInfo());
+ spyOn(uvJob);
+ doReturn(true).when(uvJob).isUserVisibleJob();
+ JobStatus regJob = createJobStatus("testGetRescheduleJobForFailure", createJobInfo());
+
+ // Reschedule for a non-user reason
+ JobStatus rescheduledUiJob = mService.getRescheduleJobForFailureLocked(uiJob,
+ JobParameters.STOP_REASON_DEVICE_STATE,
+ JobParameters.INTERNAL_STOP_REASON_DEVICE_THERMAL);
+ JobStatus rescheduledUvJob = mService.getRescheduleJobForFailureLocked(uvJob,
+ JobParameters.STOP_REASON_DEVICE_STATE,
+ JobParameters.INTERNAL_STOP_REASON_DEVICE_THERMAL);
+ JobStatus rescheduledRegJob = mService.getRescheduleJobForFailureLocked(regJob,
+ JobParameters.STOP_REASON_DEVICE_STATE,
+ JobParameters.INTERNAL_STOP_REASON_DEVICE_THERMAL);
+ assertNotNull(rescheduledUiJob);
+ assertNotNull(rescheduledUvJob);
+ assertNotNull(rescheduledRegJob);
+
+ // Reschedule for a user reason. The user-visible jobs shouldn't be rescheduled.
+ spyOn(rescheduledUvJob);
+ doReturn(true).when(rescheduledUvJob).isUserVisibleJob();
+ rescheduledUiJob = mService.getRescheduleJobForFailureLocked(rescheduledUiJob,
+ JobParameters.STOP_REASON_USER,
+ JobParameters.INTERNAL_STOP_REASON_USER_UI_STOP);
+ rescheduledUvJob = mService.getRescheduleJobForFailureLocked(rescheduledUvJob,
+ JobParameters.STOP_REASON_USER,
+ JobParameters.INTERNAL_STOP_REASON_USER_UI_STOP);
+ rescheduledRegJob = mService.getRescheduleJobForFailureLocked(rescheduledRegJob,
+ JobParameters.STOP_REASON_USER,
+ JobParameters.INTERNAL_STOP_REASON_USER_UI_STOP);
+ assertNull(rescheduledUiJob);
+ assertNull(rescheduledUvJob);
+ assertNotNull(rescheduledRegJob);
+ }
+
+ /**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with the correct delay and deadline constraints if the periodic job is scheduled with the
* minimum possible period.
@@ -1274,14 +1320,14 @@
mService.getJobStore().add(job2a);
mService.getJobStore().add(job2b);
- mService.stopUserVisibleJobsInternal("pkg1", 1);
+ mService.notePendingUserRequestedAppStopInternal("pkg1", 1, "test");
assertEquals(4, mService.getPendingJobQueue().size());
assertTrue(mService.getPendingJobQueue().contains(job1a));
assertTrue(mService.getPendingJobQueue().contains(job1b));
assertTrue(mService.getPendingJobQueue().contains(job2a));
assertTrue(mService.getPendingJobQueue().contains(job2b));
- mService.stopUserVisibleJobsInternal("pkg1", 0);
+ mService.notePendingUserRequestedAppStopInternal("pkg1", 0, "test");
assertEquals(2, mService.getPendingJobQueue().size());
assertFalse(mService.getPendingJobQueue().contains(job1a));
assertEquals(JobScheduler.PENDING_JOB_REASON_USER, mService.getPendingJobReason(job1a));
@@ -1290,7 +1336,7 @@
assertTrue(mService.getPendingJobQueue().contains(job2a));
assertTrue(mService.getPendingJobQueue().contains(job2b));
- mService.stopUserVisibleJobsInternal("pkg2", 0);
+ mService.notePendingUserRequestedAppStopInternal("pkg2", 0, "test");
assertEquals(0, mService.getPendingJobQueue().size());
assertFalse(mService.getPendingJobQueue().contains(job1a));
assertFalse(mService.getPendingJobQueue().contains(job1b));
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
index 2c47fd9..e6bc72f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
@@ -225,6 +225,33 @@
}
@Test
+ public void testIsUserVisibleJob() {
+ JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar"))
+ .setUserInitiated(false)
+ .build();
+ JobStatus job = createJobStatus(jobInfo);
+
+ assertFalse(job.isUserVisibleJob());
+
+ // User-initiated jobs are always user-visible unless they've been demoted.
+ jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar"))
+ .setUserInitiated(true)
+ .build();
+ job = createJobStatus(jobInfo);
+
+ assertTrue(job.isUserVisibleJob());
+
+ job.addInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER);
+ assertFalse(job.isUserVisibleJob());
+
+ job.startedAsUserInitiatedJob = true;
+ assertTrue(job.isUserVisibleJob());
+
+ job.startedAsUserInitiatedJob = false;
+ assertFalse(job.isUserVisibleJob());
+ }
+
+ @Test
public void testMediaBackupExemption_lateConstraint() {
final JobInfo triggerContentJob = new JobInfo.Builder(42, TEST_JOB_COMPONENT)
.addTriggerContentUri(new JobInfo.TriggerContentUri(IMAGES_MEDIA_URI, 0))
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
index d03d196..1ed2f78 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -20,6 +20,7 @@
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
@@ -316,21 +317,10 @@
}
@Test
- public void testGetBootUser_Headless_UserCreatedIfOnlySystemUserExists() throws Exception {
+ public void testGetBootUser_Headless_ThrowsIfOnlySystemUserExists() throws Exception {
setSystemUserHeadless(true);
- int bootUser = mUmi.getBootUser();
-
- assertWithMessage("getStartingUser")
- .that(bootUser).isNotEqualTo(UserHandle.USER_SYSTEM);
-
- UserData newUser = mUsers.get(bootUser);
- assertWithMessage("New boot user is a full user")
- .that(newUser.info.isFull()).isTrue();
- assertWithMessage("New boot user is an admin user")
- .that(newUser.info.isAdmin()).isTrue();
- assertWithMessage("New boot user is the main user")
- .that(newUser.info.isMain()).isTrue();
+ assertThrows(UserManager.CheckedUserOperationException.class, () -> mUmi.getBootUser());
}
private void mockCurrentUser(@UserIdInt int userId) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUPANDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUPANDTest.java
index f69054b..38cf634 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUPANDTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUPANDTest.java
@@ -20,6 +20,7 @@
import static android.view.Display.INVALID_DISPLAY;
import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE;
+import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_ALREADY_VISIBLE;
import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE;
import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE;
import static com.android.server.pm.UserVisibilityChangedEvent.onInvisible;
@@ -74,8 +75,8 @@
assertUserCanBeAssignedExtraDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID);
// Make sure another user cannot be started on default display
- int result2 = mMediator.assignUserToDisplayOnStart(otherUserId, otherUserId, BG_VISIBLE,
- DEFAULT_DISPLAY);
+ int result2 = mMediator.assignUserToDisplayOnStart(otherUserId, visibleBgUserId,
+ BG_VISIBLE, DEFAULT_DISPLAY);
assertStartUserResult(result2, USER_ASSIGNMENT_RESULT_FAILURE,
"when user (%d) is starting on default display after it was started by user %d",
otherUserId, visibleBgUserId);
@@ -118,8 +119,8 @@
assertUserCanBeAssignedExtraDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID);
// Make sure another user cannot be started on default display
- int result2 = mMediator.assignUserToDisplayOnStart(otherUserId, otherUserId, BG_VISIBLE,
- DEFAULT_DISPLAY);
+ int result2 = mMediator.assignUserToDisplayOnStart(otherUserId, visibleBgUserId,
+ BG_VISIBLE, DEFAULT_DISPLAY);
assertStartUserResult(result2, USER_ASSIGNMENT_RESULT_FAILURE,
"when user (%d) is starting on default display after it was started by user %d",
otherUserId, visibleBgUserId);
@@ -127,9 +128,31 @@
listener.verify();
}
+ /* TODO: re-add
@Test
- public void testStartVisibleBgProfile_onDefaultDisplay_whenParentIsStartedVisibleOnBg()
+ public void
+ testStartVisibleBgProfile_onDefaultDisplay_whenParentIsStartedVisibleOnBgOnSecondaryDisplay()
+ throws Exception {
+ AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(PARENT_USER_ID));
+ startUserInSecondaryDisplay(PARENT_USER_ID, OTHER_SECONDARY_DISPLAY_ID);
+
+ int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
+ BG_VISIBLE, DEFAULT_DISPLAY);
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+
+ expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
+ expectNoDisplayAssignedToUser(PROFILE_USER_ID);
+ expectUserAssignedToDisplay(OTHER_SECONDARY_DISPLAY_ID, PARENT_USER_ID);
+
+ assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
+
+ listener.verify();
+ }
+
+ @Test
+ public void
+ testStartVisibleBgProfile_onDefaultDisplay_whenParentIsStartedVisibleOnBgOnDefaultDisplay()
throws Exception {
AsyncUserVisibilityListener listener = addListenerForEvents(
onVisible(PARENT_USER_ID),
@@ -183,4 +206,26 @@
assertUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, OTHER_SECONDARY_DISPLAY_ID);
listener.verify();
}
+
+ @Test
+ public void testStartVisibleBgUser_onDefaultDisplay_currentUserId() throws Exception {
+ int currentUserId = INITIAL_CURRENT_USER_ID;
+
+ AsyncUserVisibilityListener listener = addListenerForNoEvents();
+
+ int result = mMediator.assignUserToDisplayOnStart(currentUserId, currentUserId,
+ BG_VISIBLE, DEFAULT_DISPLAY);
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_ALREADY_VISIBLE);
+
+ // Assert current user visibility
+ expectUserIsVisible(currentUserId);
+ expectUserIsVisibleOnDisplay(currentUserId, DEFAULT_DISPLAY);
+ expectUserIsNotVisibleOnDisplay(currentUserId, INVALID_DISPLAY);
+ expectDisplayAssignedToUser(currentUserId, DEFAULT_DISPLAY);
+
+ assertUserCanBeAssignedExtraDisplay(currentUserId, OTHER_SECONDARY_DISPLAY_ID);
+
+ listener.verify();
+ }
+ */
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
index 566084a..5176d68 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
@@ -44,6 +44,7 @@
import android.util.IntArray;
import android.util.Log;
+import com.android.internal.util.Preconditions;
import com.android.server.ExtendedMockitoTestCase;
import org.junit.Before;
@@ -148,12 +149,6 @@
}
@Test
- public final void testAssignUserToDisplayOnStart_invalidUserStartMode() {
- assertThrows(IllegalArgumentException.class, () -> mMediator
- .assignUserToDisplayOnStart(USER_ID, USER_ID, 666, DEFAULT_DISPLAY));
- }
-
- @Test
public final void testStartFgUser_onSecondaryDisplay() throws Exception {
AsyncUserVisibilityListener listener = addListenerForNoEvents();
@@ -288,7 +283,7 @@
int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
BG_VISIBLE, DEFAULT_DISPLAY);
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE);
expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
expectNoDisplayAssignedToUser(PROFILE_USER_ID);
@@ -304,14 +299,14 @@
int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
BG_VISIBLE, DEFAULT_DISPLAY);
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE);
expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
expectNoDisplayAssignedToUser(PROFILE_USER_ID);
expectInitialCurrentUserAssignedToDisplay(DEFAULT_DISPLAY);
- assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
+ assertUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
listener.verify();
}
@@ -337,41 +332,6 @@
}
@Test
- public final void testStartBgProfile_onDefaultDisplay_whenParentIsNotStarted()
- throws Exception {
- AsyncUserVisibilityListener listener = addListenerForNoEvents();
-
- int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG,
- DEFAULT_DISPLAY);
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE);
-
- expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
- expectNoDisplayAssignedToUser(PROFILE_USER_ID);
-
- listener.verify();
- }
-
- @Test
- public final void testStartBgProfile_onDefaultDisplay_whenParentIsStartedOnBg()
- throws Exception {
- AsyncUserVisibilityListener listener = addListenerForNoEvents();
- startBackgroundUser(PARENT_USER_ID);
-
- int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG,
- DEFAULT_DISPLAY);
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE);
-
- expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
-
- expectNoDisplayAssignedToUser(PROFILE_USER_ID);
- expectInitialCurrentUserAssignedToDisplay(DEFAULT_DISPLAY);
-
- assertUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
-
- listener.verify();
- }
-
- @Test
public final void testStartBgProfile_onSecondaryDisplay() throws Exception {
AsyncUserVisibilityListener listener = addListenerForNoEvents();
@@ -525,6 +485,8 @@
* se.
*/
protected final void startUserInSecondaryDisplay(@UserIdInt int userId, int displayId) {
+ Preconditions.checkArgument(displayId != INVALID_DISPLAY && displayId != DEFAULT_DISPLAY,
+ "must pass a secondary display, not %d", displayId);
Log.d(TAG, "startUserInSecondaryDisplay(" + userId + ", " + displayId + ")");
int result = mMediator.assignUserToDisplayOnStart(userId, userId, BG_VISIBLE, displayId);
if (result != USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorVisibleBackgroundUserTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorVisibleBackgroundUserTestCase.java
index f084063..49c6a88 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorVisibleBackgroundUserTestCase.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorVisibleBackgroundUserTestCase.java
@@ -108,6 +108,34 @@
}
@Test
+ public final void testStartVisibleBgProfile_onDefaultDisplay_whenParentIsCurrentUser()
+ throws Exception {
+ AsyncUserVisibilityListener listener = addListenerForEvents(
+ onInvisible(INITIAL_CURRENT_USER_ID),
+ onVisible(PARENT_USER_ID),
+ onVisible(PROFILE_USER_ID));
+ startForegroundUser(PARENT_USER_ID);
+
+ int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
+ BG_VISIBLE, DEFAULT_DISPLAY);
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+ expectUserCannotBeUnassignedFromDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY);
+
+ expectUserIsVisible(PROFILE_USER_ID);
+ expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, INVALID_DISPLAY);
+ expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
+ expectUserIsVisibleOnDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY);
+ expectVisibleUsers(PARENT_USER_ID, PROFILE_USER_ID);
+
+ expectDisplayAssignedToUser(PROFILE_USER_ID, DEFAULT_DISPLAY);
+ expectUserAssignedToDisplay(DEFAULT_DISPLAY, PARENT_USER_ID);
+
+ assertUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
+
+ listener.verify();
+ }
+
+ @Test
public final void testStartFgUser_onInvalidDisplay() throws Exception {
AsyncUserVisibilityListener listener = addListenerForNoEvents();
@@ -240,83 +268,14 @@
}
@Test
- public final void testStartVisibleBgProfile_onDefaultDisplay_whenParentIsCurrentUser()
- throws Exception {
- AsyncUserVisibilityListener listener = addListenerForEvents(
- onInvisible(INITIAL_CURRENT_USER_ID),
- onVisible(PARENT_USER_ID),
- onVisible(PROFILE_USER_ID));
- startForegroundUser(PARENT_USER_ID);
-
- int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
- BG_VISIBLE, DEFAULT_DISPLAY);
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
- expectUserCannotBeUnassignedFromDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY);
-
- expectUserIsVisible(PROFILE_USER_ID);
- expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, INVALID_DISPLAY);
- expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
- expectUserIsVisibleOnDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY);
- expectVisibleUsers(PARENT_USER_ID, PROFILE_USER_ID);
-
- expectDisplayAssignedToUser(PROFILE_USER_ID, DEFAULT_DISPLAY);
- expectUserAssignedToDisplay(DEFAULT_DISPLAY, PARENT_USER_ID);
-
- assertUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
-
- listener.verify();
- }
-
- @Test
public final void
- testStartVisibleBgProfile_onDefaultDisplay_whenParentIsStartedVisibleOnAnotherDisplay()
- throws Exception {
+ testStartVisibleBgProfile_onDefaultDisplay_whenParentVisibleOnSecondaryDisplay()
+ throws Exception {
AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(PARENT_USER_ID));
startUserInSecondaryDisplay(PARENT_USER_ID, OTHER_SECONDARY_DISPLAY_ID);
int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
BG_VISIBLE, DEFAULT_DISPLAY);
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
-
- expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
- expectNoDisplayAssignedToUser(PROFILE_USER_ID);
- expectUserAssignedToDisplay(OTHER_SECONDARY_DISPLAY_ID, PARENT_USER_ID);
-
- assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
-
- listener.verify();
- }
-
- // Not supported - profiles can only be started on default display
- @Test
- public final void
- testStartVisibleBgProfile_onSecondaryDisplay_whenParentIsStartedVisibleOnThatDisplay()
- throws Exception {
- AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(PARENT_USER_ID));
- startUserInSecondaryDisplay(PARENT_USER_ID, OTHER_SECONDARY_DISPLAY_ID);
-
- int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
- BG_VISIBLE, DEFAULT_DISPLAY);
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
-
- expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
- expectNoDisplayAssignedToUser(PROFILE_USER_ID);
- expectUserAssignedToDisplay(OTHER_SECONDARY_DISPLAY_ID, PARENT_USER_ID);
-
- assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
-
- listener.verify();
- }
-
- @Test
- public final void
- testStartProfile_onDefaultDisplay_whenParentIsStartedVisibleOnSecondaryDisplay()
- throws Exception {
- AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(PARENT_USER_ID));
- startUserInSecondaryDisplay(PARENT_USER_ID, OTHER_SECONDARY_DISPLAY_ID);
-
- int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG,
- DEFAULT_DISPLAY);
assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE);
expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
index 9b48114..f8955ed 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
@@ -200,7 +200,6 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
-
ExtendedMockito.doAnswer(invocation -> {
int userId = (invocation.getArgument(0));
return getWallpaperTestDir(userId);
@@ -398,7 +397,8 @@
TypedXmlSerializer serializer = Xml.newBinarySerializer();
serializer.setOutput(new ByteArrayOutputStream(), StandardCharsets.UTF_8.name());
serializer.startDocument(StandardCharsets.UTF_8.name(), true);
- mService.writeWallpaperAttributes(serializer, "wp", systemWallpaperData);
+ mService.mWallpaperDataParser.writeWallpaperAttributes(
+ serializer, "wp", systemWallpaperData);
} catch (IOException e) {
fail("exception occurred while writing system wallpaper attributes");
}
@@ -409,7 +409,7 @@
systemWallpaperData.cropFile.getAbsolutePath());
try {
TypedXmlPullParser parser = Xml.newBinaryPullParser();
- mService.parseWallpaperAttributes(parser, shouldMatchSystem, true);
+ mService.mWallpaperDataParser.parseWallpaperAttributes(parser, shouldMatchSystem, true);
} catch (XmlPullParserException e) {
fail("exception occurred while parsing wallpaper");
}
diff --git a/services/tests/servicestests/src/com/android/server/BinaryTransparencyServiceTest.java b/services/tests/servicestests/src/com/android/server/BinaryTransparencyServiceTest.java
index 245db46..ae78dfe 100644
--- a/services/tests/servicestests/src/com/android/server/BinaryTransparencyServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/BinaryTransparencyServiceTest.java
@@ -40,7 +40,6 @@
import android.hardware.fingerprint.FingerprintSensorProperties;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
-import android.os.Bundle;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.SystemProperties;
@@ -166,36 +165,6 @@
}
@Test
- public void getApexInfo_postInitialize_returnsValidEntries() throws RemoteException {
- prepApexInfo();
- List result = mTestInterface.getApexInfo();
- Assert.assertNotNull("Apex info map should not be null", result);
- // TODO(265244016): When PackageManagerInternal is a mock, it's harder to keep the
- // `measurePackage` working in unit test. Disable it for now. We may need more refactoring
- // or cover this in integration tests.
- // Assert.assertFalse("Apex info map should not be empty", result.isEmpty());
- }
-
- @Test
- public void getApexInfo_postInitialize_returnsActualApexs()
- throws RemoteException, PackageManager.NameNotFoundException {
- prepApexInfo();
- List resultList = mTestInterface.getApexInfo();
-
- PackageManager pm = mContext.getPackageManager();
- Assert.assertNotNull(pm);
- List<Bundle> castedResult = (List<Bundle>) resultList;
- for (Bundle resultBundle : castedResult) {
- String packageName = resultBundle.getString(
- BinaryTransparencyService.BUNDLE_PACKAGE_NAME);
- Assert.assertNotNull("Package name for APEX should not be null", packageName);
- Assert.assertTrue(packageName + "is not an APEX!",
- resultBundle.getBoolean(
- BinaryTransparencyService.BUNDLE_PACKAGE_IS_APEX));
- }
- }
-
- @Test
public void testCollectBiometricProperties_disablesFeature() {
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_BIOMETRICS,
BinaryTransparencyService.KEY_ENABLE_BIOMETRIC_PROPERTY_VERIFICATION,
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
index d996e37..bf23d9d 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
@@ -19,6 +19,7 @@
import static android.accessibilityservice.MagnificationConfig.MAGNIFICATION_MODE_FULLSCREEN;
import static com.android.server.accessibility.magnification.FullScreenMagnificationController.MagnificationInfoChangedCallback;
+import static com.android.server.accessibility.magnification.MockWindowMagnificationConnection.TEST_DISPLAY;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -48,6 +49,9 @@
import android.graphics.Region;
import android.hardware.display.DisplayManagerInternal;
import android.os.Looper;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.test.mock.MockContentResolver;
import android.view.DisplayInfo;
import android.view.MagnificationSpec;
import android.view.accessibility.MagnificationAnimationCallback;
@@ -55,6 +59,7 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.android.internal.util.test.FakeSettingsProvider;
import com.android.server.LocalServices;
import com.android.server.accessibility.AccessibilityTraceManager;
import com.android.server.accessibility.test.MessageCapturingHandler;
@@ -71,6 +76,7 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.mockito.stubbing.Answer;
+import org.testng.Assert;
import java.util.Locale;
@@ -93,6 +99,7 @@
static final int DISPLAY_1 = 1;
static final int DISPLAY_COUNT = 2;
static final int INVALID_DISPLAY = 2;
+ private static final int CURRENT_USER_ID = UserHandle.USER_SYSTEM;
final FullScreenMagnificationController.ControllerContext mMockControllerCtx =
mock(FullScreenMagnificationController.ControllerContext.class);
@@ -105,8 +112,8 @@
MagnificationInfoChangedCallback.class);
private final MessageCapturingHandler mMessageCapturingHandler = new MessageCapturingHandler(
null);
- private final MagnificationScaleProvider mScaleProvider = mock(
- MagnificationScaleProvider.class);
+ private MagnificationScaleProvider mScaleProvider;
+ private MockContentResolver mResolver;
private final ArgumentCaptor<MagnificationConfig> mConfigCaptor = ArgumentCaptor.forClass(
MagnificationConfig.class);
@@ -129,6 +136,12 @@
when(mMockControllerCtx.getWindowManager()).thenReturn(mMockWindowManager);
when(mMockControllerCtx.getHandler()).thenReturn(mMessageCapturingHandler);
when(mMockControllerCtx.getAnimationDuration()).thenReturn(1000L);
+ mResolver = new MockContentResolver();
+ mResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+ when(mMockContext.getContentResolver()).thenReturn(mResolver);
+ Settings.Secure.putFloatForUser(mResolver,
+ Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 2.0f,
+ CURRENT_USER_ID);
initMockWindowManager();
final DisplayInfo displayInfo = new DisplayInfo();
@@ -137,6 +150,7 @@
LocalServices.removeServiceForTest(DisplayManagerInternal.class);
LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
+ mScaleProvider = new MagnificationScaleProvider(mMockContext);
mFullScreenMagnificationController = new FullScreenMagnificationController(
mMockControllerCtx, new Object(), mRequestObserver, mScaleProvider);
}
@@ -1168,6 +1182,20 @@
verify(mRequestObserver).onImeWindowVisibilityChanged(eq(DISPLAY_0), eq(true));
}
+ @Test
+ public void persistScale_setValueWhenScaleIsOne_nothingChanged() {
+ final float persistedScale =
+ mFullScreenMagnificationController.getPersistedScale(TEST_DISPLAY);
+
+ PointF pivotPoint = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER;
+ mFullScreenMagnificationController.setScale(DISPLAY_0, 1.0f, pivotPoint.x, pivotPoint.y,
+ false, SERVICE_ID_1);
+ mFullScreenMagnificationController.persistScale(TEST_DISPLAY);
+
+ Assert.assertEquals(mFullScreenMagnificationController.getPersistedScale(TEST_DISPLAY),
+ persistedScale);
+ }
+
private void setScaleToMagnifying() {
register(DISPLAY_0);
float scale = 2.0f;
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java
index 25ad2be..d841dfc 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java
@@ -274,6 +274,19 @@
}
@Test
+ public void persistScale_setValueWhenScaleIsOne_nothingChanged() {
+ mWindowMagnificationManager.setConnection(mMockConnection.getConnection());
+ final float persistedScale = mWindowMagnificationManager.getPersistedScale(TEST_DISPLAY);
+
+ mWindowMagnificationManager.setScale(TEST_DISPLAY, 1.0f);
+ mWindowMagnificationManager.persistScale(TEST_DISPLAY);
+
+ assertEquals(Settings.Secure.getFloatForUser(mResolver,
+ Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 0f,
+ CURRENT_USER_ID), persistedScale);
+ }
+
+ @Test
public void scaleSetterGetter_enabledOnTestDisplay_expectedValue() {
mWindowMagnificationManager.setConnection(mMockConnection.getConnection());
mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 2.0f, NaN, NaN);
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index b146c27..18d629f 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -412,7 +412,7 @@
mInjector.mHandler.clearAllRecordedMessages();
// Verify that continueUserSwitch worked as expected
continueAndCompleteUserSwitch(userState, oldUserId, newUserId);
- verify(mInjector, times(0)).dismissKeyguard(any(), anyString());
+ verify(mInjector, times(0)).dismissKeyguard(any());
verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen();
continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false);
verifySystemUserVisibilityChangesNeverNotified();
@@ -433,7 +433,7 @@
mInjector.mHandler.clearAllRecordedMessages();
// Verify that continueUserSwitch worked as expected
continueAndCompleteUserSwitch(userState, oldUserId, newUserId);
- verify(mInjector, times(1)).dismissKeyguard(any(), anyString());
+ verify(mInjector, times(1)).dismissKeyguard(any());
verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen();
continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false);
verifySystemUserVisibilityChangesNeverNotified();
@@ -1148,7 +1148,7 @@
}
@Override
- protected void dismissKeyguard(Runnable runnable, String reason) {
+ protected void dismissKeyguard(Runnable runnable) {
runnable.run();
}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
index 99f7905..e605a31 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
@@ -35,6 +35,7 @@
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.content.ComponentName;
+import android.hardware.biometrics.BiometricFingerprintConstants;
import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.common.ICancellationSignal;
import android.hardware.biometrics.common.OperationContext;
@@ -364,6 +365,16 @@
showHideOverlay(c -> c.onLockoutPermanent());
}
+ @Test
+ public void testPowerPressForwardsErrorMessage() throws RemoteException {
+ final FingerprintAuthenticationClient client = createClient();
+
+ client.onError(BiometricFingerprintConstants.FINGERPRINT_ERROR_VENDOR,
+ BiometricFingerprintConstants.BIOMETRIC_ERROR_POWER_PRESSED);
+
+ verify(mClientMonitorCallbackConverter).onError(anyInt(), anyInt(),
+ eq(BiometricFingerprintConstants.BIOMETRIC_ERROR_POWER_PRESSED), eq(0));
+ }
private void showHideOverlay(Consumer<FingerprintAuthenticationClient> block)
throws RemoteException {
final FingerprintAuthenticationClient client = createClient();
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
index 26524d7..a40d3fe 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
@@ -16,8 +16,6 @@
package com.android.server.biometrics.sensors.fingerprint.aidl;
-import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_POWER_PRESSED;
-
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -29,6 +27,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.hardware.biometrics.BiometricFingerprintConstants;
import android.hardware.biometrics.common.OperationContext;
import android.hardware.biometrics.fingerprint.ISession;
import android.hardware.biometrics.fingerprint.PointerContext;
@@ -276,11 +275,12 @@
@Test
public void testPowerPressForwardsAcquireMessage() throws RemoteException {
final FingerprintEnrollClient client = createClient();
- client.start(mCallback);
- client.onPowerPressed();
+
+ client.onAcquired(BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR,
+ BiometricFingerprintConstants.BIOMETRIC_ERROR_POWER_PRESSED);
verify(mClientMonitorCallbackConverter).onAcquired(anyInt(),
- eq(FINGERPRINT_ACQUIRED_POWER_PRESSED), anyInt());
+ eq(BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_POWER_PRESSED), eq(0));
}
private void showHideOverlay(Consumer<FingerprintEnrollClient> block)
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
index 82f6493..c952609 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
@@ -945,13 +945,15 @@
}
mListener = listener;
- mListener.onSupportedDeviceStatesChanged(mSupportedDeviceStates);
+ mListener.onSupportedDeviceStatesChanged(mSupportedDeviceStates,
+ SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED);
mListener.onStateChanged(mSupportedDeviceStates[0].getIdentifier());
}
public void notifySupportedDeviceStates(DeviceState[] supportedDeviceStates) {
mSupportedDeviceStates = supportedDeviceStates;
- mListener.onSupportedDeviceStatesChanged(supportedDeviceStates);
+ mListener.onSupportedDeviceStatesChanged(supportedDeviceStates,
+ SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED);
}
public void setState(int identifier) {
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateNotificationControllerTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateNotificationControllerTest.java
new file mode 100644
index 0000000..8196d6a
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateNotificationControllerTest.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.devicestate;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.platform.test.annotations.Presubmit;
+import android.util.SparseArray;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+
+/**
+ * Unit tests for {@link DeviceStateNotificationController}.
+ * <p/>
+ * Run with <code>atest com.android.server.devicestate.DeviceStateNotificationControllerTest</code>.
+ */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class DeviceStateNotificationControllerTest {
+
+ private static final int STATE_WITHOUT_NOTIFICATION = 1;
+ private static final int STATE_WITH_ACTIVE_NOTIFICATION = 2;
+ private static final int STATE_WITH_ACTIVE_AND_THERMAL_NOTIFICATION = 3;
+
+ private static final int VALID_APP_UID = 1000;
+ private static final int INVALID_APP_UID = 2000;
+ private static final String VALID_APP_NAME = "Valid app name";
+ private static final String INVALID_APP_NAME = "Invalid app name";
+ private static final String VALID_APP_LABEL = "Valid app label";
+
+ private static final String NAME_1 = "name1";
+ private static final String TITLE_1 = "title1";
+ private static final String CONTENT_1 = "content1:%1$s";
+ private static final String NAME_2 = "name2";
+ private static final String TITLE_2 = "title2";
+ private static final String CONTENT_2 = "content2:%1$s";
+ private static final String THERMAL_TITLE_2 = "thermal_title2";
+ private static final String THERMAL_CONTENT_2 = "thermal_content2";
+
+ private DeviceStateNotificationController mController;
+
+ private final ArgumentCaptor<Notification> mNotificationCaptor = ArgumentCaptor.forClass(
+ Notification.class);
+ private final NotificationManager mNotificationManager = mock(NotificationManager.class);
+
+ @Before
+ public void setup() throws Exception {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ Handler handler = mock(Handler.class);
+ PackageManager packageManager = mock(PackageManager.class);
+ Runnable cancelStateRunnable = mock(Runnable.class);
+ ApplicationInfo applicationInfo = mock(ApplicationInfo.class);
+
+ final SparseArray<DeviceStateNotificationController.NotificationInfo> notificationInfos =
+ new SparseArray<>();
+ notificationInfos.put(STATE_WITH_ACTIVE_NOTIFICATION,
+ new DeviceStateNotificationController.NotificationInfo(
+ NAME_1, TITLE_1, CONTENT_1,
+ "", ""));
+ notificationInfos.put(STATE_WITH_ACTIVE_AND_THERMAL_NOTIFICATION,
+ new DeviceStateNotificationController.NotificationInfo(
+ NAME_2, TITLE_2, CONTENT_2,
+ THERMAL_TITLE_2, THERMAL_CONTENT_2));
+
+ when(packageManager.getNameForUid(VALID_APP_UID)).thenReturn(VALID_APP_NAME);
+ when(packageManager.getNameForUid(INVALID_APP_UID)).thenReturn(INVALID_APP_NAME);
+ when(packageManager.getApplicationInfo(eq(VALID_APP_NAME), ArgumentMatchers.any()))
+ .thenReturn(applicationInfo);
+ when(packageManager.getApplicationInfo(eq(INVALID_APP_NAME), ArgumentMatchers.any()))
+ .thenThrow(new PackageManager.NameNotFoundException());
+ when(applicationInfo.loadLabel(eq(packageManager))).thenReturn(VALID_APP_LABEL);
+
+ mController = new DeviceStateNotificationController(
+ context, handler, cancelStateRunnable, notificationInfos,
+ packageManager, mNotificationManager);
+ }
+
+ @Test
+ public void test_activeNotification() {
+ mController.showStateActiveNotificationIfNeeded(
+ STATE_WITH_ACTIVE_NOTIFICATION, VALID_APP_UID);
+
+ // Verify that the notification manager is called with correct notification information.
+ verify(mNotificationManager).notify(
+ eq(DeviceStateNotificationController.NOTIFICATION_TAG),
+ eq(DeviceStateNotificationController.NOTIFICATION_ID),
+ mNotificationCaptor.capture());
+ Notification notification = mNotificationCaptor.getValue();
+ assertEquals(TITLE_1, notification.extras.getString(Notification.EXTRA_TITLE));
+ assertEquals(String.format(CONTENT_1, VALID_APP_LABEL),
+ notification.extras.getString(Notification.EXTRA_TEXT));
+ assertEquals(Notification.FLAG_ONGOING_EVENT,
+ notification.flags & Notification.FLAG_ONGOING_EVENT);
+
+ // Verify that the notification action is as expected.
+ Notification.Action[] actions = notification.actions;
+ assertEquals(1, actions.length);
+ Notification.Action action = actions[0];
+ assertEquals(DeviceStateNotificationController.INTENT_ACTION_CANCEL_STATE,
+ action.actionIntent.getIntent().getAction());
+
+ // Verify that the notification is properly canceled.
+ mController.cancelNotification(STATE_WITH_ACTIVE_NOTIFICATION);
+ verify(mNotificationManager).cancel(
+ DeviceStateNotificationController.NOTIFICATION_TAG,
+ DeviceStateNotificationController.NOTIFICATION_ID);
+ }
+
+ @Test
+ public void test_thermalNotification() {
+ // Verify that the active notification is created.
+ mController.showStateActiveNotificationIfNeeded(
+ STATE_WITH_ACTIVE_AND_THERMAL_NOTIFICATION, VALID_APP_UID);
+ verify(mNotificationManager).notify(
+ eq(DeviceStateNotificationController.NOTIFICATION_TAG),
+ eq(DeviceStateNotificationController.NOTIFICATION_ID),
+ mNotificationCaptor.capture());
+ Notification notification = mNotificationCaptor.getValue();
+ assertEquals(TITLE_2, notification.extras.getString(Notification.EXTRA_TITLE));
+ assertEquals(String.format(CONTENT_2, VALID_APP_LABEL),
+ notification.extras.getString(Notification.EXTRA_TEXT));
+ assertEquals(Notification.FLAG_ONGOING_EVENT,
+ notification.flags & Notification.FLAG_ONGOING_EVENT);
+ Mockito.clearInvocations(mNotificationManager);
+
+ // Verify that the thermal critical notification is created.
+ mController.showThermalCriticalNotificationIfNeeded(
+ STATE_WITH_ACTIVE_AND_THERMAL_NOTIFICATION);
+ verify(mNotificationManager).notify(
+ eq(DeviceStateNotificationController.NOTIFICATION_TAG),
+ eq(DeviceStateNotificationController.NOTIFICATION_ID),
+ mNotificationCaptor.capture());
+ notification = mNotificationCaptor.getValue();
+ assertEquals(THERMAL_TITLE_2, notification.extras.getString(Notification.EXTRA_TITLE));
+ assertEquals(THERMAL_CONTENT_2, notification.extras.getString(Notification.EXTRA_TEXT));
+ assertEquals(0, notification.flags & Notification.FLAG_ONGOING_EVENT);
+
+ // Verify that the notification is canceled.
+ mController.cancelNotification(STATE_WITH_ACTIVE_NOTIFICATION);
+ verify(mNotificationManager).cancel(
+ DeviceStateNotificationController.NOTIFICATION_TAG,
+ DeviceStateNotificationController.NOTIFICATION_ID);
+ }
+
+ @Test
+ public void test_deviceStateWithoutNotification() {
+ // Verify that no notification is created.
+ mController.showStateActiveNotificationIfNeeded(
+ STATE_WITHOUT_NOTIFICATION, VALID_APP_UID);
+ verify(mNotificationManager, Mockito.never()).notify(
+ eq(DeviceStateNotificationController.NOTIFICATION_TAG),
+ eq(DeviceStateNotificationController.NOTIFICATION_ID),
+ mNotificationCaptor.capture());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java b/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java
index 430504c..fe37f42 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java
@@ -58,7 +58,7 @@
@Test
public void addRequest() {
- OverrideRequest request = new OverrideRequest(new Binder(), 0 /* pid */,
+ OverrideRequest request = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
assertNull(mStatusListener.getLastStatus(request));
@@ -68,14 +68,14 @@
@Test
public void addRequest_cancelExistingRequestThroughNewRequest() {
- OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
assertNull(mStatusListener.getLastStatus(firstRequest));
mController.addRequest(firstRequest);
assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE);
- OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
1 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
assertNull(mStatusListener.getLastStatus(secondRequest));
@@ -86,7 +86,7 @@
@Test
public void addRequest_cancelActiveRequest() {
- OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
mController.addRequest(firstRequest);
@@ -100,7 +100,7 @@
@Test
public void addBaseStateRequest() {
- OverrideRequest request = new OverrideRequest(new Binder(), 0 /* pid */,
+ OverrideRequest request = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE);
assertNull(mStatusListener.getLastStatus(request));
@@ -110,14 +110,14 @@
@Test
public void addBaseStateRequest_cancelExistingBaseStateRequestThroughNewRequest() {
- OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE);
assertNull(mStatusListener.getLastStatus(firstRequest));
mController.addBaseStateRequest(firstRequest);
assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE);
- OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
1 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE);
assertNull(mStatusListener.getLastStatus(secondRequest));
@@ -128,7 +128,7 @@
@Test
public void addBaseStateRequest_cancelActiveBaseStateRequest() {
- OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE);
mController.addBaseStateRequest(firstRequest);
@@ -142,12 +142,13 @@
@Test
public void handleBaseStateChanged() {
- OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
0 /* requestedState */,
DeviceStateRequest.FLAG_CANCEL_WHEN_BASE_CHANGES /* flags */,
OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
OverrideRequest baseStateRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ 0 /* uid */,
0 /* requestedState */,
0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE);
@@ -167,10 +168,11 @@
@Test
public void handleProcessDied() {
- OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
OverrideRequest baseStateRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ 0 /* uid */,
1 /* requestedState */,
0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE);
@@ -189,10 +191,11 @@
public void handleProcessDied_stickyRequests() {
mController.setStickyRequestsAllowed(true);
- OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
OverrideRequest baseStateRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ 0 /* uid */,
1 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE);
mController.addRequest(firstRequest);
@@ -211,10 +214,11 @@
@Test
public void handleNewSupportedStates() {
- OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
1 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
OverrideRequest baseStateRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ 0 /* uid */,
1 /* requestedState */,
0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE);
@@ -224,18 +228,20 @@
mController.addBaseStateRequest(baseStateRequest);
assertEquals(mStatusListener.getLastStatus(baseStateRequest).intValue(), STATUS_ACTIVE);
- mController.handleNewSupportedStates(new int[]{0, 1});
+ mController.handleNewSupportedStates(new int[]{0, 1},
+ DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_DEFAULT);
assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE);
assertEquals(mStatusListener.getLastStatus(baseStateRequest).intValue(), STATUS_ACTIVE);
- mController.handleNewSupportedStates(new int[]{0});
+ mController.handleNewSupportedStates(new int[]{0},
+ DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_DEFAULT);
assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_CANCELED);
assertEquals(mStatusListener.getLastStatus(baseStateRequest).intValue(), STATUS_CANCELED);
}
@Test
public void cancelOverrideRequestsTest() {
- OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
1 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
mController.addRequest(firstRequest);
@@ -250,7 +256,7 @@
private Map<OverrideRequest, Integer> mLastStatusMap = new HashMap<>();
@Override
- public void onStatusChanged(@NonNull OverrideRequest request, int newStatus) {
+ public void onStatusChanged(@NonNull OverrideRequest request, int newStatus, int flags) {
mLastStatusMap.put(request, newStatus);
}
diff --git a/services/tests/servicestests/src/com/android/server/display/DeviceStateToLayoutMapTest.java b/services/tests/servicestests/src/com/android/server/display/DeviceStateToLayoutMapTest.java
index bcae50e..8f2a1e5 100644
--- a/services/tests/servicestests/src/com/android/server/display/DeviceStateToLayoutMapTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DeviceStateToLayoutMapTest.java
@@ -19,6 +19,7 @@
import static org.junit.Assert.assertEquals;
+import android.view.Display;
import android.view.DisplayAddress;
import androidx.test.filters.SmallTest;
@@ -64,10 +65,14 @@
Layout testLayout = new Layout();
testLayout.createDisplayLocked(
DisplayAddress.fromPhysicalDisplayId(123456L), /* isDefault= */ true,
- /* isEnabled= */ true, mDisplayIdProducerMock);
+ /* isEnabled= */ true, mDisplayIdProducerMock,
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
testLayout.createDisplayLocked(
DisplayAddress.fromPhysicalDisplayId(78910L), /* isDefault= */ false,
- /* isEnabled= */ false, mDisplayIdProducerMock);
+ /* isEnabled= */ false, mDisplayIdProducerMock,
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
assertEquals(testLayout, configLayout);
}
@@ -78,10 +83,14 @@
Layout testLayout = new Layout();
testLayout.createDisplayLocked(
DisplayAddress.fromPhysicalDisplayId(78910L), /* isDefault= */ true,
- /* isEnabled= */ true, mDisplayIdProducerMock);
+ /* isEnabled= */ true, mDisplayIdProducerMock,
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
testLayout.createDisplayLocked(
DisplayAddress.fromPhysicalDisplayId(123456L), /* isDefault= */ false,
- /* isEnabled= */ false, mDisplayIdProducerMock);
+ /* isEnabled= */ false, mDisplayIdProducerMock,
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
assertEquals(testLayout, configLayout);
}
@@ -91,12 +100,48 @@
Layout configLayout = mDeviceStateToLayoutMap.get(2);
Layout testLayout = new Layout();
- testLayout.createDisplayLocked(
+
+ Layout.Display display1 = testLayout.createDisplayLocked(
DisplayAddress.fromPhysicalDisplayId(345L), /* isDefault= */ true,
- /* isEnabled= */ true, mDisplayIdProducerMock);
+ /* isEnabled= */ true, mDisplayIdProducerMock,
+ /* brightnessThrottlingMapId= */ "concurrent",
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
+ display1.setPosition(Layout.Display.POSITION_FRONT);
+
+ Layout.Display display2 = testLayout.createDisplayLocked(
+ DisplayAddress.fromPhysicalDisplayId(678L), /* isDefault= */ false,
+ /* isEnabled= */ true, mDisplayIdProducerMock,
+ /* brightnessThrottlingMapId= */ "concurrent",
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
+ display2.setPosition(Layout.Display.POSITION_REAR);
+
+ assertEquals(testLayout, configLayout);
+ }
+
+ @Test
+ public void testRearDisplayLayout() {
+ Layout configLayout = mDeviceStateToLayoutMap.get(2);
+
+ assertEquals(Layout.Display.POSITION_FRONT, configLayout.getAt(0).getPosition());
+ assertEquals(Layout.Display.POSITION_REAR, configLayout.getAt(1).getPosition());
+ }
+
+ @Test
+ public void testRefreshRateZoneId() {
+ Layout configLayout = mDeviceStateToLayoutMap.get(3);
+
+ Layout testLayout = new Layout();
+ Layout.Display display1 = testLayout.createDisplayLocked(
+ DisplayAddress.fromPhysicalDisplayId(345L), /* isDefault= */ true,
+ /* isEnabled= */ true, mDisplayIdProducerMock,
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
+ display1.setRefreshRateZoneId("test1");
testLayout.createDisplayLocked(
DisplayAddress.fromPhysicalDisplayId(678L), /* isDefault= */ false,
- /* isEnabled= */ true, mDisplayIdProducerMock);
+ /* isEnabled= */ true, mDisplayIdProducerMock,
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
assertEquals(testLayout, configLayout);
}
@@ -139,6 +184,21 @@
+ "<state>2</state> \n"
+ "<display enabled=\"true\" defaultDisplay=\"true\">\n"
+ "<address>345</address>\n"
+ + "<position>front</position>\n"
+ + "<brightnessThrottlingMapId>concurrent</brightnessThrottlingMapId>\n"
+ + "</display>\n"
+ + "<display enabled=\"true\">\n"
+ + "<address>678</address>\n"
+ + "<position>rear</position>\n"
+ + "<brightnessThrottlingMapId>concurrent</brightnessThrottlingMapId>\n"
+ + "</display>\n"
+ + "</layout>\n"
+
+ + "<layout>\n"
+ + "<state>3</state> \n"
+ + "<display enabled=\"true\" defaultDisplay=\"true\" "
+ + "refreshRateZoneId=\"test1\">\n"
+ + "<address>345</address>\n"
+ "</display>\n"
+ "<display enabled=\"true\">\n"
+ "<address>678</address>\n"
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 0ea20a8..0e9c171 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -53,6 +53,8 @@
public final class DisplayDeviceConfigTest {
private static final int DEFAULT_PEAK_REFRESH_RATE = 75;
private static final int DEFAULT_REFRESH_RATE = 120;
+ private static final int DEFAULT_HIGH_BLOCKING_ZONE_REFRESH_RATE = 55;
+ private static final int DEFAULT_LOW_BLOCKING_ZONE_REFRESH_RATE = 95;
private static final int[] LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{10, 30};
private static final int[] LOW_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{1, 21};
private static final int[] HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{160};
@@ -149,8 +151,17 @@
assertArrayEquals(new float[]{23, 24, 25},
mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA);
- assertEquals(75, mDisplayDeviceConfig.getDefaultLowRefreshRate());
- assertEquals(90, mDisplayDeviceConfig.getDefaultHighRefreshRate());
+ assertEquals(75, mDisplayDeviceConfig.getDefaultLowBlockingZoneRefreshRate());
+ assertEquals(90, mDisplayDeviceConfig.getDefaultHighBlockingZoneRefreshRate());
+ assertEquals(85, mDisplayDeviceConfig.getDefaultPeakRefreshRate());
+ assertEquals(45, mDisplayDeviceConfig.getDefaultRefreshRate());
+
+ assertEquals(2, mDisplayDeviceConfig.getRefreshRangeProfiles().size());
+ assertEquals(60, mDisplayDeviceConfig.getRefreshRange("test1").min, SMALL_DELTA);
+ assertEquals(60, mDisplayDeviceConfig.getRefreshRange("test1").max, SMALL_DELTA);
+ assertEquals(80, mDisplayDeviceConfig.getRefreshRange("test2").min, SMALL_DELTA);
+ assertEquals(90, mDisplayDeviceConfig.getRefreshRange("test2").max, SMALL_DELTA);
+
assertArrayEquals(new int[]{45, 55},
mDisplayDeviceConfig.getLowDisplayBrightnessThresholds());
assertArrayEquals(new int[]{50, 60},
@@ -189,7 +200,7 @@
DisplayDeviceConfig.convertThermalStatus(ThermalStatus.shutdown), 0.025f
));
assertEquals(new DisplayDeviceConfig.BrightnessThrottlingData(throttlingLevels),
- mDisplayDeviceConfig.getBrightnessThrottlingData());
+ mDisplayDeviceConfig.getBrightnessThrottlingData("default"));
throttlingLevels.clear();
throttlingLevels.add(new DisplayDeviceConfig.BrightnessThrottlingData.ThrottlingLevel(
@@ -211,7 +222,7 @@
DisplayDeviceConfig.convertThermalStatus(ThermalStatus.shutdown), 0.0125f
));
assertEquals(new DisplayDeviceConfig.BrightnessThrottlingData(throttlingLevels),
- mDisplayDeviceConfig.getConcurrentDisplaysBrightnessThrottlingData());
+ mDisplayDeviceConfig.getBrightnessThrottlingData("concurrent"));
assertNotNull(mDisplayDeviceConfig.getHostUsiVersion());
assertEquals(mDisplayDeviceConfig.getHostUsiVersion().getMajorVersion(), 2);
@@ -278,8 +289,15 @@
mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle(), ZERO_DELTA);
assertArrayEquals(new float[]{29, 30, 31},
mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA);
- assertEquals(mDisplayDeviceConfig.getDefaultLowRefreshRate(), DEFAULT_REFRESH_RATE);
- assertEquals(mDisplayDeviceConfig.getDefaultHighRefreshRate(), DEFAULT_PEAK_REFRESH_RATE);
+ assertEquals(mDisplayDeviceConfig.getDefaultLowBlockingZoneRefreshRate(),
+ DEFAULT_LOW_BLOCKING_ZONE_REFRESH_RATE);
+ assertEquals(mDisplayDeviceConfig.getDefaultHighBlockingZoneRefreshRate(),
+ DEFAULT_HIGH_BLOCKING_ZONE_REFRESH_RATE);
+ assertEquals(mDisplayDeviceConfig.getDefaultPeakRefreshRate(), DEFAULT_PEAK_REFRESH_RATE);
+ assertEquals(mDisplayDeviceConfig.getDefaultRefreshRate(), DEFAULT_REFRESH_RATE);
+
+ assertEquals(0, mDisplayDeviceConfig.getRefreshRangeProfiles().size());
+
assertArrayEquals(mDisplayDeviceConfig.getLowDisplayBrightnessThresholds(),
LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE);
assertArrayEquals(mDisplayDeviceConfig.getLowAmbientBrightnessThresholds(),
@@ -509,7 +527,7 @@
+ "<brightness>0.025</brightness>\n"
+ "</brightnessThrottlingPoint>\n"
+ "</brightnessThrottlingMap>\n"
- + "<concurrentDisplaysBrightnessThrottlingMap>\n"
+ + "<brightnessThrottlingMap id=\"concurrent\">\n"
+ "<brightnessThrottlingPoint>\n"
+ "<thermalStatus>light</thermalStatus>\n"
+ "<brightness>0.2</brightness>\n"
@@ -534,9 +552,25 @@
+ "<thermalStatus>shutdown</thermalStatus>\n"
+ "<brightness>0.0125</brightness>\n"
+ "</brightnessThrottlingPoint>\n"
- + "</concurrentDisplaysBrightnessThrottlingMap>\n"
+ + "</brightnessThrottlingMap>\n"
+ "</thermalThrottling>\n"
+ "<refreshRate>\n"
+ + "<defaultRefreshRate>45</defaultRefreshRate>\n"
+ + "<defaultPeakRefreshRate>85</defaultPeakRefreshRate>\n"
+ + "<refreshRateZoneProfiles>"
+ + "<refreshRateZoneProfile id=\"test1\">"
+ + "<refreshRateRange>\n"
+ + "<minimum>60</minimum>\n"
+ + "<maximum>60</maximum>\n"
+ + "</refreshRateRange>\n"
+ + "</refreshRateZoneProfile>\n"
+ + "<refreshRateZoneProfile id=\"test2\">"
+ + "<refreshRateRange>\n"
+ + "<minimum>80</minimum>\n"
+ + "<maximum>90</maximum>\n"
+ + "</refreshRateRange>\n"
+ + "</refreshRateZoneProfile>\n"
+ + "</refreshRateZoneProfiles>"
+ "<lowerBlockingZoneConfigs>\n"
+ "<defaultRefreshRate>75</defaultRefreshRate>\n"
+ "<blockingZoneThreshold>\n"
@@ -642,10 +676,14 @@
.thenReturn(new int[]{370, 380, 390});
// Configs related to refresh rates and blocking zones
- when(mResources.getInteger(com.android.internal.R.integer.config_defaultPeakRefreshRate))
+ when(mResources.getInteger(R.integer.config_defaultPeakRefreshRate))
.thenReturn(DEFAULT_PEAK_REFRESH_RATE);
- when(mResources.getInteger(com.android.internal.R.integer.config_defaultRefreshRate))
+ when(mResources.getInteger(R.integer.config_defaultRefreshRate))
.thenReturn(DEFAULT_REFRESH_RATE);
+ when(mResources.getInteger(R.integer.config_fixedRefreshRateInHighZone))
+ .thenReturn(DEFAULT_HIGH_BLOCKING_ZONE_REFRESH_RATE);
+ when(mResources.getInteger(R.integer.config_defaultRefreshRateInZone))
+ .thenReturn(DEFAULT_LOW_BLOCKING_ZONE_REFRESH_RATE);
when(mResources.getIntArray(R.array.config_brightnessThresholdsOfPeakRefreshRate))
.thenReturn(LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE);
when(mResources.getIntArray(R.array.config_ambientThresholdsOfPeakRefreshRate))
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
index 2d252cb..1904671 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -56,6 +56,7 @@
import android.hardware.display.DisplayViewport;
import android.hardware.display.DisplayedContentSample;
import android.hardware.display.DisplayedContentSamplingAttributes;
+import android.hardware.display.HdrConversionMode;
import android.hardware.display.IDisplayManagerCallback;
import android.hardware.display.IVirtualDisplayCallback;
import android.hardware.display.VirtualDisplayConfig;
@@ -157,7 +158,7 @@
VirtualDisplayAdapter getVirtualDisplayAdapter(SyncRoot syncRoot, Context context,
Handler handler, DisplayAdapter.Listener displayAdapterListener) {
return new VirtualDisplayAdapter(syncRoot, context, handler, displayAdapterListener,
- (String name, boolean secure) -> mMockDisplayToken);
+ (String name, boolean secure, float refreshRate) -> mMockDisplayToken);
}
@Override
@@ -171,6 +172,17 @@
}
});
}
+
+ @Override
+ void setHdrConversionMode(int conversionMode, int preferredHdrOutputType,
+ int[] autoHdrTypes) {
+ return;
+ }
+
+ @Override
+ int[] getSupportedHdrOutputTypes() {
+ return new int[]{};
+ }
}
private final DisplayManagerService.Injector mBasicInjector = new BasicInjector();
@@ -240,8 +252,7 @@
// the usage of SensorManager, which is available only after the PowerManagerService
// is ready.
resetConfigToIgnoreSensorManager(mContext);
- DisplayManagerService displayManager =
- new DisplayManagerService(mContext, mBasicInjector);
+ DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
registerDefaultDisplays(displayManager);
displayManager.systemReady(false /* safeMode */);
displayManager.windowManagerAndInputReady();
@@ -316,8 +327,7 @@
// the usage of SensorManager, which is available only after the PowerManagerService
// is ready.
resetConfigToIgnoreSensorManager(mContext);
- DisplayManagerService displayManager =
- new DisplayManagerService(mContext, mBasicInjector);
+ DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
registerDefaultDisplays(displayManager);
displayManager.systemReady(false /* safeMode */);
displayManager.windowManagerAndInputReady();
@@ -1511,6 +1521,22 @@
}
+ @Test
+ public void testHdrConversionModeEquals() {
+ assertEquals(
+ new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_FORCE, 2),
+ new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_FORCE, 2));
+ assertNotEquals(
+ new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_FORCE, 2),
+ new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_FORCE, 3));
+ assertEquals(
+ new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_SYSTEM),
+ new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_SYSTEM));
+ assertNotEquals(
+ new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_FORCE, 2),
+ new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_SYSTEM));
+ }
+
private void testDisplayInfoFrameRateOverrideModeCompat(boolean compatChangeEnabled)
throws Exception {
DisplayManagerService displayManager =
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
index a35f4ea..0eff0da 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
@@ -2260,6 +2260,10 @@
.thenReturn(75);
when(resources.getInteger(R.integer.config_defaultRefreshRate))
.thenReturn(45);
+ when(resources.getInteger(R.integer.config_fixedRefreshRateInHighZone))
+ .thenReturn(65);
+ when(resources.getInteger(R.integer.config_defaultRefreshRateInZone))
+ .thenReturn(85);
when(resources.getIntArray(R.array.config_brightnessThresholdsOfPeakRefreshRate))
.thenReturn(new int[]{5});
when(resources.getIntArray(R.array.config_ambientThresholdsOfPeakRefreshRate))
@@ -2279,6 +2283,8 @@
assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 45, 0.0);
assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 75,
0.0);
+ assertEquals(director.getBrightnessObserver().getRefreshRateInHighZone(), 65);
+ assertEquals(director.getBrightnessObserver().getRefreshRateInLowZone(), 85);
assertArrayEquals(director.getBrightnessObserver().getHighDisplayBrightnessThreshold(),
new int[]{250});
assertArrayEquals(director.getBrightnessObserver().getHighAmbientBrightnessThreshold(),
@@ -2290,17 +2296,21 @@
// Notify that the default display is updated, such that DisplayDeviceConfig has new values
DisplayDeviceConfig displayDeviceConfig = mock(DisplayDeviceConfig.class);
- when(displayDeviceConfig.getDefaultLowRefreshRate()).thenReturn(50);
- when(displayDeviceConfig.getDefaultHighRefreshRate()).thenReturn(55);
+ when(displayDeviceConfig.getDefaultLowBlockingZoneRefreshRate()).thenReturn(50);
+ when(displayDeviceConfig.getDefaultHighBlockingZoneRefreshRate()).thenReturn(55);
+ when(displayDeviceConfig.getDefaultRefreshRate()).thenReturn(60);
+ when(displayDeviceConfig.getDefaultPeakRefreshRate()).thenReturn(65);
when(displayDeviceConfig.getLowDisplayBrightnessThresholds()).thenReturn(new int[]{25});
when(displayDeviceConfig.getLowAmbientBrightnessThresholds()).thenReturn(new int[]{30});
when(displayDeviceConfig.getHighDisplayBrightnessThresholds()).thenReturn(new int[]{210});
when(displayDeviceConfig.getHighAmbientBrightnessThresholds()).thenReturn(new int[]{2100});
director.defaultDisplayDeviceUpdated(displayDeviceConfig);
- assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 50, 0.0);
- assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 55,
+ assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 60, 0.0);
+ assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 65,
0.0);
+ assertEquals(director.getBrightnessObserver().getRefreshRateInHighZone(), 55);
+ assertEquals(director.getBrightnessObserver().getRefreshRateInLowZone(), 50);
assertArrayEquals(director.getBrightnessObserver().getHighDisplayBrightnessThreshold(),
new int[]{210});
assertArrayEquals(director.getBrightnessObserver().getHighAmbientBrightnessThreshold(),
@@ -2313,6 +2323,8 @@
// Notify that the default display is updated, such that DeviceConfig has new values
FakeDeviceConfig config = mInjector.getDeviceConfig();
config.setDefaultPeakRefreshRate(60);
+ config.setRefreshRateInHighZone(65);
+ config.setRefreshRateInLowZone(70);
config.setLowAmbientBrightnessThresholds(new int[]{20});
config.setLowDisplayBrightnessThresholds(new int[]{10});
config.setHighDisplayBrightnessThresholds(new int[]{255});
@@ -2320,9 +2332,11 @@
director.defaultDisplayDeviceUpdated(displayDeviceConfig);
- assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 50, 0.0);
+ assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 60, 0.0);
assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 60,
0.0);
+ assertEquals(director.getBrightnessObserver().getRefreshRateInHighZone(), 65);
+ assertEquals(director.getBrightnessObserver().getRefreshRateInLowZone(), 70);
assertArrayEquals(director.getBrightnessObserver().getHighDisplayBrightnessThreshold(),
new int[]{255});
assertArrayEquals(director.getBrightnessObserver().getHighAmbientBrightnessThreshold(),
@@ -2362,8 +2376,8 @@
any(Handler.class));
DisplayDeviceConfig ddcMock = mock(DisplayDeviceConfig.class);
- when(ddcMock.getDefaultLowRefreshRate()).thenReturn(50);
- when(ddcMock.getDefaultHighRefreshRate()).thenReturn(55);
+ when(ddcMock.getDefaultLowBlockingZoneRefreshRate()).thenReturn(50);
+ when(ddcMock.getDefaultHighBlockingZoneRefreshRate()).thenReturn(55);
when(ddcMock.getLowDisplayBrightnessThresholds()).thenReturn(new int[]{25});
when(ddcMock.getLowAmbientBrightnessThresholds()).thenReturn(new int[]{30});
when(ddcMock.getHighDisplayBrightnessThresholds()).thenReturn(new int[]{210});
@@ -2388,6 +2402,74 @@
eq(lightSensorTwo), anyInt(), any(Handler.class));
}
+ @Test
+ public void testAuthenticationPossibleSetsPhysicalRateRangesToMax() throws RemoteException {
+ DisplayModeDirector director =
+ createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0);
+ // don't call director.start(createMockSensorManager());
+ // DisplayObserver will reset mSupportedModesByDisplay
+ director.onBootCompleted();
+ ArgumentCaptor<IUdfpsRefreshRateRequestCallback> captor =
+ ArgumentCaptor.forClass(IUdfpsRefreshRateRequestCallback.class);
+ verify(mStatusBarMock).setUdfpsRefreshRateCallback(captor.capture());
+
+ captor.getValue().onAuthenticationPossible(DISPLAY_ID, true);
+
+ Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE);
+ assertThat(vote.refreshRateRanges.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+ assertThat(vote.refreshRateRanges.physical.max).isWithin(FLOAT_TOLERANCE).of(90);
+ }
+
+ @Test
+ public void testAuthenticationPossibleUnsetsVote() throws RemoteException {
+ DisplayModeDirector director =
+ createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0);
+ director.start(createMockSensorManager());
+ director.onBootCompleted();
+ ArgumentCaptor<IUdfpsRefreshRateRequestCallback> captor =
+ ArgumentCaptor.forClass(IUdfpsRefreshRateRequestCallback.class);
+ verify(mStatusBarMock).setUdfpsRefreshRateCallback(captor.capture());
+ captor.getValue().onAuthenticationPossible(DISPLAY_ID, true);
+ captor.getValue().onAuthenticationPossible(DISPLAY_ID, false);
+
+ Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE);
+ assertNull(vote);
+ }
+
+ @Test
+ public void testUdfpsRequestSetsPhysicalRateRangesToMax() throws RemoteException {
+ DisplayModeDirector director =
+ createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0);
+ // don't call director.start(createMockSensorManager());
+ // DisplayObserver will reset mSupportedModesByDisplay
+ director.onBootCompleted();
+ ArgumentCaptor<IUdfpsRefreshRateRequestCallback> captor =
+ ArgumentCaptor.forClass(IUdfpsRefreshRateRequestCallback.class);
+ verify(mStatusBarMock).setUdfpsRefreshRateCallback(captor.capture());
+
+ captor.getValue().onRequestEnabled(DISPLAY_ID);
+
+ Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_UDFPS);
+ assertThat(vote.refreshRateRanges.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+ assertThat(vote.refreshRateRanges.physical.max).isWithin(FLOAT_TOLERANCE).of(90);
+ }
+
+ @Test
+ public void testUdfpsRequestUnsetsUnsetsVote() throws RemoteException {
+ DisplayModeDirector director =
+ createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0);
+ director.start(createMockSensorManager());
+ director.onBootCompleted();
+ ArgumentCaptor<IUdfpsRefreshRateRequestCallback> captor =
+ ArgumentCaptor.forClass(IUdfpsRefreshRateRequestCallback.class);
+ verify(mStatusBarMock).setUdfpsRefreshRateCallback(captor.capture());
+ captor.getValue().onRequestEnabled(DISPLAY_ID);
+ captor.getValue().onRequestDisabled(DISPLAY_ID);
+
+ Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_UDFPS);
+ assertNull(vote);
+ }
+
private Temperature getSkinTemp(@Temperature.ThrottlingStatus int status) {
return new Temperature(30.0f, Temperature.TYPE_SKIN, "test_skin_temp", status);
}
diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
index a860387..488c533 100644
--- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -19,6 +19,7 @@
import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.DEFAULT_DISPLAY_GROUP;
+import static android.view.Display.FLAG_REAR;
import static android.view.Display.TYPE_EXTERNAL;
import static android.view.Display.TYPE_INTERNAL;
import static android.view.Display.TYPE_VIRTUAL;
@@ -30,6 +31,8 @@
import static com.android.server.display.DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY;
import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_ADDED;
import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_REMOVED;
+import static com.android.server.display.layout.Layout.Display.POSITION_REAR;
+import static com.android.server.display.layout.Layout.Display.POSITION_UNKNOWN;
import static com.google.common.truth.Truth.assertThat;
@@ -296,9 +299,11 @@
Layout layout1 = new Layout();
layout1.createDisplayLocked(info(device1).address, /* isDefault= */ true,
- /* isEnabled= */ true, mIdProducer);
+ /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
layout1.createDisplayLocked(info(device2).address, /* isDefault= */ false,
- /* isEnabled= */ true, mIdProducer);
+ /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
when(mDeviceStateToLayoutMapSpy.get(STATE_DEFAULT)).thenReturn(layout1);
assertThat(layout1.size()).isEqualTo(2);
final int logicalId2 = layout1.getByAddress(info(device2).address).getLogicalDisplayId();
@@ -331,17 +336,20 @@
add(device3);
Layout layout1 = new Layout();
- layout1.createDisplayLocked(info(device1).address,
- /* isDefault= */ true, /* isEnabled= */ true, mIdProducer);
+ layout1.createDisplayLocked(info(device1).address, /* isDefault= */ true,
+ /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
when(mDeviceStateToLayoutMapSpy.get(STATE_DEFAULT)).thenReturn(layout1);
final int layoutState2 = 2;
Layout layout2 = new Layout();
- layout2.createDisplayLocked(info(device2).address,
- /* isDefault= */ false, /* isEnabled= */ true, mIdProducer);
+ layout2.createDisplayLocked(info(device2).address, /* isDefault= */ false,
+ /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
// Device3 is the default display.
- layout2.createDisplayLocked(info(device3).address,
- /* isDefault= */ true, /* isEnabled= */ true, mIdProducer);
+ layout2.createDisplayLocked(info(device3).address, /* isDefault= */ true,
+ /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
when(mDeviceStateToLayoutMapSpy.get(layoutState2)).thenReturn(layout2);
assertThat(layout2.size()).isEqualTo(2);
final int logicalId2 = layout2.getByAddress(info(device2).address).getLogicalDisplayId();
@@ -563,16 +571,22 @@
Layout layout = new Layout();
layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address,
- true, true, mIdProducer);
+ true, true, mIdProducer,
+ /* brightnessThrottlingMapId= */ "concurrent",
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address,
- false, true, mIdProducer);
+ false, true, mIdProducer,
+ /* brightnessThrottlingMapId= */ "concurrent",
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
when(mDeviceStateToLayoutMapSpy.get(0)).thenReturn(layout);
layout = new Layout();
layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address,
- false, false, mIdProducer);
+ false, false, mIdProducer, /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address,
- true, true, mIdProducer);
+ true, true, mIdProducer, /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
when(mDeviceStateToLayoutMapSpy.get(1)).thenReturn(layout);
when(mDeviceStateToLayoutMapSpy.get(2)).thenReturn(layout);
@@ -599,6 +613,14 @@
assertTrue(mLogicalDisplayMapper.getDisplayLocked(device2).isEnabledLocked());
assertFalse(mLogicalDisplayMapper.getDisplayLocked(device1).isInTransitionLocked());
assertFalse(mLogicalDisplayMapper.getDisplayLocked(device2).isInTransitionLocked());
+ assertEquals(-1, mLogicalDisplayMapper.getDisplayLocked(device1)
+ .getLeadDisplayIdLocked());
+ assertEquals(0, mLogicalDisplayMapper.getDisplayLocked(device2)
+ .getLeadDisplayIdLocked());
+ assertEquals("concurrent", mLogicalDisplayMapper.getDisplayLocked(device1)
+ .getBrightnessThrottlingDataIdLocked());
+ assertEquals("concurrent", mLogicalDisplayMapper.getDisplayLocked(device2)
+ .getBrightnessThrottlingDataIdLocked());
mLogicalDisplayMapper.setDeviceStateLocked(1, false);
advanceTime(1000);
@@ -606,6 +628,12 @@
assertTrue(mLogicalDisplayMapper.getDisplayLocked(device2).isEnabledLocked());
assertFalse(mLogicalDisplayMapper.getDisplayLocked(device1).isInTransitionLocked());
assertFalse(mLogicalDisplayMapper.getDisplayLocked(device2).isInTransitionLocked());
+ assertEquals(DisplayDeviceConfig.DEFAULT_BRIGHTNESS_THROTTLING_DATA_ID,
+ mLogicalDisplayMapper.getDisplayLocked(device1)
+ .getBrightnessThrottlingDataIdLocked());
+ assertEquals(DisplayDeviceConfig.DEFAULT_BRIGHTNESS_THROTTLING_DATA_ID,
+ mLogicalDisplayMapper.getDisplayLocked(device2)
+ .getBrightnessThrottlingDataIdLocked());
mLogicalDisplayMapper.setDeviceStateLocked(2, false);
advanceTime(1000);
@@ -613,6 +641,12 @@
assertTrue(mLogicalDisplayMapper.getDisplayLocked(device2).isEnabledLocked());
assertFalse(mLogicalDisplayMapper.getDisplayLocked(device1).isInTransitionLocked());
assertFalse(mLogicalDisplayMapper.getDisplayLocked(device2).isInTransitionLocked());
+ assertEquals(DisplayDeviceConfig.DEFAULT_BRIGHTNESS_THROTTLING_DATA_ID,
+ mLogicalDisplayMapper.getDisplayLocked(device1)
+ .getBrightnessThrottlingDataIdLocked());
+ assertEquals(DisplayDeviceConfig.DEFAULT_BRIGHTNESS_THROTTLING_DATA_ID,
+ mLogicalDisplayMapper.getDisplayLocked(device2)
+ .getBrightnessThrottlingDataIdLocked());
}
@Test
@@ -633,17 +667,23 @@
displayAddressOne,
/* isDefault= */ true,
/* isEnabled= */ true,
- mIdProducer);
+ mIdProducer,
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
threeDevicesEnabledLayout.createDisplayLocked(
displayAddressTwo,
/* isDefault= */ false,
/* isEnabled= */ true,
- mIdProducer);
+ mIdProducer,
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
threeDevicesEnabledLayout.createDisplayLocked(
displayAddressThree,
/* isDefault= */ false,
/* isEnabled= */ true,
- mIdProducer);
+ mIdProducer,
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
when(mDeviceStateToLayoutMapSpy.get(STATE_DEFAULT))
.thenReturn(threeDevicesEnabledLayout);
@@ -678,17 +718,23 @@
displayAddressOne,
/* isDefault= */ true,
/* isEnabled= */ true,
- mIdProducer);
+ mIdProducer,
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
oneDeviceEnabledLayout.createDisplayLocked(
displayAddressTwo,
/* isDefault= */ false,
/* isEnabled= */ false,
- mIdProducer);
+ mIdProducer,
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
oneDeviceEnabledLayout.createDisplayLocked(
displayAddressThree,
/* isDefault= */ false,
/* isEnabled= */ false,
- mIdProducer);
+ mIdProducer,
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
when(mDeviceStateToLayoutMapSpy.get(0)).thenReturn(oneDeviceEnabledLayout);
when(mDeviceStateToLayoutMapSpy.get(1)).thenReturn(threeDevicesEnabledLayout);
@@ -754,6 +800,46 @@
assertFalse(display2.isEnabledLocked());
}
+ @Test
+ public void testDisplayFlagRear() {
+ DisplayDevice device1 = createDisplayDevice(TYPE_INTERNAL, 600, 800,
+ FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+ DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 600, 800,
+ FLAG_REAR);
+
+ Layout layout = new Layout();
+ layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address,
+ true, true, mIdProducer, /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
+ layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address,
+ false, true, mIdProducer, /* brightnessThrottlingMapId= */ null,
+ POSITION_REAR, Display.DEFAULT_DISPLAY);
+ when(mDeviceStateToLayoutMapSpy.get(0)).thenReturn(layout);
+
+ when(mDeviceStateToLayoutMapSpy.size()).thenReturn(1);
+
+ LogicalDisplay display1 = add(device1);
+ assertEquals(info(display1).address, info(device1).address);
+ assertEquals(DEFAULT_DISPLAY, id(display1));
+
+ LogicalDisplay display2 = add(device2);
+ assertEquals(info(display2).address, info(device2).address);
+ // We can only have one default display
+ assertEquals(DEFAULT_DISPLAY, id(display1));
+
+ mLogicalDisplayMapper.setDeviceStateLocked(0, false);
+ advanceTime(1000);
+ mLogicalDisplayMapper.onBootCompleted();
+ advanceTime(1000);
+
+ assertTrue(mLogicalDisplayMapper.getDisplayLocked(device1).isEnabledLocked());
+ assertTrue(mLogicalDisplayMapper.getDisplayLocked(device2).isEnabledLocked());
+
+ assertEquals(POSITION_UNKNOWN,
+ mLogicalDisplayMapper.getDisplayLocked(device1).getDevicePositionLocked());
+ assertEquals(POSITION_REAR,
+ mLogicalDisplayMapper.getDisplayLocked(device2).getDevicePositionLocked());
+ }
/////////////////
// Helper Methods
diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayTest.java
index 1d70fc6..d28050d 100644
--- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayTest.java
@@ -26,12 +26,15 @@
import android.app.PropertyInvalidatedCache;
import android.graphics.Point;
+import android.view.Display;
import android.view.DisplayInfo;
import android.view.Surface;
import android.view.SurfaceControl;
import androidx.test.filters.SmallTest;
+import com.android.server.display.layout.Layout;
+
import org.junit.Before;
import org.junit.Test;
@@ -47,6 +50,7 @@
private LogicalDisplay mLogicalDisplay;
private DisplayDevice mDisplayDevice;
+ private DisplayDeviceRepository mDeviceRepo;
private final DisplayDeviceInfo mDisplayDeviceInfo = new DisplayDeviceInfo();
@Before
@@ -66,7 +70,7 @@
// Disable binder caches in this process.
PropertyInvalidatedCache.disableForTestMode();
- DisplayDeviceRepository repo = new DisplayDeviceRepository(
+ mDeviceRepo = new DisplayDeviceRepository(
new DisplayManagerService.SyncRoot(),
new PersistentDataStore(new PersistentDataStore.Injector() {
@Override
@@ -82,8 +86,8 @@
@Override
public void finishWrite(OutputStream os, boolean success) {}
}));
- repo.onDisplayDeviceEvent(mDisplayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
- mLogicalDisplay.updateLocked(repo);
+ mDeviceRepo.onDisplayDeviceEvent(mDisplayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
+ mLogicalDisplay.updateLocked(mDeviceRepo);
}
@Test
@@ -137,4 +141,29 @@
verify(t).setDisplayFlags(any(), eq(SurfaceControl.DISPLAY_RECEIVES_INPUT));
reset(t);
}
+
+ @Test
+ public void testRearDisplaysArePresentationDisplaysThatDestroyContentOnRemoval() {
+ // Assert that the display isn't a presentation display by default, with a default remove
+ // mode
+ assertEquals(0, mLogicalDisplay.getDisplayInfoLocked().flags);
+ assertEquals(Display.REMOVE_MODE_MOVE_CONTENT_TO_PRIMARY,
+ mLogicalDisplay.getDisplayInfoLocked().removeMode);
+
+ // Update position and test to see that it's been updated to a rear, presentation display
+ // that destroys content on removal
+ mLogicalDisplay.setDevicePositionLocked(Layout.Display.POSITION_REAR);
+ mLogicalDisplay.updateLocked(mDeviceRepo);
+ assertEquals(Display.FLAG_REAR | Display.FLAG_PRESENTATION,
+ mLogicalDisplay.getDisplayInfoLocked().flags);
+ assertEquals(Display.REMOVE_MODE_DESTROY_CONTENT,
+ mLogicalDisplay.getDisplayInfoLocked().removeMode);
+
+ // And then check the unsetting the position resets both
+ mLogicalDisplay.setDevicePositionLocked(Layout.Display.POSITION_UNKNOWN);
+ mLogicalDisplay.updateLocked(mDeviceRepo);
+ assertEquals(0, mLogicalDisplay.getDisplayInfoLocked().flags);
+ assertEquals(Display.REMOVE_MODE_MOVE_CONTENT_TO_PRIMARY,
+ mLogicalDisplay.getDisplayInfoLocked().removeMode);
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
index 5ca695b..c9612cd 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
@@ -49,8 +49,8 @@
import android.os.UserManager;
import android.os.storage.IStorageManager;
import android.os.storage.StorageManager;
-import android.provider.Settings;
import android.provider.DeviceConfig;
+import android.provider.Settings;
import android.security.KeyStore;
import androidx.test.InstrumentationRegistry;
@@ -83,16 +83,15 @@
protected static final int MANAGED_PROFILE_USER_ID = 12;
protected static final int TURNED_OFF_PROFILE_USER_ID = 17;
protected static final int SECONDARY_USER_ID = 20;
+ protected static final int TERTIARY_USER_ID = 21;
- private static final UserInfo PRIMARY_USER_INFO = new UserInfo(PRIMARY_USER_ID, null, null,
- UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_ADMIN | UserInfo.FLAG_PRIMARY
- | UserInfo.FLAG_MAIN);
- private static final UserInfo SECONDARY_USER_INFO = new UserInfo(SECONDARY_USER_ID, null, null,
- UserInfo.FLAG_INITIALIZED);
+ protected UserInfo mPrimaryUserInfo;
+ protected UserInfo mSecondaryUserInfo;
+ protected UserInfo mTertiaryUserInfo;
private ArrayList<UserInfo> mPrimaryUserProfiles = new ArrayList<>();
- LockSettingsService mService;
+ LockSettingsServiceTestable mService;
LockSettingsInternal mLocalService;
MockLockSettingsContext mContext;
@@ -117,6 +116,7 @@
FingerprintManager mFingerprintManager;
FaceManager mFaceManager;
PackageManager mPackageManager;
+ LockSettingsServiceTestable.MockInjector mInjector;
@Rule
public FakeSettingsProviderRule mSettingsRule = FakeSettingsProvider.rule();
@@ -162,22 +162,61 @@
mSpManager = new MockSyntheticPasswordManager(mContext, mStorage, mGateKeeperService,
mUserManager, mPasswordSlotManager);
mAuthSecretService = mock(IAuthSecret.class);
- mService = new LockSettingsServiceTestable(mContext, mStorage,
- mGateKeeperService, mKeyStore, setUpStorageManagerMock(), mActivityManager,
- mSpManager, mAuthSecretService, mGsiService, mRecoverableKeyStoreManager,
- mUserManagerInternal, mDeviceStateCache);
+ mInjector =
+ new LockSettingsServiceTestable.MockInjector(
+ mContext,
+ mStorage,
+ mKeyStore,
+ mActivityManager,
+ setUpStorageManagerMock(),
+ mSpManager,
+ mGsiService,
+ mRecoverableKeyStoreManager,
+ mUserManagerInternal,
+ mDeviceStateCache);
+ mService =
+ new LockSettingsServiceTestable(mInjector, mGateKeeperService, mAuthSecretService);
mService.mHasSecureLockScreen = true;
- when(mUserManager.getUserInfo(eq(PRIMARY_USER_ID))).thenReturn(PRIMARY_USER_INFO);
- mPrimaryUserProfiles.add(PRIMARY_USER_INFO);
+ mPrimaryUserInfo =
+ new UserInfo(
+ PRIMARY_USER_ID,
+ null,
+ null,
+ UserInfo.FLAG_INITIALIZED
+ | UserInfo.FLAG_ADMIN
+ | UserInfo.FLAG_PRIMARY
+ | UserInfo.FLAG_MAIN
+ | UserInfo.FLAG_FULL);
+ mSecondaryUserInfo =
+ new UserInfo(
+ SECONDARY_USER_ID,
+ null,
+ null,
+ UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_FULL);
+ mTertiaryUserInfo =
+ new UserInfo(
+ TERTIARY_USER_ID,
+ null,
+ null,
+ UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_FULL);
+
+ when(mUserManager.getUserInfo(eq(PRIMARY_USER_ID))).thenReturn(mPrimaryUserInfo);
+ when(mUserManagerInternal.getUserInfo(eq(PRIMARY_USER_ID))).thenReturn(mPrimaryUserInfo);
+ mPrimaryUserProfiles.add(mPrimaryUserInfo);
installChildProfile(MANAGED_PROFILE_USER_ID);
installAndTurnOffChildProfile(TURNED_OFF_PROFILE_USER_ID);
for (UserInfo profile : mPrimaryUserProfiles) {
when(mUserManager.getProfiles(eq(profile.id))).thenReturn(mPrimaryUserProfiles);
}
- when(mUserManager.getUserInfo(eq(SECONDARY_USER_ID))).thenReturn(SECONDARY_USER_INFO);
+ when(mUserManager.getUserInfo(eq(SECONDARY_USER_ID))).thenReturn(mSecondaryUserInfo);
+ when(mUserManagerInternal.getUserInfo(eq(SECONDARY_USER_ID)))
+ .thenReturn(mSecondaryUserInfo);
+ when(mUserManager.getUserInfo(eq(TERTIARY_USER_ID))).thenReturn(mTertiaryUserInfo);
+ when(mUserManagerInternal.getUserInfo(eq(TERTIARY_USER_ID))).thenReturn(mTertiaryUserInfo);
final ArrayList<UserInfo> allUsers = new ArrayList<>(mPrimaryUserProfiles);
- allUsers.add(SECONDARY_USER_INFO);
+ allUsers.add(mSecondaryUserInfo);
+ allUsers.add(mTertiaryUserInfo);
when(mUserManager.getUsers()).thenReturn(allUsers);
when(mActivityManager.unlockUser2(anyInt(), any())).thenAnswer(
@@ -227,9 +266,10 @@
userInfo.profileGroupId = PRIMARY_USER_ID;
mPrimaryUserProfiles.add(userInfo);
when(mUserManager.getUserInfo(eq(profileId))).thenReturn(userInfo);
- when(mUserManager.getProfileParent(eq(profileId))).thenReturn(PRIMARY_USER_INFO);
+ when(mUserManager.getProfileParent(eq(profileId))).thenReturn(mPrimaryUserInfo);
when(mUserManager.isUserRunning(eq(profileId))).thenReturn(true);
when(mUserManager.isUserUnlocked(eq(profileId))).thenReturn(true);
+ when(mUserManagerInternal.getUserInfo(eq(profileId))).thenReturn(userInfo);
// TODO(b/258213147): Remove
when(mUserManagerInternal.isUserManaged(eq(profileId))).thenReturn(true);
when(mDeviceStateCache.isUserOrganizationManaged(eq(profileId)))
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
index f0f0632..9686c38 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
@@ -30,6 +30,7 @@
import android.os.storage.IStorageManager;
import android.security.KeyStore;
import android.security.keystore.KeyPermanentlyInvalidatedException;
+import android.service.gatekeeper.IGateKeeperService;
import com.android.internal.widget.LockscreenCredential;
import com.android.server.ServiceThread;
@@ -40,7 +41,7 @@
public class LockSettingsServiceTestable extends LockSettingsService {
- private static class MockInjector extends LockSettingsService.Injector {
+ public static class MockInjector extends LockSettingsService.Injector {
private LockSettingsStorage mLockSettingsStorage;
private KeyStore mKeyStore;
@@ -52,6 +53,9 @@
private UserManagerInternal mUserManagerInternal;
private DeviceStateCache mDeviceStateCache;
+ public boolean mIsHeadlessSystemUserMode = false;
+ public boolean mIsMainUserPermanentAdmin = false;
+
public MockInjector(Context context, LockSettingsStorage storage, KeyStore keyStore,
IActivityManager activityManager,
IStorageManager storageManager, SyntheticPasswordManager spManager,
@@ -140,19 +144,22 @@
return mock(ManagedProfilePasswordCache.class);
}
+ @Override
+ public boolean isHeadlessSystemUserMode() {
+ return mIsHeadlessSystemUserMode;
+ }
+
+ @Override
+ public boolean isMainUserPermanentAdmin() {
+ return mIsMainUserPermanentAdmin;
+ }
}
- public MockInjector mInjector;
-
- protected LockSettingsServiceTestable(Context context,
- LockSettingsStorage storage, FakeGateKeeperService gatekeeper, KeyStore keystore,
- IStorageManager storageManager, IActivityManager mActivityManager,
- SyntheticPasswordManager spManager, IAuthSecret authSecretService,
- FakeGsiService gsiService, RecoverableKeyStoreManager recoverableKeyStoreManager,
- UserManagerInternal userManagerInternal, DeviceStateCache deviceStateCache) {
- super(new MockInjector(context, storage, keystore, mActivityManager,
- storageManager, spManager, gsiService, recoverableKeyStoreManager,
- userManagerInternal, deviceStateCache));
+ protected LockSettingsServiceTestable(
+ LockSettingsService.Injector injector,
+ IGateKeeperService gatekeeper,
+ IAuthSecret authSecretService) {
+ super(injector);
mGateKeeperService = gatekeeper;
mAuthSecretService = authSecretService;
}
@@ -199,4 +206,10 @@
UserInfo userInfo = mUserManager.getUserInfo(userId);
return userInfo.isCloneProfile() || userInfo.isManagedProfile();
}
+
+ void clearAuthSecret() {
+ synchronized (mHeadlessAuthSecretLock) {
+ mAuthSecret = null;
+ }
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index 57593cf..62d8a76 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -16,6 +16,10 @@
package com.android.server.locksettings;
+import static android.content.pm.UserInfo.FLAG_FULL;
+import static android.content.pm.UserInfo.FLAG_MAIN;
+import static android.content.pm.UserInfo.FLAG_PRIMARY;
+
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD_OR_PIN;
@@ -27,9 +31,9 @@
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
@@ -247,6 +251,15 @@
@Test
public void testUnlockUserKeyIfUnsecuredPassesPrimaryUserAuthSecret() throws RemoteException {
+ initSpAndSetCredential(PRIMARY_USER_ID, newPassword(null));
+ reset(mAuthSecretService);
+ mLocalService.unlockUserKeyIfUnsecured(PRIMARY_USER_ID);
+ verify(mAuthSecretService).setPrimaryUserCredential(any(byte[].class));
+ }
+
+ @Test
+ public void testUnlockUserKeyIfUnsecuredPassesPrimaryUserAuthSecretIfPasswordIsCleared()
+ throws RemoteException {
LockscreenCredential password = newPassword("password");
initSpAndSetCredential(PRIMARY_USER_ID, password);
mService.setLockCredential(nonePassword(), password, PRIMARY_USER_ID);
@@ -256,6 +269,56 @@
verify(mAuthSecretService).setPrimaryUserCredential(any(byte[].class));
}
+ private void setupHeadlessTest() {
+ mInjector.mIsHeadlessSystemUserMode = true;
+ mInjector.mIsMainUserPermanentAdmin = true;
+ mPrimaryUserInfo.flags &= ~(FLAG_FULL | FLAG_PRIMARY);
+ mSecondaryUserInfo.flags |= FLAG_MAIN;
+ mService.initializeSyntheticPassword(PRIMARY_USER_ID);
+ mService.initializeSyntheticPassword(SECONDARY_USER_ID);
+ mService.initializeSyntheticPassword(TERTIARY_USER_ID);
+ reset(mAuthSecretService);
+ }
+
+ @Test
+ public void testHeadlessSystemUserDoesNotPassAuthSecret() throws RemoteException {
+ setupHeadlessTest();
+ mLocalService.unlockUserKeyIfUnsecured(PRIMARY_USER_ID);
+ verify(mAuthSecretService, never()).setPrimaryUserCredential(any(byte[].class));
+ }
+
+ @Test
+ public void testHeadlessSecondaryUserPassesAuthSecret() throws RemoteException {
+ setupHeadlessTest();
+ mLocalService.unlockUserKeyIfUnsecured(SECONDARY_USER_ID);
+ verify(mAuthSecretService).setPrimaryUserCredential(any(byte[].class));
+ }
+
+ @Test
+ public void testHeadlessTertiaryUserPassesSameAuthSecret() throws RemoteException {
+ setupHeadlessTest();
+ mLocalService.unlockUserKeyIfUnsecured(SECONDARY_USER_ID);
+ var captor = ArgumentCaptor.forClass(byte[].class);
+ verify(mAuthSecretService).setPrimaryUserCredential(captor.capture());
+ var value = captor.getValue();
+ reset(mAuthSecretService);
+ mLocalService.unlockUserKeyIfUnsecured(TERTIARY_USER_ID);
+ verify(mAuthSecretService).setPrimaryUserCredential(eq(value));
+ }
+
+ @Test
+ public void testHeadlessTertiaryUserPassesSameAuthSecretAfterReset() throws RemoteException {
+ setupHeadlessTest();
+ mLocalService.unlockUserKeyIfUnsecured(SECONDARY_USER_ID);
+ var captor = ArgumentCaptor.forClass(byte[].class);
+ verify(mAuthSecretService).setPrimaryUserCredential(captor.capture());
+ var value = captor.getValue();
+ mService.clearAuthSecret();
+ reset(mAuthSecretService);
+ mLocalService.unlockUserKeyIfUnsecured(TERTIARY_USER_ID);
+ verify(mAuthSecretService).setPrimaryUserCredential(eq(value));
+ }
+
@Test
public void testTokenBasedResetPassword() throws RemoteException {
LockscreenCredential password = newPassword("password");
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java
index 966c047..50f3a88 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java
@@ -8,6 +8,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.server.locksettings.LockSettingsStorage.PersistentData;
+
import com.google.android.collect.Sets;
import org.junit.Before;
@@ -29,7 +30,7 @@
// sequentially, starting at slot 0.
@Test
public void testFrpWeaverSlotNotReused() {
- final int userId = 10;
+ final int userId = SECONDARY_USER_ID;
final int frpWeaverSlot = 0;
setDeviceProvisioned(false);
@@ -45,7 +46,7 @@
// it's here as a control for testFrpWeaverSlotNotReused().
@Test
public void testFrpWeaverSlotReused() {
- final int userId = 10;
+ final int userId = SECONDARY_USER_ID;
final int frpWeaverSlot = 0;
setDeviceProvisioned(true);
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java
index bb79fd8..b9adc30 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java
@@ -40,6 +40,7 @@
import android.Manifest;
import android.app.KeyguardManager;
import android.app.PendingIntent;
+import android.app.StartLockscreenValidationRequest;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
@@ -61,11 +62,13 @@
import com.android.internal.util.ArrayUtils;
import com.android.internal.widget.LockPatternUtils;
import com.android.security.SecureBox;
+import com.android.server.locksettings.LockSettingsService;
import com.android.server.locksettings.recoverablekeystore.storage.ApplicationKeyStorage;
import com.android.server.locksettings.recoverablekeystore.storage.CleanupManager;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage;
+import com.android.server.locksettings.recoverablekeystore.storage.RemoteLockscreenValidationSessionStorage;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -160,14 +163,17 @@
@Mock private ApplicationKeyStorage mApplicationKeyStorage;
@Mock private CleanupManager mCleanupManager;
@Mock private ScheduledExecutorService mExecutorService;
+ @Mock private LockSettingsService mLockSettingsService;
@Spy private TestOnlyInsecureCertificateHelper mTestOnlyInsecureCertificateHelper;
+ private int mUserId;
private RecoverableKeyStoreDb mRecoverableKeyStoreDb;
private File mDatabaseFile;
private RecoverableKeyStoreManager mRecoverableKeyStoreManager;
private RecoverySessionStorage mRecoverySessionStorage;
private RecoverySnapshotStorage mRecoverySnapshotStorage;
private PlatformEncryptionKey mPlatformEncryptionKey;
+ private RemoteLockscreenValidationSessionStorage mRemoteLockscreenValidationSessionStorage;
@Before
public void setUp() throws Exception {
@@ -177,7 +183,9 @@
mDatabaseFile = context.getDatabasePath(DATABASE_FILE_NAME);
mRecoverableKeyStoreDb = RecoverableKeyStoreDb.newInstance(context);
+ mUserId = UserHandle.getCallingUserId();
mRecoverySessionStorage = new RecoverySessionStorage();
+ mRemoteLockscreenValidationSessionStorage = new RemoteLockscreenValidationSessionStorage();
when(mMockContext.getSystemService(anyString())).thenReturn(mKeyguardManager);
when(mMockContext.getSystemServiceName(any())).thenReturn("test");
@@ -198,7 +206,8 @@
mPlatformKeyManager,
mApplicationKeyStorage,
mTestOnlyInsecureCertificateHelper,
- mCleanupManager);
+ mCleanupManager,
+ mRemoteLockscreenValidationSessionStorage);
}
@After
@@ -1269,6 +1278,85 @@
verify(mExecutorService).schedule(any(Runnable.class), anyLong(), any());
}
+ @Test
+ public void startRemoteLockscreenValidation_credentialsNotSet() throws Exception {
+ when(mLockSettingsService.getCredentialType(anyInt())).thenReturn(
+ LockPatternUtils.CREDENTIAL_TYPE_NONE);
+ try {
+ mRecoverableKeyStoreManager.startRemoteLockscreenValidation(mLockSettingsService);
+ fail("should have thrown");
+ } catch (IllegalStateException e) {
+ assertThat(e.getMessage()).contains("not set");
+ }
+ verify(mLockSettingsService).getCredentialType(mUserId);
+ mRemoteLockscreenValidationSessionStorage.finishSession(mUserId);
+ }
+
+ @Test
+ public void startRemoteLockscreenValidation_checksPermission() throws Exception {
+ when(mLockSettingsService.getCredentialType(anyInt())).thenReturn(
+ LockPatternUtils.CREDENTIAL_TYPE_PIN);
+ mRecoverableKeyStoreManager.startRemoteLockscreenValidation(mLockSettingsService);
+ // TODO(b/254335492): Check new system permission
+ verify(mMockContext, times(1))
+ .enforceCallingOrSelfPermission(
+ eq(Manifest.permission.RECOVER_KEYSTORE), any());
+ mRemoteLockscreenValidationSessionStorage.finishSession(mUserId);
+ }
+
+ @Test
+ public void startRemoteLockscreenValidation_returnsCredentailsType() throws Exception {
+ when(mLockSettingsService.getCredentialType(anyInt())).thenReturn(
+ LockPatternUtils.CREDENTIAL_TYPE_PIN);
+ StartLockscreenValidationRequest request =
+ mRecoverableKeyStoreManager.startRemoteLockscreenValidation(mLockSettingsService);
+ int credetialsType = request.getLockscreenUiType();
+
+ assertThat(credetialsType).isEqualTo(KeyguardManager.PIN);
+
+ verify(mLockSettingsService).getCredentialType(anyInt());
+ mRemoteLockscreenValidationSessionStorage.finishSession(mUserId);
+ }
+
+ @Test
+ public void startRemoteLockscreenValidation_returnsRemainingAttempts() throws Exception {
+ when(mLockSettingsService.getCredentialType(anyInt())).thenReturn(
+ LockPatternUtils.CREDENTIAL_TYPE_PATTERN);
+ mRecoverableKeyStoreDb.setBadRemoteGuessCounter(mUserId, 3);
+ StartLockscreenValidationRequest request =
+ mRecoverableKeyStoreManager.startRemoteLockscreenValidation(mLockSettingsService);
+ int credetialsType = request.getLockscreenUiType();
+ assertThat(credetialsType).isEqualTo(KeyguardManager.PATTERN);
+ // TODO(b/254335492): Verify returned value
+ mRemoteLockscreenValidationSessionStorage.finishSession(mUserId);
+ }
+
+ @Test
+ public void startRemoteLockscreenValidation_password() throws Exception {
+ when(mLockSettingsService.getCredentialType(anyInt())).thenReturn(
+ LockPatternUtils.CREDENTIAL_TYPE_PASSWORD);
+ mRecoverableKeyStoreDb.setBadRemoteGuessCounter(mUserId, 3);
+ StartLockscreenValidationRequest request =
+ mRecoverableKeyStoreManager.startRemoteLockscreenValidation(mLockSettingsService);
+ int credetialsType = request.getLockscreenUiType();
+ assertThat(credetialsType).isEqualTo(KeyguardManager.PASSWORD);
+ mRemoteLockscreenValidationSessionStorage.finishSession(mUserId);
+ }
+
+ @Test
+ public void validateRemoteLockscreen_noActiveSession() throws Exception {
+ when(mLockSettingsService.getCredentialType(anyInt())).thenReturn(
+ LockPatternUtils.CREDENTIAL_TYPE_NONE);
+ byte[] invalidGuess = new byte[]{1, 2, 3};
+ try {
+ mRecoverableKeyStoreManager.validateRemoteLockscreen(invalidGuess,
+ mLockSettingsService);
+ fail("should have thrown");
+ } catch (IllegalStateException e) {
+ assertThat(e.getMessage()).contains("session");
+ }
+ }
+
private static byte[] encryptedApplicationKey(
SecretKey recoveryKey, byte[] applicationKey) throws Exception {
return KeySyncUtils.encryptKeysWithRecoveryKey(recoveryKey, ImmutableMap.of(
diff --git a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
new file mode 100644
index 0000000..52c6777
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.os;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.os.Binder;
+import android.os.BugreportManager.BugreportCallback;
+import android.os.IBinder;
+import android.os.IDumpstateListener;
+import android.os.RemoteException;
+import android.util.ArraySet;
+import android.util.Pair;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileDescriptor;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class BugreportManagerServiceImplTest {
+
+ Context mContext;
+ BugreportManagerServiceImpl mService;
+ BugreportManagerServiceImpl.BugreportFileManager mBugreportFileManager;
+
+ int mCallingUid = 1234;
+ String mCallingPackage = "test.package";
+
+ String mBugreportFile = "bugreport-file.zip";
+ String mBugreportFile2 = "bugreport-file2.zip";
+
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ ArraySet<String> mAllowlistedPackages = new ArraySet<>();
+ mAllowlistedPackages.add(mContext.getPackageName());
+ mService = new BugreportManagerServiceImpl(
+ new BugreportManagerServiceImpl.Injector(mContext, mAllowlistedPackages));
+ mBugreportFileManager = new BugreportManagerServiceImpl.BugreportFileManager();
+ }
+
+ @Test
+ public void testBugreportFileManagerFileExists() {
+ Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage);
+ mBugreportFileManager.addBugreportFileForCaller(
+ callingInfo, mBugreportFile);
+
+ assertThrows(IllegalArgumentException.class, () ->
+ mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
+ callingInfo, "unknown-file.zip"));
+
+ // No exception should be thrown.
+ mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(callingInfo, mBugreportFile);
+ }
+
+ @Test
+ public void testBugreportFileManagerMultipleFiles() {
+ Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage);
+ mBugreportFileManager.addBugreportFileForCaller(
+ callingInfo, mBugreportFile);
+ mBugreportFileManager.addBugreportFileForCaller(
+ callingInfo, mBugreportFile2);
+
+ // No exception should be thrown.
+ mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(callingInfo, mBugreportFile);
+ mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(callingInfo, mBugreportFile2);
+ }
+
+ @Test
+ public void testBugreportFileManagerFileDoesNotExist() {
+ Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage);
+ assertThrows(IllegalArgumentException.class,
+ () -> mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
+ callingInfo, "test-file.zip"));
+ }
+
+ @Test
+ public void testRetrieveBugreportWithoutFilesForCaller() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+ Listener listener = new Listener(latch);
+ mService.retrieveBugreport(Binder.getCallingUid(), mContext.getPackageName(),
+ new FileDescriptor(), mBugreportFile, listener);
+ assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(listener.getErrorCode()).isEqualTo(
+ BugreportCallback.BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE);
+ }
+
+ private static class Listener implements IDumpstateListener {
+ CountDownLatch mLatch;
+ int mErrorCode;
+
+ Listener(CountDownLatch latch) {
+ mLatch = latch;
+ }
+
+ @Override
+ public IBinder asBinder() {
+ return null;
+ }
+
+ @Override
+ public void onProgress(int progress) throws RemoteException {
+ }
+
+ @Override
+ public void onError(int errorCode) throws RemoteException {
+ mErrorCode = errorCode;
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onFinished(String bugreportFile) throws RemoteException {
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onScreenshotTaken(boolean success) throws RemoteException {
+ }
+
+ @Override
+ public void onUiIntensiveBugreportDumpsFinished() throws RemoteException {
+ }
+
+ int getErrorCode() {
+ return mErrorCode;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/os/OWNERS b/services/tests/servicestests/src/com/android/server/os/OWNERS
new file mode 100644
index 0000000..feb8011
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/os/OWNERS
@@ -0,0 +1,2 @@
+# Component 153446
+include /platform/frameworks/native:/cmds/dumpstate/OWNERS
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/os/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/os/TEST_MAPPING
new file mode 100644
index 0000000..9902446
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/os/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+ "presubmit": [
+ {
+ "name": "BugreportManagerServiceImplTests",
+ "options": [
+ {
+ "include-filter": "com.android.server.os."
+ }
+ ]
+ }
+ ]
+}
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index c760efd..b4dd631 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -72,6 +72,7 @@
private static final long EPOCH_PLUS_30_YEARS = 30L * 365 * 24 * 60 * 60 * 1000L; // 30 years
private static final int SWITCH_USER_TIMEOUT_SECONDS = 40; // 40 seconds
+ private static final int REMOVE_USER_TIMEOUT_SECONDS = 40; // 40 seconds
// Packages which are used during tests.
private static final String[] PACKAGES = new String[] {
@@ -302,7 +303,7 @@
@MediumTest
@Test
public void testRemoveUserByHandle_ThrowsException() {
- assertThrows(IllegalArgumentException.class, () -> removeUser(null));
+ assertThrows(IllegalArgumentException.class, () -> mUserManager.removeUser(null));
}
@MediumTest
@@ -1464,6 +1465,12 @@
}
private void runThenWaitForUserRemoval(Runnable runnable, int userIdToWaitUntilDeleted) {
+ if (!hasUser(userIdToWaitUntilDeleted)) {
+ runnable.run();
+ mUsersToRemove.remove(userIdToWaitUntilDeleted);
+ return;
+ }
+
Function<Intent, Boolean> checker = intent -> {
UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER, UserHandle.class);
return userHandle != null && userHandle.getIdentifier() == userIdToWaitUntilDeleted;
@@ -1472,6 +1479,7 @@
BlockingBroadcastReceiver blockingBroadcastReceiver = BlockingBroadcastReceiver.create(
mContext, Intent.ACTION_USER_REMOVED, checker);
+ blockingBroadcastReceiver.setTimeout(REMOVE_USER_TIMEOUT_SECONDS);
blockingBroadcastReceiver.register();
try (blockingBroadcastReceiver) {
diff --git a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
index 7fac9b6..7125796 100644
--- a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
@@ -19,6 +19,9 @@
import static android.content.Context.SENSOR_SERVICE;
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED;
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL;
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL;
import static com.android.server.policy.DeviceStateProviderImpl.DEFAULT_DEVICE_STATE;
import static org.junit.Assert.assertArrayEquals;
@@ -124,7 +127,8 @@
DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
provider.setListener(listener);
- verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
assertArrayEquals(new DeviceState[]{DEFAULT_DEVICE_STATE},
mDeviceStateArrayCaptor.getValue());
@@ -151,7 +155,8 @@
DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
provider.setListener(listener);
- verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
final DeviceState[] expectedStates = new DeviceState[]{
new DeviceState(1, "", 0 /* flags */),
new DeviceState(2, "", 0 /* flags */) };
@@ -183,7 +188,8 @@
DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
provider.setListener(listener);
- verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
final DeviceState[] expectedStates = new DeviceState[]{
new DeviceState(1, "", DeviceState.FLAG_CANCEL_OVERRIDE_REQUESTS),
new DeviceState(2, "", 0 /* flags */) };
@@ -212,7 +218,8 @@
DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
provider.setListener(listener);
- verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
final DeviceState[] expectedStates = new DeviceState[]{
new DeviceState(1, "", 0 /* flags */),
new DeviceState(2, "", 0 /* flags */) };
@@ -247,7 +254,8 @@
DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
provider.setListener(listener);
- verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
final DeviceState[] expectedStates = new DeviceState[]{
new DeviceState(1, "", 0 /* flags */),
new DeviceState(2, "CLOSED", 0 /* flags */) };
@@ -265,7 +273,8 @@
Mockito.clearInvocations(listener);
provider.notifyLidSwitchChanged(1, true /* lidOpen */);
- verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+ verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
verify(listener).onStateChanged(mIntegerCaptor.capture());
assertEquals(1, mIntegerCaptor.getValue().intValue());
}
@@ -336,7 +345,8 @@
DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
provider.setListener(listener);
- verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
assertArrayEquals(
new DeviceState[]{
new DeviceState(1, "CLOSED", 0 /* flags */),
@@ -358,7 +368,8 @@
provider.onSensorChanged(event0);
- verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+ verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
verify(listener).onStateChanged(mIntegerCaptor.capture());
assertEquals(3, mIntegerCaptor.getValue().intValue());
@@ -370,7 +381,8 @@
provider.onSensorChanged(event1);
- verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+ verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
verify(listener).onStateChanged(mIntegerCaptor.capture());
assertEquals(2, mIntegerCaptor.getValue().intValue());
@@ -382,7 +394,8 @@
provider.onSensorChanged(event2);
- verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+ verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
verify(listener).onStateChanged(mIntegerCaptor.capture());
assertEquals(1, mIntegerCaptor.getValue().intValue());
}
@@ -397,7 +410,8 @@
DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
provider.setListener(listener);
- verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
assertArrayEquals(
new DeviceState[]{
new DeviceState(1, "CLOSED", 0 /* flags */),
@@ -410,12 +424,14 @@
Mockito.clearInvocations(listener);
provider.onThermalStatusChanged(PowerManager.THERMAL_STATUS_MODERATE);
- verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+ verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
Mockito.clearInvocations(listener);
// The THERMAL_TEST state should be disabled.
provider.onThermalStatusChanged(PowerManager.THERMAL_STATUS_CRITICAL);
- verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL));
assertArrayEquals(
new DeviceState[]{
new DeviceState(1, "CLOSED", 0 /* flags */),
@@ -426,7 +442,8 @@
// The THERMAL_TEST state should be re-enabled.
provider.onThermalStatusChanged(PowerManager.THERMAL_STATUS_LIGHT);
- verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL));
assertArrayEquals(
new DeviceState[]{
new DeviceState(1, "CLOSED", 0 /* flags */),
@@ -468,7 +485,8 @@
provider.onSensorChanged(event2);
- verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+ verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
verify(listener, never()).onStateChanged(mIntegerCaptor.capture());
}
@@ -513,7 +531,8 @@
DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
provider.setListener(listener);
- verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
assertArrayEquals(
new DeviceState[]{
new DeviceState(1, "CLOSED", 0 /* flags */),
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 20e8cd4..5c69c84 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -19,7 +19,6 @@
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.app.Notification.EXTRA_ALLOW_DURING_SETUP;
import static android.app.Notification.FLAG_AUTO_CANCEL;
import static android.app.Notification.FLAG_BUBBLE;
import static android.app.Notification.FLAG_CAN_COLORIZE;
@@ -79,6 +78,7 @@
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNotSame;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
@@ -218,7 +218,6 @@
import com.android.server.notification.NotificationManagerService.NotificationListeners;
import com.android.server.pm.PackageManagerService;
import com.android.server.pm.UserManagerInternal;
-import com.android.server.pm.permission.PermissionManagerServiceInternal;
import com.android.server.policy.PermissionPolicyInternal;
import com.android.server.statusbar.StatusBarManagerInternal;
import com.android.server.uri.UriGrantsManagerInternal;
@@ -307,9 +306,6 @@
ActivityManager mActivityManager;
@Mock
TelecomManager mTelecomManager;
-
- @Mock
- PermissionManagerServiceInternal mPermissionInternal;
@Mock
Resources mResources;
@Mock
@@ -523,7 +519,7 @@
mAppOpsManager, mAppOpsService, mUm, mHistoryManager, mStatsManager,
mock(TelephonyManager.class),
mAmi, mToastRateLimiter, mPermissionHelper, mock(UsageStatsManagerInternal.class),
- mTelecomManager, mLogger, mPermissionInternal);
+ mTelecomManager, mLogger);
// Return first true for RoleObserver main-thread check
when(mMainLooper.isCurrentThread()).thenReturn(true).thenReturn(false);
mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY, mMainLooper);
@@ -1651,6 +1647,46 @@
}
@Test
+ public void testEnqueueNotificationWithTag_nullAction_fixed() throws Exception {
+ Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
+ .setContentTitle("foo")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .addAction(new Notification.Action.Builder(null, "one", null).build())
+ .addAction(new Notification.Action.Builder(null, "two", null).build())
+ .addAction(new Notification.Action.Builder(null, "three", null).build())
+ .build();
+ n.actions[1] = null;
+
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, n, 0);
+ waitForIdle();
+
+ StatusBarNotification[] posted = mBinderService.getActiveNotifications(PKG);
+ assertThat(posted).hasLength(1);
+ assertThat(posted[0].getNotification().actions).hasLength(2);
+ assertThat(posted[0].getNotification().actions[0].title.toString()).isEqualTo("one");
+ assertThat(posted[0].getNotification().actions[1].title.toString()).isEqualTo("three");
+ }
+
+ @Test
+ public void testEnqueueNotificationWithTag_allNullActions_fixed() throws Exception {
+ Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
+ .setContentTitle("foo")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .addAction(new Notification.Action.Builder(null, "one", null).build())
+ .addAction(new Notification.Action.Builder(null, "two", null).build())
+ .build();
+ n.actions[0] = null;
+ n.actions[1] = null;
+
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, n, 0);
+ waitForIdle();
+
+ StatusBarNotification[] posted = mBinderService.getActiveNotifications(PKG);
+ assertThat(posted).hasLength(1);
+ assertThat(posted[0].getNotification().actions).isNull();
+ }
+
+ @Test
public void testCancelNonexistentNotification() throws Exception {
mBinderService.cancelNotificationWithTag(PKG, PKG,
"testCancelNonexistentNotification", 0, 0);
@@ -4173,35 +4209,8 @@
}
@Test
- public void testNoNotificationDuringSetupPermission() throws Exception {
- when(mPermissionInternal.checkPermission(any(), any(), anyInt()))
- .thenReturn(PERMISSION_DENIED);
- Bundle extras = new Bundle();
- extras.putBoolean(EXTRA_ALLOW_DURING_SETUP, true);
- Notification.Builder nb = new Notification.Builder(mContext,
- mTestNotificationChannel.getId())
- .setContentTitle("foo")
- .addExtras(extras)
- .setSmallIcon(android.R.drawable.sym_def_app_icon);
- StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1,
- "testNoNotificationDuringSetupPermission", mUid, 0,
- nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0);
- NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel);
-
- mBinderService.enqueueNotificationWithTag(PKG, PKG, sbn.getTag(),
- nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
- waitForIdle();
-
- NotificationRecord posted = mService.findNotificationLocked(
- PKG, nr.getSbn().getTag(), nr.getSbn().getId(), nr.getSbn().getUserId());
-
- assertFalse(posted.getNotification().extras.containsKey(EXTRA_ALLOW_DURING_SETUP));
- }
-
- @Test
public void testNoFakeColorizedPermission() throws Exception {
- when(mPermissionInternal.checkPermission(any(), any(), anyInt()))
- .thenReturn(PERMISSION_DENIED);
+ when(mPackageManagerClient.checkPermission(any(), any())).thenReturn(PERMISSION_DENIED);
Notification.Builder nb = new Notification.Builder(mContext,
mTestNotificationChannel.getId())
.setContentTitle("foo")
@@ -4226,7 +4235,7 @@
@Test
public void testMediaStyleRemote_hasPermission() throws RemoteException {
String deviceName = "device";
- when(mPermissionInternal.checkPermission(
+ when(mPackageManager.checkPermission(
eq(android.Manifest.permission.MEDIA_CONTENT_CONTROL), any(), anyInt()))
.thenReturn(PERMISSION_GRANTED);
Notification.MediaStyle style = new Notification.MediaStyle();
@@ -4255,7 +4264,7 @@
@Test
public void testMediaStyleRemote_noPermission() throws RemoteException {
String deviceName = "device";
- when(mPermissionInternal.checkPermission(
+ when(mPackageManager.checkPermission(
eq(android.Manifest.permission.MEDIA_CONTENT_CONTROL), any(), anyInt()))
.thenReturn(PERMISSION_DENIED);
Notification.MediaStyle style = new Notification.MediaStyle();
@@ -4283,7 +4292,7 @@
@Test
public void testSubstituteAppName_hasPermission() throws RemoteException {
String subName = "Substitute Name";
- when(mPermissionInternal.checkPermission(
+ when(mPackageManager.checkPermission(
eq(android.Manifest.permission.SUBSTITUTE_NOTIFICATION_APP_NAME), any(), anyInt()))
.thenReturn(PERMISSION_GRANTED);
Bundle extras = new Bundle();
@@ -4310,7 +4319,7 @@
@Test
public void testSubstituteAppName_noPermission() throws RemoteException {
- when(mPermissionInternal.checkPermission(
+ when(mPackageManager.checkPermission(
eq(android.Manifest.permission.SUBSTITUTE_NOTIFICATION_APP_NAME), any(), anyInt()))
.thenReturn(PERMISSION_DENIED);
Bundle extras = new Bundle();
@@ -5820,7 +5829,7 @@
@Test
public void testCanUseManagedServices_hasPermission() throws Exception {
- when(mPermissionInternal.checkPermission("perm", "pkg", 0))
+ when(mPackageManager.checkPermission("perm", "pkg", 0))
.thenReturn(PackageManager.PERMISSION_GRANTED);
assertEquals(true, mService.canUseManagedServices("pkg", 0, "perm"));
@@ -5828,7 +5837,7 @@
@Test
public void testCanUseManagedServices_noPermission() throws Exception {
- when(mPermissionInternal.checkPermission("perm", "pkg", 0))
+ when(mPackageManager.checkPermission("perm", "pkg", 0))
.thenReturn(PackageManager.PERMISSION_DENIED);
assertEquals(false, mService.canUseManagedServices("pkg", 0, "perm"));
@@ -6783,7 +6792,7 @@
private void setIfPackageHasPermissionToAvoidToastRateLimiting(
String pkg, boolean hasPermission) throws Exception {
- when(mPermissionInternal.checkPermission(android.Manifest.permission.UNLIMITED_TOASTS,
+ when(mPackageManager.checkPermission(android.Manifest.permission.UNLIMITED_TOASTS,
pkg, UserHandle.getUserId(mUid)))
.thenReturn(hasPermission ? PERMISSION_GRANTED : PERMISSION_DENIED);
}
@@ -10075,4 +10084,211 @@
mInternalService.sendReviewPermissionsNotification();
verify(mMockNm, never()).notify(anyString(), anyInt(), any(Notification.class));
}
+
+ @Test
+ public void fixSystemNotification_withOnGoingFlag_shouldBeNonDismissible()
+ throws Exception {
+ // Given: a notification from an app on the system partition has the flag
+ // FLAG_ONGOING_EVENT set
+ // feature flag: NOTIFICATION_ONGOING_DISMISSAL is on
+ mService.setOngoingDismissal(true);
+ Notification n = new Notification.Builder(mContext, "test")
+ .setOngoing(true)
+ .build();
+
+ final ApplicationInfo systemAppInfo = new ApplicationInfo();
+ systemAppInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
+ when(mPackageManagerClient.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+ .thenReturn(systemAppInfo);
+
+ // When: fix the notification with NotificationManagerService
+ mService.fixNotification(n, PKG, "tag", 9, 0);
+
+ // Then: the notification's flag FLAG_NO_DISMISS should be set
+ assertNotSame(0, n.flags & Notification.FLAG_NO_DISMISS);
+
+ // Avoid affecting other tests
+ mService.setOngoingDismissal(false);
+ }
+
+ @Test
+ public void fixMediaNotification_withOnGoingFlag_shouldBeNonDismissible()
+ throws Exception {
+ // Given: a media notification has the flag FLAG_ONGOING_EVENT set
+ // feature flag: NOTIFICATION_ONGOING_DISMISSAL is on
+ mService.setOngoingDismissal(true);
+ Notification n = new Notification.Builder(mContext, "test")
+ .setOngoing(true)
+ .setStyle(new Notification.MediaStyle()
+ .setMediaSession(mock(MediaSession.Token.class)))
+ .build();
+
+ // When: fix the notification with NotificationManagerService
+ mService.fixNotification(n, PKG, "tag", 9, 0);
+
+ // Then: the notification's flag FLAG_NO_DISMISS should be set
+ assertNotSame(0, n.flags & Notification.FLAG_NO_DISMISS);
+
+ // Avoid affecting other tests
+ mService.setOngoingDismissal(false);
+ }
+
+ @Test
+ public void fixNonExemptNotification_withOnGoingFlag_shouldBeDismissible() throws Exception {
+ // Given: a non-exempt notification has the flag FLAG_ONGOING_EVENT set
+ // feature flag: NOTIFICATION_ONGOING_DISMISSAL is on
+ mService.setOngoingDismissal(true);
+ Notification n = new Notification.Builder(mContext, "test")
+ .setOngoing(true)
+ .build();
+
+ // When: fix the notification with NotificationManagerService
+ mService.fixNotification(n, PKG, "tag", 9, 0);
+
+ // Then: the notification's flag FLAG_NO_DISMISS should not be set
+ assertEquals(0, n.flags & Notification.FLAG_NO_DISMISS);
+
+ // Avoid affecting other tests
+ mService.setOngoingDismissal(false);
+ }
+
+ @Test
+ public void fixNonExemptNotification_withNoDismissFlag_shouldBeDismissible()
+ throws Exception {
+ // Given: a non-exempt notification has the flag FLAG_NO_DISMISS set (even though this is
+ // not allowed)
+ // feature flag: NOTIFICATION_ONGOING_DISMISSAL is on
+ mService.setOngoingDismissal(true);
+ Notification n = new Notification.Builder(mContext, "test")
+ .build();
+ n.flags |= Notification.FLAG_NO_DISMISS;
+
+ // When: fix the notification with NotificationManagerService
+ mService.fixNotification(n, PKG, "tag", 9, 0);
+
+ // Then: the notification's flag FLAG_NO_DISMISS should be cleared
+ assertEquals(0, n.flags & Notification.FLAG_NO_DISMISS);
+
+ // Avoid affecting other tests
+ mService.setOngoingDismissal(false);
+ }
+
+ @Test
+ public void fixSystemNotification_withoutOnGoingFlag_shouldBeDismissible() throws Exception {
+ // Given: a notification from an app on the system partition doesn't have the flag
+ // FLAG_ONGOING_EVENT set
+ // feature flag: NOTIFICATION_ONGOING_DISMISSAL is on
+ mService.setOngoingDismissal(true);
+ Notification n = new Notification.Builder(mContext, "test")
+ .setOngoing(false)
+ .build();
+
+ final ApplicationInfo systemAppInfo = new ApplicationInfo();
+ systemAppInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
+ when(mPackageManagerClient.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+ .thenReturn(systemAppInfo);
+
+ // When: fix the notification with NotificationManagerService
+ mService.fixNotification(n, PKG, "tag", 9, 0);
+
+ // Then: the notification's flag FLAG_NO_DISMISS should not be set
+ assertEquals(0, n.flags & Notification.FLAG_NO_DISMISS);
+
+ // Avoid affecting other tests
+ mService.setOngoingDismissal(false);
+ }
+
+ @Test
+ public void fixSystemNotification_withoutOnGoingFlag_withNoDismissFlag_shouldBeDismissible()
+ throws Exception {
+ // Given: a notification from an app on the system partition doesn't have the flag
+ // FLAG_ONGOING_EVENT set, but has the flag FLAG_NO_DISMISS set
+ // feature flag: NOTIFICATION_ONGOING_DISMISSAL is on
+ mService.setOngoingDismissal(true);
+ Notification n = new Notification.Builder(mContext, "test")
+ .setOngoing(false)
+ .build();
+ n.flags |= Notification.FLAG_NO_DISMISS;
+
+ final ApplicationInfo systemAppInfo = new ApplicationInfo();
+ systemAppInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
+ when(mPackageManagerClient.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+ .thenReturn(systemAppInfo);
+
+ // When: fix the notification with NotificationManagerService
+ mService.fixNotification(n, PKG, "tag", 9, 0);
+
+ // Then: the notification's flag FLAG_NO_DISMISS should be cleared
+ assertEquals(0, n.flags & Notification.FLAG_NO_DISMISS);
+
+ // Avoid affecting other tests
+ mService.setOngoingDismissal(false);
+ }
+
+ @Test
+ public void fixMediaNotification_withoutOnGoingFlag_shouldBeDismissible() throws Exception {
+ // Given: a media notification doesn't have the flag FLAG_ONGOING_EVENT set
+ // feature flag: NOTIFICATION_ONGOING_DISMISSAL is on
+ mService.setOngoingDismissal(true);
+ Notification n = new Notification.Builder(mContext, "test")
+ .setOngoing(false)
+ .setStyle(new Notification.MediaStyle()
+ .setMediaSession(mock(MediaSession.Token.class)))
+ .build();
+
+ // When: fix the notification with NotificationManagerService
+ mService.fixNotification(n, PKG, "tag", 9, 0);
+
+ // Then: the notification's flag FLAG_NO_DISMISS should not be set
+ assertEquals(0, n.flags & Notification.FLAG_NO_DISMISS);
+
+ // Avoid affecting other tests
+ mService.setOngoingDismissal(false);
+ }
+
+ @Test
+ public void fixMediaNotification_withoutOnGoingFlag_withNoDismissFlag_shouldBeDismissible()
+ throws Exception {
+ // Given: a media notification doesn't have the flag FLAG_ONGOING_EVENT set,
+ // but has the flag FLAG_NO_DISMISS set
+ // feature flag: NOTIFICATION_ONGOING_DISMISSAL is on
+ mService.setOngoingDismissal(true);
+ Notification n = new Notification.Builder(mContext, "test")
+ .setOngoing(false)
+ .setStyle(new Notification.MediaStyle()
+ .setMediaSession(mock(MediaSession.Token.class)))
+ .build();
+ n.flags |= Notification.FLAG_NO_DISMISS;
+
+ // When: fix the notification with NotificationManagerService
+ mService.fixNotification(n, PKG, "tag", 9, 0);
+
+ // Then: the notification's flag FLAG_NO_DISMISS should be cleared
+ assertEquals(0, n.flags & Notification.FLAG_NO_DISMISS);
+
+ // Avoid affecting other tests
+ mService.setOngoingDismissal(false);
+ }
+
+ @Test
+ public void fixNonExempt_Notification_withoutOnGoingFlag_shouldBeDismissible()
+ throws Exception {
+ // Given: a non-exempt notification has the flag FLAG_ONGOING_EVENT set
+ // feature flag: NOTIFICATION_ONGOING_DISMISSAL is on
+ mService.setOngoingDismissal(true);
+ mService.setOngoingDismissal(true);
+ Notification n = new Notification.Builder(mContext, "test")
+ .setOngoing(false)
+ .build();
+
+ // When: fix the notification with NotificationManagerService
+ mService.fixNotification(n, PKG, "tag", 9, 0);
+
+ // Then: the notification's flag FLAG_NO_DISMISS should not be set
+ assertEquals(0, n.flags & Notification.FLAG_NO_DISMISS);
+
+ // Avoid affecting other tests
+ mService.setOngoingDismissal(false);
+ }
+
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java
index 3a01c2d..98c156e 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java
@@ -72,7 +72,6 @@
import com.android.server.lights.LightsManager;
import com.android.server.notification.NotificationManagerService.NotificationAssistants;
import com.android.server.notification.NotificationManagerService.NotificationListeners;
-import com.android.server.pm.permission.PermissionManagerServiceInternal;
import com.android.server.uri.UriGrantsManagerInternal;
import com.android.server.utils.quota.MultiRateLimiter;
import com.android.server.wm.ActivityTaskManagerInternal;
@@ -169,9 +168,8 @@
mock(StatsManager.class), mock(TelephonyManager.class),
mock(ActivityManagerInternal.class),
mock(MultiRateLimiter.class), mock(PermissionHelper.class),
- mock(UsageStatsManagerInternal.class), mock(TelecomManager.class),
- mock(NotificationChannelLogger.class),
- mock(PermissionManagerServiceInternal.class));
+ mock(UsageStatsManagerInternal.class), mock (TelecomManager.class),
+ mock(NotificationChannelLogger.class));
} catch (SecurityException e) {
if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) {
throw e;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
index 61a6985..b8c94da 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
@@ -36,6 +36,8 @@
int countLogSmartSuggestionsVisible = 0;
Set<Integer> mChannelToastsSent = new HashSet<>();
+ public boolean ONGOING_DISMISSAL = false;
+
String stringArrayResourceValue;
@Nullable
NotificationAssistantAccessGrantedCallback mNotificationAssistantAccessGrantedCallback;
@@ -159,4 +161,9 @@
return mGetStrongAuthForUserReturnValue;
}
}
+
+ // Mock SystemProperties
+ protected void setOngoingDismissal(boolean ongoingDismissal) {
+ ONGOING_DISMISSAL = ongoingDismissal;
+ }
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 49edde5..12f124e 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -525,6 +525,13 @@
@Test
public void testZenUpgradeNotification() {
+ /**
+ * Commit a485ec65b5ba947d69158ad90905abf3310655cf disabled DND status change
+ * notification on watches. So, assume that the device is not watch.
+ */
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false);
+
// shows zen upgrade notification if stored settings says to shows,
// zen has not been updated, boot is completed
// and we're setting zen mode on
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index e663245..e5fe32a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -48,7 +48,7 @@
import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS;
import static android.os.Process.NOBODY_UID;
import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.InsetsState.ITYPE_IME;
+import static android.view.InsetsSource.ID_IME;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
@@ -3177,18 +3177,18 @@
public void testImeInsetsFrozenFlag_resetWhenReportedToBeImeInputTarget() {
final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
- mDisplayContent.getInsetsStateController().getSourceProvider(ITYPE_IME).setWindowContainer(
+ mDisplayContent.getInsetsStateController().getImeSourceProvider().setWindowContainer(
mImeWindow, null, null);
mImeWindow.getControllableInsetProvider().setServerVisible(true);
- InsetsSource imeSource = new InsetsSource(ITYPE_IME, ime());
+ InsetsSource imeSource = new InsetsSource(ID_IME, ime());
app.mAboveInsetsState.addSource(imeSource);
mDisplayContent.setImeLayeringTarget(app);
mDisplayContent.updateImeInputAndControlTarget(app);
InsetsState state = app.getInsetsState();
- assertFalse(state.getSource(imeSource.getId()).isVisible());
- assertTrue(state.getSource(imeSource.getId()).getFrame().isEmpty());
+ assertFalse(state.getOrCreateSource(imeSource.getId(), ime()).isVisible());
+ assertTrue(state.getOrCreateSource(imeSource.getId(), ime()).getFrame().isEmpty());
// Simulate app is closing and expect IME insets is frozen.
mDisplayContent.mOpeningApps.clear();
@@ -3211,8 +3211,8 @@
// Verify when IME is visible and the app can receive the right IME insets from policy.
makeWindowVisibleAndDrawn(app, mImeWindow);
state = app.getInsetsState();
- assertTrue(state.getSource(ITYPE_IME).isVisible());
- assertEquals(state.getSource(ITYPE_IME).getFrame(), imeSource.getFrame());
+ assertTrue(state.peekSource(ID_IME).isVisible());
+ assertEquals(state.peekSource(ID_IME).getFrame(), imeSource.getFrame());
}
@SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD })
@@ -3222,7 +3222,7 @@
final WindowState app1 = createWindow(null, TYPE_APPLICATION, "app1");
final WindowState app2 = createWindow(null, TYPE_APPLICATION, "app2");
- mDisplayContent.getInsetsStateController().getSourceProvider(ITYPE_IME).setWindowContainer(
+ mDisplayContent.getInsetsStateController().getImeSourceProvider().setWindowContainer(
mImeWindow, null, null);
mImeWindow.getControllableInsetProvider().setServerVisible(true);
@@ -3237,7 +3237,7 @@
mDisplayContent.getInsetsStateController().onInsetsModified(app1);
// Verify app1's IME insets is visible and app2's IME insets frozen flag set.
- assertTrue(app1.getInsetsState().peekSource(ITYPE_IME).isVisible());
+ assertTrue(app1.getInsetsState().peekSource(ID_IME).isVisible());
assertTrue(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput);
// Simulate switching to app2 to make it visible to be IME targets.
@@ -3256,7 +3256,7 @@
verify(app2.mClient, atLeastOnce()).resized(any(), anyBoolean(), any(),
insetsStateCaptor.capture(), anyBoolean(), anyBoolean(), anyInt(), anyInt(),
anyBoolean());
- assertFalse(app2.getInsetsState().getSource(ITYPE_IME).isVisible());
+ assertFalse(app2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
}
@Test
@@ -3288,7 +3288,7 @@
makeWindowVisibleAndDrawn(app1, app2);
final InsetsStateController controller = mDisplayContent.getInsetsStateController();
- controller.getSourceProvider(ITYPE_IME).setWindowContainer(
+ controller.getImeSourceProvider().setWindowContainer(
ime, null, null);
ime.getControllableInsetProvider().setServerVisible(true);
@@ -3306,8 +3306,8 @@
controller.onInsetsModified(app1);
// Expect all activities in split-screen will get IME insets visible state
- assertTrue(app1.getInsetsState().peekSource(ITYPE_IME).isVisible());
- assertTrue(app2.getInsetsState().peekSource(ITYPE_IME).isVisible());
+ assertTrue(app1.getInsetsState().peekSource(ID_IME).isVisible());
+ assertTrue(app2.getInsetsState().peekSource(ID_IME).isVisible());
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 3b34ba4..1ce8c61 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -105,6 +105,7 @@
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.when;
+import android.annotation.NonNull;
import android.app.ActivityTaskManager;
import android.app.WindowConfiguration;
import android.content.res.Configuration;
@@ -123,6 +124,7 @@
import android.util.ArraySet;
import android.util.DisplayMetrics;
import android.view.ContentRecordingSession;
+import android.view.Display;
import android.view.DisplayCutout;
import android.view.DisplayInfo;
import android.view.Gravity;
@@ -1557,13 +1559,13 @@
// If the visibility of insets state is changed, the rotated state should be updated too.
final InsetsState rotatedState = app.getFixedRotationTransformInsetsState();
final InsetsState state = mDisplayContent.getInsetsStateController().getRawInsetsState();
- assertEquals(state.getSource(ITYPE_STATUS_BAR).isVisible(),
- rotatedState.getSource(ITYPE_STATUS_BAR).isVisible());
- state.getSource(ITYPE_STATUS_BAR).setVisible(
- !rotatedState.getSource(ITYPE_STATUS_BAR).isVisible());
+ assertEquals(state.isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars()),
+ rotatedState.isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars()));
+ state.setSourceVisible(ITYPE_STATUS_BAR,
+ !rotatedState.isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars()));
mDisplayContent.getInsetsStateController().notifyInsetsChanged();
- assertEquals(state.getSource(ITYPE_STATUS_BAR).isVisible(),
- rotatedState.getSource(ITYPE_STATUS_BAR).isVisible());
+ assertEquals(state.isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars()),
+ rotatedState.isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars()));
final Rect outFrame = new Rect();
final Rect outInsets = new Rect();
@@ -1831,6 +1833,111 @@
}
@Test
+ public void testSecondaryInternalDisplayRotationFollowsDefaultDisplay() {
+ // Skip freezing so the unrelated conditions in updateRotationUnchecked won't disturb.
+ doNothing().when(mWm).startFreezingDisplay(anyInt(), anyInt(), any(), anyInt());
+
+ final DisplayRotationCoordinator coordinator =
+ mRootWindowContainer.getDisplayRotationCoordinator();
+ final DisplayContent defaultDisplayContent = mDisplayContent;
+ final DisplayRotation defaultDisplayRotation = defaultDisplayContent.getDisplayRotation();
+ coordinator.removeDefaultDisplayRotationChangedCallback();
+
+ DeviceStateController deviceStateController = mock(DeviceStateController.class);
+ when(deviceStateController.shouldMatchBuiltInDisplayOrientationToReverseDefaultDisplay())
+ .thenReturn(true);
+
+ // Create secondary display
+ final DisplayContent secondaryDisplayContent =
+ createSecondaryDisplayContent(Display.TYPE_INTERNAL, deviceStateController);
+ final DisplayRotation secondaryDisplayRotation =
+ secondaryDisplayContent.getDisplayRotation();
+ try {
+ // TestDisplayContent bypasses this method but we need it for this test
+ doCallRealMethod().when(secondaryDisplayRotation).updateRotationUnchecked(anyBoolean());
+
+ // TestDisplayContent creates this as a mock. Lets set it up to test our use case.
+ when(secondaryDisplayContent.mDeviceStateController
+ .shouldMatchBuiltInDisplayOrientationToReverseDefaultDisplay()).thenReturn(
+ true);
+
+ // Check that secondary display registered callback
+ assertEquals(secondaryDisplayRotation.mDefaultDisplayRotationChangedCallback,
+ coordinator.mDefaultDisplayRotationChangedCallback);
+
+ // Set the default display to a known orientation. This may be a zero or non-zero
+ // rotation since mDisplayInfo.logicalWidth/Height depends on the DUT's default display
+ defaultDisplayRotation.updateOrientation(SCREEN_ORIENTATION_PORTRAIT, false);
+ assertEquals(defaultDisplayRotation.mPortraitRotation,
+ defaultDisplayRotation.getRotation());
+ assertEquals(defaultDisplayRotation.mPortraitRotation,
+ coordinator.getDefaultDisplayCurrentRotation());
+
+ // Check that in the initial state, the secondary display is in the right rotation
+ assertRotationsAreCorrectlyReversed(defaultDisplayRotation.getRotation(),
+ secondaryDisplayRotation.getRotation());
+
+ // Update primary display rotation, check display coordinator rotation is the default
+ // display's landscape rotation, and that the secondary display rotation is correct.
+ defaultDisplayRotation.updateOrientation(SCREEN_ORIENTATION_LANDSCAPE, false);
+ assertEquals(defaultDisplayRotation.mLandscapeRotation,
+ defaultDisplayRotation.getRotation());
+ assertEquals(defaultDisplayRotation.mLandscapeRotation,
+ coordinator.getDefaultDisplayCurrentRotation());
+ assertRotationsAreCorrectlyReversed(defaultDisplayRotation.getRotation(),
+ secondaryDisplayRotation.getRotation());
+ } finally {
+ secondaryDisplayRotation.removeDefaultDisplayRotationChangedCallback();
+ }
+ }
+
+ @Test
+ public void testSecondaryNonInternalDisplayDoesNotFollowDefaultDisplay() {
+ // Skip freezing so the unrelated conditions in updateRotationUnchecked won't disturb.
+ doNothing().when(mWm).startFreezingDisplay(anyInt(), anyInt(), any(), anyInt());
+
+ final DisplayRotationCoordinator coordinator =
+ mRootWindowContainer.getDisplayRotationCoordinator();
+ coordinator.removeDefaultDisplayRotationChangedCallback();
+
+ DeviceStateController deviceStateController = mock(DeviceStateController.class);
+ when(deviceStateController.shouldMatchBuiltInDisplayOrientationToReverseDefaultDisplay())
+ .thenReturn(true);
+
+ // Create secondary non-internal displays
+ createSecondaryDisplayContent(Display.TYPE_EXTERNAL, deviceStateController);
+ assertNull(coordinator.mDefaultDisplayRotationChangedCallback);
+ createSecondaryDisplayContent(Display.TYPE_VIRTUAL, deviceStateController);
+ assertNull(coordinator.mDefaultDisplayRotationChangedCallback);
+ }
+
+ private DisplayContent createSecondaryDisplayContent(int displayType,
+ @NonNull DeviceStateController deviceStateController) {
+ final DisplayInfo secondaryDisplayInfo = new DisplayInfo();
+ secondaryDisplayInfo.copyFrom(mDisplayInfo);
+ secondaryDisplayInfo.type = displayType;
+
+ return new TestDisplayContent.Builder(mAtm, secondaryDisplayInfo)
+ .setDeviceStateController(deviceStateController)
+ .build();
+ }
+
+ private static void assertRotationsAreCorrectlyReversed(@Surface.Rotation int rotation1,
+ @Surface.Rotation int rotation2) {
+ if (rotation1 == ROTATION_0) {
+ assertEquals(rotation1, rotation2);
+ } else if (rotation1 == ROTATION_180) {
+ assertEquals(rotation1, rotation2);
+ } else if (rotation1 == ROTATION_90) {
+ assertEquals(ROTATION_270, rotation2);
+ } else if (rotation1 == ROTATION_270) {
+ assertEquals(ROTATION_90, rotation2);
+ } else {
+ throw new IllegalArgumentException("Unknown rotation: " + rotation1 + ", " + rotation2);
+ }
+ }
+
+ @Test
public void testRemoteRotation() {
final DisplayContent dc = mDisplayContent;
final DisplayRotation dr = dc.getDisplayRotation();
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyLayoutTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyLayoutTests.java
index 6733470..6656f4c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyLayoutTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyLayoutTests.java
@@ -43,6 +43,7 @@
import android.platform.test.annotations.Presubmit;
import android.view.DisplayInfo;
import android.view.InsetsFrameProvider;
+import android.view.InsetsSource;
import android.view.InsetsState;
import android.view.PrivacyIndicatorBounds;
import android.view.RoundedCorners;
@@ -162,19 +163,20 @@
InsetsStateController controller = mDisplayContent.getInsetsStateController();
controller.onPostLayout();
- InsetsSourceProvider statusBarProvider = controller.getSourceProvider(ITYPE_STATUS_BAR);
+ InsetsSourceProvider statusBarProvider = controller.peekSourceProvider(ITYPE_STATUS_BAR);
assertEquals(new Rect(0, 0, 500, 100), statusBarProvider.getSource().getFrame());
assertEquals(Insets.of(0, 100, 0, 0),
statusBarProvider.getSource().calculateInsets(new Rect(0, 0, 500, 500),
false /* ignoreVisibility */));
- InsetsSourceProvider topGesturesProvider = controller.getSourceProvider(ITYPE_TOP_GESTURES);
+ InsetsSourceProvider topGesturesProvider = controller.peekSourceProvider(
+ ITYPE_TOP_GESTURES);
assertEquals(new Rect(0, 0, 500, 100), topGesturesProvider.getSource().getFrame());
assertEquals(Insets.of(0, 100, 0, 0),
topGesturesProvider.getSource().calculateInsets(new Rect(0, 0, 500, 500),
false /* ignoreVisibility */));
- InsetsSourceProvider navigationBarProvider = controller.getSourceProvider(
+ InsetsSourceProvider navigationBarProvider = controller.peekSourceProvider(
ITYPE_NAVIGATION_BAR);
assertNotEquals(new Rect(0, 0, 500, 100), navigationBarProvider.getSource().getFrame());
}
@@ -193,7 +195,7 @@
mDisplayContent.getInsetsStateController().onPostLayout();
InsetsSourceProvider provider =
- mDisplayContent.getInsetsStateController().getSourceProvider(ITYPE_STATUS_BAR);
+ mDisplayContent.getInsetsStateController().peekSourceProvider(ITYPE_STATUS_BAR);
// In the new flexible insets setup, the insets frame should always respect the window
// layout result.
assertEquals(new Rect(0, 0, 500, 100), provider.getSource().getFrame());
@@ -255,7 +257,7 @@
mDisplayContent.getInsetsStateController().getRawInsetsState());
// Exclude comparing IME insets because currently the simulated layout only focuses on the
// insets from status bar and navigation bar.
- realInsetsState.removeSource(InsetsState.ITYPE_IME);
+ realInsetsState.removeSource(InsetsSource.ID_IME);
realInsetsState.removeSource(InsetsState.ITYPE_CAPTION_BAR);
assertEquals(new ToStringComparatorWrapper<>(realInsetsState),
@@ -270,9 +272,9 @@
.rotationForActivityInDifferentOrientation(eq(mWindow.mActivityRecord));
mWindow.mAboveInsetsState.set(
mDisplayContent.getInsetsStateController().getRawInsetsState());
- final Rect frame = mWindow.getInsetsState().getSource(ITYPE_STATUS_BAR).getFrame();
+ final Rect frame = mWindow.getInsetsState().peekSource(ITYPE_STATUS_BAR).getFrame();
mDisplayContent.rotateInDifferentOrientationIfNeeded(mWindow.mActivityRecord);
- final Rect rotatedFrame = mWindow.getInsetsState().getSource(ITYPE_STATUS_BAR).getFrame();
+ final Rect rotatedFrame = mWindow.getInsetsState().peekSource(ITYPE_STATUS_BAR).getFrame();
assertEquals(DISPLAY_WIDTH, frame.width());
assertEquals(DISPLAY_HEIGHT, rotatedFrame.width());
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index cf9ec81..6bb7769 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -17,8 +17,8 @@
package com.android.server.wm;
import static android.view.DisplayCutout.NO_CUTOUT;
+import static android.view.InsetsSource.ID_IME;
import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_IME;
import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
import static android.view.RoundedCorners.NO_ROUNDED_CORNERS;
import static android.view.Surface.ROTATION_0;
@@ -335,7 +335,7 @@
displayPolicy.layoutWindowLw(mNavBarWindow, null, mDisplayContent.mDisplayFrames);
displayPolicy.layoutWindowLw(mImeWindow, null, mDisplayContent.mDisplayFrames);
- final InsetsSource imeSource = state.peekSource(ITYPE_IME);
+ final InsetsSource imeSource = state.peekSource(ID_IME);
final InsetsSource navBarSource = state.peekSource(ITYPE_NAVIGATION_BAR);
assertNotNull(imeSource);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
index 45b30b2..4954e89 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
@@ -16,6 +16,7 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
@@ -30,7 +31,9 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static org.junit.Assert.assertEquals;
@@ -58,6 +61,8 @@
import androidx.test.filters.SmallTest;
+import com.android.internal.R;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -128,6 +133,69 @@
}
@Test
+ public void testOpenedCameraInSplitScreen_showToast() {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ spyOn(mTask);
+ spyOn(mDisplayRotationCompatPolicy);
+ doReturn(WINDOWING_MODE_MULTI_WINDOW).when(mActivity).getWindowingMode();
+ doReturn(WINDOWING_MODE_MULTI_WINDOW).when(mTask).getWindowingMode();
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ verify(mDisplayRotationCompatPolicy).showToast(
+ R.string.display_rotation_camera_compat_toast_in_split_screen);
+ }
+
+ @Test
+ public void testOnScreenRotationAnimationFinished_treatmentNotEnabled_doNotShowToast() {
+ when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ anyBoolean()))
+ .thenReturn(false);
+ spyOn(mDisplayRotationCompatPolicy);
+
+ mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished();
+
+ verify(mDisplayRotationCompatPolicy, never()).showToast(
+ R.string.display_rotation_camera_compat_toast_after_rotation);
+ }
+
+ @Test
+ public void testOnScreenRotationAnimationFinished_noOpenCamera_doNotShowToast() {
+ spyOn(mDisplayRotationCompatPolicy);
+
+ mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished();
+
+ verify(mDisplayRotationCompatPolicy, never()).showToast(
+ R.string.display_rotation_camera_compat_toast_after_rotation);
+ }
+
+ @Test
+ public void testOnScreenRotationAnimationFinished_notFullscreen_doNotShowToast() {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ doReturn(WINDOWING_MODE_MULTI_WINDOW).when(mActivity).getWindowingMode();
+ spyOn(mDisplayRotationCompatPolicy);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished();
+
+ verify(mDisplayRotationCompatPolicy, never()).showToast(
+ R.string.display_rotation_camera_compat_toast_after_rotation);
+ }
+
+ @Test
+ public void testOnScreenRotationAnimationFinished_showToast() {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ spyOn(mDisplayRotationCompatPolicy);
+
+ mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished();
+
+ verify(mDisplayRotationCompatPolicy).showToast(
+ R.string.display_rotation_camera_compat_toast_after_rotation);
+ }
+
+ @Test
public void testTreatmentNotEnabled_noForceRotationOrRefresh() throws Exception {
when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
/* checkDeviceConfig */ anyBoolean()))
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCoordinatorTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCoordinatorTests.java
new file mode 100644
index 0000000..4557df0
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCoordinatorTests.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static org.junit.Assert.assertEquals;
+
+import android.annotation.NonNull;
+import android.platform.test.annotations.Presubmit;
+import android.view.Surface;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Test class for {@link DisplayRotationCoordinator}
+ *
+ * Build/Install/Run:
+ * atest DisplayRotationCoordinatorTests
+ */
+@SmallTest
+@Presubmit
+public class DisplayRotationCoordinatorTests {
+
+ @NonNull
+ private final DisplayRotationCoordinator mCoordinator = new DisplayRotationCoordinator();
+
+ @Test
+ public void testDefaultDisplayRotationChangedWhenNoCallbackRegistered() {
+ // Does not cause NPE
+ mCoordinator.onDefaultDisplayRotationChanged(Surface.ROTATION_90);
+ }
+
+ @Test (expected = UnsupportedOperationException.class)
+ public void testSecondRegistrationWithoutRemovingFirst() {
+ Runnable callback1 = mock(Runnable.class);
+ Runnable callback2 = mock(Runnable.class);
+ mCoordinator.setDefaultDisplayRotationChangedCallback(callback1);
+ mCoordinator.setDefaultDisplayRotationChangedCallback(callback2);
+ assertEquals(callback1, mCoordinator.mDefaultDisplayRotationChangedCallback);
+ }
+
+ @Test
+ public void testSecondRegistrationAfterRemovingFirst() {
+ Runnable callback1 = mock(Runnable.class);
+ mCoordinator.setDefaultDisplayRotationChangedCallback(callback1);
+ mCoordinator.removeDefaultDisplayRotationChangedCallback();
+
+ Runnable callback2 = mock(Runnable.class);
+ mCoordinator.setDefaultDisplayRotationChangedCallback(callback2);
+
+ mCoordinator.onDefaultDisplayRotationChanged(Surface.ROTATION_90);
+ verify(callback2).run();
+ verify(callback1, never()).run();
+ }
+
+ @Test
+ public void testRegisterThenDefaultDisplayRotationChanged() {
+ Runnable callback = mock(Runnable.class);
+ mCoordinator.setDefaultDisplayRotationChangedCallback(callback);
+ assertEquals(Surface.ROTATION_0, mCoordinator.getDefaultDisplayCurrentRotation());
+ verify(callback, never()).run();
+
+ mCoordinator.onDefaultDisplayRotationChanged(Surface.ROTATION_90);
+ verify(callback).run();
+ assertEquals(Surface.ROTATION_90, mCoordinator.getDefaultDisplayCurrentRotation());
+ }
+
+ @Test
+ public void testDefaultDisplayRotationChangedThenRegister() {
+ mCoordinator.onDefaultDisplayRotationChanged(Surface.ROTATION_90);
+ Runnable callback = mock(Runnable.class);
+ mCoordinator.setDefaultDisplayRotationChangedCallback(callback);
+ verify(callback).run();
+ assertEquals(Surface.ROTATION_90, mCoordinator.getDefaultDisplayCurrentRotation());
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
index ed2b0a3..21e8ec4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
@@ -1135,7 +1135,7 @@
mDeviceStateController = mock(DeviceStateController.class);
mTarget = new DisplayRotation(sMockWm, mMockDisplayContent, mMockDisplayAddress,
mMockDisplayPolicy, mMockDisplayWindowSettings, mMockContext, new Object(),
- mDeviceStateController) {
+ mDeviceStateController, mock(DisplayRotationCoordinator.class)) {
@Override
DisplayRotationImmersiveAppCompatPolicy initImmersiveAppCompatPolicy(
WindowManagerService service, DisplayContent displayContent) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
index 4eaae9f..d1a41ae 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
@@ -552,7 +552,7 @@
/** Please use the {@link Builder} to create. */
DualDisplayContent(RootWindowContainer rootWindowContainer,
Display display) {
- super(rootWindowContainer, display);
+ super(rootWindowContainer, display, mock(DeviceStateController.class));
mFirstRoot = getGroupRoot(FEATURE_FIRST_ROOT);
mSecondRoot = getGroupRoot(FEATURE_SECOND_ROOT);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
index d4e860e..20bb549 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
@@ -16,7 +16,7 @@
package com.android.server.wm;
-import static android.view.InsetsState.ITYPE_IME;
+import static android.view.InsetsSource.ID_IME;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
@@ -39,7 +39,7 @@
@RunWith(WindowTestRunner.class)
public class ImeInsetsSourceProviderTest extends WindowTestsBase {
- private InsetsSource mImeSource = new InsetsSource(ITYPE_IME, ime());
+ private InsetsSource mImeSource = new InsetsSource(ID_IME, ime());
private ImeInsetsSourceProvider mImeProvider;
@Before
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
index 9887839..1a126cf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
@@ -276,12 +276,11 @@
policy.updateBarControlTarget(mAppWindow);
waitUntilWindowAnimatorIdle();
assertFalse(mDisplayContent.getInsetsStateController().getRawInsetsState()
- .getSource(ITYPE_STATUS_BAR).isVisible());
+ .isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars()));
assertFalse(mDisplayContent.getInsetsStateController().getRawInsetsState()
- .getSource(ITYPE_NAVIGATION_BAR).isVisible());
+ .isSourceOrDefaultVisible(ITYPE_NAVIGATION_BAR, navigationBars()));
- policy.showTransient(new int[]{ITYPE_STATUS_BAR, ITYPE_NAVIGATION_BAR},
- true /* isGestureOnSystemBar */);
+ policy.showTransient(navigationBars() | statusBars(), true /* isGestureOnSystemBar */);
waitUntilWindowAnimatorIdle();
final InsetsSourceControl[] controls =
mDisplayContent.getInsetsStateController().getControlsForDispatch(mAppWindow);
@@ -293,9 +292,9 @@
}
assertTrue(mDisplayContent.getInsetsStateController().getRawInsetsState()
- .getSource(ITYPE_STATUS_BAR).isVisible());
+ .isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars()));
assertTrue(mDisplayContent.getInsetsStateController().getRawInsetsState()
- .getSource(ITYPE_NAVIGATION_BAR).isVisible());
+ .isSourceOrDefaultVisible(ITYPE_NAVIGATION_BAR, navigationBars()));
}
@SetupWindows(addWindows = W_ACTIVITY)
@@ -308,11 +307,11 @@
spyOn(policy);
doNothing().when(policy).startAnimation(anyBoolean(), any());
policy.updateBarControlTarget(mAppWindow);
- policy.showTransient(new int[]{ITYPE_STATUS_BAR, ITYPE_NAVIGATION_BAR},
+ policy.showTransient(navigationBars() | statusBars(),
true /* isGestureOnSystemBar */);
waitUntilWindowAnimatorIdle();
- assertTrue(policy.isTransient(ITYPE_STATUS_BAR));
- assertFalse(policy.isTransient(ITYPE_NAVIGATION_BAR));
+ assertTrue(policy.isTransient(statusBars()));
+ assertFalse(policy.isTransient(navigationBars()));
final InsetsSourceControl[] controls =
mDisplayContent.getInsetsStateController().getControlsForDispatch(mAppWindow);
@@ -344,7 +343,7 @@
spyOn(policy);
doNothing().when(policy).startAnimation(anyBoolean(), any());
policy.updateBarControlTarget(mAppWindow);
- policy.showTransient(new int[]{ITYPE_STATUS_BAR, ITYPE_NAVIGATION_BAR},
+ policy.showTransient(navigationBars() | statusBars(),
true /* isGestureOnSystemBar */);
waitUntilWindowAnimatorIdle();
InsetsSourceControl[] controls =
@@ -362,11 +361,11 @@
final InsetsState clientState = mAppWindow.getInsetsState();
// The transient bar states for client should be invisible.
- assertFalse(clientState.getSource(ITYPE_STATUS_BAR).isVisible());
- assertFalse(clientState.getSource(ITYPE_NAVIGATION_BAR).isVisible());
+ assertFalse(clientState.isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars()));
+ assertFalse(clientState.isSourceOrDefaultVisible(ITYPE_NAVIGATION_BAR, navigationBars()));
// The original state shouldn't be modified.
- assertTrue(state.getSource(ITYPE_STATUS_BAR).isVisible());
- assertTrue(state.getSource(ITYPE_NAVIGATION_BAR).isVisible());
+ assertTrue(state.isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars()));
+ assertTrue(state.isSourceOrDefaultVisible(ITYPE_NAVIGATION_BAR, navigationBars()));
mAppWindow.setRequestedVisibleTypes(
navigationBars() | statusBars(), navigationBars() | statusBars());
@@ -393,13 +392,13 @@
spyOn(policy);
doNothing().when(policy).startAnimation(anyBoolean(), any());
policy.updateBarControlTarget(app);
- policy.showTransient(new int[]{ITYPE_STATUS_BAR, ITYPE_NAVIGATION_BAR},
+ policy.showTransient(navigationBars() | statusBars(),
true /* isGestureOnSystemBar */);
final InsetsSourceControl[] controls =
mDisplayContent.getInsetsStateController().getControlsForDispatch(app);
policy.updateBarControlTarget(app2);
- assertFalse(policy.isTransient(ITYPE_STATUS_BAR));
- assertFalse(policy.isTransient(ITYPE_NAVIGATION_BAR));
+ assertFalse(policy.isTransient(statusBars()));
+ assertFalse(policy.isTransient(navigationBars()));
}
private WindowState addNavigationBar() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
index 4d69979..74fde65 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
@@ -19,12 +19,10 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-import static android.view.InsetsState.ITYPE_CLIMATE_BAR;
-import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_IME;
-import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_STATUS_BAR;
+import static android.view.InsetsSource.ID_IME;
import static android.view.WindowInsets.Type.ime;
+import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.statusBars;
import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
@@ -48,6 +46,7 @@
import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
import android.util.SparseArray;
+import android.view.InsetsSource;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
@@ -63,6 +62,15 @@
@RunWith(WindowTestRunner.class)
public class InsetsStateControllerTest extends WindowTestsBase {
+ private static final int ID_STATUS_BAR =
+ InsetsSource.createId(null /* owner */, 0 /* index */, statusBars());
+ private static final int ID_NAVIGATION_BAR =
+ InsetsSource.createId(null /* owner */, 0 /* index */, navigationBars());
+ private static final int ID_CLIMATE_BAR =
+ InsetsSource.createId(null /* owner */, 1 /* index */, statusBars());
+ private static final int ID_EXTRA_NAVIGATION_BAR =
+ InsetsSource.createId(null /* owner */, 1 /* index */, navigationBars());
+
@Test
public void testStripForDispatch_navBar() {
final WindowState navBar = createWindow(null, TYPE_APPLICATION, "navBar");
@@ -72,14 +80,15 @@
// IME cannot be the IME target.
ime.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
- getController().getSourceProvider(ITYPE_STATUS_BAR).setWindowContainer(statusBar, null,
- null);
- getController().getSourceProvider(ITYPE_NAVIGATION_BAR).setWindowContainer(navBar, null,
- null);
- getController().getSourceProvider(ITYPE_IME).setWindowContainer(ime, null, null);
+ getController().getOrCreateSourceProvider(ID_STATUS_BAR, statusBars())
+ .setWindowContainer(statusBar, null, null);
+ getController().getOrCreateSourceProvider(ID_NAVIGATION_BAR, navigationBars())
+ .setWindowContainer(navBar, null, null);
+ getController().getOrCreateSourceProvider(ID_IME, ime())
+ .setWindowContainer(ime, null, null);
- assertNull(navBar.getInsetsState().peekSource(ITYPE_IME));
- assertNull(navBar.getInsetsState().peekSource(ITYPE_STATUS_BAR));
+ assertNull(navBar.getInsetsState().peekSource(ID_IME));
+ assertNull(navBar.getInsetsState().peekSource(ID_STATUS_BAR));
}
@Test
@@ -88,15 +97,15 @@
final WindowState navBar = createWindow(null, TYPE_APPLICATION, "navBar");
final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
- getController().getSourceProvider(ITYPE_STATUS_BAR).setWindowContainer(statusBar, null,
- null);
- getController().getSourceProvider(ITYPE_NAVIGATION_BAR).setWindowContainer(navBar, null,
- null);
+ getController().getOrCreateSourceProvider(ID_STATUS_BAR, statusBars())
+ .setWindowContainer(statusBar, null, null);
+ getController().getOrCreateSourceProvider(ID_NAVIGATION_BAR, navigationBars())
+ .setWindowContainer(navBar, null, null);
app.setWindowingMode(WINDOWING_MODE_PINNED);
- assertNull(app.getInsetsState().peekSource(ITYPE_STATUS_BAR));
- assertNull(app.getInsetsState().peekSource(ITYPE_NAVIGATION_BAR));
- assertNull(app.getInsetsState().peekSource(ITYPE_IME));
+ assertNull(app.getInsetsState().peekSource(ID_STATUS_BAR));
+ assertNull(app.getInsetsState().peekSource(ID_NAVIGATION_BAR));
+ assertNull(app.getInsetsState().peekSource(ID_IME));
}
@Test
@@ -105,14 +114,14 @@
final WindowState navBar = createWindow(null, TYPE_APPLICATION, "navBar");
final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
- getController().getSourceProvider(ITYPE_STATUS_BAR).setWindowContainer(statusBar, null,
- null);
- getController().getSourceProvider(ITYPE_NAVIGATION_BAR).setWindowContainer(navBar, null,
- null);
+ getController().getOrCreateSourceProvider(ID_STATUS_BAR, statusBars())
+ .setWindowContainer(statusBar, null, null);
+ getController().getOrCreateSourceProvider(ID_NAVIGATION_BAR, navigationBars())
+ .setWindowContainer(navBar, null, null);
app.setWindowingMode(WINDOWING_MODE_FREEFORM);
- assertNull(app.getInsetsState().peekSource(ITYPE_STATUS_BAR));
- assertNull(app.getInsetsState().peekSource(ITYPE_NAVIGATION_BAR));
+ assertNull(app.getInsetsState().peekSource(ID_STATUS_BAR));
+ assertNull(app.getInsetsState().peekSource(ID_NAVIGATION_BAR));
}
@Test
@@ -121,57 +130,58 @@
final WindowState navBar = createWindow(null, TYPE_APPLICATION, "navBar");
final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
- getController().getSourceProvider(ITYPE_STATUS_BAR).setWindowContainer(statusBar, null,
- null);
- getController().getSourceProvider(ITYPE_NAVIGATION_BAR).setWindowContainer(navBar, null,
- null);
+ getController().getOrCreateSourceProvider(ID_STATUS_BAR, statusBars())
+ .setWindowContainer(statusBar, null, null);
+ getController().getOrCreateSourceProvider(ID_NAVIGATION_BAR, navigationBars())
+ .setWindowContainer(navBar, null, null);
app.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
app.setAlwaysOnTop(true);
- assertNull(app.getInsetsState().peekSource(ITYPE_STATUS_BAR));
- assertNull(app.getInsetsState().peekSource(ITYPE_NAVIGATION_BAR));
+ assertNull(app.getInsetsState().peekSource(ID_STATUS_BAR));
+ assertNull(app.getInsetsState().peekSource(ID_NAVIGATION_BAR));
}
@SetupWindows(addWindows = W_INPUT_METHOD)
@Test
public void testStripForDispatch_independentSources() {
- getController().getSourceProvider(ITYPE_IME).setWindowContainer(mImeWindow, null, null);
+ getController().getOrCreateSourceProvider(ID_IME, ime())
+ .setWindowContainer(mImeWindow, null, null);
final WindowState app1 = createWindow(null, TYPE_APPLICATION, "app1");
final WindowState app2 = createWindow(null, TYPE_APPLICATION, "app2");
- app1.mAboveInsetsState.addSource(getController().getRawInsetsState().getSource(ITYPE_IME));
+ app1.mAboveInsetsState.addSource(getController().getRawInsetsState().peekSource(ID_IME));
- getController().getRawInsetsState().setSourceVisible(ITYPE_IME, true);
- assertFalse(app2.getInsetsState().getSource(ITYPE_IME)
- .isVisible());
- assertTrue(app1.getInsetsState().getSource(ITYPE_IME)
- .isVisible());
+ getController().getRawInsetsState().setSourceVisible(ID_IME, true);
+ assertFalse(app2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
+ assertTrue(app1.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
}
@SetupWindows(addWindows = W_INPUT_METHOD)
@Test
public void testStripForDispatch_belowIme() {
- getController().getSourceProvider(ITYPE_IME).setWindowContainer(mImeWindow, null, null);
+ getController().getOrCreateSourceProvider(ID_IME, ime())
+ .setWindowContainer(mImeWindow, null, null);
final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
- app.mAboveInsetsState.getSource(ITYPE_IME).setVisible(true);
- app.mAboveInsetsState.getSource(ITYPE_IME).setFrame(mImeWindow.getFrame());
+ app.mAboveInsetsState.getOrCreateSource(ID_IME, ime())
+ .setVisible(true)
+ .setFrame(mImeWindow.getFrame());
- getController().getRawInsetsState().setSourceVisible(ITYPE_IME, true);
- assertTrue(app.getInsetsState().getSource(ITYPE_IME).isVisible());
+ getController().getRawInsetsState().setSourceVisible(ID_IME, true);
+ assertTrue(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
}
@SetupWindows(addWindows = W_INPUT_METHOD)
@Test
public void testStripForDispatch_aboveIme() {
- getController().getSourceProvider(ITYPE_IME).setWindowContainer(mImeWindow, null, null);
+ getController().getOrCreateSourceProvider(ID_IME, ime())
+ .setWindowContainer(mImeWindow, null, null);
final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
- getController().getRawInsetsState().setSourceVisible(ITYPE_IME, true);
- assertFalse(app.getInsetsState().getSource(ITYPE_IME)
- .isVisible());
+ getController().getRawInsetsState().setSourceVisible(ID_IME, true);
+ assertFalse(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
}
@SetupWindows(addWindows = W_INPUT_METHOD)
@@ -185,7 +195,8 @@
// Make IME and stay visible during the test.
mImeWindow.setHasSurface(true);
- getController().getSourceProvider(ITYPE_IME).setWindowContainer(mImeWindow, null, null);
+ getController().getOrCreateSourceProvider(ID_IME, ime())
+ .setWindowContainer(mImeWindow, null, null);
getController().onImeControlTargetChanged(
mDisplayContent.getImeInputTarget().getWindowState());
mDisplayContent.getImeInputTarget().getWindowState().setRequestedVisibleTypes(ime(), ime());
@@ -205,9 +216,8 @@
mDisplayContent.applySurfaceChangesTransaction();
// app won't get visible IME insets while above IME even when IME is visible.
- assertTrue(getController().getRawInsetsState().getSourceOrDefaultVisibility(ITYPE_IME));
- assertFalse(app.getInsetsState().getSource(ITYPE_IME)
- .isVisible());
+ assertTrue(getController().getRawInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
+ assertFalse(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
// Reset invocation counter.
clearInvocations(app);
@@ -216,20 +226,22 @@
app.mAttrs.flags &= ~FLAG_NOT_FOCUSABLE;
mDisplayContent.computeImeTarget(true);
mDisplayContent.applySurfaceChangesTransaction();
- app.mAboveInsetsState.getSource(ITYPE_IME).setVisible(true);
- app.mAboveInsetsState.getSource(ITYPE_IME).setFrame(mImeWindow.getFrame());
+ app.mAboveInsetsState.getOrCreateSource(ID_IME, ime())
+ .setVisible(true)
+ .setFrame(mImeWindow.getFrame());
// Make sure app got notified.
verify(app, atLeastOnce()).notifyInsetsChanged();
// app will get visible IME insets while below IME.
- assertTrue(app.getInsetsState().getSource(ITYPE_IME).isVisible());
+ assertTrue(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
}
@SetupWindows(addWindows = W_INPUT_METHOD)
@Test
public void testStripForDispatch_childWindow_altFocusable() {
- getController().getSourceProvider(ITYPE_IME).setWindowContainer(mImeWindow, null, null);
+ getController().getOrCreateSourceProvider(ID_IME, ime())
+ .setWindowContainer(mImeWindow, null, null);
final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
final WindowState child = createWindow(app, TYPE_APPLICATION, "child");
@@ -241,20 +253,20 @@
mDisplayContent.setLayoutNeeded();
mDisplayContent.applySurfaceChangesTransaction();
- getController().getRawInsetsState().setSourceVisible(ITYPE_IME, true);
- assertTrue(app.getInsetsState().getSource(ITYPE_IME).isVisible());
- assertFalse(child.getInsetsState().getSource(ITYPE_IME)
- .isVisible());
+ getController().getRawInsetsState().setSourceVisible(ID_IME, true);
+ assertTrue(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
+ assertFalse(child.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
}
@SetupWindows(addWindows = W_INPUT_METHOD)
@Test
public void testStripForDispatch_childWindow_splitScreen() {
- getController().getSourceProvider(ITYPE_IME).setWindowContainer(mImeWindow, null, null);
+ getController().getOrCreateSourceProvider(ID_IME, ime())
+ .setWindowContainer(mImeWindow, null, null);
final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
final WindowState child = createWindow(app, TYPE_APPLICATION, "child");
- app.mAboveInsetsState.addSource(getController().getRawInsetsState().peekSource(ITYPE_IME));
+ app.mAboveInsetsState.addSource(getController().getRawInsetsState().peekSource(ID_IME));
child.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
child.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
@@ -262,10 +274,9 @@
mDisplayContent.setLayoutNeeded();
mDisplayContent.applySurfaceChangesTransaction();
- getController().getRawInsetsState().setSourceVisible(ITYPE_IME, true);
- assertTrue(app.getInsetsState().getSource(ITYPE_IME).isVisible());
- assertFalse(child.getInsetsState().getSource(ITYPE_IME)
- .isVisible());
+ getController().getRawInsetsState().setSourceVisible(ID_IME, true);
+ assertTrue(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
+ assertFalse(child.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
}
@Test
@@ -277,20 +288,21 @@
ime.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
WindowContainerInsetsSourceProvider statusBarProvider =
- getController().getSourceProvider(ITYPE_STATUS_BAR);
+ getController().getOrCreateSourceProvider(ID_STATUS_BAR, statusBars());
final SparseArray<TriConsumer<DisplayFrames, WindowContainer, Rect>> imeOverrideProviders =
new SparseArray<>();
imeOverrideProviders.put(TYPE_INPUT_METHOD, ((displayFrames, windowState, rect) ->
rect.set(0, 1, 2, 3)));
statusBarProvider.setWindowContainer(statusBar, null, imeOverrideProviders);
- getController().getSourceProvider(ITYPE_IME).setWindowContainer(ime, null, null);
+ getController().getOrCreateSourceProvider(ID_IME, ime())
+ .setWindowContainer(ime, null, null);
statusBar.setControllableInsetProvider(statusBarProvider);
statusBar.updateSourceFrame(statusBar.getFrame());
statusBarProvider.onPostLayout();
final InsetsState state = ime.getInsetsState();
- assertEquals(new Rect(0, 1, 2, 3), state.getSource(ITYPE_STATUS_BAR).getFrame());
+ assertEquals(new Rect(0, 1, 2, 3), state.peekSource(ID_STATUS_BAR).getFrame());
}
@Test
@@ -300,15 +312,14 @@
final WindowState climateBar = createWindow(null, TYPE_APPLICATION, "climateBar");
final WindowState extraNavBar = createWindow(null, TYPE_APPLICATION, "extraNavBar");
final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
- getController().getSourceProvider(ITYPE_STATUS_BAR).setWindowContainer(statusBar, null,
- null);
- getController().getSourceProvider(ITYPE_NAVIGATION_BAR).setWindowContainer(navBar, null,
- null);
- getController().getSourceProvider(ITYPE_CLIMATE_BAR).setWindowContainer(climateBar, null,
- null);
- getController().getSourceProvider(ITYPE_EXTRA_NAVIGATION_BAR).setWindowContainer(
- extraNavBar, null,
- null);
+ getController().getOrCreateSourceProvider(ID_STATUS_BAR, statusBars())
+ .setWindowContainer(statusBar, null, null);
+ getController().getOrCreateSourceProvider(ID_NAVIGATION_BAR, navigationBars())
+ .setWindowContainer(navBar, null, null);
+ getController().getOrCreateSourceProvider(ID_CLIMATE_BAR, statusBars())
+ .setWindowContainer(climateBar, null, null);
+ getController().getOrCreateSourceProvider(ID_EXTRA_NAVIGATION_BAR, navigationBars())
+ .setWindowContainer(extraNavBar, null, null);
getController().onBarControlTargetChanged(app, null, app, null);
InsetsSourceControl[] controls = getController().getControlsForDispatch(app);
assertEquals(4, controls.length);
@@ -318,8 +329,8 @@
public void testControlRevoked() {
final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar");
final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
- getController().getSourceProvider(ITYPE_STATUS_BAR).setWindowContainer(statusBar, null,
- null);
+ getController().getOrCreateSourceProvider(ID_STATUS_BAR, statusBars())
+ .setWindowContainer(statusBar, null, null);
getController().onBarControlTargetChanged(app, null, null, null);
assertNotNull(getController().getControlsForDispatch(app));
getController().onBarControlTargetChanged(null, null, null, null);
@@ -330,8 +341,8 @@
public void testControlRevoked_animation() {
final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar");
final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
- getController().getSourceProvider(ITYPE_STATUS_BAR).setWindowContainer(statusBar, null,
- null);
+ getController().getOrCreateSourceProvider(ID_STATUS_BAR, statusBars())
+ .setWindowContainer(statusBar, null, null);
getController().onBarControlTargetChanged(app, null, null, null);
assertNotNull(getController().getControlsForDispatch(app));
statusBar.cancelAnimation();
@@ -343,21 +354,22 @@
final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar");
final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
final WindowContainerInsetsSourceProvider provider = getController()
- .getSourceProvider(ITYPE_STATUS_BAR);
+ .getOrCreateSourceProvider(ID_STATUS_BAR, statusBars());
provider.setWindowContainer(statusBar, null, null);
final InsetsState rotatedState = new InsetsState(app.getInsetsState(),
true /* copySources */);
+ rotatedState.getOrCreateSource(ID_STATUS_BAR, statusBars());
spyOn(app.mToken);
doReturn(rotatedState).when(app.mToken).getFixedRotationTransformInsetsState();
- assertTrue(rotatedState.getSource(ITYPE_STATUS_BAR).isVisible());
+ assertTrue(rotatedState.isSourceOrDefaultVisible(ID_STATUS_BAR, statusBars()));
provider.getSource().setVisible(false);
- mDisplayContent.getInsetsPolicy().showTransient(new int[] { ITYPE_STATUS_BAR },
+ mDisplayContent.getInsetsPolicy().showTransient(statusBars(),
true /* isGestureOnSystemBar */);
- assertTrue(mDisplayContent.getInsetsPolicy().isTransient(ITYPE_STATUS_BAR));
- assertFalse(app.getInsetsState().getSource(ITYPE_STATUS_BAR).isVisible());
+ assertTrue(mDisplayContent.getInsetsPolicy().isTransient(statusBars()));
+ assertFalse(app.getInsetsState().isSourceOrDefaultVisible(ID_STATUS_BAR, statusBars()));
}
@Test
@@ -366,18 +378,18 @@
final WindowState statusBar = createTestWindow("statusBar");
final WindowState navBar = createTestWindow("navBar");
- getController().getSourceProvider(ITYPE_STATUS_BAR).setWindowContainer(statusBar, null,
- null);
+ getController().getOrCreateSourceProvider(ID_STATUS_BAR, statusBars())
+ .setWindowContainer(statusBar, null, null);
- assertNull(app.mAboveInsetsState.peekSource(ITYPE_STATUS_BAR));
- assertNull(statusBar.mAboveInsetsState.peekSource(ITYPE_STATUS_BAR));
- assertNull(navBar.mAboveInsetsState.peekSource(ITYPE_STATUS_BAR));
+ assertNull(app.mAboveInsetsState.peekSource(ID_STATUS_BAR));
+ assertNull(statusBar.mAboveInsetsState.peekSource(ID_STATUS_BAR));
+ assertNull(navBar.mAboveInsetsState.peekSource(ID_STATUS_BAR));
getController().updateAboveInsetsState(true /* notifyInsetsChange */);
- assertNotNull(app.mAboveInsetsState.peekSource(ITYPE_STATUS_BAR));
- assertNull(statusBar.mAboveInsetsState.peekSource(ITYPE_STATUS_BAR));
- assertNull(navBar.mAboveInsetsState.peekSource(ITYPE_STATUS_BAR));
+ assertNotNull(app.mAboveInsetsState.peekSource(ID_STATUS_BAR));
+ assertNull(statusBar.mAboveInsetsState.peekSource(ID_STATUS_BAR));
+ assertNull(navBar.mAboveInsetsState.peekSource(ID_STATUS_BAR));
verify(app, atLeastOnce()).notifyInsetsChanged();
}
@@ -388,18 +400,18 @@
final WindowState statusBar = createTestWindow("statusBar");
final WindowState navBar = createTestWindow("navBar");
- getController().getSourceProvider(ITYPE_STATUS_BAR).setWindowContainer(statusBar, null,
- null);
- getController().getSourceProvider(ITYPE_NAVIGATION_BAR).setWindowContainer(navBar, null,
- null);
+ getController().getOrCreateSourceProvider(ID_STATUS_BAR, statusBars())
+ .setWindowContainer(statusBar, null, null);
+ getController().getOrCreateSourceProvider(ID_NAVIGATION_BAR, navigationBars())
+ .setWindowContainer(navBar, null, null);
- assertNull(app.mAboveInsetsState.peekSource(ITYPE_STATUS_BAR));
- assertNull(app.mAboveInsetsState.peekSource(ITYPE_NAVIGATION_BAR));
+ assertNull(app.mAboveInsetsState.peekSource(ID_STATUS_BAR));
+ assertNull(app.mAboveInsetsState.peekSource(ID_NAVIGATION_BAR));
getController().updateAboveInsetsState(true /* notifyInsetsChange */);
- assertNotNull(app.mAboveInsetsState.peekSource(ITYPE_STATUS_BAR));
- assertNotNull(app.mAboveInsetsState.peekSource(ITYPE_NAVIGATION_BAR));
+ assertNotNull(app.mAboveInsetsState.peekSource(ID_STATUS_BAR));
+ assertNotNull(app.mAboveInsetsState.peekSource(ID_NAVIGATION_BAR));
verify(app, atLeastOnce()).notifyInsetsChanged();
}
@@ -411,38 +423,40 @@
final WindowState statusBar = createTestWindow("statusBar");
final WindowState navBar = createTestWindow("navBar");
- getController().getSourceProvider(ITYPE_IME).setWindowContainer(ime, null, null);
+ final InsetsSourceProvider imeSourceProvider =
+ getController().getOrCreateSourceProvider(ID_IME, ime());
+ imeSourceProvider.setWindowContainer(ime, null, null);
waitUntilHandlersIdle();
clearInvocations(mDisplayContent);
- getController().getSourceProvider(ITYPE_IME).setClientVisible(true);
+ imeSourceProvider.setClientVisible(true);
waitUntilHandlersIdle();
// The visibility change should trigger a traversal to notify the change.
verify(mDisplayContent).notifyInsetsChanged(any());
- getController().getSourceProvider(ITYPE_STATUS_BAR).setWindowContainer(statusBar, null,
- null);
- getController().getSourceProvider(ITYPE_NAVIGATION_BAR).setWindowContainer(navBar, null,
- null);
+ getController().getOrCreateSourceProvider(ID_STATUS_BAR, statusBars())
+ .setWindowContainer(statusBar, null, null);
+ getController().getOrCreateSourceProvider(ID_NAVIGATION_BAR, navigationBars())
+ .setWindowContainer(navBar, null, null);
getController().updateAboveInsetsState(false /* notifyInsetsChange */);
// ime is below others.
- assertNull(app.mAboveInsetsState.peekSource(ITYPE_IME));
- assertNull(statusBar.mAboveInsetsState.peekSource(ITYPE_IME));
- assertNull(navBar.mAboveInsetsState.peekSource(ITYPE_IME));
- assertNotNull(ime.mAboveInsetsState.peekSource(ITYPE_STATUS_BAR));
- assertNotNull(ime.mAboveInsetsState.peekSource(ITYPE_NAVIGATION_BAR));
+ assertNull(app.mAboveInsetsState.peekSource(ID_IME));
+ assertNull(statusBar.mAboveInsetsState.peekSource(ID_IME));
+ assertNull(navBar.mAboveInsetsState.peekSource(ID_IME));
+ assertNotNull(ime.mAboveInsetsState.peekSource(ID_STATUS_BAR));
+ assertNotNull(ime.mAboveInsetsState.peekSource(ID_NAVIGATION_BAR));
ime.getParent().positionChildAt(POSITION_TOP, ime, true /* includingParents */);
getController().updateAboveInsetsState(true /* notifyInsetsChange */);
// ime is above others.
- assertNotNull(app.mAboveInsetsState.peekSource(ITYPE_IME));
- assertNotNull(statusBar.mAboveInsetsState.peekSource(ITYPE_IME));
- assertNotNull(navBar.mAboveInsetsState.peekSource(ITYPE_IME));
- assertNull(ime.mAboveInsetsState.peekSource(ITYPE_STATUS_BAR));
- assertNull(ime.mAboveInsetsState.peekSource(ITYPE_NAVIGATION_BAR));
+ assertNotNull(app.mAboveInsetsState.peekSource(ID_IME));
+ assertNotNull(statusBar.mAboveInsetsState.peekSource(ID_IME));
+ assertNotNull(navBar.mAboveInsetsState.peekSource(ID_IME));
+ assertNull(ime.mAboveInsetsState.peekSource(ID_STATUS_BAR));
+ assertNull(ime.mAboveInsetsState.peekSource(ID_NAVIGATION_BAR));
verify(ime, atLeastOnce()).notifyInsetsChanged();
verify(app, atLeastOnce()).notifyInsetsChanged();
@@ -456,7 +470,8 @@
final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, imeToken, "ime");
final WindowState app = createTestWindow("app");
- getController().getSourceProvider(ITYPE_IME).setWindowContainer(ime, null, null);
+ getController().getOrCreateSourceProvider(ID_IME, ime())
+ .setWindowContainer(ime, null, null);
ime.getControllableInsetProvider().setServerVisible(true);
app.mActivityRecord.setVisibility(true);
@@ -468,7 +483,7 @@
assertTrue(ime.getControllableInsetProvider().getSource().isVisible());
getController().updateAboveInsetsState(true /* notifyInsetsChange */);
- assertNotNull(app.getInsetsState().peekSource(ITYPE_IME));
+ assertNotNull(app.getInsetsState().peekSource(ID_IME));
verify(app, atLeastOnce()).notifyInsetsChanged();
// Expect the app will still get IME insets even when the app was invisible.
@@ -476,7 +491,7 @@
app.mActivityRecord.setVisible(false);
app.setHasSurface(false);
getController().updateAboveInsetsState(true /* notifyInsetsChange */);
- assertNotNull(app.getInsetsState().peekSource(ITYPE_IME));
+ assertNotNull(app.getInsetsState().peekSource(ID_IME));
verify(app, atLeastOnce()).notifyInsetsChanged();
// Expect the app will get IME insets when the app is requesting visible.
@@ -484,19 +499,19 @@
app.mActivityRecord.setVisibility(true);
assertTrue(app.isVisibleRequested());
getController().updateAboveInsetsState(true /* notifyInsetsChange */);
- assertNotNull(app.getInsetsState().peekSource(ITYPE_IME));
+ assertNotNull(app.getInsetsState().peekSource(ID_IME));
verify(app, atLeastOnce()).notifyInsetsChanged();
}
@Test
public void testDispatchGlobalInsets() {
final WindowState navBar = createWindow(null, TYPE_APPLICATION, "navBar");
- getController().getSourceProvider(ITYPE_NAVIGATION_BAR).setWindowContainer(navBar, null,
- null);
+ getController().getOrCreateSourceProvider(ID_NAVIGATION_BAR, navigationBars())
+ .setWindowContainer(navBar, null, null);
final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
- assertNull(app.getInsetsState().peekSource(ITYPE_NAVIGATION_BAR));
+ assertNull(app.getInsetsState().peekSource(ID_NAVIGATION_BAR));
app.mAttrs.receiveInsetsIgnoringZOrder = true;
- assertNotNull(app.getInsetsState().peekSource(ITYPE_NAVIGATION_BAR));
+ assertNotNull(app.getInsetsState().peekSource(ID_NAVIGATION_BAR));
}
@SetupWindows(addWindows = W_INPUT_METHOD)
@@ -506,7 +521,8 @@
final WindowState app2 = createTestWindow("app2");
makeWindowVisible(mImeWindow);
- final InsetsSourceProvider imeInsetsProvider = getController().getSourceProvider(ITYPE_IME);
+ final InsetsSourceProvider imeInsetsProvider =
+ getController().getOrCreateSourceProvider(ID_IME, ime());
imeInsetsProvider.setWindowContainer(mImeWindow, null, null);
imeInsetsProvider.updateSourceFrame(mImeWindow.getFrame());
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
index ead1a86..12b7c9d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
@@ -51,7 +51,7 @@
* Tests for the {@link LetterboxConfiguration} class.
*
* Build/Install/Run:
- * atest WmTests:LetterboxConfigurationTests
+ * atest WmTests:LetterboxConfigurationTest
*/
@SmallTest
@Presubmit
@@ -243,18 +243,18 @@
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
DEVICE_CONFIG_KEY_ENABLE_COMPAT_FAKE_FOCUS, "true", false);
mLetterboxConfiguration.setIsCompatFakeFocusEnabled(false);
- assertFalse(mLetterboxConfiguration.isCompatFakeFocusEnabledOnDevice());
+ assertFalse(mLetterboxConfiguration.isCompatFakeFocusEnabled());
// Set runtime flag to false and build time flag to true
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
DEVICE_CONFIG_KEY_ENABLE_COMPAT_FAKE_FOCUS, "false", false);
mLetterboxConfiguration.setIsCompatFakeFocusEnabled(true);
- assertFalse(mLetterboxConfiguration.isCompatFakeFocusEnabledOnDevice());
+ assertFalse(mLetterboxConfiguration.isCompatFakeFocusEnabled());
// Set runtime flag to true so that both are enabled
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
DEVICE_CONFIG_KEY_ENABLE_COMPAT_FAKE_FOCUS, "true", false);
- assertTrue(mLetterboxConfiguration.isCompatFakeFocusEnabledOnDevice());
+ assertTrue(mLetterboxConfiguration.isCompatFakeFocusEnabled());
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
DEVICE_CONFIG_KEY_ENABLE_COMPAT_FAKE_FOCUS, Boolean.toString(wasFakeFocusEnabled),
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index 110ef89..d8037f6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -20,8 +20,10 @@
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS;
import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT;
import static android.content.pm.ActivityInfo.OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION;
@@ -37,6 +39,7 @@
import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS;
import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -71,6 +74,7 @@
import com.android.internal.R;
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
import org.junit.Before;
@@ -579,6 +583,42 @@
/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED), SCREEN_ORIENTATION_UNSPECIFIED);
}
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
+ OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
+ public void testOverrideOrientationIfNeeded_whenCameraNotActive_returnsUnchanged() {
+ doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled(anyBoolean());
+
+ // Recreate DisplayContent with DisplayRotationCompatPolicy
+ mActivity = setUpActivityWithComponent();
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
+ doReturn(false).when(mDisplayContent.mDisplayRotationCompatPolicy)
+ .isActivityEligibleForOrientationOverride(eq(mActivity));
+
+ assertEquals(mController.overrideOrientationIfNeeded(
+ /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED), SCREEN_ORIENTATION_UNSPECIFIED);
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
+ OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
+ public void testOverrideOrientationIfNeeded_whenCameraActive_returnsPortrait() {
+ doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled(anyBoolean());
+
+ // Recreate DisplayContent with DisplayRotationCompatPolicy
+ mActivity = setUpActivityWithComponent();
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
+ doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy)
+ .isActivityEligibleForOrientationOverride(eq(mActivity));
+
+ assertEquals(mController.overrideOrientationIfNeeded(
+ /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED), SCREEN_ORIENTATION_PORTRAIT);
+ }
+
// shouldUseDisplayLandscapeNaturalOrientation
@Test
@@ -629,6 +669,72 @@
assertFalse(mController.shouldUseDisplayLandscapeNaturalOrientation());
}
+ @Test
+ @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
+ public void testShouldSendFakeFocus_overrideEnabled_returnsTrue() {
+ doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertTrue(mController.shouldSendFakeFocus());
+ }
+
+ @Test
+ @DisableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
+ public void testShouldSendFakeFocus_overrideDisabled_returnsFalse() {
+ doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertFalse(mController.shouldSendFakeFocus());
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
+ public void testIsCompatFakeFocusEnabled_propertyDisabledAndOverrideEnabled_fakeFocusDisabled()
+ throws Exception {
+ doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+ mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ false);
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertFalse(mController.shouldSendFakeFocus());
+ }
+
+ @Test
+ @DisableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
+ public void testIsCompatFakeFocusEnabled_propertyEnabled_noOverride_fakeFocusEnabled()
+ throws Exception {
+ doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+ mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ true);
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertTrue(mController.shouldSendFakeFocus());
+ }
+
+ @Test
+ public void testIsCompatFakeFocusEnabled_propertyDisabled_fakeFocusDisabled()
+ throws Exception {
+ doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+ mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ false);
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertFalse(mController.shouldSendFakeFocus());
+ }
+
+ @Test
+ public void testIsCompatFakeFocusEnabled_propertyEnabled_fakeFocusEnabled()
+ throws Exception {
+ doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+ mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ true);
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertTrue(mController.shouldSendFakeFocus());
+ }
+
private void mockThatProperty(String propertyName, boolean value) throws Exception {
Property property = new Property(propertyName, /* value */ value, /* packageName */ "",
/* className */ "");
diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
index bcaf886..0037e57 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
@@ -24,7 +24,9 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import android.hardware.display.DisplayManager;
@@ -289,6 +291,14 @@
assertEquals(FRAME_RATE_VOTE_NONE, overrideWindow.mFrameRateVote);
assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
+
+ // If there will be display size change when switching from preferred mode to default mode,
+ // then keep the current preferred mode during animating.
+ mDisplayInfo = spy(mDisplayInfo);
+ final Mode defaultMode = new Mode(4321 /* width */, 1234 /* height */, LOW_REFRESH_RATE);
+ doReturn(defaultMode).when(mDisplayInfo).getDefaultMode();
+ mPolicy = new RefreshRatePolicy(mWm, mDisplayInfo, mDenylist);
+ assertEquals(LOW_MODE_ID, mPolicy.getPreferredModeId(overrideWindow));
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index b8d31ab..6db0f27 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -16,8 +16,10 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
@@ -56,8 +58,6 @@
import static com.android.server.wm.ActivityRecord.State.RESUMED;
import static com.android.server.wm.ActivityRecord.State.STOPPED;
import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING;
-import static com.android.server.wm.LetterboxConfiguration.PROPERTY_COMPAT_FAKE_FOCUS_OPT_IN;
-import static com.android.server.wm.LetterboxConfiguration.PROPERTY_COMPAT_FAKE_FOCUS_OPT_OUT;
import static com.android.server.wm.WindowContainer.POSITION_TOP;
import static com.google.common.truth.Truth.assertThat;
@@ -1895,6 +1895,43 @@
}
@Test
+ @EnableCompatChanges({ActivityInfo.OVERRIDE_RESPECT_REQUESTED_ORIENTATION})
+ public void testOverrideRespectRequestedOrientationIsEnabled_orientationIsRespected() {
+ // Set up a display in landscape
+ setUpDisplaySizeWithApp(2800, 1400);
+
+ final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */ false,
+ RESIZE_MODE_UNRESIZEABLE, SCREEN_ORIENTATION_PORTRAIT);
+ activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+
+ // Display should be rotated.
+ assertEquals(SCREEN_ORIENTATION_PORTRAIT, activity.mDisplayContent.getOrientation());
+
+ // No size compat mode
+ assertFalse(activity.inSizeCompatMode());
+ }
+
+ @Test
+ @EnableCompatChanges({ActivityInfo.OVERRIDE_RESPECT_REQUESTED_ORIENTATION})
+ public void testOverrideRespectRequestedOrientationIsEnabled_multiWindow_orientationIgnored() {
+ // Set up a display in landscape
+ setUpDisplaySizeWithApp(2800, 1400);
+
+ final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */ false,
+ RESIZE_MODE_UNRESIZEABLE, SCREEN_ORIENTATION_PORTRAIT);
+ TaskFragment taskFragment = activity.getTaskFragment();
+ spyOn(taskFragment);
+ activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ doReturn(WINDOWING_MODE_MULTI_WINDOW).when(taskFragment).getWindowingMode();
+
+ // Display should not be rotated.
+ assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, activity.mDisplayContent.getOrientation());
+
+ // No size compat mode
+ assertFalse(activity.inSizeCompatMode());
+ }
+
+ @Test
public void testSplitAspectRatioForUnresizableLandscapeApps() {
// Set up a display in portrait and ignoring orientation request.
int screenWidth = 1400;
@@ -3654,7 +3691,8 @@
assertEquals(newDensity, mActivity.getConfiguration().densityDpi);
}
- private ActivityRecord setUpActivityForCompatFakeFocusTest() {
+ @Test
+ public void testShouldSendFakeFocus_compatFakeFocusEnabled() {
final ActivityRecord activity = new ActivityBuilder(mAtm)
.setCreateTask(true)
.setOnTop(true)
@@ -3663,69 +3701,40 @@
com.android.server.wm.SizeCompatTests.class.getName()))
.build();
final Task task = activity.getTask();
+ spyOn(activity.mLetterboxUiController);
+ doReturn(true).when(activity.mLetterboxUiController).shouldSendFakeFocus();
+
task.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
- spyOn(activity.mWmService.mLetterboxConfiguration);
- doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
- .isCompatFakeFocusEnabledOnDevice();
- return activity;
- }
-
- @Test
- @EnableCompatChanges({ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
- public void testShouldSendFakeFocus_overrideEnabled_returnsTrue() {
- ActivityRecord activity = setUpActivityForCompatFakeFocusTest();
-
assertTrue(activity.shouldSendCompatFakeFocus());
- }
- @Test
- @DisableCompatChanges({ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
- public void testShouldSendFakeFocus_overrideDisabled_returnsFalse() {
- ActivityRecord activity = setUpActivityForCompatFakeFocusTest();
+ task.setWindowingMode(WINDOWING_MODE_PINNED);
+ assertFalse(activity.shouldSendCompatFakeFocus());
+ task.setWindowingMode(WINDOWING_MODE_FREEFORM);
assertFalse(activity.shouldSendCompatFakeFocus());
}
@Test
- @EnableCompatChanges({ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
- public void testIsCompatFakeFocusEnabled_optOutPropertyAndOverrideEnabled_fakeFocusDisabled() {
- ActivityRecord activity = setUpActivityForCompatFakeFocusTest();
- doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
- .getPackageManagerProperty(any(), eq(PROPERTY_COMPAT_FAKE_FOCUS_OPT_OUT));
+ public void testShouldSendFakeFocus_compatFakeFocusDisabled() {
+ final ActivityRecord activity = new ActivityBuilder(mAtm)
+ .setCreateTask(true)
+ .setOnTop(true)
+ // Set the component to be that of the test class in order to enable compat changes
+ .setComponent(ComponentName.createRelative(mContext,
+ com.android.server.wm.SizeCompatTests.class.getName()))
+ .build();
+ final Task task = activity.getTask();
+ spyOn(activity.mLetterboxUiController);
+ doReturn(false).when(activity.mLetterboxUiController).shouldSendFakeFocus();
- assertFalse(activity.mWmService.mLetterboxConfiguration
- .isCompatFakeFocusEnabled(activity.info));
- }
+ task.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
+ assertFalse(activity.shouldSendCompatFakeFocus());
- @Test
- @DisableCompatChanges({ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
- public void testIsCompatFakeFocusEnabled_optInPropertyEnabled_noOverride_fakeFocusEnabled() {
- ActivityRecord activity = setUpActivityForCompatFakeFocusTest();
- doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
- .getPackageManagerProperty(any(), eq(PROPERTY_COMPAT_FAKE_FOCUS_OPT_IN));
+ task.setWindowingMode(WINDOWING_MODE_PINNED);
+ assertFalse(activity.shouldSendCompatFakeFocus());
- assertTrue(activity.mWmService.mLetterboxConfiguration
- .isCompatFakeFocusEnabled(activity.info));
- }
-
- @Test
- public void testIsCompatFakeFocusEnabled_optOutPropertyEnabled_fakeFocusDisabled() {
- ActivityRecord activity = setUpActivityForCompatFakeFocusTest();
- doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
- .getPackageManagerProperty(any(), eq(PROPERTY_COMPAT_FAKE_FOCUS_OPT_OUT));
-
- assertFalse(activity.mWmService.mLetterboxConfiguration
- .isCompatFakeFocusEnabled(activity.info));
- }
-
- @Test
- public void testIsCompatFakeFocusEnabled_optInPropertyEnabled_fakeFocusEnabled() {
- ActivityRecord activity = setUpActivityForCompatFakeFocusTest();
- doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
- .getPackageManagerProperty(any(), eq(PROPERTY_COMPAT_FAKE_FOCUS_OPT_IN));
-
- assertTrue(activity.mWmService.mLetterboxConfiguration
- .isCompatFakeFocusEnabled(activity.info));
+ task.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ assertFalse(activity.shouldSendCompatFakeFocus());
}
private int getExpectedSplitSize(int dimensionToSplit) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java
index 22d72ed..c538727 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java
@@ -54,11 +54,11 @@
final CountDownLatch finishedLatch = new CountDownLatch(1);
SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(TAG);
syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown);
- SyncTarget syncTarget = new SyncTarget();
+ SurfaceSyncGroup syncTarget = new SurfaceSyncGroup("FakeSyncTarget");
syncGroup.add(syncTarget, null /* runnable */);
syncGroup.markSyncReady();
- syncTarget.onBufferReady();
+ syncTarget.markSyncReady();
finishedLatch.await(5, TimeUnit.SECONDS);
assertEquals(0, finishedLatch.getCount());
@@ -69,22 +69,22 @@
final CountDownLatch finishedLatch = new CountDownLatch(1);
SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(TAG);
syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown);
- SyncTarget syncTarget1 = new SyncTarget();
- SyncTarget syncTarget2 = new SyncTarget();
- SyncTarget syncTarget3 = new SyncTarget();
+ SurfaceSyncGroup syncTarget1 = new SurfaceSyncGroup("FakeSyncTarget1");
+ SurfaceSyncGroup syncTarget2 = new SurfaceSyncGroup("FakeSyncTarget2");
+ SurfaceSyncGroup syncTarget3 = new SurfaceSyncGroup("FakeSyncTarget3");
syncGroup.add(syncTarget1, null /* runnable */);
syncGroup.add(syncTarget2, null /* runnable */);
syncGroup.add(syncTarget3, null /* runnable */);
syncGroup.markSyncReady();
- syncTarget1.onBufferReady();
+ syncTarget1.markSyncReady();
assertNotEquals(0, finishedLatch.getCount());
- syncTarget3.onBufferReady();
+ syncTarget3.markSyncReady();
assertNotEquals(0, finishedLatch.getCount());
- syncTarget2.onBufferReady();
+ syncTarget2.markSyncReady();
finishedLatch.await(5, TimeUnit.SECONDS);
assertEquals(0, finishedLatch.getCount());
@@ -94,8 +94,8 @@
public void testAddSyncWhenSyncComplete() {
SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(TAG);
- SyncTarget syncTarget1 = new SyncTarget();
- SyncTarget syncTarget2 = new SyncTarget();
+ SurfaceSyncGroup syncTarget1 = new SurfaceSyncGroup("FakeSyncTarget1");
+ SurfaceSyncGroup syncTarget2 = new SurfaceSyncGroup("FakeSyncTarget2");
assertTrue(syncGroup.add(syncTarget1, null /* runnable */));
syncGroup.markSyncReady();
@@ -114,21 +114,21 @@
syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown);
syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown);
- SyncTarget syncTarget1 = new SyncTarget();
- SyncTarget syncTarget2 = new SyncTarget();
+ SurfaceSyncGroup syncTarget1 = new SurfaceSyncGroup("FakeSyncTarget1");
+ SurfaceSyncGroup syncTarget2 = new SurfaceSyncGroup("FakeSyncTarget2");
assertTrue(syncGroup1.add(syncTarget1, null /* runnable */));
assertTrue(syncGroup2.add(syncTarget2, null /* runnable */));
syncGroup1.markSyncReady();
syncGroup2.markSyncReady();
- syncTarget1.onBufferReady();
+ syncTarget1.markSyncReady();
finishedLatch1.await(5, TimeUnit.SECONDS);
assertEquals(0, finishedLatch1.getCount());
assertNotEquals(0, finishedLatch2.getCount());
- syncTarget2.onBufferReady();
+ syncTarget2.markSyncReady();
finishedLatch2.await(5, TimeUnit.SECONDS);
assertEquals(0, finishedLatch2.getCount());
@@ -144,8 +144,8 @@
syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown);
syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown);
- SyncTarget syncTarget1 = new SyncTarget();
- SyncTarget syncTarget2 = new SyncTarget();
+ SurfaceSyncGroup syncTarget1 = new SurfaceSyncGroup("FakeSyncTarget1");
+ SurfaceSyncGroup syncTarget2 = new SurfaceSyncGroup("FakeSyncTarget2");
assertTrue(syncGroup1.add(syncTarget1, null /* runnable */));
assertTrue(syncGroup2.add(syncTarget2, null /* runnable */));
@@ -155,12 +155,12 @@
// Finish syncTarget2 first to test that the syncGroup is not complete until the merged sync
// is also done.
- syncTarget2.onBufferReady();
+ syncTarget2.markSyncReady();
finishedLatch2.await(1, TimeUnit.SECONDS);
// Sync did not complete yet
assertNotEquals(0, finishedLatch2.getCount());
- syncTarget1.onBufferReady();
+ syncTarget1.markSyncReady();
// The first sync will still get a callback when it's sync requirements are done.
finishedLatch1.await(5, TimeUnit.SECONDS);
@@ -180,13 +180,13 @@
syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown);
syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown);
- SyncTarget syncTarget1 = new SyncTarget();
- SyncTarget syncTarget2 = new SyncTarget();
+ SurfaceSyncGroup syncTarget1 = new SurfaceSyncGroup("FakeSyncTarget1");
+ SurfaceSyncGroup syncTarget2 = new SurfaceSyncGroup("FakeSyncTarget2");
assertTrue(syncGroup1.add(syncTarget1, null /* runnable */));
assertTrue(syncGroup2.add(syncTarget2, null /* runnable */));
syncGroup1.markSyncReady();
- syncTarget1.onBufferReady();
+ syncTarget1.markSyncReady();
// The first sync will still get a callback when it's sync requirements are done.
finishedLatch1.await(5, TimeUnit.SECONDS);
@@ -194,7 +194,7 @@
syncGroup2.add(syncGroup1, null /* runnable */);
syncGroup2.markSyncReady();
- syncTarget2.onBufferReady();
+ syncTarget2.markSyncReady();
// Verify that the second sync will receive complete since the merged sync was already
// completed before the merge.
@@ -212,9 +212,9 @@
syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown);
syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown);
- SyncTarget syncTarget1 = new SyncTarget();
- SyncTarget syncTarget2 = new SyncTarget();
- SyncTarget syncTarget3 = new SyncTarget();
+ SurfaceSyncGroup syncTarget1 = new SurfaceSyncGroup("FakeSyncTarget1");
+ SurfaceSyncGroup syncTarget2 = new SurfaceSyncGroup("FakeSyncTarget2");
+ SurfaceSyncGroup syncTarget3 = new SurfaceSyncGroup("FakeSyncTarget3");
assertTrue(syncGroup1.add(syncTarget1, null /* runnable */));
assertTrue(syncGroup1.add(syncTarget2, null /* runnable */));
@@ -228,8 +228,8 @@
// Make target1 and target3 ready, but not target2. SyncGroup2 should not be ready since
// SyncGroup2 also waits for all of SyncGroup1 to finish, which includes target2
- syncTarget1.onBufferReady();
- syncTarget3.onBufferReady();
+ syncTarget1.markSyncReady();
+ syncTarget3.markSyncReady();
// Neither SyncGroup will be ready.
finishedLatch1.await(1, TimeUnit.SECONDS);
@@ -238,7 +238,7 @@
assertEquals(1, finishedLatch1.getCount());
assertEquals(1, finishedLatch2.getCount());
- syncTarget2.onBufferReady();
+ syncTarget2.markSyncReady();
// Both sync groups should be ready after target2 completed.
finishedLatch1.await(5, TimeUnit.SECONDS);
@@ -257,13 +257,13 @@
syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown);
syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown);
- SyncTarget syncTarget1 = new SyncTarget();
- SyncTarget syncTarget2 = new SyncTarget();
- SyncTarget syncTarget3 = new SyncTarget();
+ SurfaceSyncGroup syncTarget1 = new SurfaceSyncGroup("FakeSyncTarget1");
+ SurfaceSyncGroup syncTarget2 = new SurfaceSyncGroup("FakeSyncTarget2");
+ SurfaceSyncGroup syncTarget3 = new SurfaceSyncGroup("FakeSyncTarget3");
assertTrue(syncGroup1.add(syncTarget1, null /* runnable */));
assertTrue(syncGroup1.add(syncTarget2, null /* runnable */));
- syncTarget2.onBufferReady();
+ syncTarget2.markSyncReady();
// Add syncTarget1 to syncGroup2 so it forces syncGroup1 into syncGroup2
assertTrue(syncGroup2.add(syncTarget1, null /* runnable */));
@@ -272,7 +272,7 @@
syncGroup1.markSyncReady();
syncGroup2.markSyncReady();
- syncTarget1.onBufferReady();
+ syncTarget1.markSyncReady();
// Only SyncGroup1 will be ready, but SyncGroup2 still needs its own targets to be ready.
finishedLatch1.await(1, TimeUnit.SECONDS);
@@ -281,7 +281,7 @@
assertEquals(0, finishedLatch1.getCount());
assertEquals(1, finishedLatch2.getCount());
- syncTarget3.onBufferReady();
+ syncTarget3.markSyncReady();
// SyncGroup2 is finished after target3 completed.
finishedLatch2.await(1, TimeUnit.SECONDS);
@@ -302,7 +302,7 @@
SurfaceControl.Transaction targetTransaction = spy(new StubTransaction());
SurfaceSyncGroup.setTransactionFactory(() -> targetTransaction);
- SyncTarget syncTarget = new SyncTarget();
+ SurfaceSyncGroup syncTarget = new SurfaceSyncGroup("FakeSyncTarget");
assertTrue(
syncGroup.add(syncTarget.mISurfaceSyncGroup, true /* parentSyncGroupMerge */,
null /* runnable */));
@@ -329,7 +329,7 @@
SurfaceControl.Transaction targetTransaction = spy(new StubTransaction());
SurfaceSyncGroup.setTransactionFactory(() -> targetTransaction);
- SyncTarget syncTarget = new SyncTarget();
+ SurfaceSyncGroup syncTarget = new SurfaceSyncGroup("FakeSyncTarget");
assertTrue(syncGroup.add(syncTarget, null /* runnable */));
syncTarget.markSyncReady();
@@ -344,13 +344,13 @@
final CountDownLatch finishedLatch = new CountDownLatch(1);
SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(TAG);
syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown);
- SyncTarget syncTarget = new SyncTarget();
+ SurfaceSyncGroup syncTarget = new SurfaceSyncGroup("FakeSyncTarget");
syncGroup.add(syncTarget, null /* runnable */);
// Add the syncTarget to the same syncGroup and ensure it doesn't crash.
syncGroup.add(syncTarget, null /* runnable */);
syncGroup.markSyncReady();
- syncTarget.onBufferReady();
+ syncTarget.markSyncReady();
try {
finishedLatch.await(5, TimeUnit.SECONDS);
@@ -359,14 +359,4 @@
}
assertEquals(0, finishedLatch.getCount());
}
-
- private static class SyncTarget extends SurfaceSyncGroup {
- SyncTarget() {
- super("FakeSyncTarget");
- }
-
- void onBufferReady() {
- markSyncReady();
- }
- }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index 42bbd2d..94193f4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -352,6 +352,7 @@
mAtmService.setWindowManager(mWmService);
mWmService.mDisplayEnabled = true;
mWmService.mDisplayReady = true;
+ mAtmService.getTransitionController().mIsWaitingForDisplayEnabled = false;
// Set configuration for default display
mWmService.getDefaultDisplayContentLocked().reconfigureDisplayLocked();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 5099869..dab842c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -45,11 +45,13 @@
import static org.mockito.Mockito.clearInvocations;
import android.content.res.Configuration;
+import android.graphics.Color;
import android.graphics.Rect;
import android.os.Binder;
import android.platform.test.annotations.Presubmit;
import android.view.SurfaceControl;
import android.window.ITaskFragmentOrganizer;
+import android.window.TaskFragmentAnimationParams;
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentOrganizer;
@@ -299,35 +301,44 @@
@Test
public void testEmbeddedTaskFragmentEnterPip_singleActivity_resetOrganizerOverrideConfig() {
- final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
- .setOrganizer(mOrganizer)
- .setFragmentToken(new Binder())
- .setCreateParentTask()
- .createActivityCount(1)
+ final Task task = createTask(mDisplayContent);
+ final TaskFragment taskFragment0 = createTaskFragmentWithEmbeddedActivity(task, mOrganizer);
+ final TaskFragment taskFragment1 = new TaskFragmentBuilder(mAtm)
+ .setParentTask(task)
.build();
- final Task task = taskFragment.getTask();
- final ActivityRecord activity = taskFragment.getTopMostActivity();
+ final ActivityRecord activity = taskFragment0.getTopMostActivity();
final Rect taskFragmentBounds = new Rect(0, 0, 300, 1000);
task.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
- taskFragment.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
- taskFragment.setBounds(taskFragmentBounds);
+ taskFragment0.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
+ taskFragment0.setBounds(taskFragmentBounds);
+ taskFragment0.setAdjacentTaskFragment(taskFragment1);
+ taskFragment0.setCompanionTaskFragment(taskFragment1);
+ taskFragment0.setAnimationParams(new TaskFragmentAnimationParams.Builder()
+ .setAnimationBackgroundColor(Color.GREEN)
+ .build());
assertEquals(taskFragmentBounds, activity.getBounds());
assertEquals(WINDOWING_MODE_MULTI_WINDOW, activity.getWindowingMode());
+ assertEquals(taskFragment1, taskFragment0.getAdjacentTaskFragment());
+ assertEquals(taskFragment1, taskFragment0.getCompanionTaskFragment());
+ assertNotEquals(TaskFragmentAnimationParams.DEFAULT, taskFragment0.getAnimationParams());
// Move activity to pinned root task.
mRootWindowContainer.moveActivityToPinnedRootTask(activity,
null /* launchIntoPipHostActivity */, "test");
// Ensure taskFragment requested config is reset.
- assertEquals(taskFragment, activity.getOrganizedTaskFragment());
+ assertEquals(taskFragment0, activity.getOrganizedTaskFragment());
assertEquals(task, activity.getTask());
assertTrue(task.inPinnedWindowingMode());
- assertTrue(taskFragment.inPinnedWindowingMode());
+ assertTrue(taskFragment0.inPinnedWindowingMode());
final Rect taskBounds = task.getBounds();
- assertEquals(taskBounds, taskFragment.getBounds());
+ assertEquals(taskBounds, taskFragment0.getBounds());
assertEquals(taskBounds, activity.getBounds());
- assertEquals(Configuration.EMPTY, taskFragment.getRequestedOverrideConfiguration());
+ assertEquals(Configuration.EMPTY, taskFragment0.getRequestedOverrideConfiguration());
+ assertNull(taskFragment0.getAdjacentTaskFragment());
+ assertNull(taskFragment0.getCompanionTaskFragment());
+ assertEquals(TaskFragmentAnimationParams.DEFAULT, taskFragment0.getAnimationParams());
// Because the whole Task is entering PiP, no need to record for future reparent.
assertNull(activity.mLastTaskFragmentOrganizerBeforePip);
}
@@ -335,18 +346,8 @@
@Test
public void testEmbeddedTaskFragmentEnterPip_multiActivities_notifyOrganizer() {
final Task task = createTask(mDisplayContent);
- final TaskFragment taskFragment0 = new TaskFragmentBuilder(mAtm)
- .setParentTask(task)
- .setOrganizer(mOrganizer)
- .setFragmentToken(new Binder())
- .createActivityCount(1)
- .build();
- final TaskFragment taskFragment1 = new TaskFragmentBuilder(mAtm)
- .setParentTask(task)
- .setOrganizer(mOrganizer)
- .setFragmentToken(new Binder())
- .createActivityCount(1)
- .build();
+ final TaskFragment taskFragment0 = createTaskFragmentWithEmbeddedActivity(task, mOrganizer);
+ final TaskFragment taskFragment1 = createTaskFragmentWithEmbeddedActivity(task, mOrganizer);
final ActivityRecord activity0 = taskFragment0.getTopMostActivity();
final ActivityRecord activity1 = taskFragment1.getTopMostActivity();
activity0.setVisibility(true /* visible */, false /* deferHidingClient */);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
index e7813ff..2bfc5ec 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
@@ -56,6 +56,7 @@
import android.platform.test.annotations.Presubmit;
import android.view.Gravity;
import android.view.InsetsState;
+import android.view.WindowInsets;
import androidx.test.filters.SmallTest;
@@ -1918,16 +1919,20 @@
state.setDisplayFrame(displayFrame);
if (sl > dl) {
- state.getSource(ITYPE_CLIMATE_BAR).setFrame(dl, dt, sl, db);
+ state.getOrCreateSource(ITYPE_CLIMATE_BAR, WindowInsets.Type.statusBars())
+ .setFrame(dl, dt, sl, db);
}
if (st > dt) {
- state.getSource(ITYPE_STATUS_BAR).setFrame(dl, dt, dr, st);
+ state.getOrCreateSource(ITYPE_STATUS_BAR, WindowInsets.Type.statusBars())
+ .setFrame(dl, dt, dr, st);
}
if (sr < dr) {
- state.getSource(ITYPE_EXTRA_NAVIGATION_BAR).setFrame(sr, dt, dr, db);
+ state.getOrCreateSource(ITYPE_EXTRA_NAVIGATION_BAR, WindowInsets.Type.navigationBars())
+ .setFrame(sr, dt, dr, db);
}
if (sb < db) {
- state.getSource(ITYPE_NAVIGATION_BAR).setFrame(dl, sb, dr, db);
+ state.getOrCreateSource(ITYPE_NAVIGATION_BAR, WindowInsets.Type.navigationBars())
+ .setFrame(dl, sb, dr, db);
}
// Recompute config and push to children.
display.onRequestedOverrideConfigurationChanged(display.getConfiguration());
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
index 83be4f0..fec079b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
@@ -27,8 +27,10 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Configuration;
@@ -51,8 +53,9 @@
public static final int DEFAULT_LOGICAL_DISPLAY_DENSITY = 300;
/** Please use the {@link Builder} to create, visible for use in test builder overrides only. */
- TestDisplayContent(RootWindowContainer rootWindowContainer, Display display) {
- super(display, rootWindowContainer);
+ TestDisplayContent(RootWindowContainer rootWindowContainer, Display display,
+ @NonNull DeviceStateController deviceStateController) {
+ super(display, rootWindowContainer, deviceStateController);
// Normally this comes from display-properties as exposed by WM. Without that, just
// hard-code to FULLSCREEN for tests.
setWindowingMode(WINDOWING_MODE_FULLSCREEN);
@@ -97,6 +100,8 @@
private int mStatusBarHeight = 0;
private SettingsEntry mOverrideSettings;
private DisplayMetrics mDisplayMetrics;
+ @NonNull
+ private DeviceStateController mDeviceStateController = mock(DeviceStateController.class);
@Mock
Context mMockContext;
@Mock
@@ -198,8 +203,13 @@
com.android.internal.R.dimen.default_minimal_size_resizable_task);
return this;
}
+ Builder setDeviceStateController(@NonNull DeviceStateController deviceStateController) {
+ mDeviceStateController = deviceStateController;
+ return this;
+ }
TestDisplayContent createInternal(Display display) {
- return new TestDisplayContent(mService.mRootWindowContainer, display);
+ return new TestDisplayContent(mService.mRootWindowContainer, display,
+ mDeviceStateController);
}
TestDisplayContent build() {
SystemServicesTestRule.checkHoldsLock(mService.mGlobalLock);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java
index 19da718..7d13de8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java
@@ -16,7 +16,7 @@
package com.android.server.wm;
-import static android.view.InsetsState.ITYPE_IME;
+import static android.view.InsetsSource.ID_IME;
import static android.view.InsetsState.ITYPE_STATUS_BAR;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.statusBars;
@@ -47,7 +47,7 @@
private InsetsSource mSource = new InsetsSource(ITYPE_STATUS_BAR, statusBars());
private WindowContainerInsetsSourceProvider mProvider;
- private InsetsSource mImeSource = new InsetsSource(ITYPE_IME, ime());
+ private InsetsSource mImeSource = new InsetsSource(ID_IME, ime());
private WindowContainerInsetsSourceProvider mImeProvider;
@Before
@@ -170,10 +170,10 @@
final WindowState target = createWindow(null, TYPE_APPLICATION, "target");
statusBar.getFrame().set(0, 0, 500, 100);
mProvider.setWindowContainer(statusBar, null, null);
- mProvider.updateControlForFakeTarget(target);
+ mProvider.updateFakeControlTarget(target);
assertNotNull(mProvider.getControl(target));
assertNull(mProvider.getControl(target).getLeash());
- mProvider.updateControlForFakeTarget(null);
+ mProvider.updateFakeControlTarget(null);
assertNull(mProvider.getControl(target));
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index d3e5a8a..17b44ee 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -20,7 +20,7 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-import static android.view.InsetsState.ITYPE_IME;
+import static android.view.InsetsSource.ID_IME;
import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
import static android.view.InsetsState.ITYPE_STATUS_BAR;
import static android.view.Surface.ROTATION_0;
@@ -437,13 +437,15 @@
final WindowState app = mAppWindow;
statusBar.mHasSurface = true;
assertTrue(statusBar.isVisible());
- mDisplayContent.getInsetsStateController().getSourceProvider(ITYPE_STATUS_BAR)
+ mDisplayContent.getInsetsStateController()
+ .getOrCreateSourceProvider(ITYPE_STATUS_BAR, statusBars())
.setWindowContainer(statusBar, null /* frameProvider */,
null /* imeFrameProvider */);
mDisplayContent.getInsetsStateController().onBarControlTargetChanged(
app, null /* fakeTopControlling */, app, null /* fakeNavControlling */);
app.setRequestedVisibleTypes(0, statusBars());
- mDisplayContent.getInsetsStateController().getSourceProvider(ITYPE_STATUS_BAR)
+ mDisplayContent.getInsetsStateController()
+ .getOrCreateSourceProvider(ITYPE_STATUS_BAR, statusBars())
.updateClientVisibility(app);
waitUntilHandlersIdle();
assertFalse(statusBar.isVisible());
@@ -1039,12 +1041,16 @@
mAppWindow.mAboveInsetsState.addSource(navSource);
navSource.setVisible(false);
- assertTrue(mImeWindow.getInsetsState().getSourceOrDefaultVisibility(ITYPE_NAVIGATION_BAR));
- assertFalse(mAppWindow.getInsetsState().getSourceOrDefaultVisibility(ITYPE_NAVIGATION_BAR));
+ assertTrue(mImeWindow.getInsetsState().isSourceOrDefaultVisible(
+ ITYPE_NAVIGATION_BAR, navigationBars()));
+ assertFalse(mAppWindow.getInsetsState().isSourceOrDefaultVisible(
+ ITYPE_NAVIGATION_BAR, navigationBars()));
navSource.setVisible(true);
- assertTrue(mImeWindow.getInsetsState().getSourceOrDefaultVisibility(ITYPE_NAVIGATION_BAR));
- assertTrue(mAppWindow.getInsetsState().getSourceOrDefaultVisibility(ITYPE_NAVIGATION_BAR));
+ assertTrue(mImeWindow.getInsetsState().isSourceOrDefaultVisible(
+ ITYPE_NAVIGATION_BAR, navigationBars()));
+ assertTrue(mAppWindow.getInsetsState().isSourceOrDefaultVisible(
+ ITYPE_NAVIGATION_BAR, navigationBars()));
}
@Test
@@ -1069,8 +1075,8 @@
controller.updateAboveInsetsState(false);
// Expect all app windows behind IME can receive IME insets visible.
- assertTrue(app.getInsetsState().getSource(ITYPE_IME).isVisible());
- assertTrue(app2.getInsetsState().getSource(ITYPE_IME).isVisible());
+ assertTrue(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
+ assertTrue(app2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
// Simulate app plays closing transition to app2.
app.mActivityRecord.commitVisibility(false, false);
@@ -1078,8 +1084,8 @@
assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput);
// Verify the IME insets is visible on app, but not for app2 during app task switching.
- assertTrue(app.getInsetsState().getSource(ITYPE_IME).isVisible());
- assertFalse(app2.getInsetsState().getSource(ITYPE_IME).isVisible());
+ assertTrue(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
+ assertFalse(app2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
}
@Test
@@ -1108,8 +1114,8 @@
// Expect app windows behind IME can receive IME insets visible,
// but not for app2 in background.
- assertTrue(app.getInsetsState().getSource(ITYPE_IME).isVisible());
- assertFalse(app2.getInsetsState().getSource(ITYPE_IME).isVisible());
+ assertTrue(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
+ assertFalse(app2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
// Simulate app plays closing transition to app2.
// And app2 is now IME layering target but not yet to be the IME input target.
@@ -1119,8 +1125,8 @@
assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput);
// Verify the IME insets is still visible on app, but not for app2 during task switching.
- assertTrue(app.getInsetsState().getSource(ITYPE_IME).isVisible());
- assertFalse(app2.getInsetsState().getSource(ITYPE_IME).isVisible());
+ assertTrue(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
+ assertFalse(app2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
}
@SetupWindows(addWindows = W_ACTIVITY)
@@ -1165,18 +1171,18 @@
mNotificationShadeWindow.setHasSurface(true);
mNotificationShadeWindow.mAttrs.flags &= ~FLAG_NOT_FOCUSABLE;
assertTrue(mNotificationShadeWindow.canBeImeTarget());
- mDisplayContent.getInsetsStateController().getSourceProvider(ITYPE_IME).setWindowContainer(
- mImeWindow, null, null);
+ mDisplayContent.getInsetsStateController().getOrCreateSourceProvider(ID_IME, ime())
+ .setWindowContainer(mImeWindow, null, null);
mDisplayContent.computeImeTarget(true);
assertEquals(mNotificationShadeWindow, mDisplayContent.getImeTarget(IME_TARGET_LAYERING));
mDisplayContent.getInsetsStateController().getRawInsetsState()
- .setSourceVisible(ITYPE_IME, true);
+ .setSourceVisible(ID_IME, true);
// Verify notificationShade can still get IME insets even windowing mode is multi-window.
InsetsState state = mNotificationShadeWindow.getInsetsState();
- assertNotNull(state.peekSource(ITYPE_IME));
- assertTrue(state.getSource(ITYPE_IME).isVisible());
+ assertNotNull(state.peekSource(ID_IME));
+ assertTrue(state.isSourceOrDefaultVisible(ID_IME, ime()));
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index e5efe05..3e8d259 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -79,6 +79,7 @@
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
import android.hardware.display.DisplayManager;
+import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
@@ -750,11 +751,11 @@
}
/**
- * Creates a {@link TaskFragment} with {@link ActivityRecord} and attach it to the
+ * Creates a {@link TaskFragment} with {@link ActivityRecord}, and attaches it to the
* {@code parentTask}.
*
- * @param parentTask the {@link Task} this TaskFragment is going to be attached
- * @return the created TaskFragment
+ * @param parentTask the {@link Task} this {@link TaskFragment} is going to be attached.
+ * @return the created {@link TaskFragment}
*/
static TaskFragment createTaskFragmentWithActivity(@NonNull Task parentTask) {
return new TaskFragmentBuilder(parentTask.mAtmService)
@@ -763,13 +764,27 @@
.build();
}
+ /**
+ * Creates an embedded {@link TaskFragment} organized by {@code organizer} with
+ * {@link ActivityRecord}, and attaches it to the {@code parentTask}.
+ *
+ * @param parentTask the {@link Task} this {@link TaskFragment} is going to be attached.
+ * @param organizer the {@link TaskFragmentOrganizer} this {@link TaskFragment} is going to be
+ * organized by.
+ * @return the created {@link TaskFragment}
+ */
static TaskFragment createTaskFragmentWithEmbeddedActivity(@NonNull Task parentTask,
- TaskFragmentOrganizer organizer) {
- return new TaskFragmentBuilder(parentTask.mAtmService)
+ @NonNull TaskFragmentOrganizer organizer) {
+ final IBinder fragmentToken = new Binder();
+ final TaskFragment taskFragment = new TaskFragmentBuilder(parentTask.mAtmService)
.setParentTask(parentTask)
.createActivityCount(1)
.setOrganizer(organizer)
+ .setFragmentToken(fragmentToken)
.build();
+ parentTask.mAtmService.mWindowOrganizerController.mLaunchTaskFragments
+ .put(fragmentToken, taskFragment);
+ return taskFragment;
}
/** Creates a {@link DisplayContent} that supports IME and adds it to the system. */
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java
index 3ff791b..714eb4b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java
@@ -16,7 +16,7 @@
package com.android.server.wm;
-import static android.view.InsetsState.ITYPE_IME;
+import static android.view.InsetsSource.ID_IME;
import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
@@ -39,6 +39,7 @@
import android.os.Bundle;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
+import android.view.WindowInsets;
import android.window.WindowContext;
import androidx.test.filters.SmallTest;
@@ -262,8 +263,9 @@
@Test
public void testSetInsetsFrozen_notAffectImeWindowState() {
// Pre-condition: make the IME window be controlled by IME insets provider.
- mDisplayContent.getInsetsStateController().getSourceProvider(ITYPE_IME).setWindowContainer(
- mDisplayContent.mInputMethodWindow, null, null);
+ mDisplayContent.getInsetsStateController()
+ .getOrCreateSourceProvider(ID_IME, WindowInsets.Type.ime())
+ .setWindowContainer(mDisplayContent.mInputMethodWindow, null, null);
// Simulate an app window to be the IME layering target, assume the app window has no
// frozen insets state by default.
diff --git a/services/usb/java/com/android/server/usb/UsbAlsaManager.java b/services/usb/java/com/android/server/usb/UsbAlsaManager.java
index 17c354a..aa1d556 100644
--- a/services/usb/java/com/android/server/usb/UsbAlsaManager.java
+++ b/services/usb/java/com/android/server/usb/UsbAlsaManager.java
@@ -23,7 +23,9 @@
import android.media.IAudioService;
import android.media.midi.MidiDeviceInfo;
import android.os.Bundle;
+import android.os.FileObserver;
import android.os.ServiceManager;
+import android.os.SystemClock;
import android.provider.Settings;
import android.service.usb.UsbAlsaManagerProto;
import android.util.Slog;
@@ -34,9 +36,11 @@
import libcore.io.IoUtils;
+import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
/**
@@ -52,6 +56,11 @@
private static final String ALSA_DIRECTORY = "/dev/snd/";
+ private static final int ALSA_DEVICE_TYPE_UNKNOWN = 0;
+ private static final int ALSA_DEVICE_TYPE_PLAYBACK = 1;
+ private static final int ALSA_DEVICE_TYPE_CAPTURE = 2;
+ private static final int ALSA_DEVICE_TYPE_MIDI = 3;
+
private final Context mContext;
private IAudioService mAudioService;
private final boolean mHasMidiFeature;
@@ -110,11 +119,26 @@
/**
* List of connected MIDI devices
*/
- private final HashMap<String, UsbMidiDevice>
- mMidiDevices = new HashMap<String, UsbMidiDevice>();
+ private final HashMap<String, UsbAlsaMidiDevice>
+ mMidiDevices = new HashMap<String, UsbAlsaMidiDevice>();
- // UsbMidiDevice for USB peripheral mode (gadget) device
- private UsbMidiDevice mPeripheralMidiDevice = null;
+ // UsbAlsaMidiDevice for USB peripheral mode (gadget) device
+ private UsbAlsaMidiDevice mPeripheralMidiDevice = null;
+
+ private final HashSet<Integer> mAlsaCards = new HashSet<>();
+ private final FileObserver mAlsaObserver = new FileObserver(new File(ALSA_DIRECTORY),
+ FileObserver.CREATE | FileObserver.DELETE) {
+ public void onEvent(int event, String path) {
+ switch (event) {
+ case FileObserver.CREATE:
+ alsaFileAdded(path);
+ break;
+ case FileObserver.DELETE:
+ alsaFileRemoved(path);
+ break;
+ }
+ }
+ };
/* package */ UsbAlsaManager(Context context) {
mContext = context;
@@ -124,6 +148,7 @@
public void systemReady() {
mAudioService = IAudioService.Stub.asInterface(
ServiceManager.getService(Context.AUDIO_SERVICE));
+ mAlsaObserver.startWatching();
}
/**
@@ -224,6 +249,8 @@
return;
}
+ waitForAlsaDevice(cardRec.getCardNum(), true /*isAdded*/);
+
// Add it to the devices list
boolean hasInput = parser.hasInput()
&& !isDeviceDenylisted(usbDevice.getVendorId(), usbDevice.getProductId(),
@@ -304,11 +331,11 @@
Slog.d(TAG, "numLegacyMidiOutputs:" + numLegacyMidiOutputs);
}
- UsbMidiDevice usbMidiDevice = UsbMidiDevice.create(mContext, properties,
+ UsbAlsaMidiDevice midiDevice = UsbAlsaMidiDevice.create(mContext, properties,
cardRec.getCardNum(), 0 /*device*/, numLegacyMidiInputs,
numLegacyMidiOutputs);
- if (usbMidiDevice != null) {
- mMidiDevices.put(deviceAddress, usbMidiDevice);
+ if (midiDevice != null) {
+ mMidiDevices.put(deviceAddress, midiDevice);
}
}
}
@@ -322,15 +349,16 @@
UsbAlsaDevice alsaDevice = removeAlsaDeviceFromList(deviceAddress);
Slog.i(TAG, "USB Audio Device Removed: " + alsaDevice);
if (alsaDevice != null && alsaDevice == mSelectedDevice) {
+ waitForAlsaDevice(alsaDevice.getCardNum(), false /*isAdded*/);
deselectAlsaDevice();
selectDefaultDevice(); // if there any external devices left, select one of them
}
// MIDI
- UsbMidiDevice usbMidiDevice = mMidiDevices.remove(deviceAddress);
- if (usbMidiDevice != null) {
+ UsbAlsaMidiDevice midiDevice = mMidiDevices.remove(deviceAddress);
+ if (midiDevice != null) {
Slog.i(TAG, "USB MIDI Device Removed: " + deviceAddress);
- IoUtils.closeQuietly(usbMidiDevice);
+ IoUtils.closeQuietly(midiDevice);
}
logDevices("usbDeviceRemoved()");
@@ -353,7 +381,7 @@
com.android.internal.R.string.usb_midi_peripheral_product_name));
properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_CARD, card);
properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_DEVICE, device);
- mPeripheralMidiDevice = UsbMidiDevice.create(mContext, properties, card, device,
+ mPeripheralMidiDevice = UsbAlsaMidiDevice.create(mContext, properties, card, device,
1 /* numInputs */, 1 /* numOutputs */);
} else if (!enabled && mPeripheralMidiDevice != null) {
IoUtils.closeQuietly(mPeripheralMidiDevice);
@@ -361,6 +389,89 @@
}
}
+ private boolean waitForAlsaDevice(int card, boolean isAdded) {
+ if (DEBUG) {
+ Slog.e(TAG, "waitForAlsaDevice(c:" + card + ")");
+ }
+
+ // This value was empirically determined.
+ final int kWaitTimeMs = 2500;
+
+ synchronized (mAlsaCards) {
+ long timeoutMs = SystemClock.elapsedRealtime() + kWaitTimeMs;
+ while ((isAdded ^ mAlsaCards.contains(card))
+ && timeoutMs > SystemClock.elapsedRealtime()) {
+ long waitTimeMs = timeoutMs - SystemClock.elapsedRealtime();
+ if (waitTimeMs > 0) {
+ try {
+ mAlsaCards.wait(waitTimeMs);
+ } catch (InterruptedException e) {
+ Slog.d(TAG, "usb: InterruptedException while waiting for ALSA file.");
+ }
+ }
+ }
+ final boolean cardFound = mAlsaCards.contains(card);
+ if ((isAdded ^ cardFound) && timeoutMs > SystemClock.elapsedRealtime()) {
+ Slog.e(TAG, "waitForAlsaDevice(" + card + ") timeout");
+ } else {
+ Slog.i(TAG, "waitForAlsaDevice for device card=" + card + ", isAdded=" + isAdded
+ + ", found=" + cardFound);
+ }
+ return cardFound;
+ }
+ }
+
+ private int getCardNumberFromAlsaFilePath(String path) {
+ int type = ALSA_DEVICE_TYPE_UNKNOWN;
+ if (path.startsWith("pcmC")) {
+ if (path.endsWith("p")) {
+ type = ALSA_DEVICE_TYPE_PLAYBACK;
+ } else if (path.endsWith("c")) {
+ type = ALSA_DEVICE_TYPE_CAPTURE;
+ }
+ } else if (path.startsWith("midiC")) {
+ type = ALSA_DEVICE_TYPE_MIDI;
+ }
+
+ if (type == ALSA_DEVICE_TYPE_UNKNOWN) {
+ Slog.i(TAG, "Unknown type file(" + path + ") added.");
+ return -1;
+ }
+ try {
+ int c_index = path.indexOf('C');
+ int d_index = path.indexOf('D');
+ return Integer.parseInt(path.substring(c_index + 1, d_index));
+ } catch (Exception e) {
+ Slog.e(TAG, "Could not parse ALSA file name " + path, e);
+ return -1;
+ }
+ }
+
+ private void alsaFileAdded(String path) {
+ Slog.i(TAG, "alsaFileAdded(" + path + ")");
+ final int card = getCardNumberFromAlsaFilePath(path);
+ if (card == -1) {
+ return;
+ }
+ synchronized (mAlsaCards) {
+ if (!mAlsaCards.contains(card)) {
+ Slog.d(TAG, "Adding ALSA device card=" + card);
+ mAlsaCards.add(card);
+ mAlsaCards.notifyAll();
+ }
+ }
+ }
+
+ private void alsaFileRemoved(String path) {
+ final int card = getCardNumberFromAlsaFilePath(path);
+ if (card == -1) {
+ return;
+ }
+ synchronized (mAlsaCards) {
+ mAlsaCards.remove(card);
+ }
+ }
+
//
// Devices List
//
@@ -389,9 +500,9 @@
}
for (String deviceAddr : mMidiDevices.keySet()) {
- // A UsbMidiDevice does not have a handle to the UsbDevice anymore
- mMidiDevices.get(deviceAddr).dump(deviceAddr, dump, "midi_devices",
- UsbAlsaManagerProto.MIDI_DEVICES);
+ // A UsbAlsaMidiDevice does not have a handle to the UsbDevice anymore
+ mMidiDevices.get(deviceAddr).dump(deviceAddr, dump, "alsa_midi_devices",
+ UsbAlsaManagerProto.ALSA_MIDI_DEVICES);
}
dump.end(token);
diff --git a/services/usb/java/com/android/server/usb/UsbMidiDevice.java b/services/usb/java/com/android/server/usb/UsbAlsaMidiDevice.java
similarity index 91%
rename from services/usb/java/com/android/server/usb/UsbMidiDevice.java
rename to services/usb/java/com/android/server/usb/UsbAlsaMidiDevice.java
index d9ad703..e92c034 100644
--- a/services/usb/java/com/android/server/usb/UsbMidiDevice.java
+++ b/services/usb/java/com/android/server/usb/UsbAlsaMidiDevice.java
@@ -10,7 +10,7 @@
* 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 an
+ * See the License for the specific language governing permissions and
* limitations under the License.
*/
@@ -24,7 +24,7 @@
import android.media.midi.MidiManager;
import android.media.midi.MidiReceiver;
import android.os.Bundle;
-import android.service.usb.UsbMidiDeviceProto;
+import android.service.usb.UsbAlsaMidiDeviceProto;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
@@ -43,8 +43,12 @@
import java.io.FileOutputStream;
import java.io.IOException;
-public final class UsbMidiDevice implements Closeable {
- private static final String TAG = "UsbMidiDevice";
+/**
+ * Opens device connections to MIDI 1.0 endpoints.
+ * These endpoints will use ALSA.
+ */
+public final class UsbAlsaMidiDevice implements Closeable {
+ private static final String TAG = "UsbAlsaMidiDevice";
private final int mAlsaCard;
private final int mAlsaDevice;
@@ -142,13 +146,13 @@
}
/**
- * Creates an UsbMidiDevice based on the input parameters. Read/Write streams
+ * Creates an UsbAlsaMidiDevice based on the input parameters. Read/Write streams
* will be created individually as some devices don't have the same number of
* inputs and outputs.
*/
- public static UsbMidiDevice create(Context context, Bundle properties, int card,
+ public static UsbAlsaMidiDevice create(Context context, Bundle properties, int card,
int device, int numInputs, int numOutputs) {
- UsbMidiDevice midiDevice = new UsbMidiDevice(card, device, numInputs, numOutputs);
+ UsbAlsaMidiDevice midiDevice = new UsbAlsaMidiDevice(card, device, numInputs, numOutputs);
if (!midiDevice.register(context, properties)) {
IoUtils.closeQuietly(midiDevice);
Log.e(TAG, "createDeviceServer failed");
@@ -157,7 +161,7 @@
return midiDevice;
}
- private UsbMidiDevice(int card, int device, int numInputs, int numOutputs) {
+ private UsbAlsaMidiDevice(int card, int device, int numInputs, int numOutputs) {
mAlsaCard = card;
mAlsaDevice = device;
mNumInputs = numInputs;
@@ -217,7 +221,7 @@
if (inputStreamCount > 0) {
// Create input thread which will read from all output ports of the physical device
- new Thread("UsbMidiDevice input thread") {
+ new Thread("UsbAlsaMidiDevice input thread") {
@Override
public void run() {
byte[] buffer = new byte[BUFFER_SIZE];
@@ -272,13 +276,13 @@
final FileOutputStream outputStreamF = mOutputStreams[port];
final int portF = port;
- new Thread("UsbMidiDevice output thread " + port) {
+ new Thread("UsbAlsaMidiDevice output thread " + port) {
@Override
public void run() {
while (true) {
MidiEvent event;
try {
- event = (MidiEvent)eventSchedulerF.waitNextEvent();
+ event = (MidiEvent) eventSchedulerF.waitNextEvent();
} catch (InterruptedException e) {
// try again
continue;
@@ -303,9 +307,9 @@
}
private boolean register(Context context, Bundle properties) {
- MidiManager midiManager = (MidiManager)context.getSystemService(Context.MIDI_SERVICE);
+ MidiManager midiManager = context.getSystemService(MidiManager.class);
if (midiManager == null) {
- Log.e(TAG, "No MidiManager in UsbMidiDevice.register()");
+ Log.e(TAG, "No MidiManager in UsbAlsaMidiDevice.register()");
return false;
}
@@ -365,9 +369,9 @@
long id) {
long token = dump.start(idName, id);
- dump.write("device_address", UsbMidiDeviceProto.DEVICE_ADDRESS, deviceAddr);
- dump.write("card", UsbMidiDeviceProto.CARD, mAlsaCard);
- dump.write("device", UsbMidiDeviceProto.DEVICE, mAlsaDevice);
+ dump.write("device_address", UsbAlsaMidiDeviceProto.DEVICE_ADDRESS, deviceAddr);
+ dump.write("card", UsbAlsaMidiDeviceProto.CARD, mAlsaCard);
+ dump.write("device", UsbAlsaMidiDeviceProto.DEVICE, mAlsaDevice);
dump.end(token);
}
diff --git a/services/voiceinteraction/Android.bp b/services/voiceinteraction/Android.bp
index af0eca9..7332d2d 100644
--- a/services/voiceinteraction/Android.bp
+++ b/services/voiceinteraction/Android.bp
@@ -20,6 +20,7 @@
srcs: [":services.voiceinteraction-sources"],
libs: [
"services.core",
+ "app-compat-annotations",
"android.hardware.power-V1-java",
"android.hardware.power-V1.0-java",
],
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
index 81717f4..7824c69 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
@@ -298,35 +298,33 @@
}
@Override
- public int startRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback,
+ public int startRecognition(GenericSoundModel soundModel,
+ IRecognitionStatusCallback callback,
RecognitionConfig config, boolean runInBatterySaverMode) {
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
+
+ if (soundModel == null) {
+ Slog.e(TAG, "Null model passed to startRecognition");
+ return STATUS_ERROR;
+ }
+
if (runInBatterySaverMode) {
enforceCallingPermission(Manifest.permission.SOUND_TRIGGER_RUN_IN_BATTERY_SAVER);
}
if (DEBUG) {
- Slog.i(TAG, "startRecognition(): Uuid : " + parcelUuid);
+ Slog.i(TAG, "startRecognition(): Uuid : " + soundModel.toString());
}
sEventLogger.enqueue(new EventLogger.StringEvent(
- "startRecognition(): Uuid : " + parcelUuid));
+ "startRecognition(): Uuid : " + soundModel.getUuid().toString()));
- GenericSoundModel model = getSoundModel(parcelUuid);
- if (model == null) {
- Slog.w(TAG, "Null model in database for id: " + parcelUuid);
-
- sEventLogger.enqueue(new EventLogger.StringEvent(
- "startRecognition(): Null model in database for id: " + parcelUuid));
-
- return STATUS_ERROR;
- }
-
- int ret = mSoundTriggerHelper.startGenericRecognition(parcelUuid.getUuid(), model,
+ int ret = mSoundTriggerHelper.startGenericRecognition(soundModel.getUuid(),
+ soundModel,
callback, config, runInBatterySaverMode);
if (ret == STATUS_OK) {
- mSoundModelStatTracker.onStart(parcelUuid.getUuid());
+ mSoundModelStatTracker.onStart(soundModel.getUuid());
}
return ret;
}
@@ -379,8 +377,7 @@
sEventLogger.enqueue(new EventLogger.StringEvent("updateSoundModel(): model = "
+ soundModel));
-
- mDbHelper.updateGenericSoundModel(soundModel);
+ mDbHelper.updateGenericSoundModel(soundModel);
}
}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 445446e..cc22847 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -51,7 +51,6 @@
import android.os.ServiceManager;
import android.os.SharedMemory;
import android.provider.DeviceConfig;
-import android.service.voice.HotwordDetectedResult;
import android.service.voice.HotwordDetectionService;
import android.service.voice.HotwordDetector;
import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback;
@@ -66,6 +65,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IHotwordRecognitionStatusCallback;
+import com.android.internal.app.IVisualQueryDetectionAttentionListener;
import com.android.internal.infra.ServiceConnector;
import com.android.server.LocalServices;
import com.android.server.pm.permission.PermissionManagerServiceInternal;
@@ -88,19 +88,18 @@
static final boolean DEBUG = false;
/**
- * For apps targeting Android API {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above,
- * implementors of the {@link HotwordDetectionService} must not augment the phrase IDs which are
- * supplied via {@link HotwordDetectionService
- * #onDetect(AlwaysOnHotwordDetector.EventPayload, long, HotwordDetectionService.Callback)}.
+ * For apps targeting Android API Build.VERSION_CODES#UPSIDE_DOWN_CAKE and above,
+ * implementors of the HotwordDetectionService must not augment the phrase IDs which are
+ * supplied via HotwordDetectionService
+ * #onDetect(AlwaysOnHotwordDetector.EventPayload, long, HotwordDetectionService.Callback).
*
- * <p>The {@link HotwordDetectedResult#getHotwordPhraseId()} must match one of the phrase IDs
- * from the {@link android.service.voice.AlwaysOnHotwordDetector
- * .EventPayload#getKeyphraseRecognitionExtras()} list.
+ * <p>The HotwordDetectedResult#getHotwordPhraseId() must match one of the phrase IDs
+ * from the AlwaysOnHotwordDetector.EventPayload#getKeyphraseRecognitionExtras() list.
* </p>
*
- * <p>This behavior change is made to ensure the {@link HotwordDetectionService} honors what
- * it receives from the {@link android.hardware.soundtrigger.SoundTriggerModule}, and it
- * cannot signal to the client application a phrase which was not origially detected.
+ * <p>This behavior change is made to ensure the HotwordDetectionService honors what
+ * it receives from the android.hardware.soundtrigger.SoundTriggerModule, and it
+ * cannot signal to the client application a phrase which was not originally detected.
* </p>
*/
@ChangeId
@@ -328,6 +327,15 @@
session.startListeningFromMicLocked(audioFormat, callback);
}
+ public void setVisualQueryDetectionAttentionListenerLocked(
+ @Nullable IVisualQueryDetectionAttentionListener listener) {
+ final VisualQueryDetectorSession session = getVisualQueryDetectorSessionLocked();
+ if (session == null) {
+ return;
+ }
+ session.setVisualQueryDetectionAttentionListenerLocked(listener);
+ }
+
/**
* This method is only used by VisualQueryDetector.
*/
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
index 621c3de..33150d8 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
@@ -33,6 +33,7 @@
import android.util.Slog;
import com.android.internal.app.IHotwordRecognitionStatusCallback;
+import com.android.internal.app.IVisualQueryDetectionAttentionListener;
import java.io.PrintWriter;
import java.util.Objects;
@@ -49,6 +50,7 @@
final class VisualQueryDetectorSession extends DetectorSession {
private static final String TAG = "VisualQueryDetectorSession";
+ private IVisualQueryDetectionAttentionListener mAttentionListener;
private boolean mEgressingData;
private boolean mQueryStreaming;
@@ -64,6 +66,7 @@
logging);
mEgressingData = false;
mQueryStreaming = false;
+ mAttentionListener = null;
}
@Override
@@ -74,6 +77,11 @@
//TODO(b/261783819): Starts detection in VisualQueryDetectionService.
}
+ void setVisualQueryDetectionAttentionListenerLocked(
+ @Nullable IVisualQueryDetectionAttentionListener listener) {
+ mAttentionListener = listener;
+ }
+
@SuppressWarnings("GuardedBy")
void startPerceivingLocked(IVisualQueryDetectionVoiceInteractionCallback callback) {
if (DEBUG) {
@@ -86,15 +94,31 @@
@Override
public void onAttentionGained() {
Slog.v(TAG, "BinderCallback#onAttentionGained");
- //TODO check to see if there is an active SysUI listener registered
mEgressingData = true;
+ if (mAttentionListener == null) {
+ return;
+ }
+ try {
+ mAttentionListener.onAttentionGained();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error delivering attention gained event.", e);
+ return;
+ }
}
@Override
public void onAttentionLost() {
Slog.v(TAG, "BinderCallback#onAttentionLost");
- //TODO check to see if there is an active SysUI listener registered
mEgressingData = false;
+ if (mAttentionListener == null) {
+ return;
+ }
+ try {
+ mAttentionListener.onAttentionLost();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error delivering attention lost event.", e);
+ return;
+ }
}
@Override
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 38bf9c2..0abed0b 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -87,6 +87,7 @@
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IHotwordRecognitionStatusCallback;
+import com.android.internal.app.IVisualQueryDetectionAttentionListener;
import com.android.internal.app.IVoiceActionCheckCallback;
import com.android.internal.app.IVoiceInteractionManagerService;
import com.android.internal.app.IVoiceInteractionSessionListener;
@@ -1338,6 +1339,39 @@
}
}
+ @android.annotation.EnforcePermission(
+ android.Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE)
+ @Override
+ public void enableVisualQueryDetection(
+ IVisualQueryDetectionAttentionListener listener) {
+ super.enableVisualQueryDetection_enforcePermission();
+ synchronized (this) {
+
+ if (mImpl == null) {
+ Slog.w(TAG,
+ "enableVisualQueryDetection without running voice interaction service");
+ return;
+ }
+ this.mImpl.setVisualQueryDetectionAttentionListenerLocked(listener);
+ }
+ }
+
+ @android.annotation.EnforcePermission(
+ android.Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE)
+ @Override
+ public void disableVisualQueryDetection() {
+ super.disableVisualQueryDetection_enforcePermission();
+ synchronized (this) {
+ if (mImpl == null) {
+ Slog.w(TAG,
+ "disableVisualQueryDetection without running voice interaction"
+ + " service");
+ return;
+ }
+ this.mImpl.setVisualQueryDetectionAttentionListenerLocked(null);
+ }
+ }
+
@Override
public void startPerceiving(
IVisualQueryDetectionVoiceInteractionCallback callback)
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index 127195c..3dbd269 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -72,7 +72,9 @@
import android.util.Slog;
import android.view.IWindowManager;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IHotwordRecognitionStatusCallback;
+import com.android.internal.app.IVisualQueryDetectionAttentionListener;
import com.android.internal.app.IVoiceActionCheckCallback;
import com.android.internal.app.IVoiceInteractionSessionShowCallback;
import com.android.internal.app.IVoiceInteractor;
@@ -247,6 +249,7 @@
Context.RECEIVER_EXPORTED);
}
+ @GuardedBy("this")
public void grantImplicitAccessLocked(int grantRecipientUid, @Nullable Intent intent) {
final int grantRecipientAppId = UserHandle.getAppId(grantRecipientUid);
final int grantRecipientUserId = UserHandle.getUserId(grantRecipientUid);
@@ -256,6 +259,7 @@
/* direct= */ true);
}
+ @GuardedBy("this")
public boolean showSessionLocked(@Nullable Bundle args, int flags,
@Nullable String attributionTag,
@Nullable IVoiceInteractionSessionShowCallback showCallback,
@@ -328,6 +332,7 @@
}
}
+ @GuardedBy("this")
public boolean hideSessionLocked() {
if (mActiveSession != null) {
return mActiveSession.hideLocked();
@@ -335,6 +340,7 @@
return false;
}
+ @GuardedBy("this")
public boolean deliverNewSessionLocked(IBinder token,
IVoiceInteractionSession session, IVoiceInteractor interactor) {
if (mActiveSession == null || token != mActiveSession.mToken) {
@@ -345,6 +351,7 @@
return true;
}
+ @GuardedBy("this")
public int startVoiceActivityLocked(@Nullable String callingFeatureId, int callingPid,
int callingUid, IBinder token, Intent intent, String resolvedType) {
try {
@@ -367,6 +374,7 @@
}
}
+ @GuardedBy("this")
public int startAssistantActivityLocked(@Nullable String callingFeatureId, int callingPid,
int callingUid, IBinder token, Intent intent, String resolvedType) {
try {
@@ -389,6 +397,7 @@
}
}
+ @GuardedBy("this")
public void requestDirectActionsLocked(@NonNull IBinder token, int taskId,
@NonNull IBinder assistToken, @Nullable RemoteCallback cancellationCallback,
@NonNull RemoteCallback callback) {
@@ -444,6 +453,7 @@
}
}
+ @GuardedBy("this")
void performDirectActionLocked(@NonNull IBinder token, @NonNull String actionId,
@Nullable Bundle arguments, int taskId, IBinder assistToken,
@Nullable RemoteCallback cancellationCallback,
@@ -470,6 +480,7 @@
}
}
+ @GuardedBy("this")
public void setKeepAwakeLocked(IBinder token, boolean keepAwake) {
try {
if (mActiveSession == null || token != mActiveSession.mToken) {
@@ -482,6 +493,7 @@
}
}
+ @GuardedBy("this")
public void closeSystemDialogsLocked(IBinder token) {
try {
if (mActiveSession == null || token != mActiveSession.mToken) {
@@ -494,6 +506,7 @@
}
}
+ @GuardedBy("this")
public void finishLocked(IBinder token, boolean finishTask) {
if (mActiveSession == null || (!finishTask && token != mActiveSession.mToken)) {
Slog.w(TAG, "finish does not match active session");
@@ -503,6 +516,7 @@
mActiveSession = null;
}
+ @GuardedBy("this")
public void setDisabledShowContextLocked(int callingUid, int flags) {
int activeUid = mInfo.getServiceInfo().applicationInfo.uid;
if (callingUid != activeUid) {
@@ -512,6 +526,7 @@
mDisabledShowContext = flags;
}
+ @GuardedBy("this")
public int getDisabledShowContextLocked(int callingUid) {
int activeUid = mInfo.getServiceInfo().applicationInfo.uid;
if (callingUid != activeUid) {
@@ -521,6 +536,7 @@
return mDisabledShowContext;
}
+ @GuardedBy("this")
public int getUserDisabledShowContextLocked(int callingUid) {
int activeUid = mInfo.getServiceInfo().applicationInfo.uid;
if (callingUid != activeUid) {
@@ -534,6 +550,7 @@
return mInfo.getSupportsLocalInteraction();
}
+ @GuardedBy("this")
public void startListeningVisibleActivityChangedLocked(@NonNull IBinder token) {
if (DEBUG) {
Slog.d(TAG, "startListeningVisibleActivityChangedLocked: token=" + token);
@@ -546,6 +563,7 @@
mActiveSession.startListeningVisibleActivityChangedLocked();
}
+ @GuardedBy("this")
public void stopListeningVisibleActivityChangedLocked(@NonNull IBinder token) {
if (DEBUG) {
Slog.d(TAG, "stopListeningVisibleActivityChangedLocked: token=" + token);
@@ -558,6 +576,7 @@
mActiveSession.stopListeningVisibleActivityChangedLocked();
}
+ @GuardedBy("this")
public void notifyActivityDestroyedLocked(@NonNull IBinder activityToken) {
if (DEBUG) {
Slog.d(TAG, "notifyActivityDestroyedLocked activityToken=" + activityToken);
@@ -572,6 +591,7 @@
mActiveSession.notifyActivityDestroyedLocked(activityToken);
}
+ @GuardedBy("this")
public void notifyActivityEventChangedLocked(@NonNull IBinder activityToken, int type) {
if (DEBUG) {
Slog.d(TAG, "notifyActivityEventChangedLocked type=" + type);
@@ -586,6 +606,7 @@
mActiveSession.notifyActivityEventChangedLocked(activityToken, type);
}
+ @GuardedBy("this")
public void updateStateLocked(
@Nullable PersistableBundle options,
@Nullable SharedMemory sharedMemory,
@@ -606,6 +627,7 @@
}
}
+ @GuardedBy("this")
private void verifyDetectorForHotwordDetectionLocked(
@Nullable SharedMemory sharedMemory,
IHotwordRecognitionStatusCallback callback,
@@ -663,6 +685,7 @@
voiceInteractionServiceUid);
}
+ @GuardedBy("this")
private void verifyDetectorForVisualQueryDetectionLocked(@Nullable SharedMemory sharedMemory) {
Slog.v(TAG, "verifyDetectorForVisualQueryDetectionLocked");
@@ -701,6 +724,7 @@
}
}
+ @GuardedBy("this")
public void initAndVerifyDetectorLocked(
@NonNull Identity voiceInteractorIdentity,
@Nullable PersistableBundle options,
@@ -730,6 +754,7 @@
detectorType);
}
+ @GuardedBy("this")
public void destroyDetectorLocked(IBinder token) {
Slog.v(TAG, "destroyDetectorLocked");
@@ -748,6 +773,7 @@
}
}
+ @GuardedBy("this")
public void shutdownHotwordDetectionServiceLocked() {
if (DEBUG) {
Slog.d(TAG, "shutdownHotwordDetectionServiceLocked");
@@ -760,6 +786,16 @@
mHotwordDetectionConnection = null;
}
+ @GuardedBy("this")
+ public void setVisualQueryDetectionAttentionListenerLocked(
+ @Nullable IVisualQueryDetectionAttentionListener listener) {
+ if (mHotwordDetectionConnection == null) {
+ return;
+ }
+ mHotwordDetectionConnection.setVisualQueryDetectionAttentionListenerLocked(listener);
+ }
+
+ @GuardedBy("this")
public void startPerceivingLocked(IVisualQueryDetectionVoiceInteractionCallback callback) {
if (DEBUG) {
Slog.d(TAG, "startPerceivingLocked");
@@ -773,6 +809,7 @@
mHotwordDetectionConnection.startPerceivingLocked(callback);
}
+ @GuardedBy("this")
public void stopPerceivingLocked() {
if (DEBUG) {
Slog.d(TAG, "stopPerceivingLocked");
@@ -786,6 +823,7 @@
mHotwordDetectionConnection.stopPerceivingLocked();
}
+ @GuardedBy("this")
public void startListeningFromMicLocked(
AudioFormat audioFormat,
IMicrophoneHotwordDetectionVoiceInteractionCallback callback) {
@@ -801,6 +839,7 @@
mHotwordDetectionConnection.startListeningFromMicLocked(audioFormat, callback);
}
+ @GuardedBy("this")
public void startListeningFromExternalSourceLocked(
ParcelFileDescriptor audioStream,
AudioFormat audioFormat,
@@ -825,6 +864,7 @@
options, token, callback);
}
+ @GuardedBy("this")
public void stopListeningFromMicLocked() {
if (DEBUG) {
Slog.d(TAG, "stopListeningFromMicLocked");
@@ -838,6 +878,7 @@
mHotwordDetectionConnection.stopListeningFromMicLocked();
}
+ @GuardedBy("this")
public void triggerHardwareRecognitionEventForTestLocked(
SoundTrigger.KeyphraseRecognitionEvent event,
IHotwordRecognitionStatusCallback callback) {
@@ -852,6 +893,7 @@
mHotwordDetectionConnection.triggerHardwareRecognitionEventForTestLocked(event, callback);
}
+ @GuardedBy("this")
public IRecognitionStatusCallback createSoundTriggerCallbackLocked(
IHotwordRecognitionStatusCallback callback) {
if (DEBUG) {
@@ -876,6 +918,7 @@
return null;
}
+ @GuardedBy("this")
boolean isIsolatedProcessLocked(@NonNull ServiceInfo serviceInfo) {
return (serviceInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) != 0
&& (serviceInfo.flags & ServiceInfo.FLAG_EXTERNAL_SERVICE) == 0;
@@ -889,6 +932,7 @@
mHotwordDetectionConnection.forceRestart();
}
+ @GuardedBy("this")
void setDebugHotwordLoggingLocked(boolean logging) {
if (mHotwordDetectionConnection == null) {
Slog.w(TAG, "Failed to set temporary debug logging: no hotword detection active");
@@ -897,6 +941,7 @@
mHotwordDetectionConnection.setDebugHotwordLoggingLocked(logging);
}
+ @GuardedBy("this")
void resetHotwordDetectionConnectionLocked() {
if (DEBUG) {
Slog.d(TAG, "resetHotwordDetectionConnectionLocked");
@@ -911,6 +956,7 @@
mHotwordDetectionConnection = null;
}
+ @GuardedBy("this")
public void dumpLocked(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!mValid) {
pw.print(" NOT VALID: ");
@@ -947,6 +993,7 @@
}
}
+ @GuardedBy("this")
void startLocked() {
Intent intent = new Intent(VoiceInteractionService.SERVICE_INTERFACE);
intent.setComponent(mComponent);
@@ -971,6 +1018,7 @@
}
}
+ @GuardedBy("this")
void shutdownLocked() {
// If there is an active session, cancel it to allow it to clean up its window and other
// state.
@@ -998,6 +1046,7 @@
}
}
+ @GuardedBy("this")
void notifySoundModelsChangedLocked() {
if (mService == null) {
Slog.w(TAG, "Not bound to voice interaction service " + mComponent);
diff --git a/telecomm/java/android/telecom/CallControl.java b/telecomm/java/android/telecom/CallControl.java
index 315ac67..f3c91f6 100644
--- a/telecomm/java/android/telecom/CallControl.java
+++ b/telecomm/java/android/telecom/CallControl.java
@@ -28,6 +28,7 @@
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.os.ResultReceiver;
+import android.text.TextUtils;
import com.android.internal.telecom.ClientTransactionalServiceRepository;
import com.android.internal.telecom.ICallControl;
@@ -67,7 +68,7 @@
/**
* @return the callId Telecom assigned to this CallControl object which should be attached to
- * an individual call.
+ * an individual call.
*/
@NonNull
public ParcelUuid getCallId() {
@@ -78,9 +79,9 @@
* Request Telecom set the call state to active.
*
* @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
- * will be called on.
+ * will be called on.
* @param callback that will be completed on the Telecom side that details success or failure
- * of the requested operation.
+ * of the requested operation.
*
* {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
* switched the call state to active
@@ -109,9 +110,9 @@
* but can be extended to setting a meeting to inactive.
*
* @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
- * will be called on.
+ * will be called on.
* @param callback that will be completed on the Telecom side that details success or failure
- * of the requested operation.
+ * of the requested operation.
*
* {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
* switched the call state to inactive
@@ -136,23 +137,42 @@
}
/**
- * Request Telecom set the call state to disconnect.
+ * Request Telecom disconnect the call and remove the call from telecom tracking.
*
- * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
- * will be called on.
- * @param callback that will be completed on the Telecom side that details success or failure
- * of the requested operation.
+ * @param disconnectCause represents the cause for disconnecting the call. The only valid
+ * codes for the {@link android.telecom.DisconnectCause} passed in are:
+ * <ul>
+ * <li>{@link DisconnectCause#LOCAL}</li>
+ * <li>{@link DisconnectCause#REMOTE}</li>
+ * <li>{@link DisconnectCause#REJECTED}</li>
+ * <li>{@link DisconnectCause#MISSED}</li>
+ * </ul>
*
- * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
- * disconnected the call.
+ * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
+ * will be called on.
*
- * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
- * disconnect the call. A {@link CallException} will be passed
- * that details why the operation failed.
+ * @param callback That will be completed on the Telecom side that details success or
+ * failure of the requested operation.
+ *
+ * {@link OutcomeReceiver#onResult} will be called if Telecom has
+ * successfully disconnected the call.
+ *
+ * {@link OutcomeReceiver#onError} will be called if Telecom has failed
+ * to disconnect the call. A {@link CallException} will be passed
+ * that details why the operation failed.
+ *
+ * <p>
+ * Note: After the call has been successfully disconnected, calling any CallControl API will
+ * result in the {@link OutcomeReceiver#onError} with
+ * {@link CallException#CODE_CALL_IS_NOT_BEING_TRACKED}.
*/
public void disconnect(@NonNull DisconnectCause disconnectCause,
@CallbackExecutor @NonNull Executor executor,
@NonNull OutcomeReceiver<Void, CallException> callback) {
+ Objects.requireNonNull(disconnectCause);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ validateDisconnectCause(disconnectCause);
if (mServerInterface != null) {
try {
mServerInterface.disconnect(mCallId, disconnectCause,
@@ -166,35 +186,6 @@
}
/**
- * Request Telecom reject the incoming call.
- *
- * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
- * will be called on.
- * @param callback that will be completed on the Telecom side that details success or failure
- * of the requested operation.
- *
- * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
- * rejected the incoming call.
- *
- * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
- * reject the incoming call. A {@link CallException} will be passed
- * that details why the operation failed.
- */
- public void rejectCall(@CallbackExecutor @NonNull Executor executor,
- @NonNull OutcomeReceiver<Void, CallException> callback) {
- if (mServerInterface != null) {
- try {
- mServerInterface.rejectCall(mCallId,
- new CallControlResultReceiver("rejectCall", executor, callback));
- } catch (RemoteException e) {
- throw e.rethrowAsRuntimeException();
- }
- } else {
- throw new IllegalStateException(INTERFACE_ERROR_MSG);
- }
- }
-
- /**
* Request start a call streaming session. On receiving valid request, telecom will bind to
* the {@link CallStreamingService} implemented by a general call streaming sender. So that the
* call streaming sender can perform streaming local device audio to another remote device and
@@ -231,10 +222,10 @@
* requesting a change. Instead, the new endpoint should be one of the valid endpoints provided
* by {@link CallEventCallback#onAvailableCallEndpointsChanged(List)}.
*
- * @param callEndpoint ; The {@link CallEndpoint} to change to.
- * @param executor ; The {@link Executor} on which the {@link OutcomeReceiver} callback
+ * @param callEndpoint The {@link CallEndpoint} to change to.
+ * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
* will be called on.
- * @param callback ; The {@link OutcomeReceiver} that will be completed on the Telecom side
+ * @param callback The {@link OutcomeReceiver} that will be completed on the Telecom side
* that details success or failure of the requested operation.
*
* {@link OutcomeReceiver#onResult} will be called if Telecom has
@@ -266,7 +257,9 @@
* Since {@link OutcomeReceiver}s cannot be passed via AIDL, a ResultReceiver (which can) must
* wrap the Clients {@link OutcomeReceiver} passed in and await for the Telecom Server side
* response in {@link ResultReceiver#onReceiveResult(int, Bundle)}.
- * @hide */
+ *
+ * @hide
+ */
private class CallControlResultReceiver extends ResultReceiver {
private final String mCallingMethod;
private final Executor mExecutor;
@@ -308,4 +301,18 @@
}
return new CallException(message, CallException.CODE_ERROR_UNKNOWN);
}
+
+ /** @hide */
+ private void validateDisconnectCause(DisconnectCause disconnectCause) {
+ final int code = disconnectCause.getCode();
+ if (code != DisconnectCause.LOCAL && code != DisconnectCause.REMOTE
+ && code != DisconnectCause.MISSED && code != DisconnectCause.REJECTED) {
+ throw new IllegalArgumentException(TextUtils.formatSimple(
+ "The DisconnectCause code provided, %d , is not a valid Disconnect code. Valid "
+ + "DisconnectCause codes are limited to [DisconnectCause.LOCAL, "
+ + "DisconnectCause.REMOTE, DisconnectCause.MISSED, or "
+ + "DisconnectCause.REJECTED]", disconnectCause.getCode()));
+ }
+ }
+
}
diff --git a/telecomm/java/android/telecom/CallException.java b/telecomm/java/android/telecom/CallException.java
index 0b0de6b..d191593 100644
--- a/telecomm/java/android/telecom/CallException.java
+++ b/telecomm/java/android/telecom/CallException.java
@@ -116,17 +116,6 @@
}
/**
- * Constructor for a new CallException when only message can be specified.
- * {@code CODE_ERROR_UNKNOWN} will be default code returned when calling {@code getCode}
- *
- * @param message related to why the exception was created
- */
- public CallException(@Nullable String message) {
- super(getMessage(message, CODE_ERROR_UNKNOWN));
- mMessage = message;
- }
-
- /**
* Constructor for a new CallException that has a defined error code in this class
*
* @param message related to why the exception was created
diff --git a/telecomm/java/android/telecom/CallStreamingService.java b/telecomm/java/android/telecom/CallStreamingService.java
index affa6b6..7f11128 100644
--- a/telecomm/java/android/telecom/CallStreamingService.java
+++ b/telecomm/java/android/telecom/CallStreamingService.java
@@ -79,7 +79,7 @@
break;
case MSG_CALL_STREAMING_CHANGED_CHANGED:
int state = (int) msg.obj;
- mCall.setStreamingState(state);
+ mCall.requestStreamingState(state);
CallStreamingService.this.onCallStreamingStateChanged(state);
break;
default:
diff --git a/telecomm/java/android/telecom/StreamingCall.java b/telecomm/java/android/telecom/StreamingCall.java
index 985cccc..4b27dd6 100644
--- a/telecomm/java/android/telecom/StreamingCall.java
+++ b/telecomm/java/android/telecom/StreamingCall.java
@@ -172,7 +172,7 @@
* to request holding, unholding and disconnecting this {@code StreamingCall}.
* @param state The current streaming state of the call.
*/
- public void setStreamingState(@StreamingCallState int state) {
+ public void requestStreamingState(@StreamingCallState int state) {
mAdapter.setStreamingState(state);
}
}
diff --git a/telecomm/java/android/telecom/StreamingCallAdapter.java b/telecomm/java/android/telecom/StreamingCallAdapter.java
index bd8727d..54a3e24 100644
--- a/telecomm/java/android/telecom/StreamingCallAdapter.java
+++ b/telecomm/java/android/telecom/StreamingCallAdapter.java
@@ -25,7 +25,7 @@
* Telecom. When Telecom binds to a {@link CallStreamingService}, an instance of this class is given
* to the general streaming app through which it can manipulate the streaming calls. Whe the general
* streaming app is notified of new ongoing streaming calls, it can execute
- * {@link StreamingCall#setStreamingState(int)} for the ongoing streaming calls the user on the
+ * {@link StreamingCall#requestStreamingState(int)} for the ongoing streaming calls the user on the
* receiver side would like to hold, unhold and disconnect.
*
* @hide
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index 7c86a75a..20564d6 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -951,6 +951,23 @@
public static final String EXTRA_CALL_SOURCE = "android.telecom.extra.CALL_SOURCE";
/**
+ * Intent action to trigger "switch to managed profile" dialog for call in SystemUI
+ *
+ * @hide
+ */
+ public static final String ACTION_SHOW_SWITCH_TO_WORK_PROFILE_FOR_CALL_DIALOG =
+ "android.telecom.action.SHOW_SWITCH_TO_WORK_PROFILE_FOR_CALL_DIALOG";
+
+ /**
+ * Extra specifying the managed profile user id.
+ * This is used with {@link TelecomManager#ACTION_SHOW_SWITCH_TO_WORK_PROFILE_FOR_CALL_DIALOG}
+ *
+ * @hide
+ */
+ public static final String EXTRA_MANAGED_PROFILE_USER_ID =
+ "android.telecom.extra.MANAGED_PROFILE_USER_ID";
+
+ /**
* Indicating the call is initiated via emergency dialer's shortcut button.
*
* @hide
diff --git a/telecomm/java/com/android/internal/telecom/ICallControl.aidl b/telecomm/java/com/android/internal/telecom/ICallControl.aidl
index a5c6e44..b78a77e 100644
--- a/telecomm/java/com/android/internal/telecom/ICallControl.aidl
+++ b/telecomm/java/com/android/internal/telecom/ICallControl.aidl
@@ -28,7 +28,6 @@
void setActive(String callId, in ResultReceiver callback);
void setInactive(String callId, in ResultReceiver callback);
void disconnect(String callId, in DisconnectCause disconnectCause, in ResultReceiver callback);
- void rejectCall(String callId, in ResultReceiver callback);
void startCallStreaming(String callId, in ResultReceiver callback);
void requestCallEndpointChange(in CallEndpoint callEndpoint, in ResultReceiver callback);
}
\ No newline at end of file
diff --git a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
index d4a8600..8662359 100644
--- a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
+++ b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
@@ -20,6 +20,7 @@
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@@ -859,7 +860,7 @@
if ((subManager != null) &&
(!subManager.isSubscriptionAssociatedWithUser(subId, callerUserHandle))) {
// If subId is not associated with calling user, return false.
- Log.e(LOG_TAG,"User[User ID:" + callerUserHandle.getIdentifier()
+ Log.e(LOG_TAG, "User[User ID:" + callerUserHandle.getIdentifier()
+ "] is not associated with Subscription ID:" + subId);
return false;
@@ -869,4 +870,27 @@
}
return true;
}
+
+ /**
+ * Ensure the caller (or self, if not processing an IPC) has
+ * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE} or
+ * {@link android.Manifest.permission#READ_PHONE_NUMBERS}.
+ *
+ * @throws SecurityException if the caller does not have the required permission/privileges
+ */
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.READ_PHONE_NUMBERS,
+ android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE
+ })
+ public static boolean checkCallingOrSelfReadPrivilegedPhoneStatePermissionOrReadPhoneNumber(
+ Context context, int subId, String callingPackage, @Nullable String callingFeatureId,
+ String message) {
+ if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+ return false;
+ }
+ return (context.checkCallingOrSelfPermission(
+ Manifest.permission.READ_PRIVILEGED_PHONE_STATE) == PERMISSION_GRANTED
+ || checkCallingOrSelfReadPhoneNumber(context, subId, callingPackage,
+ callingFeatureId, message));
+ }
}
diff --git a/telephony/java/android/telephony/CallState.java b/telephony/java/android/telephony/CallState.java
index 51ecfb0..836cb53 100644
--- a/telephony/java/android/telephony/CallState.java
+++ b/telephony/java/android/telephony/CallState.java
@@ -285,12 +285,12 @@
/**
* Builder of {@link CallState}
*
- * <p>The example below shows how you might create a new {@code CallState}:
+ * <p>The example below shows how you might create a new {@code CallState}. A precise call state
+ * {@link PreciseCallStates} is mandatory to build a CallState.
*
* <pre><code>
*
- * CallState = new CallState.Builder()
- * .setCallState(3)
+ * CallState = new CallState.Builder({@link PreciseCallStates})
* .setNetworkType({@link TelephonyManager#NETWORK_TYPE_LTE})
* .setCallQuality({@link CallQuality})
* .setImsCallSessionId({@link String})
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index f0f230f..acfc194 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -3866,13 +3866,12 @@
* NR_SA - NR SA is unmetered for sub-6 frequencies
* NR_SA_MMWAVE - NR SA is unmetered for mmwave frequencies
*
- * Note that this config only applies if an unmetered SubscriptionPlan is set via
- * {@link SubscriptionManager#setSubscriptionPlans(int, List)} or an unmetered override is set
+ * Note that this config only applies if an unmetered SubscriptionPlan is set via {@link
+ * SubscriptionManager#setSubscriptionPlans(int, List, long)} or an unmetered override is set
* via {@link SubscriptionManager#setSubscriptionOverrideUnmetered(int, boolean, int[], long)}
* or {@link SubscriptionManager#setSubscriptionOverrideUnmetered(int, boolean, long)}.
* If neither SubscriptionPlans nor an override are set, then no network types can be unmetered
* regardless of the value of this config.
- * TODO: remove other unmetered keys and replace with this
* @hide
*/
public static final String KEY_UNMETERED_NETWORK_TYPES_STRING_ARRAY =
@@ -3887,73 +3886,18 @@
* NR_SA - NR SA is unmetered when roaming for sub-6 frequencies
* NR_SA_MMWAVE - NR SA is unmetered when roaming for mmwave frequencies
*
- * Note that this config only applies if an unmetered SubscriptionPlan is set via
- * {@link SubscriptionManager#setSubscriptionPlans(int, List)} or an unmetered override is set
+ * Note that this config only applies if an unmetered SubscriptionPlan is set via {@link
+ * SubscriptionManager#setSubscriptionPlans(int, List, long)} or an unmetered override is set
* via {@link SubscriptionManager#setSubscriptionOverrideUnmetered(int, boolean, int[], long)}
* or {@link SubscriptionManager#setSubscriptionOverrideUnmetered(int, boolean, long)}.
* If neither SubscriptionPlans nor an override are set, then no network types can be unmetered
* when roaming regardless of the value of this config.
- * TODO: remove KEY_UNMETERED_NR_NSA_WHEN_ROAMING_BOOL and replace with this
* @hide
*/
public static final String KEY_ROAMING_UNMETERED_NETWORK_TYPES_STRING_ARRAY =
"roaming_unmetered_network_types_string_array";
/**
- * Whether NR (non-standalone) should be unmetered for all frequencies.
- * If either {@link #KEY_UNMETERED_NR_NSA_MMWAVE_BOOL} or
- * {@link #KEY_UNMETERED_NR_NSA_SUB6_BOOL} are true, then this value will be ignored.
- * @hide
- */
- public static final String KEY_UNMETERED_NR_NSA_BOOL = "unmetered_nr_nsa_bool";
-
- /**
- * Whether NR (non-standalone) frequencies above 6GHz (millimeter wave) should be unmetered.
- * If this is true, then the value for {@link #KEY_UNMETERED_NR_NSA_BOOL} will be ignored.
- * @hide
- */
- public static final String KEY_UNMETERED_NR_NSA_MMWAVE_BOOL = "unmetered_nr_nsa_mmwave_bool";
-
- /**
- * Whether NR (non-standalone) frequencies below 6GHz (sub6) should be unmetered.
- * If this is true, then the value for {@link #KEY_UNMETERED_NR_NSA_BOOL} will be ignored.
- * @hide
- */
- public static final String KEY_UNMETERED_NR_NSA_SUB6_BOOL = "unmetered_nr_nsa_sub6_bool";
-
- /**
- * Whether NR (non-standalone) should be unmetered when the device is roaming.
- * If false, then the values for {@link #KEY_UNMETERED_NR_NSA_BOOL},
- * {@link #KEY_UNMETERED_NR_NSA_MMWAVE_BOOL}, {@link #KEY_UNMETERED_NR_NSA_SUB6_BOOL},
- * and unmetered {@link SubscriptionPlan} will be ignored.
- * @hide
- */
- public static final String KEY_UNMETERED_NR_NSA_WHEN_ROAMING_BOOL =
- "unmetered_nr_nsa_when_roaming_bool";
-
- /**
- * Whether NR (standalone) should be unmetered for all frequencies.
- * If either {@link #KEY_UNMETERED_NR_SA_MMWAVE_BOOL} or
- * {@link #KEY_UNMETERED_NR_SA_SUB6_BOOL} are true, then this value will be ignored.
- * @hide
- */
- public static final String KEY_UNMETERED_NR_SA_BOOL = "unmetered_nr_sa_bool";
-
- /**
- * Whether NR (standalone) frequencies above 6GHz (millimeter wave) should be unmetered.
- * If this is true, then the value for {@link #KEY_UNMETERED_NR_SA_BOOL} will be ignored.
- * @hide
- */
- public static final String KEY_UNMETERED_NR_SA_MMWAVE_BOOL = "unmetered_nr_sa_mmwave_bool";
-
- /**
- * Whether NR (standalone) frequencies below 6GHz (sub6) should be unmetered.
- * If this is true, then the value for {@link #KEY_UNMETERED_NR_SA_BOOL} will be ignored.
- * @hide
- */
- public static final String KEY_UNMETERED_NR_SA_SUB6_BOOL = "unmetered_nr_sa_sub6_bool";
-
- /**
* Support ASCII 7-BIT encoding for long SMS. This carrier config is used to enable
* this feature.
* @hide
@@ -10043,13 +9987,6 @@
sDefaults.putStringArray(KEY_UNMETERED_NETWORK_TYPES_STRING_ARRAY, new String[] {
"NR_NSA", "NR_NSA_MMWAVE", "NR_SA", "NR_SA_MMWAVE"});
sDefaults.putStringArray(KEY_ROAMING_UNMETERED_NETWORK_TYPES_STRING_ARRAY, new String[0]);
- sDefaults.putBoolean(KEY_UNMETERED_NR_NSA_BOOL, false);
- sDefaults.putBoolean(KEY_UNMETERED_NR_NSA_MMWAVE_BOOL, false);
- sDefaults.putBoolean(KEY_UNMETERED_NR_NSA_SUB6_BOOL, false);
- sDefaults.putBoolean(KEY_UNMETERED_NR_NSA_WHEN_ROAMING_BOOL, false);
- sDefaults.putBoolean(KEY_UNMETERED_NR_SA_BOOL, false);
- sDefaults.putBoolean(KEY_UNMETERED_NR_SA_MMWAVE_BOOL, false);
- sDefaults.putBoolean(KEY_UNMETERED_NR_SA_SUB6_BOOL, false);
sDefaults.putBoolean(KEY_ASCII_7_BIT_SUPPORT_FOR_LONG_MESSAGE_BOOL, false);
sDefaults.putBoolean(KEY_SHOW_WIFI_CALLING_ICON_IN_STATUS_BAR_BOOL, false);
sDefaults.putBoolean(KEY_CARRIER_SUPPORTS_OPP_DATA_AUTO_PROVISIONING_BOOL, false);
diff --git a/telephony/java/android/telephony/TelephonyFrameworkInitializer.java b/telephony/java/android/telephony/TelephonyFrameworkInitializer.java
index 8308821..c2f5b8f 100644
--- a/telephony/java/android/telephony/TelephonyFrameworkInitializer.java
+++ b/telephony/java/android/telephony/TelephonyFrameworkInitializer.java
@@ -23,6 +23,7 @@
import android.telephony.euicc.EuiccCardManager;
import android.telephony.euicc.EuiccManager;
import android.telephony.ims.ImsManager;
+import android.telephony.satellite.SatelliteManager;
import com.android.internal.util.Preconditions;
@@ -98,6 +99,11 @@
context -> SmsManager.getSmsManagerForContextAndSubscriptionId(context,
SubscriptionManager.DEFAULT_SUBSCRIPTION_ID)
);
+ SystemServiceRegistry.registerContextAwareService(
+ Context.SATELLITE_SERVICE,
+ SatelliteManager.class,
+ context -> new SatelliteManager(context)
+ );
}
/** @hide */
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 0921834..819b0b2 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -6000,6 +6000,7 @@
* Returns the IMS private user identity (IMPI) that was loaded from the ISIM.
* @return the IMPI, or null if not present or not loaded
* @hide
+ * @deprecated use {@link #getImsPrivateUserIdentity()}
*/
@UnsupportedAppUsage
public String getIsimImpi() {
@@ -6018,6 +6019,35 @@
}
/**
+ * Returns the IMS private user identity (IMPI) of the subscription that was loaded from the
+ * ISIM records {@link #APPTYPE_ISIM}. This value is fetched from the Elementary file EF_IMPI.
+ * The contents of the file is a <b>Ip Multimedia Service Private User Identity</b> of the user
+ * as defined in the section 4.2.2 of 3GPP TS 131 103.
+ *
+ * @return IMPI (IMS private user identity) of type string.
+ * @throws IllegalStateException in case the ISIM has’t been loaded
+ * @throws SecurityException if the caller does not have the required permission/privileges
+ * @hide
+ */
+ @NonNull
+ @RequiresPermission(android.Manifest.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER)
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
+ public String getImsPrivateUserIdentity() {
+ try {
+ IPhoneSubInfo info = getSubscriberInfoService();
+ if (info == null) {
+ Rlog.e(TAG, "getImsPrivateUserIdentity(): IPhoneSubInfo instance is NULL");
+ throw new RuntimeException("IMPI error: Subscriber Info is null");
+ }
+ return info.getImsPrivateUserIdentity(getSubId(), getOpPackageName(),
+ getAttributionTag());
+ } catch (RemoteException | NullPointerException | IllegalArgumentException ex) {
+ Rlog.e(TAG, "getImsPrivateUserIdentity() Exception = " + ex);
+ throw new RuntimeException(ex.getMessage());
+ }
+ }
+
+ /**
* Returns the IMS home network domain name that was loaded from the ISIM {@see #APPTYPE_ISIM}.
* @return the IMS domain name. Returns {@code null} if ISIM hasn't been loaded or IMS domain
* hasn't been loaded or isn't present on the ISIM.
@@ -6050,6 +6080,7 @@
* @return an array of IMPU strings, with one IMPU per string, or null if
* not present or not loaded
* @hide
+ * @deprecated use {@link #getImsPublicUserIdentities()}
*/
@UnsupportedAppUsage
@Nullable
@@ -6070,6 +6101,39 @@
}
/**
+ * Returns the IMS public user identities (IMPU) of the subscription that was loaded from the
+ * ISIM records {@link #APPTYPE_ISIM}. This value is fetched from the Elementary file EF_IMPU.
+ * The contents of the file are <b>Ip Multimedia Service Public User Identities</b> of the user
+ * as defined in the section 4.2.4 of 3GPP TS 131 103. It contains one or more records.
+ *
+ * @return List of public user identities of type android.net.Uri or empty list if
+ * EF_IMPU is not available.
+ * @throws IllegalStateException in case the ISIM hasn’t been loaded
+ * @throws SecurityException if the caller does not have the required permission/privilege
+ * @hide
+ */
+ @NonNull
+ @RequiresPermission(anyOf = {android.Manifest.permission.READ_PHONE_NUMBERS,
+ android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE})
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
+ public List<Uri> getImsPublicUserIdentities() {
+ try {
+ IPhoneSubInfo info = getSubscriberInfoService();
+ if (info == null) {
+ throw new RuntimeException("IMPU error: Subscriber Info is null");
+ }
+ return info.getImsPublicUserIdentities(getSubId(), getOpPackageName(),
+ getAttributionTag());
+ } catch (IllegalArgumentException | NullPointerException ex) {
+ Rlog.e(TAG, "getImsPublicUserIdentities Exception = " + ex);
+ } catch (RemoteException ex) {
+ Rlog.e(TAG, "getImsPublicUserIdentities Exception = " + ex);
+ ex.rethrowAsRuntimeException();
+ }
+ return Collections.EMPTY_LIST;
+ }
+
+ /**
* Device call state: No activity.
*/
public static final int CALL_STATE_IDLE = 0;
@@ -9432,7 +9496,6 @@
ALLOWED_NETWORK_TYPES_REASON_POWER,
ALLOWED_NETWORK_TYPES_REASON_CARRIER,
ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G,
- ALLOWED_NETWORK_TYPES_REASON_USER_RESTRICTIONS,
})
@Retention(RetentionPolicy.SOURCE)
public @interface AllowedNetworkTypesReason {
@@ -9471,15 +9534,6 @@
public static final int ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G = 3;
/**
- * To indicate allowed network type change is requested by an update to the
- * {@link android.os.UserManager.DISALLOW_CELLULAR_2G} user restriction.
- *
- * @hide
- */
- @SystemApi
- public static final int ALLOWED_NETWORK_TYPES_REASON_USER_RESTRICTIONS = 4;
-
- /**
* Set the allowed network types of the device and provide the reason triggering the allowed
* network change.
* <p>Requires permission: {@link android.Manifest.permission#MODIFY_PHONE_STATE} or
@@ -9503,7 +9557,6 @@
* {@link android.Manifest.permission#MODIFY_PHONE_STATE} permissions.
* <ol>
* <li>{@code TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G}</li>
- * <li>{@code TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_USER_RESTRICTIONS}</li>
* </ol>
*/
@RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
@@ -9578,7 +9631,6 @@
case TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_POWER:
case TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_CARRIER:
case TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G:
- case ALLOWED_NETWORK_TYPES_REASON_USER_RESTRICTIONS:
return true;
}
return false;
@@ -14842,7 +14894,10 @@
/**
* The extra used with an {@link #ACTION_NETWORK_COUNTRY_CHANGED} to specify the
- * the country code in ISO-3166-1 alpha-2 format.
+ * the country code in ISO-3166-1 alpha-2 format. This is the same country code returned by
+ * {@link #getNetworkCountryIso()}. This might be an empty string when the country code is not
+ * available.
+ *
* <p class="note">
* Retrieve with {@link android.content.Intent#getStringExtra(String)}.
*/
@@ -14851,11 +14906,11 @@
/**
* The extra used with an {@link #ACTION_NETWORK_COUNTRY_CHANGED} to specify the
- * last known the country code in ISO-3166-1 alpha-2 format.
+ * last known the country code in ISO-3166-1 alpha-2 format. This might be an empty string when
+ * the country code was never available. The last known country code persists across reboot.
+ *
* <p class="note">
* Retrieve with {@link android.content.Intent#getStringExtra(String)}.
- *
- * @hide
*/
public static final String EXTRA_LAST_KNOWN_NETWORK_COUNTRY =
"android.telephony.extra.LAST_KNOWN_NETWORK_COUNTRY";
diff --git a/telephony/java/android/telephony/ims/ImsCallProfile.java b/telephony/java/android/telephony/ims/ImsCallProfile.java
index 1ea7fdc..d07edeb 100644
--- a/telephony/java/android/telephony/ims/ImsCallProfile.java
+++ b/telephony/java/android/telephony/ims/ImsCallProfile.java
@@ -27,6 +27,7 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.telecom.VideoProfile;
+import android.telephony.CallState;
import android.telephony.emergency.EmergencyNumber;
import android.telephony.emergency.EmergencyNumber.EmergencyCallRouting;
import android.telephony.emergency.EmergencyNumber.EmergencyServiceCategories;
@@ -78,7 +79,9 @@
public static final int SERVICE_TYPE_EMERGENCY = 2;
/**
- * Call type none
+ * This value is returned if there is no valid IMS call type defined for the call. For example,
+ * if an ongoing call is circuit-switched and {@link CallState#getImsCallType()} is called, this
+ * value will be returned.
*/
public static final int CALL_TYPE_NONE = 0;
/**
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt b/telephony/java/android/telephony/satellite/ISatellitePositionUpdateCallback.aidl
similarity index 60%
copy from packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
copy to telephony/java/android/telephony/satellite/ISatellitePositionUpdateCallback.aidl
index 497c272..5c3fa32 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
+++ b/telephony/java/android/telephony/satellite/ISatellitePositionUpdateCallback.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,12 +14,15 @@
* limitations under the License.
*/
-package com.android.credentialmanager.jetpack.developer
+package android.telephony.satellite;
+
+import android.telephony.satellite.PointingInfo;
/**
- * Internal exception used to indicate a parsing error while converting from a framework type to
- * a jetpack type.
- *
+ * Callback for position updates from the satellite service.
* @hide
*/
-internal class FrameworkClassParsingException : Exception()
\ No newline at end of file
+oneway interface ISatellitePositionUpdateCallback {
+ void onSatellitePositionUpdate(in PointingInfo pointingInfo);
+ void onMessageTransferStateUpdate(in int state);
+}
diff --git a/telephony/java/android/telephony/satellite/PointingInfo.aidl b/telephony/java/android/telephony/satellite/PointingInfo.aidl
new file mode 100644
index 0000000..7ff95cd
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/PointingInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2023, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.satellite;
+
+parcelable PointingInfo;
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
new file mode 100644
index 0000000..7a60c58
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -0,0 +1,420 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.satellite;
+
+import android.Manifest;
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresFeature;
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyFrameworkInitializer;
+import android.util.ArrayMap;
+
+import com.android.internal.telephony.IIntegerConsumer;
+import com.android.internal.telephony.ITelephony;
+import com.android.telephony.Rlog;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+/**
+ * Manages satellite operations such as provisioning, pointing, messaging, location sharing, etc.
+ * To get the object, call {@link Context#getSystemService(Context.SATELLITE_SERVICE)}.
+ * To create an instance of {@link SatelliteManager} associated with a specific subscription ID,
+ * call {@link #createForSubscriptionId(int)}.
+ *
+ * @hide
+ */
+@RequiresFeature(PackageManager.FEATURE_TELEPHONY_SATELLITE)
+public class SatelliteManager {
+ private static final String TAG = "SatelliteManager";
+
+ /**
+ * Map of all SatellitePositionUpdateCallback and their associated callback ids.
+ */
+ private final Map<SatellitePositionUpdateCallback, Integer> mSatellitePositionUpdateCallbacks =
+ new ArrayMap<>();
+
+ /**
+ * AtomicInteger for the id of the next SatellitePositionUpdateCallback.
+ */
+ private final AtomicInteger mSatellitePositionUpdateCallbackId = new AtomicInteger(0);
+
+ /**
+ * The subscription ID for this SatelliteManager.
+ */
+ private final int mSubId;
+
+ /**
+ * Context this SatelliteManager is for.
+ */
+ @Nullable private final Context mContext;
+
+ /**
+ * Create an instance of the SatelliteManager.
+ *
+ * @param context The context the SatelliteManager belongs to.
+ */
+ public SatelliteManager(@Nullable Context context) {
+ // TODO: replace DEFAULT_SUBSCRIPTION_ID with DEFAULT_SATELLITE_SUBSCRIPTION_ID
+ this(context, SubscriptionManager.DEFAULT_SUBSCRIPTION_ID);
+ }
+
+ /**
+ * Create a new SatelliteManager associated with the given subscription ID.
+ *
+ * @param subId The subscription ID to create the SatelliteManager with.
+ * @return A SatelliteManager that uses the given subscription ID for all calls.
+ */
+ @NonNull public SatelliteManager createForSubscriptionId(int subId) {
+ return new SatelliteManager(mContext, subId);
+ }
+
+ /**
+ * Create an instance of the SatelliteManager associated with a particular subscription.
+ *
+ * @param context The context the SatelliteManager belongs to.
+ * @param subId The subscription ID associated with the SatelliteManager.
+ */
+ private SatelliteManager(@Nullable Context context, int subId) {
+ mContext = context;
+ mSubId = subId;
+ }
+
+ /**
+ * Successful response.
+ */
+ public static final int SATELLITE_SERVICE_SUCCESS = 0;
+ /**
+ * Satellite server is not reachable.
+ */
+ public static final int SATELLITE_SERVICE_SERVER_NOT_REACHABLE = 1;
+ /**
+ * Error received from the satellite server.
+ */
+ public static final int SATELLITE_SERVICE_SERVER_ERROR = 2;
+ /**
+ * Unexpected telephony internal error.
+ */
+ public static final int SATELLITE_SERVICE_TELEPHONY_INTERNAL_ERROR = 3;
+ /**
+ * Modem error received from the satellite service.
+ */
+ public static final int SATELLITE_SERVICE_MODEM_ERROR = 4;
+ /**
+ * System error received from the satellite service.
+ */
+ public static final int SATELLITE_SERVICE_SYSTEM_ERROR = 5;
+ /**
+ * Invalid arguments passed.
+ */
+ public static final int SATELLITE_SERVICE_INVALID_ARGUMENTS = 6;
+ /**
+ * Invalid modem state.
+ */
+ public static final int SATELLITE_SERVICE_INVALID_MODEM_STATE = 7;
+ /**
+ * Invalid SIM state.
+ */
+ public static final int SATELLITE_SERVICE_INVALID_SIM_STATE = 8;
+ /**
+ * Invalid state.
+ */
+ public static final int SATELLITE_SERVICE_INVALID_STATE = 9;
+ /**
+ * Satellite service is unavailable.
+ */
+ public static final int SATELLITE_SERVICE_NOT_AVAILABLE = 10;
+ /**
+ * Satellite service is not supported by the device or OS.
+ */
+ public static final int SATELLITE_SERVICE_NOT_SUPPORTED = 11;
+ /**
+ * Satellite service is rate limited.
+ */
+ public static final int SATELLITE_SERVICE_RATE_LIMITED = 12;
+ /**
+ * Satellite service has no memory available.
+ */
+ public static final int SATELLITE_SERVICE_NO_MEMORY = 13;
+ /**
+ * Satellite service has no resources available.
+ */
+ public static final int SATELLITE_SERVICE_NO_RESOURCES = 14;
+ /**
+ * Failed to send a request to the satellite service.
+ */
+ public static final int SATELLITE_SERVICE_REQUEST_FAILED = 15;
+ /**
+ * Failed to send a request to the satellite service for the given subscription ID.
+ */
+ public static final int SATELLITE_SERVICE_INVALID_SUBSCRIPTION_ID = 16;
+ /**
+ * Error received from satellite service.
+ */
+ public static final int SATELLITE_SERVICE_ERROR = 17;
+ /**
+ * Satellite service is disabled on the requested subscription.
+ */
+ public static final int SATELLITE_SERVICE_DISABLED = 18;
+
+ /** @hide */
+ @IntDef(prefix = {"SATELLITE_SERVICE_"}, value = {
+ SATELLITE_SERVICE_SUCCESS,
+ SATELLITE_SERVICE_SERVER_NOT_REACHABLE,
+ SATELLITE_SERVICE_SERVER_ERROR,
+ SATELLITE_SERVICE_TELEPHONY_INTERNAL_ERROR,
+ SATELLITE_SERVICE_MODEM_ERROR,
+ SATELLITE_SERVICE_SYSTEM_ERROR,
+ SATELLITE_SERVICE_INVALID_ARGUMENTS,
+ SATELLITE_SERVICE_INVALID_MODEM_STATE,
+ SATELLITE_SERVICE_INVALID_SIM_STATE,
+ SATELLITE_SERVICE_INVALID_STATE,
+ SATELLITE_SERVICE_NOT_AVAILABLE,
+ SATELLITE_SERVICE_NOT_SUPPORTED,
+ SATELLITE_SERVICE_RATE_LIMITED,
+ SATELLITE_SERVICE_NO_MEMORY,
+ SATELLITE_SERVICE_NO_RESOURCES,
+ SATELLITE_SERVICE_REQUEST_FAILED,
+ SATELLITE_SERVICE_INVALID_SUBSCRIPTION_ID,
+ SATELLITE_SERVICE_ERROR,
+ SATELLITE_SERVICE_DISABLED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SatelliteServiceResult {}
+
+ /**
+ * Message transfer is waiting to acquire.
+ */
+ public static final int SATELLITE_MESSAGE_TRANSFER_STATE_WAITING_TO_ACQUIRE = 0;
+ /**
+ * Message is being sent.
+ */
+ public static final int SATELLITE_MESSAGE_TRANSFER_STATE_SENDING = 1;
+ /**
+ * Message is being received.
+ */
+ public static final int SATELLITE_MESSAGE_TRANSFER_STATE_RECEIVING = 2;
+ /**
+ * Message transfer is being retried.
+ */
+ public static final int SATELLITE_MESSAGE_TRANSFER_STATE_RETRYING = 3;
+ /**
+ * Message transfer is complete.
+ */
+ public static final int SATELLITE_MESSAGE_TRANSFER_STATE_COMPLETE = 4;
+
+ /** @hide */
+ @IntDef(prefix = {"SATELLITE_MESSAGE_TRANSFER_STATE_"}, value = {
+ SATELLITE_MESSAGE_TRANSFER_STATE_WAITING_TO_ACQUIRE,
+ SATELLITE_MESSAGE_TRANSFER_STATE_SENDING,
+ SATELLITE_MESSAGE_TRANSFER_STATE_RECEIVING,
+ SATELLITE_MESSAGE_TRANSFER_STATE_RETRYING,
+ SATELLITE_MESSAGE_TRANSFER_STATE_COMPLETE
+ })
+ public @interface SatelliteMessageTransferState {}
+
+ /**
+ * Callback for position updates from the satellite service.
+ */
+ public interface SatellitePositionUpdateCallback {
+ /**
+ * Called when the satellite position changes.
+ *
+ * @param pointingInfo The pointing info containing the satellite location.
+ */
+ void onSatellitePositionUpdate(@NonNull PointingInfo pointingInfo);
+
+ /**
+ * Called when satellite message transfer state changes.
+ *
+ * @param state The new message transfer state.
+ */
+ void onMessageTransferStateUpdate(@SatelliteMessageTransferState int state);
+ }
+
+ /**
+ * Start receiving satellite position updates.
+ * This can be called by the pointing UI when the user starts pointing to the satellite.
+ * Modem should continue to report the pointing input as the device or satellite moves.
+ * Satellite position updates are started only on {@link #SATELLITE_SERVICE_SUCCESS}.
+ * All other results indicate that this operation failed.
+ *
+ * @param executor The executor to run callbacks on.
+ * @param callback The callback to notify of changes in satellite position.
+ * @return The result of the operation.
+ */
+ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
+ @SatelliteServiceResult public int startSatellitePositionUpdates(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull SatellitePositionUpdateCallback callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null) {
+ int id;
+ if (mSatellitePositionUpdateCallbacks.containsKey(callback)) {
+ id = mSatellitePositionUpdateCallbacks.get(callback);
+ } else {
+ id = mSatellitePositionUpdateCallbackId.getAndIncrement();
+ }
+ int result = telephony.startSatellitePositionUpdates(mSubId, id,
+ new ISatellitePositionUpdateCallback.Stub() {
+ @Override
+ public void onSatellitePositionUpdate(
+ @NonNull PointingInfo pointingInfo) {
+ logd("onSatellitePositionUpdate: pointingInfo=" + pointingInfo);
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> callback.onSatellitePositionUpdate(pointingInfo)));
+ }
+
+ @Override
+ public void onMessageTransferStateUpdate(
+ @SatelliteMessageTransferState int state) {
+ logd("onMessageTransferStateUpdate: state=" + state);
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> callback.onMessageTransferStateUpdate(state)));
+ }
+ });
+ if (result == SATELLITE_SERVICE_SUCCESS) {
+ mSatellitePositionUpdateCallbacks.put(callback, id);
+ }
+ return result;
+ } else {
+ throw new IllegalStateException("telephony service is null.");
+ }
+ } catch (RemoteException ex) {
+ loge("startSatellitePositionUpdates RemoteException: " + ex);
+ ex.rethrowFromSystemServer();
+ }
+ return SATELLITE_SERVICE_REQUEST_FAILED;
+ }
+
+ /**
+ * Stop receiving satellite position updates.
+ * This can be called by the pointing UI when the user stops pointing to the satellite.
+ * Satellite position updates are stopped only on {@link #SATELLITE_SERVICE_SUCCESS}.
+ * All other results indicate that this operation failed.
+ *
+ * @param callback The callback that was passed in {@link
+ * #startSatellitePositionUpdates(Executor, SatellitePositionUpdateCallback)}.
+ * @return The result of the operation.
+ * @throws IllegalArgumentException if the callback is invalid.
+ */
+ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
+ @SatelliteServiceResult public int stopSatellitePositionUpdates(
+ @NonNull SatellitePositionUpdateCallback callback) {
+ Objects.requireNonNull(callback);
+
+ if (!mSatellitePositionUpdateCallbacks.containsKey(callback)) {
+ throw new IllegalArgumentException(
+ "startSatellitePositionUpdates was never called with the callback provided.");
+ }
+
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null) {
+ int result = telephony.stopSatellitePositionUpdates(mSubId,
+ mSatellitePositionUpdateCallbacks.get(callback));
+ if (result == SATELLITE_SERVICE_SUCCESS) {
+ mSatellitePositionUpdateCallbacks.remove(callback);
+ // TODO: Notify SmsHandler that pointing UI stopped
+ }
+ return result;
+ } else {
+ throw new IllegalStateException("telephony service is null.");
+ }
+ } catch (RemoteException ex) {
+ loge("stopSatellitePositionUpdates RemoteException: " + ex);
+ ex.rethrowFromSystemServer();
+ }
+ return SATELLITE_SERVICE_REQUEST_FAILED;
+ }
+
+ /**
+ * Get maximum number of characters per text message on satellite.
+ * @param executor - The executor on which the result listener will be called.
+ * @param resultListener - Listener that will be called when the operation is successful.
+ * If this method returns {@link #SATELLITE_SERVICE_SUCCESS}, listener
+ * will be called with maximum characters limit.
+ *
+ * @throws SecurityException if the caller doesn't have required permission.
+ *
+ * @return The result of the operation
+ */
+ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
+ @SatelliteServiceResult
+ public int getMaxCharactersPerSatelliteTextMessage(@NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<Integer> resultListener) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(resultListener);
+
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null) {
+ IIntegerConsumer internalCallback = new IIntegerConsumer.Stub() {
+ @Override
+ public void accept(int result) {
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(result)));
+ }
+ };
+
+ return telephony.getMaxCharactersPerSatelliteTextMessage(mSubId, internalCallback);
+ } else {
+ throw new IllegalStateException("telephony service is null.");
+ }
+ } catch (RemoteException ex) {
+ loge("getMaxCharactersPerSatelliteTextMessage() RemoteException:" + ex);
+ ex.rethrowFromSystemServer();
+ }
+ return SATELLITE_SERVICE_REQUEST_FAILED;
+ }
+
+ private static ITelephony getITelephony() {
+ ITelephony binder = ITelephony.Stub.asInterface(TelephonyFrameworkInitializer
+ .getTelephonyServiceManager()
+ .getTelephonyServiceRegisterer()
+ .get());
+ if (binder == null) {
+ throw new RuntimeException("Could not find Telephony Service.");
+ }
+ return binder;
+ }
+
+ private static void logd(@NonNull String log) {
+ Rlog.d(TAG, log);
+ }
+
+ private static void loge(@NonNull String log) {
+ Rlog.e(TAG, log);
+ }
+}
diff --git a/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl b/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl
index 3dfc81e..b4d93fd 100644
--- a/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl
+++ b/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl
@@ -180,6 +180,19 @@
String getIsimImpi(int subId);
/**
+ * Fetches the ISIM PrivateUserIdentity (EF_IMPI) based on subId
+ *
+ * @param subId subscriptionId
+ * @return IMPI (IMS private user identity) of type string or null if EF_IMPI is not available.
+ * @throws IllegalArgumentException if the subscriptionId is not valid
+ * @throws IllegalStateException in case the ISIM hasn’t been loaded.
+ * @throws SecurityException if the caller does not have the required permission
+ */
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ + "android.Manifest.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER)")
+ String getImsPrivateUserIdentity(int subId, String callingPackage, String callingFeatureId);
+
+ /**
* Returns the IMS home network domain name that was loaded from the ISIM.
* @return the IMS domain name, or null if not present or not loaded
*/
@@ -193,6 +206,14 @@
String[] getIsimImpu(int subId);
/**
+ * Fetches the ISIM public user identities (EF_IMPU) from UICC based on subId
+ */
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" +
+ "anyOf={android.Manifest.permission.READ_PHONE_NUMBERS, android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE})")
+ List<Uri> getImsPublicUserIdentities(int subId, String callingPackage,
+ String callingFeatureId);
+
+ /**
* Returns the IMS Service Table (IST) that was loaded from the ISIM.
* @return IMS Service Table or null if not present or not loaded
*/
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 48b1657..5486365 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -66,6 +66,7 @@
import android.telephony.ims.aidl.IImsRegistration;
import android.telephony.ims.aidl.IImsRegistrationCallback;
import android.telephony.ims.aidl.IRcsConfigCallback;
+import android.telephony.satellite.ISatellitePositionUpdateCallback;
import com.android.ims.internal.IImsServiceFeatureCallback;
import com.android.internal.telephony.CellNetworkScanResult;
import com.android.internal.telephony.IBooleanConsumer;
@@ -2636,14 +2637,14 @@
*/
boolean isRemovableEsimDefaultEuicc(String callingPackage);
- /**
- * Get the component name of the default app to direct respond-via-message intent for the
- * user associated with this subscription, update the cache if there is no respond-via-message
- * application currently configured for this user.
- * @return component name of the app and class to direct Respond Via Message intent to, or
- * {@code null} if the functionality is not supported.
- * @hide
- */
+ /**
+ * Get the component name of the default app to direct respond-via-message intent for the
+ * user associated with this subscription, update the cache if there is no respond-via-message
+ * application currently configured for this user.
+ * @return component name of the app and class to direct Respond Via Message intent to, or
+ * {@code null} if the functionality is not supported.
+ * @hide
+ */
ComponentName getDefaultRespondViaMessageApplication(int subId, boolean updateIfNeeded);
/**
@@ -2667,12 +2668,12 @@
void setNullCipherAndIntegrityEnabled(boolean enabled);
/**
- * Get whether the radio is able to connect with null ciphering or integrity
- * algorithms. Note that this retrieves the phone-global preference and not
- * the state of the radio.
- *
- * @hide
- */
+ * Get whether the radio is able to connect with null ciphering or integrity
+ * algorithms. Note that this retrieves the phone-global preference and not
+ * the state of the radio.
+ *
+ * @hide
+ */
boolean isNullCipherAndIntegrityPreferenceEnabled();
/**
@@ -2696,5 +2697,21 @@
/**
* Get the carrier restriction status of the device.
*/
- void getCarrierRestrictionStatus(IIntegerConsumer internalCallback, String packageName);
-}
+ void getCarrierRestrictionStatus(IIntegerConsumer internalCallback, String packageName);
+
+ /**
+ * Start receiving satellite pointing updates.
+ */
+ int startSatellitePositionUpdates(int subId, int callbackId,
+ in ISatellitePositionUpdateCallback callback);
+
+ /**
+ * Stop receiving satellite pointing updates.
+ */
+ int stopSatellitePositionUpdates(int subId, int callbackId);
+
+ /**
+ * Get maximum number of characters per text message on satellite.
+ */
+ int getMaxCharactersPerSatelliteTextMessage(int subId, IIntegerConsumer internalCallback);
+}
\ No newline at end of file
diff --git a/test-mock/api/system-current.txt b/test-mock/api/system-current.txt
index f87785b..9e022f0 100644
--- a/test-mock/api/system-current.txt
+++ b/test-mock/api/system-current.txt
@@ -5,9 +5,7 @@
method public android.content.Context createCredentialProtectedStorageContext();
method public java.io.File getPreloadsFileCache();
method public boolean isCredentialProtectedStorage();
- method public void sendBroadcast(android.content.Intent, String, android.os.Bundle);
method public void sendBroadcastAsUser(android.content.Intent, android.os.UserHandle, String, android.os.Bundle);
- method public void sendOrderedBroadcast(android.content.Intent, String, android.os.Bundle, android.content.BroadcastReceiver, android.os.Handler, int, String, android.os.Bundle);
}
@Deprecated public class MockPackageManager extends android.content.pm.PackageManager {
diff --git a/tests/BinaryTransparencyHostTest/Android.bp b/tests/BinaryTransparencyHostTest/Android.bp
index 142e3dd..dc6bdff 100644
--- a/tests/BinaryTransparencyHostTest/Android.bp
+++ b/tests/BinaryTransparencyHostTest/Android.bp
@@ -35,6 +35,7 @@
data: [
":BinaryTransparencyTestApp",
":EasterEgg",
+ ":com.android.apex.cts.shim.v2_rebootless_prebuilt",
],
test_suites: [
"general-tests",
diff --git a/tests/BinaryTransparencyHostTest/src/android/transparency/test/BaseInstallMultiple.java b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BaseInstallMultiple.java
new file mode 100644
index 0000000..3e94f25
--- /dev/null
+++ b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BaseInstallMultiple.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.transparency.test;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Base class for invoking the install-multiple command via ADB. Subclass this for less typing:
+ *
+ * <code> private class InstallMultiple extends BaseInstallMultiple<InstallMultiple> { public
+ * InstallMultiple() { super(getDevice(), null); } } </code>
+ */
+/*package*/ class BaseInstallMultiple<T extends BaseInstallMultiple<?>> {
+
+ private final ITestDevice mDevice;
+ private final IBuildInfo mBuild;
+
+ private final List<String> mArgs = new ArrayList<>();
+ private final Map<File, String> mFileToRemoteMap = new HashMap<>();
+
+ /*package*/ BaseInstallMultiple(ITestDevice device, IBuildInfo buildInfo) {
+ mDevice = device;
+ mBuild = buildInfo;
+ addArg("-g");
+ }
+
+ T addArg(String arg) {
+ mArgs.add(arg);
+ return (T) this;
+ }
+
+ T addFile(String filename) throws FileNotFoundException {
+ return addFile(filename, filename);
+ }
+
+ T addFile(String filename, String remoteName) throws FileNotFoundException {
+ CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mBuild);
+ mFileToRemoteMap.put(buildHelper.getTestFile(filename), remoteName);
+ return (T) this;
+ }
+
+ T inheritFrom(String packageName) {
+ addArg("-r");
+ addArg("-p " + packageName);
+ return (T) this;
+ }
+
+ void run() throws DeviceNotAvailableException {
+ run(true);
+ }
+
+ void runExpectingFailure() throws DeviceNotAvailableException {
+ run(false);
+ }
+
+ private void run(boolean expectingSuccess) throws DeviceNotAvailableException {
+ final ITestDevice device = mDevice;
+
+ // Create an install session
+ final StringBuilder cmd = new StringBuilder();
+ cmd.append("pm install-create");
+ for (String arg : mArgs) {
+ cmd.append(' ').append(arg);
+ }
+
+ String result = device.executeShellCommand(cmd.toString());
+ TestCase.assertTrue(result, result.startsWith("Success"));
+
+ final int start = result.lastIndexOf("[");
+ final int end = result.lastIndexOf("]");
+ int sessionId = -1;
+ try {
+ if (start != -1 && end != -1 && start < end) {
+ sessionId = Integer.parseInt(result.substring(start + 1, end));
+ }
+ } catch (NumberFormatException e) {
+ throw new IllegalStateException("Failed to parse install session: " + result);
+ }
+ if (sessionId == -1) {
+ throw new IllegalStateException("Failed to create install session: " + result);
+ }
+
+ // Push our files into session. Ideally we'd use stdin streaming,
+ // but ddmlib doesn't support it yet.
+ for (final Map.Entry<File, String> entry : mFileToRemoteMap.entrySet()) {
+ final File file = entry.getKey();
+ final String remoteName = entry.getValue();
+ final String remotePath = "/data/local/tmp/" + file.getName();
+ if (!device.pushFile(file, remotePath)) {
+ throw new IllegalStateException("Failed to push " + file);
+ }
+
+ cmd.setLength(0);
+ cmd.append("pm install-write");
+ cmd.append(' ').append(sessionId);
+ cmd.append(' ').append(remoteName);
+ cmd.append(' ').append(remotePath);
+
+ result = device.executeShellCommand(cmd.toString());
+ TestCase.assertTrue(result, result.startsWith("Success"));
+ }
+
+ // Everything staged; let's pull trigger
+ cmd.setLength(0);
+ cmd.append("pm install-commit");
+ cmd.append(' ').append(sessionId);
+
+ result = device.executeShellCommand(cmd.toString());
+ if (expectingSuccess) {
+ TestCase.assertTrue(result, result.contains("Success"));
+ } else {
+ TestCase.assertFalse(result, result.contains("Success"));
+ }
+ }
+}
diff --git a/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java
index 84bed92..b8e9a17 100644
--- a/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java
+++ b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java
@@ -16,27 +16,39 @@
package android.transparency.test;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
-import org.junit.After;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.concurrent.TimeUnit;
+
// TODO: Add @Presubmit
@RunWith(DeviceJUnit4ClassRunner.class)
public final class BinaryTransparencyHostTest extends BaseHostJUnit4Test {
private static final String PACKAGE_NAME = "android.transparency.test.app";
- @After
- public void tearDown() throws Exception {
- uninstallPackage("com.android.egg");
+ private static final String JOB_ID = "1740526926";
+
+ /** Waiting time for the job to be scheduled */
+ private static final int JOB_CREATION_MAX_SECONDS = 30;
+
+ @Before
+ public void setUp() throws Exception {
+ cancelPendingJob();
}
@Test
@@ -59,13 +71,63 @@
@Test
public void testCollectAllUpdatedPreloadInfo() throws Exception {
- installPackage("EasterEgg.apk");
- runDeviceTest("testCollectAllUpdatedPreloadInfo");
+ try {
+ updatePreloadApp();
+ runDeviceTest("testCollectAllUpdatedPreloadInfo");
+ } finally {
+ // No need to wait until job complete, since we can't verifying very meaningfully.
+ cancelPendingJob();
+ uninstallPackage("com.android.egg");
+ }
}
@Test
- public void testMeasureMbas() throws Exception {
- // TODO(265244016): figure out a way to install an MBA
+ public void testCollectAllSilentInstalledMbaInfo() throws Exception {
+ try {
+ new InstallMultiple()
+ .addFile("ApkVerityTestApp.apk")
+ .addFile("ApkVerityTestAppSplit.apk")
+ .run();
+ updatePreloadApp();
+ assertNotNull(getDevice().getAppPackageInfo("com.android.apkverity"));
+ assertNotNull(getDevice().getAppPackageInfo("com.android.egg"));
+
+ assertTrue(getDevice().setProperty("debug.transparency.bg-install-apps",
+ "com.android.apkverity,com.android.egg"));
+ runDeviceTest("testCollectAllSilentInstalledMbaInfo");
+ } finally {
+ // No need to wait until job complete, since we can't verifying very meaningfully.
+ cancelPendingJob();
+ uninstallPackage("com.android.apkverity");
+ uninstallPackage("com.android.egg");
+ }
+ }
+
+ @Test
+ public void testRebootlessApexUpdateTriggersJobScheduling() throws Exception {
+ try {
+ installRebootlessApex();
+
+ // Verify
+ expectJobToBeScheduled();
+ } finally {
+ // No need to wait until job complete, since we can't verifying very meaningfully.
+ uninstallRebootlessApexThenReboot();
+ }
+ }
+
+ @Test
+ public void testPreloadUpdateTriggersJobScheduling() throws Exception {
+ try {
+ updatePreloadApp();
+
+ // Verify
+ expectJobToBeScheduled();
+ } finally {
+ // No need to wait until job complete, since we can't verifying very meaningfully.
+ cancelPendingJob();
+ uninstallPackage("com.android.egg");
+ }
}
private void runDeviceTest(String method) throws DeviceNotAvailableException {
@@ -74,4 +136,71 @@
options.setTestMethodName(method);
runDeviceTests(options);
}
+
+ private void cancelPendingJob() throws DeviceNotAvailableException {
+ CommandResult result = getDevice().executeShellV2Command(
+ "cmd jobscheduler cancel android " + JOB_ID);
+ if (result.getStatus() == CommandStatus.SUCCESS) {
+ CLog.d("Canceling, output: " + result.getStdout());
+ } else {
+ CLog.d("Something went wrong, error: " + result.getStderr());
+ }
+ }
+
+ private void expectJobToBeScheduled() throws Exception {
+ for (int i = 0; i < JOB_CREATION_MAX_SECONDS; i++) {
+ CommandResult result = getDevice().executeShellV2Command(
+ "cmd jobscheduler get-job-state android " + JOB_ID);
+ String state = result.getStdout().toString();
+ CLog.i("Job status: " + state);
+ if (state.startsWith("unknown")) {
+ // The job hasn't been scheduled yet. So try again.
+ TimeUnit.SECONDS.sleep(1);
+ } else if (result.getExitCode() != 0) {
+ fail("Failing due to unexpected job state: " + result);
+ } else {
+ // The job exists, which is all we care about here
+ return;
+ }
+ }
+ fail("Timed out waiting for the job to be scheduled");
+ }
+
+ private void installRebootlessApex() throws Exception {
+ installPackage("com.android.apex.cts.shim.v2_rebootless.apex", "--force-non-staged");
+ }
+
+ private void uninstallRebootlessApexThenReboot() throws DeviceNotAvailableException {
+ // Reboot only if the APEX is not the pre-install one.
+ CommandResult result = getDevice().executeShellV2Command(
+ "pm list packages -f --apex-only |grep com.android.apex.cts.shim");
+ assertTrue(result.getStatus() == CommandStatus.SUCCESS);
+ if (result.getStdout().contains("/data/apex/active/")) {
+ uninstallPackage("com.android.apex.cts.shim");
+ getDevice().reboot();
+
+ // Reboot enforces SELinux. Make it permissive again.
+ CommandResult runResult = getDevice().executeShellV2Command("setenforce 0");
+ assertTrue(runResult.getStatus() == CommandStatus.SUCCESS);
+ }
+ }
+
+ private void updatePreloadApp() throws DeviceNotAvailableException {
+ CommandResult result = getDevice().executeShellV2Command("pm path com.android.egg");
+ assertTrue(result.getStatus() == CommandStatus.SUCCESS);
+ assertThat(result.getStdout()).startsWith("package:/system/app/");
+ String path = result.getStdout().replaceFirst("^package:", "");
+
+ result = getDevice().executeShellV2Command("pm install " + path);
+ assertTrue(result.getStatus() == CommandStatus.SUCCESS);
+ }
+
+ private class InstallMultiple extends BaseInstallMultiple<InstallMultiple> {
+ InstallMultiple() {
+ super(getDevice(), getBuild());
+ // Needed since in getMockBackgroundInstalledPackages, getPackageInfo runs as the caller
+ // uid. This also makes it consistent with installPackage's behavior.
+ addArg("--force-queryable");
+ }
+ }
}
diff --git a/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java b/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java
index aedb366..c087a85 100644
--- a/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java
+++ b/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java
@@ -33,8 +33,10 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.ArrayList;
import java.util.HashSet;
import java.util.HexFormat;
+import java.util.Set;
import java.util.stream.Collectors;
@RunWith(AndroidJUnit4.class)
@@ -56,11 +58,12 @@
assertThat(args).isNotNull();
int number = Integer.valueOf(args.getString("apex-number"));
assertThat(number).isGreaterThan(0);
- var expectedApexNames = new HashSet<String>();
+ var expectedApexNames = new ArrayList<String>();
for (var i = 0; i < number; i++) {
String moduleName = args.getString("apex-" + Integer.toString(i));
expectedApexNames.add(moduleName);
}
+ assertThat(expectedApexNames).containsNoDuplicates();
// Action
var apexInfoList = mBt.collectAllApexInfo(/* includeTestOnly */ true);
@@ -109,4 +112,35 @@
assertThat(updatedPreload.mbaStatus).isEqualTo(/* MBA_STATUS_UPDATED_PRELOAD */ 2);
assertThat(updatedPreload.signerDigests).asList().containsNoneOf(null, "");
}
+
+ @Test
+ public void testCollectAllSilentInstalledMbaInfo() {
+ // Action
+ var appInfoList = mBt.collectAllSilentInstalledMbaInfo(new Bundle());
+
+ // Verify
+ assertThat(appInfoList).isNotEmpty(); // because we just installed from the host side
+
+ var expectedAppNames = Set.of("com.android.apkverity", "com.android.egg");
+ var actualAppNames = appInfoList.stream().map((appInfo) -> appInfo.packageName)
+ .collect(Collectors.toList());
+ assertThat(actualAppNames).containsAtLeastElementsIn(expectedAppNames);
+
+ var actualSplitNames = new ArrayList<String>();
+ for (var appInfo : appInfoList) {
+ Log.d(TAG, "Received " + appInfo.packageName + " as a silent install");
+ if (expectedAppNames.contains(appInfo.packageName)) {
+ assertThat(appInfo.longVersion).isGreaterThan(0);
+ assertThat(appInfo.digestAlgorithm).isGreaterThan(0);
+ assertThat(appInfo.digest).isNotEmpty();
+ assertThat(appInfo.mbaStatus).isEqualTo(/* MBA_STATUS_NEW_INSTALL */ 3);
+ assertThat(appInfo.signerDigests).asList().containsNoneOf(null, "");
+
+ if (appInfo.splitName != null) {
+ actualSplitNames.add(appInfo.splitName);
+ }
+ }
+ }
+ assertThat(actualSplitNames).containsExactly("feature_x"); // Name of ApkVerityTestAppSplit
+ }
}
diff --git a/tests/ChoreographerTests/Android.bp b/tests/ChoreographerTests/Android.bp
new file mode 100644
index 0000000..ff252f7
--- /dev/null
+++ b/tests/ChoreographerTests/Android.bp
@@ -0,0 +1,46 @@
+// Copyright 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+ name: "ChoreographerTests",
+ srcs: ["src/**/*.java"],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ ],
+ static_libs: [
+ "androidx.test.core",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "compatibility-device-util-axt",
+ "com.google.android.material_material",
+ "truth-prebuilt",
+ ],
+ resource_dirs: ["src/main/res"],
+ certificate: "platform",
+ platform_apis: true,
+ test_suites: ["device-tests"],
+ optimize: {
+ enabled: false,
+ },
+}
diff --git a/tests/ChoreographerTests/AndroidManifest.xml b/tests/ChoreographerTests/AndroidManifest.xml
new file mode 100644
index 0000000..3283c90
--- /dev/null
+++ b/tests/ChoreographerTests/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.view.choreographertests">
+
+ <application android:debuggable="true" android:testOnly="true">
+ <uses-library android:name="android.test.runner"/>
+ <activity
+ android:name=".GraphicsActivity"
+ android:exported="false">
+ </activity>
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.view.choreographertests"
+ android:label="Tests of android.view.ChoreographerTests">
+ </instrumentation>
+</manifest>
\ No newline at end of file
diff --git a/tests/ChoreographerTests/AndroidTest.xml b/tests/ChoreographerTests/AndroidTest.xml
new file mode 100644
index 0000000..e717699
--- /dev/null
+++ b/tests/ChoreographerTests/AndroidTest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<configuration description="Config for ChoreographerTests cases">
+ <option name="test-suite-tag" value="cts" />
+ <option name="test-tag" value="ChoreographerTests"/>
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="install-arg" value="-t" />
+ <option name="test-file-name" value="ChoreographerTests.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.view.choreographertests" />
+ <option name="hidden-api-checks" value="false" />
+ <option name="isolated-storage" value="false" />
+ </test>
+</configuration>
\ No newline at end of file
diff --git a/tests/ChoreographerTests/OWNERS b/tests/ChoreographerTests/OWNERS
new file mode 100644
index 0000000..2b7de25
--- /dev/null
+++ b/tests/ChoreographerTests/OWNERS
@@ -0,0 +1,2 @@
+include platform/frameworks/base:/graphics/java/android/graphics/OWNERS
+include platform/frameworks/native:/services/surfaceflinger/OWNERS
\ No newline at end of file
diff --git a/tests/ChoreographerTests/TEST_MAPPING b/tests/ChoreographerTests/TEST_MAPPING
new file mode 100644
index 0000000..16a48ea
--- /dev/null
+++ b/tests/ChoreographerTests/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "ChoreographerTests"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java
new file mode 100644
index 0000000..3ea9651
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java
@@ -0,0 +1,455 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.choreographertests;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.Manifest;
+import android.app.compat.CompatChanges;
+import android.hardware.display.DisplayManager;
+import android.os.Looper;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+import android.view.Choreographer;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class AttachedChoreographerTest {
+ private static final String TAG = "AttachedChoreographerTest";
+ private static final long DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID = 170503758;
+ private static final long THRESHOLD_MS = 10;
+ private static final int FRAME_ITERATIONS = 21;
+ private static final int CALLBACK_MISSED_THRESHOLD = 2;
+
+ private final CountDownLatch mTestCompleteSignal = new CountDownLatch(2);
+ private final CountDownLatch mSurfaceCreationCountDown = new CountDownLatch(1);
+ private final CountDownLatch mNoCallbackSignal = new CountDownLatch(1);
+
+ private ActivityScenario<GraphicsActivity> mScenario;
+ private int mInitialMatchContentFrameRate;
+ private DisplayManager mDisplayManager;
+ private SurfaceView mSurfaceView;
+ private SurfaceHolder mSurfaceHolder;
+ private boolean mIsFirstCallback = true;
+ private int mCallbackMissedCounter = 0;
+
+ @Before
+ public void setUp() throws Exception {
+ mScenario = ActivityScenario.launch(GraphicsActivity.class);
+ mScenario.moveToState(Lifecycle.State.CREATED);
+ mScenario.onActivity(activity -> {
+ mSurfaceView = activity.findViewById(R.id.surface);
+ mSurfaceHolder = mSurfaceView.getHolder();
+ mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ mSurfaceCreationCountDown.countDown();
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width,
+ int height) {
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ }
+ });
+ });
+
+ UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ uiDevice.wakeUp();
+ uiDevice.executeShellCommand("wm dismiss-keyguard");
+ mScenario.moveToState(Lifecycle.State.RESUMED);
+
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+ Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
+ Manifest.permission.MODIFY_REFRESH_RATE_SWITCHING_TYPE,
+ Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS,
+ Manifest.permission.MANAGE_GAME_MODE);
+ mScenario.onActivity(activity -> {
+ mDisplayManager = activity.getSystemService(DisplayManager.class);
+ mInitialMatchContentFrameRate = toSwitchingType(
+ mDisplayManager.getMatchContentFrameRateUserPreference());
+ mDisplayManager.setRefreshRateSwitchingType(
+ DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY);
+ mDisplayManager.setShouldAlwaysRespectAppRequestedMode(true);
+ boolean changeIsEnabled =
+ CompatChanges.isChangeEnabled(
+ DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID);
+ Log.i(TAG, "DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGE_ID is "
+ + (changeIsEnabled ? "enabled" : "disabled"));
+ });
+ }
+
+ @After
+ public void tearDown() {
+ mDisplayManager.setRefreshRateSwitchingType(mInitialMatchContentFrameRate);
+ mDisplayManager.setShouldAlwaysRespectAppRequestedMode(false);
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .dropShellPermissionIdentity();
+ }
+
+ @Test
+ public void testCreateChoreographer() {
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ mTestCompleteSignal.countDown();
+ SurfaceControl sc1 = new SurfaceControl(sc, "AttachedChoreographerTests");
+ // Create attached choreographer with getChoreographer
+ sc1.getChoreographer();
+ assertTrue(sc1.hasChoreographer());
+ assertTrue(sc1.isValid());
+ sc1.release();
+
+ SurfaceControl sc2 = new SurfaceControl(sc, "AttachedChoreographerTests");
+ // Create attached choreographer with Looper.myLooper
+ sc2.getChoreographer(Looper.myLooper());
+ assertTrue(sc2.hasChoreographer());
+ assertTrue(sc2.isValid());
+ sc2.release();
+
+ SurfaceControl sc3 = new SurfaceControl(sc, "AttachedChoreographerTests");
+ // Create attached choreographer with Looper.myLooper
+ sc3.getChoreographer(Looper.getMainLooper());
+ assertTrue(sc3.hasChoreographer());
+ assertTrue(sc3.isValid());
+ sc3.release();
+ mTestCompleteSignal.countDown();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
+ fail("Test not finished in 2 Seconds");
+ }
+ }
+
+ @Test
+ public void testCopySurfaceControl() {
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ // Create attached choreographer
+ sc.getChoreographer();
+ assertTrue(sc.hasChoreographer());
+
+ // Use copy constructor
+ SurfaceControl copyConstructorSc = new SurfaceControl(sc, "AttachedChoreographerTests");
+ //Choreographer isn't copied over.
+ assertFalse(copyConstructorSc.hasChoreographer());
+ copyConstructorSc.getChoreographer();
+ assertTrue(copyConstructorSc.hasChoreographer());
+ mTestCompleteSignal.countDown();
+
+ // Use copyFrom
+ SurfaceControl copyFromSc = new SurfaceControl();
+ copyFromSc.copyFrom(sc, "AttachedChoreographerTests");
+ //Choreographer isn't copied over.
+ assertFalse(copyFromSc.hasChoreographer());
+ copyFromSc.getChoreographer();
+ assertTrue(copyFromSc.hasChoreographer());
+ mTestCompleteSignal.countDown();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
+ fail("Test not finished in 2 Seconds");
+ }
+ }
+
+ @Test
+ public void testMirrorSurfaceControl() {
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ // Create attached choreographer
+ sc.getChoreographer();
+ assertTrue(sc.hasChoreographer());
+ mTestCompleteSignal.countDown();
+
+ // Use mirrorSurface
+ SurfaceControl mirrorSc = SurfaceControl.mirrorSurface(sc);
+ //Choreographer isn't copied over.
+ assertFalse(mirrorSc.hasChoreographer());
+ mirrorSc.getChoreographer();
+ assertTrue(mirrorSc.hasChoreographer());
+ // make SurfaceControl invalid by releasing it.
+ mirrorSc.release();
+
+ assertTrue(sc.isValid());
+ assertFalse(mirrorSc.isValid());
+ assertFalse(mirrorSc.hasChoreographer());
+ assertThrows(NullPointerException.class, mirrorSc::getChoreographer);
+ mTestCompleteSignal.countDown();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
+ fail("Test not finished in 2 Seconds");
+ }
+ }
+
+ @Test
+ public void testPostFrameCallback() {
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ sc.getChoreographer().postFrameCallback(
+ frameTimeNanos -> mTestCompleteSignal.countDown());
+
+ SurfaceControl copySc = new SurfaceControl(sc, "AttachedChoreographerTests");
+ Choreographer copyChoreographer = copySc.getChoreographer();
+ // make SurfaceControl invalid by releasing it.
+ copySc.release();
+
+ assertTrue(sc.isValid());
+ assertFalse(copySc.isValid());
+ copyChoreographer.postFrameCallback(frameTimeNanos -> mNoCallbackSignal.countDown());
+ assertDoesReceiveCallback();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
+ fail("Test not finished in 2 Seconds");
+ }
+ }
+
+ @Test
+ public void testPostFrameCallbackDelayed() {
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ sc.getChoreographer(Looper.getMainLooper()).postFrameCallbackDelayed(
+ callback -> mTestCompleteSignal.countDown(),
+ /* delayMillis */ 5);
+
+ SurfaceControl copySc = new SurfaceControl(sc, "AttachedChoreographerTests");
+ Choreographer copyChoreographer = copySc.getChoreographer();
+ // make SurfaceControl invalid by releasing it.
+ copySc.release();
+
+ assertTrue(sc.isValid());
+ assertFalse(copySc.isValid());
+ copyChoreographer.postFrameCallbackDelayed(
+ frameTimeNanos -> mNoCallbackSignal.countDown(), /* delayMillis */5);
+ assertDoesReceiveCallback();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
+ fail("Test not finished in 2 Seconds");
+ }
+ }
+
+ @Test
+ public void testPostCallback() {
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ sc.getChoreographer().postCallback(Choreographer.CALLBACK_COMMIT,
+ mTestCompleteSignal::countDown, /* token */ this);
+
+ SurfaceControl copySc = new SurfaceControl(sc, "AttachedChoreographerTests");
+ Choreographer copyChoreographer = copySc.getChoreographer();
+ // make SurfaceControl invalid by releasing it.
+ copySc.release();
+
+ assertTrue(sc.isValid());
+ assertFalse(copySc.isValid());
+ copyChoreographer.postCallback(Choreographer.CALLBACK_COMMIT,
+ mNoCallbackSignal::countDown, /* token */ this);
+ assertDoesReceiveCallback();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
+ fail("Test not finished in 2 Seconds");
+ }
+ }
+
+ @Test
+ public void testPostCallbackDelayed() {
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ sc.getChoreographer().postCallbackDelayed(Choreographer.CALLBACK_COMMIT,
+ mTestCompleteSignal::countDown, /* token */ this, /* delayMillis */ 5);
+
+ SurfaceControl copySc = new SurfaceControl(sc, "AttachedChoreographerTests");
+ Choreographer copyChoreographer = copySc.getChoreographer();
+ // make SurfaceControl invalid by releasing it.
+ copySc.release();
+
+ assertTrue(sc.isValid());
+ assertFalse(copySc.isValid());
+ copyChoreographer.postCallbackDelayed(Choreographer.CALLBACK_COMMIT,
+ mNoCallbackSignal::countDown, /* token */ this, /* delayMillis */ 5);
+ assertDoesReceiveCallback();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
+ fail("Test not finished in 2 Seconds");
+ }
+ }
+
+ @Test
+ public void testPostVsyncCallback() {
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeout */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ sc.getChoreographer().postVsyncCallback(data -> mTestCompleteSignal.countDown());
+
+ SurfaceControl copySc = new SurfaceControl(sc, "AttachedChoreographerTests");
+ Choreographer copyChoreographer = copySc.getChoreographer();
+ // make SurfaceControl invalid by releasing it.
+ copySc.release();
+
+ assertTrue(sc.isValid());
+ assertFalse(copySc.isValid());
+ copyChoreographer.postVsyncCallback(data -> mNoCallbackSignal.countDown());
+ assertDoesReceiveCallback();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
+ fail("Test not finished in 2 Seconds");
+ }
+ }
+
+ @Test
+ public void testChoreographerDivisorRefreshRate() {
+ for (int divisor : new int[]{2, 3}) {
+ CountDownLatch continueLatch = new CountDownLatch(1);
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+ SurfaceControl sc = mSurfaceView.getSurfaceControl();
+ Choreographer choreographer = sc.getChoreographer();
+ SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+ float displayRefreshRate = activity.getDisplay().getMode().getRefreshRate();
+ float fps = displayRefreshRate / divisor;
+ long callbackDurationMs = Math.round(1000 / fps);
+ mCallbackMissedCounter = 0;
+ transaction.setFrameRate(sc, fps, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE)
+ .addTransactionCommittedListener(Runnable::run,
+ () -> verifyVsyncCallbacks(choreographer,
+ callbackDurationMs, continueLatch, FRAME_ITERATIONS))
+ .apply();
+ });
+ // wait for the previous callbacks to finish before moving to the next divisor
+ if (waitForCountDown(continueLatch, /* timeoutInSeconds */ 5L)) {
+ fail("Test not finished in 5 Seconds");
+ }
+ }
+ }
+
+ private void verifyVsyncCallbacks(Choreographer choreographer, long callbackDurationMs,
+ CountDownLatch continueLatch, int frameCount) {
+ long callbackRequestedTimeNs = System.nanoTime();
+ choreographer.postVsyncCallback(frameData -> {
+ if (frameCount > 0) {
+ if (!mIsFirstCallback) {
+ // Skip the first callback as it takes 1 frame
+ // to reflect the new refresh rate
+ long callbackDurationDiffMs = getCallbackDurationDiffInMs(
+ frameData.getFrameTimeNanos(),
+ callbackRequestedTimeNs, callbackDurationMs);
+ if (callbackDurationDiffMs < 0 || callbackDurationDiffMs > THRESHOLD_MS) {
+ mCallbackMissedCounter++;
+ Log.e(TAG, "Frame #" + Math.abs(frameCount - FRAME_ITERATIONS)
+ + " vsync callback failed, expected callback in "
+ + callbackDurationMs
+ + " With threshold of " + THRESHOLD_MS
+ + " but actual duration difference is " + callbackDurationDiffMs);
+ }
+ }
+ mIsFirstCallback = false;
+ verifyVsyncCallbacks(choreographer, callbackDurationMs,
+ continueLatch, frameCount - 1);
+ } else {
+ assertTrue("Missed timeline for " + mCallbackMissedCounter + " callbacks, while "
+ + CALLBACK_MISSED_THRESHOLD + " missed callbacks are allowed",
+ mCallbackMissedCounter <= CALLBACK_MISSED_THRESHOLD);
+ continueLatch.countDown();
+ }
+ });
+ }
+
+ private long getCallbackDurationDiffInMs(long callbackTimeNs, long requestedTimeNs,
+ long expectedCallbackMs) {
+ long actualTimeMs = TimeUnit.NANOSECONDS.toMillis(callbackTimeNs)
+ - TimeUnit.NANOSECONDS.toMillis(requestedTimeNs);
+ return Math.abs(expectedCallbackMs - actualTimeMs);
+ }
+
+ private boolean waitForCountDown(CountDownLatch countDownLatch, long timeoutInSeconds) {
+ try {
+ return !countDownLatch.await(timeoutInSeconds, TimeUnit.SECONDS);
+ } catch (InterruptedException ex) {
+ throw new AssertionError("Test interrupted", ex);
+ }
+ }
+
+ private int toSwitchingType(int matchContentFrameRateUserPreference) {
+ switch (matchContentFrameRateUserPreference) {
+ case DisplayManager.MATCH_CONTENT_FRAMERATE_NEVER:
+ return DisplayManager.SWITCHING_TYPE_NONE;
+ case DisplayManager.MATCH_CONTENT_FRAMERATE_SEAMLESSS_ONLY:
+ return DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS;
+ case DisplayManager.MATCH_CONTENT_FRAMERATE_ALWAYS:
+ return DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS;
+ default:
+ return -1;
+ }
+ }
+
+ private void assertDoesReceiveCallback() {
+ try {
+ if (mNoCallbackSignal.await(/* timeout */ 50L, TimeUnit.MILLISECONDS)) {
+ fail("Callback not supposed to be generated");
+ } else {
+ mTestCompleteSignal.countDown();
+ }
+ } catch (InterruptedException e) {
+ fail("Callback wait is interrupted " + e);
+ }
+ }
+}
diff --git a/tests/ChoreographerTests/src/main/java/android/view/choreographertests/GraphicsActivity.java b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/GraphicsActivity.java
new file mode 100644
index 0000000..50a6850
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/GraphicsActivity.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.choreographertests;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class GraphicsActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_attached_choreographer);
+ }
+}
diff --git a/tests/ChoreographerTests/src/main/res/layout/activity_attached_choreographer.xml b/tests/ChoreographerTests/src/main/res/layout/activity_attached_choreographer.xml
new file mode 100644
index 0000000..d6c8212
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/res/layout/activity_attached_choreographer.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <SurfaceView
+ android:id="@+id/surface"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/tests/ChoreographerTests/src/main/res/mipmap-hdpi/ic_launcher.png b/tests/ChoreographerTests/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/tests/ChoreographerTests/src/main/res/mipmap-mdpi/ic_launcher.png b/tests/ChoreographerTests/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/tests/ChoreographerTests/src/main/res/mipmap-xhdpi/ic_launcher.png b/tests/ChoreographerTests/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/ChoreographerTests/src/main/res/mipmap-xxhdpi/ic_launcher.png b/tests/ChoreographerTests/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/ChoreographerTests/src/main/res/values/strings.xml b/tests/ChoreographerTests/src/main/res/values/strings.xml
new file mode 100644
index 0000000..e66b001
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/res/values/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <string name="app_name">ChoreographerTests</string>
+</resources>
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
index 64ed453..73b5afe 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
@@ -184,12 +184,12 @@
}
open fun cujCompleted() {
- entireScreenCovered()
- statusBarLayerIsVisibleAtStartAndEnd()
- statusBarLayerPositionAtStartAndEnd()
- statusBarWindowIsAlwaysVisible()
- visibleLayersShownMoreThanOneConsecutiveEntry()
- visibleWindowsShownMoreThanOneConsecutiveEntry()
+ runAndIgnoreAssumptionViolation { entireScreenCovered() }
+ runAndIgnoreAssumptionViolation { statusBarLayerIsVisibleAtStartAndEnd() }
+ runAndIgnoreAssumptionViolation { statusBarLayerPositionAtStartAndEnd() }
+ runAndIgnoreAssumptionViolation { statusBarWindowIsAlwaysVisible() }
+ runAndIgnoreAssumptionViolation { visibleLayersShownMoreThanOneConsecutiveEntry() }
+ runAndIgnoreAssumptionViolation { visibleWindowsShownMoreThanOneConsecutiveEntry() }
runAndIgnoreAssumptionViolation { taskBarLayerIsVisibleAtStartAndEnd() }
runAndIgnoreAssumptionViolation { taskBarWindowIsAlwaysVisible() }
runAndIgnoreAssumptionViolation { navBarLayerIsVisibleAtStartAndEnd() }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
index f26ce25..8d2c643 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
@@ -259,7 +259,7 @@
snapshotLayers
.mapNotNull { snapshotLayer -> snapshotLayer.layer?.visibleRegion }
.toTypedArray()
- val snapshotRegion = RegionSubject.assertThat(visibleAreas, this, timestamp)
+ val snapshotRegion = RegionSubject(visibleAreas, this, timestamp)
val appVisibleRegion = it.visibleRegion(component)
if (snapshotRegion.region.isNotEmpty) {
snapshotRegion.coversExactly(appVisibleRegion.region)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplitTest.kt
similarity index 63%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplit.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplitTest.kt
index ea67729..7f2e057f 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplit.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplitTest.kt
@@ -33,13 +33,13 @@
* Test opening an activity that will launch another activity as ActivityEmbedding placeholder in
* split.
*
- * To run this test: `atest FlickerTests:OpenActivityEmbeddingPlaceholderSplit`
+ * To run this test: `atest FlickerTests:OpenActivityEmbeddingPlaceholderSplitTest`
*/
@RequiresDevice
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenActivityEmbeddingPlaceholderSplit(flicker: FlickerTest) :
+class OpenActivityEmbeddingPlaceholderSplitTest(flicker: FlickerTest) :
ActivityEmbeddingTestBase(flicker) {
/** {@inheritDoc} */
@@ -55,9 +55,21 @@
}
}
+ /** Main activity should become invisible after launching the placeholder primary activity. */
@Presubmit
@Test
- fun mainActivityBecomesInvisible() {
+ fun mainActivityWindowBecomesInvisible() {
+ flicker.assertWm {
+ isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ .then()
+ .isAppWindowInvisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ }
+ }
+
+ /** Main activity should become invisible after launching the placeholder primary activity. */
+ @Presubmit
+ @Test
+ fun mainActivityLayerBecomesInvisible() {
flicker.assertLayers {
isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
.then()
@@ -65,76 +77,41 @@
}
}
+ /**
+ * Placeholder primary and secondary should become visible after launch. The windows are not
+ * necessarily to become visible at the same time.
+ */
@Presubmit
@Test
- fun placeholderSplitBecomesVisible() {
- flicker.assertLayers {
- isInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+ fun placeholderSplitWindowsBecomeVisible() {
+ flicker.assertWm {
+ notContains(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
.then()
- .isVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+ .isAppWindowInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+ .then()
+ .isAppWindowVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
}
- flicker.assertLayers {
- isInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+ flicker.assertWm {
+ notContains(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
.then()
- .isVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+ .isAppWindowInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+ .then()
+ .isAppWindowVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
}
}
- /** {@inheritDoc} */
- @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
-
- /** {@inheritDoc} */
+ /** Placeholder primary and secondary should become visible together after launch. */
@Presubmit
@Test
- override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun statusBarLayerIsVisibleAtStartAndEnd() =
- super.statusBarLayerIsVisibleAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
- super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
- /** {@inheritDoc} */
- @Presubmit
- @Test
- override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
- super.visibleLayersShownMoreThanOneConsecutiveEntry()
+ fun placeholderSplitLayersBecomeVisible() {
+ flicker.assertLayers {
+ isInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+ isInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+ .then()
+ .isVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+ .isVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+ }
+ }
companion object {
/**
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingSecondaryToSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingSecondaryToSplitTest.kt
new file mode 100644
index 0000000..5fff15c
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingSecondaryToSplitTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.activityembedding
+
+import android.platform.test.annotations.Presubmit
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.ComponentNameMatcher
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test opening a secondary activity that will split with the main activity.
+ *
+ * To run this test: `atest FlickerTests:OpenActivityEmbeddingSecondaryToSplitTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class OpenActivityEmbeddingSecondaryToSplitTest(flicker: FlickerTest) :
+ ActivityEmbeddingTestBase(flicker) {
+
+ /** {@inheritDoc} */
+ override val transition: FlickerBuilder.() -> Unit = {
+ setup {
+ tapl.setExpectedRotationCheckEnabled(false)
+ testApp.launchViaIntent(wmHelper)
+ }
+ transitions { testApp.launchSecondaryActivity(wmHelper) }
+ teardown {
+ tapl.goHome()
+ testApp.exit(wmHelper)
+ }
+ }
+
+ /** Main activity should remain visible when enter split from fullscreen. */
+ @Presubmit
+ @Test
+ fun mainActivityWindowIsAlwaysVisible() {
+ flicker.assertWm {
+ isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ }
+ }
+
+ /**
+ * Main activity surface is animated from fullscreen to ActivityEmbedding split.
+ * During the transition, there is a period of time that it is covered by a snapshot of itself.
+ */
+ @Presubmit
+ @Test
+ fun mainActivityLayerIsAlwaysVisible() {
+ flicker.assertLayers {
+ isVisible(
+ ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT.or(
+ ComponentNameMatcher.TRANSITION_SNAPSHOT
+ )
+ )
+ }
+ flicker.assertLayersEnd {
+ isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ .isInvisible(ComponentNameMatcher.TRANSITION_SNAPSHOT)
+ }
+ }
+
+ /** Secondary activity should become visible after launching into split. */
+ @Presubmit
+ @Test
+ fun secondaryActivityWindowBecomesVisible() {
+ flicker.assertWm {
+ notContains(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ .then()
+ .isAppWindowInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ .then()
+ .isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ }
+ }
+
+ /** Secondary activity should become visible after launching into split. */
+ @Presubmit
+ @Test
+ fun secondaryActivityLayerBecomesVisible() {
+ flicker.assertLayers {
+ isInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ .then()
+ .isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ }
+ }
+
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): Collection<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests()
+ }
+ }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
index 368cc56..a2db587 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
@@ -39,6 +39,25 @@
) : StandardAppHelper(instr, launcherName, component) {
/**
+ * Clicks the button to launch the secondary activity, which should split with the main activity
+ * based on the split pair rule.
+ */
+ fun launchSecondaryActivity(wmHelper: WindowManagerStateHelper) {
+ val launchButton =
+ uiDevice.wait(
+ Until.findObject(By.res(getPackage(), "launch_secondary_activity_button")),
+ FIND_TIMEOUT
+ )
+ require(launchButton != null) { "Can't find launch secondary activity button on screen." }
+ launchButton.click()
+ wmHelper
+ .StateSyncBuilder()
+ .withActivityState(SECONDARY_ACTIVITY_COMPONENT, STATE_RESUMED)
+ .withActivityState(MAIN_ACTIVITY_COMPONENT, STATE_RESUMED)
+ .waitForAndVerify()
+ }
+
+ /**
* Clicks the button to launch the placeholder primary activity, which should launch the
* placeholder secondary activity based on the placeholder rule.
*/
@@ -63,6 +82,9 @@
val MAIN_ACTIVITY_COMPONENT =
ActivityOptions.ActivityEmbedding.MainActivity.COMPONENT.toFlickerComponent()
+ val SECONDARY_ACTIVITY_COMPONENT =
+ ActivityOptions.ActivityEmbedding.SecondaryActivity.COMPONENT.toFlickerComponent()
+
val PLACEHOLDER_PRIMARY_COMPONENT =
ActivityOptions.ActivityEmbedding.PlaceholderPrimaryActivity.COMPONENT
.toFlickerComponent()
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt
similarity index 94%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt
index fed953c..67d3195 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt
@@ -31,7 +31,7 @@
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
import java.util.regex.Pattern
-class ImeAppAutoFocusHelper
+class ImeShownOnAppStartHelper
@JvmOverloads
constructor(
instr: Instrumentation,
@@ -121,11 +121,14 @@
}
fun toggleFixPortraitOrientation(wmHelper: WindowManagerStateHelper) {
- val button = uiDevice.wait(Until.findObject(By.res(getPackage(),
- "toggle_fixed_portrait_btn")), FIND_TIMEOUT)
+ val button =
+ uiDevice.wait(
+ Until.findObject(By.res(getPackage(), "toggle_fixed_portrait_btn")),
+ FIND_TIMEOUT
+ )
require(button != null) {
"Button not found, this usually happens when the device " +
- "was left in an unknown state (e.g. Screen turned off)"
+ "was left in an unknown state (e.g. Screen turned off)"
}
button.click()
mInstrumentation.waitForIdleSync()
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
index 18792a8..d9aec10 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
@@ -294,10 +294,10 @@
.StateSyncBuilder()
.add("pipWindowMinimized") {
val pipAppWindow =
- it.wmState.visibleWindows.firstOrNull { window ->
- this.windowMatchesAnyOf(window)
- }
- ?: return@add false
+ it.wmState.visibleWindows.firstOrNull { window ->
+ this.windowMatchesAnyOf(window)
+ }
+ ?: return@add false
val pipRegion = pipAppWindow.frameRegion
return@add windowRect.coversMoreThan(pipRegion)
}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTestCfArm.kt
deleted file mode 100644
index 70443d8..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTestCfArm.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm.flicker.ime
-
-import com.android.server.wm.flicker.FlickerTest
-import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import org.junit.FixMethodOrder
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeAutoOpenWindowToAppTestCfArm(flicker: FlickerTest) :
- CloseImeAutoOpenWindowToAppTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTestCfArm.kt
deleted file mode 100644
index 2ffc757..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTestCfArm.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm.flicker.ime
-
-import com.android.server.wm.flicker.FlickerTest
-import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import org.junit.FixMethodOrder
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class CloseImeAutoOpenWindowToHomeTestCfArm(flicker: FlickerTest) :
- CloseImeAutoOpenWindowToHomeTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeEditorPopupDialogTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt
similarity index 95%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeEditorPopupDialogTest.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt
index c72405c..ca0d571 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeEditorPopupDialogTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt
@@ -37,7 +37,7 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeEditorPopupDialogTest(flicker: FlickerTest) : BaseTest(flicker) {
+class CloseImeOnDismissPopupDialogTest(flicker: FlickerTest) : BaseTest(flicker) {
private val imeTestApp = ImeEditorPopupDialogAppHelper(instrumentation)
/** {@inheritDoc} */
@@ -90,7 +90,7 @@
imeSnapshotLayer.layer?.visibleRegion
}
.toTypedArray()
- val imeVisibleRegion = RegionSubject.assertThat(visibleAreas, this, timestamp)
+ val imeVisibleRegion = RegionSubject(visibleAreas, this, timestamp)
val appVisibleRegion = it.visibleRegion(imeTestApp)
if (imeVisibleRegion.region.isNotEmpty) {
imeVisibleRegion.coversAtMost(appVisibleRegion.region)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt
similarity index 97%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt
index 840575a..4890bba 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt
@@ -41,7 +41,7 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class CloseImeWindowToHomeTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class CloseImeOnGoHomeTest(flicker: FlickerTest) : BaseTest(flicker) {
private val testApp = ImeAppHelper(instrumentation)
/** {@inheritDoc} */
@@ -100,6 +100,7 @@
flicker.assertLayers { this.isVisible(testApp).then().isInvisible(testApp) }
}
+ @Presubmit
@Test
@IwTest(focusArea = "ime")
override fun cujCompleted() {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTestCfArm.kt
similarity index 92%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTestCfArm.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTestCfArm.kt
index 34bd455..96b23bc 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTestCfArm.kt
@@ -26,4 +26,4 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenImeWindowTestCfArm(flicker: FlickerTest) : OpenImeWindowTest(flicker)
+class CloseImeOnGoHomeTestCfArm(flicker: FlickerTest) : CloseImeOnGoHomeTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt
similarity index 92%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt
index 2081424..aa1e772 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt
@@ -22,7 +22,7 @@
import com.android.server.wm.flicker.FlickerBuilder
import com.android.server.wm.flicker.FlickerTest
import com.android.server.wm.flicker.FlickerTestFactory
-import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
+import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
import com.android.server.wm.traces.common.ComponentNameMatcher
import com.android.server.wm.traces.common.service.PlatformConsts
@@ -48,8 +48,8 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class CloseImeAutoOpenWindowToHomeTest(flicker: FlickerTest) : BaseTest(flicker) {
- private val testApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
+open class CloseImeShownOnAppStartOnGoHomeTest(flicker: FlickerTest) : BaseTest(flicker) {
+ private val testApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation)
/** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit = {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTestCfArm.kt
similarity index 90%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTestCfArm.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTestCfArm.kt
index 0e6cb53..ed5d309 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTestCfArm.kt
@@ -26,5 +26,5 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenImeWindowFromFixedOrientationAppTestCfArm(flicker: FlickerTest) :
- OpenImeWindowFromFixedOrientationAppTest(flicker)
+open class CloseImeShownOnAppStartOnGoHomeTestCfArm(flicker: FlickerTest) :
+ CloseImeShownOnAppStartOnGoHomeTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt
similarity index 91%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt
index 522f9e7..dfbfde8 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt
@@ -22,7 +22,7 @@
import com.android.server.wm.flicker.FlickerBuilder
import com.android.server.wm.flicker.FlickerTest
import com.android.server.wm.flicker.FlickerTestFactory
-import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
+import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
import com.android.server.wm.traces.common.ComponentNameMatcher
import com.android.server.wm.traces.common.service.PlatformConsts
@@ -48,8 +48,8 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class CloseImeAutoOpenWindowToAppTest(flicker: FlickerTest) : BaseTest(flicker) {
- private val testApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
+open class CloseImeShownOnAppStartToAppOnPressBackTest(flicker: FlickerTest) : BaseTest(flicker) {
+ private val testApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation)
/** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit = {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTestCfArm.kt
similarity index 88%
copy from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt
copy to tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTestCfArm.kt
index 87d9abd..0a89991 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTestCfArm.kt
@@ -26,4 +26,5 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeWindowToHomeTestCfArm(flicker: FlickerTest) : CloseImeWindowToHomeTest(flicker)
+class CloseImeShownOnAppStartToAppOnPressBackTestCfArm(flicker: FlickerTest) :
+ CloseImeShownOnAppStartToAppOnPressBackTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt
similarity index 97%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt
index 13feeb1..63f7200 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt
@@ -43,7 +43,7 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class CloseImeWindowToAppTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class CloseImeToAppOnPressBackTest(flicker: FlickerTest) : BaseTest(flicker) {
private val testApp = ImeAppHelper(instrumentation)
/** {@inheritDoc} */
@@ -102,6 +102,7 @@
flicker.assertWm { this.isAppWindowOnTop(testApp) }
}
+ @Presubmit
@Test
@IwTest(focusArea = "ime")
override fun cujCompleted() {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTestCfArm.kt
similarity index 90%
copy from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt
copy to tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTestCfArm.kt
index 87d9abd..37e8c6b 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTestCfArm.kt
@@ -26,4 +26,5 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeWindowToHomeTestCfArm(flicker: FlickerTest) : CloseImeWindowToHomeTest(flicker)
+class CloseImeToAppOnPressBackTestCfArm(flicker: FlickerTest) :
+ CloseImeToAppOnPressBackTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowAndCloseTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
similarity index 96%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowAndCloseTest.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
index 7d1f6cb..0b7b165 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowAndCloseTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
@@ -46,7 +46,7 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenImeWindowAndCloseTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class CloseImeToHomeOnFinishActivityTest(flicker: FlickerTest) : BaseTest(flicker) {
private val simpleApp = SimpleAppHelper(instrumentation)
private val testApp = ImeAppHelper(instrumentation)
@@ -79,6 +79,7 @@
super.visibleLayersShownMoreThanOneConsecutiveEntry()
}
+ @Presubmit
@Test
@IwTest(focusArea = "ime")
override fun cujCompleted() {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTestCfArm.kt
similarity index 89%
copy from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt
copy to tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTestCfArm.kt
index 87d9abd..116bc1b 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTestCfArm.kt
@@ -26,4 +26,5 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeWindowToHomeTestCfArm(flicker: FlickerTest) : CloseImeWindowToHomeTest(flicker)
+open class CloseImeToHomeOnFinishActivityTestCfArm(flicker: FlickerTest) :
+ CloseImeToHomeOnFinishActivityTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTestCfArm.kt
deleted file mode 100644
index f40f5e6..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTestCfArm.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm.flicker.ime
-
-import com.android.server.wm.flicker.FlickerTest
-import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import org.junit.FixMethodOrder
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeWindowToAppTestCfArm(flicker: FlickerTest) : CloseImeWindowToAppTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTestCfArm.kt
deleted file mode 100644
index 04cae53..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTestCfArm.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm.flicker.ime
-
-import com.android.server.wm.flicker.FlickerTest
-import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import org.junit.FixMethodOrder
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class LaunchAppShowImeOnStartTestCfArm(flicker: FlickerTest) :
- LaunchAppShowImeOnStartTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowAndCloseTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowAndCloseTestCfArm.kt
deleted file mode 100644
index c40dfae..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowAndCloseTestCfArm.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm.flicker.ime
-
-import com.android.server.wm.flicker.FlickerTest
-import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import org.junit.FixMethodOrder
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenImeWindowAndCloseTestCfArm(flicker: FlickerTest) :
- OpenImeWindowAndCloseTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
index 806ce99..09ef54f 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
@@ -19,11 +19,11 @@
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.BaseTest
import com.android.server.wm.flicker.FlickerBuilder
-import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
import com.android.server.wm.flicker.FlickerTest
import com.android.server.wm.flicker.FlickerTestFactory
-import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
+import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
import com.android.server.wm.flicker.traces.region.RegionSubject
import com.android.server.wm.traces.common.ComponentNameMatcher
import com.android.server.wm.traces.common.service.PlatformConsts
@@ -34,15 +34,15 @@
import org.junit.runners.Parameterized
/**
- * Test IME window shown on the app with fixing portrait orientation.
- * To run this test: `atest FlickerTests:OpenImeWindowToFixedPortraitAppTest`
+ * Test IME window shown on the app with fixing portrait orientation. To run this test: `atest
+ * FlickerTests:OpenImeWindowToFixedPortraitAppTest`
*/
@RequiresDevice
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class OpenImeWindowToFixedPortraitAppTest(flicker: FlickerTest) : BaseTest(flicker) {
- private val testApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
+ private val testApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation)
/** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit = {
@@ -52,9 +52,7 @@
// Enable letterbox when the app calls setRequestedOrientation
device.executeShellCommand("cmd window set-ignore-orientation-request true")
}
- transitions {
- testApp.toggleFixPortraitOrientation(wmHelper)
- }
+ transitions { testApp.toggleFixPortraitOrientation(wmHelper) }
teardown {
testApp.exit()
device.executeShellCommand("cmd window set-ignore-orientation-request false")
@@ -64,17 +62,13 @@
@Postsubmit
@Test
fun imeLayerVisibleStart() {
- flicker.assertLayersStart {
- this.isVisible(ComponentNameMatcher.IME)
- }
+ flicker.assertLayersStart { this.isVisible(ComponentNameMatcher.IME) }
}
@Postsubmit
@Test
fun imeLayerExistsEnd() {
- flicker.assertLayersEnd {
- this.isVisible(ComponentNameMatcher.IME)
- }
+ flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.IME) }
}
@Postsubmit
@@ -86,7 +80,7 @@
}
flicker.assertLayersEnd {
this.visibleRegion(ComponentNameMatcher.IME)
- .coversExactly(imeLayerVisibleRegionBeforeTransition!!.region)
+ .coversExactly(imeLayerVisibleRegionBeforeTransition!!.region)
}
}
@@ -96,7 +90,7 @@
val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation)
flicker.assertLayersEnd {
this.visibleRegion(testApp.or(ComponentNameMatcher.LETTERBOX))
- .coversExactly(displayBounds)
+ .coversExactly(displayBounds)
}
}
@@ -111,14 +105,13 @@
@JvmStatic
fun getParams(): Collection<FlickerTest> {
return FlickerTestFactory.nonRotationTests(
- supportedRotations = listOf(
- PlatformConsts.Rotation.ROTATION_90,
- ),
- supportedNavigationModes = listOf(
- PlatformConsts.NavBar.MODE_3BUTTON,
- PlatformConsts.NavBar.MODE_GESTURAL
- )
- )
+ supportedRotations =
+ listOf(
+ PlatformConsts.Rotation.ROTATION_90,
+ ),
+ supportedNavigationModes =
+ listOf(PlatformConsts.NavBar.MODE_3BUTTON, PlatformConsts.NavBar.MODE_GESTURAL)
+ )
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTestCfArm.kt
deleted file mode 100644
index 8e6d7dc..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTestCfArm.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm.flicker.ime
-
-import com.android.server.wm.flicker.FlickerTest
-import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import org.junit.FixMethodOrder
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenImeWindowToOverViewTestCfArm(flicker: FlickerTest) : OpenImeWindowToOverViewTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt
similarity index 92%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt
index d41843f..89d37db 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt
@@ -23,7 +23,7 @@
import com.android.server.wm.flicker.FlickerBuilder
import com.android.server.wm.flicker.FlickerTest
import com.android.server.wm.flicker.FlickerTestFactory
-import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
+import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
import com.android.server.wm.flicker.helpers.setRotation
import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
@@ -45,8 +45,10 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenImeWindowFromFixedOrientationAppTest(flicker: FlickerTest) : BaseTest(flicker) {
- private val imeTestApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
+open class ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest(flicker: FlickerTest) :
+ BaseTest(flicker) {
+ private val imeTestApp =
+ ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation)
/** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit = {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTestCfArm.kt
similarity index 86%
copy from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt
copy to tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTestCfArm.kt
index 87d9abd..307821f 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTestCfArm.kt
@@ -26,4 +26,5 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeWindowToHomeTestCfArm(flicker: FlickerTest) : CloseImeWindowToHomeTest(flicker)
+class ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTestCfArm(flicker: FlickerTest) :
+ ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt
similarity index 94%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt
index 38791a2..8b25d7a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt
@@ -22,7 +22,7 @@
import com.android.server.wm.flicker.FlickerBuilder
import com.android.server.wm.flicker.FlickerTest
import com.android.server.wm.flicker.FlickerTestFactory
-import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
+import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
import com.android.server.wm.flicker.helpers.reopenAppFromOverview
import com.android.server.wm.flicker.helpers.setRotation
import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
@@ -41,8 +41,9 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class ReOpenImeWindowTest(flicker: FlickerTest) : BaseTest(flicker) {
- private val testApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
+open class ShowImeOnAppStartWhenLaunchingAppFromOverviewTest(flicker: FlickerTest) :
+ BaseTest(flicker) {
+ private val testApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation)
/** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit = {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt
similarity index 94%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt
index 477ddb3..1e908d6 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt
@@ -23,7 +23,7 @@
import com.android.server.wm.flicker.FlickerBuilder
import com.android.server.wm.flicker.FlickerTest
import com.android.server.wm.flicker.FlickerTestFactory
-import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
+import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
import com.android.server.wm.flicker.helpers.setRotation
@@ -47,9 +47,11 @@
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@Presubmit
-open class SwitchImeWindowsFromGestureNavTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest(flicker: FlickerTest) :
+ BaseTest(flicker) {
private val testApp = SimpleAppHelper(instrumentation)
- private val imeTestApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
+ private val imeTestApp =
+ ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation)
@Before
open fun before() {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTestCfArm.kt
similarity index 87%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTestCfArm.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTestCfArm.kt
index 1d3658e..82c38a3 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTestCfArm.kt
@@ -28,5 +28,5 @@
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@Presubmit
-open class SwitchImeWindowsFromGestureNavTestCfArm(flicker: FlickerTest) :
- SwitchImeWindowsFromGestureNavTest(flicker)
+open class ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTestCfArm(flicker: FlickerTest) :
+ ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTestShellTransit.kt
similarity index 93%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTestShellTransit.kt
index e297e89..6c6003f 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTestShellTransit.kt
@@ -38,8 +38,8 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class SwitchImeWindowsFromGestureNavTest_ShellTransit(flicker: FlickerTest) :
- SwitchImeWindowsFromGestureNavTest(flicker) {
+class ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTestShellTransit(flicker: FlickerTest) :
+ ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest(flicker) {
@Before
override fun before() {
Assume.assumeTrue(isShellTransitionsEnabled)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt
similarity index 94%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTest.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt
index 6685cf7..0fced8c 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt
@@ -22,7 +22,7 @@
import com.android.server.wm.flicker.FlickerBuilder
import com.android.server.wm.flicker.FlickerTest
import com.android.server.wm.flicker.FlickerTestFactory
-import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
+import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
import com.android.server.wm.flicker.helpers.ImeStateInitializeHelper
import com.android.server.wm.flicker.helpers.setRotation
import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
@@ -72,8 +72,8 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class LaunchAppShowImeOnStartTest(flicker: FlickerTest) : BaseTest(flicker) {
- private val testApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
+open class ShowImeOnAppStartWhenLaunchingAppTest(flicker: FlickerTest) : BaseTest(flicker) {
+ private val testApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation)
private val initializeApp = ImeStateInitializeHelper(instrumentation)
/** {@inheritDoc} */
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnStartWhenLaunchingAppCfArmTest.kt
similarity index 89%
copy from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt
copy to tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnStartWhenLaunchingAppCfArmTest.kt
index 87d9abd..e7ecb87 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnStartWhenLaunchingAppCfArmTest.kt
@@ -26,4 +26,5 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeWindowToHomeTestCfArm(flicker: FlickerTest) : CloseImeWindowToHomeTest(flicker)
+class ShowImeOnStartWhenLaunchingAppCfArmTest(flicker: FlickerTest) :
+ ShowImeOnAppStartWhenLaunchingAppTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt
similarity index 96%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt
index 5b830e5..851651e 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt
@@ -37,7 +37,7 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenImeWindowTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class ShowImeWhenFocusingOnInputFieldTest(flicker: FlickerTest) : BaseTest(flicker) {
private val testApp = ImeAppHelper(instrumentation)
/** {@inheritDoc} */
@@ -50,6 +50,7 @@
}
}
+ @Presubmit
@Test
@IwTest(focusArea = "ime")
override fun cujCompleted() {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTestCfArm.kt
similarity index 89%
copy from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt
copy to tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTestCfArm.kt
index 87d9abd..0c53155 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTestCfArm.kt
@@ -26,4 +26,5 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeWindowToHomeTestCfArm(flicker: FlickerTest) : CloseImeWindowToHomeTest(flicker)
+class ShowImeWhenFocusingOnInputFieldTestCfArm(flicker: FlickerTest) :
+ ShowImeWhenFocusingOnInputFieldTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeAndDialogThemeAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt
similarity index 93%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeAndDialogThemeAppTest.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt
index cb530da..7ef09c7 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeAndDialogThemeAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt
@@ -25,7 +25,7 @@
import com.android.server.wm.flicker.FlickerBuilder
import com.android.server.wm.flicker.FlickerTest
import com.android.server.wm.flicker.FlickerTestFactory
-import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
+import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
import com.android.server.wm.traces.common.ComponentNameMatcher
import com.android.server.wm.traces.common.service.PlatformConsts
@@ -45,8 +45,8 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class LaunchAppShowImeAndDialogThemeAppTest(flicker: FlickerTest) : BaseTest(flicker) {
- private val testApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
+class ShowImeWhileDismissingThemedPopupDialogTest(flicker: FlickerTest) : BaseTest(flicker) {
+ private val testApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation)
/** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit = {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt
similarity index 96%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt
index d95af9d..fd420454 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt
@@ -22,7 +22,7 @@
import com.android.server.wm.flicker.FlickerBuilder
import com.android.server.wm.flicker.FlickerTest
import com.android.server.wm.flicker.FlickerTestFactory
-import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
+import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
import com.android.server.wm.flicker.navBarLayerIsVisibleAtStartAndEnd
@@ -46,8 +46,9 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenImeWindowToOverViewTest(flicker: FlickerTest) : BaseTest(flicker) {
- private val imeTestApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
+open class ShowImeWhileEnteringOverviewTest(flicker: FlickerTest) : BaseTest(flicker) {
+ private val imeTestApp =
+ ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation)
/** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit = {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTestCfArm.kt
similarity index 90%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTestCfArm.kt
index 87d9abd..9308fbb 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTestCfArm.kt
@@ -26,4 +26,5 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeWindowToHomeTestCfArm(flicker: FlickerTest) : CloseImeWindowToHomeTest(flicker)
+class ShowImeWhileEnteringOverviewTestCfArm(flicker: FlickerTest) :
+ ShowImeWhileEnteringOverviewTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
index d76c94d..dbf5959 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
@@ -111,9 +111,9 @@
val appWindow = it.windowState(testApp.`package`)
val flags = appWindow.windowState?.attributes?.flags ?: 0
appWindow
- .verify("isFullScreen")
+ .check { "isFullScreen" }
.that(flags.and(WindowManager.LayoutParams.FLAG_FULLSCREEN))
- .isGreaterThan(0)
+ .isGreater(0)
}
}
}
@@ -127,13 +127,13 @@
val appWindow = it.windowState(testApp.`package`)
val rotationAnimation = appWindow.windowState?.attributes?.rotationAnimation ?: 0
appWindow
- .verify("isRotationSeamless")
+ .check { "isRotationSeamless" }
.that(
rotationAnimation.and(
WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS
)
)
- .isGreaterThan(0)
+ .isGreater(0)
}
}
}
diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
index cd47f60..5361d73f 100644
--- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
@@ -179,6 +179,13 @@
</intent-filter>
</activity>
<activity
+ android:name=".ActivityEmbeddingSecondaryActivity"
+ android:label="ActivityEmbedding Secondary"
+ android:taskAffinity="com.android.server.wm.flicker.testapp.ActivityEmbedding"
+ android:theme="@style/CutoutShortEdges"
+ android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+ android:exported="false"/>
+ <activity
android:name=".ActivityEmbeddingPlaceholderPrimaryActivity"
android:label="ActivityEmbedding Placeholder Primary"
android:taskAffinity="com.android.server.wm.flicker.testapp.ActivityEmbedding"
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml
index 19c81a8..d78b9a8 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml
@@ -22,6 +22,14 @@
android:background="@android:color/holo_orange_light">
<Button
+ android:id="@+id/launch_secondary_activity_button"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:layout_centerHorizontal="true"
+ android:onClick="launchSecondaryActivity"
+ android:text="Launch Secondary Activity" />
+
+ <Button
android:id="@+id/launch_placeholder_split_button"
android:layout_width="wrap_content"
android:layout_height="48dp"
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java
index 04a590d..6a7a2cc 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java
@@ -25,6 +25,7 @@
import androidx.window.extensions.embedding.ActivityEmbeddingComponent;
import androidx.window.extensions.embedding.EmbeddingRule;
+import androidx.window.extensions.embedding.SplitPairRule;
import androidx.window.extensions.embedding.SplitPlaceholderRule;
import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper;
@@ -40,20 +41,23 @@
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_embedding_main_layout);
+ }
- initializeSplitRules();
+ /** R.id.launch_secondary_activity_button onClick */
+ public void launchSecondaryActivity(View view) {
+ initializeSplitRules(createSplitPairRules());
+ startActivity(new Intent().setComponent(
+ ActivityOptions.ActivityEmbedding.SecondaryActivity.COMPONENT));
}
/** R.id.launch_placeholder_split_button onClick */
public void launchPlaceholderSplit(View view) {
- startActivity(
- new Intent().setComponent(
- ActivityOptions.ActivityEmbedding.PlaceholderPrimaryActivity.COMPONENT
- )
- );
+ initializeSplitRules(createSplitPlaceholderRules());
+ startActivity(new Intent().setComponent(
+ ActivityOptions.ActivityEmbedding.PlaceholderPrimaryActivity.COMPONENT));
}
- private void initializeSplitRules() {
+ private void initializeSplitRules(Set<EmbeddingRule> rules) {
ActivityEmbeddingComponent embeddingComponent =
ActivityEmbeddingAppHelper.getActivityEmbeddingComponent();
if (embeddingComponent == null) {
@@ -62,14 +66,28 @@
finish();
return;
}
-
- embeddingComponent.setEmbeddingRules(getSplitRules());
+ embeddingComponent.setEmbeddingRules(rules);
}
- private Set<EmbeddingRule> getSplitRules() {
+ private Set<EmbeddingRule> createSplitPairRules() {
final Set<EmbeddingRule> rules = new ArraySet<>();
+ final SplitPairRule rule = new SplitPairRule.Builder(
+ activitiesPair -> activitiesPair.first instanceof ActivityEmbeddingMainActivity
+ && activitiesPair.second instanceof ActivityEmbeddingSecondaryActivity,
+ activityIntentPair ->
+ activityIntentPair.first instanceof ActivityEmbeddingMainActivity
+ && activityIntentPair.second.getComponent().equals(ActivityOptions
+ .ActivityEmbedding.SecondaryActivity.COMPONENT),
+ windowMetrics -> true)
+ .setSplitRatio(DEFAULT_SPLIT_RATIO)
+ .build();
+ rules.add(rule);
+ return rules;
+ }
- final SplitPlaceholderRule placeholderRule = new SplitPlaceholderRule.Builder(
+ private Set<EmbeddingRule> createSplitPlaceholderRules() {
+ final Set<EmbeddingRule> rules = new ArraySet<>();
+ final SplitPlaceholderRule rule = new SplitPlaceholderRule.Builder(
new Intent().setComponent(
ActivityOptions.ActivityEmbedding.PlaceholderSecondaryActivity.COMPONENT),
activity -> activity instanceof ActivityEmbeddingPlaceholderPrimaryActivity,
@@ -78,7 +96,7 @@
windowMetrics -> true)
.setSplitRatio(DEFAULT_SPLIT_RATIO)
.build();
- rules.add(placeholderRule);
+ rules.add(rule);
return rules;
}
}
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
new file mode 100644
index 0000000..00f4c25
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.testapp;
+
+import android.graphics.Color;
+
+/**
+ * Activity to be used as the secondary activity to split with
+ * {@link ActivityEmbeddingMainActivity}.
+ */
+public class ActivityEmbeddingSecondaryActivity extends ActivityEmbeddingBaseActivity {
+ @Override
+ int getBackgroundColor() {
+ return Color.YELLOW;
+ }
+}
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
index 00684de..b61bc0c 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
@@ -87,6 +87,12 @@
FLICKER_APP_PACKAGE + ".ActivityEmbeddingMainActivity");
}
+ public static class SecondaryActivity {
+ public static final String LABEL = "ActivityEmbeddingSecondaryActivity";
+ public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
+ FLICKER_APP_PACKAGE + ".ActivityEmbeddingSecondaryActivity");
+ }
+
public static class PlaceholderPrimaryActivity {
public static final String LABEL = "ActivityEmbeddingPlaceholderPrimaryActivity";
public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
diff --git a/tests/HwAccelerationTest/AndroidManifest.xml b/tests/HwAccelerationTest/AndroidManifest.xml
index e1f6acc..80c7a21 100644
--- a/tests/HwAccelerationTest/AndroidManifest.xml
+++ b/tests/HwAccelerationTest/AndroidManifest.xml
@@ -120,6 +120,15 @@
</intent-filter>
</activity>
+ <activity android:name="ColorBitmapActivity"
+ android:label="Bitmaps/BitmapColors"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="com.android.test.hwui.TEST"/>
+ </intent-filter>
+ </activity>
+
<activity android:name="PathOffsetActivity"
android:label="Path/Offset"
android:exported="true">
@@ -1136,6 +1145,15 @@
</intent-filter>
</activity>
+ <activity android:name="HardwareBufferRendererActivity"
+ android:label="HardwareRenderer/HardwareBufferRenderer"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="com.android.test.hwui.TEST"/>
+ </intent-filter>
+ </activity>
+
<activity android:name="MyLittleTextureView"
android:label="HardwareRenderer/MyLittleTextureView"
android:screenOrientation="fullSensor"
diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/ColorBitmapActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/ColorBitmapActivity.java
new file mode 100644
index 0000000..017de60
--- /dev/null
+++ b/tests/HwAccelerationTest/src/com/android/test/hwui/ColorBitmapActivity.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.test.hwui;
+
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorSpace;
+import android.graphics.HardwareBufferRenderer;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.RenderNode;
+import android.graphics.Shader;
+import android.hardware.HardwareBuffer;
+import android.media.Image;
+import android.media.ImageWriter;
+import android.os.Bundle;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+
+import java.time.Duration;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+
+@SuppressWarnings({"UnusedDeclaration"})
+public class ColorBitmapActivity extends Activity implements SurfaceHolder.Callback,
+ AdapterView.OnItemSelectedListener {
+
+ private static final int WIDTH = 512;
+ private static final int HEIGHT = 512;
+
+ private ImageView mImageView;
+ private SurfaceView mSurfaceView;
+ private HardwareBuffer mGradientBuffer;
+ private ImageWriter mImageWriter;
+ private ColorSpace mColorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
+ private String[] mColorNames = {"sRGB", "BT2020_HLG", "BT2020_PQ"};
+ private String mCurrentColorName = "sRGB";
+
+ private FutureTask<HardwareBuffer> authorGradientBuffer(HardwareBuffer buffer) {
+ HardwareBufferRenderer renderer = new HardwareBufferRenderer(buffer);
+ RenderNode node = new RenderNode("content");
+ node.setPosition(0, 0, buffer.getWidth(), buffer.getHeight());
+
+ Canvas canvas = node.beginRecording();
+ LinearGradient gradient = new LinearGradient(
+ 0, 0, buffer.getWidth(), buffer.getHeight(), 0xFF000000,
+ 0xFFFFFFFF, Shader.TileMode.CLAMP);
+ Paint paint = new Paint();
+ paint.setShader(gradient);
+ canvas.drawRect(0f, 0f, buffer.getWidth(), buffer.getHeight(), paint);
+ node.endRecording();
+
+ renderer.setContentRoot(node);
+
+ ColorSpace colorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
+ FutureTask<HardwareBuffer> resolvedBuffer = new FutureTask<>(() -> buffer);
+ renderer.obtainRenderRequest()
+ .setColorSpace(colorSpace)
+ .draw(Executors.newSingleThreadExecutor(), result -> {
+ result.getFence().await(Duration.ofSeconds(3));
+ resolvedBuffer.run();
+ });
+ return resolvedBuffer;
+ }
+
+ private FutureTask<HardwareBuffer> getGradientBuffer() {
+ HardwareBuffer buffer = HardwareBuffer.create(
+ WIDTH, HEIGHT, PixelFormat.RGBA_8888, 1,
+ HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
+ | HardwareBuffer.USAGE_GPU_COLOR_OUTPUT);
+ return authorGradientBuffer(buffer);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ try {
+
+ mColorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
+
+ ArrayAdapter<String> adapter = new ArrayAdapter<>(
+ this, android.R.layout.simple_spinner_item, mColorNames);
+
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ Spinner spinner = new Spinner(this);
+ spinner.setAdapter(adapter);
+ spinner.setOnItemSelectedListener(this);
+
+ mGradientBuffer = getGradientBuffer().get();
+
+ LinearLayout linearLayout = new LinearLayout(this);
+ linearLayout.setOrientation(LinearLayout.VERTICAL);
+
+ mImageView = new ImageView(this);
+
+ mSurfaceView = new SurfaceView(this);
+ mSurfaceView.getHolder().addCallback(this);
+
+ linearLayout.addView(spinner, new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT));
+
+ linearLayout.addView(mImageView, new LinearLayout.LayoutParams(WIDTH, HEIGHT));
+ linearLayout.addView(mSurfaceView, new LinearLayout.LayoutParams(WIDTH, HEIGHT));
+
+ setContentView(linearLayout);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private ColorSpace getFromName(String name) {
+ if (name.equals("sRGB")) {
+ return ColorSpace.get(ColorSpace.Named.SRGB);
+ } else if (name.equals("BT2020_HLG")) {
+ return ColorSpace.get(ColorSpace.Named.BT2020_HLG);
+ } else if (name.equals("BT2020_PQ")) {
+ return ColorSpace.get(ColorSpace.Named.BT2020_PQ);
+ }
+
+ throw new RuntimeException("Unrecognized Colorspace!");
+ }
+
+ private void populateBuffers() {
+ Bitmap bitmap = Bitmap.wrapHardwareBuffer(
+ mGradientBuffer, ColorSpace.get(ColorSpace.Named.SRGB));
+ Bitmap copy = bitmap.copy(Bitmap.Config.ARGB_8888, false);
+ copy.setColorSpace(mColorSpace);
+ mImageView.setImageBitmap(copy);
+
+ try (Image image = mImageWriter.dequeueInputImage()) {
+ authorGradientBuffer(image.getHardwareBuffer()).get();
+ image.setDataSpace(mColorSpace.getDataSpace());
+ mImageWriter.queueInputImage(image);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ mImageWriter = new ImageWriter.Builder(holder.getSurface())
+ .setUsage(HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
+ | HardwareBuffer.USAGE_GPU_COLOR_OUTPUT
+ | HardwareBuffer.USAGE_COMPOSER_OVERLAY)
+ .build();
+ populateBuffers();
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ mImageWriter.close();
+ mImageWriter = null;
+ }
+
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ mCurrentColorName = mColorNames[position];
+ mColorSpace = getFromName(mCurrentColorName);
+ populateBuffers();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+
+ }
+}
diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/HardwareBufferRendererActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/HardwareBufferRendererActivity.java
new file mode 100644
index 0000000..e4de434
--- /dev/null
+++ b/tests/HwAccelerationTest/src/com/android/test/hwui/HardwareBufferRendererActivity.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2019 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.test.hwui;
+
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorSpace;
+import android.graphics.ColorSpace.Named;
+import android.graphics.HardwareBufferRenderer;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.RenderNode;
+import android.hardware.HardwareBuffer;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import java.time.Duration;
+import java.util.concurrent.Executors;
+
+public class HardwareBufferRendererActivity extends Activity {
+
+ private ImageView mImageView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mImageView = new ImageView(this);
+ mImageView.setBackgroundColor(Color.MAGENTA);
+ FrameLayout layout = new FrameLayout(this);
+ layout.setBackgroundColor(Color.CYAN);
+ layout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ layout.addView(mImageView, new FrameLayout.LayoutParams(100, 100));
+ setContentView(layout);
+
+ HardwareBuffer buffer = HardwareBuffer.create(100, 100, PixelFormat.RGBA_8888, 1,
+ HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE | HardwareBuffer.USAGE_GPU_COLOR_OUTPUT);
+ HardwareBufferRenderer renderer = new HardwareBufferRenderer(buffer);
+ RenderNode node = new RenderNode("content");
+ node.setPosition(0, 0, 100, 100);
+
+ Canvas canvas = node.beginRecording();
+ canvas.drawColor(Color.BLUE);
+
+ Paint paint = new Paint();
+ paint.setColor(Color.RED);
+ canvas.drawRect(0f, 0f, 50f, 50f, paint);
+ node.endRecording();
+
+ renderer.setContentRoot(node);
+
+ ColorSpace colorSpace = ColorSpace.get(Named.SRGB);
+ Handler handler = new Handler(Looper.getMainLooper());
+ renderer.obtainRenderRequest()
+ .setColorSpace(colorSpace)
+ .draw(Executors.newSingleThreadExecutor(), result -> {
+ result.getFence().await(Duration.ofMillis(3000));
+ handler.post(() -> {
+ Bitmap bitmap = Bitmap.wrapHardwareBuffer(buffer, colorSpace);
+ Bitmap copy = bitmap.copy(Config.ARGB_8888, false);
+ mImageView.setImageBitmap(copy);
+ });
+ });
+ }
+}
diff --git a/tests/SurfaceViewBufferTests/src/com/android/test/BufferPresentationTests.kt b/tests/SurfaceViewBufferTests/src/com/android/test/BufferPresentationTests.kt
index b67dc380..7a8d949 100644
--- a/tests/SurfaceViewBufferTests/src/com/android/test/BufferPresentationTests.kt
+++ b/tests/SurfaceViewBufferTests/src/com/android/test/BufferPresentationTests.kt
@@ -15,7 +15,7 @@
*/
package com.android.test
-import com.android.server.wm.flicker.traces.layers.LayersTraceSubject.Companion.assertThat
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertTrue
import org.junit.Test
@@ -37,7 +37,7 @@
1000 /* ms */))
}
- assertThat(trace).hasFrameSequence("SurfaceView", 1..numFrames)
+ LayersTraceSubject(trace).hasFrameSequence("SurfaceView", 1..numFrames)
}
@Test
@@ -51,7 +51,7 @@
assertEquals(0, activity.mSurfaceProxy.waitUntilBufferDisplayed(2, 5000 /* ms */))
}
- assertThat(trace).hasFrameSequence("SurfaceView", 1..2L)
+ LayersTraceSubject(trace).hasFrameSequence("SurfaceView", 1..2L)
}
@Test
@@ -69,7 +69,7 @@
5000 /* ms */))
}
- assertThat(trace).hasFrameSequence("SurfaceView", 1..numFrames)
+ LayersTraceSubject(trace).hasFrameSequence("SurfaceView", 1..numFrames)
}
@Test
@@ -92,7 +92,7 @@
5000 /* ms */))
}
- assertThat(trace).hasFrameSequence("SurfaceView", 1..numFrames)
+ LayersTraceSubject(trace).hasFrameSequence("SurfaceView", 1..numFrames)
}
@Test
diff --git a/tests/SurfaceViewBufferTests/src/com/android/test/BufferRejectionTests.kt b/tests/SurfaceViewBufferTests/src/com/android/test/BufferRejectionTests.kt
index e9e0246..da53387 100644
--- a/tests/SurfaceViewBufferTests/src/com/android/test/BufferRejectionTests.kt
+++ b/tests/SurfaceViewBufferTests/src/com/android/test/BufferRejectionTests.kt
@@ -16,7 +16,7 @@
package com.android.test
import android.graphics.Point
-import com.android.server.wm.flicker.traces.layers.LayersTraceSubject.Companion.assertThat
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
import com.android.test.SurfaceViewBufferTestBase.Companion.ScalingMode
import com.android.test.SurfaceViewBufferTestBase.Companion.Transform
import junit.framework.Assert.assertEquals
@@ -45,10 +45,10 @@
activity.mSurfaceProxy.waitUntilBufferDisplayed(3, 500 /* ms */)
}
// Verify we reject buffers since scaling mode == NATIVE_WINDOW_SCALING_MODE_FREEZE
- assertThat(trace).layer("SurfaceView", 2).doesNotExist()
+ LayersTraceSubject(trace).layer("SurfaceView", 2).doesNotExist()
// Verify the next buffer is submitted with the correct size
- assertThat(trace).layer("SurfaceView", 3).also {
+ LayersTraceSubject(trace).layer("SurfaceView", 3).also {
it.hasBufferSize(defaultBufferSize)
// scaling mode is not passed down to the layer for blast
if (useBlastAdapter) {
@@ -81,9 +81,9 @@
}
// verify buffer size is reset to default buffer size
- assertThat(trace).layer("SurfaceView", 1).hasBufferSize(defaultBufferSize)
- assertThat(trace).layer("SurfaceView", 2).doesNotExist()
- assertThat(trace).layer("SurfaceView", 3).hasBufferSize(bufferSize)
+ LayersTraceSubject(trace).layer("SurfaceView", 1).hasBufferSize(defaultBufferSize)
+ LayersTraceSubject(trace).layer("SurfaceView", 2).doesNotExist()
+ LayersTraceSubject(trace).layer("SurfaceView", 3).hasBufferSize(bufferSize)
}
@Test
@@ -109,10 +109,11 @@
}
// verify buffer size is reset to default buffer size
- assertThat(trace).layer("SurfaceView", 1).hasBufferSize(defaultBufferSize)
- assertThat(trace).layer("SurfaceView", 2).doesNotExist()
- assertThat(trace).layer("SurfaceView", 3).hasBufferSize(rotatedBufferSize)
- assertThat(trace).layer("SurfaceView", 3).hasBufferOrientation(Transform.ROT_90.value)
+ LayersTraceSubject(trace).layer("SurfaceView", 1).hasBufferSize(defaultBufferSize)
+ LayersTraceSubject(trace).layer("SurfaceView", 2).doesNotExist()
+ LayersTraceSubject(trace).layer("SurfaceView", 3).hasBufferSize(rotatedBufferSize)
+ LayersTraceSubject(trace).layer("SurfaceView", 3)
+ .hasBufferOrientation(Transform.ROT_90.value)
}
@Test
@@ -141,11 +142,11 @@
}
for (count in 0 until 5) {
- assertThat(trace).layer("SurfaceView", (count * 3) + 1L)
+ LayersTraceSubject(trace).layer("SurfaceView", (count * 3) + 1L)
.hasBufferSize(defaultBufferSize)
- assertThat(trace).layer("SurfaceView", (count * 3) + 2L)
+ LayersTraceSubject(trace).layer("SurfaceView", (count * 3) + 2L)
.doesNotExist()
- assertThat(trace).layer("SurfaceView", (count * 3) + 3L)
+ LayersTraceSubject(trace).layer("SurfaceView", (count * 3) + 3L)
.hasBufferSize(bufferSize)
}
}
diff --git a/tests/SurfaceViewBufferTests/src/com/android/test/GeometryTests.kt b/tests/SurfaceViewBufferTests/src/com/android/test/GeometryTests.kt
index 0802990..2d6c664 100644
--- a/tests/SurfaceViewBufferTests/src/com/android/test/GeometryTests.kt
+++ b/tests/SurfaceViewBufferTests/src/com/android/test/GeometryTests.kt
@@ -19,7 +19,7 @@
import android.graphics.Point
import android.graphics.Rect
import android.os.SystemClock
-import com.android.server.wm.flicker.traces.layers.LayersTraceSubject.Companion.assertThat
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
import com.android.test.SurfaceViewBufferTestBase.Companion.ScalingMode
import com.android.test.SurfaceViewBufferTestBase.Companion.Transform
import junit.framework.Assert.assertEquals
@@ -43,7 +43,7 @@
}
// verify buffer size is reset to default buffer size
- assertThat(trace).layer("SurfaceView", 1).hasBufferSize(defaultBufferSize)
+ LayersTraceSubject(trace).layer("SurfaceView", 1).hasBufferSize(defaultBufferSize)
}
@Test
@@ -56,7 +56,7 @@
activity.mSurfaceProxy.waitUntilBufferDisplayed(1, 500 /* ms */)
}
- assertThat(trace).layer("SurfaceView", 1).also {
+ LayersTraceSubject(trace).layer("SurfaceView", 1).also {
it.hasBufferSize(bufferSize)
it.hasLayerSize(defaultBufferSize)
it.hasScalingMode(ScalingMode.SCALE_TO_WINDOW.ordinal)
@@ -73,7 +73,7 @@
activity.mSurfaceProxy.waitUntilBufferDisplayed(1, 500 /* ms */)
}
- assertThat(trace).layer("SurfaceView", 1).also {
+ LayersTraceSubject(trace).layer("SurfaceView", 1).also {
it.hasBufferSize(bufferSize)
it.hasLayerSize(defaultBufferSize)
it.hasScalingMode(ScalingMode.SCALE_TO_WINDOW.ordinal)
@@ -102,9 +102,9 @@
}
// verify buffer size is reset to default buffer size
- assertThat(trace).layer("SurfaceView", 1).hasBufferSize(defaultBufferSize)
- assertThat(trace).layer("SurfaceView", 2).doesNotExist()
- assertThat(trace).layer("SurfaceView", 3).hasBufferSize(bufferSize)
+ LayersTraceSubject(trace).layer("SurfaceView", 1).hasBufferSize(defaultBufferSize)
+ LayersTraceSubject(trace).layer("SurfaceView", 2).doesNotExist()
+ LayersTraceSubject(trace).layer("SurfaceView", 3).hasBufferSize(bufferSize)
}
@Test
@@ -118,7 +118,7 @@
activity.mSurfaceProxy.waitUntilBufferDisplayed(index + 1L, 500 /* ms */)
}
- assertThat(trace).layer("SurfaceView", index + 1L).also {
+ LayersTraceSubject(trace).layer("SurfaceView", index + 1L).also {
it.hasBufferSize(defaultBufferSize)
it.hasLayerSize(defaultBufferSize)
it.hasBufferOrientation(transform.value)
@@ -145,7 +145,7 @@
}
// check that the layer and buffer starts with the default size
- assertThat(trace).layer("SurfaceView", 1).also {
+ LayersTraceSubject(trace).layer("SurfaceView", 1).also {
it.hasBufferSize(defaultBufferSize)
it.hasLayerSize(defaultBufferSize)
}
@@ -169,7 +169,7 @@
checkPixels(svBounds, Color.BLUE)
}
- assertThat(trace).layer("SurfaceView", 1).also {
+ LayersTraceSubject(trace).layer("SurfaceView", 1).also {
it.hasLayerSize(newSize)
it.hasBufferSize(defaultBufferSize)
}
@@ -193,7 +193,7 @@
}
// check that the layer and buffer starts with the default size
- assertThat(trace).layer("SurfaceView", 1).also {
+ LayersTraceSubject(trace).layer("SurfaceView", 1).also {
it.hasBufferSize(defaultBufferSize)
it.hasLayerSize(defaultBufferSize)
}
@@ -216,7 +216,7 @@
checkPixels(svBounds, Color.BLUE)
}
- assertThat(trace).layer("SurfaceView", 1).also {
+ LayersTraceSubject(trace).layer("SurfaceView", 1).also {
it.hasLayerSize(defaultBufferSize)
it.hasBufferSize(defaultBufferSize)
}
diff --git a/tests/SurfaceViewBufferTests/src/com/android/test/InverseDisplayTransformTests.kt b/tests/SurfaceViewBufferTests/src/com/android/test/InverseDisplayTransformTests.kt
index 69012bd..cf4186d 100644
--- a/tests/SurfaceViewBufferTests/src/com/android/test/InverseDisplayTransformTests.kt
+++ b/tests/SurfaceViewBufferTests/src/com/android/test/InverseDisplayTransformTests.kt
@@ -16,7 +16,7 @@
package com.android.test
import android.graphics.Point
-import com.android.server.wm.flicker.traces.layers.LayersTraceSubject.Companion.assertThat
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
import com.android.test.SurfaceViewBufferTestBase.Companion.Transform
import junit.framework.Assert.assertEquals
import org.junit.Assume.assumeFalse
@@ -69,8 +69,8 @@
}
// verify buffer size is reset to default buffer size
- assertThat(trace).layer("SurfaceView", 1).hasBufferSize(defaultBufferSize)
- assertThat(trace).layer("SurfaceView", 2).doesNotExist()
- assertThat(trace).layer("SurfaceView", 3).hasBufferSize(rotatedBufferSize)
+ LayersTraceSubject(trace).layer("SurfaceView", 1).hasBufferSize(defaultBufferSize)
+ LayersTraceSubject(trace).layer("SurfaceView", 2).doesNotExist()
+ LayersTraceSubject(trace).layer("SurfaceView", 3).hasBufferSize(rotatedBufferSize)
}
}
\ No newline at end of file
diff --git a/tests/SurfaceViewBufferTests/src/com/android/test/SharedBufferModeTests.kt b/tests/SurfaceViewBufferTests/src/com/android/test/SharedBufferModeTests.kt
index ee41d79..61d4095 100644
--- a/tests/SurfaceViewBufferTests/src/com/android/test/SharedBufferModeTests.kt
+++ b/tests/SurfaceViewBufferTests/src/com/android/test/SharedBufferModeTests.kt
@@ -17,7 +17,7 @@
import android.graphics.Color
import android.graphics.Rect
-import com.android.server.wm.flicker.traces.layers.LayersTraceSubject.Companion.assertThat
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
import junit.framework.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@@ -39,7 +39,7 @@
}
}
- assertThat(trace).hasFrameSequence("SurfaceView", 1..numFrames)
+ LayersTraceSubject(trace).hasFrameSequence("SurfaceView", 1..numFrames)
}
/** Submit buffers as fast as possible testing that we are not blocked when dequeuing the buffer
@@ -57,7 +57,7 @@
5000 /* ms */))
}
- assertThat(trace).hasFrameSequence("SurfaceView", numFrames..numFrames)
+ LayersTraceSubject(trace).hasFrameSequence("SurfaceView", numFrames..numFrames)
}
/** Keep overwriting the buffer without queuing buffers and check that we present the latest
diff --git a/tests/TaskOrganizerTest/src/com/android/test/taskembed/ResizeTasksSyncTest.kt b/tests/TaskOrganizerTest/src/com/android/test/taskembed/ResizeTasksSyncTest.kt
index 03b43cc..722e671 100644
--- a/tests/TaskOrganizerTest/src/com/android/test/taskembed/ResizeTasksSyncTest.kt
+++ b/tests/TaskOrganizerTest/src/com/android/test/taskembed/ResizeTasksSyncTest.kt
@@ -23,7 +23,7 @@
import androidx.test.runner.AndroidJUnit4
import com.android.server.wm.flicker.monitor.LayersTraceMonitor
import com.android.server.wm.flicker.monitor.withSFTracing
-import com.android.server.wm.flicker.traces.layers.LayersTraceSubject.Companion.assertThat
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
import org.junit.After
import org.junit.Before
import org.junit.FixMethodOrder
@@ -90,13 +90,13 @@
secondBounds.offsetTo(0, 0)
// verify buffer size should be changed to expected values.
- assertThat(trace).layer(FIRST_ACTIVITY, frame.toLong()).also {
+ LayersTraceSubject(trace).layer(FIRST_ACTIVITY, frame.toLong()).also {
val firstTaskSize = Point(firstBounds.width(), firstBounds.height())
it.hasLayerSize(firstTaskSize)
it.hasBufferSize(firstTaskSize)
}
- assertThat(trace).layer(SECOND_ACTIVITY, frame.toLong()).also {
+ LayersTraceSubject(trace).layer(SECOND_ACTIVITY, frame.toLong()).also {
val secondTaskSize = Point(secondBounds.width(), secondBounds.height())
it.hasLayerSize(secondTaskSize)
it.hasBufferSize(secondTaskSize)
diff --git a/wifi/java/Android.bp b/wifi/java/Android.bp
index 225e750..434226d 100644
--- a/wifi/java/Android.bp
+++ b/wifi/java/Android.bp
@@ -27,7 +27,10 @@
filegroup {
name: "framework-wifi-non-updatable-sources-internal",
- srcs: ["src/**/*.java"],
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.aidl",
+ ],
path: "src",
visibility: ["//visibility:private"],
}
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/OWNERS b/wifi/java/src/android/net/wifi/sharedconnectivity/OWNERS
new file mode 100644
index 0000000..2a4acc1
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 1216021
+
+asapperstein@google.com
+etancohen@google.com
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt b/wifi/java/src/android/net/wifi/sharedconnectivity/app/DeviceInfo.aidl
similarity index 64%
copy from packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
copy to wifi/java/src/android/net/wifi/sharedconnectivity/app/DeviceInfo.aidl
index 497c272..35d5c15 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/DeviceInfo.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,12 +14,6 @@
* limitations under the License.
*/
-package com.android.credentialmanager.jetpack.developer
+package android.net.wifi.sharedconnectivity.app;
-/**
- * Internal exception used to indicate a parsing error while converting from a framework type to
- * a jetpack type.
- *
- * @hide
- */
-internal class FrameworkClassParsingException : Exception()
\ No newline at end of file
+parcelable DeviceInfo;
\ No newline at end of file
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/DeviceInfo.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/DeviceInfo.java
new file mode 100644
index 0000000..7874b2a
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/DeviceInfo.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.net.wifi.sharedconnectivity.service.SharedConnectivityService;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * A data class representing a device providing connectivity.
+ * This class is used in IPC calls between the implementer of {@link SharedConnectivityService} and
+ * the consumers of {@link com.android.wifitrackerlib}.
+ *
+ * @hide
+ */
+@SystemApi
+public final class DeviceInfo implements Parcelable {
+
+ /**
+ * Device type providing connectivity is unknown.
+ */
+ public static final int DEVICE_TYPE_UNKNOWN = 0;
+
+ /**
+ * Device providing connectivity is a mobile phone.
+ */
+ public static final int DEVICE_TYPE_PHONE = 1;
+
+ /**
+ * Device providing connectivity is a tablet.
+ */
+ public static final int DEVICE_TYPE_TABLET = 2;
+
+ /**
+ * Device providing connectivity is a laptop.
+ */
+ public static final int DEVICE_TYPE_LAPTOP = 3;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ DEVICE_TYPE_UNKNOWN,
+ DEVICE_TYPE_PHONE,
+ DEVICE_TYPE_TABLET,
+ DEVICE_TYPE_LAPTOP
+ })
+ public @interface DeviceType {}
+
+ @DeviceType private final int mDeviceType;
+ private final String mDeviceName;
+ private final String mModelName;
+ private final int mBatteryPercentage;
+ private final int mConnectionStrength;
+
+ /**
+ * Builder class for {@link DeviceInfo}.
+ */
+ public static final class Builder {
+ private int mDeviceType;
+ private String mDeviceName;
+ private String mModelName;
+ private int mBatteryPercentage;
+ private int mConnectionStrength;
+
+ public Builder() {}
+
+ /**
+ * Sets the device type that provides connectivity.
+ *
+ * @param deviceType Device type as represented by IntDef {@link DeviceType}.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setDeviceType(@DeviceType int deviceType) {
+ mDeviceType = deviceType;
+ return this;
+ }
+
+ /**
+ * Sets the device name of the remote device.
+ *
+ * @param deviceName The user configurable device name.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setDeviceName(@NonNull String deviceName) {
+ mDeviceName = deviceName;
+ return this;
+ }
+
+ /**
+ * Sets the model name of the remote device.
+ *
+ * @param modelName The OEM configured name for the device model.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setModelName(@NonNull String modelName) {
+ mModelName = modelName;
+ return this;
+ }
+
+ /**
+ * Sets the battery charge percentage of the remote device.
+ *
+ * @param batteryPercentage The battery charge percentage in the range 0 to 100.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setBatteryPercentage(@IntRange(from = 0, to = 100) int batteryPercentage) {
+ mBatteryPercentage = batteryPercentage;
+ return this;
+ }
+
+ /**
+ * Sets the displayed connection strength of the remote device to the internet.
+ *
+ * @param connectionStrength Connection strength in range 0 to 3.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setConnectionStrength(@IntRange(from = 0, to = 3) int connectionStrength) {
+ mConnectionStrength = connectionStrength;
+ return this;
+ }
+
+ /**
+ * Builds the {@link DeviceInfo} object.
+ *
+ * @return Returns the built {@link DeviceInfo} object.
+ */
+ @NonNull
+ public DeviceInfo build() {
+ return new DeviceInfo(mDeviceType, mDeviceName, mModelName, mBatteryPercentage,
+ mConnectionStrength);
+ }
+ }
+
+ private static void validate(int deviceType, String deviceName, String modelName,
+ int batteryPercentage, int connectionStrength) {
+ if (deviceType != DEVICE_TYPE_UNKNOWN && deviceType != DEVICE_TYPE_PHONE
+ && deviceType != DEVICE_TYPE_TABLET && deviceType != DEVICE_TYPE_LAPTOP) {
+ throw new IllegalArgumentException("Illegal device type");
+ }
+ if (Objects.isNull(deviceName)) {
+ throw new IllegalArgumentException("DeviceName must be set");
+ }
+ if (Objects.isNull(modelName)) {
+ throw new IllegalArgumentException("ModelName must be set");
+ }
+ if (batteryPercentage < 0 || batteryPercentage > 100) {
+ throw new IllegalArgumentException("BatteryPercentage must be in range 0-100");
+ }
+ if (connectionStrength < 0 || connectionStrength > 3) {
+ throw new IllegalArgumentException("ConnectionStrength must be in range 0-3");
+ }
+ }
+
+ private DeviceInfo(@DeviceType int deviceType, @NonNull String deviceName,
+ @NonNull String modelName, int batteryPercentage, int connectionStrength) {
+ validate(deviceType, deviceName, modelName, batteryPercentage, connectionStrength);
+ mDeviceType = deviceType;
+ mDeviceName = deviceName;
+ mModelName = modelName;
+ mBatteryPercentage = batteryPercentage;
+ mConnectionStrength = connectionStrength;
+ }
+
+ /**
+ * Gets the device type that provides connectivity.
+ *
+ * @return Returns the device type as represented by IntDef {@link DeviceType}.
+ */
+ @DeviceType
+ public int getDeviceType() {
+ return mDeviceType;
+ }
+
+ /**
+ * Gets the device name of the remote device.
+ *
+ * @return Returns the user configurable device name.
+ */
+ @NonNull
+ public String getDeviceName() {
+ return mDeviceName;
+ }
+
+ /**
+ * Gets the model name of the remote device.
+ *
+ * @return Returns the OEM configured name for the device model.
+ */
+ @NonNull
+ public String getModelName() {
+ return mModelName;
+ }
+
+ /**
+ * Gets the battery charge percentage of the remote device.
+ *
+ * @return Returns the battery charge percentage in the range 0 to 100.
+ */
+ @NonNull
+ @IntRange(from = 0, to = 100)
+ public int getBatteryPercentage() {
+ return mBatteryPercentage;
+ }
+
+ /**
+ * Gets the displayed connection strength of the remote device to the internet.
+ *
+ * @return Returns the connection strength in range 0 to 3.
+ */
+ @NonNull
+ @IntRange(from = 0, to = 3)
+ public int getConnectionStrength() {
+ return mConnectionStrength;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof DeviceInfo)) return false;
+ DeviceInfo other = (DeviceInfo) obj;
+ return mDeviceType == other.getDeviceType()
+ && Objects.equals(mDeviceName, other.mDeviceName)
+ && Objects.equals(mModelName, other.mModelName)
+ && mBatteryPercentage == other.mBatteryPercentage
+ && mConnectionStrength == other.mConnectionStrength;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDeviceType, mDeviceName, mModelName, mBatteryPercentage,
+ mConnectionStrength);
+ }
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mDeviceType);
+ dest.writeString(mDeviceName);
+ dest.writeString(mModelName);
+ dest.writeInt(mBatteryPercentage);
+ dest.writeInt(mConnectionStrength);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Creator<DeviceInfo> CREATOR = new Creator<DeviceInfo>() {
+ @Override
+ public DeviceInfo createFromParcel(Parcel in) {
+ return new DeviceInfo(in.readInt(), in.readString(), in.readString(), in.readInt(),
+ in.readInt());
+ }
+
+ @Override
+ public DeviceInfo[] newArray(int size) {
+ return new DeviceInfo[size];
+ }
+ };
+
+ @Override
+ public String toString() {
+ return new StringBuilder("DeviceInfo[")
+ .append("deviceType=").append(mDeviceType)
+ .append(", deviceName=").append(mDeviceName)
+ .append(", modelName=").append(mModelName)
+ .append(", batteryPercentage=").append(mBatteryPercentage)
+ .append(", connectionStrength=").append(mConnectionStrength)
+ .append("]").toString();
+ }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt b/wifi/java/src/android/net/wifi/sharedconnectivity/app/KnownNetwork.aidl
similarity index 64%
copy from packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
copy to wifi/java/src/android/net/wifi/sharedconnectivity/app/KnownNetwork.aidl
index 497c272..140d72a 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/KnownNetwork.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,12 +14,6 @@
* limitations under the License.
*/
-package com.android.credentialmanager.jetpack.developer
+package android.net.wifi.sharedconnectivity.app;
-/**
- * Internal exception used to indicate a parsing error while converting from a framework type to
- * a jetpack type.
- *
- * @hide
- */
-internal class FrameworkClassParsingException : Exception()
\ No newline at end of file
+parcelable KnownNetwork;
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/KnownNetwork.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/KnownNetwork.java
new file mode 100644
index 0000000..34b7e94
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/KnownNetwork.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import static android.net.wifi.WifiAnnotations.SecurityType;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A data class representing a known Wifi network.
+ *
+ * @hide
+ */
+@SystemApi
+public final class KnownNetwork implements Parcelable {
+ /**
+ * Network is known by a nearby device with the same user account.
+ */
+ public static final int NETWORK_SOURCE_NEARBY_SELF = 0;
+
+ /**
+ * Network is known via cloud storage associated with this device's user account.
+ */
+ public static final int NETWORK_SOURCE_CLOUD_SELF = 1;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ NETWORK_SOURCE_NEARBY_SELF,
+ NETWORK_SOURCE_CLOUD_SELF
+ })
+ public @interface NetworkSource {}
+
+ @NetworkSource private final int mNetworkSource;
+ private final String mSsid;
+ @SecurityType private final int[] mSecurityTypes;
+ private final DeviceInfo mDeviceInfo;
+
+ /**
+ * Builder class for {@link KnownNetwork}.
+ */
+ public static final class Builder {
+ @NetworkSource private int mNetworkSource = -1;
+ private String mSsid;
+ @SecurityType private int[] mSecurityTypes;
+ private android.net.wifi.sharedconnectivity.app.DeviceInfo mDeviceInfo;
+
+ public Builder() {}
+
+ /**
+ * Sets the indicated source of the known network.
+ *
+ * @param networkSource The network source as defined by IntDef {@link NetworkSource}.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setNetworkSource(@NetworkSource int networkSource) {
+ mNetworkSource = networkSource;
+ return this;
+ }
+
+ /**
+ * Sets the SSID of the known network.
+ *
+ * @param ssid The SSID of the known network. Surrounded by double quotes if UTF-8.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setSsid(@NonNull String ssid) {
+ mSsid = ssid;
+ return this;
+ }
+
+ /**
+ * Sets the security types of the known network.
+ *
+ * @param securityTypes The array of security types supported by the known network.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setSecurityTypes(@NonNull @SecurityType int[] securityTypes) {
+ mSecurityTypes = securityTypes;
+ return this;
+ }
+
+ /**
+ * Sets the device information of the device providing connectivity.
+ *
+ * @param deviceInfo The array of security types supported by the known network.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setDeviceInfo(@NonNull DeviceInfo deviceInfo) {
+ mDeviceInfo = deviceInfo;
+ return this;
+ }
+
+ /**
+ * Builds the {@link KnownNetwork} object.
+ *
+ * @return Returns the built {@link KnownNetwork} object.
+ */
+ @NonNull
+ public KnownNetwork build() {
+ return new KnownNetwork(
+ mNetworkSource,
+ mSsid,
+ mSecurityTypes,
+ mDeviceInfo);
+ }
+ }
+
+ private static void validate(int networkSource, String ssid, int [] securityTypes) {
+ if (networkSource != NETWORK_SOURCE_CLOUD_SELF && networkSource
+ != NETWORK_SOURCE_NEARBY_SELF) {
+ throw new IllegalArgumentException("Illegal network source");
+ }
+ if (TextUtils.isEmpty(ssid)) {
+ throw new IllegalArgumentException("SSID must be set");
+ }
+ if (securityTypes == null || securityTypes.length == 0) {
+ throw new IllegalArgumentException("SecurityTypes must be set");
+ }
+ }
+
+ private KnownNetwork(
+ @NetworkSource int networkSource,
+ @NonNull String ssid,
+ @NonNull @SecurityType int[] securityTypes,
+ @NonNull android.net.wifi.sharedconnectivity.app.DeviceInfo deviceInfo) {
+ validate(networkSource, ssid, securityTypes);
+ mNetworkSource = networkSource;
+ mSsid = ssid;
+ mSecurityTypes = securityTypes;
+ mDeviceInfo = deviceInfo;
+ }
+
+ /**
+ * Gets the indicated source of the known network.
+ *
+ * @return Returns the network source as defined by IntDef {@link NetworkSource}.
+ */
+ @NonNull
+ @NetworkSource
+ public int getNetworkSource() {
+ return mNetworkSource;
+ }
+
+ /**
+ * Gets the SSID of the known network.
+ *
+ * @return Returns the SSID of the known network. Surrounded by double quotes if UTF-8.
+ */
+ @NonNull
+ public String getSsid() {
+ return mSsid;
+ }
+
+ /**
+ * Gets the security types of the known network.
+ *
+ * @return Returns the array of security types supported by the known network.
+ */
+ @NonNull
+ @SecurityType
+ public int[] getSecurityTypes() {
+ return mSecurityTypes;
+ }
+
+ /**
+ * Gets the device information of the device providing connectivity.
+ *
+ * @return Returns the array of security types supported by the known network.
+ */
+ @NonNull
+ public DeviceInfo getDeviceInfo() {
+ return mDeviceInfo;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof KnownNetwork)) return false;
+ KnownNetwork other = (KnownNetwork) obj;
+ return mNetworkSource == other.getNetworkSource()
+ && Objects.equals(mSsid, other.getSsid())
+ && Arrays.equals(mSecurityTypes, other.getSecurityTypes())
+ && Objects.equals(mDeviceInfo, other.getDeviceInfo());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mNetworkSource, mSsid, Arrays.hashCode(mSecurityTypes),
+ mDeviceInfo.hashCode());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mNetworkSource);
+ dest.writeString(mSsid);
+ dest.writeIntArray(mSecurityTypes);
+ dest.writeTypedObject(mDeviceInfo, 0);
+ }
+
+ @NonNull
+ public static final Creator<KnownNetwork> CREATOR = new Creator<>() {
+ @Override
+ public KnownNetwork createFromParcel(Parcel in) {
+ return new KnownNetwork(in.readInt(), in.readString(), in.createIntArray(),
+ in.readTypedObject(DeviceInfo.CREATOR));
+ }
+
+ @Override
+ public KnownNetwork[] newArray(int size) {
+ return new KnownNetwork[size];
+ }
+ };
+
+ @Override
+ public String toString() {
+ return new StringBuilder("KnownNetwork[")
+ .append("NetworkSource=").append(mNetworkSource)
+ .append(", ssid=").append(mSsid)
+ .append(", securityTypes=").append(Arrays.toString(mSecurityTypes))
+ .append(", deviceInfo=").append(mDeviceInfo.toString())
+ .append("]").toString();
+ }
+}
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityClientCallback.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityClientCallback.java
new file mode 100644
index 0000000..dcb5201
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityClientCallback.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.net.wifi.sharedconnectivity.service.SharedConnectivityService;
+
+import java.util.List;
+
+/**
+ * Interface for clients of {@link SharedConnectivityManager} to register for changes in network
+ * status.
+ *
+ * @hide
+ */
+@SystemApi
+public interface SharedConnectivityClientCallback {
+ /**
+ * This method is being called by {@link SharedConnectivityService} to notify of a change in the
+ * list of available Tether Networks.
+ * @param networks Updated Tether Network list.
+ */
+ void onTetherNetworksUpdated(@NonNull List<TetherNetwork> networks);
+
+ /**
+ * This method is being called by {@link SharedConnectivityService} to notify of a change in the
+ * list of available Known Networks.
+ * @param networks Updated Known Network list.
+ */
+ void onKnownNetworksUpdated(@NonNull List<KnownNetwork> networks);
+
+ /**
+ * This method is being called by {@link SharedConnectivityService} to notify of a change in the
+ * state of share connectivity settings.
+ * @param state The new state.
+ */
+ void onSharedConnectivitySettingsChanged(@NonNull SharedConnectivitySettingsState state);
+}
+
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java
new file mode 100644
index 0000000..b43e4f7
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.res.Resources;
+import android.net.wifi.sharedconnectivity.service.ISharedConnectivityCallback;
+import android.net.wifi.sharedconnectivity.service.ISharedConnectivityService;
+import android.net.wifi.sharedconnectivity.service.SharedConnectivityService;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * This class is the library used by consumers of Shared Connectivity data to bind to the service,
+ * receive callbacks from, and send user actions to the service.
+ *
+ * @hide
+ */
+@SystemApi
+public class SharedConnectivityManager {
+ private static final String TAG = SharedConnectivityManager.class.getSimpleName();
+ private static final boolean DEBUG = true;
+ private static final String SERVICE_PACKAGE_NAME = "sharedconnectivity_service_package";
+ private static final String SERVICE_CLASS_NAME = "sharedconnectivity_service_class";
+
+ private static final class SharedConnectivityCallbackProxy extends
+ ISharedConnectivityCallback.Stub {
+ private final Executor mExecutor;
+ private final SharedConnectivityClientCallback mCallback;
+
+ SharedConnectivityCallbackProxy(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull SharedConnectivityClientCallback callback) {
+ mExecutor = executor;
+ mCallback = callback;
+ }
+
+ @Override
+ public void onTetherNetworksUpdated(@NonNull List<TetherNetwork> networks) {
+ if (mCallback != null) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onTetherNetworksUpdated(networks));
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
+
+ @Override
+ public void onKnownNetworksUpdated(@NonNull List<KnownNetwork> networks) {
+ if (mCallback != null) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onKnownNetworksUpdated(networks));
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
+
+ @Override
+ public void onSharedConnectivitySettingsChanged(
+ @NonNull SharedConnectivitySettingsState state) {
+ if (mCallback != null) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onSharedConnectivitySettingsChanged(state));
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ };
+ }
+
+ private ISharedConnectivityService mService;
+ private final Map<SharedConnectivityClientCallback, SharedConnectivityCallbackProxy>
+ mProxyMap = new HashMap<>();
+
+ /**
+ * Constructor for new instance of {@link SharedConnectivityManager}.
+ *
+ * Automatically binds to implementation of {@link SharedConnectivityService} specified in
+ * device overlay.
+ */
+ @SuppressLint("ManagerConstructor")
+ public SharedConnectivityManager(@NonNull Context context) {
+ ServiceConnection serviceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mService = ISharedConnectivityService.Stub.asInterface(service);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (DEBUG) Log.i(TAG, "onServiceDisconnected");
+ mService = null;
+ mProxyMap.clear();
+ }
+ };
+ bind(context, serviceConnection);
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ public void setService(@Nullable IInterface service) {
+ mService = (ISharedConnectivityService) service;
+ }
+
+ private void bind(Context context, ServiceConnection serviceConnection) {
+ Resources resources = context.getResources();
+ int packageNameId = resources.getIdentifier(SERVICE_PACKAGE_NAME, "string",
+ context.getPackageName());
+ int classNameId = resources.getIdentifier(SERVICE_CLASS_NAME, "string",
+ context.getPackageName());
+ if (packageNameId == 0 || classNameId == 0) {
+ throw new Resources.NotFoundException("Package and class names for"
+ + " shared connectivity service must be defined");
+ }
+
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName(resources.getString(packageNameId),
+ resources.getString(classNameId)));
+ context.bindService(
+ intent,
+ serviceConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ /**
+ * Registers a callback for receiving updates to the list of Tether Networks and Known Networks.
+ *
+ * @param executor The Executor used to invoke the callback.
+ * @param callback The callback of type {@link SharedConnectivityClientCallback} that is invoked
+ * when the service updates either the list of Tether Networks or Known
+ * Networks.
+ * @return Returns true if the registration was successful, false otherwise.
+ */
+ public boolean registerCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull SharedConnectivityClientCallback callback) {
+ Objects.requireNonNull(executor, "executor cannot be null");
+ Objects.requireNonNull(callback, "callback cannot be null");
+ if (mService == null || mProxyMap.containsKey(callback)) return false;
+ try {
+ SharedConnectivityCallbackProxy proxy =
+ new SharedConnectivityCallbackProxy(executor, callback);
+ mService.registerCallback(proxy);
+ mProxyMap.put(callback, proxy);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception in registerCallback", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Unregisters a callback.
+ *
+ * @return Returns true if the callback was successfully unregistered, false otherwise.
+ */
+ public boolean unregisterCallback(
+ @NonNull SharedConnectivityClientCallback callback) {
+ Objects.requireNonNull(callback, "callback cannot be null");
+ if (mService == null || !mProxyMap.containsKey(callback)) return false;
+ try {
+ mService.unregisterCallback(mProxyMap.get(callback));
+ mProxyMap.remove(callback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception in unregisterCallback", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Send command to the implementation of {@link SharedConnectivityService} requesting connection
+ * to the specified Tether Network.
+ *
+ * @param network {@link TetherNetwork} object representing the network the user has requested
+ * a connection to.
+ * @return Returns true if the service received the command. Does not guarantee that the
+ * connection was successful.
+ */
+ public boolean connectTetherNetwork(@NonNull TetherNetwork network) {
+ if (mService == null) return false;
+ try {
+ mService.connectTetherNetwork(network);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception in connectTetherNetwork", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Send command to the implementation of {@link SharedConnectivityService} requesting
+ * disconnection from the active Tether Network.
+ *
+ * @return Returns true if the service received the command. Does not guarantee that the
+ * disconnection was successful.
+ */
+ public boolean disconnectTetherNetwork() {
+ if (mService == null) return false;
+ try {
+ mService.disconnectTetherNetwork();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception in disconnectTetherNetwork", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Send command to the implementation of {@link SharedConnectivityService} requesting connection
+ * to the specified Known Network.
+ *
+ * @param network {@link KnownNetwork} object representing the network the user has requested
+ * a connection to.
+ * @return Returns true if the service received the command. Does not guarantee that the
+ * connection was successful.
+ */
+ public boolean connectKnownNetwork(@NonNull KnownNetwork network) {
+ if (mService == null) return false;
+ try {
+ mService.connectKnownNetwork(network);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception in connectKnownNetwork", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Send command to the implementation of {@link SharedConnectivityService} requesting removal of
+ * the specified Known Network from the list of Known Networks.
+ *
+ * @return Returns true if the service received the command. Does not guarantee that the
+ * forget action was successful.
+ */
+ public boolean forgetKnownNetwork(@NonNull KnownNetwork network) {
+ if (mService == null) return false;
+ try {
+ mService.forgetKnownNetwork(network);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception in forgetKnownNetwork", e);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsState.aidl
similarity index 64%
rename from packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
rename to wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsState.aidl
index 497c272..289afac 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsState.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,12 +14,6 @@
* limitations under the License.
*/
-package com.android.credentialmanager.jetpack.developer
+package android.net.wifi.sharedconnectivity.app;
-/**
- * Internal exception used to indicate a parsing error while converting from a framework type to
- * a jetpack type.
- *
- * @hide
- */
-internal class FrameworkClassParsingException : Exception()
\ No newline at end of file
+parcelable SharedConnectivitySettingsState;
\ No newline at end of file
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsState.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsState.java
new file mode 100644
index 0000000..dd2fa94
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsState.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+
+/**
+ * A data class representing the shared connectivity settings state.
+ *
+ * This class represents a snapshot of the settings and can be out of date if the settings changed
+ * after receiving an object of this class.
+ *
+ * @hide
+ */
+@SystemApi
+public final class SharedConnectivitySettingsState implements Parcelable {
+
+ private final boolean mInstantTetherEnabled;
+ private final Bundle mExtras;
+
+ /**
+ * Builder class for {@link SharedConnectivitySettingsState}.
+ */
+ public static final class Builder {
+ private boolean mInstantTetherEnabled;
+ private Bundle mExtras;
+
+ public Builder() {}
+
+ /**
+ * Sets the state of Instant Tether in settings
+ *
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setInstantTetherEnabled(boolean instantTetherEnabled) {
+ mInstantTetherEnabled = instantTetherEnabled;
+ return this;
+ }
+
+ /**
+ * Sets the extras bundle
+ *
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setExtras(@NonNull Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Builds the {@link SharedConnectivitySettingsState} object.
+ *
+ * @return Returns the built {@link SharedConnectivitySettingsState} object.
+ */
+ @NonNull
+ public SharedConnectivitySettingsState build() {
+ return new SharedConnectivitySettingsState(mInstantTetherEnabled, mExtras);
+ }
+ }
+
+ private SharedConnectivitySettingsState(boolean instantTetherEnabled, Bundle extras) {
+ mInstantTetherEnabled = instantTetherEnabled;
+ mExtras = extras;
+ }
+
+ /**
+ * Gets the state of Instant Tether in settings
+ *
+ * @return Returns true for enabled, false otherwise.
+ */
+ @NonNull
+ public boolean isInstantTetherEnabled() {
+ return mInstantTetherEnabled;
+ }
+
+ /**
+ * Gets the extras Bundle.
+ *
+ * @return Returns a Bundle object.
+ */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof SharedConnectivitySettingsState)) return false;
+ SharedConnectivitySettingsState other = (SharedConnectivitySettingsState) obj;
+ return mInstantTetherEnabled == other.isInstantTetherEnabled();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mInstantTetherEnabled);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeBoolean(mInstantTetherEnabled);
+ dest.writeBundle(mExtras);
+ }
+
+ @NonNull
+ public static final Creator<SharedConnectivitySettingsState> CREATOR =
+ new Creator<SharedConnectivitySettingsState>() {
+ @Override
+ public SharedConnectivitySettingsState createFromParcel(Parcel in) {
+ return new SharedConnectivitySettingsState(in.readBoolean(),
+ in.readBundle(getClass().getClassLoader()));
+ }
+
+ @Override
+ public SharedConnectivitySettingsState[] newArray(int size) {
+ return new SharedConnectivitySettingsState[size];
+ }
+ };
+
+ @Override
+ public String toString() {
+ return new StringBuilder("SharedConnectivitySettingsState[")
+ .append("instantTetherEnabled=").append(mInstantTetherEnabled)
+ .append("extras=").append(mExtras.toString())
+ .append("]").toString();
+ }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt b/wifi/java/src/android/net/wifi/sharedconnectivity/app/TetherNetwork.aidl
similarity index 64%
copy from packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
copy to wifi/java/src/android/net/wifi/sharedconnectivity/app/TetherNetwork.aidl
index 497c272..6cc4cfe 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/TetherNetwork.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,12 +14,6 @@
* limitations under the License.
*/
-package com.android.credentialmanager.jetpack.developer
+package android.net.wifi.sharedconnectivity.app;
-/**
- * Internal exception used to indicate a parsing error while converting from a framework type to
- * a jetpack type.
- *
- * @hide
- */
-internal class FrameworkClassParsingException : Exception()
\ No newline at end of file
+parcelable TetherNetwork;
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/TetherNetwork.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/TetherNetwork.java
new file mode 100644
index 0000000..bbdad53
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/TetherNetwork.java
@@ -0,0 +1,369 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import static android.net.wifi.WifiAnnotations.SecurityType;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.net.wifi.sharedconnectivity.service.SharedConnectivityService;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A data class representing an Instant Tether network.
+ * This class is used in IPC calls between the implementer of {@link SharedConnectivityService} and
+ * the consumers of {@link com.android.wifitrackerlib}.
+ *
+ * @hide
+ */
+@SystemApi
+public final class TetherNetwork implements Parcelable {
+ /**
+ * Remote device is connected to the internet via an unknown connection.
+ */
+ public static final int NETWORK_TYPE_UNKNOWN = 0;
+
+ /**
+ * Remote device is connected to the internet via a cellular connection.
+ */
+ public static final int NETWORK_TYPE_CELLULAR = 1;
+
+ /**
+ * Remote device is connected to the internet via a Wi-Fi connection.
+ */
+ public static final int NETWORK_TYPE_WIFI = 2;
+
+ /**
+ * Remote device is connected to the internet via an ethernet connection.
+ */
+ public static final int NETWORK_TYPE_ETHERNET = 3;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ NETWORK_TYPE_UNKNOWN,
+ NETWORK_TYPE_CELLULAR,
+ NETWORK_TYPE_WIFI,
+ NETWORK_TYPE_ETHERNET
+ })
+ public @interface NetworkType {}
+
+ private final long mDeviceId;
+ private final DeviceInfo mDeviceInfo;
+ @NetworkType private final int mNetworkType;
+ private final String mNetworkName;
+ @Nullable private final String mHotspotSsid;
+ @Nullable private final String mHotspotBssid;
+ @Nullable @SecurityType private final int[] mHotspotSecurityTypes;
+
+ /**
+ * Builder class for {@link TetherNetwork}.
+ */
+ public static final class Builder {
+ private long mDeviceId = -1;
+ private DeviceInfo mDeviceInfo;
+ @NetworkType private int mNetworkType;
+ private String mNetworkName;
+ @Nullable private String mHotspotSsid;
+ @Nullable private String mHotspotBssid;
+ @Nullable @SecurityType private int[] mHotspotSecurityTypes;
+
+ public Builder() {}
+
+ /**
+ * Set the remote device ID.
+ *
+ * @param deviceId Locally unique ID for this Instant Tether network.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setDeviceId(long deviceId) {
+ mDeviceId = deviceId;
+ return this;
+ }
+
+ /**
+ * Sets information about the device providing connectivity.
+ *
+ * @param deviceInfo The user configurable device name.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setDeviceInfo(@NonNull DeviceInfo deviceInfo) {
+ mDeviceInfo = deviceInfo;
+ return this;
+ }
+
+ /**
+ * Sets the network type that the remote device is connected to.
+ *
+ * @param networkType Network type as represented by IntDef {@link NetworkType}.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setNetworkType(@NetworkType int networkType) {
+ mNetworkType = networkType;
+ return this;
+ }
+
+ /**
+ * Sets the display name of the network the remote device is connected to.
+ *
+ * @param networkName Network display name. (e.g. "Google Fi", "Hotel WiFi", "Ethernet")
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setNetworkName(@NonNull String networkName) {
+ mNetworkName = networkName;
+ return this;
+ }
+
+ /**
+ * Sets the hotspot SSID being broadcast by the remote device, or null if hotspot is off.
+ *
+ * @param hotspotSsid The SSID of the hotspot. Surrounded by double quotes if UTF-8.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setHotspotSsid(@NonNull String hotspotSsid) {
+ mHotspotSsid = hotspotSsid;
+ return this;
+ }
+
+ /**
+ * Sets the hotspot BSSID being broadcast by the remote device, or null if hotspot is off.
+ *
+ * @param hotspotBssid The BSSID of the hotspot.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setHotspotBssid(@NonNull String hotspotBssid) {
+ mHotspotBssid = hotspotBssid;
+ return this;
+ }
+
+ /**
+ * Sets the hotspot security types supported by the remote device, or null if hotspot is
+ * off.
+ *
+ * @param hotspotSecurityTypes The array of security types supported by the hotspot.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setHotspotSecurityTypes(@NonNull @SecurityType int[] hotspotSecurityTypes) {
+ mHotspotSecurityTypes = hotspotSecurityTypes;
+ return this;
+ }
+
+ /**
+ * Builds the {@link TetherNetwork} object.
+ *
+ * @return Returns the built {@link TetherNetwork} object.
+ */
+ @NonNull
+ public TetherNetwork build() {
+ return new TetherNetwork(
+ mDeviceId,
+ mDeviceInfo,
+ mNetworkType,
+ mNetworkName,
+ mHotspotSsid,
+ mHotspotBssid,
+ mHotspotSecurityTypes);
+ }
+ }
+
+ private static void validate(long deviceId, int networkType, String networkName) {
+ if (deviceId < 0) {
+ throw new IllegalArgumentException("DeviceId must be set");
+ }
+ if (networkType != NETWORK_TYPE_CELLULAR && networkType != NETWORK_TYPE_WIFI
+ && networkType != NETWORK_TYPE_ETHERNET && networkType != NETWORK_TYPE_UNKNOWN) {
+ throw new IllegalArgumentException("Illegal network type");
+ }
+ if (Objects.isNull(networkName)) {
+ throw new IllegalArgumentException("NetworkName must be set");
+ }
+ }
+
+ private TetherNetwork(
+ long deviceId,
+ DeviceInfo deviceInfo,
+ @NetworkType int networkType,
+ @NonNull String networkName,
+ @Nullable String hotspotSsid,
+ @Nullable String hotspotBssid,
+ @Nullable @SecurityType int[] hotspotSecurityTypes) {
+ validate(deviceId,
+ networkType,
+ networkName);
+ mDeviceId = deviceId;
+ mDeviceInfo = deviceInfo;
+ mNetworkType = networkType;
+ mNetworkName = networkName;
+ mHotspotSsid = hotspotSsid;
+ mHotspotBssid = hotspotBssid;
+ mHotspotSecurityTypes = hotspotSecurityTypes;
+ }
+
+ /**
+ * Gets the remote device ID.
+ *
+ * @return Returns the locally unique ID for this Instant Tether network.
+ */
+ @NonNull
+ public long getDeviceId() {
+ return mDeviceId;
+ }
+
+ /**
+ * Gets information about the device providing connectivity.
+ *
+ * @return Returns the locally unique ID for this Instant Tether network.
+ */
+ @NonNull
+ public DeviceInfo getDeviceInfo() {
+ return mDeviceInfo;
+ }
+
+ /**
+ * Gets the network type that the remote device is connected to.
+ *
+ * @return Returns the network type as represented by IntDef {@link NetworkType}.
+ */
+ @NonNull
+ @NetworkType
+ public int getNetworkType() {
+ return mNetworkType;
+ }
+
+ /**
+ * Gets the display name of the network the remote device is connected to.
+ *
+ * @return Returns the network display name. (e.g. "Google Fi", "Hotel WiFi", "Ethernet")
+ */
+ @NonNull
+ public String getNetworkName() {
+ return mNetworkName;
+ }
+
+ /**
+ * Gets the hotspot SSID being broadcast by the remote device, or null if hotspot is off.
+ *
+ * @return Returns the SSID of the hotspot. Surrounded by double quotes if UTF-8.
+ */
+ @Nullable
+ public String getHotspotSsid() {
+ return mHotspotSsid;
+ }
+
+ /**
+ * Gets the hotspot BSSID being broadcast by the remote device, or null if hotspot is off.
+ *
+ * @return Returns the BSSID of the hotspot.
+ */
+ @Nullable
+ public String getHotspotBssid() {
+ return mHotspotBssid;
+ }
+
+ /**
+ * Gets the hotspot security types supported by the remote device.
+ *
+ * @return Returns the array of security types supported by the hotspot.
+ */
+ @Nullable
+ @SecurityType
+ public int[] getHotspotSecurityTypes() {
+ return mHotspotSecurityTypes;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof TetherNetwork)) return false;
+ TetherNetwork other = (TetherNetwork) obj;
+ return mDeviceId == other.getDeviceId()
+ && Objects.equals(mDeviceInfo, other.getDeviceInfo())
+ && mNetworkType == other.getNetworkType()
+ && Objects.equals(mNetworkName, other.getNetworkName())
+ && Objects.equals(mHotspotSsid, other.getHotspotSsid())
+ && Objects.equals(mHotspotBssid, other.getHotspotBssid())
+ && Arrays.equals(mHotspotSecurityTypes, other.getHotspotSecurityTypes());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDeviceId, mDeviceInfo, mNetworkName, mHotspotSsid, mHotspotBssid,
+ Arrays.hashCode(mHotspotSecurityTypes));
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeLong(mDeviceId);
+ dest.writeTypedObject(mDeviceInfo, 0);
+ dest.writeInt(mNetworkType);
+ dest.writeString(mNetworkName);
+ dest.writeString(mHotspotSsid);
+ dest.writeString(mHotspotBssid);
+ dest.writeIntArray(mHotspotSecurityTypes);
+ }
+
+ @NonNull
+ public static final Creator<TetherNetwork> CREATOR = new Creator<>() {
+ @Override
+ public TetherNetwork createFromParcel(Parcel in) {
+ return new TetherNetwork(in.readLong(), in.readTypedObject(
+ android.net.wifi.sharedconnectivity.app.DeviceInfo.CREATOR),
+ in.readInt(), in.readString(), in.readString(), in.readString(),
+ in.createIntArray());
+ }
+
+ @Override
+ public TetherNetwork[] newArray(int size) {
+ return new TetherNetwork[size];
+ }
+ };
+
+ @Override
+ public String toString() {
+ return new StringBuilder("TetherNetwork[")
+ .append("deviceId=").append(mDeviceId)
+ .append(", networkType=").append(mNetworkType)
+ .append(", deviceInfo=").append(mDeviceInfo.toString())
+ .append(", networkName=").append(mNetworkName)
+ .append(", hotspotSsid=").append(mHotspotSsid)
+ .append(", hotspotBssid=").append(mHotspotBssid)
+ .append(", hotspotSecurityTypes=").append(Arrays.toString(mHotspotSecurityTypes))
+ .append("]").toString();
+ }
+}
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/service/ISharedConnectivityCallback.aidl b/wifi/java/src/android/net/wifi/sharedconnectivity/service/ISharedConnectivityCallback.aidl
new file mode 100644
index 0000000..6e56138
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/service/ISharedConnectivityCallback.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.service;
+
+import android.net.wifi.sharedconnectivity.app.KnownNetwork;
+import android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState;
+import android.net.wifi.sharedconnectivity.app.TetherNetwork;
+
+/*
+ * @hide
+ */
+interface ISharedConnectivityCallback {
+ oneway void onTetherNetworksUpdated(in List<TetherNetwork> networks);
+ oneway void onKnownNetworksUpdated(in List<KnownNetwork> networks);
+ oneway void onSharedConnectivitySettingsChanged(in SharedConnectivitySettingsState state);
+}
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/service/ISharedConnectivityService.aidl b/wifi/java/src/android/net/wifi/sharedconnectivity/service/ISharedConnectivityService.aidl
new file mode 100644
index 0000000..5d79405
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/service/ISharedConnectivityService.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.service;
+
+import android.net.wifi.sharedconnectivity.app.KnownNetwork;
+import android.net.wifi.sharedconnectivity.app.TetherNetwork;
+import android.net.wifi.sharedconnectivity.service.ISharedConnectivityCallback;
+
+/*
+ * @hide
+ */
+interface ISharedConnectivityService {
+ void registerCallback(in ISharedConnectivityCallback callback);
+ void unregisterCallback(in ISharedConnectivityCallback callback);
+ void connectTetherNetwork(in TetherNetwork network);
+ void disconnectTetherNetwork();
+ void connectKnownNetwork(in KnownNetwork network);
+ void forgetKnownNetwork(in KnownNetwork network);
+}
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityService.java b/wifi/java/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityService.java
new file mode 100644
index 0000000..234319a
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityService.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.service;
+
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.NETWORK_SETUP_WIZARD;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.wifi.sharedconnectivity.app.KnownNetwork;
+import android.net.wifi.sharedconnectivity.app.SharedConnectivityManager;
+import android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState;
+import android.net.wifi.sharedconnectivity.app.TetherNetwork;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * This class is the partly implemented service for injecting Shared Connectivity networks into the
+ * Wi-Fi Pickers and other relevant UI surfaces.
+ *
+ * Implementing application should extend this service and override the indicated methods.
+ * Callers to the service should use {@link SharedConnectivityManager} to bind to the implemented
+ * service as specified in the configuration overlay.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class SharedConnectivityService extends Service {
+ private static final String TAG = SharedConnectivityService.class.getSimpleName();
+ private static final boolean DEBUG = true;
+
+ private final Handler mHandler;
+ private final List<ISharedConnectivityCallback> mCallbacks = new ArrayList<>();
+ // Used to find DeathRecipient when unregistering a callback to call unlinkToDeath.
+ private final Map<ISharedConnectivityCallback, DeathRecipient> mDeathRecipientMap =
+ new HashMap<>();
+
+ private List<TetherNetwork> mTetherNetworks = Collections.emptyList();
+ private List<KnownNetwork> mKnownNetworks = Collections.emptyList();
+ private SharedConnectivitySettingsState mSettingsState;
+
+ public SharedConnectivityService() {
+ mHandler = new Handler(getMainLooper());
+ }
+
+ public SharedConnectivityService(@NonNull Handler handler) {
+ mHandler = handler;
+ }
+
+ private final class DeathRecipient implements IBinder.DeathRecipient {
+ ISharedConnectivityCallback mCallback;
+
+ DeathRecipient(ISharedConnectivityCallback callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void binderDied() {
+ mCallbacks.remove(mCallback);
+ mDeathRecipientMap.remove(mCallback);
+ }
+ }
+
+ @Override
+ @Nullable
+ public IBinder onBind(@NonNull Intent intent) {
+ if (DEBUG) Log.i(TAG, "onBind intent=" + intent);
+ return new ISharedConnectivityService.Stub() {
+ @Override
+ public void registerCallback(ISharedConnectivityCallback callback) {
+ checkPermissions();
+ mHandler.post(() -> registerCallback(callback));
+ }
+
+ @Override
+ public void unregisterCallback(ISharedConnectivityCallback callback) {
+ checkPermissions();
+ mHandler.post(() -> unregisterCallback(callback));
+ }
+
+ @Override
+ public void connectTetherNetwork(TetherNetwork network) {
+ checkPermissions();
+ mHandler.post(() -> onConnectTetherNetwork(network));
+ }
+
+ @Override
+ public void disconnectTetherNetwork() {
+ checkPermissions();
+ mHandler.post(() -> onDisconnectTetherNetwork());
+ }
+
+ @Override
+ public void connectKnownNetwork(KnownNetwork network) {
+ checkPermissions();
+ mHandler.post(() -> onConnectKnownNetwork(network));
+ }
+
+ @Override
+ public void forgetKnownNetwork(KnownNetwork network) {
+ checkPermissions();
+ mHandler.post(() -> onForgetKnownNetwork(network));
+ }
+
+ @RequiresPermission(anyOf = {android.Manifest.permission.NETWORK_SETTINGS,
+ android.Manifest.permission.NETWORK_SETUP_WIZARD})
+ private void checkPermissions() {
+ if (checkCallingPermission(NETWORK_SETTINGS) != PackageManager.PERMISSION_GRANTED
+ && checkCallingPermission(NETWORK_SETUP_WIZARD)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Calling process must have NETWORK_SETTINGS or"
+ + " NETWORK_SETUP_WIZARD permission");
+ }
+ }
+ };
+ }
+
+ private void registerCallback(ISharedConnectivityCallback callback) {
+ // Listener gets triggered on first register using cashed data
+ if (!notifyTetherNetworkUpdate(callback) || !notifyKnownNetworkUpdate(callback)) {
+ if (DEBUG) Log.w(TAG, "Failed to notify client");
+ return;
+ }
+
+ DeathRecipient deathRecipient = new DeathRecipient(callback);
+ try {
+ callback.asBinder().linkToDeath(deathRecipient, 0);
+ mCallbacks.add(callback);
+ mDeathRecipientMap.put(callback, deathRecipient);
+ } catch (RemoteException e) {
+ if (DEBUG) Log.w(TAG, "Exception in registerCallback", e);
+ }
+ }
+
+ private void unregisterCallback(ISharedConnectivityCallback callback) {
+ DeathRecipient deathRecipient = mDeathRecipientMap.get(callback);
+ if (deathRecipient != null) {
+ callback.asBinder().unlinkToDeath(deathRecipient, 0);
+ mDeathRecipientMap.remove(callback);
+ }
+ mCallbacks.remove(callback);
+ }
+
+ private boolean notifyTetherNetworkUpdate(ISharedConnectivityCallback callback) {
+ try {
+ callback.onTetherNetworksUpdated(mTetherNetworks);
+ } catch (RemoteException e) {
+ if (DEBUG) Log.w(TAG, "Exception in notifyTetherNetworkUpdate", e);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean notifyKnownNetworkUpdate(ISharedConnectivityCallback callback) {
+ try {
+ callback.onKnownNetworksUpdated(mKnownNetworks);
+ } catch (RemoteException e) {
+ if (DEBUG) Log.w(TAG, "Exception in notifyKnownNetworkUpdate", e);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean notifySettingsStateUpdate(ISharedConnectivityCallback callback) {
+ try {
+ callback.onSharedConnectivitySettingsChanged(mSettingsState);
+ } catch (RemoteException e) {
+ if (DEBUG) Log.w(TAG, "Exception in notifySettingsStateUpdate", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Implementing application should call this method to provide an up-to-date list of Tether
+ * Networks to be displayed to the user.
+ *
+ * This method updates the cached list and notifies all registered callbacks. Any callbacks that
+ * are inaccessible will be unregistered.
+ *
+ * @param networks The updated list of {@link TetherNetwork} objects.
+ */
+ public final void setTetherNetworks(@NonNull List<TetherNetwork> networks) {
+ mTetherNetworks = networks;
+
+ for (ISharedConnectivityCallback callback:mCallbacks) {
+ notifyTetherNetworkUpdate(callback);
+ }
+ }
+
+ /**
+ * Implementing application should call this method to provide an up-to-date list of Known
+ * Networks to be displayed to the user.
+ *
+ * This method updates the cached list and notifies all registered callbacks. Any callbacks that
+ * are inaccessible will be unregistered.
+ *
+ * @param networks The updated list of {@link KnownNetwork} objects.
+ */
+ public final void setKnownNetworks(@NonNull List<KnownNetwork> networks) {
+ mKnownNetworks = networks;
+
+ for (ISharedConnectivityCallback callback:mCallbacks) {
+ notifyKnownNetworkUpdate(callback);
+ }
+ }
+
+ /**
+ * Implementing application should call this method to provide an up-to-date state of Shared
+ * connectivity settings state.
+ *
+ * This method updates the cached state and notifies all registered callbacks. Any callbacks
+ * that are inaccessible will be unregistered.
+ *
+ * @param settingsState The updated state {@link SharedConnectivitySettingsState}
+ * objects.
+ */
+ public final void setSettingsState(@NonNull SharedConnectivitySettingsState settingsState) {
+ mSettingsState = settingsState;
+
+ for (ISharedConnectivityCallback callback:mCallbacks) {
+ notifySettingsStateUpdate(callback);
+ }
+ }
+
+ /**
+ * Implementing application should implement this method.
+ *
+ * Implementation should initiate a connection to the Tether Network indicated.
+ *
+ * @param network Object identifying the Tether Network the user has requested a connection to.
+ */
+ public abstract void onConnectTetherNetwork(@NonNull TetherNetwork network);
+
+ /**
+ * Implementing application should implement this method.
+ *
+ * Implementation should initiate a disconnection from the active Tether Network.
+ */
+ public abstract void onDisconnectTetherNetwork();
+
+ /**
+ * Implementing application should implement this method.
+ *
+ * Implementation should initiate a connection to the Known Network indicated.
+ *
+ * @param network Object identifying the Known Network the user has requested a connection to.
+ */
+ public abstract void onConnectKnownNetwork(@NonNull KnownNetwork network);
+
+ /**
+ * Implementing application should implement this method.
+ *
+ * Implementation should remove the Known Network indicated from the synced list of networks.
+ *
+ * @param network Object identifying the Known Network the user has requested to forget.
+ */
+ public abstract void onForgetKnownNetwork(@NonNull KnownNetwork network);
+}
diff --git a/wifi/tests/src/android/net/wifi/sharedconnectivity/OWNERS b/wifi/tests/src/android/net/wifi/sharedconnectivity/OWNERS
new file mode 100644
index 0000000..8873d07
--- /dev/null
+++ b/wifi/tests/src/android/net/wifi/sharedconnectivity/OWNERS
@@ -0,0 +1 @@
+file:/wifi/java/src/android/net/wifi/sharedconnectivity/OWNERS
diff --git a/wifi/tests/src/android/net/wifi/sharedconnectivity/app/DeviceInfoTest.java b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/DeviceInfoTest.java
new file mode 100644
index 0000000..f8f0700
--- /dev/null
+++ b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/DeviceInfoTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import static android.net.wifi.sharedconnectivity.app.DeviceInfo.DEVICE_TYPE_LAPTOP;
+import static android.net.wifi.sharedconnectivity.app.DeviceInfo.DEVICE_TYPE_PHONE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link android.app.sharedconnectivity.DeviceInfo}.
+ */
+@SmallTest
+public class DeviceInfoTest {
+
+ private static final int DEVICE_TYPE = DEVICE_TYPE_PHONE;
+ private static final String DEVICE_NAME = "TEST_NAME";
+ private static final String DEVICE_MODEL = "TEST_MODEL";
+ private static final int BATTERY_PERCENTAGE = 50;
+ private static final int CONNECTION_STRENGTH = 2;
+
+ private static final int DEVICE_TYPE_1 = DEVICE_TYPE_LAPTOP;
+ private static final String DEVICE_NAME_1 = "TEST_NAME1";
+ private static final String DEVICE_MODEL_1 = "TEST_MODEL1";
+ private static final int BATTERY_PERCENTAGE_1 = 30;
+ private static final int CONNECTION_STRENGTH_1 = 1;
+
+ /**
+ * Verifies parcel serialization/deserialization.
+ */
+ @Test
+ public void testParcelOperation() {
+ DeviceInfo info = buildDeviceInfoBuilder().build();
+
+ Parcel parcelW = Parcel.obtain();
+ info.writeToParcel(parcelW, 0);
+ byte[] bytes = parcelW.marshall();
+ parcelW.recycle();
+
+ Parcel parcelR = Parcel.obtain();
+ parcelR.unmarshall(bytes, 0, bytes.length);
+ parcelR.setDataPosition(0);
+ DeviceInfo fromParcel = DeviceInfo.CREATOR.createFromParcel(parcelR);
+
+ assertEquals(info, fromParcel);
+ assertEquals(info.hashCode(), fromParcel.hashCode());
+ }
+
+ /**
+ * Verifies the Equals operation
+ */
+ @Test
+ public void testEqualsOperation() {
+ DeviceInfo info1 = buildDeviceInfoBuilder().build();
+ DeviceInfo info2 = buildDeviceInfoBuilder().build();
+ assertEquals(info1, info2);
+
+ DeviceInfo.Builder builder = buildDeviceInfoBuilder().setDeviceType(DEVICE_TYPE_1);
+ assertNotEquals(info1, builder.build());
+
+ builder = buildDeviceInfoBuilder().setDeviceName(DEVICE_NAME_1);
+ assertNotEquals(info1, builder.build());
+
+ builder = buildDeviceInfoBuilder().setModelName(DEVICE_MODEL_1);
+ assertNotEquals(info1, builder.build());
+
+ builder = buildDeviceInfoBuilder()
+ .setBatteryPercentage(BATTERY_PERCENTAGE_1);
+ assertNotEquals(info1, builder.build());
+
+ builder = buildDeviceInfoBuilder()
+ .setConnectionStrength(CONNECTION_STRENGTH_1);
+ assertNotEquals(info1, builder.build());
+ }
+
+ /**
+ * Verifies the get methods return the expected data.
+ */
+ @Test
+ public void testGetMethods() {
+ DeviceInfo info = buildDeviceInfoBuilder().build();
+ assertEquals(info.getDeviceType(), DEVICE_TYPE);
+ assertEquals(info.getDeviceName(), DEVICE_NAME);
+ assertEquals(info.getModelName(), DEVICE_MODEL);
+ assertEquals(info.getBatteryPercentage(), BATTERY_PERCENTAGE);
+ assertEquals(info.getConnectionStrength(), CONNECTION_STRENGTH);
+ assertEquals(info.getConnectionStrength(), CONNECTION_STRENGTH);
+ }
+
+ private DeviceInfo.Builder buildDeviceInfoBuilder() {
+ return new DeviceInfo.Builder().setDeviceType(DEVICE_TYPE).setDeviceName(DEVICE_NAME)
+ .setModelName(DEVICE_MODEL).setBatteryPercentage(BATTERY_PERCENTAGE)
+ .setConnectionStrength(CONNECTION_STRENGTH);
+ }
+}
diff --git a/wifi/tests/src/android/net/wifi/sharedconnectivity/app/KnownNetworkTest.java b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/KnownNetworkTest.java
new file mode 100644
index 0000000..266afcc
--- /dev/null
+++ b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/KnownNetworkTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import static android.net.wifi.WifiInfo.SECURITY_TYPE_PSK;
+import static android.net.wifi.WifiInfo.SECURITY_TYPE_WEP;
+import static android.net.wifi.sharedconnectivity.app.DeviceInfo.DEVICE_TYPE_PHONE;
+import static android.net.wifi.sharedconnectivity.app.DeviceInfo.DEVICE_TYPE_TABLET;
+import static android.net.wifi.sharedconnectivity.app.KnownNetwork.NETWORK_SOURCE_CLOUD_SELF;
+import static android.net.wifi.sharedconnectivity.app.KnownNetwork.NETWORK_SOURCE_NEARBY_SELF;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link android.app.sharedconnectivity.KnownNetwork}.
+ */
+@SmallTest
+public class KnownNetworkTest {
+
+ private static final int NETWORK_SOURCE = NETWORK_SOURCE_NEARBY_SELF;
+ private static final String SSID = "TEST_SSID";
+ private static final int[] SECURITY_TYPES = {SECURITY_TYPE_WEP};
+ private static final DeviceInfo DEVICE_INFO = new DeviceInfo.Builder()
+ .setDeviceType(DEVICE_TYPE_TABLET).setDeviceName("TEST_NAME").setModelName("TEST_MODEL")
+ .setConnectionStrength(2).setBatteryPercentage(50).build();
+ private static final int NETWORK_SOURCE_1 = NETWORK_SOURCE_CLOUD_SELF;
+ private static final String SSID_1 = "TEST_SSID1";
+ private static final int[] SECURITY_TYPES_1 = {SECURITY_TYPE_PSK};
+ private static final DeviceInfo DEVICE_INFO_1 = new DeviceInfo.Builder()
+ .setDeviceType(DEVICE_TYPE_PHONE).setDeviceName("TEST_NAME_1")
+ .setModelName("TEST_MODEL_1").setConnectionStrength(3).setBatteryPercentage(33).build();
+
+ /**
+ * Verifies parcel serialization/deserialization.
+ */
+ @Test
+ public void testParcelOperation() {
+ KnownNetwork network = buildKnownNetworkBuilder().build();
+
+ Parcel parcelW = Parcel.obtain();
+ network.writeToParcel(parcelW, 0);
+ byte[] bytes = parcelW.marshall();
+ parcelW.recycle();
+
+ Parcel parcelR = Parcel.obtain();
+ parcelR.unmarshall(bytes, 0, bytes.length);
+ parcelR.setDataPosition(0);
+ KnownNetwork fromParcel = KnownNetwork.CREATOR.createFromParcel(parcelR);
+
+ assertEquals(network, fromParcel);
+ assertEquals(network.hashCode(), fromParcel.hashCode());
+ }
+
+ /**
+ * Verifies the Equals operation
+ */
+ @Test
+ public void testEqualsOperation() {
+ KnownNetwork network1 = buildKnownNetworkBuilder().build();
+ KnownNetwork network2 = buildKnownNetworkBuilder().build();
+ assertEquals(network1, network2);
+
+ KnownNetwork.Builder builder = buildKnownNetworkBuilder()
+ .setNetworkSource(NETWORK_SOURCE_1);
+ assertNotEquals(network1, builder.build());
+
+ builder = buildKnownNetworkBuilder().setSsid(SSID_1);
+ assertNotEquals(network1, builder.build());
+
+ builder = buildKnownNetworkBuilder().setSecurityTypes(SECURITY_TYPES_1);
+ assertNotEquals(network1, builder.build());
+
+ builder = buildKnownNetworkBuilder().setDeviceInfo(DEVICE_INFO_1);
+ assertNotEquals(network1, builder.build());
+ }
+
+ /**
+ * Verifies the get methods return the expected data.
+ */
+ @Test
+ public void testGetMethods() {
+ KnownNetwork network = buildKnownNetworkBuilder().build();
+ assertEquals(network.getNetworkSource(), NETWORK_SOURCE);
+ assertEquals(network.getSsid(), SSID);
+ assertArrayEquals(network.getSecurityTypes(), SECURITY_TYPES);
+ assertEquals(network.getDeviceInfo(), DEVICE_INFO);
+ }
+
+ private KnownNetwork.Builder buildKnownNetworkBuilder() {
+ return new KnownNetwork.Builder().setNetworkSource(NETWORK_SOURCE).setSsid(SSID)
+ .setSecurityTypes(SECURITY_TYPES).setDeviceInfo(DEVICE_INFO);
+ }
+}
diff --git a/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManagerTest.java b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManagerTest.java
new file mode 100644
index 0000000..9aeccac
--- /dev/null
+++ b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManagerTest.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import static android.net.wifi.WifiInfo.SECURITY_TYPE_EAP;
+import static android.net.wifi.WifiInfo.SECURITY_TYPE_WEP;
+import static android.net.wifi.sharedconnectivity.app.DeviceInfo.DEVICE_TYPE_TABLET;
+import static android.net.wifi.sharedconnectivity.app.KnownNetwork.NETWORK_SOURCE_NEARBY_SELF;
+import static android.net.wifi.sharedconnectivity.app.TetherNetwork.NETWORK_TYPE_CELLULAR;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.wifi.sharedconnectivity.service.ISharedConnectivityService;
+import android.os.RemoteException;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Unit tests for {@link SharedConnectivityManager}.
+ */
+@SmallTest
+public class SharedConnectivityManagerTest {
+ private static final long DEVICE_ID = 11L;
+ private static final DeviceInfo DEVICE_INFO = new DeviceInfo.Builder()
+ .setDeviceType(DEVICE_TYPE_TABLET).setDeviceName("TEST_NAME").setModelName("TEST_MODEL")
+ .setConnectionStrength(2).setBatteryPercentage(50).build();
+ private static final int NETWORK_TYPE = NETWORK_TYPE_CELLULAR;
+ private static final String NETWORK_NAME = "TEST_NETWORK";
+ private static final String HOTSPOT_SSID = "TEST_SSID";
+ private static final int[] HOTSPOT_SECURITY_TYPES = {SECURITY_TYPE_WEP, SECURITY_TYPE_EAP};
+
+ private static final int NETWORK_SOURCE = NETWORK_SOURCE_NEARBY_SELF;
+ private static final String SSID = "TEST_SSID";
+ private static final int[] SECURITY_TYPES = {SECURITY_TYPE_WEP};
+
+ private static final int SERVICE_PACKAGE_ID = 1;
+ private static final int SERVICE_CLASS_ID = 2;
+
+ private static final String SERVICE_PACKAGE_NAME = "TEST_PACKAGE";
+ private static final String SERVICE_CLASS_NAME = "TEST_CLASS";
+ private static final String PACKAGE_NAME = "TEST_PACKAGE";
+
+ @Mock Context mContext;
+ @Mock
+ ISharedConnectivityService mService;
+ @Mock Executor mExecutor;
+ @Mock
+ SharedConnectivityClientCallback mClientCallback;
+ @Mock Resources mResources;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ setResources(mContext);
+ }
+
+ /**
+ * Verifies constructor is binding to service.
+ */
+ @Test
+ public void testBindingToService() {
+ new SharedConnectivityManager(mContext);
+ verify(mContext).bindService(any(), any(), anyInt());
+ }
+
+ /**
+ * Verifies callback is registered in the service only once and only when service is not null.
+ */
+ @Test
+ public void testRegisterCallback() throws Exception {
+ SharedConnectivityManager manager = new SharedConnectivityManager(mContext);
+ manager.setService(null);
+ assertFalse(manager.registerCallback(mExecutor, mClientCallback));
+
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ assertTrue(manager.registerCallback(mExecutor, mClientCallback));
+ verify(mService).registerCallback(any());
+
+ // Registering the same callback twice should fail.
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ manager.registerCallback(mExecutor, mClientCallback);
+ assertFalse(manager.registerCallback(mExecutor, mClientCallback));
+
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ doThrow(new RemoteException()).when(mService).registerCallback(any());
+ assertFalse(manager.registerCallback(mExecutor, mClientCallback));
+ }
+
+ /**
+ * Verifies callback is unregistered from the service if it was registered before and only when
+ * service is not null.
+ */
+ @Test
+ public void testUnregisterCallback() throws Exception {
+ SharedConnectivityManager manager = new SharedConnectivityManager(mContext);
+ manager.setService(null);
+ assertFalse(manager.unregisterCallback(mClientCallback));
+
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ manager.registerCallback(mExecutor, mClientCallback);
+ assertTrue(manager.unregisterCallback(mClientCallback));
+ verify(mService).unregisterCallback(any());
+
+
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ manager.registerCallback(mExecutor, mClientCallback);
+ manager.unregisterCallback(mClientCallback);
+ assertFalse(manager.unregisterCallback(mClientCallback));
+
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ doThrow(new RemoteException()).when(mService).unregisterCallback(any());
+ assertFalse(manager.unregisterCallback(mClientCallback));
+ }
+
+ /**
+ * Verifies service is called when not null and exceptions are handles when calling
+ * connectTetherNetwork.
+ */
+ @Test
+ public void testConnectTetherNetwork() throws RemoteException {
+ TetherNetwork network = buildTetherNetwork();
+
+ SharedConnectivityManager manager = new SharedConnectivityManager(mContext);
+ manager.setService(null);
+ assertFalse(manager.connectTetherNetwork(network));
+
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ manager.connectTetherNetwork(network);
+ verify(mService).connectTetherNetwork(network);
+
+ doThrow(new RemoteException()).when(mService).connectTetherNetwork(network);
+ assertFalse(manager.connectTetherNetwork(network));
+ }
+
+ /**
+ * Verifies service is called when not null and exceptions are handles when calling
+ * disconnectTetherNetwork.
+ */
+ @Test
+ public void testDisconnectTetherNetwork() throws RemoteException {
+ SharedConnectivityManager manager = new SharedConnectivityManager(mContext);
+ manager.setService(null);
+ assertFalse(manager.disconnectTetherNetwork());
+
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ manager.disconnectTetherNetwork();
+ verify(mService).disconnectTetherNetwork();
+
+ doThrow(new RemoteException()).when(mService).disconnectTetherNetwork();
+ assertFalse(manager.disconnectTetherNetwork());
+ }
+
+ /**
+ * Verifies service is called when not null and exceptions are handles when calling
+ * connectKnownNetwork.
+ */
+ @Test
+ public void testConnectKnownNetwork() throws RemoteException {
+ KnownNetwork network = buildKnownNetwork();
+
+ SharedConnectivityManager manager = new SharedConnectivityManager(mContext);
+ manager.setService(null);
+ assertFalse(manager.connectKnownNetwork(network));
+
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ manager.connectKnownNetwork(network);
+ verify(mService).connectKnownNetwork(network);
+
+ doThrow(new RemoteException()).when(mService).connectKnownNetwork(network);
+ assertFalse(manager.connectKnownNetwork(network));
+ }
+
+ /**
+ * Verifies service is called when not null and exceptions are handles when calling
+ * forgetKnownNetwork.
+ */
+ @Test
+ public void testForgetKnownNetwork() throws RemoteException {
+ KnownNetwork network = buildKnownNetwork();
+
+ SharedConnectivityManager manager = new SharedConnectivityManager(mContext);
+ manager.setService(null);
+ assertFalse(manager.forgetKnownNetwork(network));
+
+ manager = new SharedConnectivityManager(mContext);
+ manager.setService(mService);
+ manager.forgetKnownNetwork(network);
+ verify(mService).forgetKnownNetwork(network);
+
+ doThrow(new RemoteException()).when(mService).forgetKnownNetwork(network);
+ assertFalse(manager.forgetKnownNetwork(network));
+ }
+
+ private void setResources(@Mock Context context) {
+ when(context.getResources()).thenReturn(mResources);
+ when(context.getPackageName()).thenReturn(PACKAGE_NAME);
+ when(mResources.getIdentifier(anyString(), anyString(), anyString()))
+ .thenReturn(SERVICE_PACKAGE_ID, SERVICE_CLASS_ID);
+ when(mResources.getString(SERVICE_PACKAGE_ID)).thenReturn(SERVICE_PACKAGE_NAME);
+ when(mResources.getString(SERVICE_CLASS_ID)).thenReturn(SERVICE_CLASS_NAME);
+ }
+
+ private TetherNetwork buildTetherNetwork() {
+ return new TetherNetwork.Builder()
+ .setDeviceId(DEVICE_ID)
+ .setDeviceInfo(DEVICE_INFO)
+ .setNetworkType(NETWORK_TYPE)
+ .setNetworkName(NETWORK_NAME)
+ .setHotspotSsid(HOTSPOT_SSID)
+ .setHotspotSecurityTypes(HOTSPOT_SECURITY_TYPES)
+ .build();
+ }
+
+ private KnownNetwork buildKnownNetwork() {
+ return new KnownNetwork.Builder().setNetworkSource(NETWORK_SOURCE).setSsid(SSID)
+ .setSecurityTypes(SECURITY_TYPES).build();
+ }
+}
diff --git a/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsStateTest.java b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsStateTest.java
new file mode 100644
index 0000000..3137c72
--- /dev/null
+++ b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivitySettingsStateTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState}.
+ */
+@SmallTest
+public class SharedConnectivitySettingsStateTest {
+ private static final boolean INSTANT_TETHER_STATE = true;
+
+ private static final boolean INSTANT_TETHER_STATE_1 = false;
+ /**
+ * Verifies parcel serialization/deserialization.
+ */
+ @Test
+ public void testParcelOperation() {
+ SharedConnectivitySettingsState state = buildSettingsStateBuilder().build();
+
+ Parcel parcelW = Parcel.obtain();
+ state.writeToParcel(parcelW, 0);
+ byte[] bytes = parcelW.marshall();
+ parcelW.recycle();
+
+ Parcel parcelR = Parcel.obtain();
+ parcelR.unmarshall(bytes, 0, bytes.length);
+ parcelR.setDataPosition(0);
+ SharedConnectivitySettingsState fromParcel =
+ SharedConnectivitySettingsState.CREATOR.createFromParcel(parcelR);
+
+ assertEquals(state, fromParcel);
+ assertEquals(state.hashCode(), fromParcel.hashCode());
+ }
+
+ /**
+ * Verifies the Equals operation
+ */
+ @Test
+ public void testEqualsOperation() {
+ SharedConnectivitySettingsState state1 = buildSettingsStateBuilder().build();
+ SharedConnectivitySettingsState state2 = buildSettingsStateBuilder().build();
+ assertEquals(state1, state2);
+
+ SharedConnectivitySettingsState.Builder builder = buildSettingsStateBuilder()
+ .setInstantTetherEnabled(INSTANT_TETHER_STATE_1);
+ assertNotEquals(state1, builder.build());
+ }
+
+ /**
+ * Verifies the get methods return the expected data.
+ */
+ @Test
+ public void testGetMethods() {
+ SharedConnectivitySettingsState state = buildSettingsStateBuilder().build();
+ assertEquals(state.isInstantTetherEnabled(), INSTANT_TETHER_STATE);
+ }
+
+ private SharedConnectivitySettingsState.Builder buildSettingsStateBuilder() {
+ return new SharedConnectivitySettingsState.Builder()
+ .setInstantTetherEnabled(INSTANT_TETHER_STATE);
+ }
+}
diff --git a/wifi/tests/src/android/net/wifi/sharedconnectivity/app/TetherNetworkTest.java b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/TetherNetworkTest.java
new file mode 100644
index 0000000..b01aec4
--- /dev/null
+++ b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/TetherNetworkTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.app;
+
+import static android.net.wifi.WifiInfo.SECURITY_TYPE_EAP;
+import static android.net.wifi.WifiInfo.SECURITY_TYPE_PSK;
+import static android.net.wifi.WifiInfo.SECURITY_TYPE_WEP;
+import static android.net.wifi.sharedconnectivity.app.DeviceInfo.DEVICE_TYPE_PHONE;
+import static android.net.wifi.sharedconnectivity.app.DeviceInfo.DEVICE_TYPE_TABLET;
+import static android.net.wifi.sharedconnectivity.app.TetherNetwork.NETWORK_TYPE_CELLULAR;
+import static android.net.wifi.sharedconnectivity.app.TetherNetwork.NETWORK_TYPE_WIFI;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link android.net.wifi.sharedconnectivity.app.TetherNetwork}.
+ */
+@SmallTest
+public class TetherNetworkTest {
+ private static final long DEVICE_ID = 11L;
+ private static final DeviceInfo DEVICE_INFO = new DeviceInfo.Builder()
+ .setDeviceType(DEVICE_TYPE_TABLET).setDeviceName("TEST_NAME").setModelName("TEST_MODEL")
+ .setConnectionStrength(2).setBatteryPercentage(50).build();
+ private static final int NETWORK_TYPE = NETWORK_TYPE_CELLULAR;
+ private static final String NETWORK_NAME = "TEST_NETWORK";
+ private static final String HOTSPOT_SSID = "TEST_SSID";
+ private static final String HOTSPOT_BSSID = "TEST _BSSID";
+ private static final int[] HOTSPOT_SECURITY_TYPES = {SECURITY_TYPE_WEP, SECURITY_TYPE_EAP};
+
+ private static final long DEVICE_ID_1 = 111L;
+ private static final DeviceInfo DEVICE_INFO_1 = new DeviceInfo.Builder()
+ .setDeviceType(DEVICE_TYPE_PHONE).setDeviceName("TEST_NAME").setModelName("TEST_MODEL")
+ .setConnectionStrength(2).setBatteryPercentage(50).build();
+ private static final int NETWORK_TYPE_1 = NETWORK_TYPE_WIFI;
+ private static final String NETWORK_NAME_1 = "TEST_NETWORK1";
+ private static final String HOTSPOT_SSID_1 = "TEST_SSID1";
+ private static final String HOTSPOT_BSSID_1 = "TEST _BSSID1";
+ private static final int[] HOTSPOT_SECURITY_TYPES_1 = {SECURITY_TYPE_PSK, SECURITY_TYPE_EAP};
+
+ /**
+ * Verifies parcel serialization/deserialization.
+ */
+ @Test
+ public void testParcelOperation() {
+ TetherNetwork network = buildTetherNetworkBuilder().build();
+
+ Parcel parcelW = Parcel.obtain();
+ network.writeToParcel(parcelW, 0);
+ byte[] bytes = parcelW.marshall();
+ parcelW.recycle();
+
+ Parcel parcelR = Parcel.obtain();
+ parcelR.unmarshall(bytes, 0, bytes.length);
+ parcelR.setDataPosition(0);
+ TetherNetwork fromParcel = TetherNetwork.CREATOR.createFromParcel(parcelR);
+
+ assertEquals(network, fromParcel);
+ assertEquals(network.hashCode(), fromParcel.hashCode());
+ }
+
+ /**
+ * Verifies the Equals operation
+ */
+ @Test
+ public void testEqualsOperation() {
+ TetherNetwork network1 = buildTetherNetworkBuilder().build();
+ TetherNetwork network2 = buildTetherNetworkBuilder().build();
+ assertEquals(network1, network2);
+
+ TetherNetwork.Builder builder = buildTetherNetworkBuilder().setDeviceId(DEVICE_ID_1);
+ assertNotEquals(network1, builder.build());
+
+ builder = buildTetherNetworkBuilder().setDeviceInfo(DEVICE_INFO_1);
+ assertNotEquals(network1, builder.build());
+
+ builder = buildTetherNetworkBuilder().setNetworkType(NETWORK_TYPE_1);
+ assertNotEquals(network1, builder.build());
+
+ builder = buildTetherNetworkBuilder().setNetworkName(NETWORK_NAME_1);
+ assertNotEquals(network1, builder.build());
+
+ builder = buildTetherNetworkBuilder().setHotspotSsid(HOTSPOT_SSID_1);
+ assertNotEquals(network1, builder.build());
+
+ builder = buildTetherNetworkBuilder().setHotspotBssid(HOTSPOT_BSSID_1);
+ assertNotEquals(network1, builder.build());
+
+ builder = buildTetherNetworkBuilder().setHotspotSecurityTypes(HOTSPOT_SECURITY_TYPES_1);
+ assertNotEquals(network1, builder.build());
+ }
+
+ /**
+ * Verifies the get methods return the expected data.
+ */
+ @Test
+ public void testGetMethods() {
+ TetherNetwork network = buildTetherNetworkBuilder().build();
+ assertEquals(network.getDeviceId(), DEVICE_ID);
+ assertEquals(network.getDeviceInfo(), DEVICE_INFO);
+ assertEquals(network.getNetworkType(), NETWORK_TYPE);
+ assertEquals(network.getNetworkName(), NETWORK_NAME);
+ assertEquals(network.getHotspotSsid(), HOTSPOT_SSID);
+ assertEquals(network.getHotspotBssid(), HOTSPOT_BSSID);
+ assertArrayEquals(network.getHotspotSecurityTypes(), HOTSPOT_SECURITY_TYPES);
+ }
+
+ private TetherNetwork.Builder buildTetherNetworkBuilder() {
+ return new TetherNetwork.Builder()
+ .setDeviceId(DEVICE_ID)
+ .setDeviceInfo(DEVICE_INFO)
+ .setNetworkType(NETWORK_TYPE)
+ .setNetworkName(NETWORK_NAME)
+ .setHotspotSsid(HOTSPOT_SSID)
+ .setHotspotBssid(HOTSPOT_BSSID)
+ .setHotspotSecurityTypes(HOTSPOT_SECURITY_TYPES);
+ }
+}
diff --git a/wifi/tests/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityServiceTest.java b/wifi/tests/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityServiceTest.java
new file mode 100644
index 0000000..e15be8b
--- /dev/null
+++ b/wifi/tests/src/android/net/wifi/sharedconnectivity/service/SharedConnectivityServiceTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.sharedconnectivity.service;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.content.Intent;
+import android.net.wifi.sharedconnectivity.app.KnownNetwork;
+import android.net.wifi.sharedconnectivity.app.TetherNetwork;
+import android.os.Handler;
+import android.os.test.TestLooper;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link android.net.wifi.sharedconnectivity.service.SharedConnectivityService}.
+ */
+@SmallTest
+public class SharedConnectivityServiceTest {
+
+ /**
+ * Verifies service returns
+ */
+ @Test
+ public void testOnBind() {
+ SharedConnectivityService service = createService();
+ assertNotNull(service.onBind(new Intent()));
+ }
+
+ @Test
+ public void testCallbacks() {
+ SharedConnectivityService service = createService();
+ ISharedConnectivityService.Stub binder =
+ (ISharedConnectivityService.Stub) service.onBind(new Intent());
+ }
+
+ private SharedConnectivityService createService() {
+ return new SharedConnectivityService(new Handler(new TestLooper().getLooper())) {
+ @Override
+ public void onConnectTetherNetwork(TetherNetwork network) {}
+
+ @Override
+ public void onDisconnectTetherNetwork() {}
+
+ @Override
+ public void onConnectKnownNetwork(KnownNetwork network) {}
+
+ @Override
+ public void onForgetKnownNetwork(KnownNetwork network) {}
+ };
+ }
+}