Merge "Lock screen LWP: adds information to `dumpsys wallpaper`"
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/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/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 954f094..d3de158 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";
@@ -7197,6 +7198,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
@@ -10711,6 +10713,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";
@@ -10804,6 +10807,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";
@@ -10859,6 +10867,7 @@
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";
@@ -12108,11 +12117,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;
@@ -12121,16 +12130,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;
}
@@ -12197,6 +12206,7 @@
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();
@@ -12252,6 +12262,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);
@@ -13498,6 +13509,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);
@@ -13510,31 +13533,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 {
@@ -40284,10 +40295,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 getGetCredentialOption();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.service.credentials.GetCredentialRequest> CREATOR;
}
@@ -41649,7 +41660,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>);
@@ -41698,7 +41708,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();
@@ -51309,7 +51318,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);
}
@@ -60323,7 +60332,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 fe73faa..cf92861 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -2119,6 +2119,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 {
@@ -3626,7 +3633,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);
@@ -3642,7 +3648,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[]);
@@ -12270,7 +12275,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>);
}
}
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 62a1eb9..360113b 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2409,6 +2409,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";
@@ -3146,6 +3148,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);
}
@@ -3265,6 +3270,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/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java
index f6992c9..d3b03c0 100644
--- a/core/java/android/app/BroadcastOptions.java
+++ b/core/java/android/app/BroadcastOptions.java
@@ -774,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;
diff --git a/core/java/android/app/ForegroundServiceTypePolicy.java b/core/java/android/app/ForegroundServiceTypePolicy.java
index 20d19c1..d8eb03e 100644
--- a/core/java/android/app/ForegroundServiceTypePolicy.java
+++ b/core/java/android/app/ForegroundServiceTypePolicy.java
@@ -109,7 +109,7 @@
*/
// TODO (b/254661666): Change to @EnabledAfter(T)
@ChangeId
- @Disabled
+ @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.TIRAMISU)
@Overridable
public static final long FGS_TYPE_NONE_DISABLED_CHANGE_ID = 255038118L;
@@ -144,7 +144,7 @@
*/
// TODO (b/254661666): Change to @EnabledAfter(T)
@ChangeId
- @Disabled
+ @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.TIRAMISU)
@Overridable
public static final long FGS_TYPE_PERMISSION_CHANGE_ID = 254662522L;
@@ -1059,7 +1059,7 @@
if (policy.isTypeDisabled(callerUid)) {
return FGS_TYPE_POLICY_CHECK_DISABLED;
}
- int permissionResult = PERMISSION_DENIED;
+ int permissionResult = PERMISSION_GRANTED;
// Do we have the permission to start FGS with this type.
if (policy.mAllOfPermissions != null) {
permissionResult = policy.mAllOfPermissions.checkPermissions(context,
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/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/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/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/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/Context.java b/core/java/android/content/Context.java
index a6e074c..6aa7f3f 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -6237,7 +6237,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.
*
* @return {@link PackageManager#PERMISSION_GRANTED} if the given
@@ -6327,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.
*
@@ -6471,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.
*
@@ -6499,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
*
@@ -6625,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.
*
@@ -6649,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.
@@ -6708,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/Intent.java b/core/java/android/content/Intent.java
index 30984b2..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.
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 fdfa1b2..83f0894 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
@@ -1313,6 +1324,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 d1f28ee..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
@@ -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;
}
@@ -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/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index d13a97d..d49cc44 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -115,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.
@@ -619,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);
@@ -644,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);
}
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/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 0a7eb43..2d2b0fc 100644
--- a/core/java/android/os/BugreportManager.java
+++ b/core/java/android/os/BugreportManager.java
@@ -279,7 +279,7 @@
* <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} and {@code screenshotFd}.
+ * <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}.
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 4f96805..653998f 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -15901,6 +15901,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/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..e808ace 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 getGetCredentialOption() {
+ 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/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/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..26fda34 100644
--- a/core/java/android/view/DisplayEventReceiver.java
+++ b/core/java/android/view/DisplayEventReceiver.java
@@ -80,7 +80,7 @@
private MessageQueue mMessageQueue;
private static native long nativeInit(WeakReference<DisplayEventReceiver> receiver,
- MessageQueue messageQueue, int vsyncSource, int eventRegistration);
+ MessageQueue messageQueue, int vsyncSource, int eventRegistration, long layerHandle);
private static native void nativeDispose(long receiverPtr);
@FastNative
private static native void nativeScheduleVsync(long receiverPtr);
@@ -93,7 +93,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,15 +107,17 @@
* @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);
+ vsyncSource, eventRegistration, layerHandle);
}
@Override
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..70a7739 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,20 +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) {
@@ -833,16 +803,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 +851,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 +879,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 +899,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 +960,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 +982,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 +1021,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..18e7e05 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,13 @@
mNativeObject = 0;
mNativeHandle = 0;
mCloseGuard.close();
+ synchronized (mChoreographerLock) {
+ if (mChoreographer != null) {
+ mChoreographer.invalidate();
+ // TODO(b/266121235): Use NativeAllocationRegistry to clean up Choreographer.
+ mChoreographer = null;
+ }
+ }
}
}
@@ -1692,7 +1757,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 +1826,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 +2505,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/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index cb316e2..872b4f9 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -23,7 +23,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;
@@ -9040,7 +9040,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 */);
}
@@ -9072,7 +9072,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 */);
}
@@ -11511,33 +11511,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..5ec5219 100644
--- a/core/java/android/view/WindowLayout.java
+++ b/core/java/android/view/WindowLayout.java
@@ -16,7 +16,7 @@
package android.view;
-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.ViewGroup.LayoutParams.MATCH_PARENT;
@@ -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 */));
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 367fd03..3d3f4f6 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -891,6 +891,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/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/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/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/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/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/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/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_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp
index a8d8a43..8855b78 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;
@@ -268,7 +272,7 @@
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},
// @FastNative
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/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..0378539 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
-->
@@ -6921,8 +6929,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_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/config.xml b/core/res/res/values/config.xml
index 1e3074c..14eaf34 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:
@@ -3233,6 +3240,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.. -->
@@ -4104,16 +4117,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">
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index d91e463..7777f11 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" />
@@ -1440,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" />
@@ -2500,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" />
@@ -3399,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" />
@@ -3414,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. -->
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/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/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/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/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
index 3adae70..20602a1 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
@@ -25,12 +25,13 @@
import android.util.ArraySet;
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 java.util.concurrent.Executor;
-import java.util.function.Consumer;
/**
* Reference implementation of androidx.window.extensions.area OEM interface for use with
@@ -185,6 +186,27 @@
}
}
+ @Override
+ public void addRearDisplayPresentationStatusListener(
+ @NonNull Consumer<ExtensionWindowAreaStatus> consumer) {}
+
+ @Override
+ public void removeRearDisplayPresentationStatusListener(
+ @NonNull Consumer<ExtensionWindowAreaStatus> consumer) {}
+
+ @Override
+ public void startRearDisplayPresentationSession(@NonNull Activity activity,
+ @NonNull Consumer<@WindowAreaSessionState Integer> consumer) {}
+
+ @Override
+ public void endRearDisplayPresentationSession() {}
+
+ @Override
+ @Nullable
+ public ExtensionWindowAreaPresentation getRearDisplayPresentation() {
+ return null;
+ }
+
@GuardedBy("mLock")
private int getCurrentStatus() {
if (mRearDisplaySessionStatus == WindowAreaComponent.SESSION_STATE_ACTIVE
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..4ed53a8 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.
@@ -640,35 +741,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 +1424,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 +1458,7 @@
// Cleanup any dependent references.
for (TaskFragmentContainer containerToUpdate : taskContainer.mContainers) {
- containerToUpdate.removeContainerToFinishOnExit(container);
+ containerToUpdate.removeContainersToFinishOnExit(containers);
}
}
@@ -1453,26 +1538,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 +1680,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 +1759,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 +1903,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 +2190,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..5d51760 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);
@@ -515,9 +513,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 +538,7 @@
}
static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) {
- return shouldShowSplit(splitContainer.getSplitAttributes());
+ return shouldShowSplit(splitContainer.getCurrentSplitAttributes());
}
static boolean shouldShowSplit(@NonNull SplitAttributes splitAttributes) {
@@ -549,12 +547,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 +955,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..ccb274a 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -37,6 +37,7 @@
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Iterator;
import java.util.List;
@@ -243,7 +244,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 +437,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 +486,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 +512,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;
}
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..be7c26e 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
@@ -228,7 +228,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 +248,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 +273,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 +339,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 +400,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 +442,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 +506,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 +528,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 +581,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 +597,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 +639,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 +694,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..378ad81 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..e36dfc3 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,21 @@
}
/**
+ * 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");
+ }
+ }
+
+ /**
* Fills the overflow bubbles by loading them from disk.
*/
void loadOverflowBubblesFromDisk() {
@@ -1750,6 +1772,25 @@
}
@Override
+ public boolean isAppBubbleTaskId(int taskId) {
+ Bubble appBubble = mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE);
+ return appBubble != null && appBubble.getTaskId() == 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/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/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/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/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/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/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/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/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/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/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index a48cd2b..c66ea5e 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(
@@ -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..d8420cd 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%
rename from packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
rename 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/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
index 5e432b9..f8d008e 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
@@ -37,6 +36,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
@@ -50,7 +50,6 @@
import com.android.credentialmanager.common.ui.ContainerCard
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 +287,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 +397,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 +570,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 +827,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,7 +845,7 @@
modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
)
}
- TYPE_PASSWORD_CREDENTIAL -> {
+ CredentialType.PASSWORD -> {
TextOnSurfaceVariant(
text = requestDisplayInfo.title,
style = MaterialTheme.typography.titleLarge,
@@ -859,7 +858,7 @@
modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
)
}
- else -> {
+ CredentialType.UNKNOWN -> {
if (requestDisplayInfo.subtitle != null) {
TextOnSurfaceVariant(
text = requestDisplayInfo.title,
@@ -917,8 +916,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/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/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/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..4b73e94 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
@@ -24,6 +24,7 @@
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import com.android.settingslib.spa.framework.compose.LocalNavController
+import com.android.settingslib.spa.framework.util.genEntryId
private const val INJECT_ENTRY_NAME = "INJECT"
private const val ROOT_ENTRY_NAME = "ROOT"
@@ -188,7 +189,7 @@
val page = fromPage ?: owner
val isEnabled = page.isEnabled()
return SettingsEntry(
- id = id(),
+ id = genEntryId(name, owner, fromPage, toPage),
name = name,
owner = owner,
displayName = displayName,
@@ -274,11 +275,6 @@
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)
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..14dc785 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
@@ -49,7 +49,7 @@
entryMap = mutableMapOf()
pageWithEntryMap = mutableMapOf()
- val nullPage = SettingsPage.createNull()
+ val nullPage = NullPageProvider.createSettingsPage()
val entryQueue = LinkedList<SettingsEntry>()
for (page in sppRepository.getAllRootPages()) {
val rootEntry =
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..c810648 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,20 @@
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
}
@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/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..730aa8f 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,22 +91,22 @@
).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()
}
@@ -115,21 +115,21 @@
fun testGetEntryPath() {
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 +140,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/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 2bdbb16..d222b98 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.
*
@@ -682,6 +692,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 17ef283..156993d 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.FLAG_ONGOING_SESSION;
import static android.media.RouteListingPreference.Item.FLAG_SUGGESTED_ROUTE;
+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_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;
@@ -194,29 +202,59 @@
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.SubText
- public int getDisableReason() {
+ public int getSelectionBehavior() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && mItem != null
- ? mItem.getSubText()
- : -1;
+ ? mItem.getSelectionBehavior() : SELECTION_BEHAVIOR_NONE;
}
/**
- * 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.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);
+ }
+
+ /**
* Checks if device is suggested device from application
*
* @return true if device is suggested device
@@ -513,7 +551,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_ROUTE) != 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/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..c966757
--- /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;
+ var 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..31038cd 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;
@@ -372,6 +375,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/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/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 46c604b..2c04630 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
+++ b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
@@ -25,9 +25,17 @@
"androidx.coordinatorlayout_coordinatorlayout",
"androidx.core_core",
"androidx.viewpager_viewpager",
- "SettingsLib",
+ "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 0b4d7cd..77412d9 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml
+++ b/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml
@@ -19,11 +19,12 @@
<uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS"/>
- <application>
+ <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 ecb2bf4..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
@@ -228,6 +228,13 @@
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(
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 c764d53..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
@@ -87,7 +87,7 @@
* (1.-sparkleRing) * in_fadeSparkle;
float rippleInsideAlpha = (1.-inside) * in_fadeFill;
- float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing * in_sparkle_strength;
+ float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a;
vec4 ripple = vec4(in_color.rgb, 1.0) * rippleAlpha;
return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
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 d110850..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)
@@ -91,6 +95,7 @@
open inner class DefaultClockFaceController(
override val view: AnimatableClockView,
+ val seedColor: Int?,
) : ClockFaceController {
// MAGENTA is a placeholder, and will be assigned correctly in initialize
@@ -105,6 +110,9 @@
}
init {
+ if (seedColor != null) {
+ currentColor = seedColor
+ }
view.setColors(currentColor, currentColor)
}
@@ -132,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)
@@ -152,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.
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/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
index 7727589..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,7 +71,11 @@
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)
@@ -99,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 */
@@ -167,3 +183,34 @@
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/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_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/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/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/config.xml b/packages/SystemUI/res/values/config.xml
index 034f145..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>
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 943844f..8d95164 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -247,6 +247,8 @@
<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] -->
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/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 7a094a7..ea079a9 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -48,6 +48,7 @@
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
@@ -96,6 +97,8 @@
if (regionSamplingEnabled) {
clock?.smallClock?.view?.addOnLayoutChangeListener(mLayoutChangedListener)
clock?.largeClock?.view?.addOnLayoutChangeListener(mLayoutChangedListener)
+ } else {
+ updateColors()
}
updateFontSizes()
updateTimeListeners()
@@ -271,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) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index afa9ef6..d23ea9e 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;
@@ -3219,6 +3219,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) {
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/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index c1c7f2d..fb65588 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -124,16 +124,7 @@
};
private final ScreenDecorationsLogger mLogger;
- private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() {
- @Override
- public void onFaceSensorLocationChanged() {
- mLogger.onSensorLocationChanged();
- if (mExecutor != null) {
- mExecutor.execute(
- () -> updateOverlayProviderViews(new Integer[]{mFaceScanningViewId}));
- }
- }
- };
+ private final AuthController mAuthController;
private DisplayTracker mDisplayTracker;
@VisibleForTesting
@@ -340,9 +331,22 @@
mFaceScanningFactory = faceScanningFactory;
mFaceScanningViewId = com.android.systemui.R.id.face_scanning_anim;
mLogger = logger;
- authController.addCallback(mAuthControllerCallback);
+ 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) {
@@ -353,6 +357,7 @@
mExecutor = mThreadFactory.buildDelayableExecutorOnHandler(mHandler);
mExecutor.execute(this::startOnScreenDecorationsThread);
mDotViewController.setUiExecutor(mExecutor);
+ mAuthController.addCallback(mAuthControllerCallback);
}
private boolean isPrivacyDotEnabled() {
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/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/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 64d5518..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();
@@ -713,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
@@ -757,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);
});
}
}
@@ -838,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;
@@ -880,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 45ca24d..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. */
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/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/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/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 caa0e96..b5a1f45 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -27,6 +27,7 @@
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
@@ -67,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
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index cee58f9..43fb39f 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -265,11 +265,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 =
@@ -310,9 +309,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
@@ -353,17 +350,17 @@
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 = unreleasedFlag(915, "media_resume_progress")
+ val MEDIA_RESUME_PROGRESS = releasedFlag(915, "media_resume_progress")
// TODO(b/267166152) : Tracking Bug
- val MEDIA_RETAIN_RECOMMENDATIONS = unreleasedFlag(916, "media_retain_recommendations")
+ val MEDIA_RETAIN_RECOMMENDATIONS = releasedFlag(916, "media_retain_recommendations")
// 1000 - dock
val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")
@@ -495,7 +492,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
@@ -519,6 +516,9 @@
// 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")
@@ -542,6 +542,8 @@
// 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
@@ -550,7 +552,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)
@@ -572,10 +574,14 @@
@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
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/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/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/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index e57b169..9f5d372 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -16,6 +16,11 @@
package com.android.systemui.media.dialog;
+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;
@@ -23,7 +28,6 @@
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;
@@ -194,16 +198,29 @@
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
+ && mController.isSubStatusSupported()
+ && mController.isAdvancedLayoutSupported() && device.hasSubtext()) {
+ boolean isActiveWithOngoingSession =
+ device.hasOngoingSession() && currentlyConnected;
+ if (isActiveWithOngoingSession) {
+ //Selected device which has ongoing session, disable seekbar since we
+ //only allow volume control on Host
+ mSeekBar.setVolume(0);
+ disableSeekBar();
+ mCurrentActivePosition = position;
+ }
setUpDeviceIcon(device);
- mSubTitleText.setText(
- Api34Impl.composeDisabledReason(device.getDisableReason(), mContext));
- updateConnectionFailedStatusIcon();
- updateFullItemClickListener(null);
- setTwoLineLayout(device, false /* bFocused */, false /* showSeekBar */,
+ mSubTitleText.setText(device.getSubtextString());
+ Drawable deviceStatusIcon =
+ Api34Impl.getDeviceStatusIconBasedOnSelectionBehavior(device, mContext);
+ if (deviceStatusIcon != null) {
+ updateDeviceStatusIcon(deviceStatusIcon);
+ }
+ updateClickActionBasedOnSelectionBehavior(device);
+ setTwoLineLayout(device, isActiveWithOngoingSession /* bFocused */,
+ isActiveWithOngoingSession /* showSeekBar */,
false /* showProgressBar */, true /* showSubtitle */,
- true /* showStatus */);
+ deviceStatusIcon != null /* showStatus */);
} else if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) {
setUpDeviceIcon(device);
updateConnectionFailedStatusIcon();
@@ -220,6 +237,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 +253,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 +285,7 @@
initSeekbar(device, isCurrentSeekbarInvisible);
}
} else if (isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
+ //groupable device
setUpDeviceIcon(device);
updateGroupableCheckBox(false, true, device);
if (mController.isAdvancedLayoutSupported()) {
@@ -280,7 +300,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 +317,23 @@
ColorStateList(states, colors));
}
+ private void updateClickActionBasedOnSelectionBehavior(MediaDevice device) {
+ View.OnClickListener clickListener = Api34Impl.getClickListenerBasedOnSelectionBehavior(
+ device, mController, v -> onItemClick(v, device));
+ updateFullItemClickListener(clickListener);
+ }
+
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 +447,30 @@
@RequiresApi(34)
private static class Api34Impl {
@DoNotInline
- static String composeDisabledReason(
- @RouteListingPreference.Item.SubText int reason, Context context) {
- switch(reason) {
- case SUBTEXT_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 null;
+ }
+
+ @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..b5e829e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -278,12 +278,29 @@
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(
+ showSeekBar ? mController.getColorConnectedItemBackground()
+ : mController.getColorItemBackground(),
+ PorterDuff.Mode.SRC_IN));
+ ViewGroup.MarginLayoutParams params =
+ (ViewGroup.MarginLayoutParams) mItemLayout.getLayoutParams();
+ params.rightMargin = 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);
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..d234dff 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);
}
}
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/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..ae37fb6 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,15 @@
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.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 +48,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
+private const val MIN_DURATION_CANCELLED_ANIMATION = 200L
+private const val MIN_DURATION_COMMITTED_ANIMATION = 200L
+private const val MIN_DURATION_ACTIVE_ANIMATION = 300L
+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
+private 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
+private 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
-
-/**
- * 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)
+private const val DEBUG = false
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 {
+ 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,23 +92,23 @@
* 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
@@ -138,17 +119,15 @@
private 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 +141,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
@@ -192,17 +181,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 +188,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 +251,7 @@
updateConfiguration()
updateArrowDirection(configurationController.isLayoutRtl)
updateArrowState(GestureState.GONE, force = true)
- updateRestingArrowDimens(animated = false, currentState)
+ updateRestingArrowDimens()
configurationController.addCallback(configurationListener)
}
@@ -297,22 +268,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 +332,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 +350,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 +363,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 +422,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 +470,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 +493,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 +578,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 +590,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 +651,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 +792,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 +935,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 +978,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/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 52dd769..d423048 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
@@ -535,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/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 2ac7f7a..6ee0a46 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -6135,6 +6135,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 +6227,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/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index 4d7496d..fe76c7d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -35,6 +35,7 @@
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
@@ -54,11 +55,13 @@
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
@@ -81,6 +84,7 @@
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,
@@ -165,6 +169,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 {
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/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/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/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 307787b..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;
@@ -373,6 +374,7 @@
@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/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/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index fc11148..4cf5a4b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -34,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;
@@ -104,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;
@@ -119,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;
@@ -161,6 +167,8 @@
private PrivacyDotViewController.ShowingListener mPrivacyDotShowingListener;
@Mock
private CutoutDecorProviderFactory mCutoutFactory;
+ @Captor
+ private ArgumentCaptor<AuthController.Callback> mAuthControllerCallback;
private List<DecorProvider> mMockCutoutList;
@Before
@@ -169,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);
@@ -1166,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/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/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/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/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/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/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index f40e3a6..003af80f 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,6 +16,10 @@
package com.android.systemui.media.dialog;
+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,17 @@
}
@Test
- public void subStatusSupported_onBindViewHolder_bindFailedStateDevice_verifyView() {
+ 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(SUBTEXT_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 +454,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.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()).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 ef10e40..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
@@ -624,7 +624,7 @@
}
@Test
- fun commandQueueCallback_receiverSucceededThenReceiverTriggered_invalidTransitionLogged() {
+ fun commandQueueCallback_receiverSucceededThenThisDeviceSucceeded_invalidTransitionLogged() {
displayReceiverTriggered()
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
@@ -634,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
)
@@ -644,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,
@@ -654,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/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/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/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/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
index 0a576de..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
@@ -223,6 +227,7 @@
statusBarStateController,
deviceProvisionedController,
keyguardBypassController,
+ keyguardUpdateMonitor,
execution,
executor,
bgExecutor,
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/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/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/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/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 70ee3b8..a4deed3 100644
--- a/services/api/current.txt
+++ b/services/api/current.txt
@@ -227,8 +227,9 @@
package com.android.server.security {
- public final class FileIntegrityLocal {
- method public static void setUpFsVerity(@NonNull String) throws java.io.IOException;
+ 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;
}
}
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/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/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 423a090..b272502 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -13588,8 +13588,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.
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/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/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index f22624c..12784bf 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,
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 d888c81..c01424d 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -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
diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java
index 35df3ee..c0ea561 100644
--- a/services/core/java/com/android/server/backup/SystemBackupAgent.java
+++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java
@@ -86,7 +86,8 @@
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);
+ ACCOUNT_MANAGER_HELPER, USAGE_STATS_HELPER, PREFERRED_HELPER, SHORTCUT_MANAGER_HELPER
+ );
private int mUserId = UserHandle.USER_SYSTEM;
@@ -101,7 +102,7 @@
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(SHORTCUT_MANAGER_HELPER, new ShortcutBackupHelper(mUserId));
addHelper(ACCOUNT_MANAGER_HELPER, new AccountManagerBackupHelper(mUserId));
addHelper(SLICES_HELPER, new SliceBackupHelper(this));
addHelper(PEOPLE_HELPER, new PeopleBackupHelper(mUserId));
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/DeviceStateNotificationController.java b/services/core/java/com/android/server/devicestate/DeviceStateNotificationController.java
index 2f14998..9008740 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateNotificationController.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateNotificationController.java
@@ -16,6 +16,7 @@
package com.android.server.devicestate;
+import android.annotation.DrawableRes;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Notification;
@@ -103,7 +104,7 @@
showNotification(
info.name, info.activeNotificationTitle,
String.format(info.activeNotificationContent, requesterApplicationLabel),
- true /* ongoing */
+ true /* ongoing */, R.drawable.ic_dual_screen
);
} else {
Slog.e(TAG, "Cannot determine the requesting app name when showing state active "
@@ -124,7 +125,8 @@
}
showNotification(
info.name, info.thermalCriticalNotificationTitle,
- info.thermalCriticalNotificationContent, false /* ongoing */
+ info.thermalCriticalNotificationContent, false /* ongoing */,
+ R.drawable.ic_thermostat
);
}
@@ -158,11 +160,12 @@
* @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) {
+ @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(R.drawable.ic_lock) // TODO(b/266833171) update icons when available.
+ .setSmallIcon(iconRes)
.setContentTitle(title)
.setContentText(content)
.setSubText(name)
diff --git a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
index 0c164b4..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,16 +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,
- d.getBrightnessThrottlingMapId());
+ d.getBrightnessThrottlingMapId(),
+ leadDisplayId);
if (FRONT_STRING.equals(d.getPosition())) {
display.setPosition(POSITION_FRONT);
@@ -132,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/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 2af995b..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;
@@ -503,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)
@@ -515,7 +517,6 @@
*
* Spline interpolation is used to determine the auto-brightness
* backlight values for lux levels between these control points.
- *
*/
private float[] mBrightnessLevelsLux;
@@ -619,6 +620,10 @@
*/
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
* vendors to tune the curves to have exact same brightness for different refresh rate. So
@@ -1354,6 +1359,23 @@
}
/**
+ * @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;
+ }
+
+ /**
* @return An array of lower display brightness thresholds. This, in combination with lower
* ambient brightness thresholds help define buckets in which the refresh rate switching is not
* allowed
@@ -1500,6 +1522,7 @@
+ ", mDefaultHighBlockingZoneRefreshRate= " + mDefaultHighBlockingZoneRefreshRate
+ ", mDefaultPeakRefreshRate= " + mDefaultPeakRefreshRate
+ ", mDefaultRefreshRate= " + mDefaultRefreshRate
+ + ", mRefreshRateZoneProfiles= " + mRefreshRateZoneProfiles
+ ", mLowDisplayBrightnessThresholds= "
+ Arrays.toString(mLowDisplayBrightnessThresholds)
+ ", mLowAmbientBrightnessThresholds= "
@@ -1828,6 +1851,7 @@
loadDefaultRefreshRate(refreshRateConfigs);
loadLowerRefreshRateBlockingZones(lowerBlockingZoneConfig);
loadHigherRefreshRateBlockingZones(higherBlockingZoneConfig);
+ loadRefreshRateZoneProfiles(refreshRateConfigs);
}
private void loadPeakDefaultRefreshRate(RefreshRateConfigs refreshRateConfigs) {
@@ -1850,6 +1874,21 @@
}
}
+ /** 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.
*/
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 3759a8b..70069c6 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -245,7 +245,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 +647,7 @@
updateSettingsLocked();
updateUserDisabledHdrTypesFromSettingsLocked();
updateUserPreferredDisplayModeSettingsLocked();
+ updateHdrConversionModeSettingsLocked();
}
mDisplayModeDirector.setDesiredDisplayModeSpecsListener(
@@ -1806,6 +1807,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 +1988,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 +2011,7 @@
private @Display.HdrCapabilities.HdrType int[] getSupportedHdrOutputTypesInternal() {
if (mSupportedHdrOutputType == null) {
- mSupportedHdrOutputType = DisplayControl.getSupportedHdrOutputTypes();
+ mSupportedHdrOutputType = mInjector.getSupportedHdrOutputTypes();
}
return mSupportedHdrOutputType;
}
@@ -2604,6 +2631,10 @@
}
}
+ if (mHdrConversionMode != null) {
+ pw.println(" mHdrConversionMode=" + mHdrConversionMode);
+ }
+
pw.println();
final int displayStateCount = mDisplayStates.size();
pw.println("Display States: size=" + displayStateCount);
@@ -2712,6 +2743,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 5f6660b..29caefb 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(),
@@ -1186,26 +1186,29 @@
// rest of low priority voters. It votes [0, max(PEAK, MIN)]
public static final int PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE = 7;
+ // For concurrent displays we want to limit refresh rate on all displays
+ public static final int PRIORITY_LAYOUT_LIMITED_FRAME_RATE = 8;
+
// 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 = 9;
// 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 = 10;
// 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 = 11;
// 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 = 12;
// 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 = 13;
// 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.
@@ -1657,10 +1660,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 +1694,9 @@
@Override
public void onDisplayAdded(int displayId) {
- updateDisplayModes(displayId);
+ DisplayInfo displayInfo = getDisplayInfo(displayId);
+ updateDisplayModes(displayId, displayInfo);
+ updateLayoutLimitedFrameRate(displayId, displayInfo);
}
@Override
@@ -1698,23 +1705,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)) {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 40eec33..b58d907 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1011,7 +1011,7 @@
}
mBrightnessSettingListener = brightnessValue -> {
Message msg = mHandler.obtainMessage(MSG_UPDATE_BRIGHTNESS, brightnessValue);
- mHandler.sendMessage(msg);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
};
mBrightnessSetting.registerListener(mBrightnessSettingListener);
@@ -1040,7 +1040,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,
@@ -1065,7 +1065,7 @@
mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold();
float ambientBrighteningMinThreshold =
mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold();
- HysteresisLevels ambientBrightnessThresholds = new HysteresisLevels(
+ HysteresisLevels ambientBrightnessThresholds = mInjector.getHysteresisLevels(
ambientBrighteningThresholds, ambientDarkeningThresholds,
ambientBrighteningLevels, ambientDarkeningLevels, ambientDarkeningMinThreshold,
ambientBrighteningMinThreshold);
@@ -1083,7 +1083,7 @@
mDisplayDeviceConfig.getScreenDarkeningMinThreshold();
float screenBrighteningMinThreshold =
mDisplayDeviceConfig.getScreenBrighteningMinThreshold();
- HysteresisLevels screenBrightnessThresholds = new HysteresisLevels(
+ HysteresisLevels screenBrightnessThresholds = mInjector.getHysteresisLevels(
screenBrighteningThresholds, screenDarkeningThresholds,
screenBrighteningLevels, screenDarkeningLevels, screenDarkeningMinThreshold,
screenBrighteningMinThreshold, true);
@@ -1101,7 +1101,7 @@
mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle();
float[] ambientDarkeningLevelsIdle =
mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle();
- HysteresisLevels ambientBrightnessThresholdsIdle = new HysteresisLevels(
+ HysteresisLevels ambientBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
ambientBrighteningThresholdsIdle, ambientDarkeningThresholdsIdle,
ambientBrighteningLevelsIdle, ambientDarkeningLevelsIdle,
ambientDarkeningMinThresholdIdle, ambientBrighteningMinThresholdIdle);
@@ -1119,7 +1119,7 @@
mDisplayDeviceConfig.getScreenBrighteningLevelsIdle();
float[] screenDarkeningLevelsIdle =
mDisplayDeviceConfig.getScreenDarkeningLevelsIdle();
- HysteresisLevels screenBrightnessThresholdsIdle = new HysteresisLevels(
+ HysteresisLevels screenBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
screenBrighteningThresholdsIdle, screenDarkeningThresholdsIdle,
screenBrighteningLevelsIdle, screenDarkeningLevelsIdle,
screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
@@ -1155,8 +1155,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,
@@ -1257,7 +1257,7 @@
public void onAnimationEnd() {
sendUpdatePowerState();
Message msg = mHandler.obtainMessage(MSG_BRIGHTNESS_RAMP_DONE);
- mHandler.sendMessage(msg);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
}
};
@@ -2949,7 +2949,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);
}
}
}
@@ -3105,7 +3106,7 @@
@Override
public void onScreenOn() {
Message msg = mHandler.obtainMessage(MSG_SCREEN_ON_UNBLOCKED, this);
- mHandler.sendMessage(msg);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
}
}
@@ -3113,7 +3114,7 @@
@Override
public void onScreenOff() {
Message msg = mHandler.obtainMessage(MSG_SCREEN_OFF_UNBLOCKED, this);
- mHandler.sendMessage(msg);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
}
}
@@ -3195,6 +3196,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 6092ad7..23ef680 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -850,7 +850,7 @@
BrightnessSetting.BrightnessSettingListener brightnessSettingListener = brightnessValue -> {
Message msg = mHandler.obtainMessage(MSG_UPDATE_BRIGHTNESS, brightnessValue);
- mHandler.sendMessage(msg);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
};
mDisplayBrightnessController
.registerBrightnessSettingChangeListener(brightnessSettingListener);
@@ -880,7 +880,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,
@@ -905,7 +905,7 @@
mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold();
float ambientBrighteningMinThreshold =
mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold();
- HysteresisLevels ambientBrightnessThresholds = new HysteresisLevels(
+ HysteresisLevels ambientBrightnessThresholds = mInjector.getHysteresisLevels(
ambientBrighteningThresholds, ambientDarkeningThresholds,
ambientBrighteningLevels, ambientDarkeningLevels, ambientDarkeningMinThreshold,
ambientBrighteningMinThreshold);
@@ -923,7 +923,7 @@
mDisplayDeviceConfig.getScreenDarkeningMinThreshold();
float screenBrighteningMinThreshold =
mDisplayDeviceConfig.getScreenBrighteningMinThreshold();
- HysteresisLevels screenBrightnessThresholds = new HysteresisLevels(
+ HysteresisLevels screenBrightnessThresholds = mInjector.getHysteresisLevels(
screenBrighteningThresholds, screenDarkeningThresholds,
screenBrighteningLevels, screenDarkeningLevels, screenDarkeningMinThreshold,
screenBrighteningMinThreshold, true);
@@ -941,7 +941,7 @@
mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle();
float[] ambientDarkeningLevelsIdle =
mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle();
- HysteresisLevels ambientBrightnessThresholdsIdle = new HysteresisLevels(
+ HysteresisLevels ambientBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
ambientBrighteningThresholdsIdle, ambientDarkeningThresholdsIdle,
ambientBrighteningLevelsIdle, ambientDarkeningLevelsIdle,
ambientDarkeningMinThresholdIdle, ambientBrighteningMinThresholdIdle);
@@ -959,7 +959,7 @@
mDisplayDeviceConfig.getScreenBrighteningLevelsIdle();
float[] screenDarkeningLevelsIdle =
mDisplayDeviceConfig.getScreenDarkeningLevelsIdle();
- HysteresisLevels screenBrightnessThresholdsIdle = new HysteresisLevels(
+ HysteresisLevels screenBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
screenBrighteningThresholdsIdle, screenDarkeningThresholdsIdle,
screenBrighteningLevelsIdle, screenDarkeningLevelsIdle,
screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
@@ -995,8 +995,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,
@@ -1094,7 +1094,7 @@
public void onAnimationEnd() {
sendUpdatePowerState();
Message msg = mHandler.obtainMessage(MSG_BRIGHTNESS_RAMP_DONE);
- mHandler.sendMessage(msg);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
}
};
@@ -2458,7 +2458,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);
}
}
}
@@ -2589,7 +2590,7 @@
@Override
public void onScreenOn() {
Message msg = mHandler.obtainMessage(MSG_SCREEN_ON_UNBLOCKED, this);
- mHandler.sendMessage(msg);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
}
}
@@ -2597,7 +2598,7 @@
@Override
public void onScreenOff() {
Message msg = mHandler.obtainMessage(MSG_SCREEN_OFF_UNBLOCKED, this);
- mHandler.sendMessage(msg);
+ mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
}
}
@@ -2671,6 +2672,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/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index bad4b3c..473317c 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,13 @@
// 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 mPosition = Layout.Display.POSITION_UNKNOWN;
+
/**
* 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
@@ -170,6 +181,13 @@
mBrightnessThrottlingDataId = DisplayDeviceConfig.DEFAULT_BRIGHTNESS_THROTTLING_DATA_ID;
}
+ public void setPositionLocked(int position) {
+ mPosition = position;
+ }
+ public int getPositionLocked() {
+ return mPosition;
+ }
+
/**
* Gets the logical display id of this logical display.
*
@@ -424,6 +442,11 @@
mBaseDisplayInfo.roundedCorners = deviceInfo.roundedCorners;
mBaseDisplayInfo.installOrientation = deviceInfo.installOrientation;
mBaseDisplayInfo.displayShape = deviceInfo.displayShape;
+
+ if (mPosition == Layout.Display.POSITION_REAR) {
+ mBaseDisplayInfo.flags |= Display.FLAG_REAR;
+ }
+
mPrimaryDisplayDeviceInfo = deviceInfo;
mInfo.set(null);
}
@@ -809,11 +832,33 @@
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 getLeadDisplayLocked() {
+ 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=" + mPosition);
pw.println("mHasContent=" + mHasContent);
pw.println("mDesiredDisplayModeSpecs={" + mDesiredDisplayModeSpecs + "}");
pw.println("mRequestedColorMode=" + mRequestedColorMode);
@@ -827,6 +872,7 @@
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 a2ca1c0..56c9056 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;
@@ -639,7 +641,7 @@
&& !nextDeviceInfo.address.equals(deviceInfo.address)) {
layout.createDisplayLocked(nextDeviceInfo.address,
/* isDefault= */ true, /* isEnabled= */ true, mIdProducer,
- /* brightnessThrottlingMapId= */ null);
+ /* brightnessThrottlingMapId= */ null, DEFAULT_DISPLAY);
applyLayoutLocked();
return;
}
@@ -977,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(
@@ -989,15 +992,23 @@
newDisplay.swapDisplaysLocked(oldDisplay);
}
+ newDisplay.setPositionLocked(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
@@ -1068,7 +1079,7 @@
}
final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
layout.createDisplayLocked(info.address, /* isDefault= */ true, /* isEnabled= */ true,
- mIdProducer, /* brightnessThrottlingMapId= */ null);
+ mIdProducer, /* brightnessThrottlingMapId= */ null, NO_LEAD_DISPLAY);
}
private int assignLayerStackLocked(int displayId) {
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 5de9f0b..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;
@@ -37,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);
/**
@@ -73,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, String brightnessThrottlingMapId) {
+ 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;
@@ -95,7 +124,7 @@
// same logical display ID.
final int logicalDisplayId = idProducer.getId(isDefault);
final Display display = new Display(address, logicalDisplayId, isEnabled,
- brightnessThrottlingMapId);
+ brightnessThrottlingMapId, position, leadDisplayId);
mDisplays.add(display);
return display;
@@ -203,13 +232,27 @@
@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) {
+ 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
@@ -220,6 +263,8 @@
+ ", addr: " + mAddress
+ ((mPosition == POSITION_UNKNOWN) ? "" : ", position: " + mPosition)
+ ", brightnessThrottlingMapId: " + mBrightnessThrottlingMapId
+ + ", mRefreshRateZoneId: " + mRefreshRateZoneId
+ + ", mLeadDisplayId: " + mLeadDisplayId
+ "}";
}
@@ -236,7 +281,9 @@
&& otherDisplay.mLogicalDisplayId == this.mLogicalDisplayId
&& this.mAddress.equals(otherDisplay.mAddress)
&& Objects.equals(mBrightnessThrottlingMapId,
- otherDisplay.mBrightnessThrottlingMapId);
+ otherDisplay.mBrightnessThrottlingMapId)
+ && Objects.equals(otherDisplay.mRefreshRateZoneId, this.mRefreshRateZoneId)
+ && this.mLeadDisplayId == otherDisplay.mLeadDisplayId;
}
@Override
@@ -247,6 +294,8 @@
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;
}
@@ -262,6 +311,20 @@
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;
}
@@ -272,5 +335,32 @@
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/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index ebc18bc..c7f4a49 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -29,6 +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.os.UserHandle.USER_SYSTEM;
import static android.provider.DeviceConfig.NAMESPACE_AUTO_PIN_CONFIRMATION;
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
@@ -70,6 +71,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,6 +112,7 @@
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;
@@ -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;
@@ -1991,7 +2029,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);
}
}
@@ -2584,43 +2622,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 +2742,7 @@
LockscreenCredential.createNone(), sp, userId);
setCurrentLskfBasedProtectorId(protectorId, userId);
setUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey());
- onSyntheticPasswordKnown(userId, sp);
+ onSyntheticPasswordCreated(userId, sp);
return sp;
}
}
@@ -2702,7 +2809,7 @@
}
mStrongAuth.reportSuccessfulStrongAuthUnlock(userId);
- onSyntheticPasswordKnown(userId, sp);
+ onSyntheticPasswordUnlocked(userId, sp);
}
private void setDeviceUnlockedForUser(int userId) {
@@ -2990,7 +3097,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 ea000a0..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}.
@@ -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/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 57ae934..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;
@@ -576,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")
@@ -1201,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();
}
@@ -6689,6 +6696,16 @@
(userId == UserHandle.USER_ALL) ? USER_SYSTEM : userId);
Notification.addFieldsFromContext(ai, notification);
+ // 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) {
@@ -6707,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 =
@@ -6750,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;
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 0d5392b..1ed5999 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -101,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;
@@ -687,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) {
}
}
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 6546f6a..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;
@@ -1314,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) {
}
});
@@ -1477,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) {
}
}
@@ -1502,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 5d41b4c..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;
@@ -4948,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);
}
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 9c91879..7e7205d 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -120,7 +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.FileIntegrityLocal;
+import com.android.server.security.FileIntegrity;
import com.android.server.utils.Slogf;
import com.android.server.utils.Snappable;
import com.android.server.utils.SnapshotCache;
@@ -2714,8 +2714,8 @@
}
try {
- FileIntegrityLocal.setUpFsVerity(mSettingsFilename.getAbsolutePath());
- FileIntegrityLocal.setUpFsVerity(mSettingsReserveCopyFilename.getAbsolutePath());
+ FileIntegrity.setUpFsVerity(mSettingsFilename);
+ FileIntegrity.setUpFsVerity(mSettingsReserveCopyFilename);
} catch (IOException e) {
Slog.e(TAG, "Failed to verity-protect settings", e);
}
diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java
index 26a990c..a9edce1 100644
--- a/services/core/java/com/android/server/pm/UserManagerInternal.java
+++ b/services/core/java/com/android/server/pm/UserManagerInternal.java
@@ -553,11 +553,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/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/FileIntegrityLocal.java b/services/core/java/com/android/server/security/FileIntegrity.java
similarity index 63%
rename from services/core/java/com/android/server/security/FileIntegrityLocal.java
rename to services/core/java/com/android/server/security/FileIntegrity.java
index 8c7219b..7b87d99 100644
--- a/services/core/java/com/android/server/security/FileIntegrityLocal.java
+++ b/services/core/java/com/android/server/security/FileIntegrity.java
@@ -18,19 +18,22 @@
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 FileIntegrityLocal {
- private FileIntegrityLocal() {}
+public final class FileIntegrity {
+ private FileIntegrity() {}
/**
* Enables fs-verity, if supported by the filesystem.
@@ -38,7 +41,18 @@
* @hide
*/
@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
- public static void setUpFsVerity(@NonNull String filePath) throws IOException {
- VerityUtils.setUpFsverity(filePath);
+ 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/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/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..6fc9161 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -46,7 +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.InsetsSource.ID_IME;
import static android.view.InsetsState.ITYPE_LEFT_GESTURES;
import static android.view.InsetsState.ITYPE_RIGHT_GESTURES;
import static android.view.Surface.ROTATION_0;
@@ -120,12 +120,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;
@@ -293,6 +291,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;
/**
@@ -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) -> {
@@ -2088,12 +2095,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 +2165,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 +3328,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 +3383,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 +3550,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()) {
@@ -4038,7 +4037,7 @@
final int imePid = mInputMethodWindow.mSession.mPid;
mAtmService.onImeWindowSetOnDisplayArea(imePid, mImeWindowsContainer);
}
- mInsetsStateController.getSourceProvider(ITYPE_IME).setWindowContainer(win,
+ mInsetsStateController.getSourceProvider(ID_IME).setWindowContainer(win,
mDisplayPolicy.getImeSourceFrameProvider(), null);
computeImeTarget(true /* updateImeTarget */);
updateImeControlTarget();
@@ -4287,7 +4286,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 +4513,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 +4933,7 @@
mInsetsStateController.getImeSourceProvider().checkShowImePostLayout();
mLastHasContent = mTmpApplySurfaceChangesTransactionState.displayHasContent;
- if (!mWmService.mDisplayFrozen) {
+ if (!mWmService.mDisplayFrozen && !mDisplayRotation.isRotatingSeamlessly()) {
mWmService.mDisplayManagerInternal.setDisplayProperties(mDisplayId,
mLastHasContent,
mTmpApplySurfaceChangesTransactionState.preferredRefreshRate,
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..e87680a 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1741,21 +1741,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
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/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/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 1df534f..bd82113 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -25,7 +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.InsetsSource.ID_IME;
import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
import static android.view.InsetsState.ITYPE_STATUS_BAR;
import static android.view.SyncRtSurfaceTransactionApplier.applyParams;
@@ -315,12 +315,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;
}
@@ -340,8 +342,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);
}
@@ -400,7 +401,7 @@
// 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());
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index 49eaea2..f5af292 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;
@@ -541,7 +541,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;
}
}
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 455cd48..a3f62b2 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -17,16 +17,19 @@
package com.android.server.wm;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
+import static android.view.InsetsSource.ID_IME;
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.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,7 +37,7 @@
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.SparseArray;
-import android.view.InsetsSource;
+import android.util.proto.ProtoOutputStream;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.InsetsState.InternalInsetsType;
@@ -93,13 +96,11 @@
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);
- };
+ mSourceProviderFunc = id -> (id == ID_IME)
+ ? new ImeInsetsSourceProvider(mState.getOrCreateSource(
+ id, ime()), this, mDisplayContent)
+ : new WindowContainerInsetsSourceProvider(mState.getOrCreateSource(
+ id, InsetsState.toPublicType(id)), this, mDisplayContent);
}
InsetsState getRawInsetsState() {
@@ -124,14 +125,14 @@
}
/**
- * @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 getSourceProvider(int id) {
+ return mProviders.computeIfAbsent(id, mSourceProviderFunc);
}
ImeInsetsSourceProvider getImeSourceProvider() {
- return (ImeInsetsSourceProvider) getSourceProvider(ITYPE_IME);
+ return (ImeInsetsSourceProvider) getSourceProvider(ID_IME);
}
/**
@@ -216,7 +217,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);
+ onControlChanged(ID_IME, target);
ProtoLog.d(WM_DEBUG_IME, "onImeControlTargetChanged %s",
target != null ? target.getWindow() : "null");
notifyPendingInsetsControlChanged();
@@ -386,4 +387,15 @@
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..f38de6e 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -234,6 +234,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;
@@ -440,6 +444,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 +1285,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 +1304,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 +1369,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;
}
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/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 6eab745..b46a720 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;
@@ -365,6 +366,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;
@@ -5461,10 +5463,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 +6087,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 +6107,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 +7841,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 +7857,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/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 31f30af..703cb8a4 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;
@@ -1531,6 +1530,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 +1699,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;
@@ -1882,8 +1883,8 @@
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) {
+ final InsetsSource source = mProvidedInsetsSources.valueAt(i);
+ if (source.getType() == WindowInsets.Type.navigationBars()) {
return true;
}
}
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/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 ab1badb..ef5aa60 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -477,6 +477,10 @@
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"/>
@@ -487,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 3e1db4c..ed9f959 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -194,10 +194,12 @@
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 {
@@ -208,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();
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 78c9a54..45e10a8 100644
--- a/services/core/xsd/display-layout-config/display-layout-config.xsd
+++ b/services/core/xsd/display-layout-config/display-layout-config.xsd
@@ -55,5 +55,6 @@
</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 6a28d8a..2c16c37 100644
--- a/services/core/xsd/display-layout-config/schema/current.txt
+++ b/services/core/xsd/display-layout-config/schema/current.txt
@@ -6,6 +6,7 @@
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);
@@ -13,6 +14,7 @@
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/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index bb26fa9..ff72ed7 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;
@@ -270,10 +268,10 @@
// All requested credential descriptions based on the given request.
Set<String> requestedCredentialDescriptions =
- request.getGetCredentialOptions().stream().map(
- getCredentialOption -> getCredentialOption
+ request.getCredentialOptions().stream().map(
+ credentialOption -> credentialOption
.getCredentialRetrievalData()
- .getString(GetCredentialOption
+ .getString(CredentialOption
.FLATTENED_REQUEST))
.collect(Collectors.toSet());
@@ -344,11 +342,11 @@
// Initiate all provider sessions
List<ProviderSession> providerSessions =
- initiateProviderSessions(
- session,
- request.getGetCredentialOptions().stream()
- .map(GetCredentialOption::getType)
- .collect(Collectors.toList()));
+ initiateProviderSessions(
+ session,
+ request.getCredentialOptions().stream()
+ .map(CredentialOption::getType)
+ .collect(Collectors.toList()));
if (providerSessions.isEmpty()) {
try {
@@ -364,11 +362,7 @@
}
// Iterate over all provider sessions and invoke the request
- providerSessions.forEach(
- providerGetSession -> providerGetSession
- .getRemoteCredentialService().onBeginGetCredential(
- (BeginGetCredentialRequest) providerGetSession.getProviderRequest(),
- /*callback=*/providerGetSession));
+ providerSessions.forEach(ProviderSession::invokeSession);
return cancelTransport;
}
@@ -412,12 +406,7 @@
// Iterate over all provider sessions and invoke the request
providerSessions.forEach(
- providerCreateSession -> providerCreateSession
- .getRemoteCredentialService()
- .onCreateCredential(
- (BeginCreateCredentialRequest)
- providerCreateSession.getProviderRequest(),
- /* callback= */ providerCreateSession));
+ ProviderSession::invokeSession);
return cancelTransport;
}
@@ -530,14 +519,7 @@
// Iterate over all provider sessions and invoke the request
providerSessions.forEach(
- providerClearSession -> {
- providerClearSession
- .getRemoteCredentialService()
- .onClearCredentialState(
- (android.service.credentials.ClearCredentialStateRequest)
- providerClearSession.getProviderRequest(),
- /* callback= */ providerClearSession);
- });
+ ProviderSession::invokeSession);
return cancelTransport;
}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java
index 48e35b2..b112649 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() {
+ this.mRemoteCredentialService.onClearCredentialState(
+ this.getProviderRequest(),
+ /*callback=*/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..cc5a8ab 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
@@ -197,6 +197,13 @@
}
}
+ @Override
+ protected void invokeSession() {
+ this.mRemoteCredentialService.onCreateCredential(
+ this.getProviderRequest(),
+ /*callback=*/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..dec3432 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;
@@ -107,7 +107,7 @@
) {
return new BeginGetCredentialRequest.Builder(callingAppInfo)
.setBeginGetCredentialOptions(
- filteredRequest.getGetCredentialOptions().stream().map(
+ filteredRequest.getCredentialOptions().stream().map(
option -> {
return new BeginGetCredentialOption(
option.getType(),
@@ -121,8 +121,8 @@
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,7 +135,7 @@
if (!filteredOptions.isEmpty()) {
return new android.credentials.GetCredentialRequest
.Builder(clientRequest.getData())
- .setGetCredentialOptions(
+ .setCredentialOptions(
filteredOptions).build();
}
Log.i(TAG, "In createProviderRequest - returning null");
@@ -230,6 +230,13 @@
}
}
+ @Override
+ protected void invokeSession() {
+ this.mRemoteCredentialService.onBeginGetCredential(
+ this.getProviderRequest(),
+ /*callback=*/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
diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java
index 678c752..8e0d6f8 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java
@@ -208,4 +208,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/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..cc6c36e
--- /dev/null
+++ b/services/java/com/android/server/HsumBootUserInitializer.java
@@ -0,0 +1,190 @@
+/*
+ * 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.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;
+
+ /** 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())");
+
+ // TODO(b/204091126): in the long term, we need to decide who's reponsible for that,
+ // this class or the setup wizard app
+ provisionHeadlessSystemUser();
+
+ 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) {
+ 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.");
+ }
+ }
+
+ /* 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/SystemServer.java b/services/java/com/android/server/SystemServer.java
index a15c6d2..d22be9e 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -75,7 +75,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 +2693,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();
@@ -2961,10 +2972,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/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/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/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/display/DisplayPowerController2Test.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
index 57e873d..5ca01ee 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
@@ -19,13 +19,14 @@
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.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
@@ -41,6 +42,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;
@@ -55,6 +57,7 @@
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;
@@ -77,13 +80,18 @@
public final class DisplayPowerController2Test {
private static final String UNIQUE_DISPLAY_ID = "unique_id_test123";
private static final int DISPLAY_ID = Display.DEFAULT_DISPLAY;
+ private static final int FOLLOWER_DISPLAY_ID = Display.DEFAULT_DISPLAY + 1;
private MockitoSession mSession;
private OffsettableClock mClock;
private TestLooper mTestLooper;
private Handler mHandler;
private DisplayPowerController2.Injector mInjector;
+ private DisplayPowerController2.Injector mFollowerInjector;
private Context mContextSpy;
+ private DisplayPowerController2 mDpc;
+ private DisplayPowerController2 mFollowerDpc;
+ private Sensor mProxSensor;
@Mock
private DisplayPowerCallbacks mDisplayPowerCallbacksMock;
@@ -94,14 +102,22 @@
@Mock
private HighBrightnessModeMetadata mHighBrightnessModeMetadataMock;
@Mock
+ private HighBrightnessModeMetadata mFollowerHighBrightnessModeMetadataMock;
+ @Mock
private LogicalDisplay mLogicalDisplayMock;
@Mock
+ private LogicalDisplay mFollowerLogicalDisplayMock;
+ @Mock
private DisplayDevice mDisplayDeviceMock;
@Mock
+ private DisplayDevice mFollowerDisplayDeviceMock;
+ @Mock
private BrightnessTracker mBrightnessTrackerMock;
@Mock
private BrightnessSetting mBrightnessSettingMock;
@Mock
+ private BrightnessSetting mFollowerBrightnessSettingMock;
+ @Mock
private WindowManagerPolicy mWindowManagerPolicyMock;
@Mock
private PowerManager mPowerManagerMock;
@@ -110,10 +126,22 @@
@Mock
private DisplayDeviceConfig mDisplayDeviceConfigMock;
@Mock
+ private DisplayDeviceConfig mFollowerDisplayDeviceConfigMock;
+ @Mock
private DisplayPowerState mDisplayPowerStateMock;
@Mock
private DualRampAnimator<DisplayPowerState> mDualRampAnimatorMock;
@Mock
+ private DualRampAnimator<DisplayPowerState> mFollowerDualRampAnimatorMock;
+ @Mock
+ private AutomaticBrightnessController mAutomaticBrightnessControllerMock;
+ @Mock
+ private AutomaticBrightnessController mFollowerAutomaticBrightnessControllerMock;
+ @Mock
+ private BrightnessMappingStrategy mBrightnessMapperMock;
+ @Mock
+ private HysteresisLevels mHysteresisLevelsMock;
+ @Mock
private WakelockController mWakelockController;
@Mock
private ColorDisplayService.ColorDisplayServiceInternal mCdsiMock;
@@ -126,6 +154,7 @@
mSession = ExtendedMockito.mockitoSession()
.initMocks(this)
.strictness(Strictness.LENIENT)
+ .spyStatic(SystemProperties.class)
.spyStatic(LocalServices.class)
.spyStatic(BatteryStatsService.class)
.startMocking();
@@ -167,6 +196,113 @@
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 mAutomaticBrightnessControllerMock;
+ }
+
+ @Override
+ BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
+ DisplayDeviceConfig displayDeviceConfig,
+ DisplayWhiteBalanceController displayWhiteBalanceController) {
+ return mBrightnessMapperMock;
+ }
+
+ @Override
+ HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+ float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+ float[] darkeningThresholdLevels, float minDarkeningThreshold,
+ float minBrighteningThreshold) {
+ return mHysteresisLevelsMock;
+ }
+
+ @Override
+ HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+ float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+ float[] darkeningThresholdLevels, float minDarkeningThreshold,
+ float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
+ return mHysteresisLevelsMock;
+ }
+ };
+ mFollowerInjector = 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 mFollowerDualRampAnimatorMock;
+ }
+
+ @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 mFollowerAutomaticBrightnessControllerMock;
+ }
+
+ @Override
+ BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
+ DisplayDeviceConfig displayDeviceConfig,
+ DisplayWhiteBalanceController displayWhiteBalanceController) {
+ return mBrightnessMapperMock;
+ }
+
+ @Override
+ HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+ float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+ float[] darkeningThresholdLevels, float minDarkeningThreshold,
+ float minBrighteningThreshold) {
+ return mHysteresisLevelsMock;
+ }
+
+ @Override
+ HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+ float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+ float[] darkeningThresholdLevels, float minDarkeningThreshold,
+ float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
+ return mHysteresisLevelsMock;
+ }
};
addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock);
@@ -174,11 +310,30 @@
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());
+ doAnswer((Answer<Void>) invocationOnMock -> null).when(BatteryStatsService::getService);
+
+ setUpDisplay(DISPLAY_ID, UNIQUE_DISPLAY_ID, mLogicalDisplayMock, mDisplayDeviceMock,
+ mDisplayDeviceConfigMock);
+ setUpDisplay(FOLLOWER_DISPLAY_ID, UNIQUE_DISPLAY_ID, mFollowerLogicalDisplayMock,
+ mFollowerDisplayDeviceMock, mFollowerDisplayDeviceConfigMock);
+
+ mProxSensor = setUpProxSensor();
+
+ mDpc = new DisplayPowerController2(
+ mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
+ mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
+ mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
+ }, mHighBrightnessModeMetadataMock);
+ mFollowerDpc = new DisplayPowerController2(
+ mContextSpy, mFollowerInjector, mDisplayPowerCallbacksMock, mHandler,
+ mSensorManagerMock, mDisplayBlankerMock, mFollowerLogicalDisplayMock,
+ mBrightnessTrackerMock, mFollowerBrightnessSettingMock, () -> {
+ }, mFollowerHighBrightnessModeMetadataMock);
}
@After
@@ -189,30 +344,20 @@
@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);
// send a display power request
DisplayPowerRequest dpr = new DisplayPowerRequest();
dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
dpr.useProximitySensor = true;
- dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
+ mDpc.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
@@ -221,8 +366,7 @@
verify(mWakelockController).acquireWakelock(
WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
-
- dpc.stop();
+ mDpc.stop();
advanceTime(1);
// two times, one for unfinished business and one for proximity
verify(mWakelockController).acquireWakelock(
@@ -232,29 +376,19 @@
}
@Test
- public void testProximitySensorListenerNotRegisteredForNonDefaultDisplay() throws Exception {
- setUpDisplay(1, UNIQUE_DISPLAY_ID);
-
- Sensor proxSensor = setUpProxSensor();
-
- DisplayPowerController2 dpc = new DisplayPowerController2(
- mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
- mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
- mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
- }, mHighBrightnessModeMetadataMock);
-
+ public void testProximitySensorListenerNotRegisteredForNonDefaultDisplay() {
when(mDisplayPowerStateMock.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 */);
+ mFollowerDpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
// Run updatePowerState
advanceTime(1);
verify(mSensorManagerMock, never()).registerListener(any(SensorEventListener.class),
- eq(proxSensor), anyInt(), any(Handler.class));
+ eq(mProxSensor), anyInt(), any(Handler.class));
}
/**
@@ -284,56 +418,158 @@
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() {
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ mFollowerDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
- 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);
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mBrightnessSettingMock).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
- defaultDpc.addDisplayBrightnessFollower(followerDpc);
+ mDpc.addDisplayBrightnessFollower(mFollowerDpc);
- defaultDpc.setBrightness(0.3f);
- 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(mAutomaticBrightnessControllerMock.convertToNits(leadBrightness)).thenReturn(nits);
+ when(mFollowerAutomaticBrightnessControllerMock.convertToFloatScale(nits))
+ .thenReturn(followerBrightness);
+ when(mBrightnessSettingMock.getBrightness()).thenReturn(leadBrightness);
+ listener.onBrightnessChanged(leadBrightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mDualRampAnimatorMock).animateTo(eq(leadBrightness), anyFloat(), anyFloat());
+ verify(mFollowerDualRampAnimatorMock).animateTo(eq(followerBrightness), anyFloat(),
+ anyFloat());
- defaultDpc.setBrightness(0.6f);
- assertEquals(defaultDpc.getBrightnessInfo().brightness,
- followerDpc.getBrightnessInfo().brightness, 0);
+ clearInvocations(mDualRampAnimatorMock, mFollowerDualRampAnimatorMock);
- float brightness = 0.1f;
- defaultDpc.clearDisplayBrightnessFollowers();
- defaultDpc.setBrightness(brightness);
- assertNotEquals(brightness, followerDpc.getBrightnessInfo().brightness, 0);
+ // Test the same float scale value
+ float brightness = 0.6f;
+ nits = 600;
+ when(mAutomaticBrightnessControllerMock.convertToNits(brightness)).thenReturn(nits);
+ when(mFollowerAutomaticBrightnessControllerMock.convertToFloatScale(nits))
+ .thenReturn(brightness);
+ when(mBrightnessSettingMock.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(mFollowerDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+
+ clearInvocations(mDualRampAnimatorMock, mFollowerDualRampAnimatorMock);
+
+ // Test clear followers
+ mDpc.clearDisplayBrightnessFollowers();
+ when(mBrightnessSettingMock.getBrightness()).thenReturn(leadBrightness);
+ listener.onBrightnessChanged(leadBrightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mDualRampAnimatorMock).animateTo(eq(leadBrightness), anyFloat(), anyFloat());
+ verify(mFollowerDualRampAnimatorMock, never()).animateTo(eq(followerBrightness), anyFloat(),
+ anyFloat());
+ }
+
+ @Test
+ public void testDisplayBrightnessFollowers_FollowerDoesNotSupportNits() {
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ mFollowerDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mBrightnessSettingMock).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+ mDpc.addDisplayBrightnessFollower(mFollowerDpc);
+
+ float brightness = 0.3f;
+ when(mAutomaticBrightnessControllerMock.convertToNits(brightness)).thenReturn(300f);
+ when(mFollowerAutomaticBrightnessControllerMock.convertToFloatScale(anyFloat()))
+ .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
+ when(mBrightnessSettingMock.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(mFollowerDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+ }
+
+ @Test
+ public void testDisplayBrightnessFollowers_LeadDpcDoesNotSupportNits() {
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ mFollowerDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mBrightnessSettingMock).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+ mDpc.addDisplayBrightnessFollower(mFollowerDpc);
+
+ float brightness = 0.3f;
+ when(mAutomaticBrightnessControllerMock.convertToNits(anyFloat())).thenReturn(-1f);
+ when(mBrightnessSettingMock.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(mFollowerDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+ }
+
+ @Test
+ public void testDisplayBrightnessFollowers_NeitherDpcSupportsNits() {
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ mFollowerDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mBrightnessSettingMock).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+ mDpc.addDisplayBrightnessFollower(mFollowerDpc);
+
+ float brightness = 0.3f;
+ when(mAutomaticBrightnessControllerMock.convertToNits(anyFloat())).thenReturn(-1f);
+ when(mFollowerAutomaticBrightnessControllerMock.convertToFloatScale(anyFloat()))
+ .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
+ when(mBrightnessSettingMock.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(mFollowerDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
}
}
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 6bf5b62..996a9ab 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -19,14 +19,14 @@
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.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
@@ -40,7 +40,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;
@@ -55,6 +57,7 @@
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;
@@ -77,13 +80,18 @@
public final class DisplayPowerControllerTest {
private static final String UNIQUE_DISPLAY_ID = "unique_id_test123";
private static final int DISPLAY_ID = Display.DEFAULT_DISPLAY;
+ private static final int FOLLOWER_DISPLAY_ID = Display.DEFAULT_DISPLAY + 1;
private MockitoSession mSession;
private OffsettableClock mClock;
private TestLooper mTestLooper;
private Handler mHandler;
private DisplayPowerController.Injector mInjector;
+ private DisplayPowerController.Injector mFollowerInjector;
private Context mContextSpy;
+ private DisplayPowerController mDpc;
+ private DisplayPowerController mFollowerDpc;
+ private Sensor mProxSensor;
@Mock
private DisplayPowerCallbacks mDisplayPowerCallbacksMock;
@@ -94,14 +102,22 @@
@Mock
private LogicalDisplay mLogicalDisplayMock;
@Mock
+ private LogicalDisplay mFollowerLogicalDisplayMock;
+ @Mock
private DisplayDevice mDisplayDeviceMock;
@Mock
+ private DisplayDevice mFollowerDisplayDeviceMock;
+ @Mock
private HighBrightnessModeMetadata mHighBrightnessModeMetadataMock;
@Mock
+ private HighBrightnessModeMetadata mFollowerHighBrightnessModeMetadataMock;
+ @Mock
private BrightnessTracker mBrightnessTrackerMock;
@Mock
private BrightnessSetting mBrightnessSettingMock;
@Mock
+ private BrightnessSetting mFollowerBrightnessSettingMock;
+ @Mock
private WindowManagerPolicy mWindowManagerPolicyMock;
@Mock
private PowerManager mPowerManagerMock;
@@ -110,10 +126,22 @@
@Mock
private DisplayDeviceConfig mDisplayDeviceConfigMock;
@Mock
+ private DisplayDeviceConfig mFollowerDisplayDeviceConfigMock;
+ @Mock
private DisplayPowerState mDisplayPowerStateMock;
@Mock
private DualRampAnimator<DisplayPowerState> mDualRampAnimatorMock;
@Mock
+ private DualRampAnimator<DisplayPowerState> mFollowerDualRampAnimatorMock;
+ @Mock
+ private AutomaticBrightnessController mAutomaticBrightnessControllerMock;
+ @Mock
+ private AutomaticBrightnessController mFollowerAutomaticBrightnessControllerMock;
+ @Mock
+ private BrightnessMappingStrategy mBrightnessMapperMock;
+ @Mock
+ private HysteresisLevels mHysteresisLevelsMock;
+ @Mock
private ColorDisplayService.ColorDisplayServiceInternal mCdsiMock;
@Captor
@@ -124,6 +152,7 @@
mSession = ExtendedMockito.mockitoSession()
.initMocks(this)
.strictness(Strictness.LENIENT)
+ .spyStatic(SystemProperties.class)
.spyStatic(LocalServices.class)
.spyStatic(BatteryStatsService.class)
.startMocking();
@@ -149,6 +178,113 @@
FloatProperty<DisplayPowerState> secondProperty) {
return mDualRampAnimatorMock;
}
+
+ @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 mAutomaticBrightnessControllerMock;
+ }
+
+ @Override
+ BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
+ DisplayDeviceConfig displayDeviceConfig,
+ DisplayWhiteBalanceController displayWhiteBalanceController) {
+ return mBrightnessMapperMock;
+ }
+
+ @Override
+ HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+ float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+ float[] darkeningThresholdLevels, float minDarkeningThreshold,
+ float minBrighteningThreshold) {
+ return mHysteresisLevelsMock;
+ }
+
+ @Override
+ HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+ float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+ float[] darkeningThresholdLevels, float minDarkeningThreshold,
+ float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
+ return mHysteresisLevelsMock;
+ }
+ };
+ mFollowerInjector = 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 mFollowerDualRampAnimatorMock;
+ }
+
+ @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 mFollowerAutomaticBrightnessControllerMock;
+ }
+
+ @Override
+ BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
+ DisplayDeviceConfig displayDeviceConfig,
+ DisplayWhiteBalanceController displayWhiteBalanceController) {
+ return mBrightnessMapperMock;
+ }
+
+ @Override
+ HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+ float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+ float[] darkeningThresholdLevels, float minDarkeningThreshold,
+ float minBrighteningThreshold) {
+ return mHysteresisLevelsMock;
+ }
+
+ @Override
+ HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+ float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+ float[] darkeningThresholdLevels, float minDarkeningThreshold,
+ float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
+ return mHysteresisLevelsMock;
+ }
};
addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock);
@@ -156,11 +292,30 @@
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());
+ ColorDisplayService.ColorDisplayServiceInternal.class));
+ doAnswer((Answer<Void>) invocationOnMock -> null).when(BatteryStatsService::getService);
+
+ setUpDisplay(DISPLAY_ID, UNIQUE_DISPLAY_ID, mLogicalDisplayMock, mDisplayDeviceMock,
+ mDisplayDeviceConfigMock);
+ setUpDisplay(FOLLOWER_DISPLAY_ID, UNIQUE_DISPLAY_ID, mFollowerLogicalDisplayMock,
+ mFollowerDisplayDeviceMock, mFollowerDisplayDeviceConfigMock);
+
+ mProxSensor = setUpProxSensor();
+
+ mDpc = new DisplayPowerController(
+ mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
+ mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
+ mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
+ }, mHighBrightnessModeMetadataMock);
+ mFollowerDpc = new DisplayPowerController(
+ mContextSpy, mFollowerInjector, mDisplayPowerCallbacksMock, mHandler,
+ mSensorManagerMock, mDisplayBlankerMock, mFollowerLogicalDisplayMock,
+ mBrightnessTrackerMock, mFollowerBrightnessSettingMock, () -> {
+ }, mFollowerHighBrightnessModeMetadataMock);
}
@After
@@ -171,72 +326,52 @@
@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);
// send a display power request
DisplayPowerRequest dpr = new DisplayPowerRequest();
dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
dpr.useProximitySensor = true;
- dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
+ mDpc.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));
+ mDpc.getSuspendBlockerUnfinishedBusinessId(DISPLAY_ID));
verify(mDisplayPowerCallbacksMock).acquireSuspendBlocker(
- dpc.getSuspendBlockerProxDebounceId(DISPLAY_ID));
+ mDpc.getSuspendBlockerProxDebounceId(DISPLAY_ID));
- dpc.stop();
+ mDpc.stop();
advanceTime(1);
// two times, one for unfinished business and one for proximity
verify(mDisplayPowerCallbacksMock).releaseSuspendBlocker(
- dpc.getSuspendBlockerUnfinishedBusinessId(DISPLAY_ID));
+ mDpc.getSuspendBlockerUnfinishedBusinessId(DISPLAY_ID));
verify(mDisplayPowerCallbacksMock).releaseSuspendBlocker(
- dpc.getSuspendBlockerProxDebounceId(DISPLAY_ID));
+ mDpc.getSuspendBlockerProxDebounceId(DISPLAY_ID));
}
@Test
- public void testProximitySensorListenerNotRegisteredForNonDefaultDisplay() throws Exception {
- setUpDisplay(1, UNIQUE_DISPLAY_ID);
-
- Sensor proxSensor = setUpProxSensor();
-
- DisplayPowerController dpc = new DisplayPowerController(
- mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
- mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
- mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
- }, mHighBrightnessModeMetadataMock);
-
+ public void testProximitySensorListenerNotRegisteredForNonDefaultDisplay() {
when(mDisplayPowerStateMock.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 */);
+ mFollowerDpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
// Run updatePowerState
advanceTime(1);
verify(mSensorManagerMock, never()).registerListener(any(SensorEventListener.class),
- eq(proxSensor), anyInt(), any(Handler.class));
+ eq(mProxSensor), anyInt(), any(Handler.class));
}
/**
@@ -266,56 +401,158 @@
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() {
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ mFollowerDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
- 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);
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mBrightnessSettingMock).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
- defaultDpc.addDisplayBrightnessFollower(followerDpc);
+ mDpc.addDisplayBrightnessFollower(mFollowerDpc);
- defaultDpc.setBrightness(0.3f);
- 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(mAutomaticBrightnessControllerMock.convertToNits(leadBrightness)).thenReturn(nits);
+ when(mFollowerAutomaticBrightnessControllerMock.convertToFloatScale(nits))
+ .thenReturn(followerBrightness);
+ when(mBrightnessSettingMock.getBrightness()).thenReturn(leadBrightness);
+ listener.onBrightnessChanged(leadBrightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mDualRampAnimatorMock).animateTo(eq(leadBrightness), anyFloat(), anyFloat());
+ verify(mFollowerDualRampAnimatorMock).animateTo(eq(followerBrightness), anyFloat(),
+ anyFloat());
- defaultDpc.setBrightness(0.6f);
- assertEquals(defaultDpc.getBrightnessInfo().brightness,
- followerDpc.getBrightnessInfo().brightness, 0);
+ clearInvocations(mDualRampAnimatorMock, mFollowerDualRampAnimatorMock);
- float brightness = 0.1f;
- defaultDpc.clearDisplayBrightnessFollowers();
- defaultDpc.setBrightness(brightness);
- assertNotEquals(brightness, followerDpc.getBrightnessInfo().brightness, 0);
+ // Test the same float scale value
+ float brightness = 0.6f;
+ nits = 600;
+ when(mAutomaticBrightnessControllerMock.convertToNits(brightness)).thenReturn(nits);
+ when(mFollowerAutomaticBrightnessControllerMock.convertToFloatScale(nits))
+ .thenReturn(brightness);
+ when(mBrightnessSettingMock.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(mFollowerDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+
+ clearInvocations(mDualRampAnimatorMock, mFollowerDualRampAnimatorMock);
+
+ // Test clear followers
+ mDpc.clearDisplayBrightnessFollowers();
+ when(mBrightnessSettingMock.getBrightness()).thenReturn(leadBrightness);
+ listener.onBrightnessChanged(leadBrightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mDualRampAnimatorMock).animateTo(eq(leadBrightness), anyFloat(), anyFloat());
+ verify(mFollowerDualRampAnimatorMock, never()).animateTo(eq(followerBrightness), anyFloat(),
+ anyFloat());
+ }
+
+ @Test
+ public void testDisplayBrightnessFollowers_FollowerDoesNotSupportNits() {
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ mFollowerDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mBrightnessSettingMock).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+ mDpc.addDisplayBrightnessFollower(mFollowerDpc);
+
+ float brightness = 0.3f;
+ when(mAutomaticBrightnessControllerMock.convertToNits(brightness)).thenReturn(300f);
+ when(mFollowerAutomaticBrightnessControllerMock.convertToFloatScale(anyFloat()))
+ .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
+ when(mBrightnessSettingMock.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(mFollowerDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+ }
+
+ @Test
+ public void testDisplayBrightnessFollowers_LeadDpcDoesNotSupportNits() {
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ mFollowerDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mBrightnessSettingMock).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+ mDpc.addDisplayBrightnessFollower(mFollowerDpc);
+
+ float brightness = 0.3f;
+ when(mAutomaticBrightnessControllerMock.convertToNits(anyFloat())).thenReturn(-1f);
+ when(mBrightnessSettingMock.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(mFollowerDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+ }
+
+ @Test
+ public void testDisplayBrightnessFollowers_NeitherDpcSupportsNits() {
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ mDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ mFollowerDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+ ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+ verify(mBrightnessSettingMock).registerListener(listenerCaptor.capture());
+ BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+ mDpc.addDisplayBrightnessFollower(mFollowerDpc);
+
+ float brightness = 0.3f;
+ when(mAutomaticBrightnessControllerMock.convertToNits(anyFloat())).thenReturn(-1f);
+ when(mFollowerAutomaticBrightnessControllerMock.convertToFloatScale(anyFloat()))
+ .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
+ when(mBrightnessSettingMock.getBrightness()).thenReturn(brightness);
+ listener.onBrightnessChanged(brightness);
+ advanceTime(1); // Send messages, run updatePowerState
+ verify(mDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+ verify(mFollowerDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
}
}
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/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/display/DeviceStateToLayoutMapTest.java b/services/tests/servicestests/src/com/android/server/display/DeviceStateToLayoutMapTest.java
index 7645a3a..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;
@@ -65,11 +66,13 @@
testLayout.createDisplayLocked(
DisplayAddress.fromPhysicalDisplayId(123456L), /* isDefault= */ true,
/* isEnabled= */ true, mDisplayIdProducerMock,
- /* brightnessThrottlingMapId= */ null);
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
testLayout.createDisplayLocked(
DisplayAddress.fromPhysicalDisplayId(78910L), /* isDefault= */ false,
/* isEnabled= */ false, mDisplayIdProducerMock,
- /* brightnessThrottlingMapId= */ null);
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
assertEquals(testLayout, configLayout);
}
@@ -81,11 +84,13 @@
testLayout.createDisplayLocked(
DisplayAddress.fromPhysicalDisplayId(78910L), /* isDefault= */ true,
/* isEnabled= */ true, mDisplayIdProducerMock,
- /* brightnessThrottlingMapId= */ null);
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
testLayout.createDisplayLocked(
DisplayAddress.fromPhysicalDisplayId(123456L), /* isDefault= */ false,
/* isEnabled= */ false, mDisplayIdProducerMock,
- /* brightnessThrottlingMapId= */ null);
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
assertEquals(testLayout, configLayout);
}
@@ -99,18 +104,48 @@
Layout.Display display1 = testLayout.createDisplayLocked(
DisplayAddress.fromPhysicalDisplayId(345L), /* isDefault= */ true,
/* isEnabled= */ true, mDisplayIdProducerMock,
- /* brightnessThrottlingMapId= */ "concurrent");
+ /* 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");
+ /* 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,
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
+
+ assertEquals(testLayout, configLayout);
+ }
+
////////////////////
// Helper Methods //
////////////////////
@@ -158,6 +193,17 @@
+ "<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"
+ + "</display>\n"
+ + "</layout>\n"
+ "</layouts>\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 42bdbec..0e9c171 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -155,6 +155,13 @@
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},
@@ -288,6 +295,9 @@
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(),
@@ -547,6 +557,20 @@
+ "<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"
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 9a43762..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;
@@ -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/LogicalDisplayMapperTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
index f8cc7b2..bd2b5fd 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, /* brightnessThrottlingMapId= */ null);
+ /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
layout1.createDisplayLocked(info(device2).address, /* isDefault= */ false,
- /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null);
+ /* 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();
@@ -332,16 +337,19 @@
Layout layout1 = new Layout();
layout1.createDisplayLocked(info(device1).address, /* isDefault= */ true,
- /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null);
+ /* 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, /* brightnessThrottlingMapId= */ null);
+ /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
// Device3 is the default display.
layout2.createDisplayLocked(info(device3).address, /* isDefault= */ true,
- /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null);
+ /* 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();
@@ -564,17 +572,21 @@
Layout layout = new Layout();
layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address,
true, true, mIdProducer,
- /* brightnessThrottlingMapId= */ "concurrent");
+ /* brightnessThrottlingMapId= */ "concurrent",
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address,
false, true, mIdProducer,
- /* brightnessThrottlingMapId= */ "concurrent");
+ /* brightnessThrottlingMapId= */ "concurrent",
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
when(mDeviceStateToLayoutMapSpy.get(0)).thenReturn(layout);
layout = new Layout();
layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address,
- false, false, mIdProducer, /* brightnessThrottlingMapId= */ null);
+ false, false, mIdProducer, /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address,
- true, true, mIdProducer, /* brightnessThrottlingMapId= */ null);
+ true, true, mIdProducer, /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
when(mDeviceStateToLayoutMapSpy.get(1)).thenReturn(layout);
when(mDeviceStateToLayoutMapSpy.get(2)).thenReturn(layout);
@@ -601,6 +613,10 @@
assertTrue(mLogicalDisplayMapper.getDisplayLocked(device2).isEnabledLocked());
assertFalse(mLogicalDisplayMapper.getDisplayLocked(device1).isInTransitionLocked());
assertFalse(mLogicalDisplayMapper.getDisplayLocked(device2).isInTransitionLocked());
+ assertEquals(-1, mLogicalDisplayMapper.getDisplayLocked(device1)
+ .getLeadDisplayLocked());
+ assertEquals(0, mLogicalDisplayMapper.getDisplayLocked(device2)
+ .getLeadDisplayLocked());
assertEquals("concurrent", mLogicalDisplayMapper.getDisplayLocked(device1)
.getBrightnessThrottlingDataIdLocked());
assertEquals("concurrent", mLogicalDisplayMapper.getDisplayLocked(device2)
@@ -652,19 +668,22 @@
/* isDefault= */ true,
/* isEnabled= */ true,
mIdProducer,
- /* brightnessThrottlingMapId= */ null);
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
threeDevicesEnabledLayout.createDisplayLocked(
displayAddressTwo,
/* isDefault= */ false,
/* isEnabled= */ true,
mIdProducer,
- /* brightnessThrottlingMapId= */ null);
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
threeDevicesEnabledLayout.createDisplayLocked(
displayAddressThree,
/* isDefault= */ false,
/* isEnabled= */ true,
mIdProducer,
- /* brightnessThrottlingMapId= */ null);
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
when(mDeviceStateToLayoutMapSpy.get(STATE_DEFAULT))
.thenReturn(threeDevicesEnabledLayout);
@@ -700,19 +719,22 @@
/* isDefault= */ true,
/* isEnabled= */ true,
mIdProducer,
- /* brightnessThrottlingMapId= */ null);
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
oneDeviceEnabledLayout.createDisplayLocked(
displayAddressTwo,
/* isDefault= */ false,
/* isEnabled= */ false,
mIdProducer,
- /* brightnessThrottlingMapId= */ null);
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
oneDeviceEnabledLayout.createDisplayLocked(
displayAddressThree,
/* isDefault= */ false,
/* isEnabled= */ false,
mIdProducer,
- /* brightnessThrottlingMapId= */ null);
+ /* brightnessThrottlingMapId= */ null,
+ /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
when(mDeviceStateToLayoutMapSpy.get(0)).thenReturn(oneDeviceEnabledLayout);
when(mDeviceStateToLayoutMapSpy.get(1)).thenReturn(threeDevicesEnabledLayout);
@@ -778,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).getPositionLocked());
+ assertEquals(POSITION_REAR,
+ mLogicalDisplayMapper.getDisplayLocked(device2).getPositionLocked());
+ }
/////////////////
// Helper Methods
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/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 5f8a2b5..5c69c84 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -78,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;
@@ -1646,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);
@@ -10043,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/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/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index e663245..56d59b4 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().getSourceProvider(ID_IME).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().getSourceProvider(ID_IME).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.getSourceProvider(ID_IME).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..45cf530 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;
@@ -255,7 +256,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 +271,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..dba2995 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
@@ -276,9 +276,9 @@
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 */);
@@ -293,9 +293,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)
@@ -362,11 +362,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());
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..88ecd3f 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,13 @@
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.InsetsSource.ID_IME;
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.WindowInsets.Type.ime;
+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;
@@ -76,9 +77,9 @@
null);
getController().getSourceProvider(ITYPE_NAVIGATION_BAR).setWindowContainer(navBar, null,
null);
- getController().getSourceProvider(ITYPE_IME).setWindowContainer(ime, null, null);
+ getController().getSourceProvider(ID_IME).setWindowContainer(ime, null, null);
- assertNull(navBar.getInsetsState().peekSource(ITYPE_IME));
+ assertNull(navBar.getInsetsState().peekSource(ID_IME));
assertNull(navBar.getInsetsState().peekSource(ITYPE_STATUS_BAR));
}
@@ -96,7 +97,7 @@
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_IME));
}
@Test
@@ -135,43 +136,41 @@
@SetupWindows(addWindows = W_INPUT_METHOD)
@Test
public void testStripForDispatch_independentSources() {
- getController().getSourceProvider(ITYPE_IME).setWindowContainer(mImeWindow, null, null);
+ getController().getSourceProvider(ID_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().getSourceProvider(ID_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().getSourceProvider(ID_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 +184,7 @@
// Make IME and stay visible during the test.
mImeWindow.setHasSurface(true);
- getController().getSourceProvider(ITYPE_IME).setWindowContainer(mImeWindow, null, null);
+ getController().getSourceProvider(ID_IME).setWindowContainer(mImeWindow, null, null);
getController().onImeControlTargetChanged(
mDisplayContent.getImeInputTarget().getWindowState());
mDisplayContent.getImeInputTarget().getWindowState().setRequestedVisibleTypes(ime(), ime());
@@ -205,9 +204,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 +214,21 @@
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().getSourceProvider(ID_IME).setWindowContainer(mImeWindow, null, null);
final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
final WindowState child = createWindow(app, TYPE_APPLICATION, "child");
@@ -241,20 +240,19 @@
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().getSourceProvider(ID_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 +260,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
@@ -283,14 +280,14 @@
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().getSourceProvider(ID_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(ITYPE_STATUS_BAR).getFrame());
}
@Test
@@ -348,16 +345,17 @@
final InsetsState rotatedState = new InsetsState(app.getInsetsState(),
true /* copySources */);
+ rotatedState.getOrCreateSource(ITYPE_STATUS_BAR, statusBars());
spyOn(app.mToken);
doReturn(rotatedState).when(app.mToken).getFixedRotationTransformInsetsState();
- assertTrue(rotatedState.getSource(ITYPE_STATUS_BAR).isVisible());
+ assertTrue(rotatedState.isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars()));
provider.getSource().setVisible(false);
mDisplayContent.getInsetsPolicy().showTransient(new int[] { ITYPE_STATUS_BAR },
true /* isGestureOnSystemBar */);
assertTrue(mDisplayContent.getInsetsPolicy().isTransient(ITYPE_STATUS_BAR));
- assertFalse(app.getInsetsState().getSource(ITYPE_STATUS_BAR).isVisible());
+ assertFalse(app.getInsetsState().isSourceOrDefaultVisible(ITYPE_STATUS_BAR, statusBars()));
}
@Test
@@ -411,11 +409,11 @@
final WindowState statusBar = createTestWindow("statusBar");
final WindowState navBar = createTestWindow("navBar");
- getController().getSourceProvider(ITYPE_IME).setWindowContainer(ime, null, null);
+ getController().getSourceProvider(ID_IME).setWindowContainer(ime, null, null);
waitUntilHandlersIdle();
clearInvocations(mDisplayContent);
- getController().getSourceProvider(ITYPE_IME).setClientVisible(true);
+ getController().getSourceProvider(ID_IME).setClientVisible(true);
waitUntilHandlersIdle();
// The visibility change should trigger a traversal to notify the change.
verify(mDisplayContent).notifyInsetsChanged(any());
@@ -428,9 +426,9 @@
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));
+ assertNull(app.mAboveInsetsState.peekSource(ID_IME));
+ assertNull(statusBar.mAboveInsetsState.peekSource(ID_IME));
+ assertNull(navBar.mAboveInsetsState.peekSource(ID_IME));
assertNotNull(ime.mAboveInsetsState.peekSource(ITYPE_STATUS_BAR));
assertNotNull(ime.mAboveInsetsState.peekSource(ITYPE_NAVIGATION_BAR));
@@ -438,9 +436,9 @@
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));
+ assertNotNull(app.mAboveInsetsState.peekSource(ID_IME));
+ assertNotNull(statusBar.mAboveInsetsState.peekSource(ID_IME));
+ assertNotNull(navBar.mAboveInsetsState.peekSource(ID_IME));
assertNull(ime.mAboveInsetsState.peekSource(ITYPE_STATUS_BAR));
assertNull(ime.mAboveInsetsState.peekSource(ITYPE_NAVIGATION_BAR));
@@ -456,7 +454,7 @@
final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, imeToken, "ime");
final WindowState app = createTestWindow("app");
- getController().getSourceProvider(ITYPE_IME).setWindowContainer(ime, null, null);
+ getController().getSourceProvider(ID_IME).setWindowContainer(ime, null, null);
ime.getControllableInsetProvider().setServerVisible(true);
app.mActivityRecord.setVisibility(true);
@@ -468,7 +466,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 +474,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,7 +482,7 @@
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();
}
@@ -506,7 +504,7 @@
final WindowState app2 = createTestWindow("app2");
makeWindowVisible(mImeWindow);
- final InsetsSourceProvider imeInsetsProvider = getController().getSourceProvider(ITYPE_IME);
+ final InsetsSourceProvider imeInsetsProvider = getController().getSourceProvider(ID_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/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..1e5ec4c 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
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java
index 56c59cc..731a235 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java
@@ -16,12 +16,13 @@
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.InsetsSource.ID_IME;
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.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;
@@ -40,6 +41,7 @@
import android.platform.test.annotations.Presubmit;
import android.view.DisplayCutout;
import android.view.Gravity;
+import android.view.InsetsSource;
import android.view.InsetsState;
import android.view.WindowInsets;
import android.view.WindowLayout;
@@ -88,9 +90,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(ITYPE_STATUS_BAR, statusBars()).setFrame(
0, 0, DISPLAY_WIDTH, STATUS_BAR_HEIGHT);
- mState.getSource(ITYPE_NAVIGATION_BAR).setFrame(
+ mState.getOrCreateSource(ITYPE_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 +119,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 +268,8 @@
@Test
public void fitInvisibleInsets() {
- mState.getSource(ITYPE_STATUS_BAR).setVisible(false);
- mState.getSource(ITYPE_NAVIGATION_BAR).setVisible(false);
+ mState.setSourceVisible(ITYPE_STATUS_BAR, false);
+ mState.setSourceVisible(ITYPE_NAVIGATION_BAR, false);
computeFrames();
assertInsetByTopBottom(0, 0, mFrames.displayFrame);
@@ -277,8 +279,8 @@
@Test
public void fitInvisibleInsetsIgnoringVisibility() {
- mState.getSource(ITYPE_STATUS_BAR).setVisible(false);
- mState.getSource(ITYPE_NAVIGATION_BAR).setVisible(false);
+ mState.setSourceVisible(ITYPE_STATUS_BAR, false);
+ mState.setSourceVisible(ITYPE_NAVIGATION_BAR, false);
mAttrs.setFitInsetsIgnoringVisibility(true);
computeFrames();
@@ -289,9 +291,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();
@@ -363,8 +365,8 @@
@Test
public void layoutInDisplayCutoutModeDefaultWithInvisibleSystemBars() {
addDisplayCutout();
- mState.getSource(ITYPE_STATUS_BAR).setVisible(false);
- mState.getSource(ITYPE_NAVIGATION_BAR).setVisible(false);
+ mState.setSourceVisible(ITYPE_STATUS_BAR, false);
+ mState.setSourceVisible(ITYPE_NAVIGATION_BAR, false);
mAttrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
mAttrs.setFitInsetsTypes(0);
computeFrames();
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..c44869b 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;
@@ -1039,12 +1039,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 +1073,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 +1082,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 +1112,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 +1123,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 +1169,18 @@
mNotificationShadeWindow.setHasSurface(true);
mNotificationShadeWindow.mAttrs.flags &= ~FLAG_NOT_FOCUSABLE;
assertTrue(mNotificationShadeWindow.canBeImeTarget());
- mDisplayContent.getInsetsStateController().getSourceProvider(ITYPE_IME).setWindowContainer(
+ mDisplayContent.getInsetsStateController().getSourceProvider(ID_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/WindowTokenTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java
index 3ff791b..bd63560 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;
@@ -262,7 +262,7 @@
@Test
public void testSetInsetsFrozen_notAffectImeWindowState() {
// Pre-condition: make the IME window be controlled by IME insets provider.
- mDisplayContent.getInsetsStateController().getSourceProvider(ITYPE_IME).setWindowContainer(
+ mDisplayContent.getInsetsStateController().getSourceProvider(ID_IME).setWindowContainer(
mDisplayContent.mInputMethodWindow, null, null);
// Simulate an app window to be the IME layering target, assume the app window has no
diff --git a/services/usb/java/com/android/server/usb/UsbAlsaManager.java b/services/usb/java/com/android/server/usb/UsbAlsaManager.java
index e1de58e..aa1d556 100644
--- a/services/usb/java/com/android/server/usb/UsbAlsaManager.java
+++ b/services/usb/java/com/android/server/usb/UsbAlsaManager.java
@@ -119,11 +119,11 @@
/**
* 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),
@@ -331,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);
}
}
}
@@ -355,10 +355,10 @@
}
// 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()");
@@ -381,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);
@@ -500,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/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/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/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java
index 8db3d00..6fe548f 100644
--- a/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java
+++ b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java
@@ -16,17 +16,20 @@
package android.transparency.test;
+import static com.google.common.truth.Truth.assertThat;
+
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;
@@ -40,12 +43,11 @@
private static final String JOB_ID = "1740526926";
/** Waiting time for the job to be scheduled */
- private static final int JOB_CREATION_MAX_SECONDS = 5;
+ private static final int JOB_CREATION_MAX_SECONDS = 30;
- @After
- public void tearDown() throws Exception {
- uninstallPackage("com.android.egg");
- uninstallRebootlessApex();
+ @Before
+ public void setUp() throws Exception {
+ cancelPendingJob();
}
@Test
@@ -68,35 +70,41 @@
@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 testRebootlessApexUpdateTriggersJobScheduling() throws Exception {
- cancelPendingJob();
- installRebootlessApex();
+ try {
+ installRebootlessApex();
- // Verify
- expectJobToBeScheduled();
- // Just cancel since we can't verifying very meaningfully.
- cancelPendingJob();
+ // Verify
+ expectJobToBeScheduled();
+ } finally {
+ // No need to wait until job complete, since we can't verifying very meaningfully.
+ uninstallRebootlessApexThenReboot();
+ }
}
@Test
public void testPreloadUpdateTriggersJobScheduling() throws Exception {
- cancelPendingJob();
- installPackage("EasterEgg.apk");
+ try {
+ updatePreloadApp();
- // Verify
- expectJobToBeScheduled();
- // Just cancel since we can't verifying very meaningfully.
- cancelPendingJob();
- }
-
- @Test
- public void testMeasureMbas() throws Exception {
- // TODO(265244016): figure out a way to install an MBA
+ // 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 {
@@ -109,7 +117,11 @@
private void cancelPendingJob() throws DeviceNotAvailableException {
CommandResult result = getDevice().executeShellV2Command(
"cmd jobscheduler cancel android " + JOB_ID);
- assertTrue(result.getStatus() == CommandStatus.SUCCESS);
+ 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 {
@@ -117,6 +129,7 @@
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);
@@ -134,7 +147,7 @@
installPackage("com.android.apex.cts.shim.v2_rebootless.apex", "--force-non-staged");
}
- private void uninstallRebootlessApex() throws DeviceNotAvailableException {
+ 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");
@@ -148,4 +161,14 @@
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);
+ }
}
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..176bc28e 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,6 +33,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.ArrayList;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.stream.Collectors;
@@ -56,11 +57,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);
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..44112fc
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java
@@ -0,0 +1,474 @@
+/*
+ * 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 int THRESHOLD_MS = 10;
+ private static final int CALLBACK_TIME_10_FPS = 100;
+ private static final int CALLBACK_TIME_30_FPS = 33;
+ 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 final CountDownLatch mFramesSignal = new CountDownLatch(FRAME_ITERATIONS);
+
+ 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);
+ mCallbackMissedCounter = 0;
+ mScenario.onActivity(activity -> {
+ mSurfaceView = activity.findViewById(R.id.surface);
+ mSurfaceHolder = mSurfaceView.getHolder();
+ mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width,
+ int height) {
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ mSurfaceCreationCountDown.countDown();
+ }
+
+ @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 test_create_choreographer() {
+ 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 test_copy_surface_control() {
+ 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 test_mirror_surface_control() {
+ 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 test_postFrameCallback() {
+ 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 test_postFrameCallbackDelayed() {
+ 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 test_postCallback() {
+ 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 test_postCallbackDelayed() {
+ 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 test_postVsyncCallback() {
+ 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 test_choreographer_10Hz_refreshRate() {
+ 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();
+ transaction.setFrameRate(sc, 10.0f, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE)
+ .addTransactionCommittedListener(Runnable::run,
+ () -> verifyVsyncCallbacks(choreographer,
+ CALLBACK_TIME_10_FPS))
+ .apply();
+ mTestCompleteSignal.countDown();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 5L)) {
+ fail("Test not finished in 5 Seconds");
+ }
+ }
+
+ @Test
+ public void test_choreographer_30Hz_refreshRate() {
+ 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();
+ transaction.setFrameRate(sc, 30.0f, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE)
+ .addTransactionCommittedListener(Runnable::run,
+ () -> verifyVsyncCallbacks(choreographer,
+ CALLBACK_TIME_30_FPS))
+ .apply();
+ mTestCompleteSignal.countDown();
+ });
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 5L)) {
+ fail("Test not finished in 5 Seconds");
+ }
+ }
+
+ private void verifyVsyncCallbacks(Choreographer choreographer, int callbackDurationMs) {
+ long callbackRequestedTimeNs = System.nanoTime();
+ choreographer.postVsyncCallback(frameData -> {
+ mFramesSignal.countDown();
+ final long frameCount = mFramesSignal.getCount();
+ 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);
+ } else {
+ assertTrue("Missed timeline for " + mCallbackMissedCounter + " callbacks, while "
+ + CALLBACK_MISSED_THRESHOLD + " missed callbacks are allowed",
+ mCallbackMissedCounter <= CALLBACK_MISSED_THRESHOLD);
+ mTestCompleteSignal.countDown();
+ }
+ });
+ }
+
+ private long getCallbackDurationDiffInMs(long callbackTimeNs, long requestedTimeNs,
+ int 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/ime/CloseImeOnGoHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt
index b40720b..4890bba 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt
@@ -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/CloseImeToAppOnPressBackTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt
index f6838f4..63f7200 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt
@@ -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/CloseImeToHomeOnFinishActivityTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
index a744cd7..0b7b165 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
@@ -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/ShowImeWhenFocusingOnInputFieldTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt
index 36747cc7..851651e 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt
@@ -50,6 +50,7 @@
}
}
+ @Presubmit
@Test
@IwTest(focusArea = "ime")
override fun cujCompleted() {
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/wifi/tests/src/android/net/wifi/OWNERS b/wifi/tests/src/android/net/wifi/OWNERS
new file mode 100644
index 0000000..8873d07
--- /dev/null
+++ b/wifi/tests/src/android/net/wifi/OWNERS
@@ -0,0 +1 @@
+file:/wifi/java/src/android/net/wifi/sharedconnectivity/OWNERS