Merge "Add SpaLogger."
diff --git a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java
index 2682dd7..442c130 100644
--- a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java
+++ b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java
@@ -36,9 +36,11 @@
/**
* Cancel the jobs for a given uid (e.g. when app data is cleared)
+ *
+ * @param includeProxiedJobs Include jobs scheduled for this UID by other apps
*/
- void cancelJobsForUid(int uid, @JobParameters.StopReason int reason, int debugReasonCode,
- String debugReason);
+ void cancelJobsForUid(int uid, boolean includeProxiedJobs,
+ @JobParameters.StopReason int reason, int debugReasonCode, String debugReason);
/**
* These are for activity manager to communicate to use what is currently performing backups.
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 5860384..20bca35 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -34,6 +34,7 @@
import android.content.pm.UserInfo;
import android.os.BatteryStats;
import android.os.Handler;
+import android.os.Looper;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -181,6 +182,7 @@
private final JobSchedulerService mService;
private final Context mContext;
private final Handler mHandler;
+ private final Injector mInjector;
private PowerManager mPowerManager;
@@ -378,9 +380,15 @@
}
JobConcurrencyManager(JobSchedulerService service) {
+ this(service, new Injector());
+ }
+
+ @VisibleForTesting
+ JobConcurrencyManager(JobSchedulerService service, Injector injector) {
mService = service;
mLock = mService.mLock;
mContext = service.getTestableContext();
+ mInjector = injector;
mHandler = JobSchedulerBackgroundThread.getHandler();
@@ -414,7 +422,7 @@
ServiceManager.getService(BatteryStats.SERVICE_NAME));
for (int i = 0; i < STANDARD_CONCURRENCY_LIMIT; i++) {
mIdleContexts.add(
- new JobServiceContext(mService, this, batteryStats,
+ mInjector.createJobServiceContext(mService, this, batteryStats,
mService.mJobPackageTracker, mContext.getMainLooper()));
}
}
@@ -657,15 +665,40 @@
return;
}
+ prepareForAssignmentDeterminationLocked(
+ mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable);
+
+ if (DEBUG) {
+ Slog.d(TAG, printAssignments("running jobs initial",
+ mRecycledStoppable, mRecycledPreferredUidOnly));
+ }
+
+ determineAssignmentsLocked(
+ mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable);
+
+ if (DEBUG) {
+ Slog.d(TAG, printAssignments("running jobs final",
+ mRecycledStoppable, mRecycledPreferredUidOnly, mRecycledChanged));
+
+ Slog.d(TAG, "work count results: " + mWorkCountTracker);
+ }
+
+ carryOutAssignmentChangesLocked(mRecycledChanged);
+
+ cleanUpAfterAssignmentChangesLocked(
+ mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable);
+
+ noteConcurrency();
+ }
+
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ void prepareForAssignmentDeterminationLocked(final ArraySet<ContextAssignment> idle,
+ final List<ContextAssignment> preferredUidOnly,
+ final List<ContextAssignment> stoppable) {
final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue();
final List<JobServiceContext> activeServices = mActiveServices;
- // To avoid GC churn, we recycle the arrays.
- final ArraySet<ContextAssignment> changed = mRecycledChanged;
- final ArraySet<ContextAssignment> idle = mRecycledIdle;
- final ArrayList<ContextAssignment> preferredUidOnly = mRecycledPreferredUidOnly;
- final ArrayList<ContextAssignment> stoppable = mRecycledStoppable;
-
updateCounterConfigLocked();
// Reset everything since we'll re-evaluate the current state.
mWorkCountTracker.resetCounts();
@@ -719,15 +752,21 @@
assignment.context = jsc;
idle.add(assignment);
}
- if (DEBUG) {
- Slog.d(TAG, printAssignments("running jobs initial", stoppable, preferredUidOnly));
- }
mWorkCountTracker.onCountDone();
+ }
- JobStatus nextPending;
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ void determineAssignmentsLocked(final ArraySet<ContextAssignment> changed,
+ final ArraySet<ContextAssignment> idle,
+ final List<ContextAssignment> preferredUidOnly,
+ final List<ContextAssignment> stoppable) {
+ final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue();
+ final List<JobServiceContext> activeServices = mActiveServices;
pendingJobQueue.resetIterator();
- int projectedRunningCount = numRunningJobs;
+ JobStatus nextPending;
+ int projectedRunningCount = activeServices.size();
while ((nextPending = pendingJobQueue.next()) != null) {
if (mRunningJobs.contains(nextPending)) {
// Should never happen.
@@ -895,13 +934,10 @@
packageStats);
}
}
- if (DEBUG) {
- Slog.d(TAG, printAssignments("running jobs final",
- stoppable, preferredUidOnly, changed));
+ }
- Slog.d(TAG, "assignJobsToContexts: " + mWorkCountTracker.toString());
- }
-
+ @GuardedBy("mLock")
+ private void carryOutAssignmentChangesLocked(final ArraySet<ContextAssignment> changed) {
for (int c = changed.size() - 1; c >= 0; --c) {
final ContextAssignment assignment = changed.valueAt(c);
final JobStatus js = assignment.context.getRunningJobLocked();
@@ -925,6 +961,13 @@
assignment.clear();
mContextAssignmentPool.release(assignment);
}
+ }
+
+ @GuardedBy("mLock")
+ private void cleanUpAfterAssignmentChangesLocked(final ArraySet<ContextAssignment> changed,
+ final ArraySet<ContextAssignment> idle,
+ final List<ContextAssignment> preferredUidOnly,
+ final List<ContextAssignment> stoppable) {
for (int s = stoppable.size() - 1; s >= 0; --s) {
final ContextAssignment assignment = stoppable.get(s);
assignment.clear();
@@ -947,7 +990,6 @@
preferredUidOnly.clear();
mWorkCountTracker.resetStagingCount();
mActivePkgStats.forEach(mPackageStatsStagingCountClearer);
- noteConcurrency();
}
@GuardedBy("mLock")
@@ -1496,7 +1538,7 @@
@NonNull
private JobServiceContext createNewJobServiceContext() {
- return new JobServiceContext(mService, this,
+ return mInjector.createJobServiceContext(mService, this,
IBatteryStats.Stub.asInterface(
ServiceManager.getService(BatteryStats.SERVICE_NAME)),
mService.mJobPackageTracker, mContext.getMainLooper());
@@ -1778,6 +1820,10 @@
@VisibleForTesting
static class WorkTypeConfig {
@VisibleForTesting
+ static final String KEY_PREFIX_MAX = CONFIG_KEY_PREFIX_CONCURRENCY + "max_";
+ @VisibleForTesting
+ static final String KEY_PREFIX_MIN = CONFIG_KEY_PREFIX_CONCURRENCY + "min_";
+ @VisibleForTesting
static final String KEY_PREFIX_MAX_TOTAL = CONFIG_KEY_PREFIX_CONCURRENCY + "max_total_";
private static final String KEY_PREFIX_MAX_TOP = CONFIG_KEY_PREFIX_CONCURRENCY + "max_top_";
private static final String KEY_PREFIX_MAX_FGS = CONFIG_KEY_PREFIX_CONCURRENCY + "max_fgs_";
@@ -2329,7 +2375,8 @@
}
}
- private static final class ContextAssignment {
+ @VisibleForTesting
+ static final class ContextAssignment {
public JobServiceContext context;
public int preferredUid = JobServiceContext.NO_PREFERRED_UID;
public int workType = WORK_TYPE_NONE;
@@ -2378,4 +2425,15 @@
mActivePkgStats.add(userId, packageName, packageStats);
return packageStats;
}
+
+ @VisibleForTesting
+ static class Injector {
+ @NonNull
+ JobServiceContext createJobServiceContext(JobSchedulerService service,
+ JobConcurrencyManager concurrencyManager, IBatteryStats batteryStats,
+ JobPackageTracker tracker, Looper looper) {
+ return new JobServiceContext(service, concurrencyManager, batteryStats,
+ tracker, looper);
+ }
+ }
}
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 f5c0ed9..bdd1fc54 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -970,12 +970,12 @@
// Has this package scheduled any jobs, such that we will take action
// if it were to be force-stopped?
if (pkgUid != -1) {
- List<JobStatus> jobsForUid;
+ ArraySet<JobStatus> jobsForUid;
synchronized (mLock) {
jobsForUid = mJobs.getJobsByUid(pkgUid);
}
for (int i = jobsForUid.size() - 1; i >= 0; i--) {
- if (jobsForUid.get(i).getSourcePackageName().equals(pkgName)) {
+ if (jobsForUid.valueAt(i).getSourcePackageName().equals(pkgName)) {
if (DEBUG) {
Slog.d(TAG, "Restart query: package " + pkgName + " at uid "
+ pkgUid + " has jobs");
@@ -1292,10 +1292,11 @@
public List<JobInfo> getPendingJobs(int uid) {
synchronized (mLock) {
- List<JobStatus> jobs = mJobs.getJobsByUid(uid);
+ ArraySet<JobStatus> jobs = mJobs.getJobsByUid(uid);
ArrayList<JobInfo> outList = new ArrayList<JobInfo>(jobs.size());
+ // Write out for loop to avoid addAll() creating an Iterator.
for (int i = jobs.size() - 1; i >= 0; i--) {
- JobStatus job = jobs.get(i);
+ final JobStatus job = jobs.valueAt(i);
outList.add(job.getJob());
}
return outList;
@@ -1304,9 +1305,9 @@
public JobInfo getPendingJob(int uid, int jobId) {
synchronized (mLock) {
- List<JobStatus> jobs = mJobs.getJobsByUid(uid);
+ ArraySet<JobStatus> jobs = mJobs.getJobsByUid(uid);
for (int i = jobs.size() - 1; i >= 0; i--) {
- JobStatus job = jobs.get(i);
+ JobStatus job = jobs.valueAt(i);
if (job.getJobId() == jobId) {
return job.getJob();
}
@@ -1348,7 +1349,7 @@
Slog.wtfStack(TAG, "Can't cancel all jobs for system package");
return;
}
- final List<JobStatus> jobsForUid = new ArrayList<>();
+ final ArraySet<JobStatus> jobsForUid = new ArraySet<>();
if (includeSchedulingApp) {
mJobs.getJobsByUid(uid, jobsForUid);
}
@@ -1356,7 +1357,7 @@
mJobs.getJobsBySourceUid(uid, jobsForUid);
}
for (int i = jobsForUid.size() - 1; i >= 0; i--) {
- final JobStatus job = jobsForUid.get(i);
+ final JobStatus job = jobsForUid.valueAt(i);
final boolean shouldCancel =
(includeSchedulingApp
&& job.getServiceComponent().getPackageName().equals(pkgName))
@@ -1368,14 +1369,16 @@
}
/**
- * Entry point from client to cancel all jobs originating from their uid.
+ * Entry point from client to cancel all jobs scheduled for or from their uid.
* This will remove the job from the master list, and cancel the job if it was staged for
* execution or being executed.
*
* @param uid Uid to check against for removal of a job.
+ * @param includeSourceApp Whether to include jobs scheduled for this UID by another UID.
+ * If false, only jobs scheduled by this UID will be cancelled.
*/
- public boolean cancelJobsForUid(int uid, @JobParameters.StopReason int reason,
- int internalReasonCode, String debugReason) {
+ public boolean cancelJobsForUid(int uid, boolean includeSourceApp,
+ @JobParameters.StopReason int reason, int internalReasonCode, String debugReason) {
if (uid == Process.SYSTEM_UID) {
Slog.wtfStack(TAG, "Can't cancel all jobs for system uid");
return false;
@@ -1383,9 +1386,15 @@
boolean jobsCanceled = false;
synchronized (mLock) {
- final List<JobStatus> jobsForUid = mJobs.getJobsByUid(uid);
+ final ArraySet<JobStatus> jobsForUid = new ArraySet<>();
+ // Get jobs scheduled by the app.
+ mJobs.getJobsByUid(uid, jobsForUid);
+ if (includeSourceApp) {
+ // Get jobs scheduled for the app by someone else.
+ mJobs.getJobsBySourceUid(uid, jobsForUid);
+ }
for (int i = 0; i < jobsForUid.size(); i++) {
- JobStatus toRemove = jobsForUid.get(i);
+ JobStatus toRemove = jobsForUid.valueAt(i);
cancelJobImplLocked(toRemove, null, reason, internalReasonCode, debugReason);
jobsCanceled = true;
}
@@ -2220,6 +2229,7 @@
updateUidState(uid, ActivityManager.PROCESS_STATE_CACHED_EMPTY);
if (disabled) {
cancelJobsForUid(uid,
+ /* includeSourceApp */ true,
JobParameters.STOP_REASON_BACKGROUND_RESTRICTION,
JobParameters.INTERNAL_STOP_REASON_CONSTRAINTS_NOT_SATISFIED,
"uid gone");
@@ -2241,6 +2251,7 @@
final boolean disabled = message.arg2 != 0;
if (disabled) {
cancelJobsForUid(uid,
+ /* includeSourceApp */ true,
JobParameters.STOP_REASON_BACKGROUND_RESTRICTION,
JobParameters.INTERNAL_STOP_REASON_CONSTRAINTS_NOT_SATISFIED,
"app uid idle");
@@ -2899,9 +2910,10 @@
}
@Override
- public void cancelJobsForUid(int uid, @JobParameters.StopReason int reason,
- int internalReasonCode, String debugReason) {
- JobSchedulerService.this.cancelJobsForUid(uid, reason, internalReasonCode, debugReason);
+ public void cancelJobsForUid(int uid, boolean includeProxiedJobs,
+ @JobParameters.StopReason int reason, int internalReasonCode, String debugReason) {
+ JobSchedulerService.this.cancelJobsForUid(uid,
+ includeProxiedJobs, reason, internalReasonCode, debugReason);
}
@Override
@@ -3273,6 +3285,8 @@
final long ident = Binder.clearCallingIdentity();
try {
JobSchedulerService.this.cancelJobsForUid(uid,
+ // Documentation says only jobs scheduled BY the app will be cancelled
+ /* includeSourceApp */ false,
JobParameters.STOP_REASON_CANCELLED_BY_APP,
JobParameters.INTERNAL_STOP_REASON_CANCELED,
"cancelAll() called by app, callingUid=" + uid);
@@ -3484,7 +3498,9 @@
if (!hasJobId) {
pw.println("Canceling all jobs for " + pkgName + " in user " + userId);
- if (!cancelJobsForUid(pkgUid, JobParameters.STOP_REASON_USER,
+ if (!cancelJobsForUid(pkgUid,
+ /* includeSourceApp */ false,
+ JobParameters.STOP_REASON_USER,
JobParameters.INTERNAL_STOP_REASON_CANCELED,
"cancel shell command for package")) {
pw.println("No matching jobs found.");
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
index ff4d26d..f731b8d 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -291,11 +291,11 @@
* @return A list of all the jobs scheduled for the source app. Never null.
*/
@NonNull
- public List<JobStatus> getJobsBySourceUid(int sourceUid) {
+ public ArraySet<JobStatus> getJobsBySourceUid(int sourceUid) {
return mJobSet.getJobsBySourceUid(sourceUid);
}
- public void getJobsBySourceUid(int sourceUid, @NonNull List<JobStatus> insertInto) {
+ public void getJobsBySourceUid(int sourceUid, @NonNull Set<JobStatus> insertInto) {
mJobSet.getJobsBySourceUid(sourceUid, insertInto);
}
@@ -304,11 +304,11 @@
* @return All JobStatus objects for a given uid from the master list. Never null.
*/
@NonNull
- public List<JobStatus> getJobsByUid(int uid) {
+ public ArraySet<JobStatus> getJobsByUid(int uid) {
return mJobSet.getJobsByUid(uid);
}
- public void getJobsByUid(int uid, @NonNull List<JobStatus> insertInto) {
+ public void getJobsByUid(int uid, @NonNull Set<JobStatus> insertInto) {
mJobSet.getJobsByUid(uid, insertInto);
}
@@ -1232,13 +1232,13 @@
mJobsPerSourceUid = new SparseArray<>();
}
- public List<JobStatus> getJobsByUid(int uid) {
- ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>();
+ public ArraySet<JobStatus> getJobsByUid(int uid) {
+ ArraySet<JobStatus> matchingJobs = new ArraySet<>();
getJobsByUid(uid, matchingJobs);
return matchingJobs;
}
- public void getJobsByUid(int uid, List<JobStatus> insertInto) {
+ public void getJobsByUid(int uid, Set<JobStatus> insertInto) {
ArraySet<JobStatus> jobs = mJobs.get(uid);
if (jobs != null) {
insertInto.addAll(jobs);
@@ -1246,13 +1246,13 @@
}
@NonNull
- public List<JobStatus> getJobsBySourceUid(int sourceUid) {
- final ArrayList<JobStatus> result = new ArrayList<JobStatus>();
+ public ArraySet<JobStatus> getJobsBySourceUid(int sourceUid) {
+ final ArraySet<JobStatus> result = new ArraySet<>();
getJobsBySourceUid(sourceUid, result);
return result;
}
- public void getJobsBySourceUid(int sourceUid, List<JobStatus> insertInto) {
+ public void getJobsBySourceUid(int sourceUid, Set<JobStatus> insertInto) {
final ArraySet<JobStatus> jobs = mJobsPerSourceUid.get(sourceUid);
if (jobs != null) {
insertInto.addAll(jobs);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
index 9c16772..bbd661a 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
@@ -50,7 +50,6 @@
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.List;
import java.util.function.Predicate;
/**
@@ -397,10 +396,10 @@
return;
}
final long nowElapsed = sElapsedRealtimeClock.millis();
- List<JobStatus> jobsByUid = mService.getJobStore().getJobsByUid(uid);
+ ArraySet<JobStatus> jobsByUid = mService.getJobStore().getJobsBySourceUid(uid);
boolean hasPrefetch = false;
for (int i = 0; i < jobsByUid.size(); i++) {
- JobStatus js = jobsByUid.get(i);
+ JobStatus js = jobsByUid.valueAt(i);
if (js.hasFlexibilityConstraint()) {
js.setFlexibilityConstraintSatisfied(nowElapsed, isFlexibilitySatisfiedLocked(js));
hasPrefetch |= js.getJob().isPrefetch();
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 999a3c0..669234b 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
@@ -126,7 +126,7 @@
/**
* Keeps track of how many flexible constraints must be satisfied for the job to execute.
*/
- private int mNumRequiredFlexibleConstraints;
+ private final int mNumRequiredFlexibleConstraints;
/**
* Number of required flexible constraints that have been dropped.
@@ -343,7 +343,8 @@
public static final int INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION = 1 << 0;
/** Minimum difference between start and end time to have flexible constraint */
- private static final long MIN_WINDOW_FOR_FLEXIBILITY_MS = HOUR_IN_MILLIS;
+ @VisibleForTesting
+ static final long MIN_WINDOW_FOR_FLEXIBILITY_MS = HOUR_IN_MILLIS;
/**
* Versatile, persistable flags for a job that's updated within the system server,
* as opposed to {@link JobInfo#flags} that's set by callers.
@@ -580,6 +581,8 @@
mNumRequiredFlexibleConstraints =
NUM_SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS + (mPreferUnmetered ? 1 : 0);
requiredConstraints |= CONSTRAINT_FLEXIBLE;
+ } else {
+ mNumRequiredFlexibleConstraints = 0;
}
this.requiredConstraints = requiredConstraints;
@@ -1152,7 +1155,7 @@
/** Returns the number of flexible job constraints required to be satisfied to execute */
public int getNumRequiredFlexibleConstraints() {
- return mNumRequiredFlexibleConstraints;
+ return mNumRequiredFlexibleConstraints - mNumDroppedFlexibleConstraints;
}
/**
@@ -1585,14 +1588,8 @@
/** Adjusts the number of required flexible constraints by the given number */
public void adjustNumRequiredFlexibleConstraints(int adjustment) {
- mNumRequiredFlexibleConstraints += adjustment;
- if (mNumRequiredFlexibleConstraints < 0) {
- mNumRequiredFlexibleConstraints = 0;
- }
- mNumDroppedFlexibleConstraints -= adjustment;
- if (mNumDroppedFlexibleConstraints < 0) {
- mNumDroppedFlexibleConstraints = 0;
- }
+ mNumDroppedFlexibleConstraints = Math.max(0, Math.min(mNumRequiredFlexibleConstraints,
+ mNumDroppedFlexibleConstraints - adjustment));
}
/**
diff --git a/core/api/current.txt b/core/api/current.txt
index 7bdaa2b..e5cc625 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -49403,6 +49403,8 @@
field public static final int AXIS_GENERIC_7 = 38; // 0x26
field public static final int AXIS_GENERIC_8 = 39; // 0x27
field public static final int AXIS_GENERIC_9 = 40; // 0x28
+ field public static final int AXIS_GESTURE_X_OFFSET = 48; // 0x30
+ field public static final int AXIS_GESTURE_Y_OFFSET = 49; // 0x31
field public static final int AXIS_HAT_X = 15; // 0xf
field public static final int AXIS_HAT_Y = 16; // 0x10
field public static final int AXIS_HSCROLL = 10; // 0xa
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 761d9fa..3dbfdae 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -6511,6 +6511,7 @@
field public static final int MIX_ROLE_PLAYERS = 0; // 0x0
field public static final int RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET = 2; // 0x2
field public static final int RULE_MATCH_ATTRIBUTE_USAGE = 1; // 0x1
+ field public static final int RULE_MATCH_AUDIO_SESSION_ID = 16; // 0x10
field public static final int RULE_MATCH_UID = 4; // 0x4
field public static final int RULE_MATCH_USERID = 8; // 0x8
}
@@ -9276,13 +9277,17 @@
}
public final class BugreportManager {
+ method @RequiresPermission(android.Manifest.permission.DUMP) @WorkerThread public void preDumpUiData();
method @RequiresPermission(android.Manifest.permission.DUMP) public void requestBugreport(@NonNull android.os.BugreportParams, @Nullable CharSequence, @Nullable CharSequence);
method @RequiresPermission(android.Manifest.permission.DUMP) @WorkerThread public void startBugreport(@NonNull android.os.ParcelFileDescriptor, @Nullable android.os.ParcelFileDescriptor, @NonNull android.os.BugreportParams, @NonNull java.util.concurrent.Executor, @NonNull android.os.BugreportManager.BugreportCallback);
}
public final class BugreportParams {
ctor public BugreportParams(int);
+ ctor public BugreportParams(int, int);
+ method public int getFlags();
method public int getMode();
+ field public static final int BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA = 1; // 0x1
field public static final int BUGREPORT_MODE_FULL = 0; // 0x0
field public static final int BUGREPORT_MODE_INTERACTIVE = 1; // 0x1
field public static final int BUGREPORT_MODE_REMOTE = 2; // 0x2
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 5695874..74eb1c5 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -9623,21 +9623,16 @@
@NonNull
public ArrayList<Action> getActionsListWithSystemActions() {
// Define the system actions we expect to see
- final Action negativeAction = makeNegativeAction();
- final Action answerAction = makeAnswerAction();
- // Sort the expected actions into the correct order:
- // * If there's no answer action, put the hang up / decline action at the end
- // * Otherwise put the answer action at the end, and put the decline action at start.
- final Action firstAction = answerAction == null ? null : negativeAction;
- final Action lastAction = answerAction == null ? negativeAction : answerAction;
+ final Action firstAction = makeNegativeAction();
+ final Action lastAction = makeAnswerAction();
// Start creating the result list.
int nonContextualActionSlotsRemaining = MAX_ACTION_BUTTONS;
ArrayList<Action> resultActions = new ArrayList<>(MAX_ACTION_BUTTONS);
- if (firstAction != null) {
- resultActions.add(firstAction);
- --nonContextualActionSlotsRemaining;
- }
+
+ // Always have a first action.
+ resultActions.add(firstAction);
+ --nonContextualActionSlotsRemaining;
// Copy actions into the new list, correcting system actions.
if (mBuilder.mActions != null) {
@@ -9653,14 +9648,14 @@
--nonContextualActionSlotsRemaining;
}
// If there's exactly one action slot left, fill it with the lastAction.
- if (nonContextualActionSlotsRemaining == 1) {
+ if (lastAction != null && nonContextualActionSlotsRemaining == 1) {
resultActions.add(lastAction);
--nonContextualActionSlotsRemaining;
}
}
}
// If there are any action slots left, the lastAction still needs to be added.
- if (nonContextualActionSlotsRemaining >= 1) {
+ if (lastAction != null && nonContextualActionSlotsRemaining >= 1) {
resultActions.add(lastAction);
}
return resultActions;
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index 27f9f54..a51b9d3 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -1406,17 +1406,6 @@
}
/**
- * Return the number of entries in the cache. This is used for testing and has package-only
- * visibility.
- * @hide
- */
- public int size() {
- synchronized (mLock) {
- return mCache.size();
- }
- }
-
- /**
* Returns a list of caches alive at the current time.
*/
@GuardedBy("sGlobalLock")
@@ -1623,12 +1612,8 @@
* @hide
*/
public static void onTrimMemory() {
- ArrayList<PropertyInvalidatedCache> activeCaches;
- synchronized (sGlobalLock) {
- activeCaches = getActiveCaches();
- }
- for (int i = 0; i < activeCaches.size(); i++) {
- activeCaches.get(i).clear();
+ for (PropertyInvalidatedCache pic : getActiveCaches()) {
+ pic.clear();
}
}
}
diff --git a/core/java/android/app/TEST_MAPPING b/core/java/android/app/TEST_MAPPING
index 0af96c2..5b0bd96 100644
--- a/core/java/android/app/TEST_MAPPING
+++ b/core/java/android/app/TEST_MAPPING
@@ -175,23 +175,6 @@
"file_patterns": [
"(/|^)KeyguardManager.java"
]
- },
- {
- "name": "FrameworksCoreTests",
- "options": [
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
- },
- {
- "exclude-annotation": "org.junit.Ignore"
- },
- {
- "include-filter": "android.app.PropertyInvalidatedCacheTest"
- }
- ],
- "file_patterns": [
- "(/|^)PropertyInvalidatedCache.java"
- ]
}
],
"presubmit-large": [
diff --git a/core/java/android/content/om/OverlayManager.java b/core/java/android/content/om/OverlayManager.java
index 0f7e01b..94275ae 100644
--- a/core/java/android/content/om/OverlayManager.java
+++ b/core/java/android/content/om/OverlayManager.java
@@ -31,8 +31,6 @@
import android.os.ServiceManager;
import android.os.UserHandle;
-import com.android.server.SystemConfig;
-
import java.util.List;
/**
@@ -50,7 +48,7 @@
* <overlayable name="OverlayableResourcesName" actor="overlay://namespace/actorName">
* }</pre></p>
*
- * <p>Actors are defined through {@link SystemConfig}. Only system packages can be used.
+ * <p>Actors are defined through SystemConfig. Only system packages can be used.
* The namespace "android" is reserved for use by AOSP and any "android" definitions must
* have an implementation on device that fulfill their intended functionality.</p>
*
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
index e13f60c..2c28268 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -45,7 +45,6 @@
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Parcelling;
import com.android.internal.util.Parcelling.BuiltIn.ForBoolean;
-import com.android.server.SystemConfig;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -803,7 +802,6 @@
*/
public static final int PRIVATE_FLAG_EXT_ATTRIBUTIONS_ARE_USER_VISIBLE = 1 << 2;
-
/**
* If false, {@link android.view.KeyEvent#KEYCODE_BACK} related events will be forwarded to
* the Activities, Dialogs and Views and {@link android.app.Activity#onBackPressed()},
@@ -815,12 +813,24 @@
*/
public static final int PRIVATE_FLAG_EXT_ENABLE_ON_BACK_INVOKED_CALLBACK = 1 << 3;
+ /**
+ * Whether or not this package is allowed to access hidden APIs. Replacement for legacy
+ * implementation of {@link #isPackageWhitelistedForHiddenApis()}.
+ *
+ * This is an internal flag and should never be used outside of this class. The real API for
+ * the hidden API enforcement policy is {@link #getHiddenApiEnforcementPolicy()}.
+ *
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_EXT_ALLOWLISTED_FOR_HIDDEN_APIS = 1 << 4;
+
/** @hide */
@IntDef(flag = true, prefix = { "PRIVATE_FLAG_EXT_" }, value = {
PRIVATE_FLAG_EXT_PROFILEABLE,
PRIVATE_FLAG_EXT_REQUEST_FOREGROUND_SERVICE_EXEMPTION,
PRIVATE_FLAG_EXT_ATTRIBUTIONS_ARE_USER_VISIBLE,
PRIVATE_FLAG_EXT_ENABLE_ON_BACK_INVOKED_CALLBACK,
+ PRIVATE_FLAG_EXT_ALLOWLISTED_FOR_HIDDEN_APIS,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ApplicationInfoPrivateFlagsExt {}
@@ -2222,7 +2232,7 @@
}
private boolean isPackageWhitelistedForHiddenApis() {
- return SystemConfig.getInstance().getHiddenApiWhitelistedApps().contains(packageName);
+ return (privateFlagsExt & PRIVATE_FLAG_EXT_ALLOWLISTED_FOR_HIDDEN_APIS) != 0;
}
/**
diff --git a/core/java/android/credentials/ui/RequestInfo.java b/core/java/android/credentials/ui/RequestInfo.java
index eddb519..5de6d73 100644
--- a/core/java/android/credentials/ui/RequestInfo.java
+++ b/core/java/android/credentials/ui/RequestInfo.java
@@ -36,6 +36,12 @@
*/
public static final @NonNull String EXTRA_REQUEST_INFO =
"android.credentials.ui.extra.REQUEST_INFO";
+ /**
+ * The intent extra key for the {@code ResultReceiver} object when launching the UX
+ * activities.
+ */
+ public static final @NonNull String EXTRA_RESULT_RECEIVER =
+ "android.credentials.ui.extra.RESULT_RECEIVER";
/** Type value for an executeGetCredential request. */
public static final @NonNull String TYPE_GET = "android.credentials.ui.TYPE_GET";
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index 861a850..8873807 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -2609,31 +2609,31 @@
* <table>
* <thead>
* <tr>
- * <th align="left">Input Format</th>
- * <th align="left">Output Format</th>
- * <th align="left">Capability</th>
+ * <th style="text-align: left;">Input Format</th>
+ * <th style="text-align: left;">Output Format</th>
+ * <th style="text-align: left;">Capability</th>
* </tr>
* </thead>
* <tbody>
* <tr>
- * <td align="left">{@link android.graphics.ImageFormat#PRIVATE }</td>
- * <td align="left">{@link android.graphics.ImageFormat#JPEG }</td>
- * <td align="left">PRIVATE_REPROCESSING</td>
+ * <td style="text-align: left;">{@link android.graphics.ImageFormat#PRIVATE }</td>
+ * <td style="text-align: left;">{@link android.graphics.ImageFormat#JPEG }</td>
+ * <td style="text-align: left;">PRIVATE_REPROCESSING</td>
* </tr>
* <tr>
- * <td align="left">{@link android.graphics.ImageFormat#PRIVATE }</td>
- * <td align="left">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
- * <td align="left">PRIVATE_REPROCESSING</td>
+ * <td style="text-align: left;">{@link android.graphics.ImageFormat#PRIVATE }</td>
+ * <td style="text-align: left;">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
+ * <td style="text-align: left;">PRIVATE_REPROCESSING</td>
* </tr>
* <tr>
- * <td align="left">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
- * <td align="left">{@link android.graphics.ImageFormat#JPEG }</td>
- * <td align="left">YUV_REPROCESSING</td>
+ * <td style="text-align: left;">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
+ * <td style="text-align: left;">{@link android.graphics.ImageFormat#JPEG }</td>
+ * <td style="text-align: left;">YUV_REPROCESSING</td>
* </tr>
* <tr>
- * <td align="left">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
- * <td align="left">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
- * <td align="left">YUV_REPROCESSING</td>
+ * <td style="text-align: left;">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
+ * <td style="text-align: left;">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
+ * <td style="text-align: left;">YUV_REPROCESSING</td>
* </tr>
* </tbody>
* </table>
@@ -2650,26 +2650,26 @@
* <table>
* <thead>
* <tr>
- * <th align="left">Input Format</th>
- * <th align="left">Output Format</th>
- * <th align="left">Capability</th>
+ * <th style="text-align: left;">Input Format</th>
+ * <th style="text-align: left;">Output Format</th>
+ * <th style="text-align: left;">Capability</th>
* </tr>
* </thead>
* <tbody>
* <tr>
- * <td align="left">{@link android.graphics.ImageFormat#PRIVATE }</td>
- * <td align="left">{@link android.graphics.ImageFormat#Y8 }</td>
- * <td align="left">PRIVATE_REPROCESSING</td>
+ * <td style="text-align: left;">{@link android.graphics.ImageFormat#PRIVATE }</td>
+ * <td style="text-align: left;">{@link android.graphics.ImageFormat#Y8 }</td>
+ * <td style="text-align: left;">PRIVATE_REPROCESSING</td>
* </tr>
* <tr>
- * <td align="left">{@link android.graphics.ImageFormat#Y8 }</td>
- * <td align="left">{@link android.graphics.ImageFormat#JPEG }</td>
- * <td align="left">YUV_REPROCESSING</td>
+ * <td style="text-align: left;">{@link android.graphics.ImageFormat#Y8 }</td>
+ * <td style="text-align: left;">{@link android.graphics.ImageFormat#JPEG }</td>
+ * <td style="text-align: left;">YUV_REPROCESSING</td>
* </tr>
* <tr>
- * <td align="left">{@link android.graphics.ImageFormat#Y8 }</td>
- * <td align="left">{@link android.graphics.ImageFormat#Y8 }</td>
- * <td align="left">YUV_REPROCESSING</td>
+ * <td style="text-align: left;">{@link android.graphics.ImageFormat#Y8 }</td>
+ * <td style="text-align: left;">{@link android.graphics.ImageFormat#Y8 }</td>
+ * <td style="text-align: left;">YUV_REPROCESSING</td>
* </tr>
* </tbody>
* </table>
@@ -2705,60 +2705,60 @@
* <table>
* <thead>
* <tr>
- * <th align="center">Format</th>
- * <th align="center">Size</th>
- * <th align="center">Hardware Level</th>
- * <th align="center">Notes</th>
+ * <th style="text-align: center;">Format</th>
+ * <th style="text-align: center;">Size</th>
+ * <th style="text-align: center;">Hardware Level</th>
+ * <th style="text-align: center;">Notes</th>
* </tr>
* </thead>
* <tbody>
* <tr>
- * <td align="center">JPEG</td>
- * <td align="center">{@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}</td>
- * <td align="center">Any</td>
- * <td align="center"></td>
+ * <td style="text-align: center;">JPEG</td>
+ * <td style="text-align: center;">{@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}</td>
+ * <td style="text-align: center;">Any</td>
+ * <td style="text-align: center;"></td>
* </tr>
* <tr>
- * <td align="center">JPEG</td>
- * <td align="center">1920x1080 (1080p)</td>
- * <td align="center">Any</td>
- * <td align="center">if 1080p <= activeArraySize</td>
+ * <td style="text-align: center;">JPEG</td>
+ * <td style="text-align: center;">1920x1080 (1080p)</td>
+ * <td style="text-align: center;">Any</td>
+ * <td style="text-align: center;">if 1080p <= activeArraySize</td>
* </tr>
* <tr>
- * <td align="center">JPEG</td>
- * <td align="center">1280x720 (720)</td>
- * <td align="center">Any</td>
- * <td align="center">if 720p <= activeArraySize</td>
+ * <td style="text-align: center;">JPEG</td>
+ * <td style="text-align: center;">1280x720 (720)</td>
+ * <td style="text-align: center;">Any</td>
+ * <td style="text-align: center;">if 720p <= activeArraySize</td>
* </tr>
* <tr>
- * <td align="center">JPEG</td>
- * <td align="center">640x480 (480p)</td>
- * <td align="center">Any</td>
- * <td align="center">if 480p <= activeArraySize</td>
+ * <td style="text-align: center;">JPEG</td>
+ * <td style="text-align: center;">640x480 (480p)</td>
+ * <td style="text-align: center;">Any</td>
+ * <td style="text-align: center;">if 480p <= activeArraySize</td>
* </tr>
* <tr>
- * <td align="center">JPEG</td>
- * <td align="center">320x240 (240p)</td>
- * <td align="center">Any</td>
- * <td align="center">if 240p <= activeArraySize</td>
+ * <td style="text-align: center;">JPEG</td>
+ * <td style="text-align: center;">320x240 (240p)</td>
+ * <td style="text-align: center;">Any</td>
+ * <td style="text-align: center;">if 240p <= activeArraySize</td>
* </tr>
* <tr>
- * <td align="center">YUV_420_888</td>
- * <td align="center">all output sizes available for JPEG</td>
- * <td align="center">FULL</td>
- * <td align="center"></td>
+ * <td style="text-align: center;">YUV_420_888</td>
+ * <td style="text-align: center;">all output sizes available for JPEG</td>
+ * <td style="text-align: center;">FULL</td>
+ * <td style="text-align: center;"></td>
* </tr>
* <tr>
- * <td align="center">YUV_420_888</td>
- * <td align="center">all output sizes available for JPEG, up to the maximum video size</td>
- * <td align="center">LIMITED</td>
- * <td align="center"></td>
+ * <td style="text-align: center;">YUV_420_888</td>
+ * <td style="text-align: center;">all output sizes available for JPEG, up to the maximum video size</td>
+ * <td style="text-align: center;">LIMITED</td>
+ * <td style="text-align: center;"></td>
* </tr>
* <tr>
- * <td align="center">IMPLEMENTATION_DEFINED</td>
- * <td align="center">same as YUV_420_888</td>
- * <td align="center">Any</td>
- * <td align="center"></td>
+ * <td style="text-align: center;">IMPLEMENTATION_DEFINED</td>
+ * <td style="text-align: center;">same as YUV_420_888</td>
+ * <td style="text-align: center;">Any</td>
+ * <td style="text-align: center;"></td>
* </tr>
* </tbody>
* </table>
@@ -2773,66 +2773,66 @@
* <table>
* <thead>
* <tr>
- * <th align="center">Format</th>
- * <th align="center">Size</th>
- * <th align="center">Hardware Level</th>
- * <th align="center">Notes</th>
+ * <th style="text-align: center;">Format</th>
+ * <th style="text-align: center;">Size</th>
+ * <th style="text-align: center;">Hardware Level</th>
+ * <th style="text-align: center;">Notes</th>
* </tr>
* </thead>
* <tbody>
* <tr>
- * <td align="center">JPEG</td>
- * <td align="center">{@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}</td>
- * <td align="center">Any</td>
- * <td align="center"></td>
+ * <td style="text-align: center;">JPEG</td>
+ * <td style="text-align: center;">{@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}</td>
+ * <td style="text-align: center;">Any</td>
+ * <td style="text-align: center;"></td>
* </tr>
* <tr>
- * <td align="center">JPEG</td>
- * <td align="center">1920x1080 (1080p)</td>
- * <td align="center">Any</td>
- * <td align="center">if 1080p <= activeArraySize</td>
+ * <td style="text-align: center;">JPEG</td>
+ * <td style="text-align: center;">1920x1080 (1080p)</td>
+ * <td style="text-align: center;">Any</td>
+ * <td style="text-align: center;">if 1080p <= activeArraySize</td>
* </tr>
* <tr>
- * <td align="center">YUV_420_888</td>
- * <td align="center">{@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}</td>
- * <td align="center">FULL</td>
- * <td align="center"></td>
+ * <td style="text-align: center;">YUV_420_888</td>
+ * <td style="text-align: center;">{@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}</td>
+ * <td style="text-align: center;">FULL</td>
+ * <td style="text-align: center;"></td>
* </tr>
* <tr>
- * <td align="center">YUV_420_888</td>
- * <td align="center">1920x1080 (1080p)</td>
- * <td align="center">FULL</td>
- * <td align="center">if 1080p <= activeArraySize</td>
+ * <td style="text-align: center;">YUV_420_888</td>
+ * <td style="text-align: center;">1920x1080 (1080p)</td>
+ * <td style="text-align: center;">FULL</td>
+ * <td style="text-align: center;">if 1080p <= activeArraySize</td>
* </tr>
* <tr>
- * <td align="center">YUV_420_888</td>
- * <td align="center">1280x720 (720)</td>
- * <td align="center">FULL</td>
- * <td align="center">if 720p <= activeArraySize</td>
+ * <td style="text-align: center;">YUV_420_888</td>
+ * <td style="text-align: center;">1280x720 (720)</td>
+ * <td style="text-align: center;">FULL</td>
+ * <td style="text-align: center;">if 720p <= activeArraySize</td>
* </tr>
* <tr>
- * <td align="center">YUV_420_888</td>
- * <td align="center">640x480 (480p)</td>
- * <td align="center">FULL</td>
- * <td align="center">if 480p <= activeArraySize</td>
+ * <td style="text-align: center;">YUV_420_888</td>
+ * <td style="text-align: center;">640x480 (480p)</td>
+ * <td style="text-align: center;">FULL</td>
+ * <td style="text-align: center;">if 480p <= activeArraySize</td>
* </tr>
* <tr>
- * <td align="center">YUV_420_888</td>
- * <td align="center">320x240 (240p)</td>
- * <td align="center">FULL</td>
- * <td align="center">if 240p <= activeArraySize</td>
+ * <td style="text-align: center;">YUV_420_888</td>
+ * <td style="text-align: center;">320x240 (240p)</td>
+ * <td style="text-align: center;">FULL</td>
+ * <td style="text-align: center;">if 240p <= activeArraySize</td>
* </tr>
* <tr>
- * <td align="center">YUV_420_888</td>
- * <td align="center">all output sizes available for FULL hardware level, up to the maximum video size</td>
- * <td align="center">LIMITED</td>
- * <td align="center"></td>
+ * <td style="text-align: center;">YUV_420_888</td>
+ * <td style="text-align: center;">all output sizes available for FULL hardware level, up to the maximum video size</td>
+ * <td style="text-align: center;">LIMITED</td>
+ * <td style="text-align: center;"></td>
* </tr>
* <tr>
- * <td align="center">IMPLEMENTATION_DEFINED</td>
- * <td align="center">same as YUV_420_888</td>
- * <td align="center">Any</td>
- * <td align="center"></td>
+ * <td style="text-align: center;">IMPLEMENTATION_DEFINED</td>
+ * <td style="text-align: center;">same as YUV_420_888</td>
+ * <td style="text-align: center;">Any</td>
+ * <td style="text-align: center;"></td>
* </tr>
* </tbody>
* </table>
@@ -2987,66 +2987,66 @@
* <table>
* <thead>
* <tr>
- * <th align="center">Format</th>
- * <th align="center">Size</th>
- * <th align="center">Hardware Level</th>
- * <th align="center">Notes</th>
+ * <th style="text-align: center;">Format</th>
+ * <th style="text-align: center;">Size</th>
+ * <th style="text-align: center;">Hardware Level</th>
+ * <th style="text-align: center;">Notes</th>
* </tr>
* </thead>
* <tbody>
* <tr>
- * <td align="center">{@link android.graphics.ImageFormat#JPEG }</td>
- * <td align="center">{@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize} (*1)</td>
- * <td align="center">Any</td>
- * <td align="center"></td>
+ * <td style="text-align: center;">{@link android.graphics.ImageFormat#JPEG }</td>
+ * <td style="text-align: center;">{@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize} (*1)</td>
+ * <td style="text-align: center;">Any</td>
+ * <td style="text-align: center;"></td>
* </tr>
* <tr>
- * <td align="center">{@link android.graphics.ImageFormat#JPEG }</td>
- * <td align="center">1920x1080 (1080p)</td>
- * <td align="center">Any</td>
- * <td align="center">if 1080p <= activeArraySize</td>
+ * <td style="text-align: center;">{@link android.graphics.ImageFormat#JPEG }</td>
+ * <td style="text-align: center;">1920x1080 (1080p)</td>
+ * <td style="text-align: center;">Any</td>
+ * <td style="text-align: center;">if 1080p <= activeArraySize</td>
* </tr>
* <tr>
- * <td align="center">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
- * <td align="center">{@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}</td>
- * <td align="center">FULL</td>
- * <td align="center"></td>
+ * <td style="text-align: center;">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
+ * <td style="text-align: center;">{@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}</td>
+ * <td style="text-align: center;">FULL</td>
+ * <td style="text-align: center;"></td>
* </tr>
* <tr>
- * <td align="center">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
- * <td align="center">1920x1080 (1080p)</td>
- * <td align="center">FULL</td>
- * <td align="center">if 1080p <= activeArraySize</td>
+ * <td style="text-align: center;">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
+ * <td style="text-align: center;">1920x1080 (1080p)</td>
+ * <td style="text-align: center;">FULL</td>
+ * <td style="text-align: center;">if 1080p <= activeArraySize</td>
* </tr>
* <tr>
- * <td align="center">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
- * <td align="center">1280x720 (720)</td>
- * <td align="center">FULL</td>
- * <td align="center">if 720p <= activeArraySize</td>
+ * <td style="text-align: center;">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
+ * <td style="text-align: center;">1280x720 (720)</td>
+ * <td style="text-align: center;">FULL</td>
+ * <td style="text-align: center;">if 720p <= activeArraySize</td>
* </tr>
* <tr>
- * <td align="center">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
- * <td align="center">640x480 (480p)</td>
- * <td align="center">FULL</td>
- * <td align="center">if 480p <= activeArraySize</td>
+ * <td style="text-align: center;">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
+ * <td style="text-align: center;">640x480 (480p)</td>
+ * <td style="text-align: center;">FULL</td>
+ * <td style="text-align: center;">if 480p <= activeArraySize</td>
* </tr>
* <tr>
- * <td align="center">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
- * <td align="center">320x240 (240p)</td>
- * <td align="center">FULL</td>
- * <td align="center">if 240p <= activeArraySize</td>
+ * <td style="text-align: center;">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
+ * <td style="text-align: center;">320x240 (240p)</td>
+ * <td style="text-align: center;">FULL</td>
+ * <td style="text-align: center;">if 240p <= activeArraySize</td>
* </tr>
* <tr>
- * <td align="center">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
- * <td align="center">all output sizes available for FULL hardware level, up to the maximum video size</td>
- * <td align="center">LIMITED</td>
- * <td align="center"></td>
+ * <td style="text-align: center;">{@link android.graphics.ImageFormat#YUV_420_888 }</td>
+ * <td style="text-align: center;">all output sizes available for FULL hardware level, up to the maximum video size</td>
+ * <td style="text-align: center;">LIMITED</td>
+ * <td style="text-align: center;"></td>
* </tr>
* <tr>
- * <td align="center">{@link android.graphics.ImageFormat#PRIVATE }</td>
- * <td align="center">same as YUV_420_888</td>
- * <td align="center">Any</td>
- * <td align="center"></td>
+ * <td style="text-align: center;">{@link android.graphics.ImageFormat#PRIVATE }</td>
+ * <td style="text-align: center;">same as YUV_420_888</td>
+ * <td style="text-align: center;">Any</td>
+ * <td style="text-align: center;"></td>
* </tr>
* </tbody>
* </table>
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index 3e1deb2..1a15596 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -990,18 +990,18 @@
* <table>
* <thead>
* <tr>
- * <th align="center">State</th>
- * <th align="center">Transition Cause</th>
- * <th align="center">New State</th>
- * <th align="center">Notes</th>
+ * <th style="text-align: center;">State</th>
+ * <th style="text-align: center;">Transition Cause</th>
+ * <th style="text-align: center;">New State</th>
+ * <th style="text-align: center;">Notes</th>
* </tr>
* </thead>
* <tbody>
* <tr>
- * <td align="center">INACTIVE</td>
- * <td align="center"></td>
- * <td align="center">INACTIVE</td>
- * <td align="center">Camera device auto exposure algorithm is disabled</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;"></td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Camera device auto exposure algorithm is disabled</td>
* </tr>
* </tbody>
* </table>
@@ -1009,120 +1009,120 @@
* <table>
* <thead>
* <tr>
- * <th align="center">State</th>
- * <th align="center">Transition Cause</th>
- * <th align="center">New State</th>
- * <th align="center">Notes</th>
+ * <th style="text-align: center;">State</th>
+ * <th style="text-align: center;">Transition Cause</th>
+ * <th style="text-align: center;">New State</th>
+ * <th style="text-align: center;">Notes</th>
* </tr>
* </thead>
* <tbody>
* <tr>
- * <td align="center">INACTIVE</td>
- * <td align="center">Camera device initiates AE scan</td>
- * <td align="center">SEARCHING</td>
- * <td align="center">Values changing</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Camera device initiates AE scan</td>
+ * <td style="text-align: center;">SEARCHING</td>
+ * <td style="text-align: center;">Values changing</td>
* </tr>
* <tr>
- * <td align="center">INACTIVE</td>
- * <td align="center">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is ON</td>
- * <td align="center">LOCKED</td>
- * <td align="center">Values locked</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is ON</td>
+ * <td style="text-align: center;">LOCKED</td>
+ * <td style="text-align: center;">Values locked</td>
* </tr>
* <tr>
- * <td align="center">SEARCHING</td>
- * <td align="center">Camera device finishes AE scan</td>
- * <td align="center">CONVERGED</td>
- * <td align="center">Good values, not changing</td>
+ * <td style="text-align: center;">SEARCHING</td>
+ * <td style="text-align: center;">Camera device finishes AE scan</td>
+ * <td style="text-align: center;">CONVERGED</td>
+ * <td style="text-align: center;">Good values, not changing</td>
* </tr>
* <tr>
- * <td align="center">SEARCHING</td>
- * <td align="center">Camera device finishes AE scan</td>
- * <td align="center">FLASH_REQUIRED</td>
- * <td align="center">Converged but too dark w/o flash</td>
+ * <td style="text-align: center;">SEARCHING</td>
+ * <td style="text-align: center;">Camera device finishes AE scan</td>
+ * <td style="text-align: center;">FLASH_REQUIRED</td>
+ * <td style="text-align: center;">Converged but too dark w/o flash</td>
* </tr>
* <tr>
- * <td align="center">SEARCHING</td>
- * <td align="center">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is ON</td>
- * <td align="center">LOCKED</td>
- * <td align="center">Values locked</td>
+ * <td style="text-align: center;">SEARCHING</td>
+ * <td style="text-align: center;">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is ON</td>
+ * <td style="text-align: center;">LOCKED</td>
+ * <td style="text-align: center;">Values locked</td>
* </tr>
* <tr>
- * <td align="center">CONVERGED</td>
- * <td align="center">Camera device initiates AE scan</td>
- * <td align="center">SEARCHING</td>
- * <td align="center">Values changing</td>
+ * <td style="text-align: center;">CONVERGED</td>
+ * <td style="text-align: center;">Camera device initiates AE scan</td>
+ * <td style="text-align: center;">SEARCHING</td>
+ * <td style="text-align: center;">Values changing</td>
* </tr>
* <tr>
- * <td align="center">CONVERGED</td>
- * <td align="center">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is ON</td>
- * <td align="center">LOCKED</td>
- * <td align="center">Values locked</td>
+ * <td style="text-align: center;">CONVERGED</td>
+ * <td style="text-align: center;">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is ON</td>
+ * <td style="text-align: center;">LOCKED</td>
+ * <td style="text-align: center;">Values locked</td>
* </tr>
* <tr>
- * <td align="center">FLASH_REQUIRED</td>
- * <td align="center">Camera device initiates AE scan</td>
- * <td align="center">SEARCHING</td>
- * <td align="center">Values changing</td>
+ * <td style="text-align: center;">FLASH_REQUIRED</td>
+ * <td style="text-align: center;">Camera device initiates AE scan</td>
+ * <td style="text-align: center;">SEARCHING</td>
+ * <td style="text-align: center;">Values changing</td>
* </tr>
* <tr>
- * <td align="center">FLASH_REQUIRED</td>
- * <td align="center">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is ON</td>
- * <td align="center">LOCKED</td>
- * <td align="center">Values locked</td>
+ * <td style="text-align: center;">FLASH_REQUIRED</td>
+ * <td style="text-align: center;">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is ON</td>
+ * <td style="text-align: center;">LOCKED</td>
+ * <td style="text-align: center;">Values locked</td>
* </tr>
* <tr>
- * <td align="center">LOCKED</td>
- * <td align="center">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is OFF</td>
- * <td align="center">SEARCHING</td>
- * <td align="center">Values not good after unlock</td>
+ * <td style="text-align: center;">LOCKED</td>
+ * <td style="text-align: center;">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is OFF</td>
+ * <td style="text-align: center;">SEARCHING</td>
+ * <td style="text-align: center;">Values not good after unlock</td>
* </tr>
* <tr>
- * <td align="center">LOCKED</td>
- * <td align="center">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is OFF</td>
- * <td align="center">CONVERGED</td>
- * <td align="center">Values good after unlock</td>
+ * <td style="text-align: center;">LOCKED</td>
+ * <td style="text-align: center;">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is OFF</td>
+ * <td style="text-align: center;">CONVERGED</td>
+ * <td style="text-align: center;">Values good after unlock</td>
* </tr>
* <tr>
- * <td align="center">LOCKED</td>
- * <td align="center">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is OFF</td>
- * <td align="center">FLASH_REQUIRED</td>
- * <td align="center">Exposure good, but too dark</td>
+ * <td style="text-align: center;">LOCKED</td>
+ * <td style="text-align: center;">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is OFF</td>
+ * <td style="text-align: center;">FLASH_REQUIRED</td>
+ * <td style="text-align: center;">Exposure good, but too dark</td>
* </tr>
* <tr>
- * <td align="center">PRECAPTURE</td>
- * <td align="center">Sequence done. {@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is OFF</td>
- * <td align="center">CONVERGED</td>
- * <td align="center">Ready for high-quality capture</td>
+ * <td style="text-align: center;">PRECAPTURE</td>
+ * <td style="text-align: center;">Sequence done. {@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is OFF</td>
+ * <td style="text-align: center;">CONVERGED</td>
+ * <td style="text-align: center;">Ready for high-quality capture</td>
* </tr>
* <tr>
- * <td align="center">PRECAPTURE</td>
- * <td align="center">Sequence done. {@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is ON</td>
- * <td align="center">LOCKED</td>
- * <td align="center">Ready for high-quality capture</td>
+ * <td style="text-align: center;">PRECAPTURE</td>
+ * <td style="text-align: center;">Sequence done. {@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is ON</td>
+ * <td style="text-align: center;">LOCKED</td>
+ * <td style="text-align: center;">Ready for high-quality capture</td>
* </tr>
* <tr>
- * <td align="center">LOCKED</td>
- * <td align="center">aeLock is ON and aePrecaptureTrigger is START</td>
- * <td align="center">LOCKED</td>
- * <td align="center">Precapture trigger is ignored when AE is already locked</td>
+ * <td style="text-align: center;">LOCKED</td>
+ * <td style="text-align: center;">aeLock is ON and aePrecaptureTrigger is START</td>
+ * <td style="text-align: center;">LOCKED</td>
+ * <td style="text-align: center;">Precapture trigger is ignored when AE is already locked</td>
* </tr>
* <tr>
- * <td align="center">LOCKED</td>
- * <td align="center">aeLock is ON and aePrecaptureTrigger is CANCEL</td>
- * <td align="center">LOCKED</td>
- * <td align="center">Precapture trigger is ignored when AE is already locked</td>
+ * <td style="text-align: center;">LOCKED</td>
+ * <td style="text-align: center;">aeLock is ON and aePrecaptureTrigger is CANCEL</td>
+ * <td style="text-align: center;">LOCKED</td>
+ * <td style="text-align: center;">Precapture trigger is ignored when AE is already locked</td>
* </tr>
* <tr>
- * <td align="center">Any state (excluding LOCKED)</td>
- * <td align="center">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is START</td>
- * <td align="center">PRECAPTURE</td>
- * <td align="center">Start AE precapture metering sequence</td>
+ * <td style="text-align: center;">Any state (excluding LOCKED)</td>
+ * <td style="text-align: center;">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is START</td>
+ * <td style="text-align: center;">PRECAPTURE</td>
+ * <td style="text-align: center;">Start AE precapture metering sequence</td>
* </tr>
* <tr>
- * <td align="center">Any state (excluding LOCKED)</td>
- * <td align="center">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is CANCEL</td>
- * <td align="center">INACTIVE</td>
- * <td align="center">Currently active precapture metering sequence is canceled</td>
+ * <td style="text-align: center;">Any state (excluding LOCKED)</td>
+ * <td style="text-align: center;">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is CANCEL</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Currently active precapture metering sequence is canceled</td>
* </tr>
* </tbody>
* </table>
@@ -1138,54 +1138,54 @@
* <table>
* <thead>
* <tr>
- * <th align="center">State</th>
- * <th align="center">Transition Cause</th>
- * <th align="center">New State</th>
- * <th align="center">Notes</th>
+ * <th style="text-align: center;">State</th>
+ * <th style="text-align: center;">Transition Cause</th>
+ * <th style="text-align: center;">New State</th>
+ * <th style="text-align: center;">Notes</th>
* </tr>
* </thead>
* <tbody>
* <tr>
- * <td align="center">INACTIVE</td>
- * <td align="center">Camera device finished AE scan</td>
- * <td align="center">CONVERGED</td>
- * <td align="center">Values are already good, transient states are skipped by camera device.</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Camera device finished AE scan</td>
+ * <td style="text-align: center;">CONVERGED</td>
+ * <td style="text-align: center;">Values are already good, transient states are skipped by camera device.</td>
* </tr>
* <tr>
- * <td align="center">Any state (excluding LOCKED)</td>
- * <td align="center">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is START, sequence done</td>
- * <td align="center">FLASH_REQUIRED</td>
- * <td align="center">Converged but too dark w/o flash after a precapture sequence, transient states are skipped by camera device.</td>
+ * <td style="text-align: center;">Any state (excluding LOCKED)</td>
+ * <td style="text-align: center;">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is START, sequence done</td>
+ * <td style="text-align: center;">FLASH_REQUIRED</td>
+ * <td style="text-align: center;">Converged but too dark w/o flash after a precapture sequence, transient states are skipped by camera device.</td>
* </tr>
* <tr>
- * <td align="center">Any state (excluding LOCKED)</td>
- * <td align="center">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is START, sequence done</td>
- * <td align="center">CONVERGED</td>
- * <td align="center">Converged after a precapture sequence, transient states are skipped by camera device.</td>
+ * <td style="text-align: center;">Any state (excluding LOCKED)</td>
+ * <td style="text-align: center;">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is START, sequence done</td>
+ * <td style="text-align: center;">CONVERGED</td>
+ * <td style="text-align: center;">Converged after a precapture sequence, transient states are skipped by camera device.</td>
* </tr>
* <tr>
- * <td align="center">Any state (excluding LOCKED)</td>
- * <td align="center">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is CANCEL, converged</td>
- * <td align="center">FLASH_REQUIRED</td>
- * <td align="center">Converged but too dark w/o flash after a precapture sequence is canceled, transient states are skipped by camera device.</td>
+ * <td style="text-align: center;">Any state (excluding LOCKED)</td>
+ * <td style="text-align: center;">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is CANCEL, converged</td>
+ * <td style="text-align: center;">FLASH_REQUIRED</td>
+ * <td style="text-align: center;">Converged but too dark w/o flash after a precapture sequence is canceled, transient states are skipped by camera device.</td>
* </tr>
* <tr>
- * <td align="center">Any state (excluding LOCKED)</td>
- * <td align="center">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is CANCEL, converged</td>
- * <td align="center">CONVERGED</td>
- * <td align="center">Converged after a precapture sequences canceled, transient states are skipped by camera device.</td>
+ * <td style="text-align: center;">Any state (excluding LOCKED)</td>
+ * <td style="text-align: center;">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is CANCEL, converged</td>
+ * <td style="text-align: center;">CONVERGED</td>
+ * <td style="text-align: center;">Converged after a precapture sequences canceled, transient states are skipped by camera device.</td>
* </tr>
* <tr>
- * <td align="center">CONVERGED</td>
- * <td align="center">Camera device finished AE scan</td>
- * <td align="center">FLASH_REQUIRED</td>
- * <td align="center">Converged but too dark w/o flash after a new scan, transient states are skipped by camera device.</td>
+ * <td style="text-align: center;">CONVERGED</td>
+ * <td style="text-align: center;">Camera device finished AE scan</td>
+ * <td style="text-align: center;">FLASH_REQUIRED</td>
+ * <td style="text-align: center;">Converged but too dark w/o flash after a new scan, transient states are skipped by camera device.</td>
* </tr>
* <tr>
- * <td align="center">FLASH_REQUIRED</td>
- * <td align="center">Camera device finished AE scan</td>
- * <td align="center">CONVERGED</td>
- * <td align="center">Converged after a new scan, transient states are skipped by camera device.</td>
+ * <td style="text-align: center;">FLASH_REQUIRED</td>
+ * <td style="text-align: center;">Camera device finished AE scan</td>
+ * <td style="text-align: center;">CONVERGED</td>
+ * <td style="text-align: center;">Converged after a new scan, transient states are skipped by camera device.</td>
* </tr>
* </tbody>
* </table>
@@ -1413,18 +1413,18 @@
* <table>
* <thead>
* <tr>
- * <th align="center">State</th>
- * <th align="center">Transition Cause</th>
- * <th align="center">New State</th>
- * <th align="center">Notes</th>
+ * <th style="text-align: center;">State</th>
+ * <th style="text-align: center;">Transition Cause</th>
+ * <th style="text-align: center;">New State</th>
+ * <th style="text-align: center;">Notes</th>
* </tr>
* </thead>
* <tbody>
* <tr>
- * <td align="center">INACTIVE</td>
- * <td align="center"></td>
- * <td align="center">INACTIVE</td>
- * <td align="center">Never changes</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;"></td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Never changes</td>
* </tr>
* </tbody>
* </table>
@@ -1432,66 +1432,66 @@
* <table>
* <thead>
* <tr>
- * <th align="center">State</th>
- * <th align="center">Transition Cause</th>
- * <th align="center">New State</th>
- * <th align="center">Notes</th>
+ * <th style="text-align: center;">State</th>
+ * <th style="text-align: center;">Transition Cause</th>
+ * <th style="text-align: center;">New State</th>
+ * <th style="text-align: center;">Notes</th>
* </tr>
* </thead>
* <tbody>
* <tr>
- * <td align="center">INACTIVE</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">ACTIVE_SCAN</td>
- * <td align="center">Start AF sweep, Lens now moving</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">ACTIVE_SCAN</td>
+ * <td style="text-align: center;">Start AF sweep, Lens now moving</td>
* </tr>
* <tr>
- * <td align="center">ACTIVE_SCAN</td>
- * <td align="center">AF sweep done</td>
- * <td align="center">FOCUSED_LOCKED</td>
- * <td align="center">Focused, Lens now locked</td>
+ * <td style="text-align: center;">ACTIVE_SCAN</td>
+ * <td style="text-align: center;">AF sweep done</td>
+ * <td style="text-align: center;">FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">Focused, Lens now locked</td>
* </tr>
* <tr>
- * <td align="center">ACTIVE_SCAN</td>
- * <td align="center">AF sweep done</td>
- * <td align="center">NOT_FOCUSED_LOCKED</td>
- * <td align="center">Not focused, Lens now locked</td>
+ * <td style="text-align: center;">ACTIVE_SCAN</td>
+ * <td style="text-align: center;">AF sweep done</td>
+ * <td style="text-align: center;">NOT_FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">Not focused, Lens now locked</td>
* </tr>
* <tr>
- * <td align="center">ACTIVE_SCAN</td>
- * <td align="center">AF_CANCEL</td>
- * <td align="center">INACTIVE</td>
- * <td align="center">Cancel/reset AF, Lens now locked</td>
+ * <td style="text-align: center;">ACTIVE_SCAN</td>
+ * <td style="text-align: center;">AF_CANCEL</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Cancel/reset AF, Lens now locked</td>
* </tr>
* <tr>
- * <td align="center">FOCUSED_LOCKED</td>
- * <td align="center">AF_CANCEL</td>
- * <td align="center">INACTIVE</td>
- * <td align="center">Cancel/reset AF</td>
+ * <td style="text-align: center;">FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">AF_CANCEL</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Cancel/reset AF</td>
* </tr>
* <tr>
- * <td align="center">FOCUSED_LOCKED</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">ACTIVE_SCAN</td>
- * <td align="center">Start new sweep, Lens now moving</td>
+ * <td style="text-align: center;">FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">ACTIVE_SCAN</td>
+ * <td style="text-align: center;">Start new sweep, Lens now moving</td>
* </tr>
* <tr>
- * <td align="center">NOT_FOCUSED_LOCKED</td>
- * <td align="center">AF_CANCEL</td>
- * <td align="center">INACTIVE</td>
- * <td align="center">Cancel/reset AF</td>
+ * <td style="text-align: center;">NOT_FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">AF_CANCEL</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Cancel/reset AF</td>
* </tr>
* <tr>
- * <td align="center">NOT_FOCUSED_LOCKED</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">ACTIVE_SCAN</td>
- * <td align="center">Start new sweep, Lens now moving</td>
+ * <td style="text-align: center;">NOT_FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">ACTIVE_SCAN</td>
+ * <td style="text-align: center;">Start new sweep, Lens now moving</td>
* </tr>
* <tr>
- * <td align="center">Any state</td>
- * <td align="center">Mode change</td>
- * <td align="center">INACTIVE</td>
- * <td align="center"></td>
+ * <td style="text-align: center;">Any state</td>
+ * <td style="text-align: center;">Mode change</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;"></td>
* </tr>
* </tbody>
* </table>
@@ -1504,36 +1504,36 @@
* <table>
* <thead>
* <tr>
- * <th align="center">State</th>
- * <th align="center">Transition Cause</th>
- * <th align="center">New State</th>
- * <th align="center">Notes</th>
+ * <th style="text-align: center;">State</th>
+ * <th style="text-align: center;">Transition Cause</th>
+ * <th style="text-align: center;">New State</th>
+ * <th style="text-align: center;">Notes</th>
* </tr>
* </thead>
* <tbody>
* <tr>
- * <td align="center">INACTIVE</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">FOCUSED_LOCKED</td>
- * <td align="center">Focus is already good or good after a scan, lens is now locked.</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">Focus is already good or good after a scan, lens is now locked.</td>
* </tr>
* <tr>
- * <td align="center">INACTIVE</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">NOT_FOCUSED_LOCKED</td>
- * <td align="center">Focus failed after a scan, lens is now locked.</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">NOT_FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">Focus failed after a scan, lens is now locked.</td>
* </tr>
* <tr>
- * <td align="center">FOCUSED_LOCKED</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">FOCUSED_LOCKED</td>
- * <td align="center">Focus is already good or good after a scan, lens is now locked.</td>
+ * <td style="text-align: center;">FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">Focus is already good or good after a scan, lens is now locked.</td>
* </tr>
* <tr>
- * <td align="center">NOT_FOCUSED_LOCKED</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">FOCUSED_LOCKED</td>
- * <td align="center">Focus is good after a scan, lens is not locked.</td>
+ * <td style="text-align: center;">NOT_FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">Focus is good after a scan, lens is not locked.</td>
* </tr>
* </tbody>
* </table>
@@ -1541,102 +1541,102 @@
* <table>
* <thead>
* <tr>
- * <th align="center">State</th>
- * <th align="center">Transition Cause</th>
- * <th align="center">New State</th>
- * <th align="center">Notes</th>
+ * <th style="text-align: center;">State</th>
+ * <th style="text-align: center;">Transition Cause</th>
+ * <th style="text-align: center;">New State</th>
+ * <th style="text-align: center;">Notes</th>
* </tr>
* </thead>
* <tbody>
* <tr>
- * <td align="center">INACTIVE</td>
- * <td align="center">Camera device initiates new scan</td>
- * <td align="center">PASSIVE_SCAN</td>
- * <td align="center">Start AF scan, Lens now moving</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Camera device initiates new scan</td>
+ * <td style="text-align: center;">PASSIVE_SCAN</td>
+ * <td style="text-align: center;">Start AF scan, Lens now moving</td>
* </tr>
* <tr>
- * <td align="center">INACTIVE</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">NOT_FOCUSED_LOCKED</td>
- * <td align="center">AF state query, Lens now locked</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">NOT_FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">AF state query, Lens now locked</td>
* </tr>
* <tr>
- * <td align="center">PASSIVE_SCAN</td>
- * <td align="center">Camera device completes current scan</td>
- * <td align="center">PASSIVE_FOCUSED</td>
- * <td align="center">End AF scan, Lens now locked</td>
+ * <td style="text-align: center;">PASSIVE_SCAN</td>
+ * <td style="text-align: center;">Camera device completes current scan</td>
+ * <td style="text-align: center;">PASSIVE_FOCUSED</td>
+ * <td style="text-align: center;">End AF scan, Lens now locked</td>
* </tr>
* <tr>
- * <td align="center">PASSIVE_SCAN</td>
- * <td align="center">Camera device fails current scan</td>
- * <td align="center">PASSIVE_UNFOCUSED</td>
- * <td align="center">End AF scan, Lens now locked</td>
+ * <td style="text-align: center;">PASSIVE_SCAN</td>
+ * <td style="text-align: center;">Camera device fails current scan</td>
+ * <td style="text-align: center;">PASSIVE_UNFOCUSED</td>
+ * <td style="text-align: center;">End AF scan, Lens now locked</td>
* </tr>
* <tr>
- * <td align="center">PASSIVE_SCAN</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">FOCUSED_LOCKED</td>
- * <td align="center">Immediate transition, if focus is good. Lens now locked</td>
+ * <td style="text-align: center;">PASSIVE_SCAN</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">Immediate transition, if focus is good. Lens now locked</td>
* </tr>
* <tr>
- * <td align="center">PASSIVE_SCAN</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">NOT_FOCUSED_LOCKED</td>
- * <td align="center">Immediate transition, if focus is bad. Lens now locked</td>
+ * <td style="text-align: center;">PASSIVE_SCAN</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">NOT_FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">Immediate transition, if focus is bad. Lens now locked</td>
* </tr>
* <tr>
- * <td align="center">PASSIVE_SCAN</td>
- * <td align="center">AF_CANCEL</td>
- * <td align="center">INACTIVE</td>
- * <td align="center">Reset lens position, Lens now locked</td>
+ * <td style="text-align: center;">PASSIVE_SCAN</td>
+ * <td style="text-align: center;">AF_CANCEL</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Reset lens position, Lens now locked</td>
* </tr>
* <tr>
- * <td align="center">PASSIVE_FOCUSED</td>
- * <td align="center">Camera device initiates new scan</td>
- * <td align="center">PASSIVE_SCAN</td>
- * <td align="center">Start AF scan, Lens now moving</td>
+ * <td style="text-align: center;">PASSIVE_FOCUSED</td>
+ * <td style="text-align: center;">Camera device initiates new scan</td>
+ * <td style="text-align: center;">PASSIVE_SCAN</td>
+ * <td style="text-align: center;">Start AF scan, Lens now moving</td>
* </tr>
* <tr>
- * <td align="center">PASSIVE_UNFOCUSED</td>
- * <td align="center">Camera device initiates new scan</td>
- * <td align="center">PASSIVE_SCAN</td>
- * <td align="center">Start AF scan, Lens now moving</td>
+ * <td style="text-align: center;">PASSIVE_UNFOCUSED</td>
+ * <td style="text-align: center;">Camera device initiates new scan</td>
+ * <td style="text-align: center;">PASSIVE_SCAN</td>
+ * <td style="text-align: center;">Start AF scan, Lens now moving</td>
* </tr>
* <tr>
- * <td align="center">PASSIVE_FOCUSED</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">FOCUSED_LOCKED</td>
- * <td align="center">Immediate transition, lens now locked</td>
+ * <td style="text-align: center;">PASSIVE_FOCUSED</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">Immediate transition, lens now locked</td>
* </tr>
* <tr>
- * <td align="center">PASSIVE_UNFOCUSED</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">NOT_FOCUSED_LOCKED</td>
- * <td align="center">Immediate transition, lens now locked</td>
+ * <td style="text-align: center;">PASSIVE_UNFOCUSED</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">NOT_FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">Immediate transition, lens now locked</td>
* </tr>
* <tr>
- * <td align="center">FOCUSED_LOCKED</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">FOCUSED_LOCKED</td>
- * <td align="center">No effect</td>
+ * <td style="text-align: center;">FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">No effect</td>
* </tr>
* <tr>
- * <td align="center">FOCUSED_LOCKED</td>
- * <td align="center">AF_CANCEL</td>
- * <td align="center">INACTIVE</td>
- * <td align="center">Restart AF scan</td>
+ * <td style="text-align: center;">FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">AF_CANCEL</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Restart AF scan</td>
* </tr>
* <tr>
- * <td align="center">NOT_FOCUSED_LOCKED</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">NOT_FOCUSED_LOCKED</td>
- * <td align="center">No effect</td>
+ * <td style="text-align: center;">NOT_FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">NOT_FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">No effect</td>
* </tr>
* <tr>
- * <td align="center">NOT_FOCUSED_LOCKED</td>
- * <td align="center">AF_CANCEL</td>
- * <td align="center">INACTIVE</td>
- * <td align="center">Restart AF scan</td>
+ * <td style="text-align: center;">NOT_FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">AF_CANCEL</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Restart AF scan</td>
* </tr>
* </tbody>
* </table>
@@ -1644,102 +1644,102 @@
* <table>
* <thead>
* <tr>
- * <th align="center">State</th>
- * <th align="center">Transition Cause</th>
- * <th align="center">New State</th>
- * <th align="center">Notes</th>
+ * <th style="text-align: center;">State</th>
+ * <th style="text-align: center;">Transition Cause</th>
+ * <th style="text-align: center;">New State</th>
+ * <th style="text-align: center;">Notes</th>
* </tr>
* </thead>
* <tbody>
* <tr>
- * <td align="center">INACTIVE</td>
- * <td align="center">Camera device initiates new scan</td>
- * <td align="center">PASSIVE_SCAN</td>
- * <td align="center">Start AF scan, Lens now moving</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Camera device initiates new scan</td>
+ * <td style="text-align: center;">PASSIVE_SCAN</td>
+ * <td style="text-align: center;">Start AF scan, Lens now moving</td>
* </tr>
* <tr>
- * <td align="center">INACTIVE</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">NOT_FOCUSED_LOCKED</td>
- * <td align="center">AF state query, Lens now locked</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">NOT_FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">AF state query, Lens now locked</td>
* </tr>
* <tr>
- * <td align="center">PASSIVE_SCAN</td>
- * <td align="center">Camera device completes current scan</td>
- * <td align="center">PASSIVE_FOCUSED</td>
- * <td align="center">End AF scan, Lens now locked</td>
+ * <td style="text-align: center;">PASSIVE_SCAN</td>
+ * <td style="text-align: center;">Camera device completes current scan</td>
+ * <td style="text-align: center;">PASSIVE_FOCUSED</td>
+ * <td style="text-align: center;">End AF scan, Lens now locked</td>
* </tr>
* <tr>
- * <td align="center">PASSIVE_SCAN</td>
- * <td align="center">Camera device fails current scan</td>
- * <td align="center">PASSIVE_UNFOCUSED</td>
- * <td align="center">End AF scan, Lens now locked</td>
+ * <td style="text-align: center;">PASSIVE_SCAN</td>
+ * <td style="text-align: center;">Camera device fails current scan</td>
+ * <td style="text-align: center;">PASSIVE_UNFOCUSED</td>
+ * <td style="text-align: center;">End AF scan, Lens now locked</td>
* </tr>
* <tr>
- * <td align="center">PASSIVE_SCAN</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">FOCUSED_LOCKED</td>
- * <td align="center">Eventual transition once the focus is good. Lens now locked</td>
+ * <td style="text-align: center;">PASSIVE_SCAN</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">Eventual transition once the focus is good. Lens now locked</td>
* </tr>
* <tr>
- * <td align="center">PASSIVE_SCAN</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">NOT_FOCUSED_LOCKED</td>
- * <td align="center">Eventual transition if cannot find focus. Lens now locked</td>
+ * <td style="text-align: center;">PASSIVE_SCAN</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">NOT_FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">Eventual transition if cannot find focus. Lens now locked</td>
* </tr>
* <tr>
- * <td align="center">PASSIVE_SCAN</td>
- * <td align="center">AF_CANCEL</td>
- * <td align="center">INACTIVE</td>
- * <td align="center">Reset lens position, Lens now locked</td>
+ * <td style="text-align: center;">PASSIVE_SCAN</td>
+ * <td style="text-align: center;">AF_CANCEL</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Reset lens position, Lens now locked</td>
* </tr>
* <tr>
- * <td align="center">PASSIVE_FOCUSED</td>
- * <td align="center">Camera device initiates new scan</td>
- * <td align="center">PASSIVE_SCAN</td>
- * <td align="center">Start AF scan, Lens now moving</td>
+ * <td style="text-align: center;">PASSIVE_FOCUSED</td>
+ * <td style="text-align: center;">Camera device initiates new scan</td>
+ * <td style="text-align: center;">PASSIVE_SCAN</td>
+ * <td style="text-align: center;">Start AF scan, Lens now moving</td>
* </tr>
* <tr>
- * <td align="center">PASSIVE_UNFOCUSED</td>
- * <td align="center">Camera device initiates new scan</td>
- * <td align="center">PASSIVE_SCAN</td>
- * <td align="center">Start AF scan, Lens now moving</td>
+ * <td style="text-align: center;">PASSIVE_UNFOCUSED</td>
+ * <td style="text-align: center;">Camera device initiates new scan</td>
+ * <td style="text-align: center;">PASSIVE_SCAN</td>
+ * <td style="text-align: center;">Start AF scan, Lens now moving</td>
* </tr>
* <tr>
- * <td align="center">PASSIVE_FOCUSED</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">FOCUSED_LOCKED</td>
- * <td align="center">Immediate trans. Lens now locked</td>
+ * <td style="text-align: center;">PASSIVE_FOCUSED</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">Immediate trans. Lens now locked</td>
* </tr>
* <tr>
- * <td align="center">PASSIVE_UNFOCUSED</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">NOT_FOCUSED_LOCKED</td>
- * <td align="center">Immediate trans. Lens now locked</td>
+ * <td style="text-align: center;">PASSIVE_UNFOCUSED</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">NOT_FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">Immediate trans. Lens now locked</td>
* </tr>
* <tr>
- * <td align="center">FOCUSED_LOCKED</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">FOCUSED_LOCKED</td>
- * <td align="center">No effect</td>
+ * <td style="text-align: center;">FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">No effect</td>
* </tr>
* <tr>
- * <td align="center">FOCUSED_LOCKED</td>
- * <td align="center">AF_CANCEL</td>
- * <td align="center">INACTIVE</td>
- * <td align="center">Restart AF scan</td>
+ * <td style="text-align: center;">FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">AF_CANCEL</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Restart AF scan</td>
* </tr>
* <tr>
- * <td align="center">NOT_FOCUSED_LOCKED</td>
- * <td align="center">AF_TRIGGER</td>
- * <td align="center">NOT_FOCUSED_LOCKED</td>
- * <td align="center">No effect</td>
+ * <td style="text-align: center;">NOT_FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">AF_TRIGGER</td>
+ * <td style="text-align: center;">NOT_FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">No effect</td>
* </tr>
* <tr>
- * <td align="center">NOT_FOCUSED_LOCKED</td>
- * <td align="center">AF_CANCEL</td>
- * <td align="center">INACTIVE</td>
- * <td align="center">Restart AF scan</td>
+ * <td style="text-align: center;">NOT_FOCUSED_LOCKED</td>
+ * <td style="text-align: center;">AF_CANCEL</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Restart AF scan</td>
* </tr>
* </tbody>
* </table>
@@ -1751,30 +1751,30 @@
* <table>
* <thead>
* <tr>
- * <th align="center">State</th>
- * <th align="center">Transition Cause</th>
- * <th align="center">New State</th>
- * <th align="center">Notes</th>
+ * <th style="text-align: center;">State</th>
+ * <th style="text-align: center;">Transition Cause</th>
+ * <th style="text-align: center;">New State</th>
+ * <th style="text-align: center;">Notes</th>
* </tr>
* </thead>
* <tbody>
* <tr>
- * <td align="center">any state</td>
- * <td align="center">CAF-->AUTO mode switch</td>
- * <td align="center">INACTIVE</td>
- * <td align="center">Mode switch without trigger, initial state must be INACTIVE</td>
+ * <td style="text-align: center;">any state</td>
+ * <td style="text-align: center;">CAF-->AUTO mode switch</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Mode switch without trigger, initial state must be INACTIVE</td>
* </tr>
* <tr>
- * <td align="center">any state</td>
- * <td align="center">CAF-->AUTO mode switch with AF_TRIGGER</td>
- * <td align="center">trigger-reachable states from INACTIVE</td>
- * <td align="center">Mode switch with trigger, INACTIVE is skipped</td>
+ * <td style="text-align: center;">any state</td>
+ * <td style="text-align: center;">CAF-->AUTO mode switch with AF_TRIGGER</td>
+ * <td style="text-align: center;">trigger-reachable states from INACTIVE</td>
+ * <td style="text-align: center;">Mode switch with trigger, INACTIVE is skipped</td>
* </tr>
* <tr>
- * <td align="center">any state</td>
- * <td align="center">AUTO-->CAF mode switch</td>
- * <td align="center">passively reachable states from INACTIVE</td>
- * <td align="center">Mode switch without trigger, passive transient state is skipped</td>
+ * <td style="text-align: center;">any state</td>
+ * <td style="text-align: center;">AUTO-->CAF mode switch</td>
+ * <td style="text-align: center;">passively reachable states from INACTIVE</td>
+ * <td style="text-align: center;">Mode switch without trigger, passive transient state is skipped</td>
* </tr>
* </tbody>
* </table>
@@ -2053,18 +2053,18 @@
* <table>
* <thead>
* <tr>
- * <th align="center">State</th>
- * <th align="center">Transition Cause</th>
- * <th align="center">New State</th>
- * <th align="center">Notes</th>
+ * <th style="text-align: center;">State</th>
+ * <th style="text-align: center;">Transition Cause</th>
+ * <th style="text-align: center;">New State</th>
+ * <th style="text-align: center;">Notes</th>
* </tr>
* </thead>
* <tbody>
* <tr>
- * <td align="center">INACTIVE</td>
- * <td align="center"></td>
- * <td align="center">INACTIVE</td>
- * <td align="center">Camera device auto white balance algorithm is disabled</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;"></td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Camera device auto white balance algorithm is disabled</td>
* </tr>
* </tbody>
* </table>
@@ -2072,54 +2072,54 @@
* <table>
* <thead>
* <tr>
- * <th align="center">State</th>
- * <th align="center">Transition Cause</th>
- * <th align="center">New State</th>
- * <th align="center">Notes</th>
+ * <th style="text-align: center;">State</th>
+ * <th style="text-align: center;">Transition Cause</th>
+ * <th style="text-align: center;">New State</th>
+ * <th style="text-align: center;">Notes</th>
* </tr>
* </thead>
* <tbody>
* <tr>
- * <td align="center">INACTIVE</td>
- * <td align="center">Camera device initiates AWB scan</td>
- * <td align="center">SEARCHING</td>
- * <td align="center">Values changing</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Camera device initiates AWB scan</td>
+ * <td style="text-align: center;">SEARCHING</td>
+ * <td style="text-align: center;">Values changing</td>
* </tr>
* <tr>
- * <td align="center">INACTIVE</td>
- * <td align="center">{@link CaptureRequest#CONTROL_AWB_LOCK android.control.awbLock} is ON</td>
- * <td align="center">LOCKED</td>
- * <td align="center">Values locked</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">{@link CaptureRequest#CONTROL_AWB_LOCK android.control.awbLock} is ON</td>
+ * <td style="text-align: center;">LOCKED</td>
+ * <td style="text-align: center;">Values locked</td>
* </tr>
* <tr>
- * <td align="center">SEARCHING</td>
- * <td align="center">Camera device finishes AWB scan</td>
- * <td align="center">CONVERGED</td>
- * <td align="center">Good values, not changing</td>
+ * <td style="text-align: center;">SEARCHING</td>
+ * <td style="text-align: center;">Camera device finishes AWB scan</td>
+ * <td style="text-align: center;">CONVERGED</td>
+ * <td style="text-align: center;">Good values, not changing</td>
* </tr>
* <tr>
- * <td align="center">SEARCHING</td>
- * <td align="center">{@link CaptureRequest#CONTROL_AWB_LOCK android.control.awbLock} is ON</td>
- * <td align="center">LOCKED</td>
- * <td align="center">Values locked</td>
+ * <td style="text-align: center;">SEARCHING</td>
+ * <td style="text-align: center;">{@link CaptureRequest#CONTROL_AWB_LOCK android.control.awbLock} is ON</td>
+ * <td style="text-align: center;">LOCKED</td>
+ * <td style="text-align: center;">Values locked</td>
* </tr>
* <tr>
- * <td align="center">CONVERGED</td>
- * <td align="center">Camera device initiates AWB scan</td>
- * <td align="center">SEARCHING</td>
- * <td align="center">Values changing</td>
+ * <td style="text-align: center;">CONVERGED</td>
+ * <td style="text-align: center;">Camera device initiates AWB scan</td>
+ * <td style="text-align: center;">SEARCHING</td>
+ * <td style="text-align: center;">Values changing</td>
* </tr>
* <tr>
- * <td align="center">CONVERGED</td>
- * <td align="center">{@link CaptureRequest#CONTROL_AWB_LOCK android.control.awbLock} is ON</td>
- * <td align="center">LOCKED</td>
- * <td align="center">Values locked</td>
+ * <td style="text-align: center;">CONVERGED</td>
+ * <td style="text-align: center;">{@link CaptureRequest#CONTROL_AWB_LOCK android.control.awbLock} is ON</td>
+ * <td style="text-align: center;">LOCKED</td>
+ * <td style="text-align: center;">Values locked</td>
* </tr>
* <tr>
- * <td align="center">LOCKED</td>
- * <td align="center">{@link CaptureRequest#CONTROL_AWB_LOCK android.control.awbLock} is OFF</td>
- * <td align="center">SEARCHING</td>
- * <td align="center">Values not good after unlock</td>
+ * <td style="text-align: center;">LOCKED</td>
+ * <td style="text-align: center;">{@link CaptureRequest#CONTROL_AWB_LOCK android.control.awbLock} is OFF</td>
+ * <td style="text-align: center;">SEARCHING</td>
+ * <td style="text-align: center;">Values not good after unlock</td>
* </tr>
* </tbody>
* </table>
@@ -2132,24 +2132,24 @@
* <table>
* <thead>
* <tr>
- * <th align="center">State</th>
- * <th align="center">Transition Cause</th>
- * <th align="center">New State</th>
- * <th align="center">Notes</th>
+ * <th style="text-align: center;">State</th>
+ * <th style="text-align: center;">Transition Cause</th>
+ * <th style="text-align: center;">New State</th>
+ * <th style="text-align: center;">Notes</th>
* </tr>
* </thead>
* <tbody>
* <tr>
- * <td align="center">INACTIVE</td>
- * <td align="center">Camera device finished AWB scan</td>
- * <td align="center">CONVERGED</td>
- * <td align="center">Values are already good, transient states are skipped by camera device.</td>
+ * <td style="text-align: center;">INACTIVE</td>
+ * <td style="text-align: center;">Camera device finished AWB scan</td>
+ * <td style="text-align: center;">CONVERGED</td>
+ * <td style="text-align: center;">Values are already good, transient states are skipped by camera device.</td>
* </tr>
* <tr>
- * <td align="center">LOCKED</td>
- * <td align="center">{@link CaptureRequest#CONTROL_AWB_LOCK android.control.awbLock} is OFF</td>
- * <td align="center">CONVERGED</td>
- * <td align="center">Values good after unlock, transient states are skipped by camera device.</td>
+ * <td style="text-align: center;">LOCKED</td>
+ * <td style="text-align: center;">{@link CaptureRequest#CONTROL_AWB_LOCK android.control.awbLock} is OFF</td>
+ * <td style="text-align: center;">CONVERGED</td>
+ * <td style="text-align: center;">Values good after unlock, transient states are skipped by camera device.</td>
* </tr>
* </tbody>
* </table>
diff --git a/core/java/android/os/BugreportManager.java b/core/java/android/os/BugreportManager.java
index 73bb8d5..222e88f 100644
--- a/core/java/android/os/BugreportManager.java
+++ b/core/java/android/os/BugreportManager.java
@@ -148,6 +148,29 @@
}
/**
+ * Speculatively pre-dumps UI data for a bugreport request that might come later.
+ *
+ * <p>Triggers the dump of certain critical UI data, e.g. traces stored in short
+ * ring buffers that might get lost by the time the actual bugreport is requested.
+ *
+ * <p>{@link #startBugreport} will then pick the pre-dumped data if both of the following
+ * conditions are met:
+ * - {@link android.os.BugreportParams#BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA} is specified.
+ * - {@link #preDumpUiData} and {@link #startBugreport} were called by the same UID.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.DUMP)
+ @WorkerThread
+ public void preDumpUiData() {
+ try {
+ mBinder.preDumpUiData(mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Starts a bugreport.
*
* <p>This starts a bugreport in the background. However the call itself can take several
@@ -198,6 +221,7 @@
bugreportFd.getFileDescriptor(),
screenshotFd.getFileDescriptor(),
params.getMode(),
+ params.getFlags(),
dsListener,
isScreenshotRequested);
} catch (RemoteException e) {
diff --git a/core/java/android/os/BugreportParams.java b/core/java/android/os/BugreportParams.java
index 279ccae..990883f 100644
--- a/core/java/android/os/BugreportParams.java
+++ b/core/java/android/os/BugreportParams.java
@@ -30,16 +30,46 @@
@SystemApi
public final class BugreportParams {
private final int mMode;
+ private final int mFlags;
+ /**
+ * Constructs a BugreportParams object to specify what kind of bugreport should be taken.
+ *
+ * @param mode of the bugreport to request
+ */
public BugreportParams(@BugreportMode int mode) {
mMode = mode;
+ mFlags = 0;
}
+ /**
+ * Constructs a BugreportParams object to specify what kind of bugreport should be taken.
+ *
+ * @param mode of the bugreport to request
+ * @param flags to customize the bugreport request
+ */
+ public BugreportParams(@BugreportMode int mode, @BugreportFlag int flags) {
+ mMode = mode;
+ mFlags = flags;
+ }
+
+ /**
+ * Returns the mode of the bugreport to request.
+ */
+ @BugreportMode
public int getMode() {
return mMode;
}
/**
+ * Returns the flags to customize the bugreport request.
+ */
+ @BugreportFlag
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /**
* Defines acceptable types of bugreports.
* @hide
*/
@@ -88,4 +118,21 @@
* Wifi.
*/
public static final int BUGREPORT_MODE_WIFI = IDumpstate.BUGREPORT_MODE_WIFI;
+
+ /**
+ * Defines acceptable flags for customizing bugreport requests.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "BUGREPORT_FLAG_" }, value = {
+ BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA
+ })
+ public @interface BugreportFlag {}
+
+ /**
+ * Flag for reusing pre-dumped UI data. The pre-dump and bugreport request calls must be
+ * performed by the same UID, otherwise the flag is ignored.
+ */
+ public static final int BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA =
+ IDumpstate.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA;
}
diff --git a/core/java/android/os/ISystemConfig.aidl b/core/java/android/os/ISystemConfig.aidl
index 15e3ce2..61b24aa 100644
--- a/core/java/android/os/ISystemConfig.aidl
+++ b/core/java/android/os/ISystemConfig.aidl
@@ -47,4 +47,9 @@
* @see SystemConfigManager#getEnabledComponentOverrides
*/
List<ComponentName> getEnabledComponentOverrides(String packageName);
+
+ /**
+ * @see SystemConfigManager#getDefaultVrComponents
+ */
+ List<ComponentName> getDefaultVrComponents();
}
diff --git a/core/java/android/os/SystemConfigManager.java b/core/java/android/os/SystemConfigManager.java
index cde2063..77843d9 100644
--- a/core/java/android/os/SystemConfigManager.java
+++ b/core/java/android/os/SystemConfigManager.java
@@ -147,4 +147,18 @@
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Return the components that are enabled by default as VR mode listener services.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.QUERY_ALL_PACKAGES)
+ public List<ComponentName> getDefaultVrComponents() {
+ try {
+ return mInterface.getDefaultVrComponents();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ return Collections.emptyList();
+ }
}
diff --git a/core/java/android/view/ImeFocusController.java b/core/java/android/view/ImeFocusController.java
index 1afd048..fa647d7 100644
--- a/core/java/android/view/ImeFocusController.java
+++ b/core/java/android/view/ImeFocusController.java
@@ -26,8 +26,6 @@
import android.view.inputmethod.InputMethodManager;
import com.android.internal.inputmethod.InputMethodDebug;
-import com.android.internal.inputmethod.StartInputFlags;
-import com.android.internal.inputmethod.StartInputReason;
/**
* Responsible for IME focus handling inside {@link ViewRootImpl}.
@@ -67,7 +65,8 @@
@UiThread
void onTraversal(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute) {
- final boolean hasImeFocus = updateImeFocusable(windowAttribute, false /* force */);
+ final boolean hasImeFocus = WindowManager.LayoutParams.mayUseInputMethod(
+ windowAttribute.flags);
if (!hasWindowFocus || isInLocalFocusMode(windowAttribute)) {
return;
}
@@ -76,30 +75,20 @@
}
mHasImeFocus = hasImeFocus;
if (mHasImeFocus) {
- onPreWindowFocus(true /* hasWindowFocus */, windowAttribute);
- onPostWindowFocus(mViewRootImpl.mView.findFocus(), true /* hasWindowFocus */,
- windowAttribute);
+ getImmDelegate().onPreWindowGainedFocus(mViewRootImpl);
+ final View focusedView = mViewRootImpl.mView.findFocus();
+ View viewForWindowFocus = focusedView != null ? focusedView : mViewRootImpl.mView;
+ getImmDelegate().onPostWindowGainedFocus(viewForWindowFocus, windowAttribute);
}
}
@UiThread
void onPreWindowFocus(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute) {
- if (!mHasImeFocus || isInLocalFocusMode(windowAttribute)) {
+ mHasImeFocus = WindowManager.LayoutParams.mayUseInputMethod(windowAttribute.flags);
+ if (!hasWindowFocus || !mHasImeFocus || isInLocalFocusMode(windowAttribute)) {
return;
}
- if (hasWindowFocus) {
- getImmDelegate().setCurrentRootView(mViewRootImpl);
- }
- }
-
- @UiThread
- boolean updateImeFocusable(WindowManager.LayoutParams windowAttribute, boolean force) {
- final boolean hasImeFocus = WindowManager.LayoutParams.mayUseInputMethod(
- windowAttribute.flags);
- if (force) {
- mHasImeFocus = hasImeFocus;
- }
- return hasImeFocus;
+ getImmDelegate().onPreWindowGainedFocus(mViewRootImpl);
}
@UiThread
@@ -115,7 +104,7 @@
windowAttribute.softInputMode));
}
- getImmDelegate().onPostWindowFocus(viewForWindowFocus, windowAttribute);
+ getImmDelegate().onPostWindowGainedFocus(viewForWindowFocus, windowAttribute);
}
/**
@@ -189,14 +178,8 @@
* @hide
*/
public interface InputMethodManagerDelegate {
- /**
- * Starts the input connection.
- */
- boolean startInput(@StartInputReason int startInputReason, View focusedView,
- @StartInputFlags int startInputFlags,
- @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, int windowFlags);
-
- void onPostWindowFocus(View viewForWindowFocus,
+ void onPreWindowGainedFocus(ViewRootImpl viewRootImpl);
+ void onPostWindowGainedFocus(View viewForWindowFocus,
@NonNull WindowManager.LayoutParams windowAttribute);
void onViewFocusChanged(@NonNull View view, boolean hasFocus);
boolean checkFocus(boolean forceNewFocus, boolean startInput, ViewRootImpl viewRootImpl);
@@ -204,7 +187,6 @@
void onWindowDismissed(ViewRootImpl viewRootImpl);
void finishInputAndReportToIme();
- void setCurrentRootView(ViewRootImpl rootView);
boolean isCurrentRootView(ViewRootImpl rootView);
}
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index b5dff5e..ceab310 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -1272,6 +1272,25 @@
*/
public static final int AXIS_GENERIC_16 = 47;
+ /**
+ * Axis constant: X gesture offset axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a touch pad, reports the distance that a swipe gesture has moved in the X axis, as a
+ * proportion of the touch pad's size. For example, if a touch pad is 1000 units wide, and a
+ * swipe gesture starts at X = 500 then moves to X = 400, this axis would have a value of
+ * -0.1.
+ * </ul>
+ */
+ public static final int AXIS_GESTURE_X_OFFSET = 48;
+
+ /**
+ * Axis constant: Y gesture offset axis of a motion event.
+ *
+ * The same as {@link #AXIS_GESTURE_X_OFFSET}, but for the Y axis.
+ */
+ public static final int AXIS_GESTURE_Y_OFFSET = 49;
+
// NOTE: If you add a new axis here you must also add it to:
// frameworks/native/include/android/input.h
// frameworks/native/libs/input/InputEventLabels.cpp
@@ -1325,6 +1344,8 @@
names.append(AXIS_GENERIC_14, "AXIS_GENERIC_14");
names.append(AXIS_GENERIC_15, "AXIS_GENERIC_15");
names.append(AXIS_GENERIC_16, "AXIS_GENERIC_16");
+ names.append(AXIS_GESTURE_X_OFFSET, "AXIS_GESTURE_X_OFFSET");
+ names.append(AXIS_GESTURE_Y_OFFSET, "AXIS_GESTURE_Y_OFFSET");
}
/**
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index b2b5f13..58c8126 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -286,7 +286,7 @@
* @hide
*/
public static final boolean LOCAL_LAYOUT =
- SystemProperties.getBoolean("persist.debug.local_layout", false);
+ SystemProperties.getBoolean("persist.debug.local_layout", true);
/**
* Set this system property to true to force the view hierarchy to render
@@ -3798,7 +3798,6 @@
}
mAttachInfo.mHasWindowFocus = hasWindowFocus;
- mImeFocusController.updateImeFocusable(mWindowAttributes, true /* force */);
mImeFocusController.onPreWindowFocus(hasWindowFocus, mWindowAttributes);
if (mView != null) {
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 08a7583..b883d0c 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -46,6 +46,7 @@
import android.annotation.RequiresPermission;
import android.annotation.SystemService;
import android.annotation.TestApi;
+import android.annotation.UiThread;
import android.annotation.UserIdInt;
import android.app.ActivityThread;
import android.compat.annotation.ChangeId;
@@ -737,28 +738,6 @@
private final class DelegateImpl implements
ImeFocusController.InputMethodManagerDelegate {
- /**
- * Used by {@link ImeFocusController} to start input connection.
- */
- @Override
- public boolean startInput(@StartInputReason int startInputReason, View focusedView,
- @StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode,
- int windowFlags) {
- ImeTracing.getInstance().triggerClientDump(
- "InputMethodManager.DelegateImpl#startInput", InputMethodManager.this,
- null /* icProto */);
- return startInputOnWindowFocusGainInternal(startInputReason, focusedView,
- startInputFlags, softInputMode, windowFlags);
- }
-
- private void finishInput() {
- ImeTracing.getInstance().triggerClientDump(
- "InputMethodManager.DelegateImpl#finishInput", InputMethodManager.this,
- null /* icProto */);
- synchronized (mH) {
- finishInputLocked();
- }
- }
/**
* Used by {@link ImeFocusController} to finish input connection and callback
@@ -781,12 +760,19 @@
}
@Override
- public void onPostWindowFocus(View viewForWindowFocus,
+ public void onPreWindowGainedFocus(ViewRootImpl viewRootImpl) {
+ synchronized (mH) {
+ setCurrentRootViewLocked(viewRootImpl);
+ }
+ }
+
+ @Override
+ public void onPostWindowGainedFocus(View viewForWindowFocus,
@NonNull WindowManager.LayoutParams windowAttribute) {
boolean forceFocus = false;
synchronized (mH) {
// Update mNextServedView when focusedView changed.
- onViewFocusChanged(viewForWindowFocus, true);
+ onViewFocusChangedInternal(viewForWindowFocus, true);
// Starting new input when the next focused view is same as served view but the
// currently active connection (if any) is not associated with it.
@@ -861,35 +847,7 @@
@Override
public void onViewFocusChanged(@Nullable View view, boolean hasFocus) {
- if (view == null || view.isTemporarilyDetached()) {
- return;
- }
- final ViewRootImpl viewRootImpl = view.getViewRootImpl();
- synchronized (mH) {
- if (mCurRootView != viewRootImpl) {
- return;
- }
- if (!view.hasImeFocus() || !view.hasWindowFocus()) {
- return;
- }
- if (DEBUG) {
- Log.d(TAG, "onViewFocusChanged, view=" + InputMethodDebug.dumpViewInfo(view));
- }
-
- // We don't need to track the next served view when the view lost focus here
- // because:
- // 1) The current view focus may be cleared temporary when in touch mode, closing
- // input at this moment isn't the right way.
- // 2) We only care about the served view change when it focused, since changing
- // input connection when the focus target changed is reasonable.
- // 3) Setting the next served view as null when no more served view should be
- // handled in other special events (e.g. view detached from window or the window
- // dismissed).
- if (hasFocus) {
- mNextServedView = view;
- }
- }
- viewRootImpl.dispatchCheckFocus();
+ onViewFocusChangedInternal(view, hasFocus);
}
@Override
@@ -912,7 +870,7 @@
}
// Close the connection when no next served view coming.
if (mNextServedView == null) {
- finishInput();
+ finishInputLocked();
closeCurrentInput();
return false;
}
@@ -923,7 +881,8 @@
}
if (startInput) {
- startInput(StartInputReason.CHECK_FOCUS, null /* focusedView */,
+ startInputOnWindowFocusGainInternal(StartInputReason.CHECK_FOCUS,
+ null /* focusedView */,
0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */);
}
return true;
@@ -951,21 +910,16 @@
return;
}
if (mServedView != null) {
- finishInput();
+ finishInputLocked();
}
- setCurrentRootView(null);
+ setCurrentRootViewLocked(null);
}
}
- /**
- * Used for {@link ImeFocusController} to set the current focused root view.
- */
- @Override
- public void setCurrentRootView(ViewRootImpl rootView) {
- synchronized (mH) {
- mImeDispatcher.switchRootView(mCurRootView, rootView);
- mCurRootView = rootView;
- }
+ @GuardedBy("mH")
+ private void setCurrentRootViewLocked(ViewRootImpl rootView) {
+ mImeDispatcher.switchRootView(mCurRootView, rootView);
+ mCurRootView = rootView;
}
/**
@@ -2413,7 +2367,7 @@
}
/**
- * Called when {@link DelegateImpl#startInput}, {@link #restartInput(View)},
+ * Called when {@link DelegateImpl#checkFocus}, {@link #restartInput(View)},
* {@link #MSG_BIND} or {@link #MSG_UNBIND}.
* Note that this method should *NOT* be called inside of {@code mH} lock to prevent start input
* background thread may blocked by other methods which already inside {@code mH} lock.
@@ -2750,6 +2704,40 @@
}
}
+ @UiThread
+ private void onViewFocusChangedInternal(@Nullable View view, boolean hasFocus) {
+ if (view == null || view.isTemporarilyDetached()) {
+ return;
+ }
+ final ViewRootImpl viewRootImpl = view.getViewRootImpl();
+ synchronized (mH) {
+ if (mCurRootView != viewRootImpl) {
+ return;
+ }
+ if (!view.hasImeFocus() || !view.hasWindowFocus()) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "onViewFocusChangedInternal, view="
+ + InputMethodDebug.dumpViewInfo(view));
+ }
+
+ // We don't need to track the next served view when the view lost focus here
+ // because:
+ // 1) The current view focus may be cleared temporary when in touch mode, closing
+ // input at this moment isn't the right way.
+ // 2) We only care about the served view change when it focused, since changing
+ // input connection when the focus target changed is reasonable.
+ // 3) Setting the next served view as null when no more served view should be
+ // handled in other special events (e.g. view detached from window or the window
+ // dismissed).
+ if (hasFocus) {
+ mNextServedView = view;
+ }
+ }
+ viewRootImpl.dispatchCheckFocus();
+ }
+
@UnsupportedAppUsage
void closeCurrentInput() {
synchronized (mH) {
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index 77317d1..2433a05 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -2160,6 +2160,11 @@
nCriterion.mValue.mUserId =
env->GetIntField(jCriterion, gAudioMixMatchCriterionFields.mIntProp);
break;
+ case RULE_MATCH_AUDIO_SESSION_ID: {
+ jint jAudioSessionId =
+ env->GetIntField(jCriterion, gAudioMixMatchCriterionFields.mIntProp);
+ nCriterion.mValue.mAudioSessionId = static_cast<audio_session_t>(jAudioSessionId);
+ } break;
case RULE_MATCH_ATTRIBUTE_USAGE:
case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET: {
jobject jAttributes = env->GetObjectField(jCriterion, gAudioMixMatchCriterionFields.mAttr);
diff --git a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
index acf4da6..2cd9f89 100644
--- a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
+++ b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
@@ -257,6 +257,13 @@
return INSTALL_FAILED_CONTAINER_ERROR;
}
+ if (fsync(fd) < 0) {
+ ALOGE("Coulnd't fsync temporary file name: %s: %s\n", localTmpFileName, strerror(errno));
+ close(fd);
+ unlink(localTmpFileName);
+ return INSTALL_FAILED_INTERNAL_ERROR;
+ }
+
close(fd);
// Set the modification time for this file to the ZIP's mod time.
diff --git a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
index daae957..180a312 100644
--- a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
+++ b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
@@ -36,7 +36,6 @@
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.StrictMode;
-import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -48,6 +47,9 @@
import androidx.test.uiautomator.UiObject2;
import androidx.test.uiautomator.Until;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Files;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -57,11 +59,22 @@
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
/**
* Tests for BugreportManager API.
@@ -74,6 +87,7 @@
private static final String TAG = "BugreportManagerTest";
private static final long BUGREPORT_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(10);
private static final long DUMPSTATE_STARTUP_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
+ private static final long DUMPSTATE_TEARDOWN_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
private static final long UIAUTOMATOR_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
@@ -89,6 +103,18 @@
private static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT";
private static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
+ private static final Path[] UI_TRACES_PREDUMPED = {
+ Paths.get("/data/misc/wmtrace/ime_trace_clients.winscope"),
+ Paths.get("/data/misc/wmtrace/ime_trace_managerservice.winscope"),
+ Paths.get("/data/misc/wmtrace/ime_trace_service.winscope"),
+ Paths.get("/data/misc/wmtrace/wm_trace.winscope"),
+ Paths.get("/data/misc/wmtrace/layers_trace.winscope"),
+ Paths.get("/data/misc/wmtrace/transactions_trace.winscope"),
+ };
+ private static final Path[] UI_TRACES_GENERATED_DURING_BUGREPORT = {
+ Paths.get("/data/misc/wmtrace/layers_trace_from_transactions.winscope"),
+ };
+
private Handler mHandler;
private Executor mExecutor;
private BugreportManager mBrm;
@@ -124,7 +150,6 @@
FileUtils.closeQuietly(mScreenshotFd);
}
-
@Test
public void normalFlow_wifi() throws Exception {
BugreportCallbackImpl callback = new BugreportCallbackImpl();
@@ -175,6 +200,66 @@
assertFdsAreClosed(mBugreportFd, mScreenshotFd);
}
+ @LargeTest
+ @Test
+ public void preDumpUiData_then_fullWithUsePreDumpFlag() throws Exception {
+ startPreDumpedUiTraces();
+
+ mBrm.preDumpUiData();
+ waitTillDumpstateExitedOrTimeout();
+ List<File> expectedPreDumpedTraceFiles = copyFilesAsRoot(UI_TRACES_PREDUMPED);
+
+ BugreportCallbackImpl callback = new BugreportCallbackImpl();
+ mBrm.startBugreport(mBugreportFd, null, fullWithUsePreDumpFlag(), mExecutor,
+ callback);
+ shareConsentDialog(ConsentReply.ALLOW);
+ waitTillDoneOrTimeout(callback);
+
+ stopPreDumpedUiTraces();
+
+ assertThat(callback.isDone()).isTrue();
+ assertThat(mBugreportFile.length()).isGreaterThan(0L);
+ assertFdsAreClosed(mBugreportFd);
+
+ assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
+ assertThatBugreportContainsFiles(UI_TRACES_GENERATED_DURING_BUGREPORT);
+
+ List<File> actualPreDumpedTraceFiles = extractFilesFromBugreport(UI_TRACES_PREDUMPED);
+ assertThatAllFileContentsAreEqual(actualPreDumpedTraceFiles, expectedPreDumpedTraceFiles);
+ }
+
+ @LargeTest
+ @Test
+ public void preDumpData_then_fullWithoutUsePreDumpFlag_ignoresPreDump() throws Exception {
+ startPreDumpedUiTraces();
+
+ // Simulate pre-dump, instead of taking a real one.
+ // In some corner cases, data dumped as part of the full bugreport could be the same as the
+ // pre-dumped data and this test would fail. Hence, here we create fake/artificial
+ // pre-dumped data that we know it won't match with the full bugreport data.
+ createFilesWithFakeDataAsRoot(UI_TRACES_PREDUMPED, "system");
+
+ List<File> preDumpedTraceFiles = copyFilesAsRoot(UI_TRACES_PREDUMPED);
+
+ BugreportCallbackImpl callback = new BugreportCallbackImpl();
+ mBrm.startBugreport(mBugreportFd, null, full(), mExecutor,
+ callback);
+ shareConsentDialog(ConsentReply.ALLOW);
+ waitTillDoneOrTimeout(callback);
+
+ stopPreDumpedUiTraces();
+
+ assertThat(callback.isDone()).isTrue();
+ assertThat(mBugreportFile.length()).isGreaterThan(0L);
+ assertFdsAreClosed(mBugreportFd);
+
+ assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
+ assertThatBugreportContainsFiles(UI_TRACES_GENERATED_DURING_BUGREPORT);
+
+ List<File> actualTraceFiles = extractFilesFromBugreport(UI_TRACES_PREDUMPED);
+ assertThatAllFileContentsAreDifferent(preDumpedTraceFiles, actualTraceFiles);
+ }
+
@Test
public void simultaneousBugreportsNotAllowed() throws Exception {
// Start bugreport #1
@@ -384,21 +469,151 @@
}
return bm;
}
-
private static File createTempFile(String prefix, String extension) throws Exception {
final File f = File.createTempFile(prefix, extension);
f.setReadable(true, true);
f.setWritable(true, true);
-
f.deleteOnExit();
return f;
}
+ private static void startPreDumpedUiTraces() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+ "cmd input_method tracing start"
+ );
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+ "cmd window tracing start"
+ );
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+ "service call SurfaceFlinger 1025 i32 1"
+ );
+ }
+
+ private static void stopPreDumpedUiTraces() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+ "cmd input_method tracing stop"
+ );
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+ "cmd window tracing stop"
+ );
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+ "service call SurfaceFlinger 1025 i32 0"
+ );
+ }
+
+ private void assertThatBugreportContainsFiles(Path[] paths)
+ throws IOException {
+ List<Path> entries = listZipArchiveEntries(mBugreportFile);
+ for (Path pathInDevice : paths) {
+ Path pathInArchive = Paths.get("FS" + pathInDevice.toString());
+ assertThat(entries).contains(pathInArchive);
+ }
+ }
+
+ private List<File> extractFilesFromBugreport(Path[] paths) throws Exception {
+ List<File> files = new ArrayList<File>();
+ for (Path pathInDevice : paths) {
+ Path pathInArchive = Paths.get("FS" + pathInDevice.toString());
+ files.add(extractZipArchiveEntry(mBugreportFile, pathInArchive));
+ }
+ return files;
+ }
+
+ private static List<Path> listZipArchiveEntries(File archive) throws IOException {
+ ArrayList<Path> entries = new ArrayList<>();
+
+ ZipInputStream stream = new ZipInputStream(
+ new BufferedInputStream(new FileInputStream(archive)));
+
+ for (ZipEntry entry = stream.getNextEntry(); entry != null; entry = stream.getNextEntry()) {
+ entries.add(Paths.get(entry.toString()));
+ }
+
+ return entries;
+ }
+
+ private static File extractZipArchiveEntry(File archive, Path entryToExtract)
+ throws Exception {
+ File extractedFile = createTempFile(entryToExtract.getFileName().toString(), ".extracted");
+
+ ZipInputStream is = new ZipInputStream(new FileInputStream(archive));
+ boolean hasFoundEntry = false;
+
+ for (ZipEntry entry = is.getNextEntry(); entry != null; entry = is.getNextEntry()) {
+ if (entry.toString().equals(entryToExtract.toString())) {
+ BufferedOutputStream os =
+ new BufferedOutputStream(new FileOutputStream(extractedFile));
+ ByteStreams.copy(is, os);
+ os.close();
+ hasFoundEntry = true;
+ break;
+ }
+
+ ByteStreams.exhaust(is); // skip entry
+ }
+
+ is.closeEntry();
+ is.close();
+
+ assertThat(hasFoundEntry).isTrue();
+
+ return extractedFile;
+ }
+
+ private static void createFilesWithFakeDataAsRoot(Path[] paths, String owner) throws Exception {
+ File src = createTempFile("fake", ".data");
+ Files.write("fake data".getBytes(StandardCharsets.UTF_8), src);
+
+ for (Path path : paths) {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+ "install -m 611 -o " + owner + " -g " + owner
+ + " " + src.getAbsolutePath() + " " + path.toString()
+ );
+ }
+ }
+
+ private static List<File> copyFilesAsRoot(Path[] paths) throws Exception {
+ ArrayList<File> files = new ArrayList<File>();
+ for (Path src : paths) {
+ File dst = createTempFile(src.getFileName().toString(), ".copy");
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+ "cp " + src.toString() + " " + dst.getAbsolutePath()
+ );
+ files.add(dst);
+ }
+ return files;
+ }
+
private static ParcelFileDescriptor parcelFd(File file) throws Exception {
return ParcelFileDescriptor.open(file,
ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND);
}
+ private static void assertThatAllFileContentsAreEqual(List<File> actual, List<File> expected)
+ throws IOException {
+ if (actual.size() != expected.size()) {
+ fail("File lists have different size");
+ }
+ for (int i = 0; i < actual.size(); ++i) {
+ if (!Files.equal(actual.get(i), expected.get(i))) {
+ fail("Contents of " + actual.get(i).toString()
+ + " != " + expected.get(i).toString());
+ }
+ }
+ }
+
+ private static void assertThatAllFileContentsAreDifferent(List<File> a, List<File> b)
+ throws IOException {
+ if (a.size() != b.size()) {
+ fail("File lists have different size");
+ }
+ for (int i = 0; i < a.size(); ++i) {
+ if (Files.equal(a.get(i), b.get(i))) {
+ fail("Contents of " + a.get(i).toString() + " == " + b.get(i).toString());
+ }
+ }
+ }
+
private static void dropPermissions() {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
@@ -410,21 +625,16 @@
}
private static boolean isDumpstateRunning() {
- String[] output;
+ String output;
try {
- output =
- UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
- .executeShellCommand("ps -A -o NAME | grep dumpstate")
- .trim()
- .split("\n");
+ output = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ .executeShellCommand("service list | grep dumpstate");
} catch (IOException e) {
Log.w(TAG, "Failed to check if dumpstate is running", e);
return false;
}
- for (String line : output) {
- // Check for an exact match since there may be other things that contain "dumpstate" as
- // a substring (e.g. the dumpstate HAL).
- if (TextUtils.equals("dumpstate", line)) {
+ for (String line : output.trim().split("\n")) {
+ if (line.matches("^.*\\s+dumpstate:\\s+\\[.*\\]$")) {
return true;
}
}
@@ -449,6 +659,17 @@
return System.currentTimeMillis();
}
+ private static void waitTillDumpstateExitedOrTimeout() throws Exception {
+ long startTimeMs = now();
+ while (isDumpstateRunning()) {
+ Thread.sleep(500 /* .5s */);
+ if (now() - startTimeMs >= DUMPSTATE_TEARDOWN_TIMEOUT_MS) {
+ break;
+ }
+ Log.d(TAG, "Waited " + (now() - startTimeMs) + "ms for dumpstate to exit");
+ }
+ }
+
private static void waitTillDumpstateRunningOrTimeout() throws Exception {
long startTimeMs = now();
while (!isDumpstateRunning()) {
@@ -500,6 +721,16 @@
return new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL);
}
+ /*
+ * Returns a {@link BugreportParams} for full bugreport that reuses pre-dumped data.
+ *
+ * <p> This can take on the order of minutes to finish
+ */
+ private static BugreportParams fullWithUsePreDumpFlag() {
+ return new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL,
+ BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA);
+ }
+
/* Allow/deny the consent dialog to sharing bugreport data or check existence only. */
private enum ConsentReply {
ALLOW,
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index f9f3b4c..0b8b29b 100644
--- a/core/tests/coretests/src/android/app/NotificationTest.java
+++ b/core/tests/coretests/src/android/app/NotificationTest.java
@@ -59,6 +59,7 @@
import static org.mockito.Mockito.spy;
import android.annotation.Nullable;
+import android.app.Notification.CallStyle;
import android.content.Context;
import android.content.Intent;
import android.content.LocusId;
@@ -92,6 +93,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.List;
import java.util.function.Consumer;
@RunWith(AndroidJUnit4.class)
@@ -531,6 +533,108 @@
}
@Test
+ public void testCallStyle_getSystemActions_forIncomingCall() {
+ PendingIntent answerIntent = createPendingIntent("answer");
+ PendingIntent declineIntent = createPendingIntent("decline");
+ Notification.CallStyle style = Notification.CallStyle.forIncomingCall(
+ new Person.Builder().setName("A Caller").build(),
+ declineIntent,
+ answerIntent
+ );
+ style.setBuilder(new Notification.Builder(mContext, "Channel"));
+
+ List<Notification.Action> actions = style.getActionsListWithSystemActions();
+
+ assertEquals(2, actions.size());
+ assertEquals(declineIntent, actions.get(0).actionIntent);
+ assertEquals(answerIntent, actions.get(1).actionIntent);
+ }
+
+ @Test
+ public void testCallStyle_getSystemActions_forOngoingCall() {
+ PendingIntent hangUpIntent = createPendingIntent("hangUp");
+ Notification.CallStyle style = Notification.CallStyle.forOngoingCall(
+ new Person.Builder().setName("A Caller").build(),
+ hangUpIntent
+ );
+ style.setBuilder(new Notification.Builder(mContext, "Channel"));
+
+ List<Notification.Action> actions = style.getActionsListWithSystemActions();
+
+ assertEquals(1, actions.size());
+ assertEquals(hangUpIntent, actions.get(0).actionIntent);
+ }
+
+ @Test
+ public void testCallStyle_getSystemActions_forIncomingCallWithOtherActions() {
+ PendingIntent answerIntent = createPendingIntent("answer");
+ PendingIntent declineIntent = createPendingIntent("decline");
+ Notification.CallStyle style = Notification.CallStyle.forIncomingCall(
+ new Person.Builder().setName("A Caller").build(),
+ declineIntent,
+ answerIntent
+ );
+ Notification.Action actionToKeep = makeNotificationAction(null);
+ Notification.Action actionToDrop = makeNotificationAction(null);
+ Notification.Builder notifBuilder = new Notification.Builder(mContext, "Channel")
+ .addAction(actionToKeep)
+ .addAction(actionToDrop); //expect to move this action to the end
+ style.setBuilder(notifBuilder); //add a builder with actions
+
+ List<Notification.Action> actions = style.getActionsListWithSystemActions();
+
+ assertEquals(4, actions.size());
+ assertEquals(declineIntent, actions.get(0).actionIntent);
+ assertEquals(actionToKeep, actions.get(1));
+ assertEquals(answerIntent, actions.get(2).actionIntent);
+ assertEquals(actionToDrop, actions.get(3));
+ }
+
+ @Test
+ public void testCallStyle_getSystemActions_forOngoingCallWithOtherActions() {
+ PendingIntent hangUpIntent = createPendingIntent("hangUp");
+ Notification.CallStyle style = Notification.CallStyle.forOngoingCall(
+ new Person.Builder().setName("A Caller").build(),
+ hangUpIntent
+ );
+ Notification.Action firstAction = makeNotificationAction(null);
+ Notification.Action secondAction = makeNotificationAction(null);
+ Notification.Builder notifBuilder = new Notification.Builder(mContext, "Channel")
+ .addAction(firstAction)
+ .addAction(secondAction);
+ style.setBuilder(notifBuilder); //add a builder with actions
+
+ List<Notification.Action> actions = style.getActionsListWithSystemActions();
+
+ assertEquals(3, actions.size());
+ assertEquals(hangUpIntent, actions.get(0).actionIntent);
+ assertEquals(firstAction, actions.get(1));
+ assertEquals(secondAction, actions.get(2));
+ }
+
+ @Test
+ public void testCallStyle_getSystemActions_dropsOldSystemActions() {
+ PendingIntent hangUpIntent = createPendingIntent("decline");
+ Notification.CallStyle style = Notification.CallStyle.forOngoingCall(
+ new Person.Builder().setName("A Caller").build(),
+ hangUpIntent
+ );
+ Bundle actionExtras = new Bundle();
+ actionExtras.putBoolean("key_action_priority", true);
+ Notification.Action oldSystemAction = makeNotificationAction(
+ builder -> builder.addExtras(actionExtras)
+ );
+ Notification.Builder notifBuilder = new Notification.Builder(mContext, "Channel")
+ .addAction(oldSystemAction);
+ style.setBuilder(notifBuilder); //add a builder with actions
+
+ List<Notification.Action> actions = style.getActionsListWithSystemActions();
+
+ assertFalse("Old versions of system actions should be dropped.",
+ actions.contains(oldSystemAction));
+ }
+
+ @Test
public void testBuild_ensureSmallIconIsNotTooBig_resizesIcon() {
Icon hugeIcon = Icon.createWithBitmap(
Bitmap.createBitmap(3000, 3000, Bitmap.Config.ARGB_8888));
@@ -788,7 +892,7 @@
@Test
public void testRestoreFromExtras_Call_invalidExtra_noCrash() {
- Notification.Style style = new Notification.CallStyle();
+ Notification.Style style = new CallStyle();
Bundle fakeTypes = new Bundle();
fakeTypes.putParcelable(EXTRA_CALL_PERSON, new Bundle());
fakeTypes.putParcelable(EXTRA_ANSWER_INTENT, new Bundle());
@@ -962,4 +1066,12 @@
}
return actionBuilder.build();
}
+
+ /**
+ * Creates a PendingIntent with the given action.
+ */
+ private PendingIntent createPendingIntent(String action) {
+ return PendingIntent.getActivity(mContext, 0, new Intent(action),
+ PendingIntent.FLAG_MUTABLE);
+ }
}
diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
index 3768063..ed2b101 100644
--- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
+++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
@@ -368,20 +368,4 @@
PropertyInvalidatedCache.MODULE_BLUETOOTH, "getState");
assertEquals(n1, "cache_key.bluetooth.get_state");
}
-
- @Test
- public void testOnTrimMemory() {
- TestCache cache = new TestCache(MODULE, "trimMemoryTest");
- // The cache is not active until it has been invalidated once.
- cache.invalidateCache();
- // Populate the cache with six entries.
- for (int i = 0; i < 6; i++) {
- cache.query(i);
- }
- // The maximum number of entries in TestCache is 4, so even though six entries were
- // created, only four are retained.
- assertEquals(4, cache.size());
- PropertyInvalidatedCache.onTrimMemory();
- assertEquals(0, cache.size());
- }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
index 591e347..215308d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
@@ -130,6 +130,10 @@
if (!cropRect.intersect(mWholeAnimationBounds)) {
// Hide the surface when it is outside of the animation area.
t.setAlpha(mLeash, 0);
+ } else if (mAnimation.hasExtension()) {
+ // Allow the surface to be shown in its original bounds in case we want to use edge
+ // extensions.
+ cropRect.union(mChange.getEndAbsBounds());
}
// cropRect is in absolute coordinate, so we need to translate it to surface top left.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index 756d802..490975c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -21,6 +21,7 @@
import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition;
+import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow;
import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
import android.animation.Animator;
@@ -45,6 +46,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+import java.util.function.Consumer;
/** To run the ActivityEmbedding animations. */
class ActivityEmbeddingAnimationRunner {
@@ -65,10 +67,31 @@
void startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction) {
+ // There may be some surface change that we want to apply after the start transaction is
+ // applied to make sure the surface is ready.
+ final List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks =
+ new ArrayList<>();
final Animator animator = createAnimator(info, startTransaction, finishTransaction,
- () -> mController.onAnimationFinished(transition));
- startTransaction.apply();
- animator.start();
+ () -> mController.onAnimationFinished(transition), postStartTransactionCallbacks);
+
+ // Start the animation.
+ if (!postStartTransactionCallbacks.isEmpty()) {
+ // postStartTransactionCallbacks require that the start transaction is already
+ // applied to run otherwise they may result in flickers and UI inconsistencies.
+ startTransaction.apply(true /* sync */);
+
+ // Run tasks that require startTransaction to already be applied
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ for (Consumer<SurfaceControl.Transaction> postStartTransactionCallback :
+ postStartTransactionCallbacks) {
+ postStartTransactionCallback.accept(t);
+ }
+ t.apply();
+ animator.start();
+ } else {
+ startTransaction.apply();
+ animator.start();
+ }
}
/**
@@ -85,9 +108,13 @@
Animator createAnimator(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull Runnable animationFinishCallback) {
- final List<ActivityEmbeddingAnimationAdapter> adapters =
- createAnimationAdapters(info, startTransaction, finishTransaction);
+ @NonNull Runnable animationFinishCallback,
+ @NonNull List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks) {
+ final List<ActivityEmbeddingAnimationAdapter> adapters = createAnimationAdapters(info,
+ startTransaction);
+ addEdgeExtensionIfNeeded(startTransaction, finishTransaction, postStartTransactionCallbacks,
+ adapters);
+ addBackgroundColorIfNeeded(info, startTransaction, finishTransaction, adapters);
long duration = 0;
for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
duration = Math.max(duration, adapter.getDurationHint());
@@ -131,8 +158,7 @@
*/
@NonNull
private List<ActivityEmbeddingAnimationAdapter> createAnimationAdapters(
- @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction) {
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) {
boolean isChangeTransition = false;
for (TransitionInfo.Change change : info.getChanges()) {
if (change.hasFlags(FLAG_IS_BEHIND_STARTING_WINDOW)) {
@@ -148,25 +174,23 @@
return createChangeAnimationAdapters(info, startTransaction);
}
if (Transitions.isClosingType(info.getType())) {
- return createCloseAnimationAdapters(info, startTransaction, finishTransaction);
+ return createCloseAnimationAdapters(info);
}
- return createOpenAnimationAdapters(info, startTransaction, finishTransaction);
+ return createOpenAnimationAdapters(info);
}
@NonNull
private List<ActivityEmbeddingAnimationAdapter> createOpenAnimationAdapters(
- @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction) {
- return createOpenCloseAnimationAdapters(info, startTransaction, finishTransaction,
- true /* isOpening */, mAnimationSpec::loadOpenAnimation);
+ @NonNull TransitionInfo info) {
+ return createOpenCloseAnimationAdapters(info, true /* isOpening */,
+ mAnimationSpec::loadOpenAnimation);
}
@NonNull
private List<ActivityEmbeddingAnimationAdapter> createCloseAnimationAdapters(
- @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction) {
- return createOpenCloseAnimationAdapters(info, startTransaction, finishTransaction,
- false /* isOpening */, mAnimationSpec::loadCloseAnimation);
+ @NonNull TransitionInfo info) {
+ return createOpenCloseAnimationAdapters(info, false /* isOpening */,
+ mAnimationSpec::loadCloseAnimation);
}
/**
@@ -175,8 +199,7 @@
*/
@NonNull
private List<ActivityEmbeddingAnimationAdapter> createOpenCloseAnimationAdapters(
- @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction, boolean isOpening,
+ @NonNull TransitionInfo info, boolean isOpening,
@NonNull AnimationProvider animationProvider) {
// We need to know if the change window is only a partial of the whole animation screen.
// If so, we will need to adjust it to make the whole animation screen looks like one.
@@ -200,8 +223,7 @@
final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>();
for (TransitionInfo.Change change : openingChanges) {
final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
- info, change, startTransaction, finishTransaction, animationProvider,
- openingWholeScreenBounds);
+ info, change, animationProvider, openingWholeScreenBounds);
if (isOpening) {
adapter.overrideLayer(offsetLayer++);
}
@@ -209,8 +231,7 @@
}
for (TransitionInfo.Change change : closingChanges) {
final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
- info, change, startTransaction, finishTransaction, animationProvider,
- closingWholeScreenBounds);
+ info, change, animationProvider, closingWholeScreenBounds);
if (!isOpening) {
adapter.overrideLayer(offsetLayer++);
}
@@ -219,20 +240,51 @@
return adapters;
}
+ /** Adds edge extension to the surfaces that have such an animation property. */
+ private void addEdgeExtensionIfNeeded(@NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks,
+ @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) {
+ for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+ final Animation animation = adapter.mAnimation;
+ if (!animation.hasExtension()) {
+ continue;
+ }
+ final TransitionInfo.Change change = adapter.mChange;
+ if (Transitions.isOpeningType(adapter.mChange.getMode())) {
+ // Need to screenshot after startTransaction is applied otherwise activity
+ // may not be visible or ready yet.
+ postStartTransactionCallbacks.add(
+ t -> edgeExtendWindow(change, animation, t, finishTransaction));
+ } else {
+ // Can screenshot now (before startTransaction is applied)
+ edgeExtendWindow(change, animation, startTransaction, finishTransaction);
+ }
+ }
+ }
+
+ /** Adds background color to the transition if any animation has such a property. */
+ private void addBackgroundColorIfNeeded(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) {
+ for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+ final int backgroundColor = getTransitionBackgroundColorIfSet(info, adapter.mChange,
+ adapter.mAnimation, 0 /* defaultColor */);
+ if (backgroundColor != 0) {
+ // We only need to show one color.
+ addBackgroundToTransition(info.getRootLeash(), backgroundColor, startTransaction,
+ finishTransaction);
+ return;
+ }
+ }
+ }
+
@NonNull
private ActivityEmbeddingAnimationAdapter createOpenCloseAnimationAdapter(
@NonNull TransitionInfo info, @NonNull TransitionInfo.Change change,
- @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
@NonNull AnimationProvider animationProvider, @NonNull Rect wholeAnimationBounds) {
final Animation animation = animationProvider.get(info, change, wholeAnimationBounds);
- // We may want to show a background color for open/close transition.
- final int backgroundColor = getTransitionBackgroundColorIfSet(info, change, animation,
- 0 /* defaultColor */);
- if (backgroundColor != 0) {
- addBackgroundToTransition(info.getRootLeash(), backgroundColor, startTransaction,
- finishTransaction);
- }
return new ActivityEmbeddingAnimationAdapter(animation, change, change.getLeash(),
wholeAnimationBounds);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
index eb6ac76..58b2366 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
@@ -181,15 +181,15 @@
@NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
final boolean isEnter = Transitions.isOpeningType(change.getMode());
final Animation animation;
- // TODO(b/207070762): Implement edgeExtension version
if (shouldShowBackdrop(info, change)) {
animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
? com.android.internal.R.anim.task_fragment_clear_top_open_enter
: com.android.internal.R.anim.task_fragment_clear_top_open_exit);
} else {
+ // Use the same edge extension animation as regular activity open.
animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
- ? com.android.internal.R.anim.task_fragment_open_enter
- : com.android.internal.R.anim.task_fragment_open_exit);
+ ? com.android.internal.R.anim.activity_open_enter
+ : com.android.internal.R.anim.activity_open_exit);
}
// Use the whole animation bounds instead of the change bounds, so that when multiple change
// targets are opening at the same time, the animation applied to each will be the same.
@@ -205,15 +205,15 @@
@NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
final boolean isEnter = Transitions.isOpeningType(change.getMode());
final Animation animation;
- // TODO(b/207070762): Implement edgeExtension version
if (shouldShowBackdrop(info, change)) {
animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
? com.android.internal.R.anim.task_fragment_clear_top_close_enter
: com.android.internal.R.anim.task_fragment_clear_top_close_exit);
} else {
+ // Use the same edge extension animation as regular activity close.
animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
- ? com.android.internal.R.anim.task_fragment_close_enter
- : com.android.internal.R.anim.task_fragment_close_exit);
+ ? com.android.internal.R.anim.activity_close_enter
+ : com.android.internal.R.anim.activity_close_exit);
}
// Use the whole animation bounds instead of the change bounds, so that when multiple change
// targets are closing at the same time, the animation applied to each will be the same.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 91c153e..9c2c2fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -59,6 +59,7 @@
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN;
import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition;
+import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow;
import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation;
import static com.android.wm.shell.transition.TransitionAnimationHelper.sDisableCustomTaskAnimationProperty;
@@ -76,10 +77,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.graphics.Canvas;
import android.graphics.Insets;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
@@ -89,7 +87,6 @@
import android.os.UserHandle;
import android.util.ArrayMap;
import android.view.Choreographer;
-import android.view.Surface;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.view.WindowManager;
@@ -97,7 +94,6 @@
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.Transformation;
-import android.window.ScreenCapture;
import android.window.TransitionInfo;
import android.window.TransitionMetrics;
import android.window.TransitionRequestInfo;
@@ -526,123 +522,6 @@
}
}
- private void edgeExtendWindow(TransitionInfo.Change change,
- Animation a, SurfaceControl.Transaction startTransaction,
- SurfaceControl.Transaction finishTransaction) {
- // Do not create edge extension surface for transfer starting window change.
- // The app surface could be empty thus nothing can draw on the hardware renderer, which will
- // block this thread when calling Surface#unlockCanvasAndPost.
- if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
- return;
- }
- final Transformation transformationAtStart = new Transformation();
- a.getTransformationAt(0, transformationAtStart);
- final Transformation transformationAtEnd = new Transformation();
- a.getTransformationAt(1, transformationAtEnd);
-
- // We want to create an extension surface that is the maximal size and the animation will
- // take care of cropping any part that overflows.
- final Insets maxExtensionInsets = Insets.min(
- transformationAtStart.getInsets(), transformationAtEnd.getInsets());
-
- final int targetSurfaceHeight = Math.max(change.getStartAbsBounds().height(),
- change.getEndAbsBounds().height());
- final int targetSurfaceWidth = Math.max(change.getStartAbsBounds().width(),
- change.getEndAbsBounds().width());
- if (maxExtensionInsets.left < 0) {
- final Rect edgeBounds = new Rect(0, 0, 1, targetSurfaceHeight);
- final Rect extensionRect = new Rect(0, 0,
- -maxExtensionInsets.left, targetSurfaceHeight);
- final int xPos = maxExtensionInsets.left;
- final int yPos = 0;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Left Edge Extension", startTransaction, finishTransaction);
- }
-
- if (maxExtensionInsets.top < 0) {
- final Rect edgeBounds = new Rect(0, 0, targetSurfaceWidth, 1);
- final Rect extensionRect = new Rect(0, 0,
- targetSurfaceWidth, -maxExtensionInsets.top);
- final int xPos = 0;
- final int yPos = maxExtensionInsets.top;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Top Edge Extension", startTransaction, finishTransaction);
- }
-
- if (maxExtensionInsets.right < 0) {
- final Rect edgeBounds = new Rect(targetSurfaceWidth - 1, 0,
- targetSurfaceWidth, targetSurfaceHeight);
- final Rect extensionRect = new Rect(0, 0,
- -maxExtensionInsets.right, targetSurfaceHeight);
- final int xPos = targetSurfaceWidth;
- final int yPos = 0;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Right Edge Extension", startTransaction, finishTransaction);
- }
-
- if (maxExtensionInsets.bottom < 0) {
- final Rect edgeBounds = new Rect(0, targetSurfaceHeight - 1,
- targetSurfaceWidth, targetSurfaceHeight);
- final Rect extensionRect = new Rect(0, 0,
- targetSurfaceWidth, -maxExtensionInsets.bottom);
- final int xPos = maxExtensionInsets.left;
- final int yPos = targetSurfaceHeight;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Bottom Edge Extension", startTransaction, finishTransaction);
- }
- }
-
- private SurfaceControl createExtensionSurface(SurfaceControl surfaceToExtend, Rect edgeBounds,
- Rect extensionRect, int xPos, int yPos, String layerName,
- SurfaceControl.Transaction startTransaction,
- SurfaceControl.Transaction finishTransaction) {
- final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder()
- .setName(layerName)
- .setParent(surfaceToExtend)
- .setHidden(true)
- .setCallsite("DefaultTransitionHandler#startAnimation")
- .setOpaque(true)
- .setBufferSize(extensionRect.width(), extensionRect.height())
- .build();
-
- ScreenCapture.LayerCaptureArgs captureArgs =
- new ScreenCapture.LayerCaptureArgs.Builder(surfaceToExtend)
- .setSourceCrop(edgeBounds)
- .setFrameScale(1)
- .setPixelFormat(PixelFormat.RGBA_8888)
- .setChildrenOnly(true)
- .setAllowProtected(true)
- .build();
- final ScreenCapture.ScreenshotHardwareBuffer edgeBuffer =
- ScreenCapture.captureLayers(captureArgs);
-
- if (edgeBuffer == null) {
- ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
- "Failed to capture edge of window.");
- return null;
- }
-
- android.graphics.BitmapShader shader =
- new android.graphics.BitmapShader(edgeBuffer.asBitmap(),
- android.graphics.Shader.TileMode.CLAMP,
- android.graphics.Shader.TileMode.CLAMP);
- final Paint paint = new Paint();
- paint.setShader(shader);
-
- final Surface surface = new Surface(edgeExtensionLayer);
- Canvas c = surface.lockHardwareCanvas();
- c.drawRect(extensionRect, paint);
- surface.unlockCanvasAndPost(c);
- surface.release();
-
- startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE);
- startTransaction.setPosition(edgeExtensionLayer, xPos, yPos);
- startTransaction.setVisibility(edgeExtensionLayer, true);
- finishTransaction.remove(edgeExtensionLayer);
-
- return edgeExtensionLayer;
- }
-
@Nullable
@Override
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
index efee6f40..e338221 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
@@ -24,6 +24,7 @@
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.view.WindowManager.transitTypeToString;
+import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE;
@@ -34,10 +35,20 @@
import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
import android.graphics.Color;
+import android.graphics.Insets;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.Shader;
import android.os.SystemProperties;
+import android.view.Surface;
import android.view.SurfaceControl;
import android.view.animation.Animation;
+import android.view.animation.Transformation;
+import android.window.ScreenCapture;
import android.window.TransitionInfo;
import com.android.internal.R;
@@ -217,4 +228,126 @@
.show(animationBackgroundSurface);
finishTransaction.remove(animationBackgroundSurface);
}
+
+ /**
+ * Adds edge extension surface to the given {@code change} for edge extension animation.
+ */
+ public static void edgeExtendWindow(@NonNull TransitionInfo.Change change,
+ @NonNull Animation a, @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ // Do not create edge extension surface for transfer starting window change.
+ // The app surface could be empty thus nothing can draw on the hardware renderer, which will
+ // block this thread when calling Surface#unlockCanvasAndPost.
+ if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
+ return;
+ }
+ final Transformation transformationAtStart = new Transformation();
+ a.getTransformationAt(0, transformationAtStart);
+ final Transformation transformationAtEnd = new Transformation();
+ a.getTransformationAt(1, transformationAtEnd);
+
+ // We want to create an extension surface that is the maximal size and the animation will
+ // take care of cropping any part that overflows.
+ final Insets maxExtensionInsets = Insets.min(
+ transformationAtStart.getInsets(), transformationAtEnd.getInsets());
+
+ final int targetSurfaceHeight = Math.max(change.getStartAbsBounds().height(),
+ change.getEndAbsBounds().height());
+ final int targetSurfaceWidth = Math.max(change.getStartAbsBounds().width(),
+ change.getEndAbsBounds().width());
+ if (maxExtensionInsets.left < 0) {
+ final Rect edgeBounds = new Rect(0, 0, 1, targetSurfaceHeight);
+ final Rect extensionRect = new Rect(0, 0,
+ -maxExtensionInsets.left, targetSurfaceHeight);
+ final int xPos = maxExtensionInsets.left;
+ final int yPos = 0;
+ createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
+ "Left Edge Extension", startTransaction, finishTransaction);
+ }
+
+ if (maxExtensionInsets.top < 0) {
+ final Rect edgeBounds = new Rect(0, 0, targetSurfaceWidth, 1);
+ final Rect extensionRect = new Rect(0, 0,
+ targetSurfaceWidth, -maxExtensionInsets.top);
+ final int xPos = 0;
+ final int yPos = maxExtensionInsets.top;
+ createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
+ "Top Edge Extension", startTransaction, finishTransaction);
+ }
+
+ if (maxExtensionInsets.right < 0) {
+ final Rect edgeBounds = new Rect(targetSurfaceWidth - 1, 0,
+ targetSurfaceWidth, targetSurfaceHeight);
+ final Rect extensionRect = new Rect(0, 0,
+ -maxExtensionInsets.right, targetSurfaceHeight);
+ final int xPos = targetSurfaceWidth;
+ final int yPos = 0;
+ createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
+ "Right Edge Extension", startTransaction, finishTransaction);
+ }
+
+ if (maxExtensionInsets.bottom < 0) {
+ final Rect edgeBounds = new Rect(0, targetSurfaceHeight - 1,
+ targetSurfaceWidth, targetSurfaceHeight);
+ final Rect extensionRect = new Rect(0, 0,
+ targetSurfaceWidth, -maxExtensionInsets.bottom);
+ final int xPos = maxExtensionInsets.left;
+ final int yPos = targetSurfaceHeight;
+ createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
+ "Bottom Edge Extension", startTransaction, finishTransaction);
+ }
+ }
+
+ /**
+ * Takes a screenshot of {@code surfaceToExtend}'s edge and extends it for edge extension
+ * animation.
+ */
+ private static SurfaceControl createExtensionSurface(@NonNull SurfaceControl surfaceToExtend,
+ @NonNull Rect edgeBounds, @NonNull Rect extensionRect, int xPos, int yPos,
+ @NonNull String layerName, @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder()
+ .setName(layerName)
+ .setParent(surfaceToExtend)
+ .setHidden(true)
+ .setCallsite("TransitionAnimationHelper#createExtensionSurface")
+ .setOpaque(true)
+ .setBufferSize(extensionRect.width(), extensionRect.height())
+ .build();
+
+ final ScreenCapture.LayerCaptureArgs captureArgs =
+ new ScreenCapture.LayerCaptureArgs.Builder(surfaceToExtend)
+ .setSourceCrop(edgeBounds)
+ .setFrameScale(1)
+ .setPixelFormat(PixelFormat.RGBA_8888)
+ .setChildrenOnly(true)
+ .setAllowProtected(true)
+ .build();
+ final ScreenCapture.ScreenshotHardwareBuffer edgeBuffer =
+ ScreenCapture.captureLayers(captureArgs);
+
+ if (edgeBuffer == null) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "Failed to capture edge of window.");
+ return null;
+ }
+
+ final BitmapShader shader = new BitmapShader(edgeBuffer.asBitmap(),
+ Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+ final Paint paint = new Paint();
+ paint.setShader(shader);
+
+ final Surface surface = new Surface(edgeExtensionLayer);
+ final Canvas c = surface.lockHardwareCanvas();
+ c.drawRect(extensionRect, paint);
+ surface.unlockCanvasAndPost(c);
+ surface.release();
+
+ startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE);
+ startTransaction.setPosition(edgeExtensionLayer, xPos, yPos);
+ startTransaction.setVisibility(edgeExtensionLayer, true);
+ finishTransaction.remove(edgeExtensionLayer);
+
+ return edgeExtensionLayer;
+ }
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
index 7d498dc..1fc0375 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
@@ -92,7 +92,7 @@
}
/** Checks that [pipApp] window is animated towards default position in right bottom corner */
- @FlakyTest(bugId = 251135384)
+ @Presubmit
@Test
fun pipLayerMovesTowardsRightBottomCorner() {
// in gestural nav the swipe makes PiP first go upwards
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
index 813ac5d..84a8c0a 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
@@ -26,7 +26,7 @@
import com.android.server.wm.flicker.FlickerTestParameter
import com.android.server.wm.flicker.FlickerTestParameterFactory
import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.isRotated
+import com.android.server.wm.flicker.helpers.WindowUtils
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
@@ -79,10 +79,18 @@
secondaryApp.windowMatchesAnyOf(window)
} ?: return@add false
- if (testSpec.startRotation.isRotated()) {
- return@add primaryAppWindow.frame.right <= secondaryAppWindow.frame.left
+ if (isLandscape(testSpec.endRotation)) {
+ return@add if (testSpec.isTablet) {
+ secondaryAppWindow.frame.right <= primaryAppWindow.frame.left
+ } else {
+ primaryAppWindow.frame.right <= secondaryAppWindow.frame.left
+ }
} else {
- return@add primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top
+ return@add if (testSpec.isTablet) {
+ primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top
+ } else {
+ primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top
+ }
}
}.waitForAndVerify()
}
@@ -101,14 +109,27 @@
val secondaryVisibleRegion = secondaryAppLayer.visibleRegion?.bounds
?: return@add false
- if (testSpec.startRotation.isRotated()) {
- return@add primaryVisibleRegion.right <= secondaryVisibleRegion.left
+ if (isLandscape(testSpec.endRotation)) {
+ return@add if (testSpec.isTablet) {
+ secondaryVisibleRegion.right <= primaryVisibleRegion.left
+ } else {
+ primaryVisibleRegion.right <= secondaryVisibleRegion.left
+ }
} else {
- return@add primaryVisibleRegion.bottom <= secondaryVisibleRegion.top
+ return@add if (testSpec.isTablet) {
+ primaryVisibleRegion.bottom <= secondaryVisibleRegion.top
+ } else {
+ primaryVisibleRegion.bottom <= secondaryVisibleRegion.top
+ }
}
}.waitForAndVerify()
}
+ private fun isLandscape(rotation: Int): Boolean {
+ val displayBounds = WindowUtils.getDisplayBounds(rotation)
+ return displayBounds.width > displayBounds.height
+ }
+
@IwTest(focusArea = "sysui")
@Presubmit
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
index 98b5912..79070b1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
@@ -40,6 +40,8 @@
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import java.util.ArrayList;
+
/**
* Tests for {@link ActivityEmbeddingAnimationRunner}.
*
@@ -62,13 +64,13 @@
final TransitionInfo.Change embeddingChange = createChange();
embeddingChange.setFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
info.addChange(embeddingChange);
- doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any());
+ doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any(), any());
mAnimRunner.startAnimation(mTransition, info, mStartTransaction, mFinishTransaction);
final ArgumentCaptor<Runnable> finishCallback = ArgumentCaptor.forClass(Runnable.class);
verify(mAnimRunner).createAnimator(eq(info), eq(mStartTransaction), eq(mFinishTransaction),
- finishCallback.capture());
+ finishCallback.capture(), any());
verify(mStartTransaction).apply();
verify(mAnimator).start();
verifyNoMoreInteractions(mFinishTransaction);
@@ -88,7 +90,8 @@
info.addChange(embeddingChange);
final Animator animator = mAnimRunner.createAnimator(
info, mStartTransaction, mFinishTransaction,
- () -> mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */));
+ () -> mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */),
+ new ArrayList());
// The animation should be empty when it is behind starting window.
assertEquals(0, animator.getDuration());
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
index 3792e83..54a12ab 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
@@ -56,13 +56,12 @@
@Mock
SurfaceControl.Transaction mFinishTransaction;
@Mock
- Transitions.TransitionFinishCallback mFinishCallback;
- @Mock
Animator mAnimator;
ActivityEmbeddingController mController;
ActivityEmbeddingAnimationRunner mAnimRunner;
ActivityEmbeddingAnimationSpec mAnimSpec;
+ Transitions.TransitionFinishCallback mFinishCallback;
@CallSuper
@Before
@@ -75,9 +74,11 @@
assertNotNull(mAnimRunner);
mAnimSpec = mAnimRunner.mAnimationSpec;
assertNotNull(mAnimSpec);
+ mFinishCallback = (wct, wctCB) -> {};
spyOn(mController);
spyOn(mAnimRunner);
spyOn(mAnimSpec);
+ spyOn(mFinishCallback);
}
/** Creates a mock {@link TransitionInfo.Change}. */
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
index baecf6f..4d98b6b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
@@ -55,7 +55,7 @@
@Before
public void setup() {
super.setUp();
- doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any());
+ doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any(), any());
}
@Test
diff --git a/media/java/android/media/audiopolicy/AudioMixingRule.java b/media/java/android/media/audiopolicy/AudioMixingRule.java
index 6aead43..08655ca 100644
--- a/media/java/android/media/audiopolicy/AudioMixingRule.java
+++ b/media/java/android/media/audiopolicy/AudioMixingRule.java
@@ -87,6 +87,13 @@
* parameter is an instance of {@link java.lang.Integer}.
*/
public static final int RULE_MATCH_USERID = 0x1 << 3;
+ /**
+ * A rule requiring the audio session id of the audio stream to match that specified.
+ * This mixing rule can be added with {@link Builder#addMixRule(int, Object)} where Object
+ * parameter is an instance of {@link java.lang.Integer}.
+ * @see android.media.AudioTrack.Builder#setSessionId
+ */
+ public static final int RULE_MATCH_AUDIO_SESSION_ID = 0x1 << 4;
private final static int RULE_EXCLUSION_MASK = 0x8000;
/**
@@ -115,6 +122,13 @@
public static final int RULE_EXCLUDE_USERID =
RULE_EXCLUSION_MASK | RULE_MATCH_USERID;
+ /**
+ * @hide
+ * A rule requiring the audio session id information to differ.
+ */
+ public static final int RULE_EXCLUDE_AUDIO_SESSION_ID =
+ RULE_EXCLUSION_MASK | RULE_MATCH_AUDIO_SESSION_ID;
+
/** @hide */
public static final class AudioMixMatchCriterion {
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@@ -166,6 +180,7 @@
break;
case RULE_MATCH_UID:
case RULE_MATCH_USERID:
+ case RULE_MATCH_AUDIO_SESSION_ID:
dest.writeInt(mIntProp);
break;
default:
@@ -315,6 +330,7 @@
case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET:
case RULE_MATCH_UID:
case RULE_MATCH_USERID:
+ case RULE_MATCH_AUDIO_SESSION_ID:
return true;
default:
return false;
@@ -338,6 +354,7 @@
case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET:
case RULE_MATCH_UID:
case RULE_MATCH_USERID:
+ case RULE_MATCH_AUDIO_SESSION_ID:
return true;
default:
return false;
@@ -445,7 +462,8 @@
* @param rule one of {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE},
* {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET} or
* {@link AudioMixingRule#RULE_MATCH_UID} or
- * {@link AudioMixingRule#RULE_MATCH_USERID}.
+ * {@link AudioMixingRule#RULE_MATCH_USERID} or
+ * {@link AudioMixingRule#RULE_MATCH_AUDIO_SESSION_ID}.
* @param property see the definition of each rule for the type to use (either an
* {@link AudioAttributes} or an {@link java.lang.Integer}).
* @return the same Builder instance.
@@ -476,7 +494,8 @@
* @param rule one of {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE},
* {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET} or
* {@link AudioMixingRule#RULE_MATCH_UID} or
- * {@link AudioMixingRule#RULE_MATCH_USERID}.
+ * {@link AudioMixingRule#RULE_MATCH_USERID} or
+ * {@link AudioMixingRule#RULE_MATCH_AUDIO_SESSION_ID}.
* @param property see the definition of each rule for the type to use (either an
* {@link AudioAttributes} or an {@link java.lang.Integer}).
* @return the same Builder instance.
@@ -606,9 +625,12 @@
* @param intProp an integer property to match or exclude, null if not used.
* @param rule one of {@link AudioMixingRule#RULE_EXCLUDE_ATTRIBUTE_USAGE},
* {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE},
- * {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET} or
+ * {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET},
* {@link AudioMixingRule#RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET},
- * {@link AudioMixingRule#RULE_MATCH_UID}, {@link AudioMixingRule#RULE_EXCLUDE_UID}.
+ * {@link AudioMixingRule#RULE_MATCH_UID},
+ * {@link AudioMixingRule#RULE_EXCLUDE_UID},
+ * {@link AudioMixingRule#RULE_MATCH_AUDIO_SESSION_ID},
+ * {@link AudioMixingRule#RULE_EXCLUDE_AUDIO_SESSION_ID}
* {@link AudioMixingRule#RULE_MATCH_USERID},
* {@link AudioMixingRule#RULE_EXCLUDE_USERID}.
* @return the same Builder instance.
@@ -645,6 +667,7 @@
break;
case RULE_MATCH_UID:
case RULE_MATCH_USERID:
+ case RULE_MATCH_AUDIO_SESSION_ID:
mCriteria.add(new AudioMixMatchCriterion(intProp, rule));
break;
default:
@@ -666,6 +689,7 @@
break;
case RULE_MATCH_UID:
case RULE_MATCH_USERID:
+ case RULE_MATCH_AUDIO_SESSION_ID:
intProp = new Integer(in.readInt());
break;
default:
diff --git a/media/java/android/media/audiopolicy/AudioPolicyConfig.java b/media/java/android/media/audiopolicy/AudioPolicyConfig.java
index f3731b6..440447e 100644
--- a/media/java/android/media/audiopolicy/AudioPolicyConfig.java
+++ b/media/java/android/media/audiopolicy/AudioPolicyConfig.java
@@ -217,6 +217,14 @@
textDump += " exclude userId ";
textDump += criterion.mIntProp;
break;
+ case AudioMixingRule.RULE_MATCH_AUDIO_SESSION_ID:
+ textDump += " match audio session id";
+ textDump += criterion.mIntProp;
+ break;
+ case AudioMixingRule.RULE_EXCLUDE_AUDIO_SESSION_ID:
+ textDump += " exclude audio session id ";
+ textDump += criterion.mIntProp;
+ break;
default:
textDump += "invalid rule!";
}
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java
index ad7ab97..a83e7d3 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java
@@ -22,9 +22,11 @@
import static android.media.audiopolicy.AudioMixingRule.MIX_ROLE_PLAYERS;
import static android.media.audiopolicy.AudioMixingRule.RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET;
import static android.media.audiopolicy.AudioMixingRule.RULE_EXCLUDE_ATTRIBUTE_USAGE;
+import static android.media.audiopolicy.AudioMixingRule.RULE_EXCLUDE_AUDIO_SESSION_ID;
import static android.media.audiopolicy.AudioMixingRule.RULE_EXCLUDE_UID;
import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET;
import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE;
+import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_AUDIO_SESSION_ID;
import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_UID;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -61,12 +63,14 @@
new AudioAttributes.Builder().setCapturePreset(VOICE_RECOGNITION).build();
private static final int TEST_UID = 42;
private static final int OTHER_UID = 77;
+ private static final int TEST_SESSION_ID = 1234;
@Test
public void testConstructValidRule() {
AudioMixingRule rule = new AudioMixingRule.Builder()
.addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES)
.addMixRule(RULE_MATCH_UID, TEST_UID)
+ .excludeMixRule(RULE_MATCH_AUDIO_SESSION_ID, TEST_SESSION_ID)
.build();
// Based on the rules, the mix type should fall back to MIX_ROLE_PLAYERS,
@@ -74,7 +78,8 @@
assertEquals(rule.getTargetMixRole(), MIX_ROLE_PLAYERS);
assertThat(rule.getCriteria(), containsInAnyOrder(
isAudioMixMatchUsageCriterion(USAGE_MEDIA),
- isAudioMixMatchUidCriterion(TEST_UID)));
+ isAudioMixMatchUidCriterion(TEST_UID),
+ isAudioMixExcludeSessionCriterion(TEST_SESSION_ID)));
}
@Test
@@ -183,6 +188,30 @@
.build());
}
+ @Test
+ public void sessionIdRuleCompatibleWithPlayersMix() {
+ int sessionId = 42;
+ AudioMixingRule rule = new AudioMixingRule.Builder()
+ .addMixRule(RULE_MATCH_AUDIO_SESSION_ID, sessionId)
+ .setTargetMixRole(MIX_ROLE_PLAYERS)
+ .build();
+
+ assertEquals(rule.getTargetMixRole(), MIX_ROLE_PLAYERS);
+ assertThat(rule.getCriteria(), containsInAnyOrder(isAudioMixSessionCriterion(sessionId)));
+ }
+
+ @Test
+ public void sessionIdRuleCompatibleWithInjectorMix() {
+ AudioMixingRule rule = new AudioMixingRule.Builder()
+ .addMixRule(RULE_MATCH_AUDIO_SESSION_ID, TEST_SESSION_ID)
+ .setTargetMixRole(MIX_ROLE_INJECTOR)
+ .build();
+
+ assertEquals(rule.getTargetMixRole(), MIX_ROLE_INJECTOR);
+ assertThat(rule.getCriteria(),
+ containsInAnyOrder(isAudioMixSessionCriterion(TEST_SESSION_ID)));
+ }
+
private static Matcher isAudioMixUidCriterion(int uid, boolean exclude) {
return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("uid mix criterion") {
@@ -257,5 +286,31 @@
return isAudioMixUsageCriterion(usage, /*exclude=*/ false);
}
+ private static Matcher isAudioMixSessionCriterion(int sessionId, boolean exclude) {
+ return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("sessionId mix criterion") {
+ @Override
+ public boolean matchesSafely(AudioMixMatchCriterion item) {
+ int excludeRule =
+ exclude ? RULE_EXCLUDE_AUDIO_SESSION_ID : RULE_MATCH_AUDIO_SESSION_ID;
+ return item.getRule() == excludeRule && item.getIntProp() == sessionId;
+ }
+
+ @Override
+ public void describeMismatchSafely(
+ AudioMixMatchCriterion item, Description mismatchDescription) {
+ mismatchDescription.appendText(
+ String.format("is not %s criterion with session id %d",
+ exclude ? "exclude" : "match", sessionId));
+ }
+ };
+ }
+
+ private static Matcher isAudioMixSessionCriterion(int sessionId) {
+ return isAudioMixSessionCriterion(sessionId, /*exclude=*/ false);
+ }
+
+ private static Matcher isAudioMixExcludeSessionCriterion(int sessionId) {
+ return isAudioMixSessionCriterion(sessionId, /*exclude=*/ true);
+ }
}
diff --git a/media/tests/EffectsTest/src/com/android/effectstest/BassBoostTest.java b/media/tests/EffectsTest/src/com/android/effectstest/BassBoostTest.java
index a207bf1..fa186c3 100644
--- a/media/tests/EffectsTest/src/com/android/effectstest/BassBoostTest.java
+++ b/media/tests/EffectsTest/src/com/android/effectstest/BassBoostTest.java
@@ -304,13 +304,13 @@
for (int j = 0; j < NUM_EFFECTS; j++) {
effects[j] = new BassBoost(0, mSession);
effects[j].setControlStatusListener(mEffectListener);
- yield();
+ this.yield();
}
for (int j = NUM_EFFECTS - 1; j >= 0; j--) {
Log.w(TAG, "HammerReleaseTest releasing effect " + (Object) effects[j]);
effects[j].release();
effects[j] = null;
- yield();
+ this.yield();
}
}
Log.w(TAG, "HammerReleaseTest ended");
diff --git a/media/tests/EffectsTest/src/com/android/effectstest/VisualizerTest.java b/media/tests/EffectsTest/src/com/android/effectstest/VisualizerTest.java
index dcfe11a..5d91a2e 100644
--- a/media/tests/EffectsTest/src/com/android/effectstest/VisualizerTest.java
+++ b/media/tests/EffectsTest/src/com/android/effectstest/VisualizerTest.java
@@ -251,13 +251,13 @@
for (int i = 0; i < NUM_ITERATIONS; i++) {
for (int j = 0; j < NUM_EFFECTS; j++) {
effects[j] = new Visualizer(mSession);
- yield();
+ this.yield();
}
for (int j = NUM_EFFECTS - 1; j >= 0; j--) {
Log.w(TAG, "HammerReleaseTest releasing effect " + (Object) effects[j]);
effects[j].release();
effects[j] = null;
- yield();
+ this.yield();
}
}
Log.w(TAG, "HammerReleaseTest ended");
diff --git a/packages/CompanionDeviceManager/res/layout/list_item_device.xml b/packages/CompanionDeviceManager/res/layout/list_item_device.xml
index db54ae3..d4439f9 100644
--- a/packages/CompanionDeviceManager/res/layout/list_item_device.xml
+++ b/packages/CompanionDeviceManager/res/layout/list_item_device.xml
@@ -39,7 +39,6 @@
android:layout_height="wrap_content"
android:paddingStart="24dp"
android:paddingEnd="24dp"
- android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItemSmall"/>
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialEntryUi.kt
new file mode 100644
index 0000000..96cc02f
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialEntryUi.kt
@@ -0,0 +1,55 @@
+/*
+ * 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
+
+import android.app.slice.Slice
+import android.credentials.ui.Entry
+import android.graphics.drawable.Icon
+
+/**
+ * UI representation for a credential entry used during the get credential flow.
+ *
+ * TODO: move to jetpack.
+ */
+class CredentialEntryUi(
+ val userName: CharSequence,
+ val displayName: CharSequence?,
+ val icon: Icon?,
+ // TODO: add last used.
+) {
+ companion object {
+ fun fromSlice(slice: Slice): CredentialEntryUi {
+ val items = slice.items
+
+ var title: String? = null
+ var subTitle: String? = null
+ var icon: Icon? = null
+
+ items.forEach {
+ if (it.hasHint(Entry.HINT_ICON)) {
+ icon = it.icon
+ } else if (it.hasHint(Entry.HINT_SUBTITLE)) {
+ subTitle = it.text.toString()
+ } else if (it.hasHint(Entry.HINT_TITLE)) {
+ title = it.text.toString()
+ }
+ }
+ // TODO: fail NPE more elegantly.
+ return CredentialEntryUi(title!!, subTitle, icon)
+ }
+ }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 46bf19c..8db547a 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -1,3 +1,19 @@
+/*
+ * 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
import android.app.slice.Slice
@@ -9,11 +25,8 @@
import android.credentials.ui.RequestInfo
import android.graphics.drawable.Icon
import android.os.Binder
-import com.android.credentialmanager.createflow.CreateOptionInfo
import com.android.credentialmanager.createflow.CreatePasskeyUiState
import com.android.credentialmanager.createflow.CreateScreenState
-import com.android.credentialmanager.createflow.ProviderInfo
-import com.android.credentialmanager.getflow.CredentialOptionInfo
import com.android.credentialmanager.getflow.GetCredentialUiState
import com.android.credentialmanager.getflow.GetScreenState
@@ -41,6 +54,39 @@
) ?: testProviderList()
}
+ fun getCredentialInitialUiState(): GetCredentialUiState {
+ val providerList = GetFlowUtils.toProviderList(providerList, context)
+ return GetCredentialUiState(
+ providerList,
+ GetScreenState.CREDENTIAL_SELECTION,
+ providerList.first()
+ )
+ }
+
+ fun createPasskeyInitialUiState(): CreatePasskeyUiState {
+ val providerList = CreateFlowUtils.toProviderList(providerList, context)
+ return CreatePasskeyUiState(
+ providers = providerList,
+ currentScreenState = CreateScreenState.PASSKEY_INTRO,
+ )
+ }
+
+ companion object {
+ lateinit var repo: CredentialManagerRepo
+
+ fun setup(
+ context: Context,
+ intent: Intent,
+ ) {
+ repo = CredentialManagerRepo(context, intent)
+ }
+
+ fun getInstance(): CredentialManagerRepo {
+ return repo
+ }
+ }
+
+ // TODO: below are prototype functionalities. To be removed for productionization.
private fun testProviderList(): List<ProviderData> {
return listOf(
ProviderData(
@@ -94,143 +140,4 @@
slice
)
}
-
- private fun getCredentialProviderList():
- List<com.android.credentialmanager.getflow.ProviderInfo> {
- return listOf(
- com.android.credentialmanager.getflow.ProviderInfo(
- icon = context.getDrawable(R.drawable.ic_passkey)!!,
- name = "Google Password Manager",
- appDomainName = "tribank.us",
- credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
- credentialOptions = listOf(
- CredentialOptionInfo(
- icon = context.getDrawable(R.drawable.ic_passkey)!!,
- title = "Elisa Backett",
- subtitle = "elisa.beckett@gmail.com",
- id = "id-1",
- ),
- CredentialOptionInfo(
- icon = context.getDrawable(R.drawable.ic_passkey)!!,
- title = "Elisa Backett Work",
- subtitle = "elisa.beckett.work@google.com",
- id = "id-2",
- ),
- )
- ),
- com.android.credentialmanager.getflow.ProviderInfo(
- icon = context.getDrawable(R.drawable.ic_passkey)!!,
- name = "Lastpass",
- appDomainName = "tribank.us",
- credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
- credentialOptions = listOf(
- CredentialOptionInfo(
- icon = context.getDrawable(R.drawable.ic_passkey)!!,
- title = "Elisa Backett",
- subtitle = "elisa.beckett@lastpass.com",
- id = "id-1",
- ),
- )
- ),
- com.android.credentialmanager.getflow.ProviderInfo(
- icon = context.getDrawable(R.drawable.ic_passkey)!!,
- name = "Dashlane",
- appDomainName = "tribank.us",
- credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
- credentialOptions = listOf(
- CredentialOptionInfo(
- icon = context.getDrawable(R.drawable.ic_passkey)!!,
- title = "Elisa Backett",
- subtitle = "elisa.beckett@dashlane.com",
- id = "id-1",
- ),
- )
- ),
- )
- }
-
- private fun createCredentialProviderList(): List<ProviderInfo> {
- return listOf(
- ProviderInfo(
- icon = context.getDrawable(R.drawable.ic_passkey)!!,
- name = "Google Password Manager",
- appDomainName = "tribank.us",
- credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
- createOptions = listOf(
- CreateOptionInfo(
- icon = context.getDrawable(R.drawable.ic_passkey)!!,
- title = "Elisa Backett",
- subtitle = "elisa.beckett@gmail.com",
- id = "id-1",
- ),
- CreateOptionInfo(
- icon = context.getDrawable(R.drawable.ic_passkey)!!,
- title = "Elisa Backett Work",
- subtitle = "elisa.beckett.work@google.com",
- id = "id-2",
- ),
- )
- ),
- ProviderInfo(
- icon = context.getDrawable(R.drawable.ic_passkey)!!,
- name = "Lastpass",
- appDomainName = "tribank.us",
- credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
- createOptions = listOf(
- CreateOptionInfo(
- icon = context.getDrawable(R.drawable.ic_passkey)!!,
- title = "Elisa Backett",
- subtitle = "elisa.beckett@lastpass.com",
- id = "id-1",
- ),
- )
- ),
- ProviderInfo(
- icon = context.getDrawable(R.drawable.ic_passkey)!!,
- name = "Dashlane",
- appDomainName = "tribank.us",
- credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
- createOptions = listOf(
- CreateOptionInfo(
- icon = context.getDrawable(R.drawable.ic_passkey)!!,
- title = "Elisa Backett",
- subtitle = "elisa.beckett@dashlane.com",
- id = "id-1",
- ),
- )
- ),
- )
- }
-
- fun getCredentialInitialUiState(): GetCredentialUiState {
- val providerList = getCredentialProviderList()
- return GetCredentialUiState(
- providerList,
- GetScreenState.CREDENTIAL_SELECTION,
- providerList.first()
- )
- }
-
- fun createPasskeyInitialUiState(): CreatePasskeyUiState {
- val providerList = createCredentialProviderList()
- return CreatePasskeyUiState(
- providers = providerList,
- currentScreenState = CreateScreenState.PASSKEY_INTRO,
- )
- }
-
- companion object {
- lateinit var repo: CredentialManagerRepo
-
- fun setup(
- context: Context,
- intent: Intent,
- ) {
- repo = CredentialManagerRepo(context, intent)
- }
-
- fun getInstance(): CredentialManagerRepo {
- return repo
- }
- }
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
index 98c824c..b538ae7 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
@@ -1,5 +1,22 @@
+/*
+ * 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
+import android.credentials.ui.RequestInfo
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
@@ -16,14 +33,20 @@
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CredentialManagerRepo.setup(this, intent)
- val startDestination = intent.extras?.getString(
- "start_destination",
- "CREATE_PASSKEY"
- ) ?: "CREATE_PASSKEY"
-
- setContent {
- CredentialSelectorTheme {
- CredentialManagerBottomSheet(startDestination)
+ val requestInfo = intent.extras?.getParcelable<RequestInfo>(RequestInfo.EXTRA_REQUEST_INFO)
+ if (requestInfo != null) {
+ val requestType = requestInfo.type
+ setContent {
+ CredentialSelectorTheme {
+ CredentialManagerBottomSheet(requestType)
+ }
+ }
+ } else {
+ // TODO: prototype only code to be removed. In production should exit.
+ setContent {
+ CredentialSelectorTheme {
+ CredentialManagerBottomSheet(RequestInfo.TYPE_CREATE)
+ }
}
}
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
new file mode 100644
index 0000000..7159ab9
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -0,0 +1,106 @@
+/*
+ * 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
+
+import android.content.Context
+import android.credentials.ui.Entry
+import android.credentials.ui.ProviderData
+import com.android.credentialmanager.createflow.CreateOptionInfo
+import com.android.credentialmanager.getflow.CredentialOptionInfo
+import com.android.credentialmanager.getflow.ProviderInfo
+
+/** Utility functions for converting CredentialManager data structures to or from UI formats. */
+class GetFlowUtils {
+ companion object {
+
+ fun toProviderList(
+ providerDataList: List<ProviderData>,
+ context: Context,
+ ): List<ProviderInfo> {
+ return providerDataList.map {
+ ProviderInfo(
+ // TODO: replace to extract from the service data structure when available
+ icon = context.getDrawable(R.drawable.ic_passkey)!!,
+ name = it.packageName,
+ appDomainName = "tribank.us",
+ credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
+ credentialOptions = toCredentialOptionInfoList(it.credentialEntries, context)
+ )
+ }
+ }
+
+
+ /* From service data structure to UI credential entry list representation. */
+ private fun toCredentialOptionInfoList(
+ credentialEntries: List<Entry>,
+ context: Context,
+ ): List<CredentialOptionInfo> {
+ return credentialEntries.map {
+ val credentialEntryUi = CredentialEntryUi.fromSlice(it.slice)
+
+ // Consider directly move the UI object into the class.
+ return@map CredentialOptionInfo(
+ // TODO: remove fallbacks
+ icon = credentialEntryUi.icon?.loadDrawable(context)
+ ?: context.getDrawable(R.drawable.ic_passkey)!!,
+ title = credentialEntryUi.userName.toString(),
+ subtitle = credentialEntryUi.displayName?.toString() ?: "Unknown display name",
+ id = it.entryId,
+ )
+ }
+ }
+ }
+}
+
+class CreateFlowUtils {
+ companion object {
+
+ fun toProviderList(
+ providerDataList: List<ProviderData>,
+ context: Context,
+ ): List<com.android.credentialmanager.createflow.ProviderInfo> {
+ return providerDataList.map {
+ com.android.credentialmanager.createflow.ProviderInfo(
+ // TODO: replace to extract from the service data structure when available
+ icon = context.getDrawable(R.drawable.ic_passkey)!!,
+ name = it.packageName,
+ appDomainName = "tribank.us",
+ credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
+ createOptions = toCreationOptionInfoList(it.credentialEntries, context),
+ )
+ }
+ }
+
+ private fun toCreationOptionInfoList(
+ creationEntries: List<Entry>,
+ context: Context,
+ ): List<CreateOptionInfo> {
+ return creationEntries.map {
+ val saveEntryUi = SaveEntryUi.fromSlice(it.slice)
+
+ return@map CreateOptionInfo(
+ // TODO: remove fallbacks
+ icon = saveEntryUi.icon?.loadDrawable(context)
+ ?: context.getDrawable(R.drawable.ic_passkey)!!,
+ title = saveEntryUi.title.toString(),
+ subtitle = saveEntryUi.subTitle?.toString() ?: "Unknown subtitle",
+ id = it.entryId,
+ )
+ }
+ }
+ }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/SaveEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/SaveEntryUi.kt
new file mode 100644
index 0000000..2b63e1d
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/SaveEntryUi.kt
@@ -0,0 +1,55 @@
+/*
+ * 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
+
+import android.app.slice.Slice
+import android.credentials.ui.Entry
+import android.graphics.drawable.Icon
+
+/**
+ * UI representation for a save entry used during the create credential flow.
+ *
+ * TODO: move to jetpack.
+ */
+class SaveEntryUi(
+ val title: CharSequence,
+ val subTitle: CharSequence?,
+ val icon: Icon?,
+ // TODO: add
+) {
+ companion object {
+ fun fromSlice(slice: Slice): SaveEntryUi {
+ val items = slice.items
+
+ var title: String? = null
+ var subTitle: String? = null
+ var icon: Icon? = null
+
+ items.forEach {
+ if (it.hasHint(Entry.HINT_ICON)) {
+ icon = it.icon
+ } else if (it.hasHint(Entry.HINT_SUBTITLE)) {
+ subTitle = it.text.toString()
+ } else if (it.hasHint(Entry.HINT_TITLE)) {
+ title = it.text.toString()
+ }
+ }
+ // TODO: fail NPE more elegantly.
+ return SaveEntryUi(title!!, subTitle, icon)
+ }
+ }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/DialogType.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/DialogType.kt
index 8bb80a1..b5e9fd00 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/DialogType.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/DialogType.kt
@@ -1,5 +1,23 @@
+/*
+ * 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.common
+import android.credentials.ui.RequestInfo
+
enum class DialogType {
CREATE_PASSKEY,
GET_CREDENTIALS,
@@ -8,10 +26,10 @@
companion object {
fun toDialogType(value: String): DialogType {
- return try {
- valueOf(value)
- } catch (e: IllegalArgumentException) {
- UNKNOWN
+ return when (value) {
+ RequestInfo.TYPE_GET -> GET_CREDENTIALS
+ RequestInfo.TYPE_CREATE -> CREATE_PASSKEY
+ else -> UNKNOWN
}
}
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
index 044688b..12f1846 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
@@ -1,3 +1,19 @@
+/*
+ * 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.createflow
import android.graphics.drawable.Drawable
@@ -14,7 +30,7 @@
val icon: Drawable,
val title: String,
val subtitle: String,
- val id: String,
+ val id: Int,
)
/** The name of the current screen. */
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
index 997519d..b61f465 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
@@ -222,7 +222,9 @@
) {
Column() {
TopAppBar(
- title = { Text(text = stringResource(R.string.string_more_options), style = Typography.subtitle1) },
+ title = {
+ Text(text = stringResource(R.string.string_more_options), style = Typography.subtitle1)
+ },
backgroundColor = lightBackgroundColor,
elevation = 0.dp,
navigationIcon =
@@ -343,7 +345,7 @@
@Composable
fun CreationSelectionCard(
providerInfo: ProviderInfo,
- onOptionSelected: (String) -> Unit,
+ onOptionSelected: (Int) -> Unit,
onCancel: () -> Unit,
multiProvider: Boolean,
onMoreOptionSelected: (String) -> Unit,
@@ -414,7 +416,7 @@
@ExperimentalMaterialApi
@Composable
-fun CreateOptionRow(createOptionInfo: CreateOptionInfo, onOptionSelected: (String) -> Unit) {
+fun CreateOptionRow(createOptionInfo: CreateOptionInfo, onOptionSelected: (Int) -> Unit) {
Chip(
modifier = Modifier.fillMaxWidth(),
onClick = {onOptionSelected(createOptionInfo.id)},
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
index 15300de..5b70f9d 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
@@ -1,3 +1,19 @@
+/*
+ * 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.createflow
import android.util.Log
@@ -42,7 +58,7 @@
)
}
- fun onCreateOptionSelected(createOptionId: String) {
+ fun onCreateOptionSelected(createOptionId: Int) {
Log.d("Account Selector", "Option selected for creation: $createOptionId")
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index 1ca70ed..0b18822 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -1,3 +1,19 @@
+/*
+ * 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.getflow
import androidx.compose.foundation.Image
@@ -74,7 +90,7 @@
@Composable
fun CredentialSelectionCard(
providerInfo: ProviderInfo,
- onOptionSelected: (String) -> Unit,
+ onOptionSelected: (Int) -> Unit,
onCancel: () -> Unit,
multiProvider: Boolean,
onMoreOptionSelected: () -> Unit,
@@ -149,7 +165,7 @@
@Composable
fun CredentialOptionRow(
credentialOptionInfo: CredentialOptionInfo,
- onOptionSelected: (String) -> Unit
+ onOptionSelected: (Int) -> Unit
) {
Chip(
modifier = Modifier.fillMaxWidth(),
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
index 06bcd7f..0fdd8ec 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
@@ -1,3 +1,19 @@
+/*
+ * 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.getflow
import android.util.Log
@@ -20,7 +36,7 @@
var uiState by mutableStateOf(credManRepo.getCredentialInitialUiState())
private set
- fun onCredentailSelected(credentialId: String) {
+ fun onCredentailSelected(credentialId: Int) {
Log.d("Account Selector", "credential selected: $credentialId")
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
index 867e9c2..acea8c9 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
@@ -1,3 +1,19 @@
+/*
+ * 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.getflow
import android.graphics.drawable.Drawable
@@ -14,7 +30,7 @@
val icon: Drawable,
val title: String,
val subtitle: String,
- val id: String,
+ val id: Int,
)
/** The name of the current screen. */
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 f9d2dbc..476dd30 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
@@ -47,13 +47,19 @@
/**
* The Activity to render ALL SPA pages, and handles jumps between SPA pages.
+ *
* One can open any SPA page by:
- * $ adb shell am start -n <BrowseActivityComponent> -e spa:SpaActivity:destination <SpaPageRoute>
- * For gallery, BrowseActivityComponent = com.android.settingslib.spa.gallery/.MainActivity
- * For SettingsGoogle, BrowseActivityComponent = com.android.settings/.spa.SpaActivity
+ * ```
+ * $ adb shell am start -n <BrowseActivityComponent> -e spaActivityDestination <SpaPageRoute>
+ * ```
+ * - For Gallery, BrowseActivityComponent = com.android.settingslib.spa.gallery/.GalleryMainActivity
+ * - For Settings, BrowseActivityComponent = com.android.settings/.spa.SpaActivity
+ *
* Some examples:
- * $ adb shell am start -n <BrowseActivityComponent> -e spa:SpaActivity:destination HOME
- * $ adb shell am start -n <BrowseActivityComponent> -e spa:SpaActivity:destination ARGUMENT/bar/5
+ * ```
+ * $ adb shell am start -n <BrowseActivityComponent> -e spaActivityDestination HOME
+ * $ adb shell am start -n <BrowseActivityComponent> -e spaActivityDestination ARGUMENT/bar/5
+ * ```
*/
open class BrowseActivity : ComponentActivity() {
private val spaEnvironment get() = SpaEnvironmentFactory.instance
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 7f2af92..ca75b77 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
@@ -19,6 +19,9 @@
import android.os.Bundle
import android.widget.Toast
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.ProvidedValue
+import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
@@ -28,6 +31,16 @@
const val INJECT_ENTRY_NAME = "INJECT"
const val ROOT_ENTRY_NAME = "ROOT"
+interface EntryData {
+ val pageId: String
+ val entryId: String
+ val isHighlighted: Boolean
+ get() = false
+}
+
+val LocalEntryDataProvider =
+ compositionLocalOf<EntryData> { error("LocalEntryDataProvider: No Default Value!") }
+
/**
* Defines data of a Settings entry.
*/
@@ -121,7 +134,22 @@
// TODO: Add highlight entry logic
Toast.makeText(context, "entry $id highlighted", Toast.LENGTH_SHORT).show()
}
- uiLayoutImpl(fullArgument(runtimeArguments))
+
+ CompositionLocalProvider(provideLocalEntryData()) {
+ uiLayoutImpl(fullArgument(runtimeArguments))
+ }
+ }
+
+ @Composable
+ fun provideLocalEntryData(): ProvidedValue<EntryData> {
+ val controller = LocalNavController.current
+ return LocalEntryDataProvider provides remember {
+ object : EntryData {
+ override val pageId = containerPage().id
+ override val entryId = id
+ override val isHighlighted = controller.highlightEntryId == id
+ }
+ }
}
}
diff --git a/packages/SettingsProvider/AndroidManifest.xml b/packages/SettingsProvider/AndroidManifest.xml
index c5bea2e..79677eea 100644
--- a/packages/SettingsProvider/AndroidManifest.xml
+++ b/packages/SettingsProvider/AndroidManifest.xml
@@ -4,6 +4,7 @@
android:sharedUserId="android.uid.system">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+ <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<application android:allowClearUserData="false"
android:label="@string/app_label"
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 8efec67..3a25d85 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -86,6 +86,7 @@
import android.os.RemoteException;
import android.os.SELinux;
import android.os.ServiceManager;
+import android.os.SystemConfigManager;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
@@ -110,12 +111,11 @@
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.FrameworkStatsLog;
import com.android.providers.settings.SettingsState.Setting;
-import com.android.server.SystemConfig;
-
-import com.google.android.collect.Sets;
import libcore.util.HexEncoding;
+import com.google.android.collect.Sets;
+
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
@@ -368,6 +368,8 @@
// We have to call in the package manager with no lock held,
private volatile IPackageManager mPackageManager;
+ private volatile SystemConfigManager mSysConfigManager;
+
@GuardedBy("mLock")
private boolean mSyncConfigDisabledUntilReboot;
@@ -397,6 +399,7 @@
synchronized (mLock) {
mUserManager = UserManager.get(getContext());
mPackageManager = AppGlobals.getPackageManager();
+ mSysConfigManager = getContext().getSystemService(SystemConfigManager.class);
mHandlerThread = new HandlerThread(LOG_TAG,
Process.THREAD_PRIORITY_BACKGROUND);
mHandlerThread.start();
@@ -3875,8 +3878,7 @@
Setting currentSetting = secureSettings.getSettingLocked(
Settings.Secure.ENABLED_VR_LISTENERS);
if (currentSetting.isNull()) {
- ArraySet<ComponentName> l =
- SystemConfig.getInstance().getDefaultVrComponents();
+ List<ComponentName> l = mSysConfigManager.getDefaultVrComponents();
if (l != null && !l.isEmpty()) {
StringBuilder b = new StringBuilder();
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index cd667ca..765ee89 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -98,7 +98,7 @@
private static final long MAX_WRITE_SETTINGS_DELAY_MILLIS = 2000;
public static final int MAX_BYTES_PER_APP_PACKAGE_UNLIMITED = -1;
- public static final int MAX_BYTES_PER_APP_PACKAGE_LIMITED = 20000;
+ public static final int MAX_BYTES_PER_APP_PACKAGE_LIMITED = 40000;
public static final int VERSION_UNDEFINED = -1;
@@ -732,19 +732,19 @@
}
@GuardedBy("mLock")
- private int getNewMemoryUsagePerPackageLocked(String packageName, int deltaKeySize,
+ private int getNewMemoryUsagePerPackageLocked(String packageName, int deltaKeyLength,
String oldValue, String newValue, String oldDefaultValue, String newDefaultValue) {
if (isExemptFromMemoryUsageCap(packageName)) {
return 0;
}
- final Integer currentSize = mPackageToMemoryUsage.get(packageName);
- final int oldValueSize = (oldValue != null) ? oldValue.length() : 0;
- final int newValueSize = (newValue != null) ? newValue.length() : 0;
- final int oldDefaultValueSize = (oldDefaultValue != null) ? oldDefaultValue.length() : 0;
- final int newDefaultValueSize = (newDefaultValue != null) ? newDefaultValue.length() : 0;
- final int deltaSize = deltaKeySize + newValueSize + newDefaultValueSize
- - oldValueSize - oldDefaultValueSize;
- return Math.max((currentSize != null) ? currentSize + deltaSize : deltaSize, 0);
+ final int currentSize = mPackageToMemoryUsage.getOrDefault(packageName, 0);
+ final int oldValueLength = (oldValue != null) ? oldValue.length() : 0;
+ final int newValueLength = (newValue != null) ? newValue.length() : 0;
+ final int oldDefaultValueLength = (oldDefaultValue != null) ? oldDefaultValue.length() : 0;
+ final int newDefaultValueLength = (newDefaultValue != null) ? newDefaultValue.length() : 0;
+ final int deltaSize = (deltaKeyLength + newValueLength + newDefaultValueLength
+ - oldValueLength - oldDefaultValueLength) * Character.BYTES;
+ return Math.max(currentSize + deltaSize, 0);
}
@GuardedBy("mLock")
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
index 5a534b9..4ed28d5 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -329,46 +329,47 @@
final String testKey1 = SETTING_NAME;
final String testValue1 = Strings.repeat("A", 100);
settingsState.insertSettingLocked(testKey1, testValue1, null, true, TEST_PACKAGE);
- int expectedMemUsage = testKey1.length() + testValue1.length()
- + testValue1.length() /* size for default */;
+ int expectedMemUsage = (testKey1.length() + testValue1.length()
+ + testValue1.length() /* size for default */) * Character.BYTES;
assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
// Test inserting another key
final String testKey2 = SETTING_NAME + "2";
settingsState.insertSettingLocked(testKey2, testValue1, null, false, TEST_PACKAGE);
- expectedMemUsage += testKey2.length() + testValue1.length();
+ expectedMemUsage += (testKey2.length() + testValue1.length()) * Character.BYTES;
assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
// Test updating first key with new default
final String testValue2 = Strings.repeat("A", 300);
settingsState.insertSettingLocked(testKey1, testValue2, null, true, TEST_PACKAGE);
- expectedMemUsage += (testValue2.length() - testValue1.length()) * 2;
+ expectedMemUsage += (testValue2.length() - testValue1.length()) * 2 * Character.BYTES;
assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
// Test updating first key without new default
final String testValue3 = Strings.repeat("A", 50);
settingsState.insertSettingLocked(testKey1, testValue3, null, false, TEST_PACKAGE);
- expectedMemUsage -= testValue2.length() - testValue3.length();
+ expectedMemUsage -= (testValue2.length() - testValue3.length()) * Character.BYTES;
assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
// Test updating second key
settingsState.insertSettingLocked(testKey2, testValue2, null, false, TEST_PACKAGE);
- expectedMemUsage -= testValue1.length() - testValue2.length();
+ expectedMemUsage -= (testValue1.length() - testValue2.length()) * Character.BYTES;
assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
// Test resetting key
settingsState.resetSettingLocked(testKey1);
- expectedMemUsage += testValue2.length() - testValue3.length();
+ expectedMemUsage += (testValue2.length() - testValue3.length()) * Character.BYTES;
assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
// Test resetting default value
settingsState.resetSettingDefaultValueLocked(testKey1);
- expectedMemUsage -= testValue2.length();
+ expectedMemUsage -= testValue2.length() * Character.BYTES;
assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
// Test deletion
settingsState.deleteSettingLocked(testKey2);
- expectedMemUsage -= testValue2.length() + testKey2.length() /* key is deleted too */;
+ expectedMemUsage -= (testValue2.length() + testKey2.length() /* key is deleted too */)
+ * Character.BYTES;
assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
// Test another package with a different key
@@ -376,7 +377,8 @@
final String testKey3 = SETTING_NAME + "3";
settingsState.insertSettingLocked(testKey3, testValue1, null, true, testPackage2);
assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
- final int expectedMemUsage2 = testKey3.length() + testValue1.length() * 2;
+ final int expectedMemUsage2 = (testKey3.length() + testValue1.length() * 2)
+ * Character.BYTES;
assertEquals(expectedMemUsage2, settingsState.getMemoryUsage(testPackage2));
// Test system package
diff --git a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
index 9476912..cb37c07 100644
--- a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
+++ b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
@@ -199,8 +199,8 @@
}
mBugreportFd = ParcelFileDescriptor.dup(invocation.getArgument(2));
return null;
- }).when(mMockIDumpstate).startBugreport(anyInt(), any(), any(), any(), anyInt(), any(),
- anyBoolean());
+ }).when(mMockIDumpstate).startBugreport(anyInt(), any(), any(), any(), anyInt(), anyInt(),
+ any(), anyBoolean());
setWarningState(mContext, STATE_HIDE);
@@ -543,7 +543,7 @@
getInstrumentation().waitForIdleSync();
verify(mMockIDumpstate, times(1)).startBugreport(anyInt(), any(), any(), any(),
- anyInt(), any(), anyBoolean());
+ anyInt(), anyInt(), any(), anyBoolean());
sendBugreportFinished();
}
@@ -608,7 +608,7 @@
ArgumentCaptor<IDumpstateListener> listenerCap = ArgumentCaptor.forClass(
IDumpstateListener.class);
verify(mMockIDumpstate, timeout(TIMEOUT)).startBugreport(anyInt(), any(), any(), any(),
- anyInt(), listenerCap.capture(), anyBoolean());
+ anyInt(), anyInt(), listenerCap.capture(), anyBoolean());
mIDumpstateListener = listenerCap.getValue();
assertNotNull("Dumpstate listener should not be null", mIDumpstateListener);
mIDumpstateListener.onProgress(0);
diff --git a/packages/SystemUI/res-keyguard/values/strings.xml b/packages/SystemUI/res-keyguard/values/strings.xml
index d90156d..8135aaa 100644
--- a/packages/SystemUI/res-keyguard/values/strings.xml
+++ b/packages/SystemUI/res-keyguard/values/strings.xml
@@ -241,4 +241,6 @@
<string name="clock_title_bubble">Bubble</string>
<!-- Name of the "Analog" clock face [CHAR LIMIT=15]-->
<string name="clock_title_analog">Analog</string>
+ <!-- Title of bouncer when we want to authenticate before continuing with action. [CHAR LIMIT=NONE] -->
+ <string name="keyguard_unlock_to_continue">Unlock your device to continue</string>
</resources>
diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml
index 1a1fc75..0e9abee 100644
--- a/packages/SystemUI/res/layout/clipboard_overlay.xml
+++ b/packages/SystemUI/res/layout/clipboard_overlay.xml
@@ -14,7 +14,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<com.android.systemui.screenshot.DraggableConstraintLayout
+<com.android.systemui.clipboardoverlay.ClipboardOverlayView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
@@ -157,4 +157,4 @@
android:layout_margin="@dimen/overlay_dismiss_button_margin"
android:src="@drawable/overlay_cancel"/>
</FrameLayout>
-</com.android.systemui.screenshot.DraggableConstraintLayout>
\ No newline at end of file
+</com.android.systemui.clipboardoverlay.ClipboardOverlayView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml b/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml
new file mode 100644
index 0000000..1a1fc75
--- /dev/null
+++ b/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml
@@ -0,0 +1,160 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<com.android.systemui.screenshot.DraggableConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/clipboard_ui"
+ android:theme="@style/FloatingOverlay"
+ android:alpha="0"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/clipboard_overlay_window_name">
+ <ImageView
+ android:id="@+id/actions_container_background"
+ android:visibility="gone"
+ android:layout_height="0dp"
+ android:layout_width="0dp"
+ android:elevation="4dp"
+ android:background="@drawable/action_chip_container_background"
+ android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
+ app:layout_constraintBottom_toBottomOf="@+id/actions_container"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="@+id/actions_container"
+ app:layout_constraintEnd_toEndOf="@+id/actions_container"/>
+ <HorizontalScrollView
+ android:id="@+id/actions_container"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal"
+ android:paddingEnd="@dimen/overlay_action_container_padding_right"
+ android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
+ android:elevation="4dp"
+ android:scrollbars="none"
+ android:layout_marginBottom="4dp"
+ app:layout_constraintHorizontal_bias="0"
+ app:layout_constraintWidth_percent="1.0"
+ app:layout_constraintWidth_max="wrap"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/preview_border"
+ app:layout_constraintEnd_toEndOf="parent">
+ <LinearLayout
+ android:id="@+id/actions"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:animateLayoutChanges="true">
+ <include layout="@layout/overlay_action_chip"
+ android:id="@+id/share_chip"/>
+ <include layout="@layout/overlay_action_chip"
+ android:id="@+id/remote_copy_chip"/>
+ <include layout="@layout/overlay_action_chip"
+ android:id="@+id/edit_chip"/>
+ </LinearLayout>
+ </HorizontalScrollView>
+ <View
+ android:id="@+id/preview_border"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginStart="@dimen/overlay_offset_x"
+ android:layout_marginBottom="12dp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:elevation="7dp"
+ app:layout_constraintEnd_toEndOf="@id/clipboard_preview_end"
+ app:layout_constraintTop_toTopOf="@id/clipboard_preview_top"
+ android:background="@drawable/overlay_border"/>
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/clipboard_preview_end"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:barrierMargin="@dimen/overlay_border_width"
+ app:barrierDirection="end"
+ app:constraint_referenced_ids="clipboard_preview"/>
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/clipboard_preview_top"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:barrierDirection="top"
+ app:barrierMargin="@dimen/overlay_border_width_neg"
+ app:constraint_referenced_ids="clipboard_preview"/>
+ <FrameLayout
+ android:id="@+id/clipboard_preview"
+ android:elevation="7dp"
+ android:background="@drawable/overlay_preview_background"
+ android:clipChildren="true"
+ android:clipToOutline="true"
+ android:clipToPadding="true"
+ android:layout_width="@dimen/clipboard_preview_size"
+ android:layout_margin="@dimen/overlay_border_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ app:layout_constraintBottom_toBottomOf="@id/preview_border"
+ app:layout_constraintStart_toStartOf="@id/preview_border"
+ app:layout_constraintEnd_toEndOf="@id/preview_border"
+ app:layout_constraintTop_toTopOf="@id/preview_border">
+ <TextView android:id="@+id/text_preview"
+ android:textFontWeight="500"
+ android:padding="8dp"
+ android:gravity="center|start"
+ android:ellipsize="end"
+ android:autoSizeTextType="uniform"
+ android:autoSizeMinTextSize="@dimen/clipboard_overlay_min_font"
+ android:autoSizeMaxTextSize="@dimen/clipboard_overlay_max_font"
+ android:textColor="?attr/overlayButtonTextColor"
+ android:textColorLink="?attr/overlayButtonTextColor"
+ android:background="?androidprv:attr/colorAccentSecondary"
+ android:layout_width="@dimen/clipboard_preview_size"
+ android:layout_height="@dimen/clipboard_preview_size"/>
+ <ImageView
+ android:id="@+id/image_preview"
+ android:scaleType="fitCenter"
+ android:adjustViewBounds="true"
+ android:contentDescription="@string/clipboard_image_preview"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+ <TextView
+ android:id="@+id/hidden_preview"
+ android:visibility="gone"
+ android:textFontWeight="500"
+ android:padding="8dp"
+ android:gravity="center"
+ android:textSize="14sp"
+ android:textColor="?attr/overlayButtonTextColor"
+ android:background="?androidprv:attr/colorAccentSecondary"
+ android:layout_width="@dimen/clipboard_preview_size"
+ android:layout_height="@dimen/clipboard_preview_size"/>
+ </FrameLayout>
+ <FrameLayout
+ android:id="@+id/dismiss_button"
+ android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
+ android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
+ 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"
+ android:contentDescription="@string/clipboard_dismiss_description">
+ <ImageView
+ android:id="@+id/dismiss_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="@dimen/overlay_dismiss_button_margin"
+ android:src="@drawable/overlay_cancel"/>
+ </FrameLayout>
+</com.android.systemui.screenshot.DraggableConstraintLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index c34db15..93ee151 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -67,7 +67,6 @@
import android.view.WindowInsets;
import android.view.WindowInsetsAnimation;
import android.view.WindowManager;
-import android.widget.AdapterView;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
@@ -318,7 +317,8 @@
}
void initMode(@Mode int mode, GlobalSettings globalSettings, FalsingManager falsingManager,
- UserSwitcherController userSwitcherController) {
+ UserSwitcherController userSwitcherController,
+ UserSwitcherViewMode.UserSwitcherCallback userSwitcherCallback) {
if (mCurrentMode == mode) return;
Log.i(TAG, "Switching mode from " + modeToString(mCurrentMode) + " to "
+ modeToString(mode));
@@ -330,7 +330,7 @@
mViewMode = new OneHandedViewMode();
break;
case MODE_USER_SWITCHER:
- mViewMode = new UserSwitcherViewMode();
+ mViewMode = new UserSwitcherViewMode(userSwitcherCallback);
break;
default:
mViewMode = new DefaultViewMode();
@@ -864,6 +864,12 @@
private UserSwitcherController.UserSwitchCallback mUserSwitchCallback =
this::setupUserSwitcher;
+ private UserSwitcherCallback mUserSwitcherCallback;
+
+ UserSwitcherViewMode(UserSwitcherCallback userSwitcherCallback) {
+ mUserSwitcherCallback = userSwitcherCallback;
+ }
+
@Override
public void init(@NonNull ConstraintLayout v, @NonNull GlobalSettings globalSettings,
@NonNull KeyguardSecurityViewFlipper viewFlipper,
@@ -1040,34 +1046,25 @@
}
};
- if (adapter.getCount() < 2) {
- // The drop down arrow is at index 1
- ((LayerDrawable) mUserSwitcher.getBackground()).getDrawable(1).setAlpha(0);
- anchor.setClickable(false);
- return;
- } else {
- ((LayerDrawable) mUserSwitcher.getBackground()).getDrawable(1).setAlpha(255);
- }
-
anchor.setOnClickListener((v) -> {
if (mFalsingManager.isFalseTap(LOW_PENALTY)) return;
mPopup = new KeyguardUserSwitcherPopupMenu(v.getContext(), mFalsingManager);
mPopup.setAnchorView(anchor);
mPopup.setAdapter(adapter);
- mPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
- public void onItemClick(AdapterView parent, View view, int pos, long id) {
- if (mFalsingManager.isFalseTap(LOW_PENALTY)) return;
- if (!view.isEnabled()) return;
-
- // Subtract one for the header
- UserRecord user = adapter.getItem(pos - 1);
- if (!user.isCurrent) {
- adapter.onUserListItemClicked(user);
- }
- mPopup.dismiss();
- mPopup = null;
- }
- });
+ mPopup.setOnItemClickListener((parent, view, pos, id) -> {
+ if (mFalsingManager.isFalseTap(LOW_PENALTY)) return;
+ if (!view.isEnabled()) return;
+ // Subtract one for the header
+ UserRecord user = adapter.getItem(pos - 1);
+ if (user.isManageUsers || user.isAddSupervisedUser) {
+ mUserSwitcherCallback.showUnlockToContinueMessage();
+ }
+ if (!user.isCurrent) {
+ adapter.onUserListItemClicked(user);
+ }
+ mPopup.dismiss();
+ mPopup = null;
+ });
mPopup.show();
});
}
@@ -1122,6 +1119,10 @@
constraintSet.applyTo(mView);
}
}
+
+ interface UserSwitcherCallback {
+ void showUnlockToContinueMessage();
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index d448f40..bcd1a1e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -620,7 +620,9 @@
mode = KeyguardSecurityContainer.MODE_ONE_HANDED;
}
- mView.initMode(mode, mGlobalSettings, mFalsingManager, mUserSwitcherController);
+ mView.initMode(mode, mGlobalSettings, mFalsingManager, mUserSwitcherController,
+ () -> showMessage(getContext().getString(R.string.keyguard_unlock_to_continue),
+ null));
}
public void reportFailedUnlockAttempt(int userId, int timeoutMs) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt
index 9eb2c11..c9128e5 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt
@@ -109,12 +109,13 @@
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
runningSecurityShiftAnimator = null
+ if (shouldRestoreLayerType) {
+ v.setLayerType(View.LAYER_TYPE_NONE, /* paint= */ null)
+ }
}
}
)
- var finishedFadingOutNonSecurityView = false
-
runningSecurityShiftAnimator.addUpdateListener { animation: ValueAnimator ->
val switchPoint = SECURITY_SHIFT_ANIMATION_FADE_OUT_PROPORTION
val isFadingOut = animation.animatedFraction < switchPoint
@@ -153,6 +154,13 @@
startRect.right + currentTranslation,
startRect.bottom
)
+ } else {
+ v.setLeftTopRightBottom(
+ startRect.left,
+ startRect.top,
+ startRect.right,
+ startRect.bottom
+ )
}
} else {
// And in again over the remaining (100-X)%.
@@ -175,32 +183,13 @@
endRect.right - translationRemaining,
endRect.bottom
)
- }
- }
- if (animation.animatedFraction == 1.0f && shouldRestoreLayerType) {
- v.setLayerType(View.LAYER_TYPE_NONE, /* paint= */ null)
- }
-
- // For views that are not the security view flipper, we do not want to apply
- // an x translation animation. Instead, we want to fade out, move to final position and
- // then fade in.
- if (v !is KeyguardSecurityViewFlipper) {
- // Opacity goes close to 0 but does not fully get to 0.
- if (opacity - 0.001f < 0f) {
+ } else {
v.setLeftTopRightBottom(
endRect.left,
endRect.top,
endRect.right,
endRect.bottom
)
- finishedFadingOutNonSecurityView = true
- } else if (!finishedFadingOutNonSecurityView) {
- v.setLeftTopRightBottom(
- startRect.left,
- startRect.top,
- startRect.right,
- startRect.bottom
- )
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
index 05e3f1c..82e5704 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
@@ -31,9 +31,12 @@
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.CoreStartable;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.util.DeviceConfigProxy;
import javax.inject.Inject;
+import javax.inject.Provider;
/**
* ClipboardListener brings up a clipboard overlay when something is copied to the clipboard.
@@ -51,20 +54,30 @@
private final Context mContext;
private final DeviceConfigProxy mDeviceConfig;
- private final ClipboardOverlayControllerFactory mOverlayFactory;
+ private final Provider<ClipboardOverlayController> mOverlayProvider;
+ private final ClipboardOverlayControllerLegacyFactory mOverlayFactory;
private final ClipboardManager mClipboardManager;
private final UiEventLogger mUiEventLogger;
- private ClipboardOverlayController mClipboardOverlayController;
+ private final FeatureFlags mFeatureFlags;
+ private boolean mUsingNewOverlay;
+ private ClipboardOverlay mClipboardOverlay;
@Inject
public ClipboardListener(Context context, DeviceConfigProxy deviceConfigProxy,
- ClipboardOverlayControllerFactory overlayFactory, ClipboardManager clipboardManager,
- UiEventLogger uiEventLogger) {
+ Provider<ClipboardOverlayController> clipboardOverlayControllerProvider,
+ ClipboardOverlayControllerLegacyFactory overlayFactory,
+ ClipboardManager clipboardManager,
+ UiEventLogger uiEventLogger,
+ FeatureFlags featureFlags) {
mContext = context;
mDeviceConfig = deviceConfigProxy;
+ mOverlayProvider = clipboardOverlayControllerProvider;
mOverlayFactory = overlayFactory;
mClipboardManager = clipboardManager;
mUiEventLogger = uiEventLogger;
+ mFeatureFlags = featureFlags;
+
+ mUsingNewOverlay = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR);
}
@Override
@@ -89,16 +102,22 @@
return;
}
- if (mClipboardOverlayController == null) {
- mClipboardOverlayController = mOverlayFactory.create(mContext);
+ boolean enabled = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR);
+ if (mClipboardOverlay == null || enabled != mUsingNewOverlay) {
+ mUsingNewOverlay = enabled;
+ if (enabled) {
+ mClipboardOverlay = mOverlayProvider.get();
+ } else {
+ mClipboardOverlay = mOverlayFactory.create(mContext);
+ }
mUiEventLogger.log(CLIPBOARD_OVERLAY_ENTERED, 0, clipSource);
} else {
mUiEventLogger.log(CLIPBOARD_OVERLAY_UPDATED, 0, clipSource);
}
- mClipboardOverlayController.setClipData(clipData, clipSource);
- mClipboardOverlayController.setOnSessionCompleteListener(() -> {
+ mClipboardOverlay.setClipData(clipData, clipSource);
+ mClipboardOverlay.setOnSessionCompleteListener(() -> {
// Session is complete, free memory until it's needed again.
- mClipboardOverlayController = null;
+ mClipboardOverlay = null;
});
}
@@ -120,4 +139,10 @@
private static boolean isEmulator() {
return SystemProperties.getBoolean("ro.boot.qemu", false);
}
+
+ interface ClipboardOverlay {
+ void setClipData(ClipData clipData, String clipSource);
+
+ void setOnSessionCompleteListener(Runnable runnable);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index 7e499eb..bfb27a4 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.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
@@ -37,11 +36,6 @@
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
-import android.annotation.MainThread;
-import android.app.ICompatCameraControlCallback;
import android.app.RemoteAction;
import android.content.BroadcastReceiver;
import android.content.ClipData;
@@ -52,14 +46,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
-import android.content.res.Configuration;
-import android.content.res.Resources;
import android.graphics.Bitmap;
-import android.graphics.Insets;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.Region;
-import android.graphics.drawable.Icon;
import android.hardware.display.DisplayManager;
import android.hardware.input.InputManager;
import android.net.Uri;
@@ -67,57 +54,37 @@
import android.os.Looper;
import android.provider.DeviceConfig;
import android.text.TextUtils;
-import android.util.DisplayMetrics;
import android.util.Log;
-import android.util.MathUtils;
import android.util.Size;
-import android.util.TypedValue;
import android.view.Display;
-import android.view.DisplayCutout;
-import android.view.Gravity;
import android.view.InputEvent;
import android.view.InputEventReceiver;
import android.view.InputMonitor;
-import android.view.LayoutInflater;
import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewRootImpl;
-import android.view.ViewTreeObserver;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityManager;
-import android.view.animation.LinearInterpolator;
-import android.view.animation.PathInterpolator;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextClassificationManager;
import android.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextLinks;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
import androidx.annotation.NonNull;
-import androidx.core.view.ViewCompat;
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import com.android.internal.logging.UiEventLogger;
-import com.android.internal.policy.PhoneWindow;
import com.android.systemui.R;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.broadcast.BroadcastSender;
-import com.android.systemui.screenshot.DraggableConstraintLayout;
-import com.android.systemui.screenshot.FloatingWindowUtil;
-import com.android.systemui.screenshot.OverlayActionChip;
+import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule.OverlayWindowContext;
import com.android.systemui.screenshot.TimeoutHandler;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Optional;
+
+import javax.inject.Inject;
/**
* Controls state and UI for the overlay that appears when something is added to the clipboard
*/
-public class ClipboardOverlayController {
+public class ClipboardOverlayController implements ClipboardListener.ClipboardOverlay {
private static final String TAG = "ClipboardOverlayCtrlr";
/** Constants for screenshot/copy deconflicting */
@@ -126,36 +93,22 @@
public static final String COPY_OVERLAY_ACTION = "com.android.systemui.COPY";
private static final int CLIPBOARD_DEFAULT_TIMEOUT_MILLIS = 6000;
- private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
- private static final int FONT_SEARCH_STEP_PX = 4;
private final Context mContext;
private final ClipboardLogger mClipboardLogger;
private final BroadcastDispatcher mBroadcastDispatcher;
private final DisplayManager mDisplayManager;
- private final DisplayMetrics mDisplayMetrics;
- private final WindowManager mWindowManager;
- private final WindowManager.LayoutParams mWindowLayoutParams;
- private final PhoneWindow mWindow;
+ private final ClipboardOverlayWindow mWindow;
private final TimeoutHandler mTimeoutHandler;
- private final AccessibilityManager mAccessibilityManager;
private final TextClassifier mTextClassifier;
- private final DraggableConstraintLayout mView;
- private final View mClipboardPreview;
- private final ImageView mImagePreview;
- private final TextView mTextPreview;
- private final TextView mHiddenPreview;
- private final View mPreviewBorder;
- private final OverlayActionChip mEditChip;
- private final OverlayActionChip mShareChip;
- private final OverlayActionChip mRemoteCopyChip;
- private final View mActionContainerBackground;
- private final View mDismissButton;
- private final LinearLayout mActionContainer;
- private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>();
+ private final ClipboardOverlayView mView;
private Runnable mOnSessionCompleteListener;
+ private Runnable mOnRemoteCopyTapped;
+ private Runnable mOnShareTapped;
+ private Runnable mOnEditTapped;
+ private Runnable mOnPreviewTapped;
private InputMonitor mInputMonitor;
private InputEventReceiver mInputEventReceiver;
@@ -163,14 +116,66 @@
private BroadcastReceiver mCloseDialogsReceiver;
private BroadcastReceiver mScreenshotReceiver;
- private boolean mBlockAttach = false;
private Animator mExitAnimator;
private Animator mEnterAnimator;
- private final int mOrientation;
- private boolean mKeyboardVisible;
+ private final ClipboardOverlayView.ClipboardOverlayCallbacks mClipboardCallbacks =
+ new ClipboardOverlayView.ClipboardOverlayCallbacks() {
+ @Override
+ public void onInteraction() {
+ mTimeoutHandler.resetTimeout();
+ }
- public ClipboardOverlayController(Context context,
+ @Override
+ public void onSwipeDismissInitiated(Animator animator) {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED);
+ mExitAnimator = animator;
+ }
+
+ @Override
+ public void onDismissComplete() {
+ hideImmediate();
+ }
+
+ @Override
+ public void onPreviewTapped() {
+ if (mOnPreviewTapped != null) {
+ mOnPreviewTapped.run();
+ }
+ }
+
+ @Override
+ public void onShareButtonTapped() {
+ if (mOnShareTapped != null) {
+ mOnShareTapped.run();
+ }
+ }
+
+ @Override
+ public void onEditButtonTapped() {
+ if (mOnEditTapped != null) {
+ mOnEditTapped.run();
+ }
+ }
+
+ @Override
+ public void onRemoteCopyButtonTapped() {
+ if (mOnRemoteCopyTapped != null) {
+ mOnRemoteCopyTapped.run();
+ }
+ }
+
+ @Override
+ public void onDismissButtonTapped() {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
+ animateOut();
+ }
+ };
+
+ @Inject
+ public ClipboardOverlayController(@OverlayWindowContext Context context,
+ ClipboardOverlayView clipboardOverlayView,
+ ClipboardOverlayWindow clipboardOverlayWindow,
BroadcastDispatcher broadcastDispatcher,
BroadcastSender broadcastSender,
TimeoutHandler timeoutHandler, UiEventLogger uiEventLogger) {
@@ -181,121 +186,26 @@
mClipboardLogger = new ClipboardLogger(uiEventLogger);
- mAccessibilityManager = AccessibilityManager.getInstance(mContext);
+ mView = clipboardOverlayView;
+ mWindow = clipboardOverlayWindow;
+ mWindow.init(mView::setInsets, () -> {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
+ hideImmediate();
+ });
+
mTextClassifier = requireNonNull(context.getSystemService(TextClassificationManager.class))
.getTextClassifier();
- mWindowManager = mContext.getSystemService(WindowManager.class);
-
- mDisplayMetrics = new DisplayMetrics();
- mContext.getDisplay().getRealMetrics(mDisplayMetrics);
-
mTimeoutHandler = timeoutHandler;
mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS);
- // Setup the window that we are going to use
- mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams();
- mWindowLayoutParams.setTitle("ClipboardOverlay");
+ mView.setCallbacks(mClipboardCallbacks);
- mWindow = FloatingWindowUtil.getFloatingWindow(mContext);
- mWindow.setWindowManager(mWindowManager, null, null);
- setWindowFocusable(false);
-
- mView = (DraggableConstraintLayout)
- LayoutInflater.from(mContext).inflate(R.layout.clipboard_overlay, null);
- mActionContainerBackground =
- requireNonNull(mView.findViewById(R.id.actions_container_background));
- mActionContainer = requireNonNull(mView.findViewById(R.id.actions));
- mClipboardPreview = requireNonNull(mView.findViewById(R.id.clipboard_preview));
- mImagePreview = requireNonNull(mView.findViewById(R.id.image_preview));
- mTextPreview = requireNonNull(mView.findViewById(R.id.text_preview));
- mHiddenPreview = requireNonNull(mView.findViewById(R.id.hidden_preview));
- mPreviewBorder = requireNonNull(mView.findViewById(R.id.preview_border));
- mEditChip = requireNonNull(mView.findViewById(R.id.edit_chip));
- mShareChip = requireNonNull(mView.findViewById(R.id.share_chip));
- mRemoteCopyChip = requireNonNull(mView.findViewById(R.id.remote_copy_chip));
- mEditChip.setAlpha(1);
- mShareChip.setAlpha(1);
- mRemoteCopyChip.setAlpha(1);
- mDismissButton = requireNonNull(mView.findViewById(R.id.dismiss_button));
-
- mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share));
- mView.setCallbacks(new DraggableConstraintLayout.SwipeDismissCallbacks() {
- @Override
- public void onInteraction() {
- mTimeoutHandler.resetTimeout();
- }
-
- @Override
- public void onSwipeDismissInitiated(Animator animator) {
- mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED);
- mExitAnimator = animator;
- }
-
- @Override
- public void onDismissComplete() {
- hideImmediate();
- }
- });
-
- mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> {
- int availableHeight = mTextPreview.getHeight()
- - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom());
- mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight());
- return true;
- });
-
- mDismissButton.setOnClickListener(view -> {
- mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
- animateOut();
- });
-
- mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true);
- mRemoteCopyChip.setIcon(
- Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true);
- mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true);
- mOrientation = mContext.getResources().getConfiguration().orientation;
-
- attachWindow();
- withWindowAttached(() -> {
+ mWindow.withWindowAttached(() -> {
mWindow.setContentView(mView);
- WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
- mKeyboardVisible = insets.isVisible(WindowInsets.Type.ime());
- updateInsets(insets);
- mWindow.peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener(
- new ViewTreeObserver.OnGlobalLayoutListener() {
- @Override
- public void onGlobalLayout() {
- WindowInsets insets =
- mWindowManager.getCurrentWindowMetrics().getWindowInsets();
- boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime());
- if (keyboardVisible != mKeyboardVisible) {
- mKeyboardVisible = keyboardVisible;
- updateInsets(insets);
- }
- }
- });
- mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback(
- new ViewRootImpl.ActivityConfigCallback() {
- @Override
- public void onConfigurationChanged(Configuration overrideConfig,
- int newDisplayId) {
- if (mContext.getResources().getConfiguration().orientation
- != mOrientation) {
- mClipboardLogger.logSessionComplete(
- CLIPBOARD_OVERLAY_DISMISSED_OTHER);
- hideImmediate();
- }
- }
-
- @Override
- public void requestCompatCameraControl(
- boolean showControl, boolean transformationApplied,
- ICompatCameraControlCallback callback) {
- Log.w(TAG, "unexpected requestCompatCameraControl call");
- }
- });
+ mView.setInsets(mWindow.getWindowInsets(),
+ mContext.getResources().getConfiguration().orientation);
});
mTimeoutHandler.setOnTimeoutRunnable(() -> {
@@ -336,21 +246,19 @@
broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION);
}
- void setClipData(ClipData clipData, String clipSource) {
+ @Override // ClipboardListener.ClipboardOverlay
+ public void setClipData(ClipData clipData, String clipSource) {
if (mExitAnimator != null && mExitAnimator.isRunning()) {
mExitAnimator.cancel();
}
reset();
- String accessibilityAnnouncement;
+ String accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null
&& clipData.getDescription().getExtras()
.getBoolean(ClipDescription.EXTRA_IS_SENSITIVE);
if (clipData == null || clipData.getItemCount() == 0) {
- showTextPreview(
- mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
- mTextPreview);
- accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
+ mView.showDefaultTextPreview();
} else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) {
ClipData.Item item = clipData.getItemAt(0);
if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
@@ -360,53 +268,47 @@
}
}
if (isSensitive) {
- showEditableText(
- mContext.getResources().getString(R.string.clipboard_asterisks), true);
+ showEditableText(mContext.getString(R.string.clipboard_asterisks), true);
} else {
showEditableText(item.getText(), false);
}
- showShareChip(clipData);
+ mOnShareTapped = () -> shareContent(clipData);
+ mView.showShareChip();
accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied);
} else if (clipData.getItemAt(0).getUri() != null) {
if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) {
- showShareChip(clipData);
+ mOnShareTapped = () -> shareContent(clipData);
+ mView.showShareChip();
accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied);
- } else {
- accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
}
} else {
- showTextPreview(
- mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
- mTextPreview);
- accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
+ mView.showDefaultTextPreview();
}
+ maybeShowRemoteCopy(clipData);
+ animateIn();
+ mView.announceForAccessibility(accessibilityAnnouncement);
+ mTimeoutHandler.resetTimeout();
+ }
+
+ private void maybeShowRemoteCopy(ClipData clipData) {
Intent remoteCopyIntent = IntentCreator.getRemoteCopyIntent(clipData, mContext);
// Only show remote copy if it's available.
PackageManager packageManager = mContext.getPackageManager();
if (packageManager.resolveActivity(
remoteCopyIntent, PackageManager.ResolveInfoFlags.of(0)) != null) {
- mRemoteCopyChip.setContentDescription(
- mContext.getString(R.string.clipboard_send_nearby_description));
- mRemoteCopyChip.setVisibility(View.VISIBLE);
- mRemoteCopyChip.setOnClickListener((v) -> {
+ mView.setRemoteCopyVisibility(true);
+ mOnRemoteCopyTapped = () -> {
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED);
mContext.startActivity(remoteCopyIntent);
animateOut();
- });
- mActionContainerBackground.setVisibility(View.VISIBLE);
+ };
} else {
- mRemoteCopyChip.setVisibility(View.GONE);
+ mView.setRemoteCopyVisibility(false);
}
- withWindowAttached(() -> {
- if (mEnterAnimator == null || !mEnterAnimator.isRunning()) {
- mView.post(this::animateIn);
- }
- mView.announceForAccessibility(accessibilityAnnouncement);
- });
- mTimeoutHandler.resetTimeout();
}
- void setOnSessionCompleteListener(Runnable runnable) {
+ @Override // ClipboardListener.ClipboardOverlay
+ public void setOnSessionCompleteListener(Runnable runnable) {
mOnSessionCompleteListener = runnable;
}
@@ -418,72 +320,29 @@
actions.addAll(classification.getActions());
}
mView.post(() -> {
- resetActionChips();
- if (actions.size() > 0) {
- mActionContainerBackground.setVisibility(View.VISIBLE);
- for (RemoteAction action : actions) {
- Intent targetIntent = action.getActionIntent().getIntent();
- ComponentName component = targetIntent.getComponent();
- if (component != null && !TextUtils.equals(source,
- component.getPackageName())) {
- OverlayActionChip chip = constructActionChip(action);
- mActionContainer.addView(chip);
- mActionChips.add(chip);
- break; // only show at most one action chip
- }
- }
- }
+ Optional<RemoteAction> action = actions.stream().filter(remoteAction -> {
+ ComponentName component = remoteAction.getActionIntent().getIntent().getComponent();
+ return component != null && !TextUtils.equals(source, component.getPackageName());
+ }).findFirst();
+ mView.resetActionChips();
+ action.ifPresent(remoteAction -> mView.setActionChip(remoteAction, () -> {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
+ animateOut();
+ }));
});
}
- private void showShareChip(ClipData clip) {
- mShareChip.setVisibility(View.VISIBLE);
- mActionContainerBackground.setVisibility(View.VISIBLE);
- mShareChip.setOnClickListener((v) -> shareContent(clip));
- }
-
- private OverlayActionChip constructActionChip(RemoteAction action) {
- OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate(
- R.layout.overlay_action_chip, mActionContainer, false);
- chip.setText(action.getTitle());
- chip.setContentDescription(action.getTitle());
- chip.setIcon(action.getIcon(), false);
- chip.setPendingIntent(action.getActionIntent(), () -> {
- mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
- animateOut();
- });
- chip.setAlpha(1);
- return chip;
- }
-
private void monitorOutsideTouches() {
InputManager inputManager = mContext.getSystemService(InputManager.class);
mInputMonitor = inputManager.monitorGestureInput("clipboard overlay", 0);
- mInputEventReceiver = new InputEventReceiver(mInputMonitor.getInputChannel(),
- Looper.getMainLooper()) {
+ mInputEventReceiver = new InputEventReceiver(
+ mInputMonitor.getInputChannel(), Looper.getMainLooper()) {
@Override
public void onInputEvent(InputEvent event) {
if (event instanceof MotionEvent) {
MotionEvent motionEvent = (MotionEvent) event;
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
- Region touchRegion = new Region();
-
- final Rect tmpRect = new Rect();
- mPreviewBorder.getBoundsOnScreen(tmpRect);
- tmpRect.inset(
- (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
- (int) FloatingWindowUtil.dpToPx(mDisplayMetrics,
- -SWIPE_PADDING_DP));
- touchRegion.op(tmpRect, Region.Op.UNION);
- mActionContainerBackground.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);
- if (!touchRegion.contains(
+ if (!mView.isInTouchRegion(
(int) motionEvent.getRawX(), (int) motionEvent.getRawY())) {
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE);
animateOut();
@@ -513,95 +372,27 @@
animateOut();
}
- private void showSinglePreview(View v) {
- mTextPreview.setVisibility(View.GONE);
- mImagePreview.setVisibility(View.GONE);
- mHiddenPreview.setVisibility(View.GONE);
- v.setVisibility(View.VISIBLE);
- }
-
- private void showTextPreview(CharSequence text, TextView textView) {
- showSinglePreview(textView);
- final CharSequence truncatedText = text.subSequence(0, Math.min(500, text.length()));
- textView.setText(truncatedText);
- updateTextSize(truncatedText, textView);
-
- textView.addOnLayoutChangeListener(
- (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
- if (right - left != oldRight - oldLeft) {
- updateTextSize(truncatedText, textView);
- }
- });
- mEditChip.setVisibility(View.GONE);
- }
-
- private void updateTextSize(CharSequence text, TextView textView) {
- Paint paint = new Paint(textView.getPaint());
- Resources res = textView.getResources();
- float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font);
- float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font);
- if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) {
- // If the text is a single word and would fit within the TextView at the min font size,
- // find the biggest font size that will fit.
- float fontSizePx = minFontSize;
- while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize
- && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) {
- fontSizePx += FONT_SEARCH_STEP_PX;
- }
- // Need to turn off autosizing, otherwise setTextSize is a no-op.
- textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
- // It's possible to hit the max font size and not fill the width, so centering
- // horizontally looks better in this case.
- textView.setGravity(Gravity.CENTER);
- textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx);
- } else {
- // Otherwise just stick with autosize.
- textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize,
- (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX);
- textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
- }
- }
-
- private static boolean fitsInView(CharSequence text, TextView textView, Paint paint,
- float fontSizePx) {
- paint.setTextSize(fontSizePx);
- float size = paint.measureText(text.toString());
- float availableWidth = textView.getWidth() - textView.getPaddingLeft()
- - textView.getPaddingRight();
- return size < availableWidth;
- }
-
- private static boolean isOneWord(CharSequence text) {
- return text.toString().split("\\s+", 2).length == 1;
- }
-
private void showEditableText(CharSequence text, boolean hidden) {
- TextView textView = hidden ? mHiddenPreview : mTextPreview;
- showTextPreview(text, textView);
- View.OnClickListener listener = v -> editText();
- setAccessibilityActionToEdit(textView);
+ mView.showTextPreview(text, hidden);
+ mView.setEditAccessibilityAction(true);
+ mOnPreviewTapped = this::editText;
if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) {
- mEditChip.setVisibility(View.VISIBLE);
- mActionContainerBackground.setVisibility(View.VISIBLE);
- mEditChip.setContentDescription(
- mContext.getString(R.string.clipboard_edit_text_description));
- mEditChip.setOnClickListener(listener);
+ mOnEditTapped = this::editText;
+ mView.showEditChip(mContext.getString(R.string.clipboard_edit_text_description));
}
- textView.setOnClickListener(listener);
}
private boolean tryShowEditableImage(Uri uri, boolean isSensitive) {
- View.OnClickListener listener = v -> editImage(uri);
+ Runnable listener = () -> editImage(uri);
ContentResolver resolver = mContext.getContentResolver();
String mimeType = resolver.getType(uri);
boolean isEditableImage = mimeType != null && mimeType.startsWith("image");
if (isSensitive) {
- mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden));
- showSinglePreview(mHiddenPreview);
+ mView.showImagePreview(null);
if (isEditableImage) {
- mHiddenPreview.setOnClickListener(listener);
- setAccessibilityActionToEdit(mHiddenPreview);
+ mOnPreviewTapped = listener;
+ mView.setEditAccessibilityAction(true);
}
} else if (isEditableImage) { // if the MIMEtype is image, try to load
try {
@@ -609,44 +400,36 @@
// The width of the view is capped, height maintains aspect ratio, so allow it to be
// taller if needed.
Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null);
- showSinglePreview(mImagePreview);
- mImagePreview.setImageBitmap(thumbnail);
- mImagePreview.setOnClickListener(listener);
- setAccessibilityActionToEdit(mImagePreview);
+ mView.showImagePreview(thumbnail);
+ mView.setEditAccessibilityAction(true);
+ mOnPreviewTapped = listener;
} catch (IOException e) {
Log.e(TAG, "Thumbnail loading failed", e);
- showTextPreview(
- mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
- mTextPreview);
+ mView.showDefaultTextPreview();
isEditableImage = false;
}
} else {
- showTextPreview(
- mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
- mTextPreview);
+ mView.showDefaultTextPreview();
}
if (isEditableImage && DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) {
- mEditChip.setVisibility(View.VISIBLE);
- mActionContainerBackground.setVisibility(View.VISIBLE);
- mEditChip.setOnClickListener(listener);
- mEditChip.setContentDescription(
- mContext.getString(R.string.clipboard_edit_image_description));
+ mView.showEditChip(mContext.getString(R.string.clipboard_edit_image_description));
}
return isEditableImage;
}
- private void setAccessibilityActionToEdit(View view) {
- ViewCompat.replaceAccessibilityAction(view,
- AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
- mContext.getString(R.string.clipboard_edit), null);
- }
-
private void animateIn() {
- if (mAccessibilityManager.isEnabled()) {
- mDismissButton.setVisibility(View.VISIBLE);
+ if (mEnterAnimator != null && mEnterAnimator.isRunning()) {
+ return;
}
- mEnterAnimator = getEnterAnimation();
+ mEnterAnimator = mView.getEnterAnimation();
+ mEnterAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ mTimeoutHandler.resetTimeout();
+ }
+ });
mEnterAnimator.start();
}
@@ -654,7 +437,7 @@
if (mExitAnimator != null && mExitAnimator.isRunning()) {
return;
}
- Animator anim = getExitAnimation();
+ Animator anim = mView.getExitAnimation();
anim.addListener(new AnimatorListenerAdapter() {
private boolean mCancelled;
@@ -676,122 +459,11 @@
anim.start();
}
- private Animator getEnterAnimation() {
- TimeInterpolator linearInterpolator = new LinearInterpolator();
- TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f);
- AnimatorSet enterAnim = new AnimatorSet();
-
- ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
- rootAnim.setInterpolator(linearInterpolator);
- rootAnim.setDuration(66);
- rootAnim.addUpdateListener(animation -> {
- mView.setAlpha(animation.getAnimatedFraction());
- });
-
- ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
- scaleAnim.setInterpolator(scaleInterpolator);
- scaleAnim.setDuration(333);
- scaleAnim.addUpdateListener(animation -> {
- float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
- mClipboardPreview.setScaleX(previewScale);
- mClipboardPreview.setScaleY(previewScale);
- mPreviewBorder.setScaleX(previewScale);
- mPreviewBorder.setScaleY(previewScale);
-
- float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
- mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
- mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
- float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction());
- float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
- mActionContainer.setScaleX(actionsScaleX);
- mActionContainer.setScaleY(actionsScaleY);
- mActionContainerBackground.setScaleX(actionsScaleX);
- mActionContainerBackground.setScaleY(actionsScaleY);
- });
-
- ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
- alphaAnim.setInterpolator(linearInterpolator);
- alphaAnim.setDuration(283);
- alphaAnim.addUpdateListener(animation -> {
- float alpha = animation.getAnimatedFraction();
- mClipboardPreview.setAlpha(alpha);
- mPreviewBorder.setAlpha(alpha);
- mDismissButton.setAlpha(alpha);
- mActionContainer.setAlpha(alpha);
- });
-
- mActionContainer.setAlpha(0);
- mPreviewBorder.setAlpha(0);
- mClipboardPreview.setAlpha(0);
- enterAnim.play(rootAnim).with(scaleAnim);
- enterAnim.play(alphaAnim).after(50).after(rootAnim);
-
- enterAnim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- super.onAnimationEnd(animation);
- mView.setAlpha(1);
- mTimeoutHandler.resetTimeout();
- }
- });
- return enterAnim;
- }
-
- private Animator getExitAnimation() {
- TimeInterpolator linearInterpolator = new LinearInterpolator();
- TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f);
- AnimatorSet exitAnim = new AnimatorSet();
-
- ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
- rootAnim.setInterpolator(linearInterpolator);
- rootAnim.setDuration(100);
- rootAnim.addUpdateListener(anim -> mView.setAlpha(1 - anim.getAnimatedFraction()));
-
- ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
- scaleAnim.setInterpolator(scaleInterpolator);
- scaleAnim.setDuration(250);
- scaleAnim.addUpdateListener(animation -> {
- float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
- mClipboardPreview.setScaleX(previewScale);
- mClipboardPreview.setScaleY(previewScale);
- mPreviewBorder.setScaleX(previewScale);
- mPreviewBorder.setScaleY(previewScale);
-
- float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
- mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
- mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
- float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction());
- float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
- mActionContainer.setScaleX(actionScaleX);
- mActionContainer.setScaleY(actionScaleY);
- mActionContainerBackground.setScaleX(actionScaleX);
- mActionContainerBackground.setScaleY(actionScaleY);
- });
-
- ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
- alphaAnim.setInterpolator(linearInterpolator);
- alphaAnim.setDuration(166);
- alphaAnim.addUpdateListener(animation -> {
- float alpha = 1 - animation.getAnimatedFraction();
- mClipboardPreview.setAlpha(alpha);
- mPreviewBorder.setAlpha(alpha);
- mDismissButton.setAlpha(alpha);
- mActionContainer.setAlpha(alpha);
- });
-
- exitAnim.play(alphaAnim).with(scaleAnim);
- exitAnim.play(rootAnim).after(150).after(alphaAnim);
- return exitAnim;
- }
-
private void hideImmediate() {
// Note this may be called multiple times if multiple dismissal events happen at the same
// time.
mTimeoutHandler.cancelTimeout();
- final View decorView = mWindow.peekDecorView();
- if (decorView != null && decorView.isAttachedToWindow()) {
- mWindowManager.removeViewImmediate(decorView);
- }
+ mWindow.remove();
if (mCloseDialogsReceiver != null) {
mBroadcastDispatcher.unregisterReceiver(mCloseDialogsReceiver);
mCloseDialogsReceiver = null;
@@ -813,129 +485,20 @@
}
}
- private void resetActionChips() {
- for (OverlayActionChip chip : mActionChips) {
- mActionContainer.removeView(chip);
- }
- mActionChips.clear();
- }
-
private void reset() {
- mView.setTranslationX(0);
- mView.setAlpha(0);
- mActionContainerBackground.setVisibility(View.GONE);
- mShareChip.setVisibility(View.GONE);
- mEditChip.setVisibility(View.GONE);
- mRemoteCopyChip.setVisibility(View.GONE);
- resetActionChips();
+ mOnRemoteCopyTapped = null;
+ mOnShareTapped = null;
+ mOnEditTapped = null;
+ mOnPreviewTapped = null;
+ mView.reset();
mTimeoutHandler.cancelTimeout();
mClipboardLogger.reset();
}
- @MainThread
- private void attachWindow() {
- View decorView = mWindow.getDecorView();
- if (decorView.isAttachedToWindow() || mBlockAttach) {
- return;
- }
- mBlockAttach = true;
- mWindowManager.addView(decorView, mWindowLayoutParams);
- decorView.requestApplyInsets();
- mView.requestApplyInsets();
- decorView.getViewTreeObserver().addOnWindowAttachListener(
- new ViewTreeObserver.OnWindowAttachListener() {
- @Override
- public void onWindowAttached() {
- mBlockAttach = false;
- }
-
- @Override
- public void onWindowDetached() {
- }
- }
- );
- }
-
- private void withWindowAttached(Runnable action) {
- View decorView = mWindow.getDecorView();
- if (decorView.isAttachedToWindow()) {
- action.run();
- } else {
- decorView.getViewTreeObserver().addOnWindowAttachListener(
- new ViewTreeObserver.OnWindowAttachListener() {
- @Override
- public void onWindowAttached() {
- mBlockAttach = false;
- decorView.getViewTreeObserver().removeOnWindowAttachListener(this);
- action.run();
- }
-
- @Override
- public void onWindowDetached() {
- }
- });
- }
- }
-
- private void updateInsets(WindowInsets insets) {
- int orientation = mContext.getResources().getConfiguration().orientation;
- FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) mView.getLayoutParams();
- if (p == null) {
- return;
- }
- DisplayCutout cutout = insets.getDisplayCutout();
- Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars());
- Insets imeInsets = insets.getInsets(WindowInsets.Type.ime());
- if (cutout == null) {
- p.setMargins(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom));
- } else {
- Insets waterfall = cutout.getWaterfallInsets();
- if (orientation == ORIENTATION_PORTRAIT) {
- p.setMargins(
- waterfall.left,
- Math.max(cutout.getSafeInsetTop(), waterfall.top),
- waterfall.right,
- Math.max(imeInsets.bottom,
- Math.max(cutout.getSafeInsetBottom(),
- Math.max(navBarInsets.bottom, waterfall.bottom))));
- } else {
- p.setMargins(
- waterfall.left,
- waterfall.top,
- waterfall.right,
- Math.max(imeInsets.bottom,
- Math.max(navBarInsets.bottom, waterfall.bottom)));
- }
- }
- mView.setLayoutParams(p);
- mView.requestLayout();
- }
-
private Display getDefaultDisplay() {
return mDisplayManager.getDisplay(DEFAULT_DISPLAY);
}
- /**
- * Updates the window focusability. If the window is already showing, then it updates the
- * window immediately, otherwise the layout params will be applied when the window is next
- * shown.
- */
- private void setWindowFocusable(boolean focusable) {
- int flags = mWindowLayoutParams.flags;
- if (focusable) {
- mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
- } else {
- mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
- }
- if (mWindowLayoutParams.flags == flags) {
- return;
- }
- final View decorView = mWindow.peekDecorView();
- if (decorView != null && decorView.isAttachedToWindow()) {
- mWindowManager.updateViewLayout(decorView, mWindowLayoutParams);
- }
- }
-
static class ClipboardLogger {
private final UiEventLogger mUiEventLogger;
private boolean mGuarded = false;
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java
new file mode 100644
index 0000000..3a040829
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java
@@ -0,0 +1,963 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.clipboardoverlay;
+
+import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.view.Display.DEFAULT_DISPLAY;
+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;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_TAPPED;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISSED_OTHER;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_EDIT_TAPPED;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_REMOTE_COPY_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.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT;
+
+import static java.util.Objects.requireNonNull;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.annotation.MainThread;
+import android.app.ICompatCameraControlCallback;
+import android.app.RemoteAction;
+import android.content.BroadcastReceiver;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Insets;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.drawable.Icon;
+import android.hardware.display.DisplayManager;
+import android.hardware.input.InputManager;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Looper;
+import android.provider.DeviceConfig;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.MathUtils;
+import android.util.Size;
+import android.util.TypedValue;
+import android.view.Display;
+import android.view.DisplayCutout;
+import android.view.Gravity;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
+import android.view.InputMonitor;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.PathInterpolator;
+import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextClassificationManager;
+import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+
+import com.android.internal.logging.UiEventLogger;
+import com.android.internal.policy.PhoneWindow;
+import com.android.systemui.R;
+import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.screenshot.DraggableConstraintLayout;
+import com.android.systemui.screenshot.FloatingWindowUtil;
+import com.android.systemui.screenshot.OverlayActionChip;
+import com.android.systemui.screenshot.TimeoutHandler;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * Controls state and UI for the overlay that appears when something is added to the clipboard
+ */
+public class ClipboardOverlayControllerLegacy implements ClipboardListener.ClipboardOverlay {
+ private static final String TAG = "ClipboardOverlayCtrlr";
+ private static final String REMOTE_COPY_ACTION = "android.intent.action.REMOTE_COPY";
+
+ /** Constants for screenshot/copy deconflicting */
+ public static final String SCREENSHOT_ACTION = "com.android.systemui.SCREENSHOT";
+ public static final String SELF_PERMISSION = "com.android.systemui.permission.SELF";
+ public static final String COPY_OVERLAY_ACTION = "com.android.systemui.COPY";
+
+ private static final String EXTRA_EDIT_SOURCE_CLIPBOARD = "edit_source_clipboard";
+
+ private static final int CLIPBOARD_DEFAULT_TIMEOUT_MILLIS = 6000;
+ private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
+ private static final int FONT_SEARCH_STEP_PX = 4;
+
+ private final Context mContext;
+ private final ClipboardLogger mClipboardLogger;
+ private final BroadcastDispatcher mBroadcastDispatcher;
+ private final DisplayManager mDisplayManager;
+ private final DisplayMetrics mDisplayMetrics;
+ private final WindowManager mWindowManager;
+ private final WindowManager.LayoutParams mWindowLayoutParams;
+ private final PhoneWindow mWindow;
+ private final TimeoutHandler mTimeoutHandler;
+ private final AccessibilityManager mAccessibilityManager;
+ private final TextClassifier mTextClassifier;
+
+ private final DraggableConstraintLayout mView;
+ private final View mClipboardPreview;
+ private final ImageView mImagePreview;
+ private final TextView mTextPreview;
+ private final TextView mHiddenPreview;
+ private final View mPreviewBorder;
+ private final OverlayActionChip mEditChip;
+ private final OverlayActionChip mShareChip;
+ private final OverlayActionChip mRemoteCopyChip;
+ private final View mActionContainerBackground;
+ private final View mDismissButton;
+ private final LinearLayout mActionContainer;
+ private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>();
+
+ private Runnable mOnSessionCompleteListener;
+
+ private InputMonitor mInputMonitor;
+ private InputEventReceiver mInputEventReceiver;
+
+ private BroadcastReceiver mCloseDialogsReceiver;
+ private BroadcastReceiver mScreenshotReceiver;
+
+ private boolean mBlockAttach = false;
+ private Animator mExitAnimator;
+ private Animator mEnterAnimator;
+ private final int mOrientation;
+ private boolean mKeyboardVisible;
+
+
+ public ClipboardOverlayControllerLegacy(Context context,
+ BroadcastDispatcher broadcastDispatcher,
+ BroadcastSender broadcastSender,
+ TimeoutHandler timeoutHandler, UiEventLogger uiEventLogger) {
+ mBroadcastDispatcher = broadcastDispatcher;
+ mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
+ final Context displayContext = context.createDisplayContext(getDefaultDisplay());
+ mContext = displayContext.createWindowContext(TYPE_SCREENSHOT, null);
+
+ mClipboardLogger = new ClipboardLogger(uiEventLogger);
+
+ mAccessibilityManager = AccessibilityManager.getInstance(mContext);
+ mTextClassifier = requireNonNull(context.getSystemService(TextClassificationManager.class))
+ .getTextClassifier();
+
+ mWindowManager = mContext.getSystemService(WindowManager.class);
+
+ mDisplayMetrics = new DisplayMetrics();
+ mContext.getDisplay().getRealMetrics(mDisplayMetrics);
+
+ mTimeoutHandler = timeoutHandler;
+ mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS);
+
+ // Setup the window that we are going to use
+ mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams();
+ mWindowLayoutParams.setTitle("ClipboardOverlay");
+
+ mWindow = FloatingWindowUtil.getFloatingWindow(mContext);
+ mWindow.setWindowManager(mWindowManager, null, null);
+
+ setWindowFocusable(false);
+
+ mView = (DraggableConstraintLayout)
+ LayoutInflater.from(mContext).inflate(R.layout.clipboard_overlay_legacy, null);
+ mActionContainerBackground =
+ requireNonNull(mView.findViewById(R.id.actions_container_background));
+ mActionContainer = requireNonNull(mView.findViewById(R.id.actions));
+ mClipboardPreview = requireNonNull(mView.findViewById(R.id.clipboard_preview));
+ mImagePreview = requireNonNull(mView.findViewById(R.id.image_preview));
+ mTextPreview = requireNonNull(mView.findViewById(R.id.text_preview));
+ mHiddenPreview = requireNonNull(mView.findViewById(R.id.hidden_preview));
+ mPreviewBorder = requireNonNull(mView.findViewById(R.id.preview_border));
+ mEditChip = requireNonNull(mView.findViewById(R.id.edit_chip));
+ mShareChip = requireNonNull(mView.findViewById(R.id.share_chip));
+ mRemoteCopyChip = requireNonNull(mView.findViewById(R.id.remote_copy_chip));
+ mEditChip.setAlpha(1);
+ mShareChip.setAlpha(1);
+ mRemoteCopyChip.setAlpha(1);
+ mDismissButton = requireNonNull(mView.findViewById(R.id.dismiss_button));
+
+ mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share));
+ mView.setCallbacks(new DraggableConstraintLayout.SwipeDismissCallbacks() {
+ @Override
+ public void onInteraction() {
+ mTimeoutHandler.resetTimeout();
+ }
+
+ @Override
+ public void onSwipeDismissInitiated(Animator animator) {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED);
+ mExitAnimator = animator;
+ }
+
+ @Override
+ public void onDismissComplete() {
+ hideImmediate();
+ }
+ });
+
+ mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> {
+ int availableHeight = mTextPreview.getHeight()
+ - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom());
+ mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight());
+ return true;
+ });
+
+ mDismissButton.setOnClickListener(view -> {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
+ animateOut();
+ });
+
+ mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true);
+ mRemoteCopyChip.setIcon(
+ Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true);
+ mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true);
+ mOrientation = mContext.getResources().getConfiguration().orientation;
+
+ attachWindow();
+ withWindowAttached(() -> {
+ mWindow.setContentView(mView);
+ WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
+ mKeyboardVisible = insets.isVisible(WindowInsets.Type.ime());
+ updateInsets(insets);
+ mWindow.peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ WindowInsets insets =
+ mWindowManager.getCurrentWindowMetrics().getWindowInsets();
+ boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime());
+ if (keyboardVisible != mKeyboardVisible) {
+ mKeyboardVisible = keyboardVisible;
+ updateInsets(insets);
+ }
+ }
+ });
+ mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback(
+ new ViewRootImpl.ActivityConfigCallback() {
+ @Override
+ public void onConfigurationChanged(Configuration overrideConfig,
+ int newDisplayId) {
+ if (mContext.getResources().getConfiguration().orientation
+ != mOrientation) {
+ mClipboardLogger.logSessionComplete(
+ CLIPBOARD_OVERLAY_DISMISSED_OTHER);
+ hideImmediate();
+ }
+ }
+
+ @Override
+ public void requestCompatCameraControl(
+ boolean showControl, boolean transformationApplied,
+ ICompatCameraControlCallback callback) {
+ Log.w(TAG, "unexpected requestCompatCameraControl call");
+ }
+ });
+ });
+
+ mTimeoutHandler.setOnTimeoutRunnable(() -> {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TIMED_OUT);
+ animateOut();
+ });
+
+ mCloseDialogsReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
+ animateOut();
+ }
+ }
+ };
+
+ mBroadcastDispatcher.registerReceiver(mCloseDialogsReceiver,
+ new IntentFilter(ACTION_CLOSE_SYSTEM_DIALOGS));
+ mScreenshotReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (SCREENSHOT_ACTION.equals(intent.getAction())) {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
+ animateOut();
+ }
+ }
+ };
+
+ mBroadcastDispatcher.registerReceiver(mScreenshotReceiver,
+ new IntentFilter(SCREENSHOT_ACTION), null, null, Context.RECEIVER_EXPORTED,
+ SELF_PERMISSION);
+ monitorOutsideTouches();
+
+ Intent copyIntent = new Intent(COPY_OVERLAY_ACTION);
+ // Set package name so the system knows it's safe
+ copyIntent.setPackage(mContext.getPackageName());
+ broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION);
+ }
+
+ @Override // ClipboardListener.ClipboardOverlay
+ public void setClipData(ClipData clipData, String clipSource) {
+ if (mExitAnimator != null && mExitAnimator.isRunning()) {
+ mExitAnimator.cancel();
+ }
+ reset();
+ String accessibilityAnnouncement;
+
+ boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null
+ && clipData.getDescription().getExtras()
+ .getBoolean(ClipDescription.EXTRA_IS_SENSITIVE);
+ if (clipData == null || clipData.getItemCount() == 0) {
+ showTextPreview(
+ mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
+ mTextPreview);
+ accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
+ } else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) {
+ ClipData.Item item = clipData.getItemAt(0);
+ if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
+ CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) {
+ if (item.getTextLinks() != null) {
+ AsyncTask.execute(() -> classifyText(clipData.getItemAt(0), clipSource));
+ }
+ }
+ if (isSensitive) {
+ showEditableText(
+ mContext.getResources().getString(R.string.clipboard_asterisks), true);
+ } else {
+ showEditableText(item.getText(), false);
+ }
+ showShareChip(clipData);
+ accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied);
+ } else if (clipData.getItemAt(0).getUri() != null) {
+ if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) {
+ showShareChip(clipData);
+ accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied);
+ } else {
+ accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
+ }
+ } else {
+ showTextPreview(
+ mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
+ mTextPreview);
+ accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
+ }
+ Intent remoteCopyIntent = IntentCreator.getRemoteCopyIntent(clipData, mContext);
+ // Only show remote copy if it's available.
+ PackageManager packageManager = mContext.getPackageManager();
+ if (packageManager.resolveActivity(
+ remoteCopyIntent, PackageManager.ResolveInfoFlags.of(0)) != null) {
+ mRemoteCopyChip.setContentDescription(
+ mContext.getString(R.string.clipboard_send_nearby_description));
+ mRemoteCopyChip.setVisibility(View.VISIBLE);
+ mRemoteCopyChip.setOnClickListener((v) -> {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED);
+ mContext.startActivity(remoteCopyIntent);
+ animateOut();
+ });
+ mActionContainerBackground.setVisibility(View.VISIBLE);
+ } else {
+ mRemoteCopyChip.setVisibility(View.GONE);
+ }
+ withWindowAttached(() -> {
+ if (mEnterAnimator == null || !mEnterAnimator.isRunning()) {
+ mView.post(this::animateIn);
+ }
+ mView.announceForAccessibility(accessibilityAnnouncement);
+ });
+ mTimeoutHandler.resetTimeout();
+ }
+
+ @Override // ClipboardListener.ClipboardOverlay
+ public void setOnSessionCompleteListener(Runnable runnable) {
+ mOnSessionCompleteListener = runnable;
+ }
+
+ private void classifyText(ClipData.Item item, String source) {
+ ArrayList<RemoteAction> actions = new ArrayList<>();
+ for (TextLinks.TextLink link : item.getTextLinks().getLinks()) {
+ TextClassification classification = mTextClassifier.classifyText(
+ item.getText(), link.getStart(), link.getEnd(), null);
+ actions.addAll(classification.getActions());
+ }
+ mView.post(() -> {
+ resetActionChips();
+ if (actions.size() > 0) {
+ mActionContainerBackground.setVisibility(View.VISIBLE);
+ for (RemoteAction action : actions) {
+ Intent targetIntent = action.getActionIntent().getIntent();
+ ComponentName component = targetIntent.getComponent();
+ if (component != null && !TextUtils.equals(source,
+ component.getPackageName())) {
+ OverlayActionChip chip = constructActionChip(action);
+ mActionContainer.addView(chip);
+ mActionChips.add(chip);
+ break; // only show at most one action chip
+ }
+ }
+ }
+ });
+ }
+
+ private void showShareChip(ClipData clip) {
+ mShareChip.setVisibility(View.VISIBLE);
+ mActionContainerBackground.setVisibility(View.VISIBLE);
+ mShareChip.setOnClickListener((v) -> shareContent(clip));
+ }
+
+ private OverlayActionChip constructActionChip(RemoteAction action) {
+ OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate(
+ R.layout.overlay_action_chip, mActionContainer, false);
+ chip.setText(action.getTitle());
+ chip.setContentDescription(action.getTitle());
+ chip.setIcon(action.getIcon(), false);
+ chip.setPendingIntent(action.getActionIntent(), () -> {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
+ animateOut();
+ });
+ chip.setAlpha(1);
+ return chip;
+ }
+
+ private void monitorOutsideTouches() {
+ InputManager inputManager = mContext.getSystemService(InputManager.class);
+ mInputMonitor = inputManager.monitorGestureInput("clipboard overlay", 0);
+ mInputEventReceiver = new InputEventReceiver(mInputMonitor.getInputChannel(),
+ Looper.getMainLooper()) {
+ @Override
+ public void onInputEvent(InputEvent event) {
+ if (event instanceof MotionEvent) {
+ MotionEvent motionEvent = (MotionEvent) event;
+ if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ Region touchRegion = new Region();
+
+ final Rect tmpRect = new Rect();
+ mPreviewBorder.getBoundsOnScreen(tmpRect);
+ tmpRect.inset(
+ (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
+ (int) FloatingWindowUtil.dpToPx(mDisplayMetrics,
+ -SWIPE_PADDING_DP));
+ touchRegion.op(tmpRect, Region.Op.UNION);
+ mActionContainerBackground.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);
+ if (!touchRegion.contains(
+ (int) motionEvent.getRawX(), (int) motionEvent.getRawY())) {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE);
+ animateOut();
+ }
+ }
+ }
+ finishInputEvent(event, true /* handled */);
+ }
+ };
+ }
+
+ private void editImage(Uri uri) {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED);
+ mContext.startActivity(IntentCreator.getImageEditIntent(uri, mContext));
+ animateOut();
+ }
+
+ private void editText() {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED);
+ mContext.startActivity(IntentCreator.getTextEditorIntent(mContext));
+ animateOut();
+ }
+
+ private void shareContent(ClipData clip) {
+ mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SHARE_TAPPED);
+ mContext.startActivity(IntentCreator.getShareIntent(clip, mContext));
+ animateOut();
+ }
+
+ private void showSinglePreview(View v) {
+ mTextPreview.setVisibility(View.GONE);
+ mImagePreview.setVisibility(View.GONE);
+ mHiddenPreview.setVisibility(View.GONE);
+ v.setVisibility(View.VISIBLE);
+ }
+
+ private void showTextPreview(CharSequence text, TextView textView) {
+ showSinglePreview(textView);
+ final CharSequence truncatedText = text.subSequence(0, Math.min(500, text.length()));
+ textView.setText(truncatedText);
+ updateTextSize(truncatedText, textView);
+
+ textView.addOnLayoutChangeListener(
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+ if (right - left != oldRight - oldLeft) {
+ updateTextSize(truncatedText, textView);
+ }
+ });
+ mEditChip.setVisibility(View.GONE);
+ }
+
+ private void updateTextSize(CharSequence text, TextView textView) {
+ Paint paint = new Paint(textView.getPaint());
+ Resources res = textView.getResources();
+ float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font);
+ float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font);
+ if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) {
+ // If the text is a single word and would fit within the TextView at the min font size,
+ // find the biggest font size that will fit.
+ float fontSizePx = minFontSize;
+ while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize
+ && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) {
+ fontSizePx += FONT_SEARCH_STEP_PX;
+ }
+ // Need to turn off autosizing, otherwise setTextSize is a no-op.
+ textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
+ // It's possible to hit the max font size and not fill the width, so centering
+ // horizontally looks better in this case.
+ textView.setGravity(Gravity.CENTER);
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx);
+ } else {
+ // Otherwise just stick with autosize.
+ textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize,
+ (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX);
+ textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
+ }
+ }
+
+ private static boolean fitsInView(CharSequence text, TextView textView, Paint paint,
+ float fontSizePx) {
+ paint.setTextSize(fontSizePx);
+ float size = paint.measureText(text.toString());
+ float availableWidth = textView.getWidth() - textView.getPaddingLeft()
+ - textView.getPaddingRight();
+ return size < availableWidth;
+ }
+
+ private static boolean isOneWord(CharSequence text) {
+ return text.toString().split("\\s+", 2).length == 1;
+ }
+
+ private void showEditableText(CharSequence text, boolean hidden) {
+ TextView textView = hidden ? mHiddenPreview : mTextPreview;
+ showTextPreview(text, textView);
+ View.OnClickListener listener = v -> editText();
+ setAccessibilityActionToEdit(textView);
+ if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
+ CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) {
+ mEditChip.setVisibility(View.VISIBLE);
+ mActionContainerBackground.setVisibility(View.VISIBLE);
+ mEditChip.setContentDescription(
+ mContext.getString(R.string.clipboard_edit_text_description));
+ mEditChip.setOnClickListener(listener);
+ }
+ textView.setOnClickListener(listener);
+ }
+
+ private boolean tryShowEditableImage(Uri uri, boolean isSensitive) {
+ View.OnClickListener listener = v -> editImage(uri);
+ ContentResolver resolver = mContext.getContentResolver();
+ String mimeType = resolver.getType(uri);
+ boolean isEditableImage = mimeType != null && mimeType.startsWith("image");
+ if (isSensitive) {
+ mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden));
+ showSinglePreview(mHiddenPreview);
+ if (isEditableImage) {
+ mHiddenPreview.setOnClickListener(listener);
+ setAccessibilityActionToEdit(mHiddenPreview);
+ }
+ } else if (isEditableImage) { // if the MIMEtype is image, try to load
+ try {
+ int size = mContext.getResources().getDimensionPixelSize(R.dimen.overlay_x_scale);
+ // The width of the view is capped, height maintains aspect ratio, so allow it to be
+ // taller if needed.
+ Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null);
+ showSinglePreview(mImagePreview);
+ mImagePreview.setImageBitmap(thumbnail);
+ mImagePreview.setOnClickListener(listener);
+ setAccessibilityActionToEdit(mImagePreview);
+ } catch (IOException e) {
+ Log.e(TAG, "Thumbnail loading failed", e);
+ showTextPreview(
+ mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
+ mTextPreview);
+ isEditableImage = false;
+ }
+ } else {
+ showTextPreview(
+ mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
+ mTextPreview);
+ }
+ if (isEditableImage && DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) {
+ mEditChip.setVisibility(View.VISIBLE);
+ mActionContainerBackground.setVisibility(View.VISIBLE);
+ mEditChip.setOnClickListener(listener);
+ mEditChip.setContentDescription(
+ mContext.getString(R.string.clipboard_edit_image_description));
+ }
+ return isEditableImage;
+ }
+
+ private void setAccessibilityActionToEdit(View view) {
+ ViewCompat.replaceAccessibilityAction(view,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ mContext.getString(R.string.clipboard_edit), null);
+ }
+
+ private void animateIn() {
+ if (mAccessibilityManager.isEnabled()) {
+ mDismissButton.setVisibility(View.VISIBLE);
+ }
+ mEnterAnimator = getEnterAnimation();
+ mEnterAnimator.start();
+ }
+
+ private void animateOut() {
+ if (mExitAnimator != null && mExitAnimator.isRunning()) {
+ return;
+ }
+ Animator anim = getExitAnimation();
+ anim.addListener(new AnimatorListenerAdapter() {
+ private boolean mCancelled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ super.onAnimationCancel(animation);
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ if (!mCancelled) {
+ hideImmediate();
+ }
+ }
+ });
+ mExitAnimator = anim;
+ anim.start();
+ }
+
+ private Animator getEnterAnimation() {
+ TimeInterpolator linearInterpolator = new LinearInterpolator();
+ TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f);
+ AnimatorSet enterAnim = new AnimatorSet();
+
+ ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
+ rootAnim.setInterpolator(linearInterpolator);
+ rootAnim.setDuration(66);
+ rootAnim.addUpdateListener(animation -> {
+ mView.setAlpha(animation.getAnimatedFraction());
+ });
+
+ ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
+ scaleAnim.setInterpolator(scaleInterpolator);
+ scaleAnim.setDuration(333);
+ scaleAnim.addUpdateListener(animation -> {
+ float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
+ mClipboardPreview.setScaleX(previewScale);
+ mClipboardPreview.setScaleY(previewScale);
+ mPreviewBorder.setScaleX(previewScale);
+ mPreviewBorder.setScaleY(previewScale);
+
+ float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
+ mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
+ mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
+ float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction());
+ float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
+ mActionContainer.setScaleX(actionsScaleX);
+ mActionContainer.setScaleY(actionsScaleY);
+ mActionContainerBackground.setScaleX(actionsScaleX);
+ mActionContainerBackground.setScaleY(actionsScaleY);
+ });
+
+ ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
+ alphaAnim.setInterpolator(linearInterpolator);
+ alphaAnim.setDuration(283);
+ alphaAnim.addUpdateListener(animation -> {
+ float alpha = animation.getAnimatedFraction();
+ mClipboardPreview.setAlpha(alpha);
+ mPreviewBorder.setAlpha(alpha);
+ mDismissButton.setAlpha(alpha);
+ mActionContainer.setAlpha(alpha);
+ });
+
+ mActionContainer.setAlpha(0);
+ mPreviewBorder.setAlpha(0);
+ mClipboardPreview.setAlpha(0);
+ enterAnim.play(rootAnim).with(scaleAnim);
+ enterAnim.play(alphaAnim).after(50).after(rootAnim);
+
+ enterAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ mView.setAlpha(1);
+ mTimeoutHandler.resetTimeout();
+ }
+ });
+ return enterAnim;
+ }
+
+ private Animator getExitAnimation() {
+ TimeInterpolator linearInterpolator = new LinearInterpolator();
+ TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f);
+ AnimatorSet exitAnim = new AnimatorSet();
+
+ ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
+ rootAnim.setInterpolator(linearInterpolator);
+ rootAnim.setDuration(100);
+ rootAnim.addUpdateListener(anim -> mView.setAlpha(1 - anim.getAnimatedFraction()));
+
+ ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
+ scaleAnim.setInterpolator(scaleInterpolator);
+ scaleAnim.setDuration(250);
+ scaleAnim.addUpdateListener(animation -> {
+ float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
+ mClipboardPreview.setScaleX(previewScale);
+ mClipboardPreview.setScaleY(previewScale);
+ mPreviewBorder.setScaleX(previewScale);
+ mPreviewBorder.setScaleY(previewScale);
+
+ float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
+ mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
+ mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
+ float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction());
+ float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
+ mActionContainer.setScaleX(actionScaleX);
+ mActionContainer.setScaleY(actionScaleY);
+ mActionContainerBackground.setScaleX(actionScaleX);
+ mActionContainerBackground.setScaleY(actionScaleY);
+ });
+
+ ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
+ alphaAnim.setInterpolator(linearInterpolator);
+ alphaAnim.setDuration(166);
+ alphaAnim.addUpdateListener(animation -> {
+ float alpha = 1 - animation.getAnimatedFraction();
+ mClipboardPreview.setAlpha(alpha);
+ mPreviewBorder.setAlpha(alpha);
+ mDismissButton.setAlpha(alpha);
+ mActionContainer.setAlpha(alpha);
+ });
+
+ exitAnim.play(alphaAnim).with(scaleAnim);
+ exitAnim.play(rootAnim).after(150).after(alphaAnim);
+ return exitAnim;
+ }
+
+ private void hideImmediate() {
+ // Note this may be called multiple times if multiple dismissal events happen at the same
+ // time.
+ mTimeoutHandler.cancelTimeout();
+ final View decorView = mWindow.peekDecorView();
+ if (decorView != null && decorView.isAttachedToWindow()) {
+ mWindowManager.removeViewImmediate(decorView);
+ }
+ if (mCloseDialogsReceiver != null) {
+ mBroadcastDispatcher.unregisterReceiver(mCloseDialogsReceiver);
+ mCloseDialogsReceiver = null;
+ }
+ if (mScreenshotReceiver != null) {
+ mBroadcastDispatcher.unregisterReceiver(mScreenshotReceiver);
+ mScreenshotReceiver = null;
+ }
+ if (mInputEventReceiver != null) {
+ mInputEventReceiver.dispose();
+ mInputEventReceiver = null;
+ }
+ if (mInputMonitor != null) {
+ mInputMonitor.dispose();
+ mInputMonitor = null;
+ }
+ if (mOnSessionCompleteListener != null) {
+ mOnSessionCompleteListener.run();
+ }
+ }
+
+ private void resetActionChips() {
+ for (OverlayActionChip chip : mActionChips) {
+ mActionContainer.removeView(chip);
+ }
+ mActionChips.clear();
+ }
+
+ private void reset() {
+ mView.setTranslationX(0);
+ mView.setAlpha(0);
+ mActionContainerBackground.setVisibility(View.GONE);
+ mShareChip.setVisibility(View.GONE);
+ mEditChip.setVisibility(View.GONE);
+ mRemoteCopyChip.setVisibility(View.GONE);
+ resetActionChips();
+ mTimeoutHandler.cancelTimeout();
+ mClipboardLogger.reset();
+ }
+
+ @MainThread
+ private void attachWindow() {
+ View decorView = mWindow.getDecorView();
+ if (decorView.isAttachedToWindow() || mBlockAttach) {
+ return;
+ }
+ mBlockAttach = true;
+ mWindowManager.addView(decorView, mWindowLayoutParams);
+ decorView.requestApplyInsets();
+ mView.requestApplyInsets();
+ decorView.getViewTreeObserver().addOnWindowAttachListener(
+ new ViewTreeObserver.OnWindowAttachListener() {
+ @Override
+ public void onWindowAttached() {
+ mBlockAttach = false;
+ }
+
+ @Override
+ public void onWindowDetached() {
+ }
+ }
+ );
+ }
+
+ private void withWindowAttached(Runnable action) {
+ View decorView = mWindow.getDecorView();
+ if (decorView.isAttachedToWindow()) {
+ action.run();
+ } else {
+ decorView.getViewTreeObserver().addOnWindowAttachListener(
+ new ViewTreeObserver.OnWindowAttachListener() {
+ @Override
+ public void onWindowAttached() {
+ mBlockAttach = false;
+ decorView.getViewTreeObserver().removeOnWindowAttachListener(this);
+ action.run();
+ }
+
+ @Override
+ public void onWindowDetached() {
+ }
+ });
+ }
+ }
+
+ private void updateInsets(WindowInsets insets) {
+ int orientation = mContext.getResources().getConfiguration().orientation;
+ FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) mView.getLayoutParams();
+ if (p == null) {
+ return;
+ }
+ DisplayCutout cutout = insets.getDisplayCutout();
+ Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars());
+ Insets imeInsets = insets.getInsets(WindowInsets.Type.ime());
+ if (cutout == null) {
+ p.setMargins(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom));
+ } else {
+ Insets waterfall = cutout.getWaterfallInsets();
+ if (orientation == ORIENTATION_PORTRAIT) {
+ p.setMargins(
+ waterfall.left,
+ Math.max(cutout.getSafeInsetTop(), waterfall.top),
+ waterfall.right,
+ Math.max(imeInsets.bottom,
+ Math.max(cutout.getSafeInsetBottom(),
+ Math.max(navBarInsets.bottom, waterfall.bottom))));
+ } else {
+ p.setMargins(
+ waterfall.left,
+ waterfall.top,
+ waterfall.right,
+ Math.max(imeInsets.bottom,
+ Math.max(navBarInsets.bottom, waterfall.bottom)));
+ }
+ }
+ mView.setLayoutParams(p);
+ mView.requestLayout();
+ }
+
+ private Display getDefaultDisplay() {
+ return mDisplayManager.getDisplay(DEFAULT_DISPLAY);
+ }
+
+ /**
+ * Updates the window focusability. If the window is already showing, then it updates the
+ * window immediately, otherwise the layout params will be applied when the window is next
+ * shown.
+ */
+ private void setWindowFocusable(boolean focusable) {
+ int flags = mWindowLayoutParams.flags;
+ if (focusable) {
+ mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ } else {
+ mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ }
+ if (mWindowLayoutParams.flags == flags) {
+ return;
+ }
+ final View decorView = mWindow.peekDecorView();
+ if (decorView != null && decorView.isAttachedToWindow()) {
+ mWindowManager.updateViewLayout(decorView, mWindowLayoutParams);
+ }
+ }
+
+ static class ClipboardLogger {
+ private final UiEventLogger mUiEventLogger;
+ private boolean mGuarded = false;
+
+ ClipboardLogger(UiEventLogger uiEventLogger) {
+ mUiEventLogger = uiEventLogger;
+ }
+
+ void logSessionComplete(@NonNull UiEventLogger.UiEventEnum event) {
+ if (!mGuarded) {
+ mGuarded = true;
+ mUiEventLogger.log(event);
+ }
+ }
+
+ void reset() {
+ mGuarded = false;
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerFactory.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacyFactory.java
similarity index 76%
rename from packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerFactory.java
rename to packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacyFactory.java
index 8b0b2a5..0d989a7 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacyFactory.java
@@ -27,17 +27,17 @@
import javax.inject.Inject;
/**
- * A factory that churns out ClipboardOverlayControllers on demand.
+ * A factory that churns out ClipboardOverlayControllerLegacys on demand.
*/
@SysUISingleton
-public class ClipboardOverlayControllerFactory {
+public class ClipboardOverlayControllerLegacyFactory {
private final UiEventLogger mUiEventLogger;
private final BroadcastDispatcher mBroadcastDispatcher;
private final BroadcastSender mBroadcastSender;
@Inject
- public ClipboardOverlayControllerFactory(BroadcastDispatcher broadcastDispatcher,
+ public ClipboardOverlayControllerLegacyFactory(BroadcastDispatcher broadcastDispatcher,
BroadcastSender broadcastSender, UiEventLogger uiEventLogger) {
this.mBroadcastDispatcher = broadcastDispatcher;
this.mBroadcastSender = broadcastSender;
@@ -45,10 +45,10 @@
}
/**
- * One new ClipboardOverlayController, coming right up!
+ * One new ClipboardOverlayControllerLegacy, coming right up!
*/
- public ClipboardOverlayController create(Context context) {
- return new ClipboardOverlayController(context, mBroadcastDispatcher, mBroadcastSender,
+ public ClipboardOverlayControllerLegacy create(Context context) {
+ return new ClipboardOverlayControllerLegacy(context, mBroadcastDispatcher, mBroadcastSender,
new TimeoutHandler(context), mUiEventLogger);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
new file mode 100644
index 0000000..2d33157
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
@@ -0,0 +1,482 @@
+/*
+ * 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 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;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.annotation.Nullable;
+import android.app.RemoteAction;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Insets;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.drawable.Icon;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.MathUtils;
+import android.util.TypedValue;
+import android.view.DisplayCutout;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowInsets;
+import android.view.accessibility.AccessibilityManager;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.PathInterpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.core.view.ViewCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+
+import com.android.systemui.R;
+import com.android.systemui.screenshot.DraggableConstraintLayout;
+import com.android.systemui.screenshot.FloatingWindowUtil;
+import com.android.systemui.screenshot.OverlayActionChip;
+
+import java.util.ArrayList;
+
+/**
+ * Handles the visual elements and animations for the clipboard overlay.
+ */
+public class ClipboardOverlayView extends DraggableConstraintLayout {
+
+ interface ClipboardOverlayCallbacks extends SwipeDismissCallbacks {
+ void onDismissButtonTapped();
+
+ void onRemoteCopyButtonTapped();
+
+ void onEditButtonTapped();
+
+ void onShareButtonTapped();
+
+ void onPreviewTapped();
+ }
+
+ private static final String TAG = "ClipboardView";
+
+ private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
+ private static final int FONT_SEARCH_STEP_PX = 4;
+
+ private final DisplayMetrics mDisplayMetrics;
+ private final AccessibilityManager mAccessibilityManager;
+ private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>();
+
+ private View mClipboardPreview;
+ private ImageView mImagePreview;
+ private TextView mTextPreview;
+ private TextView mHiddenPreview;
+ private View mPreviewBorder;
+ private OverlayActionChip mEditChip;
+ private OverlayActionChip mShareChip;
+ private OverlayActionChip mRemoteCopyChip;
+ private View mActionContainerBackground;
+ private View mDismissButton;
+ private LinearLayout mActionContainer;
+
+ public ClipboardOverlayView(Context context) {
+ this(context, null);
+ }
+
+ public ClipboardOverlayView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ClipboardOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mDisplayMetrics = new DisplayMetrics();
+ mContext.getDisplay().getRealMetrics(mDisplayMetrics);
+ mAccessibilityManager = AccessibilityManager.getInstance(mContext);
+ }
+
+ @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));
+
+ mEditChip.setAlpha(1);
+ mShareChip.setAlpha(1);
+ mRemoteCopyChip.setAlpha(1);
+ mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share));
+
+ mEditChip.setIcon(
+ Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true);
+ mRemoteCopyChip.setIcon(
+ Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true);
+ mShareChip.setIcon(
+ Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true);
+
+ mRemoteCopyChip.setContentDescription(
+ mContext.getString(R.string.clipboard_send_nearby_description));
+
+ mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> {
+ int availableHeight = mTextPreview.getHeight()
+ - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom());
+ mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight());
+ return true;
+ });
+ super.onFinishInflate();
+ }
+
+ @Override
+ public void setCallbacks(SwipeDismissCallbacks callbacks) {
+ super.setCallbacks(callbacks);
+ ClipboardOverlayCallbacks clipboardCallbacks = (ClipboardOverlayCallbacks) callbacks;
+ mEditChip.setOnClickListener(v -> clipboardCallbacks.onEditButtonTapped());
+ mShareChip.setOnClickListener(v -> clipboardCallbacks.onShareButtonTapped());
+ mDismissButton.setOnClickListener(v -> clipboardCallbacks.onDismissButtonTapped());
+ mRemoteCopyChip.setOnClickListener(v -> clipboardCallbacks.onRemoteCopyButtonTapped());
+ mClipboardPreview.setOnClickListener(v -> clipboardCallbacks.onPreviewTapped());
+ }
+
+ void setEditAccessibilityAction(boolean editable) {
+ if (editable) {
+ ViewCompat.replaceAccessibilityAction(mClipboardPreview,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ mContext.getString(R.string.clipboard_edit), null);
+ } else {
+ ViewCompat.replaceAccessibilityAction(mClipboardPreview,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ null, null);
+ }
+ }
+
+ 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();
+ }
+
+ boolean isInTouchRegion(int x, int y) {
+ Region touchRegion = new Region();
+ final Rect tmpRect = new Rect();
+
+ mPreviewBorder.getBoundsOnScreen(tmpRect);
+ tmpRect.inset(
+ (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
+ (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
+ touchRegion.op(tmpRect, Region.Op.UNION);
+
+ mActionContainerBackground.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);
+
+ return touchRegion.contains(x, y);
+ }
+
+ void setRemoteCopyVisibility(boolean visible) {
+ if (visible) {
+ mRemoteCopyChip.setVisibility(View.VISIBLE);
+ mActionContainerBackground.setVisibility(View.VISIBLE);
+ } else {
+ mRemoteCopyChip.setVisibility(View.GONE);
+ }
+ }
+
+ void showDefaultTextPreview() {
+ String copied = mContext.getString(R.string.clipboard_overlay_text_copied);
+ showTextPreview(copied, false);
+ }
+
+ void showTextPreview(CharSequence text, boolean hidden) {
+ TextView textView = hidden ? mHiddenPreview : mTextPreview;
+ showSinglePreview(textView);
+ textView.setText(text.subSequence(0, Math.min(500, text.length())));
+ updateTextSize(text, textView);
+ textView.addOnLayoutChangeListener(
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+ if (right - left != oldRight - oldLeft) {
+ updateTextSize(text, textView);
+ }
+ });
+ mEditChip.setVisibility(View.GONE);
+ }
+
+ void showImagePreview(@Nullable Bitmap thumbnail) {
+ if (thumbnail == null) {
+ mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden));
+ showSinglePreview(mHiddenPreview);
+ } else {
+ mImagePreview.setImageBitmap(thumbnail);
+ showSinglePreview(mImagePreview);
+ }
+ }
+
+ void showEditChip(String contentDescription) {
+ mEditChip.setVisibility(View.VISIBLE);
+ mActionContainerBackground.setVisibility(View.VISIBLE);
+ mEditChip.setContentDescription(contentDescription);
+ }
+
+ void showShareChip() {
+ mShareChip.setVisibility(View.VISIBLE);
+ mActionContainerBackground.setVisibility(View.VISIBLE);
+ }
+
+ void reset() {
+ setTranslationX(0);
+ setAlpha(0);
+ mActionContainerBackground.setVisibility(View.GONE);
+ mDismissButton.setVisibility(View.GONE);
+ mShareChip.setVisibility(View.GONE);
+ mEditChip.setVisibility(View.GONE);
+ mRemoteCopyChip.setVisibility(View.GONE);
+ setEditAccessibilityAction(false);
+ resetActionChips();
+ }
+
+ void resetActionChips() {
+ for (OverlayActionChip chip : mActionChips) {
+ mActionContainer.removeView(chip);
+ }
+ mActionChips.clear();
+ }
+
+ Animator getEnterAnimation() {
+ if (mAccessibilityManager.isEnabled()) {
+ mDismissButton.setVisibility(View.VISIBLE);
+ }
+ TimeInterpolator linearInterpolator = new LinearInterpolator();
+ TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f);
+ AnimatorSet enterAnim = new AnimatorSet();
+
+ ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
+ rootAnim.setInterpolator(linearInterpolator);
+ rootAnim.setDuration(66);
+ rootAnim.addUpdateListener(animation -> {
+ setAlpha(animation.getAnimatedFraction());
+ });
+
+ ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
+ scaleAnim.setInterpolator(scaleInterpolator);
+ scaleAnim.setDuration(333);
+ scaleAnim.addUpdateListener(animation -> {
+ float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
+ mClipboardPreview.setScaleX(previewScale);
+ mClipboardPreview.setScaleY(previewScale);
+ mPreviewBorder.setScaleX(previewScale);
+ mPreviewBorder.setScaleY(previewScale);
+
+ float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
+ mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
+ mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
+ float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction());
+ float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
+ mActionContainer.setScaleX(actionsScaleX);
+ mActionContainer.setScaleY(actionsScaleY);
+ mActionContainerBackground.setScaleX(actionsScaleX);
+ mActionContainerBackground.setScaleY(actionsScaleY);
+ });
+
+ ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
+ alphaAnim.setInterpolator(linearInterpolator);
+ alphaAnim.setDuration(283);
+ alphaAnim.addUpdateListener(animation -> {
+ float alpha = animation.getAnimatedFraction();
+ mClipboardPreview.setAlpha(alpha);
+ mPreviewBorder.setAlpha(alpha);
+ mDismissButton.setAlpha(alpha);
+ mActionContainer.setAlpha(alpha);
+ });
+
+ mActionContainer.setAlpha(0);
+ mPreviewBorder.setAlpha(0);
+ mClipboardPreview.setAlpha(0);
+ enterAnim.play(rootAnim).with(scaleAnim);
+ enterAnim.play(alphaAnim).after(50).after(rootAnim);
+
+ enterAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ setAlpha(1);
+ }
+ });
+ return enterAnim;
+ }
+
+ Animator getExitAnimation() {
+ TimeInterpolator linearInterpolator = new LinearInterpolator();
+ TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f);
+ AnimatorSet exitAnim = new AnimatorSet();
+
+ ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
+ rootAnim.setInterpolator(linearInterpolator);
+ rootAnim.setDuration(100);
+ rootAnim.addUpdateListener(anim -> setAlpha(1 - anim.getAnimatedFraction()));
+
+ ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
+ scaleAnim.setInterpolator(scaleInterpolator);
+ scaleAnim.setDuration(250);
+ scaleAnim.addUpdateListener(animation -> {
+ float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
+ mClipboardPreview.setScaleX(previewScale);
+ mClipboardPreview.setScaleY(previewScale);
+ mPreviewBorder.setScaleX(previewScale);
+ mPreviewBorder.setScaleY(previewScale);
+
+ float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
+ mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
+ mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
+ float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction());
+ float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
+ mActionContainer.setScaleX(actionScaleX);
+ mActionContainer.setScaleY(actionScaleY);
+ mActionContainerBackground.setScaleX(actionScaleX);
+ mActionContainerBackground.setScaleY(actionScaleY);
+ });
+
+ ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
+ alphaAnim.setInterpolator(linearInterpolator);
+ alphaAnim.setDuration(166);
+ alphaAnim.addUpdateListener(animation -> {
+ float alpha = 1 - animation.getAnimatedFraction();
+ mClipboardPreview.setAlpha(alpha);
+ mPreviewBorder.setAlpha(alpha);
+ mDismissButton.setAlpha(alpha);
+ mActionContainer.setAlpha(alpha);
+ });
+
+ exitAnim.play(alphaAnim).with(scaleAnim);
+ exitAnim.play(rootAnim).after(150).after(alphaAnim);
+ return exitAnim;
+ }
+
+ void setActionChip(RemoteAction action, Runnable onFinish) {
+ mActionContainerBackground.setVisibility(View.VISIBLE);
+ OverlayActionChip chip = constructActionChip(action, onFinish);
+ mActionContainer.addView(chip);
+ mActionChips.add(chip);
+ }
+
+ private void showSinglePreview(View v) {
+ mTextPreview.setVisibility(View.GONE);
+ mImagePreview.setVisibility(View.GONE);
+ mHiddenPreview.setVisibility(View.GONE);
+ v.setVisibility(View.VISIBLE);
+ }
+
+ private OverlayActionChip constructActionChip(RemoteAction action, Runnable onFinish) {
+ OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate(
+ R.layout.overlay_action_chip, mActionContainer, false);
+ chip.setText(action.getTitle());
+ chip.setContentDescription(action.getTitle());
+ chip.setIcon(action.getIcon(), false);
+ chip.setPendingIntent(action.getActionIntent(), onFinish);
+ chip.setAlpha(1);
+ return chip;
+ }
+
+ private static void updateTextSize(CharSequence text, TextView textView) {
+ Paint paint = new Paint(textView.getPaint());
+ Resources res = textView.getResources();
+ float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font);
+ float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font);
+ if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) {
+ // If the text is a single word and would fit within the TextView at the min font size,
+ // find the biggest font size that will fit.
+ float fontSizePx = minFontSize;
+ while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize
+ && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) {
+ fontSizePx += FONT_SEARCH_STEP_PX;
+ }
+ // Need to turn off autosizing, otherwise setTextSize is a no-op.
+ textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
+ // It's possible to hit the max font size and not fill the width, so centering
+ // horizontally looks better in this case.
+ textView.setGravity(Gravity.CENTER);
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx);
+ } else {
+ // Otherwise just stick with autosize.
+ textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize,
+ (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX);
+ textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
+ }
+ }
+
+ private static boolean fitsInView(CharSequence text, TextView textView, Paint paint,
+ float fontSizePx) {
+ paint.setTextSize(fontSizePx);
+ float size = paint.measureText(text.toString());
+ float availableWidth = textView.getWidth() - textView.getPaddingLeft()
+ - textView.getPaddingRight();
+ return size < availableWidth;
+ }
+
+ private static boolean isOneWord(CharSequence text) {
+ return text.toString().split("\\s+", 2).length == 1;
+ }
+
+ private static Rect computeMargins(WindowInsets insets, int orientation) {
+ DisplayCutout cutout = insets.getDisplayCutout();
+ Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars());
+ Insets imeInsets = insets.getInsets(WindowInsets.Type.ime());
+ if (cutout == null) {
+ return new Rect(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom));
+ } else {
+ Insets waterfall = cutout.getWaterfallInsets();
+ if (orientation == ORIENTATION_PORTRAIT) {
+ return new Rect(
+ waterfall.left,
+ Math.max(cutout.getSafeInsetTop(), waterfall.top),
+ waterfall.right,
+ Math.max(imeInsets.bottom,
+ Math.max(cutout.getSafeInsetBottom(),
+ Math.max(navBarInsets.bottom, waterfall.bottom))));
+ } else {
+ return new Rect(
+ waterfall.left,
+ waterfall.top,
+ waterfall.right,
+ Math.max(imeInsets.bottom,
+ Math.max(navBarInsets.bottom, waterfall.bottom)));
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java
new file mode 100644
index 0000000..9dac9b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java
@@ -0,0 +1,174 @@
+/*
+ * 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.annotation.MainThread;
+import android.annotation.NonNull;
+import android.app.ICompatCameraControlCallback;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+
+import com.android.internal.policy.PhoneWindow;
+import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule.OverlayWindowContext;
+import com.android.systemui.screenshot.FloatingWindowUtil;
+
+import java.util.function.BiConsumer;
+
+import javax.inject.Inject;
+
+/**
+ * Handles attaching the window and the window insets for the clipboard overlay.
+ */
+public class ClipboardOverlayWindow extends PhoneWindow
+ implements ViewRootImpl.ActivityConfigCallback {
+ private static final String TAG = "ClipboardOverlayWindow";
+
+ private final Context mContext;
+ private final WindowManager mWindowManager;
+ private final WindowManager.LayoutParams mWindowLayoutParams;
+
+ private boolean mKeyboardVisible;
+ private final int mOrientation;
+ private BiConsumer<WindowInsets, Integer> mOnKeyboardChangeListener;
+ private Runnable mOnOrientationChangeListener;
+
+ @Inject
+ ClipboardOverlayWindow(@OverlayWindowContext Context context) {
+ super(context);
+ mContext = context;
+ mOrientation = mContext.getResources().getConfiguration().orientation;
+
+ // Setup the window that we are going to use
+ requestFeature(Window.FEATURE_NO_TITLE);
+ requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS);
+ setBackgroundDrawableResource(android.R.color.transparent);
+ mWindowManager = mContext.getSystemService(WindowManager.class);
+ mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams();
+ mWindowLayoutParams.setTitle("ClipboardOverlay");
+ setWindowManager(mWindowManager, null, null);
+ setWindowFocusable(false);
+ }
+
+ /**
+ * Set callbacks for keyboard state change and orientation change and attach the window
+ *
+ * @param onKeyboardChangeListener callback for IME visibility changes
+ * @param onOrientationChangeListener callback for device orientation changes
+ */
+ public void init(@NonNull BiConsumer<WindowInsets, Integer> onKeyboardChangeListener,
+ @NonNull Runnable onOrientationChangeListener) {
+ mOnKeyboardChangeListener = onKeyboardChangeListener;
+ mOnOrientationChangeListener = onOrientationChangeListener;
+
+ attach();
+ withWindowAttached(() -> {
+ WindowInsets currentInsets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
+ mKeyboardVisible = currentInsets.isVisible(WindowInsets.Type.ime());
+ peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener(() -> {
+ WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
+ boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime());
+ if (keyboardVisible != mKeyboardVisible) {
+ mKeyboardVisible = keyboardVisible;
+ mOnKeyboardChangeListener.accept(insets, mOrientation);
+ }
+ });
+ peekDecorView().getViewRootImpl().setActivityConfigCallback(this);
+ });
+ }
+
+ @Override // ViewRootImpl.ActivityConfigCallback
+ public void onConfigurationChanged(Configuration overrideConfig, int newDisplayId) {
+ if (mContext.getResources().getConfiguration().orientation != mOrientation) {
+ mOnOrientationChangeListener.run();
+ }
+ }
+
+ @Override // ViewRootImpl.ActivityConfigCallback
+ public void requestCompatCameraControl(boolean showControl, boolean transformationApplied,
+ ICompatCameraControlCallback callback) {
+ Log.w(TAG, "unexpected requestCompatCameraControl call");
+ }
+
+ void remove() {
+ final View decorView = peekDecorView();
+ if (decorView != null && decorView.isAttachedToWindow()) {
+ mWindowManager.removeViewImmediate(decorView);
+ }
+ }
+
+ WindowInsets getWindowInsets() {
+ return mWindowManager.getCurrentWindowMetrics().getWindowInsets();
+ }
+
+ void withWindowAttached(Runnable action) {
+ View decorView = getDecorView();
+ if (decorView.isAttachedToWindow()) {
+ action.run();
+ } else {
+ decorView.getViewTreeObserver().addOnWindowAttachListener(
+ new ViewTreeObserver.OnWindowAttachListener() {
+ @Override
+ public void onWindowAttached() {
+ decorView.getViewTreeObserver().removeOnWindowAttachListener(this);
+ action.run();
+ }
+
+ @Override
+ public void onWindowDetached() {
+ }
+ });
+ }
+ }
+
+ @MainThread
+ private void attach() {
+ View decorView = getDecorView();
+ if (decorView.isAttachedToWindow()) {
+ return;
+ }
+ mWindowManager.addView(decorView, mWindowLayoutParams);
+ decorView.requestApplyInsets();
+ }
+
+ /**
+ * Updates the window focusability. If the window is already showing, then it updates the
+ * window immediately, otherwise the layout params will be applied when the window is next
+ * shown.
+ */
+ private void setWindowFocusable(boolean focusable) {
+ int flags = mWindowLayoutParams.flags;
+ if (focusable) {
+ mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ } else {
+ mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ }
+ if (mWindowLayoutParams.flags == flags) {
+ return;
+ }
+ final View decorView = peekDecorView();
+ if (decorView != null && decorView.isAttachedToWindow()) {
+ mWindowManager.updateViewLayout(decorView, mWindowLayoutParams);
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
new file mode 100644
index 0000000..2244813
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.clipboardoverlay.dagger;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.view.Display;
+import android.view.LayoutInflater;
+
+import com.android.systemui.R;
+import com.android.systemui.clipboardoverlay.ClipboardOverlayView;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Qualifier;
+
+import dagger.Module;
+import dagger.Provides;
+
+/** Module for {@link com.android.systemui.clipboardoverlay}. */
+@Module
+public interface ClipboardOverlayModule {
+
+ /**
+ *
+ */
+ @Provides
+ @OverlayWindowContext
+ static Context provideWindowContext(DisplayManager displayManager, Context context) {
+ Display display = displayManager.getDisplay(DEFAULT_DISPLAY);
+ return context.createWindowContext(display, TYPE_SCREENSHOT, null);
+ }
+
+ /**
+ *
+ */
+ @Provides
+ static ClipboardOverlayView provideClipboardOverlayView(@OverlayWindowContext Context context) {
+ return (ClipboardOverlayView) LayoutInflater.from(context).inflate(
+ R.layout.clipboard_overlay, null);
+ }
+
+ @Qualifier
+ @Documented
+ @Retention(RUNTIME)
+ @interface OverlayWindowContext {
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index dc3dadb..d7638d6 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -33,6 +33,7 @@
import com.android.systemui.biometrics.UdfpsDisplayModeProvider;
import com.android.systemui.biometrics.dagger.BiometricsModule;
import com.android.systemui.classifier.FalsingModule;
+import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule;
import com.android.systemui.controls.dagger.ControlsModule;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.demomode.dagger.DemoModeModule;
@@ -118,6 +119,7 @@
AssistModule.class,
BiometricsModule.class,
BouncerViewModule.class,
+ ClipboardOverlayModule.class,
ClockModule.class,
CoroutinesModule.class,
DreamModule.class,
@@ -165,12 +167,16 @@
@Binds
abstract BootCompleteCache bindBootCompleteCache(BootCompleteCacheImpl bootCompleteCache);
- /** */
+ /**
+ *
+ */
@Binds
public abstract ContextComponentHelper bindComponentHelper(
ContextComponentResolver componentHelper);
- /** */
+ /**
+ *
+ */
@Binds
public abstract NotificationRowBinder bindNotificationRowBinder(
NotificationRowBinderImpl notificationRowBinder);
@@ -209,6 +215,7 @@
abstract SystemClock bindSystemClock(SystemClockImpl systemClock);
// TODO: This should provided by the WM component
+
/** Provides Optional of BubbleManager */
@SysUISingleton
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 3dbadb0..7f08f86 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -105,10 +105,6 @@
*/
public static final UnreleasedFlag MODERN_BOUNCER = new UnreleasedFlag(208);
- /** Whether UserSwitcherActivity should use modern architecture. */
- public static final ReleasedFlag MODERN_USER_SWITCHER_ACTIVITY =
- new ReleasedFlag(209, true);
-
/**
* Whether the user interactor and repository should use `UserSwitcherController`.
*
@@ -309,6 +305,9 @@
public static final UnreleasedFlag A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS =
new UnreleasedFlag(1600);
+ // 1700 - clipboard
+ public static final UnreleasedFlag CLIPBOARD_OVERLAY_REFACTOR = new UnreleasedFlag(1700);
+
// Pay no attention to the reflection behind the curtain.
// ========================== Curtain ==========================
// | |
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 01cd3e2..f663b0d 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
@@ -19,7 +19,7 @@
import android.content.Intent
import com.android.internal.widget.LockPatternUtils
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
@@ -67,19 +67,19 @@
*
* @param configKey The configuration key corresponding to the [KeyguardQuickAffordanceModel] of
* the affordance that was clicked
- * @param animationController An optional controller for the activity-launch animation
+ * @param expandable An optional [Expandable] for the activity- or dialog-launch animation
*/
fun onQuickAffordanceClicked(
configKey: KClass<out KeyguardQuickAffordanceConfig>,
- animationController: ActivityLaunchAnimator.Controller?,
+ expandable: Expandable?,
) {
@Suppress("UNCHECKED_CAST") val config = registry.get(configKey as KClass<Nothing>)
- when (val result = config.onQuickAffordanceClicked(animationController)) {
+ when (val result = config.onQuickAffordanceClicked(expandable)) {
is KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity ->
launchQuickAffordance(
intent = result.intent,
canShowWhileLocked = result.canShowWhileLocked,
- animationController = animationController
+ expandable = expandable,
)
is KeyguardQuickAffordanceConfig.OnClickedResult.Handled -> Unit
}
@@ -104,6 +104,7 @@
KeyguardQuickAffordanceModel.Visible(
configKey = configs[index]::class,
icon = visibleState.icon,
+ toggle = visibleState.toggle,
)
} else {
KeyguardQuickAffordanceModel.Hidden
@@ -114,7 +115,7 @@
private fun launchQuickAffordance(
intent: Intent,
canShowWhileLocked: Boolean,
- animationController: ActivityLaunchAnimator.Controller?,
+ expandable: Expandable?,
) {
@LockPatternUtils.StrongAuthTracker.StrongAuthFlags
val strongAuthFlags =
@@ -130,13 +131,13 @@
activityStarter.postStartActivityDismissingKeyguard(
intent,
0 /* delay */,
- animationController
+ expandable?.activityLaunchController(),
)
} else {
activityStarter.startActivity(
intent,
true /* dismissShade */,
- animationController,
+ expandable?.activityLaunchController(),
true /* showOverLockscreenWhenLocked */,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt
index eb6bb92..e56b259 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt
@@ -19,6 +19,7 @@
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
import kotlin.reflect.KClass
/**
@@ -35,5 +36,7 @@
val configKey: KClass<out KeyguardQuickAffordanceConfig>,
/** An icon for the affordance. */
val icon: Icon,
+ /** The toggle state for the affordance. */
+ val toggle: KeyguardQuickAffordanceToggleState,
) : KeyguardQuickAffordanceModel()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
index 89604f0..8384260 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
@@ -20,7 +20,7 @@
import android.content.Context
import android.content.Intent
import androidx.annotation.DrawableRes
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.common.shared.model.ContentDescription
@@ -61,7 +61,7 @@
}
override fun onQuickAffordanceClicked(
- animationController: ActivityLaunchAnimator.Controller?,
+ expandable: Expandable?,
): KeyguardQuickAffordanceConfig.OnClickedResult {
return KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity(
intent =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt
index 8e1c6b7..95027d0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt
@@ -18,8 +18,9 @@
package com.android.systemui.keyguard.domain.quickaffordance
import android.content.Intent
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
import kotlinx.coroutines.flow.Flow
/** Defines interface that can act as data source for a single quick affordance model. */
@@ -27,9 +28,7 @@
val state: Flow<State>
- fun onQuickAffordanceClicked(
- animationController: ActivityLaunchAnimator.Controller?
- ): OnClickedResult
+ fun onQuickAffordanceClicked(expandable: Expandable?): OnClickedResult
/**
* Encapsulates the state of a "quick affordance" in the keyguard bottom area (for example, a
@@ -44,6 +43,9 @@
data class Visible(
/** An icon for the affordance. */
val icon: Icon,
+ /** The toggle state for the affordance. */
+ val toggle: KeyguardQuickAffordanceToggleState =
+ KeyguardQuickAffordanceToggleState.NotSupported,
) : State()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
index d97deaf..502a607 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
@@ -18,7 +18,7 @@
package com.android.systemui.keyguard.domain.quickaffordance
import com.android.systemui.R
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.common.shared.model.ContentDescription
@@ -66,7 +66,7 @@
}
override fun onQuickAffordanceClicked(
- animationController: ActivityLaunchAnimator.Controller?,
+ expandable: Expandable?,
): KeyguardQuickAffordanceConfig.OnClickedResult {
return KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity(
intent = controller.intent,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
index 9196b09..a24a0d6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
@@ -23,7 +23,7 @@
import android.service.quickaccesswallet.QuickAccessWalletClient
import android.util.Log
import com.android.systemui.R
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.common.shared.model.ContentDescription
@@ -84,11 +84,11 @@
}
override fun onQuickAffordanceClicked(
- animationController: ActivityLaunchAnimator.Controller?,
+ expandable: Expandable?,
): KeyguardQuickAffordanceConfig.OnClickedResult {
walletController.startQuickAccessUiIntent(
activityStarter,
- animationController,
+ expandable?.activityLaunchController(),
/* hasCard= */ true,
)
return KeyguardQuickAffordanceConfig.OnClickedResult.Handled
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordanceToggleState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordanceToggleState.kt
new file mode 100644
index 0000000..55d38a4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordanceToggleState.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.shared.quickaffordance
+
+/** Enumerates all possible toggle states for a quick affordance on the lock-screen. */
+sealed class KeyguardQuickAffordanceToggleState {
+ /** Toggling is not supported. */
+ object NotSupported : KeyguardQuickAffordanceToggleState()
+ /** The quick affordance is on. */
+ object On : KeyguardQuickAffordanceToggleState()
+ /** The quick affordance is off. */
+ object Off : KeyguardQuickAffordanceToggleState()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
index 65b85c0..2c99ca5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -29,7 +29,7 @@
import androidx.lifecycle.repeatOnLifecycle
import com.android.settingslib.Utils
import com.android.systemui.R
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.animation.Interpolators
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
@@ -238,14 +238,26 @@
IconViewBinder.bind(viewModel.icon, view)
+ view.isActivated = viewModel.isActivated
view.drawable.setTint(
Utils.getColorAttrDefaultColor(
view.context,
- com.android.internal.R.attr.textColorPrimary
+ if (viewModel.isActivated) {
+ com.android.internal.R.attr.textColorPrimaryInverse
+ } else {
+ com.android.internal.R.attr.textColorPrimary
+ },
)
)
view.backgroundTintList =
- Utils.getColorAttr(view.context, com.android.internal.R.attr.colorSurface)
+ Utils.getColorAttr(
+ view.context,
+ if (viewModel.isActivated) {
+ com.android.internal.R.attr.colorAccentPrimary
+ } else {
+ com.android.internal.R.attr.colorSurface
+ }
+ )
view.isClickable = viewModel.isClickable
if (viewModel.isClickable) {
@@ -268,7 +280,7 @@
viewModel.onClicked(
KeyguardQuickAffordanceViewModel.OnClickedParameters(
configKey = viewModel.configKey,
- animationController = ActivityLaunchAnimator.Controller.fromView(view),
+ expandable = Expandable.fromView(view),
)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
index 970ee4c..535ca72 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
@@ -23,6 +23,7 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@@ -119,10 +120,11 @@
onClicked = { parameters ->
quickAffordanceInteractor.onQuickAffordanceClicked(
configKey = parameters.configKey,
- animationController = parameters.animationController,
+ expandable = parameters.expandable,
)
},
isClickable = isClickable,
+ isActivated = toggle is KeyguardQuickAffordanceToggleState.On,
)
is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
index 0971f13..bf598ba 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
@@ -16,7 +16,7 @@
package com.android.systemui.keyguard.ui.viewmodel
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
import kotlin.reflect.KClass
@@ -30,9 +30,10 @@
val icon: Icon = Icon.Resource(res = 0, contentDescription = null),
val onClicked: (OnClickedParameters) -> Unit = {},
val isClickable: Boolean = false,
+ val isActivated: Boolean = false,
) {
data class OnClickedParameters(
val configKey: KClass<out KeyguardQuickAffordanceConfig>,
- val animationController: ActivityLaunchAnimator.Controller?,
+ val expandable: Expandable?,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 50a10bc..6da8c69 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -114,6 +114,7 @@
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.DisplayId;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationBarComponent.NavigationBarScope;
import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener;
@@ -211,6 +212,7 @@
private final NotificationShadeDepthController mNotificationShadeDepthController;
private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener;
private final UserContextProvider mUserContextProvider;
+ private final WakefulnessLifecycle mWakefulnessLifecycle;
private final RegionSamplingHelper mRegionSamplingHelper;
private final int mNavColorSampleMargin;
private NavigationBarFrame mFrame;
@@ -451,6 +453,28 @@
}
};
+ private final WakefulnessLifecycle.Observer mWakefulnessObserver =
+ new WakefulnessLifecycle.Observer() {
+ private void notifyScreenStateChanged(boolean isScreenOn) {
+ notifyNavigationBarScreenOn();
+ mView.onScreenStateChanged(isScreenOn);
+ }
+
+ @Override
+ public void onStartedWakingUp() {
+ notifyScreenStateChanged(true);
+ if (isGesturalModeOnDefaultDisplay(getContext(), mNavBarMode)) {
+ mRegionSamplingHelper.start(mSamplingBounds);
+ }
+ }
+
+ @Override
+ public void onFinishedGoingToSleep() {
+ notifyScreenStateChanged(false);
+ mRegionSamplingHelper.stop();
+ }
+ };
+
@Inject
NavigationBar(
NavigationBarView navigationBarView,
@@ -491,7 +515,8 @@
NavigationBarTransitions navigationBarTransitions,
EdgeBackGestureHandler edgeBackGestureHandler,
Optional<BackAnimation> backAnimation,
- UserContextProvider userContextProvider) {
+ UserContextProvider userContextProvider,
+ WakefulnessLifecycle wakefulnessLifecycle) {
super(navigationBarView);
mFrame = navigationBarFrame;
mContext = context;
@@ -529,6 +554,7 @@
mTelecomManagerOptional = telecomManagerOptional;
mInputMethodManager = inputMethodManager;
mUserContextProvider = userContextProvider;
+ mWakefulnessLifecycle = wakefulnessLifecycle;
mNavColorSampleMargin = getResources()
.getDimensionPixelSize(R.dimen.navigation_handle_sample_horizontal_margin);
@@ -682,11 +708,10 @@
prepareNavigationBarView();
checkNavBarModes();
- IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
- filter.addAction(Intent.ACTION_SCREEN_ON);
- filter.addAction(Intent.ACTION_USER_SWITCHED);
+ IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED);
mBroadcastDispatcher.registerReceiverWithHandler(mBroadcastReceiver, filter,
Handler.getMain(), UserHandle.ALL);
+ mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
notifyNavigationBarScreenOn();
mOverviewProxyService.addCallback(mOverviewProxyListener);
@@ -737,6 +762,7 @@
getBarTransitions().destroy();
mOverviewProxyService.removeCallback(mOverviewProxyListener);
mBroadcastDispatcher.unregisterReceiver(mBroadcastReceiver);
+ mWakefulnessLifecycle.removeObserver(mWakefulnessObserver);
if (mOrientationHandle != null) {
resetSecondaryHandle();
getBarTransitions().removeDarkIntensityListener(mOrientationHandleIntensityListener);
@@ -1619,19 +1645,6 @@
return;
}
String action = intent.getAction();
- if (Intent.ACTION_SCREEN_OFF.equals(action)
- || Intent.ACTION_SCREEN_ON.equals(action)) {
- notifyNavigationBarScreenOn();
- boolean isScreenOn = Intent.ACTION_SCREEN_ON.equals(action);
- mView.onScreenStateChanged(isScreenOn);
- if (isScreenOn) {
- if (isGesturalModeOnDefaultDisplay(getContext(), mNavBarMode)) {
- mRegionSamplingHelper.start(mSamplingBounds);
- }
- } else {
- mRegionSamplingHelper.stop();
- }
- }
if (Intent.ACTION_USER_SWITCHED.equals(action)) {
// The accessibility settings may be different for the new user
updateAccessibilityStateFlags();
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
index 029cf68..3fd1aa7 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
@@ -21,6 +21,7 @@
import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR;
import static android.view.Display.DEFAULT_DISPLAY;
+import static com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler.DEBUG_MISSING_GESTURE_TAG;
import static com.android.systemui.shared.recents.utilities.Utilities.isTablet;
import android.content.ContentResolver;
@@ -141,7 +142,13 @@
public void onConfigChanged(Configuration newConfig) {
boolean isOldConfigTablet = mIsTablet;
mIsTablet = isTablet(mContext);
+ boolean willApplyConfig = mConfigChanges.applyNewConfig(mContext.getResources());
boolean largeScreenChanged = mIsTablet != isOldConfigTablet;
+ // TODO(b/243765256): Disable this logging once b/243765256 is fixed.
+ Log.d(DEBUG_MISSING_GESTURE_TAG, "NavbarController: newConfig=" + newConfig
+ + " mTaskbarDelegate initialized=" + mTaskbarDelegate.isInitialized()
+ + " willApplyConfigToNavbars=" + willApplyConfig
+ + " navBarCount=" + mNavigationBars.size());
if (mTaskbarDelegate.isInitialized()) {
mTaskbarDelegate.onConfigurationChanged(newConfig);
}
@@ -150,7 +157,7 @@
return;
}
- if (mConfigChanges.applyNewConfig(mContext.getResources())) {
+ if (willApplyConfig) {
for (int i = 0; i < mNavigationBars.size(); i++) {
recreateNavigationBar(mNavigationBars.keyAt(i));
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index a8799c7..709467f 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -113,7 +113,7 @@
private static final int MAX_NUM_LOGGED_GESTURES = 10;
static final boolean DEBUG_MISSING_GESTURE = false;
- static final String DEBUG_MISSING_GESTURE_TAG = "NoBackGesture";
+ public static final String DEBUG_MISSING_GESTURE_TAG = "NoBackGesture";
private ISystemGestureExclusionListener mGestureExclusionListener =
new ISystemGestureExclusionListener.Stub() {
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
index fae0b50..1da866e 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
@@ -46,6 +46,7 @@
import com.android.systemui.R;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -78,6 +79,7 @@
private final PowerManager mPowerManager;
private final WarningsUI mWarnings;
+ private final WakefulnessLifecycle mWakefulnessLifecycle;
private InattentiveSleepWarningView mOverlayView;
private final Configuration mLastConfiguration = new Configuration();
private int mPlugType = 0;
@@ -107,11 +109,24 @@
private final BroadcastDispatcher mBroadcastDispatcher;
private final CommandQueue mCommandQueue;
private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
+ private final WakefulnessLifecycle.Observer mWakefulnessObserver =
+ new WakefulnessLifecycle.Observer() {
+ @Override
+ public void onStartedWakingUp() {
+ mScreenOffTime = -1;
+ }
+
+ @Override
+ public void onFinishedGoingToSleep() {
+ mScreenOffTime = SystemClock.elapsedRealtime();
+ }
+ };
@Inject
public PowerUI(Context context, BroadcastDispatcher broadcastDispatcher,
CommandQueue commandQueue, Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
WarningsUI warningsUI, EnhancedEstimates enhancedEstimates,
+ WakefulnessLifecycle wakefulnessLifecycle,
PowerManager powerManager) {
mContext = context;
mBroadcastDispatcher = broadcastDispatcher;
@@ -120,6 +135,7 @@
mWarnings = warningsUI;
mEnhancedEstimates = enhancedEstimates;
mPowerManager = powerManager;
+ mWakefulnessLifecycle = wakefulnessLifecycle;
}
public void start() {
@@ -138,6 +154,7 @@
false, obs, UserHandle.USER_ALL);
updateBatteryWarningLevels();
mReceiver.init();
+ mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
// Check to see if we need to let the user know that the phone previously shut down due
// to the temperature being too high.
@@ -233,8 +250,6 @@
IntentFilter filter = new IntentFilter();
filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
- filter.addAction(Intent.ACTION_SCREEN_OFF);
- filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_USER_SWITCHED);
mBroadcastDispatcher.registerReceiverWithHandler(this, filter, mHandler);
// Force get initial values. Relying on Sticky behavior until API for getting info.
@@ -317,10 +332,6 @@
plugged, bucket);
});
- } else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
- mScreenOffTime = SystemClock.elapsedRealtime();
- } else if (Intent.ACTION_SCREEN_ON.equals(action)) {
- mScreenOffTime = -1;
} else if (Intent.ACTION_USER_SWITCHED.equals(action)) {
mWarnings.userSwitched();
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
index 11d9555..d3c06f6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
@@ -23,7 +23,6 @@
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import com.android.settingslib.Utils
-import com.android.settingslib.drawable.UserIconDrawable
import com.android.systemui.R
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.ContentDescription
@@ -250,22 +249,19 @@
status: UserSwitcherStatusModel.Enabled
): FooterActionsButtonViewModel {
val icon = status.currentUserImage!!
- val iconTint =
- if (status.isGuestUser && icon !is UserIconDrawable) {
- Utils.getColorAttrDefaultColor(context, android.R.attr.colorForeground)
- } else {
- null
- }
return FooterActionsButtonViewModel(
id = R.id.multi_user_switch,
- Icon.Loaded(
- icon,
- ContentDescription.Loaded(userSwitcherContentDescription(status.currentUserName)),
- ),
- iconTint,
- R.drawable.qs_footer_action_circle,
- this::onUserSwitcherClicked,
+ icon =
+ Icon.Loaded(
+ icon,
+ ContentDescription.Loaded(
+ userSwitcherContentDescription(status.currentUserName)
+ ),
+ ),
+ iconTint = null,
+ background = R.drawable.qs_footer_action_circle,
+ onClick = this::onUserSwitcherClicked,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java b/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java
index 950806d..ead3b7b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java
@@ -49,7 +49,6 @@
private final SwipeDismissHandler mSwipeDismissHandler;
private final GestureDetector mSwipeDetector;
private View mActionsContainer;
- private View mActionsContainerBackground;
private SwipeDismissCallbacks mCallbacks;
private final DisplayMetrics mDisplayMetrics;
@@ -111,6 +110,9 @@
}
});
mSwipeDetector.setIsLongpressEnabled(false);
+
+ mCallbacks = new SwipeDismissCallbacks() {
+ }; // default to unimplemented callbacks
}
public void setCallbacks(SwipeDismissCallbacks callbacks) {
@@ -119,16 +121,13 @@
@Override
public boolean onInterceptHoverEvent(MotionEvent event) {
- if (mCallbacks != null) {
- mCallbacks.onInteraction();
- }
+ mCallbacks.onInteraction();
return super.onInterceptHoverEvent(event);
}
@Override // View
protected void onFinishInflate() {
mActionsContainer = findViewById(R.id.actions_container);
- mActionsContainerBackground = findViewById(R.id.actions_container_background);
}
@Override
@@ -186,6 +185,13 @@
inoutInfo.touchableRegion.set(r);
}
+ private int getBackgroundRight() {
+ // background expected to be null in testing.
+ // animation may have unexpected behavior if view is not present
+ View background = findViewById(R.id.actions_container_background);
+ return background == null ? 0 : background.getRight();
+ }
+
/**
* Allows a view to be swipe-dismissed, or returned to its location if distance threshold is not
* met
@@ -213,8 +219,6 @@
mGestureDetector = new GestureDetector(context, gestureListener);
mDisplayMetrics = new DisplayMetrics();
context.getDisplay().getRealMetrics(mDisplayMetrics);
- mCallbacks = new SwipeDismissCallbacks() {
- }; // default to unimplemented callbacks
}
@Override
@@ -230,7 +234,9 @@
return true;
}
if (isPastDismissThreshold()) {
- dismiss();
+ ValueAnimator anim = createSwipeDismissAnimation();
+ mCallbacks.onSwipeDismissInitiated(anim);
+ dismiss(anim);
} else {
// if we've moved, but not past the threshold, start the return animation
if (DEBUG_DISMISS) {
@@ -295,10 +301,7 @@
}
void dismiss() {
- float velocityPxPerMs = FloatingWindowUtil.dpToPx(mDisplayMetrics, VELOCITY_DP_PER_MS);
- ValueAnimator anim = createSwipeDismissAnimation(velocityPxPerMs);
- mCallbacks.onSwipeDismissInitiated(anim);
- dismiss(anim);
+ dismiss(createSwipeDismissAnimation());
}
private void dismiss(ValueAnimator animator) {
@@ -323,6 +326,11 @@
mDismissAnimation.start();
}
+ private ValueAnimator createSwipeDismissAnimation() {
+ float velocityPxPerMs = FloatingWindowUtil.dpToPx(mDisplayMetrics, VELOCITY_DP_PER_MS);
+ return createSwipeDismissAnimation(velocityPxPerMs);
+ }
+
private ValueAnimator createSwipeDismissAnimation(float velocity) {
// velocity is measured in pixels per millisecond
velocity = Math.min(3, Math.max(1, velocity));
@@ -337,7 +345,7 @@
if (startX > 0 || (startX == 0 && layoutDir == LAYOUT_DIRECTION_RTL)) {
finalX = mDisplayMetrics.widthPixels;
} else {
- finalX = -1 * mActionsContainerBackground.getRight();
+ finalX = -1 * getBackgroundRight();
}
float distance = Math.abs(finalX - startX);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 704e115..6d5121a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -28,6 +28,7 @@
import static com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW;
import static com.android.systemui.screenshot.LogConfig.logTag;
import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER;
+import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT;
import static java.util.Objects.requireNonNull;
@@ -331,9 +332,7 @@
if (DEBUG_UI) {
Log.d(TAG, "Corner timeout hit");
}
- mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT, 0,
- mPackageName);
- ScreenshotController.this.dismissScreenshot(false);
+ dismissScreenshot(SCREENSHOT_INTERACTION_TIMEOUT);
});
mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
@@ -361,8 +360,7 @@
@Override
public void onReceive(Context context, Intent intent) {
if (ClipboardOverlayController.COPY_OVERLAY_ACTION.equals(intent.getAction())) {
- mUiEventLogger.log(SCREENSHOT_DISMISSED_OTHER);
- dismissScreenshot(false);
+ dismissScreenshot(SCREENSHOT_DISMISSED_OTHER);
}
}
};
@@ -410,24 +408,20 @@
/**
* Clears current screenshot
*/
- void dismissScreenshot(boolean immediate) {
+ void dismissScreenshot(ScreenshotEvent event) {
if (DEBUG_DISMISS) {
- Log.d(TAG, "dismissScreenshot(immediate=" + immediate + ")");
+ Log.d(TAG, "dismissScreenshot");
}
// If we're already animating out, don't restart the animation
- // (but do obey an immediate dismissal)
- if (!immediate && mScreenshotView.isDismissing()) {
+ if (mScreenshotView.isDismissing()) {
if (DEBUG_DISMISS) {
Log.v(TAG, "Already dismissing, ignoring duplicate command");
}
return;
}
+ mUiEventLogger.log(event, 0, mPackageName);
mScreenshotHandler.cancelTimeout();
- if (immediate) {
- finishDismiss();
- } else {
- mScreenshotView.animateDismissal();
- }
+ mScreenshotView.animateDismissal();
}
boolean isPendingSharedTransition() {
@@ -500,7 +494,7 @@
if (DEBUG_INPUT) {
Log.d(TAG, "onKeyEvent: KeyEvent.KEYCODE_BACK");
}
- dismissScreenshot(false);
+ dismissScreenshot(SCREENSHOT_DISMISSED_OTHER);
return true;
}
return false;
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
index a4a59ce..2176825 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
@@ -89,8 +89,7 @@
Log.d(TAG, "Received ACTION_CLOSE_SYSTEM_DIALOGS");
}
if (!mScreenshot.isPendingSharedTransition()) {
- mUiEventLogger.log(SCREENSHOT_DISMISSED_OTHER);
- mScreenshot.dismissScreenshot(false);
+ mScreenshot.dismissScreenshot(SCREENSHOT_DISMISSED_OTHER);
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index c290ce2..184dc25 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -189,7 +189,6 @@
protected NotificationPresenter mPresenter;
protected ContentObserver mLockscreenSettingsObserver;
protected ContentObserver mSettingsObserver;
- private boolean mHideSilentNotificationsOnLockscreen;
@Inject
public NotificationLockscreenUserManagerImpl(Context context,
@@ -266,12 +265,6 @@
UserHandle.USER_ALL);
mContext.getContentResolver().registerContentObserver(
- mSecureSettings.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS),
- true,
- mLockscreenSettingsObserver,
- UserHandle.USER_ALL);
-
- mContext.getContentResolver().registerContentObserver(
Settings.Global.getUriFor(Settings.Global.ZEN_MODE), false,
mSettingsObserver);
@@ -340,9 +333,6 @@
final boolean allowedByDpm = (dpmFlags
& DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS) == 0;
- mHideSilentNotificationsOnLockscreen = mSecureSettings.getIntForUser(
- Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, 1, mCurrentUserId) == 0;
-
setShowLockscreenNotifications(show && allowedByDpm);
if (ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java
index 6f41425..9a7610d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java
@@ -114,7 +114,18 @@
*/
public void unbindHeadsUpView(NotificationEntry entry) {
abortBindCallback(entry);
- mStage.getStageParams(entry).markContentViewsFreeable(FLAG_CONTENT_VIEW_HEADS_UP);
+
+ // params may be null if the notification was already removed from the collection but we let
+ // it stick around during a launch animation. In this case, the heads up view has already
+ // been unbound, so we don't need to unbind it.
+ // TODO(b/253081345): Change this back to getStageParams and remove null check.
+ RowContentBindParams params = mStage.tryGetStageParams(entry);
+ if (params == null) {
+ mLogger.entryBindStageParamsNullOnUnbind(entry);
+ return;
+ }
+
+ params.markContentViewsFreeable(FLAG_CONTENT_VIEW_HEADS_UP);
mLogger.entryContentViewMarkedFreeable(entry);
mStage.requestRebind(entry, e -> mLogger.entryUnbound(e));
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
index d1feaa0..5dbec8d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
@@ -47,6 +47,14 @@
"start unbinding heads up entry $str1 "
})
}
+
+ fun entryBindStageParamsNullOnUnbind(entry: NotificationEntry) {
+ buffer.log(TAG, INFO, {
+ str1 = entry.logKey
+ }, {
+ "heads up entry bind stage params null on unbind $str1 "
+ })
+ }
}
private const val TAG = "HeadsUpViewBinder"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java
index 7c41800..d626c18 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java
@@ -21,6 +21,7 @@
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -64,7 +65,7 @@
* Get the stage parameters for the entry. Clients should use this to modify how the stage
* handles the notification content.
*/
- public final Params getStageParams(@NonNull NotificationEntry entry) {
+ public final @NonNull Params getStageParams(@NonNull NotificationEntry entry) {
Params params = mContentParams.get(entry);
if (params == null) {
// TODO: This should throw an exception but there are some cases of re-entrant calls
@@ -79,6 +80,17 @@
return params;
}
+ // TODO(b/253081345): Remove this method.
+ /**
+ * Get the stage parameters for the entry, or null if there are no stage parameters for the
+ * entry.
+ *
+ * @see #getStageParams(NotificationEntry)
+ */
+ public final @Nullable Params tryGetStageParams(@NonNull NotificationEntry entry) {
+ return mContentParams.get(entry);
+ }
+
/**
* Create a params entry for the notification for this stage.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
index 5b2d695..2f0ebf7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
@@ -35,8 +35,8 @@
protected val controller: UserSwitcherController,
) : BaseAdapter() {
- protected open val users: ArrayList<UserRecord>
- get() = controller.users
+ protected open val users: List<UserRecord>
+ get() = controller.users.filter { !controller.isKeyguardShowing || !it.isRestricted }
init {
controller.addAdapter(WeakReference(this))
@@ -112,6 +112,7 @@
item.isGuest,
item.isAddSupervisedUser,
isTablet,
+ item.isManageUsers,
)
return checkNotNull(context.getDrawable(iconRes))
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
index 494a4bb..c150654 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
@@ -53,6 +53,7 @@
import com.android.systemui.util.ViewController;
import java.util.ArrayList;
+import java.util.List;
import javax.inject.Inject;
@@ -456,7 +457,7 @@
}
void refreshUserOrder() {
- ArrayList<UserRecord> users = super.getUsers();
+ List<UserRecord> users = super.getUsers();
mUsersOrdered = new ArrayList<>(users.size());
for (int i = 0; i < users.size(); i++) {
UserRecord record = users.get(i);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
index af39eee..935fc7f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
@@ -249,7 +249,7 @@
override fun startActivity(intent: Intent) {
if (useInteractor) {
- activityStarter.startActivity(intent, /* dismissShade= */ false)
+ activityStarter.startActivity(intent, /* dismissShade= */ true)
} else {
_oldImpl.startActivity(intent)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
index 46d2f3a..c294c37 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
@@ -49,6 +49,7 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.util.LatencyTracker;
+import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.settingslib.users.UserCreatingDialog;
import com.android.systemui.GuestResetOrExitSessionReceiver;
import com.android.systemui.GuestResumeSessionReceiver;
@@ -399,6 +400,16 @@
records.add(userRecord);
}
+ if (canManageUsers()) {
+ records.add(LegacyUserDataHelper.createRecord(
+ mContext,
+ KeyguardUpdateMonitor.getCurrentUser(),
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ /* isRestricted= */ false,
+ /* isSwitchToEnabled= */ true
+ ));
+ }
+
mUiExecutor.execute(() -> {
if (records != null) {
mUsers = records;
@@ -438,6 +449,14 @@
&& mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY);
}
+ @VisibleForTesting
+ boolean canManageUsers() {
+ UserInfo currentUser = mUserTracker.getUserInfo();
+ return mUserSwitcherEnabled
+ && ((currentUser != null && currentUser.isAdmin())
+ || mAddUsersFromLockScreen.getValue());
+ }
+
private boolean createIsRestricted() {
return !mAddUsersFromLockScreen.getValue();
}
@@ -525,6 +544,8 @@
showAddUserDialog(dialogShower);
} else if (record.isAddSupervisedUser) {
startSupervisedUserActivity();
+ } else if (record.isManageUsers) {
+ startActivity(new Intent(Settings.ACTION_USER_SETTINGS));
} else {
onUserListItemClicked(record.info.id, record, dialogShower);
}
@@ -984,7 +1005,7 @@
@Override
public void startActivity(Intent intent) {
- mActivityStarter.startActivity(intent, true);
+ mActivityStarter.startActivity(intent, /* dismissShade= */ true);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt
index 108ab43..7f1195b 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt
@@ -16,426 +16,41 @@
package com.android.systemui.user
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.graphics.drawable.BitmapDrawable
-import android.graphics.drawable.Drawable
-import android.graphics.drawable.GradientDrawable
-import android.graphics.drawable.LayerDrawable
import android.os.Bundle
-import android.os.UserManager
-import android.provider.Settings
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.MotionEvent
import android.view.View
-import android.view.ViewGroup
-import android.widget.AdapterView
-import android.widget.ArrayAdapter
-import android.widget.ImageView
-import android.widget.TextView
-import android.window.OnBackInvokedCallback
-import android.window.OnBackInvokedDispatcher
import androidx.activity.ComponentActivity
-import androidx.constraintlayout.helper.widget.Flow
import androidx.lifecycle.ViewModelProvider
-import com.android.internal.annotations.VisibleForTesting
-import com.android.internal.util.UserIcons
-import com.android.settingslib.Utils
-import com.android.systemui.Gefingerpoken
import com.android.systemui.R
-import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
-import com.android.systemui.settings.UserTracker
-import com.android.systemui.statusbar.policy.BaseUserSwitcherAdapter
-import com.android.systemui.statusbar.policy.UserSwitcherController
-import com.android.systemui.user.data.source.UserRecord
import com.android.systemui.user.ui.binder.UserSwitcherViewBinder
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
import dagger.Lazy
import javax.inject.Inject
-import kotlin.math.ceil
-
-private const val USER_VIEW = "user_view"
/** Support a fullscreen user switcher */
open class UserSwitcherActivity
@Inject
constructor(
- private val userSwitcherController: UserSwitcherController,
- private val broadcastDispatcher: BroadcastDispatcher,
private val falsingCollector: FalsingCollector,
- private val falsingManager: FalsingManager,
- private val userManager: UserManager,
- private val userTracker: UserTracker,
- private val flags: FeatureFlags,
private val viewModelFactory: Lazy<UserSwitcherViewModel.Factory>,
) : ComponentActivity() {
- private lateinit var parent: UserSwitcherRootView
- private lateinit var broadcastReceiver: BroadcastReceiver
- private var popupMenu: UserSwitcherPopupMenu? = null
- private lateinit var addButton: View
- private var addUserRecords = mutableListOf<UserRecord>()
- private val onBackCallback = OnBackInvokedCallback { finish() }
- private val userSwitchedCallback: UserTracker.Callback =
- object : UserTracker.Callback {
- override fun onUserChanged(newUser: Int, userContext: Context) {
- finish()
- }
- }
- // When the add users options become available, insert another option to manage users
- private val manageUserRecord =
- UserRecord(
- null /* info */,
- null /* picture */,
- false /* isGuest */,
- false /* isCurrent */,
- false /* isAddUser */,
- false /* isRestricted */,
- false /* isSwitchToEnabled */,
- false /* isAddSupervisedUser */
- )
-
- private val adapter: UserAdapter by lazy { UserAdapter() }
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- createActivity()
- }
-
- @VisibleForTesting
- fun createActivity() {
setContentView(R.layout.user_switcher_fullscreen)
window.decorView.systemUiVisibility =
(View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
- if (isUsingModernArchitecture()) {
- Log.d(TAG, "Using modern architecture.")
- val viewModel =
- ViewModelProvider(this, viewModelFactory.get())[UserSwitcherViewModel::class.java]
- UserSwitcherViewBinder.bind(
- view = requireViewById(R.id.user_switcher_root),
- viewModel = viewModel,
- lifecycleOwner = this,
- layoutInflater = layoutInflater,
- falsingCollector = falsingCollector,
- onFinish = this::finish,
- )
- return
- } else {
- Log.d(TAG, "Not using modern architecture.")
- }
-
- parent = requireViewById<UserSwitcherRootView>(R.id.user_switcher_root)
-
- parent.touchHandler =
- object : Gefingerpoken {
- override fun onTouchEvent(ev: MotionEvent?): Boolean {
- falsingCollector.onTouchEvent(ev)
- return false
- }
- }
-
- requireViewById<View>(R.id.cancel).apply { setOnClickListener { _ -> finish() } }
-
- addButton =
- requireViewById<View>(R.id.add).apply { setOnClickListener { _ -> showPopupMenu() } }
-
- onBackInvokedDispatcher.registerOnBackInvokedCallback(
- OnBackInvokedDispatcher.PRIORITY_DEFAULT,
- onBackCallback
+ val viewModel =
+ ViewModelProvider(this, viewModelFactory.get())[UserSwitcherViewModel::class.java]
+ UserSwitcherViewBinder.bind(
+ view = requireViewById(R.id.user_switcher_root),
+ viewModel = viewModel,
+ lifecycleOwner = this,
+ layoutInflater = layoutInflater,
+ falsingCollector = falsingCollector,
+ onFinish = this::finish,
)
-
- userSwitcherController.init(parent)
- initBroadcastReceiver()
-
- parent.post { buildUserViews() }
- userTracker.addCallback(userSwitchedCallback, mainExecutor)
- }
-
- private fun showPopupMenu() {
- val items = mutableListOf<UserRecord>()
- addUserRecords.forEach { items.add(it) }
-
- var popupMenuAdapter =
- ItemAdapter(
- this,
- R.layout.user_switcher_fullscreen_popup_item,
- layoutInflater,
- { item: UserRecord -> adapter.getName(this@UserSwitcherActivity, item, true) },
- { item: UserRecord ->
- adapter.findUserIcon(item, true).mutate().apply {
- setTint(
- resources.getColor(
- R.color.user_switcher_fullscreen_popup_item_tint,
- getTheme()
- )
- )
- }
- }
- )
- popupMenuAdapter.addAll(items)
-
- popupMenu =
- UserSwitcherPopupMenu(this).apply {
- setAnchorView(addButton)
- setAdapter(popupMenuAdapter)
- setOnItemClickListener { parent: AdapterView<*>, view: View, pos: Int, id: Long ->
- if (falsingManager.isFalseTap(LOW_PENALTY) || !view.isEnabled()) {
- return@setOnItemClickListener
- }
- // -1 for the header
- val item = popupMenuAdapter.getItem(pos - 1)
- if (item == manageUserRecord) {
- val i = Intent().setAction(Settings.ACTION_USER_SETTINGS)
- this@UserSwitcherActivity.startActivity(i)
- } else {
- adapter.onUserListItemClicked(item)
- }
-
- dismiss()
- popupMenu = null
-
- if (!item.isAddUser) {
- this@UserSwitcherActivity.finish()
- }
- }
-
- show()
- }
- }
-
- private fun buildUserViews() {
- var count = 0
- var start = 0
- for (i in 0 until parent.getChildCount()) {
- if (parent.getChildAt(i).getTag() == USER_VIEW) {
- if (count == 0) start = i
- count++
- }
- }
- parent.removeViews(start, count)
- addUserRecords.clear()
- val flow = requireViewById<Flow>(R.id.flow)
- val totalWidth = parent.width
- val userViewCount = adapter.getTotalUserViews()
- val maxColumns = getMaxColumns(userViewCount)
- val horizontalGap =
- resources.getDimensionPixelSize(R.dimen.user_switcher_fullscreen_horizontal_gap)
- val totalWidthOfHorizontalGap = (maxColumns - 1) * horizontalGap
- val maxWidgetDiameter = (totalWidth - totalWidthOfHorizontalGap) / maxColumns
-
- flow.setMaxElementsWrap(maxColumns)
-
- for (i in 0 until adapter.getCount()) {
- val item = adapter.getItem(i)
- if (adapter.doNotRenderUserView(item)) {
- addUserRecords.add(item)
- } else {
- val userView = adapter.getView(i, null, parent)
- userView.requireViewById<ImageView>(R.id.user_switcher_icon).apply {
- val lp = layoutParams
- if (maxWidgetDiameter < lp.width) {
- lp.width = maxWidgetDiameter
- lp.height = maxWidgetDiameter
- layoutParams = lp
- }
- }
-
- userView.setId(View.generateViewId())
- parent.addView(userView)
-
- // Views must have an id and a parent in order for Flow to lay them out
- flow.addView(userView)
-
- userView.setOnClickListener { v ->
- if (falsingManager.isFalseTap(LOW_PENALTY) || !v.isEnabled()) {
- return@setOnClickListener
- }
-
- if (!item.isCurrent || item.isGuest) {
- adapter.onUserListItemClicked(item)
- }
- }
- }
- }
-
- if (!addUserRecords.isEmpty()) {
- addUserRecords.add(manageUserRecord)
- addButton.visibility = View.VISIBLE
- } else {
- addButton.visibility = View.GONE
- }
- }
-
- override fun onBackPressed() {
- if (isUsingModernArchitecture()) {
- return super.onBackPressed()
- }
-
- finish()
- }
-
- override fun onDestroy() {
- super.onDestroy()
- if (isUsingModernArchitecture()) {
- return
- }
- destroyActivity()
- }
-
- @VisibleForTesting
- fun destroyActivity() {
- onBackInvokedDispatcher.unregisterOnBackInvokedCallback(onBackCallback)
- broadcastDispatcher.unregisterReceiver(broadcastReceiver)
- userTracker.removeCallback(userSwitchedCallback)
- }
-
- private fun initBroadcastReceiver() {
- broadcastReceiver =
- object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- val action = intent.getAction()
- if (Intent.ACTION_SCREEN_OFF.equals(action)) {
- finish()
- }
- }
- }
-
- val filter = IntentFilter()
- filter.addAction(Intent.ACTION_SCREEN_OFF)
- broadcastDispatcher.registerReceiver(broadcastReceiver, filter)
- }
-
- @VisibleForTesting
- fun getMaxColumns(userCount: Int): Int {
- return if (userCount < 5) 4 else ceil(userCount / 2.0).toInt()
- }
-
- private fun isUsingModernArchitecture(): Boolean {
- return flags.isEnabled(Flags.MODERN_USER_SWITCHER_ACTIVITY)
- }
-
- /** Provides views to populate the option menu. */
- private class ItemAdapter(
- val parentContext: Context,
- val resource: Int,
- val layoutInflater: LayoutInflater,
- val textGetter: (UserRecord) -> String,
- val iconGetter: (UserRecord) -> Drawable
- ) : ArrayAdapter<UserRecord>(parentContext, resource) {
-
- override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
- val item = getItem(position)
- val view = convertView ?: layoutInflater.inflate(resource, parent, false)
-
- view.requireViewById<ImageView>(R.id.icon).apply { setImageDrawable(iconGetter(item)) }
- view.requireViewById<TextView>(R.id.text).apply { setText(textGetter(item)) }
-
- return view
- }
- }
-
- private inner class UserAdapter : BaseUserSwitcherAdapter(userSwitcherController) {
- override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
- val item = getItem(position)
- var view = convertView as ViewGroup?
- if (view == null) {
- view =
- layoutInflater.inflate(R.layout.user_switcher_fullscreen_item, parent, false)
- as ViewGroup
- }
- (view.getChildAt(0) as ImageView).apply { setImageDrawable(getDrawable(item)) }
- (view.getChildAt(1) as TextView).apply { setText(getName(getContext(), item)) }
-
- view.setEnabled(item.isSwitchToEnabled)
- UserSwitcherController.setSelectableAlpha(view)
- view.setTag(USER_VIEW)
- return view
- }
-
- override fun getName(context: Context, item: UserRecord, isTablet: Boolean): String {
- return if (item == manageUserRecord) {
- getString(R.string.manage_users)
- } else {
- super.getName(context, item, isTablet)
- }
- }
-
- fun findUserIcon(item: UserRecord, isTablet: Boolean = false): Drawable {
- if (item == manageUserRecord) {
- return getDrawable(R.drawable.ic_manage_users)
- }
- if (item.info == null) {
- return getIconDrawable(this@UserSwitcherActivity, item, isTablet)
- }
- val userIcon = userManager.getUserIcon(item.info.id)
- if (userIcon != null) {
- return BitmapDrawable(userIcon)
- }
- return UserIcons.getDefaultUserIcon(resources, item.info.id, false)
- }
-
- fun getTotalUserViews(): Int {
- return users.count { item -> !doNotRenderUserView(item) }
- }
-
- fun doNotRenderUserView(item: UserRecord): Boolean {
- return item.isAddUser || item.isAddSupervisedUser || item.isGuest && item.info == null
- }
-
- private fun getDrawable(item: UserRecord): Drawable {
- var drawable =
- if (item.isGuest) {
- getDrawable(R.drawable.ic_account_circle)
- } else {
- findUserIcon(item)
- }
- drawable.mutate()
-
- if (!item.isCurrent && !item.isSwitchToEnabled) {
- drawable.setTint(
- resources.getColor(
- R.color.kg_user_switcher_restricted_avatar_icon_color,
- getTheme()
- )
- )
- }
-
- val ld = getDrawable(R.drawable.user_switcher_icon_large).mutate() as LayerDrawable
- if (item == userSwitcherController.currentUserRecord) {
- (ld.findDrawableByLayerId(R.id.ring) as GradientDrawable).apply {
- val stroke =
- resources.getDimensionPixelSize(R.dimen.user_switcher_icon_selected_width)
- val color =
- Utils.getColorAttrDefaultColor(
- this@UserSwitcherActivity,
- com.android.internal.R.attr.colorAccentPrimary
- )
-
- setStroke(stroke, color)
- }
- }
-
- ld.setDrawableByLayerId(R.id.user_avatar, drawable)
- return ld
- }
-
- override fun notifyDataSetChanged() {
- super.notifyDataSetChanged()
- buildUserViews()
- }
- }
-
- companion object {
- private const val TAG = "UserSwitcherActivity"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
index 9370286..d4fb563 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
@@ -47,6 +47,9 @@
* If not disabled, this is `null`.
*/
@JvmField val enforcedAdmin: RestrictedLockUtils.EnforcedAdmin? = null,
+
+ /** Whether this record is to go to the Settings page to manage users. */
+ @JvmField val isManageUsers: Boolean = false
) {
/** Returns a new instance of [UserRecord] with its [isCurrent] set to the given value. */
fun copyWithIsCurrent(isCurrent: Boolean): UserRecord {
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt
index 1b4746a..dc004f3 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt
@@ -82,6 +82,15 @@
)
}
+ fun canManageUsers(
+ repository: UserRepository,
+ isUserSwitcherEnabled: Boolean,
+ isAddUsersFromLockScreenEnabled: Boolean,
+ ): Boolean {
+ return isUserSwitcherEnabled &&
+ (repository.getSelectedUserInfo().isAdmin || isAddUsersFromLockScreenEnabled)
+ }
+
/**
* Returns `true` if the current user is allowed to add users to the device; `false` otherwise.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
index 142a328..ba5a82a 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
@@ -102,6 +102,7 @@
interface UserCallback {
/** Returns `true` if this callback can be cleaned-up. */
fun isEvictable(): Boolean = false
+
/** Notifies that the state of users on the device has changed. */
fun onUserStateChanged()
}
@@ -164,10 +165,11 @@
get() =
if (isNewImpl) {
combine(
+ repository.selectedUserInfo,
repository.userInfos,
repository.userSwitcherSettings,
keyguardInteractor.isKeyguardShowing,
- ) { userInfos, settings, isDeviceLocked ->
+ ) { _, userInfos, settings, isDeviceLocked ->
buildList {
val hasGuestUser = userInfos.any { it.isGuest }
if (
@@ -183,35 +185,45 @@
add(UserActionModel.ENTER_GUEST_MODE)
}
- if (isDeviceLocked && !settings.isAddUsersFromLockscreen) {
+ if (!isDeviceLocked || settings.isAddUsersFromLockscreen) {
// The device is locked and our setting to allow actions that add users
// from the lock-screen is not enabled. The guest action from above is
// always allowed, even when the device is locked, but the various "add
// user" actions below are not. We can finish building the list here.
- return@buildList
+
+ val canCreateUsers =
+ UserActionsUtil.canCreateUser(
+ manager,
+ repository,
+ settings.isUserSwitcherEnabled,
+ settings.isAddUsersFromLockscreen,
+ )
+
+ if (canCreateUsers) {
+ add(UserActionModel.ADD_USER)
+ }
+
+ if (
+ UserActionsUtil.canCreateSupervisedUser(
+ manager,
+ repository,
+ settings.isUserSwitcherEnabled,
+ settings.isAddUsersFromLockscreen,
+ supervisedUserPackageName,
+ )
+ ) {
+ add(UserActionModel.ADD_SUPERVISED_USER)
+ }
}
if (
- UserActionsUtil.canCreateUser(
- manager,
+ UserActionsUtil.canManageUsers(
repository,
settings.isUserSwitcherEnabled,
settings.isAddUsersFromLockscreen,
)
) {
- add(UserActionModel.ADD_USER)
- }
-
- if (
- UserActionsUtil.canCreateSupervisedUser(
- manager,
- repository,
- settings.isUserSwitcherEnabled,
- settings.isAddUsersFromLockscreen,
- supervisedUserPackageName,
- )
- ) {
- add(UserActionModel.ADD_SUPERVISED_USER)
+ add(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
}
}
}
@@ -264,7 +276,10 @@
toRecord(
action = it,
selectedUserId = selectedUserInfo.id,
- isAddFromLockscreenEnabled = settings.isAddUsersFromLockscreen,
+ isRestricted =
+ it != UserActionModel.ENTER_GUEST_MODE &&
+ it != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT &&
+ !settings.isAddUsersFromLockscreen,
)
}
)
@@ -482,12 +497,12 @@
.setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
.setPackage(supervisedUserPackageName)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
- /* dismissShade= */ false,
+ /* dismissShade= */ true,
)
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
activityStarter.startActivity(
Intent(Settings.ACTION_USER_SETTINGS),
- /* dismissShade= */ false,
+ /* dismissShade= */ true,
)
}
} else {
@@ -575,20 +590,13 @@
private suspend fun toRecord(
action: UserActionModel,
selectedUserId: Int,
- isAddFromLockscreenEnabled: Boolean,
+ isRestricted: Boolean,
): UserRecord {
return LegacyUserDataHelper.createRecord(
context = applicationContext,
selectedUserId = selectedUserId,
actionType = action,
- isRestricted =
- if (action == UserActionModel.ENTER_GUEST_MODE) {
- // Entering guest mode is never restricted, so it's allowed to happen from the
- // lockscreen even if the "add from lockscreen" system setting is off.
- false
- } else {
- !isAddFromLockscreenEnabled
- },
+ isRestricted = isRestricted,
isSwitchToEnabled =
canSwitchUsers(selectedUserId) &&
// If the user is auto-created is must not be currently resetting.
diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
index 137de15..03a7470 100644
--- a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
@@ -80,6 +80,7 @@
context = context,
selectedUserId = selectedUserId,
),
+ isManageUsers = actionType == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
}
@@ -90,6 +91,7 @@
record.isAddUser -> UserActionModel.ADD_USER
record.isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER
record.isGuest -> UserActionModel.ENTER_GUEST_MODE
+ record.isManageUsers -> UserActionModel.NAVIGATE_TO_USER_MANAGEMENT
else -> error("Not a known action: $record")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt
index 15fdc35..e74232d 100644
--- a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt
@@ -22,7 +22,6 @@
import androidx.annotation.StringRes
import com.android.systemui.R
import com.android.systemui.user.data.source.UserRecord
-import kotlin.math.ceil
/**
* Defines utility functions for helping with legacy UI code for users.
@@ -33,16 +32,6 @@
*/
object LegacyUserUiHelper {
- /** Returns the maximum number of columns for user items in the user switcher. */
- fun getMaxUserSwitcherItemColumns(userCount: Int): Int {
- // TODO(b/243844097): remove this once we remove the old user switcher implementation.
- return if (userCount < 5) {
- 4
- } else {
- ceil(userCount / 2.0).toInt()
- }
- }
-
@JvmStatic
@DrawableRes
fun getUserSwitcherActionIconResourceId(
@@ -50,6 +39,7 @@
isGuest: Boolean,
isAddSupervisedUser: Boolean,
isTablet: Boolean = false,
+ isManageUsers: Boolean,
): Int {
return if (isAddUser && isTablet) {
R.drawable.ic_account_circle_filled
@@ -59,6 +49,8 @@
R.drawable.ic_account_circle
} else if (isAddSupervisedUser) {
R.drawable.ic_add_supervised_user
+ } else if (isManageUsers) {
+ R.drawable.ic_manage_users
} else {
R.drawable.ic_avatar_user
}
@@ -85,6 +77,7 @@
isAddUser = record.isAddUser,
isAddSupervisedUser = record.isAddSupervisedUser,
isTablet = isTablet,
+ isManageUsers = record.isManageUsers,
)
)
}
@@ -114,8 +107,9 @@
isAddUser: Boolean,
isAddSupervisedUser: Boolean,
isTablet: Boolean = false,
+ isManageUsers: Boolean,
): Int {
- check(isGuest || isAddUser || isAddSupervisedUser)
+ check(isGuest || isAddUser || isAddSupervisedUser || isManageUsers)
return when {
isGuest && isGuestUserAutoCreated && isGuestUserResetting ->
@@ -125,6 +119,7 @@
isGuest -> com.android.internal.R.string.guest_name
isAddUser -> com.android.settingslib.R.string.user_add_user
isAddSupervisedUser -> R.string.add_user_supervised
+ isManageUsers -> R.string.manage_users
else -> error("This should never happen!")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
index 5b83df7..219dae2 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
@@ -19,7 +19,6 @@
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
-import com.android.systemui.R
import com.android.systemui.common.ui.drawable.CircularDrawable
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
@@ -30,6 +29,7 @@
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
import javax.inject.Inject
+import kotlin.math.ceil
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
@@ -52,8 +52,7 @@
userInteractor.users.map { models -> models.map { user -> toViewModel(user) } }
/** The maximum number of columns that the user selection grid should use. */
- val maximumUserColumns: Flow<Int> =
- users.map { LegacyUserUiHelper.getMaxUserSwitcherItemColumns(it.size) }
+ val maximumUserColumns: Flow<Int> = users.map { getMaxUserSwitcherItemColumns(it.size) }
private val _isMenuVisible = MutableStateFlow(false)
/**
@@ -118,6 +117,15 @@
_isMenuVisible.value = false
}
+ /** Returns the maximum number of columns for user items in the user switcher. */
+ private fun getMaxUserSwitcherItemColumns(userCount: Int): Int {
+ return if (userCount < 5) {
+ 4
+ } else {
+ ceil(userCount / 2.0).toInt()
+ }
+ }
+
private fun createFinishRequestedFlow(): Flow<Boolean> {
var mostRecentSelectedUserId: Int? = null
var mostRecentIsInteractive: Boolean? = null
@@ -171,27 +179,23 @@
return UserActionViewModel(
viewKey = model.ordinal.toLong(),
iconResourceId =
- if (model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) {
- R.drawable.ic_manage_users
- } else {
- LegacyUserUiHelper.getUserSwitcherActionIconResourceId(
- isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
- isAddUser = model == UserActionModel.ADD_USER,
- isGuest = model == UserActionModel.ENTER_GUEST_MODE,
- )
- },
+ LegacyUserUiHelper.getUserSwitcherActionIconResourceId(
+ isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
+ isAddUser = model == UserActionModel.ADD_USER,
+ isGuest = model == UserActionModel.ENTER_GUEST_MODE,
+ isManageUsers = model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ isTablet = true,
+ ),
textResourceId =
- if (model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) {
- R.string.manage_users
- } else {
- LegacyUserUiHelper.getUserSwitcherActionTextResourceId(
- isGuest = model == UserActionModel.ENTER_GUEST_MODE,
- isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated,
- isGuestUserResetting = guestUserInteractor.isGuestUserResetting,
- isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
- isAddUser = model == UserActionModel.ADD_USER,
- )
- },
+ LegacyUserUiHelper.getUserSwitcherActionTextResourceId(
+ isGuest = model == UserActionModel.ENTER_GUEST_MODE,
+ isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated,
+ isGuestUserResetting = guestUserInteractor.isGuestUserResetting,
+ isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
+ isAddUser = model == UserActionModel.ADD_USER,
+ isManageUsers = model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ isTablet = true,
+ ),
onClicked = {
userInteractor.executeAction(action = model)
// We don't finish because we want to show a dialog over the full-screen UI and
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index c6ebaa8..48e8239 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -221,15 +221,17 @@
public void onResourcesUpdate_callsThroughOnRotationChange() {
// Rotation is the same, shouldn't cause an update
mKeyguardSecurityContainerController.updateResources();
- verify(mView, never()).initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ verify(mView, never()).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager),
+ eq(mUserSwitcherController),
+ any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class));
// Update rotation. Should trigger update
mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
mKeyguardSecurityContainerController.updateResources();
- verify(mView).initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager),
+ eq(mUserSwitcherController),
+ any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class));
}
private void touchDown() {
@@ -263,8 +265,9 @@
.thenReturn((KeyguardInputViewController) mKeyguardPasswordViewController);
mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Pattern);
- verify(mView).initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager),
+ eq(mUserSwitcherController),
+ any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class));
}
@Test
@@ -275,8 +278,9 @@
.thenReturn((KeyguardInputViewController) mKeyguardPasswordViewController);
mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Pattern);
- verify(mView).initMode(MODE_ONE_HANDED, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ verify(mView).initMode(eq(MODE_ONE_HANDED), eq(mGlobalSettings), eq(mFalsingManager),
+ eq(mUserSwitcherController),
+ any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class));
}
@Test
@@ -285,8 +289,26 @@
setupGetSecurityView();
mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Password);
- verify(mView).initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager),
+ eq(mUserSwitcherController),
+ any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class));
+ }
+
+ @Test
+ public void addUserSwitcherCallback() {
+ ArgumentCaptor<KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback>
+ captor = ArgumentCaptor.forClass(
+ KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class);
+
+ setupGetSecurityView();
+
+ mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Password);
+ verify(mView).initMode(anyInt(), any(GlobalSettings.class), any(FalsingManager.class),
+ any(UserSwitcherController.class),
+ captor.capture());
+ captor.getValue().showUnlockToContinueMessage();
+ verify(mKeyguardPasswordViewControllerMock).showMessage(
+ getContext().getString(R.string.keyguard_unlock_to_continue), null);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 52f8825..82d3ca7 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -119,7 +119,7 @@
int systemBarInsetAmount = 0;
mKeyguardSecurityContainer.initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ mUserSwitcherController, () -> {});
Insets imeInset = Insets.of(0, 0, 0, imeInsetAmount);
Insets systemBarInset = Insets.of(0, 0, 0, systemBarInsetAmount);
@@ -141,7 +141,7 @@
int systemBarInsetAmount = paddingBottom + 1;
mKeyguardSecurityContainer.initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ mUserSwitcherController, () -> {});
Insets imeInset = Insets.of(0, 0, 0, imeInsetAmount);
Insets systemBarInset = Insets.of(0, 0, 0, systemBarInsetAmount);
@@ -158,9 +158,10 @@
@Test
public void testDefaultViewMode() {
mKeyguardSecurityContainer.initMode(MODE_ONE_HANDED, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ mUserSwitcherController, () -> {
+ });
mKeyguardSecurityContainer.initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ mUserSwitcherController, () -> {});
ConstraintSet.Constraint viewFlipperConstraint =
getViewConstraint(mSecurityViewFlipper.getId());
assertThat(viewFlipperConstraint.layout.topToTop).isEqualTo(PARENT_ID);
@@ -377,7 +378,7 @@
private void setupUserSwitcher() {
when(mGlobalSettings.getInt(any(), anyInt())).thenReturn(ONE_HANDED_KEYGUARD_SIDE_RIGHT);
mKeyguardSecurityContainer.initMode(KeyguardSecurityContainer.MODE_USER_SWITCHER,
- mGlobalSettings, mFalsingManager, mUserSwitcherController);
+ mGlobalSettings, mFalsingManager, mUserSwitcherController, () -> {});
}
private ArrayList<UserRecord> buildUserRecords(int count) {
@@ -387,7 +388,8 @@
0 /* flags */);
users.add(new UserRecord(info, null, false /* isGuest */, false /* isCurrent */,
false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */,
- false /* isAddSupervisedUser */, null /* enforcedAdmin */));
+ false /* isAddSupervisedUser */, null /* enforcedAdmin */,
+ false /* isManageUsers */));
}
return users;
}
@@ -395,7 +397,7 @@
private void setupForUpdateKeyguardPosition(boolean oneHandedMode) {
int mode = oneHandedMode ? MODE_ONE_HANDED : MODE_DEFAULT;
mKeyguardSecurityContainer.initMode(mode, mGlobalSettings, mFalsingManager,
- mUserSwitcherController);
+ mUserSwitcherController, () -> {});
}
/** Get the ConstraintLayout constraint of the view. */
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index d52612b..f2ae7a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -35,6 +35,8 @@
import android.view.WindowManager
import android.widget.ScrollView
import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
+import com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.R
@@ -143,6 +145,35 @@
}
@Test
+ fun testDismissesOnFocusLoss_hidesKeyboardWhenVisible() {
+ val container = initializeFingerprintContainer(
+ authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL
+ )
+ waitForIdleSync()
+
+ val requestID = authContainer?.requestId ?: 0L
+
+ // Simulate keyboard was shown on the credential view
+ val windowInsetsController = container.windowInsetsController
+ spyOn(windowInsetsController)
+ spyOn(container.rootWindowInsets)
+ doReturn(true).`when`(container.rootWindowInsets).isVisible(WindowInsets.Type.ime())
+
+ container.onWindowFocusChanged(false)
+ waitForIdleSync()
+
+ // Expect hiding IME request will be invoked when dismissing the view
+ verify(windowInsetsController)?.hide(WindowInsets.Type.ime())
+
+ verify(callback).onDismissed(
+ eq(AuthDialogCallback.DISMISSED_USER_CANCELED),
+ eq<ByteArray?>(null), /* credentialAttestation */
+ eq(requestID)
+ )
+ assertThat(container.parent).isNull()
+ }
+
+ @Test
fun testActionAuthenticated_sendsDismissedAuthenticated() {
val container = initializeFingerprintContainer()
container.mBiometricCallback.onAction(
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 91214a8..e7e6918 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
@@ -38,6 +38,8 @@
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.util.DeviceConfigProxyFake;
import org.junit.Before;
@@ -47,6 +49,9 @@
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+import javax.inject.Provider;
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -55,11 +60,15 @@
@Mock
private ClipboardManager mClipboardManager;
@Mock
- private ClipboardOverlayControllerFactory mClipboardOverlayControllerFactory;
+ private ClipboardOverlayControllerLegacyFactory mClipboardOverlayControllerLegacyFactory;
+ @Mock
+ private ClipboardOverlayControllerLegacy mOverlayControllerLegacy;
@Mock
private ClipboardOverlayController mOverlayController;
@Mock
private UiEventLogger mUiEventLogger;
+ @Mock
+ private FeatureFlags mFeatureFlags;
private DeviceConfigProxyFake mDeviceConfigProxy;
private ClipData mSampleClipData;
@@ -72,12 +81,17 @@
@Captor
private ArgumentCaptor<String> mStringCaptor;
+ @Spy
+ private Provider<ClipboardOverlayController> mOverlayControllerProvider;
+
@Before
public void setup() {
+ mOverlayControllerProvider = () -> mOverlayController;
+
MockitoAnnotations.initMocks(this);
- when(mClipboardOverlayControllerFactory.create(any())).thenReturn(
- mOverlayController);
+ when(mClipboardOverlayControllerLegacyFactory.create(any()))
+ .thenReturn(mOverlayControllerLegacy);
when(mClipboardManager.hasPrimaryClip()).thenReturn(true);
@@ -94,7 +108,8 @@
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
"false", false);
ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger);
+ mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
+ mClipboardManager, mUiEventLogger, mFeatureFlags);
listener.start();
verifyZeroInteractions(mClipboardManager);
verifyZeroInteractions(mUiEventLogger);
@@ -105,7 +120,8 @@
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
"true", false);
ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger);
+ mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
+ mClipboardManager, mUiEventLogger, mFeatureFlags);
listener.start();
verify(mClipboardManager).addPrimaryClipChangedListener(any());
verifyZeroInteractions(mUiEventLogger);
@@ -113,16 +129,58 @@
@Test
public void test_consecutiveCopies() {
+ when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false);
+
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
"true", false);
ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger);
+ mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
+ mClipboardManager, mUiEventLogger, mFeatureFlags);
listener.start();
listener.onPrimaryClipChanged();
- verify(mClipboardOverlayControllerFactory).create(any());
+ verify(mClipboardOverlayControllerLegacyFactory).create(any());
- verify(mOverlayController).setClipData(mClipDataCaptor.capture(), mStringCaptor.capture());
+ verify(mOverlayControllerLegacy).setClipData(
+ mClipDataCaptor.capture(), mStringCaptor.capture());
+
+ assertEquals(mSampleClipData, mClipDataCaptor.getValue());
+ assertEquals(mSampleSource, mStringCaptor.getValue());
+
+ verify(mOverlayControllerLegacy).setOnSessionCompleteListener(mRunnableCaptor.capture());
+
+ // Should clear the overlay controller
+ mRunnableCaptor.getValue().run();
+
+ listener.onPrimaryClipChanged();
+
+ verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any());
+
+ // Not calling the runnable here, just change the clip again and verify that the overlay is
+ // NOT recreated.
+
+ listener.onPrimaryClipChanged();
+
+ verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any());
+ verifyZeroInteractions(mOverlayControllerProvider);
+ }
+
+ @Test
+ public void test_consecutiveCopies_new() {
+ when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(true);
+
+ mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
+ "true", false);
+ ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
+ mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
+ mClipboardManager, mUiEventLogger, mFeatureFlags);
+ listener.start();
+ listener.onPrimaryClipChanged();
+
+ verify(mOverlayControllerProvider).get();
+
+ verify(mOverlayController).setClipData(
+ mClipDataCaptor.capture(), mStringCaptor.capture());
assertEquals(mSampleClipData, mClipDataCaptor.getValue());
assertEquals(mSampleSource, mStringCaptor.getValue());
@@ -134,14 +192,15 @@
listener.onPrimaryClipChanged();
- verify(mClipboardOverlayControllerFactory, times(2)).create(any());
+ verify(mOverlayControllerProvider, times(2)).get();
// Not calling the runnable here, just change the clip again and verify that the overlay is
// NOT recreated.
listener.onPrimaryClipChanged();
- verify(mClipboardOverlayControllerFactory, times(2)).create(any());
+ verify(mOverlayControllerProvider, times(2)).get();
+ verifyZeroInteractions(mClipboardOverlayControllerLegacyFactory);
}
@Test
@@ -169,4 +228,40 @@
assertTrue(ClipboardListener.shouldSuppressOverlay(suppressableClipData,
ClipboardListener.SHELL_PACKAGE, false));
}
+
+ @Test
+ public void test_logging_enterAndReenter() {
+ when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false);
+
+ ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
+ mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
+ mClipboardManager, mUiEventLogger, mFeatureFlags);
+ listener.start();
+
+ listener.onPrimaryClipChanged();
+ listener.onPrimaryClipChanged();
+
+ verify(mUiEventLogger, times(1)).log(
+ ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource);
+ verify(mUiEventLogger, times(1)).log(
+ ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource);
+ }
+
+ @Test
+ public void test_logging_enterAndReenter_new() {
+ when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(true);
+
+ ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
+ mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
+ mClipboardManager, mUiEventLogger, mFeatureFlags);
+ listener.start();
+
+ listener.onPrimaryClipChanged();
+ listener.onPrimaryClipChanged();
+
+ verify(mUiEventLogger, times(1)).log(
+ ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource);
+ verify(mUiEventLogger, times(1)).log(
+ ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
new file mode 100644
index 0000000..d96ca91
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -0,0 +1,183 @@
+/*
+ * 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 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 org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.animation.Animator;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.net.Uri;
+import android.os.PersistableBundle;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.logging.UiEventLogger;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.screenshot.TimeoutHandler;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ClipboardOverlayControllerTest extends SysuiTestCase {
+
+ private ClipboardOverlayController mOverlayController;
+ @Mock
+ private ClipboardOverlayView mClipboardOverlayView;
+ @Mock
+ private ClipboardOverlayWindow mClipboardOverlayWindow;
+ @Mock
+ private BroadcastSender mBroadcastSender;
+ @Mock
+ private TimeoutHandler mTimeoutHandler;
+ @Mock
+ private UiEventLogger mUiEventLogger;
+
+ @Mock
+ private Animator mAnimator;
+
+ private ClipData mSampleClipData;
+
+ @Captor
+ private ArgumentCaptor<ClipboardOverlayView.ClipboardOverlayCallbacks> mOverlayCallbacksCaptor;
+ private ClipboardOverlayView.ClipboardOverlayCallbacks mCallbacks;
+
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ when(mClipboardOverlayView.getEnterAnimation()).thenReturn(mAnimator);
+ when(mClipboardOverlayView.getExitAnimation()).thenReturn(mAnimator);
+
+ mSampleClipData = new ClipData("Test", new String[]{"text/plain"},
+ new ClipData.Item("Test Item"));
+
+ mOverlayController = new ClipboardOverlayController(
+ mContext,
+ mClipboardOverlayView,
+ mClipboardOverlayWindow,
+ getFakeBroadcastDispatcher(),
+ mBroadcastSender,
+ mTimeoutHandler,
+ mUiEventLogger);
+ verify(mClipboardOverlayView).setCallbacks(mOverlayCallbacksCaptor.capture());
+ mCallbacks = mOverlayCallbacksCaptor.getValue();
+ }
+
+ @Test
+ public void test_setClipData_nullData() {
+ ClipData clipData = null;
+ mOverlayController.setClipData(clipData, "");
+
+ verify(mClipboardOverlayView, times(1)).showDefaultTextPreview();
+ verify(mClipboardOverlayView, times(0)).showShareChip();
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_setClipData_invalidImageData() {
+ ClipData clipData = new ClipData("", new String[]{"image/png"},
+ new ClipData.Item(Uri.parse("")));
+
+ mOverlayController.setClipData(clipData, "");
+
+ verify(mClipboardOverlayView, times(1)).showDefaultTextPreview();
+ verify(mClipboardOverlayView, times(0)).showShareChip();
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_setClipData_textData() {
+ mOverlayController.setClipData(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() {
+ 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.setClipData(data, "");
+
+ verify(mClipboardOverlayView, times(1)).showTextPreview("••••••", true);
+ verify(mClipboardOverlayView, times(1)).showShareChip();
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_setClipData_repeatedCalls() {
+ when(mAnimator.isRunning()).thenReturn(true);
+
+ mOverlayController.setClipData(mSampleClipData, "");
+ mOverlayController.setClipData(mSampleClipData, "");
+
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_viewCallbacks_onShareTapped() {
+ mOverlayController.setClipData(mSampleClipData, "");
+
+ mCallbacks.onShareButtonTapped();
+
+ verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHARE_TAPPED);
+ verify(mClipboardOverlayView, times(1)).getExitAnimation();
+ }
+
+ @Test
+ public void test_viewCallbacks_onDismissTapped() {
+ mOverlayController.setClipData(mSampleClipData, "");
+
+ mCallbacks.onDismissButtonTapped();
+
+ verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
+ verify(mClipboardOverlayView, times(1)).getExitAnimation();
+ }
+
+ @Test
+ public void test_multipleDismissals_dismissesOnce() {
+ mCallbacks.onSwipeDismissInitiated(mAnimator);
+ mCallbacks.onDismissButtonTapped();
+ mCallbacks.onSwipeDismissInitiated(mAnimator);
+ mCallbacks.onDismissButtonTapped();
+
+ verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SWIPE_DISMISSED);
+ verify(mUiEventLogger, never()).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java
deleted file mode 100644
index c7c2cd8d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.clipboardoverlay;
-
-import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED;
-
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.provider.DeviceConfig;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.util.DeviceConfigProxyFake;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class ClipboardOverlayEventTest extends SysuiTestCase {
-
- @Mock
- private ClipboardManager mClipboardManager;
- @Mock
- private ClipboardOverlayControllerFactory mClipboardOverlayControllerFactory;
- @Mock
- private ClipboardOverlayController mOverlayController;
- @Mock
- private UiEventLogger mUiEventLogger;
-
- private final String mSampleSource = "Example source";
-
- private ClipboardListener mClipboardListener;
-
-
- @Before
- public void setup() {
- MockitoAnnotations.initMocks(this);
- when(mClipboardOverlayControllerFactory.create(any())).thenReturn(
- mOverlayController);
- when(mClipboardManager.hasPrimaryClip()).thenReturn(true);
-
- ClipData sampleClipData = new ClipData("Test", new String[]{"text/plain"},
- new ClipData.Item("Test Item"));
- when(mClipboardManager.getPrimaryClip()).thenReturn(sampleClipData);
- when(mClipboardManager.getPrimaryClipSource()).thenReturn(mSampleSource);
-
- DeviceConfigProxyFake deviceConfigProxy = new DeviceConfigProxyFake();
- deviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
- "true", false);
-
- mClipboardListener = new ClipboardListener(getContext(), deviceConfigProxy,
- mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger);
- }
-
- @Test
- public void test_enterAndReenter() {
- mClipboardListener.start();
-
- mClipboardListener.onPrimaryClipChanged();
- mClipboardListener.onPrimaryClipChanged();
-
- verify(mUiEventLogger, times(1)).log(
- ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource);
- verify(mUiEventLogger, times(1)).log(
- ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource);
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
similarity index 97%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorParameterizedTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
index b6d7559..b4d5464 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
@@ -12,20 +12,20 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
+ *
*/
-package com.android.systemui.keyguard.domain.usecase
+package com.android.systemui.keyguard.domain.interactor
import android.content.Intent
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
@@ -195,6 +195,7 @@
@Mock private lateinit var userTracker: UserTracker
@Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var animationController: ActivityLaunchAnimator.Controller
+ @Mock private lateinit var expandable: Expandable
private lateinit var underTest: KeyguardQuickAffordanceInteractor
@@ -208,6 +209,7 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+ whenever(expandable.activityLaunchController()).thenReturn(animationController)
homeControls = object : FakeKeyguardQuickAffordanceConfig() {}
underTest =
@@ -259,7 +261,7 @@
underTest.onQuickAffordanceClicked(
configKey = homeControls::class,
- animationController = animationController,
+ expandable = expandable,
)
if (startActivity) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
index 1dd919a..65fd6e5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
@@ -12,9 +12,10 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
+ *
*/
-package com.android.systemui.keyguard.domain.usecase
+package com.android.systemui.keyguard.domain.interactor
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
@@ -22,13 +23,12 @@
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -103,6 +103,7 @@
homeControls.setState(
KeyguardQuickAffordanceConfig.State.Visible(
icon = ICON,
+ toggle = KeyguardQuickAffordanceToggleState.On,
)
)
@@ -123,6 +124,7 @@
assertThat(visibleModel.icon).isEqualTo(ICON)
assertThat(visibleModel.icon.contentDescription)
.isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
+ assertThat(visibleModel.toggle).isEqualTo(KeyguardQuickAffordanceToggleState.On)
job.cancel()
}
@@ -152,6 +154,7 @@
assertThat(visibleModel.icon).isEqualTo(ICON)
assertThat(visibleModel.icon.contentDescription)
.isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
+ assertThat(visibleModel.toggle).isEqualTo(KeyguardQuickAffordanceToggleState.NotSupported)
job.cancel()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
index 6ea1daa..e99c139 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
@@ -17,7 +17,7 @@
package com.android.systemui.keyguard.domain.quickaffordance
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -40,7 +40,7 @@
override val state: Flow<KeyguardQuickAffordanceConfig.State> = _state
override fun onQuickAffordanceClicked(
- animationController: ActivityLaunchAnimator.Controller?,
+ expandable: Expandable?,
): OnClickedResult {
return onClickedResult
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
index dede4ec..a809f05 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
@@ -20,7 +20,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
@@ -44,7 +44,7 @@
class HomeControlsKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
@Mock private lateinit var component: ControlsComponent
- @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller
+ @Mock private lateinit var expandable: Expandable
private lateinit var underTest: HomeControlsKeyguardQuickAffordanceConfig
@@ -103,7 +103,7 @@
fun `onQuickAffordanceClicked - canShowWhileLockedSetting is true`() = runBlockingTest {
whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(true))
- val onClickedResult = underTest.onQuickAffordanceClicked(animationController)
+ val onClickedResult = underTest.onQuickAffordanceClicked(expandable)
assertThat(onClickedResult).isInstanceOf(OnClickedResult.StartActivity::class.java)
assertThat((onClickedResult as OnClickedResult.StartActivity).canShowWhileLocked).isTrue()
@@ -113,7 +113,7 @@
fun `onQuickAffordanceClicked - canShowWhileLockedSetting is false`() = runBlockingTest {
whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(false))
- val onClickedResult = underTest.onQuickAffordanceClicked(animationController)
+ val onClickedResult = underTest.onQuickAffordanceClicked(expandable)
assertThat(onClickedResult).isInstanceOf(OnClickedResult.StartActivity::class.java)
assertThat((onClickedResult as OnClickedResult.StartActivity).canShowWhileLocked).isFalse()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
index 0a4478f..98dc4c4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
@@ -24,11 +24,13 @@
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
import com.android.systemui.wallet.controller.QuickAccessWalletController
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.launchIn
@@ -40,7 +42,6 @@
import org.junit.runners.JUnit4
import org.mockito.Mock
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@SmallTest
@@ -135,8 +136,11 @@
@Test
fun onQuickAffordanceClicked() {
val animationController: ActivityLaunchAnimator.Controller = mock()
+ val expandable: Expandable = mock {
+ whenever(this.activityLaunchController()).thenReturn(animationController)
+ }
- assertThat(underTest.onQuickAffordanceClicked(animationController))
+ assertThat(underTest.onQuickAffordanceClicked(expandable))
.isEqualTo(KeyguardQuickAffordanceConfig.OnClickedResult.Handled)
verify(walletController)
.startQuickAccessUiIntent(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index 96544e7..d674c89 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -20,7 +20,7 @@
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.doze.util.BurnInHelperWrapper
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
@@ -31,6 +31,7 @@
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -59,7 +60,7 @@
@RunWith(JUnit4::class)
class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
- @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller
+ @Mock private lateinit var expandable: Expandable
@Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper
@Mock private lateinit var lockPatternUtils: LockPatternUtils
@Mock private lateinit var keyguardStateController: KeyguardStateController
@@ -130,6 +131,7 @@
TestConfig(
isVisible = true,
isClickable = true,
+ isActivated = true,
icon = mock(),
canShowWhileLocked = false,
intent = Intent("action"),
@@ -505,6 +507,12 @@
}
KeyguardQuickAffordanceConfig.State.Visible(
icon = testConfig.icon ?: error("Icon is unexpectedly null!"),
+ toggle =
+ when (testConfig.isActivated) {
+ true -> KeyguardQuickAffordanceToggleState.On
+ false -> KeyguardQuickAffordanceToggleState.Off
+ null -> KeyguardQuickAffordanceToggleState.NotSupported
+ }
)
} else {
KeyguardQuickAffordanceConfig.State.Hidden
@@ -521,12 +529,13 @@
checkNotNull(viewModel)
assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible)
assertThat(viewModel.isClickable).isEqualTo(testConfig.isClickable)
+ assertThat(viewModel.isActivated).isEqualTo(testConfig.isActivated)
if (testConfig.isVisible) {
assertThat(viewModel.icon).isEqualTo(testConfig.icon)
viewModel.onClicked.invoke(
KeyguardQuickAffordanceViewModel.OnClickedParameters(
configKey = configKey,
- animationController = animationController,
+ expandable = expandable,
)
)
if (testConfig.intent != null) {
@@ -542,6 +551,7 @@
private data class TestConfig(
val isVisible: Boolean,
val isClickable: Boolean = false,
+ val isActivated: Boolean = false,
val icon: Icon? = null,
val canShowWhileLocked: Boolean = false,
val intent: Intent? = null,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index 0e9d279..6adce7a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -80,6 +80,7 @@
import com.android.systemui.assist.AssistManager;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.buttons.ButtonDispatcher;
import com.android.systemui.navigationbar.buttons.DeadZone;
@@ -197,6 +198,8 @@
@Mock
private UserContextProvider mUserContextProvider;
@Mock
+ private WakefulnessLifecycle mWakefulnessLifecycle;
+ @Mock
private Resources mResources;
private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
private DeviceConfigProxyFake mDeviceConfigProxyFake = new DeviceConfigProxyFake();
@@ -474,7 +477,8 @@
mNavigationBarTransitions,
mEdgeBackGestureHandler,
Optional.of(mock(BackAnimation.class)),
- mUserContextProvider));
+ mUserContextProvider,
+ mWakefulnessLifecycle));
}
private void processAllMessages() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java
index 4e9b232..c377c37 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java
@@ -46,6 +46,7 @@
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.power.PowerUI.WarningsUI;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -84,6 +85,7 @@
private PowerUI mPowerUI;
@Mock private EnhancedEstimates mEnhancedEstimates;
@Mock private PowerManager mPowerManager;
+ @Mock private WakefulnessLifecycle mWakefulnessLifecycle;
@Mock private IThermalService mThermalServiceMock;
private IThermalEventListener mUsbThermalEventListener;
private IThermalEventListener mSkinThermalEventListener;
@@ -680,7 +682,7 @@
private void createPowerUi() {
mPowerUI = new PowerUI(
mContext, mBroadcastDispatcher, mCommandQueue, mCentralSurfacesOptionalLazy,
- mMockWarnings, mEnhancedEstimates, mPowerManager);
+ mMockWarnings, mEnhancedEstimates, mWakefulnessLifecycle, mPowerManager);
mPowerUI.mThermalService = mThermalServiceMock;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
index 2a4996f..760bb9b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
@@ -192,16 +192,6 @@
// UserManager change.
assertThat(iconTint()).isNull()
- // Trigger a user info change: there should now be a tint.
- userInfoController.updateInfo { userAccount = "doe" }
- assertThat(iconTint())
- .isEqualTo(
- Utils.getColorAttrDefaultColor(
- context,
- android.R.attr.colorForeground,
- )
- )
-
// Make sure we don't tint the icon if it is a user image (and not the default image), even
// in guest mode.
userInfoController.updateInfo { this.picture = mock<UserIconDrawable>() }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DraggableConstraintLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DraggableConstraintLayoutTest.java
new file mode 100644
index 0000000..c6ce51a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DraggableConstraintLayoutTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.screenshot;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.MotionEvent;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class DraggableConstraintLayoutTest extends SysuiTestCase {
+
+ @Mock
+ DraggableConstraintLayout.SwipeDismissCallbacks mCallbacks;
+
+ private DraggableConstraintLayout mDraggableConstraintLayout;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mDraggableConstraintLayout = new DraggableConstraintLayout(mContext, null, 0);
+ }
+
+ @Test
+ public void test_dismissDoesNotCallSwipeInitiated() {
+ mDraggableConstraintLayout.setCallbacks(mCallbacks);
+
+ mDraggableConstraintLayout.dismiss();
+
+ verify(mCallbacks, never()).onSwipeDismissInitiated(any());
+ }
+
+ @Test
+ public void test_onTouchCallsOnInteraction() {
+ mDraggableConstraintLayout.setCallbacks(mCallbacks);
+
+ mDraggableConstraintLayout.onInterceptTouchEvent(
+ MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0));
+
+ verify(mCallbacks).onInteraction();
+ }
+
+ @Test
+ public void test_callbacksNotSet() {
+ // just test that it doesn't throw an NPE
+ mDraggableConstraintLayout.onInterceptTouchEvent(
+ MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0));
+ mDraggableConstraintLayout.onInterceptHoverEvent(
+ MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_ENTER, 0, 0, 0));
+ mDraggableConstraintLayout.dismiss();
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java
index 3f641df..ca65987 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java
@@ -91,6 +91,8 @@
verifyNoMoreInteractions(mLogger);
clearInvocations(mLogger);
+ when(mBindStage.tryGetStageParams(eq(mEntry))).thenReturn(new RowContentBindParams());
+
mViewBinder.unbindHeadsUpView(mEntry);
verify(mLogger).entryContentViewMarkedFreeable(eq(mEntry));
verifyNoMoreInteractions(mLogger);
@@ -139,6 +141,8 @@
verifyNoMoreInteractions(mLogger);
clearInvocations(mLogger);
+ when(mBindStage.tryGetStageParams(eq(mEntry))).thenReturn(new RowContentBindParams());
+
mViewBinder.unbindHeadsUpView(mEntry);
verify(mLogger).currentOngoingBindingAborted(eq(mEntry));
verify(mLogger).entryContentViewMarkedFreeable(eq(mEntry));
@@ -150,4 +154,30 @@
verifyNoMoreInteractions(mLogger);
clearInvocations(mLogger);
}
+
+ @Test
+ public void testLoggingForLateUnbindFlow() {
+ AtomicReference<NotifBindPipeline.BindCallback> callback = new AtomicReference<>();
+ when(mBindStage.requestRebind(any(), any())).then(i -> {
+ callback.set(i.getArgument(1));
+ return new CancellationSignal();
+ });
+
+ mViewBinder.bindHeadsUpView(mEntry, null);
+ verify(mLogger).startBindingHun(eq(mEntry));
+ verifyNoMoreInteractions(mLogger);
+ clearInvocations(mLogger);
+
+ callback.get().onBindFinished(mEntry);
+ verify(mLogger).entryBoundSuccessfully(eq(mEntry));
+ verifyNoMoreInteractions(mLogger);
+ clearInvocations(mLogger);
+
+ when(mBindStage.tryGetStageParams(eq(mEntry))).thenReturn(null);
+
+ mViewBinder.unbindHeadsUpView(mEntry);
+ verify(mLogger).entryBindStageParamsNullOnUnbind(eq(mEntry));
+ verifyNoMoreInteractions(mLogger);
+ clearInvocations(mLogger);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java
index ad3bd71..7c99568 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java
@@ -21,6 +21,10 @@
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP;
+import static junit.framework.Assert.assertEquals;
+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 org.mockito.ArgumentMatchers.any;
@@ -31,6 +35,7 @@
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
+import android.util.Log;
import androidx.test.filters.SmallTest;
@@ -100,6 +105,67 @@
verify(mBinder).unbindContent(eq(mEntry), any(), eq(flags));
}
+ class CountingWtfHandler implements Log.TerribleFailureHandler {
+ private Log.TerribleFailureHandler mOldHandler = null;
+ private int mWtfCount = 0;
+
+ public void register() {
+ mOldHandler = Log.setWtfHandler(this);
+ }
+
+ public void unregister() {
+ Log.setWtfHandler(mOldHandler);
+ mOldHandler = null;
+ }
+
+ @Override
+ public void onTerribleFailure(String tag, Log.TerribleFailure what, boolean system) {
+ mWtfCount++;
+ }
+
+ public int getWtfCount() {
+ return mWtfCount;
+ }
+ }
+
+ @Test
+ public void testGetStageParamsAfterCleanUp() {
+ // GIVEN an entry whose params have already been deleted.
+ RowContentBindParams originalParams = mRowContentBindStage.getStageParams(mEntry);
+ mRowContentBindStage.deleteStageParams(mEntry);
+
+ // WHEN a caller calls getStageParams.
+ CountingWtfHandler countingWtfHandler = new CountingWtfHandler();
+ countingWtfHandler.register();
+
+ RowContentBindParams blankParams = mRowContentBindStage.getStageParams(mEntry);
+
+ countingWtfHandler.unregister();
+
+ // THEN getStageParams logs a WTF and returns blank params created to avoid a crash.
+ assertEquals(1, countingWtfHandler.getWtfCount());
+ assertNotNull(blankParams);
+ assertNotSame(originalParams, blankParams);
+ }
+
+ @Test
+ public void testTryGetStageParamsAfterCleanUp() {
+ // GIVEN an entry whose params have already been deleted.
+ mRowContentBindStage.deleteStageParams(mEntry);
+
+ // WHEN a caller calls getStageParams.
+ CountingWtfHandler countingWtfHandler = new CountingWtfHandler();
+ countingWtfHandler.register();
+
+ RowContentBindParams nullParams = mRowContentBindStage.tryGetStageParams(mEntry);
+
+ countingWtfHandler.unregister();
+
+ // THEN getStageParams does NOT log a WTF and returns null to indicate missing params.
+ assertEquals(0, countingWtfHandler.getWtfCount());
+ assertNull(nullParams);
+ }
+
@Test
public void testRebindAllContentViews() {
// GIVEN a view with content bound.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt
index 76ecc1c..169f4fb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt
@@ -57,14 +57,18 @@
import com.android.systemui.shade.NotificationShadeWindowView
import com.android.systemui.telephony.TelephonyListenerManager
import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper
+import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
@@ -123,7 +127,7 @@
private val ownerId = UserHandle.USER_SYSTEM
private val ownerInfo = UserInfo(ownerId, "Owner", null,
UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL or UserInfo.FLAG_INITIALIZED or
- UserInfo.FLAG_PRIMARY or UserInfo.FLAG_SYSTEM,
+ UserInfo.FLAG_PRIMARY or UserInfo.FLAG_SYSTEM or UserInfo.FLAG_ADMIN,
UserManager.USER_TYPE_FULL_SYSTEM)
private val guestId = 1234
private val guestInfo = UserInfo(guestId, "Guest", null,
@@ -597,6 +601,76 @@
}
@Test
+ fun testCanManageUser_userSwitcherEnabled_addUserWhenLocked() {
+ `when`(
+ globalSettings.getIntForUser(
+ eq(Settings.Global.USER_SWITCHER_ENABLED),
+ anyInt(),
+ eq(UserHandle.USER_SYSTEM)
+ )
+ ).thenReturn(1)
+
+ `when`(
+ globalSettings.getIntForUser(
+ eq(Settings.Global.ADD_USERS_WHEN_LOCKED),
+ anyInt(),
+ eq(UserHandle.USER_SYSTEM)
+ )
+ ).thenReturn(1)
+ setupController()
+ assertTrue(userSwitcherController.canManageUsers())
+ }
+
+ @Test
+ fun testCanManageUser_userSwitcherDisabled_addUserWhenLocked() {
+ `when`(
+ globalSettings.getIntForUser(
+ eq(Settings.Global.USER_SWITCHER_ENABLED),
+ anyInt(),
+ eq(UserHandle.USER_SYSTEM)
+ )
+ ).thenReturn(0)
+
+ `when`(
+ globalSettings.getIntForUser(
+ eq(Settings.Global.ADD_USERS_WHEN_LOCKED),
+ anyInt(),
+ eq(UserHandle.USER_SYSTEM)
+ )
+ ).thenReturn(1)
+ setupController()
+ assertFalse(userSwitcherController.canManageUsers())
+ }
+
+ @Test
+ fun testCanManageUser_userSwitcherEnabled_isAdmin() {
+ `when`(
+ globalSettings.getIntForUser(
+ eq(Settings.Global.USER_SWITCHER_ENABLED),
+ anyInt(),
+ eq(UserHandle.USER_SYSTEM)
+ )
+ ).thenReturn(1)
+
+ setupController()
+ assertTrue(userSwitcherController.canManageUsers())
+ }
+
+ @Test
+ fun testCanManageUser_userSwitcherDisabled_isAdmin() {
+ `when`(
+ globalSettings.getIntForUser(
+ eq(Settings.Global.USER_SWITCHER_ENABLED),
+ anyInt(),
+ eq(UserHandle.USER_SYSTEM)
+ )
+ ).thenReturn(0)
+
+ setupController()
+ assertFalse(userSwitcherController.canManageUsers())
+ }
+
+ @Test
fun addUserSwitchCallback() {
val broadcastReceiverCaptor = argumentCaptor<BroadcastReceiver>()
verify(broadcastDispatcher).registerReceiver(
@@ -632,4 +706,22 @@
bgExecutor.runAllReady()
verify(userManager).createGuest(context)
}
+
+ @Test
+ fun onUserItemClicked_manageUsers() {
+ val manageUserRecord = LegacyUserDataHelper.createRecord(
+ mContext,
+ ownerId,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ isRestricted = false,
+ isSwitchToEnabled = true
+ )
+
+ userSwitcherController.onUserListItemClicked(manageUserRecord, null)
+ val intentCaptor = kotlinArgumentCaptor<Intent>()
+ verify(activityStarter).startActivity(intentCaptor.capture(),
+ eq(true)
+ )
+ Truth.assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/UserSwitcherActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/UserSwitcherActivityTest.kt
deleted file mode 100644
index 3968bb7..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/user/UserSwitcherActivityTest.kt
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.user
-
-import android.app.Application
-import android.os.UserManager
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper.RunWithLooper
-import android.view.LayoutInflater
-import android.view.View
-import android.view.Window
-import android.window.OnBackInvokedCallback
-import android.window.OnBackInvokedDispatcher
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.settings.UserTracker
-import com.android.systemui.statusbar.policy.UserSwitcherController
-import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-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.Mockito.`when`
-import org.mockito.Mockito.any
-import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.doNothing
-import org.mockito.Mockito.eq
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.spy
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-import java.util.concurrent.Executor
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@RunWithLooper(setAsMainLooper = true)
-class UserSwitcherActivityTest : SysuiTestCase() {
- @Mock
- private lateinit var activity: UserSwitcherActivity
- @Mock
- private lateinit var userSwitcherController: UserSwitcherController
- @Mock
- private lateinit var broadcastDispatcher: BroadcastDispatcher
- @Mock
- private lateinit var layoutInflater: LayoutInflater
- @Mock
- private lateinit var falsingCollector: FalsingCollector
- @Mock
- private lateinit var falsingManager: FalsingManager
- @Mock
- private lateinit var userManager: UserManager
- @Mock
- private lateinit var userTracker: UserTracker
- @Mock
- private lateinit var flags: FeatureFlags
- @Mock
- private lateinit var viewModelFactoryLazy: dagger.Lazy<UserSwitcherViewModel.Factory>
- @Mock
- private lateinit var onBackDispatcher: OnBackInvokedDispatcher
- @Mock
- private lateinit var decorView: View
- @Mock
- private lateinit var window: Window
- @Mock
- private lateinit var userSwitcherRootView: UserSwitcherRootView
- @Captor
- private lateinit var onBackInvokedCallback: ArgumentCaptor<OnBackInvokedCallback>
- var isFinished = false
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- activity = spy(object : UserSwitcherActivity(
- userSwitcherController,
- broadcastDispatcher,
- falsingCollector,
- falsingManager,
- userManager,
- userTracker,
- flags,
- viewModelFactoryLazy,
- ) {
- override fun getOnBackInvokedDispatcher() = onBackDispatcher
- override fun getMainExecutor(): Executor = FakeExecutor(FakeSystemClock())
- override fun finish() {
- isFinished = true
- }
- })
- `when`(activity.window).thenReturn(window)
- `when`(window.decorView).thenReturn(decorView)
- `when`(activity.findViewById<UserSwitcherRootView>(R.id.user_switcher_root))
- .thenReturn(userSwitcherRootView)
- `when`(activity.findViewById<View>(R.id.cancel)).thenReturn(mock(View::class.java))
- `when`(activity.findViewById<View>(R.id.add)).thenReturn(mock(View::class.java))
- `when`(activity.application).thenReturn(mock(Application::class.java))
- doNothing().`when`(activity).setContentView(anyInt())
- }
-
- @Test
- fun testMaxColumns() {
- assertThat(activity.getMaxColumns(3)).isEqualTo(4)
- assertThat(activity.getMaxColumns(4)).isEqualTo(4)
- assertThat(activity.getMaxColumns(5)).isEqualTo(3)
- assertThat(activity.getMaxColumns(6)).isEqualTo(3)
- assertThat(activity.getMaxColumns(7)).isEqualTo(4)
- assertThat(activity.getMaxColumns(9)).isEqualTo(5)
- }
-
- @Test
- fun onCreate_callbackRegistration() {
- activity.createActivity()
- verify(onBackDispatcher).registerOnBackInvokedCallback(
- eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT), any())
-
- activity.destroyActivity()
- verify(onBackDispatcher).unregisterOnBackInvokedCallback(any())
- }
-
- @Test
- fun onBackInvokedCallback_finishesActivity() {
- activity.createActivity()
- verify(onBackDispatcher).registerOnBackInvokedCallback(
- eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT), onBackInvokedCallback.capture())
-
- onBackInvokedCallback.value.onBackInvoked()
- assertThat(isFinished).isTrue()
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
index 37c378c..1540f85 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
@@ -202,6 +202,7 @@
fun `actions - device unlocked`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
+
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
@@ -215,6 +216,7 @@
UserActionModel.ENTER_GUEST_MODE,
UserActionModel.ADD_USER,
UserActionModel.ADD_SUPERVISED_USER,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
)
@@ -276,6 +278,7 @@
UserActionModel.ENTER_GUEST_MODE,
UserActionModel.ADD_USER,
UserActionModel.ADD_SUPERVISED_USER,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
)
@@ -283,7 +286,7 @@
}
@Test
- fun `actions - device locked - only guest action is shown`() =
+ fun `actions - device locked - only guest action and manage user is shown`() =
runBlocking(IMMEDIATE) {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
@@ -293,7 +296,13 @@
var value: List<UserActionModel>? = null
val job = underTest.actions.onEach { value = it }.launchIn(this)
- assertThat(value).isEqualTo(listOf(UserActionModel.ENTER_GUEST_MODE))
+ assertThat(value)
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT
+ )
+ )
job.cancel()
}
@@ -330,7 +339,7 @@
underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
val intentCaptor = kotlinArgumentCaptor<Intent>()
- verify(activityStarter).startActivity(intentCaptor.capture(), eq(false))
+ verify(activityStarter).startActivity(intentCaptor.capture(), eq(true))
assertThat(intentCaptor.value.action)
.isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER)
assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE)
@@ -342,7 +351,7 @@
underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
val intentCaptor = kotlinArgumentCaptor<Intent>()
- verify(activityStarter).startActivity(intentCaptor.capture(), eq(false))
+ verify(activityStarter).startActivity(intentCaptor.capture(), eq(true))
assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS)
}
@@ -561,6 +570,7 @@
UserActionModel.ENTER_GUEST_MODE,
UserActionModel.ADD_USER,
UserActionModel.ADD_SUPERVISED_USER,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
),
)
}
@@ -705,7 +715,7 @@
name,
/* iconPath= */ "",
/* flags= */ if (isPrimary) {
- UserInfo.FLAG_PRIMARY
+ UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN
} else {
0
},
diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index aec5f5e..4cf63b3 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -3732,21 +3732,34 @@
Slog.w(TAG, "agentDisconnected: the backup agent for " + packageName
+ " died: cancel current operations");
- // handleCancel() causes the PerformFullTransportBackupTask to go on to
- // tearDownAgentAndKill: that will unbindBackupAgent in the Activity Manager, so
- // that the package being backed up doesn't get stuck in restricted mode until the
- // backup time-out elapses.
- for (int token : mOperationStorage.operationTokensForPackage(packageName)) {
- if (MORE_DEBUG) {
- Slog.d(TAG, "agentDisconnected: will handleCancel(all) for token:"
- + Integer.toHexString(token));
+ // Offload operation cancellation off the main thread as the cancellation callbacks
+ // might call out to BackupTransport. Other operations started on the same package
+ // before the cancellation callback has executed will also be cancelled by the callback.
+ Runnable cancellationRunnable = () -> {
+ // handleCancel() causes the PerformFullTransportBackupTask to go on to
+ // tearDownAgentAndKill: that will unbindBackupAgent in the Activity Manager, so
+ // that the package being backed up doesn't get stuck in restricted mode until the
+ // backup time-out elapses.
+ for (int token : mOperationStorage.operationTokensForPackage(packageName)) {
+ if (MORE_DEBUG) {
+ Slog.d(TAG, "agentDisconnected: will handleCancel(all) for token:"
+ + Integer.toHexString(token));
+ }
+ handleCancel(token, true /* cancelAll */);
}
- handleCancel(token, true /* cancelAll */);
- }
+ };
+ getThreadForAsyncOperation(/* operationName */ "agent-disconnected",
+ cancellationRunnable).start();
+
mAgentConnectLock.notifyAll();
}
}
+ @VisibleForTesting
+ Thread getThreadForAsyncOperation(String operationName, Runnable operation) {
+ return new Thread(operation, operationName);
+ }
+
/**
* An application being installed will need a restore pass, then the {@link PackageManager} will
* need to be told when the restore is finished.
diff --git a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
index fb8c5b1..41b62e6 100644
--- a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
+++ b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
@@ -363,7 +363,9 @@
@Override
public void handleMessage(@NonNull Message msg) {
final int associationId = msg.what;
- onDeviceGone(mSimulated, associationId, /* sourceLoggingTag */ "simulated");
+ if (mSimulated.contains(associationId)) {
+ onDeviceGone(mSimulated, associationId, /* sourceLoggingTag */ "simulated");
+ }
}
}
}
diff --git a/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java
similarity index 99%
rename from core/java/com/android/server/SystemConfig.java
rename to services/core/java/com/android/server/SystemConfig.java
index 2c9ef4f..b7f8d38 100644
--- a/core/java/com/android/server/SystemConfig.java
+++ b/services/core/java/com/android/server/SystemConfig.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2014 The Android Open Source Project
+ * 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.
@@ -72,6 +72,8 @@
* Loads global system configuration info.
* Note: Initializing this class hits the disk and is slow. This class should generally only be
* accessed by the system_server process.
+ *
+ * @hide
*/
public class SystemConfig {
static final String TAG = "SystemConfig";
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 593e21a5..1a4da7d 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -3937,7 +3937,8 @@
// Clear its scheduled jobs
JobSchedulerInternal js = LocalServices.getService(JobSchedulerInternal.class);
// Clearing data is a user-initiated action.
- js.cancelJobsForUid(appInfo.uid, JobParameters.STOP_REASON_USER,
+ js.cancelJobsForUid(appInfo.uid, /* includeProxiedJobs */ true,
+ JobParameters.STOP_REASON_USER,
JobParameters.INTERNAL_STOP_REASON_DATA_CLEARED, "clear data");
// Clear its pending alarms
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 23c020e..3c1bf0b 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -1138,10 +1138,7 @@
mHandler.removeCallbacksAndMessages(null);
// Release any outstanding wakelocks we're still holding because of pending messages.
- mWakelockController.releaseUnfinishedBusinessSuspendBlocker();
- mWakelockController.releaseStateChangedSuspendBlocker();
- mWakelockController.releaseProxPositiveSuspendBlocker();
- mWakelockController.releaseProxNegativeSuspendBlocker();
+ mWakelockController.releaseAll();
final float brightness = mPowerState != null
? mPowerState.getScreenBrightness()
@@ -1729,7 +1726,7 @@
// Grab a wake lock if we have unfinished business.
if (!finished) {
- mWakelockController.acquireUnfinishedBusinessSuspendBlocker();
+ mWakelockController.acquireWakelock(WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
}
// Notify the power manager when ready.
@@ -1749,7 +1746,7 @@
// Release the wake lock when we have no unfinished business.
if (finished) {
- mWakelockController.releaseUnfinishedBusinessSuspendBlocker();
+ mWakelockController.releaseWakelock(WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
}
// Record if dozing for future comparison.
@@ -2248,7 +2245,8 @@
mSensorManager.unregisterListener(mProximitySensorListener);
// release wake lock(must be last)
boolean proxDebounceSuspendBlockerReleased =
- mWakelockController.releaseProxDebounceSuspendBlocker();
+ mWakelockController.releaseWakelock(
+ WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
if (proxDebounceSuspendBlockerReleased) {
mPendingProximityDebounceTime = -1;
}
@@ -2272,11 +2270,13 @@
if (positive) {
mPendingProximity = PROXIMITY_POSITIVE;
mPendingProximityDebounceTime = time + PROXIMITY_SENSOR_POSITIVE_DEBOUNCE_DELAY;
- mWakelockController.acquireProxDebounceSuspendBlocker(); // acquire wake lock
+ mWakelockController.acquireWakelock(
+ WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE); // acquire wake lock
} else {
mPendingProximity = PROXIMITY_NEGATIVE;
mPendingProximityDebounceTime = time + PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY;
- mWakelockController.acquireProxDebounceSuspendBlocker(); // acquire wake lock
+ mWakelockController.acquireWakelock(
+ WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE); // acquire wake lock
}
// Debounce the new sensor reading.
@@ -2300,7 +2300,8 @@
updatePowerState();
// (must be last)
boolean proxDebounceSuspendBlockerReleased =
- mWakelockController.releaseProxDebounceSuspendBlocker();
+ mWakelockController.releaseWakelock(
+ WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
if (proxDebounceSuspendBlockerReleased) {
mPendingProximityDebounceTime = -1;
}
@@ -2315,7 +2316,8 @@
}
private void sendOnStateChangedWithWakelock() {
- boolean wakeLockAcquired = mWakelockController.acquireStateChangedSuspendBlocker();
+ boolean wakeLockAcquired = mWakelockController.acquireWakelock(
+ WakelockController.WAKE_LOCK_STATE_CHANGED);
if (wakeLockAcquired) {
mHandler.post(mWakelockController.getOnStateChangedRunnable());
}
@@ -2479,13 +2481,13 @@
}
private void sendOnProximityPositiveWithWakelock() {
- mWakelockController.acquireProxPositiveSuspendBlocker();
+ mWakelockController.acquireWakelock(WakelockController.WAKE_LOCK_PROXIMITY_POSITIVE);
mHandler.post(mWakelockController.getOnProximityPositiveRunnable());
}
private void sendOnProximityNegativeWithWakelock() {
- mWakelockController.acquireProxNegativeSuspendBlocker();
+ mWakelockController.acquireWakelock(WakelockController.WAKE_LOCK_PROXIMITY_NEGATIVE);
mHandler.post(mWakelockController.getOnProximityNegativeRunnable());
}
diff --git a/services/core/java/com/android/server/display/WakelockController.java b/services/core/java/com/android/server/display/WakelockController.java
index cbf559f..6511f4f 100644
--- a/services/core/java/com/android/server/display/WakelockController.java
+++ b/services/core/java/com/android/server/display/WakelockController.java
@@ -16,12 +16,15 @@
package com.android.server.display;
+import android.annotation.IntDef;
import android.hardware.display.DisplayManagerInternal;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
/**
* A utility class to acquire/release suspend blockers and manage appropriate states around it.
@@ -29,8 +32,26 @@
* display states as needed.
*/
public final class WakelockController {
+ public static final int WAKE_LOCK_PROXIMITY_POSITIVE = 1;
+ public static final int WAKE_LOCK_PROXIMITY_NEGATIVE = 2;
+ public static final int WAKE_LOCK_PROXIMITY_DEBOUNCE = 3;
+ public static final int WAKE_LOCK_STATE_CHANGED = 4;
+ public static final int WAKE_LOCK_UNFINISHED_BUSINESS = 5;
+
+ private static final int WAKE_LOCK_MAX = WAKE_LOCK_UNFINISHED_BUSINESS;
private static final boolean DEBUG = false;
+ @IntDef(flag = true, prefix = "WAKE_LOCK_", value = {
+ WAKE_LOCK_PROXIMITY_POSITIVE,
+ WAKE_LOCK_PROXIMITY_NEGATIVE,
+ WAKE_LOCK_PROXIMITY_DEBOUNCE,
+ WAKE_LOCK_STATE_CHANGED,
+ WAKE_LOCK_UNFINISHED_BUSINESS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface WAKE_LOCK_TYPE {
+ }
+
// Asynchronous callbacks into the power manager service.
// Only invoked from the handler thread while no locks are held.
private final DisplayManagerInternal.DisplayPowerCallbacks mDisplayPowerCallbacks;
@@ -58,17 +79,17 @@
// (i.e. DisplayPowerController2).
private boolean mOnStateChangedPending;
- // Count of positive proximity messages currently held. Used to keep track of how many
- // suspend blocker acquisitions are pending when shutting down the DisplayPowerController2.
- // Should only be accessed on the Handler thread of the class managing the Display states
- // (i.e. DisplayPowerController2).
- private int mOnProximityPositiveMessages;
+ // When true, it implies that a positive proximity wakelock is currently held. Used to keep
+ // track if suspend blocker acquisitions is pending when shutting down the
+ // DisplayPowerController2. Should only be accessed on the Handler thread of the class
+ // managing the Display states (i.e. DisplayPowerController2).
+ private boolean mIsProximityPositiveAcquired;
- // Count of negative proximity messages currently held. Used to keep track of how many
- // suspend blocker acquisitions are pending when shutting down the DisplayPowerController2.
- // Should only be accessed on the Handler thread of the class managing the Display states
- // (i.e. DisplayPowerController2).
- private int mOnProximityNegativeMessages;
+ // When true, it implies that a negative proximity wakelock is currently held. Used to keep
+ // track if suspend blocker acquisitions is pending when shutting down the
+ // DisplayPowerController2. Should only be accessed on the Handler thread of the class
+ // managing the Display states (i.e. DisplayPowerController2).
+ private boolean mIsProximityNegativeAcquired;
/**
* The constructor of WakelockController. Manages the initialization of all the local entities
@@ -87,9 +108,86 @@
}
/**
+ * A utility to acquire a wakelock
+ *
+ * @param wakelock The type of Wakelock to be acquired
+ * @return True of the wakelock is successfully acquired. False if it is already acquired
+ */
+ public boolean acquireWakelock(@WAKE_LOCK_TYPE int wakelock) {
+ return acquireWakelockInternal(wakelock);
+ }
+
+ /**
+ * A utility to release a wakelock
+ *
+ * @param wakelock The type of Wakelock to be released
+ * @return True of an acquired wakelock is successfully released. False if it is already
+ * acquired
+ */
+ public boolean releaseWakelock(@WAKE_LOCK_TYPE int wakelock) {
+ return releaseWakelockInternal(wakelock);
+ }
+
+ /**
+ * A utility to release all the wakelock acquired by the system
+ */
+ public void releaseAll() {
+ for (int i = WAKE_LOCK_PROXIMITY_POSITIVE; i < WAKE_LOCK_MAX; i++) {
+ releaseWakelockInternal(i);
+ }
+ }
+
+ private boolean acquireWakelockInternal(@WAKE_LOCK_TYPE int wakelock) {
+ switch (wakelock) {
+ case WAKE_LOCK_PROXIMITY_POSITIVE:
+ return acquireProxPositiveSuspendBlocker();
+ case WAKE_LOCK_PROXIMITY_NEGATIVE:
+ return acquireProxNegativeSuspendBlocker();
+ case WAKE_LOCK_PROXIMITY_DEBOUNCE:
+ return acquireProxDebounceSuspendBlocker();
+ case WAKE_LOCK_STATE_CHANGED:
+ return acquireStateChangedSuspendBlocker();
+ case WAKE_LOCK_UNFINISHED_BUSINESS:
+ return acquireUnfinishedBusinessSuspendBlocker();
+ default:
+ throw new RuntimeException("Invalid wakelock attempted to be acquired");
+ }
+ }
+
+ private boolean releaseWakelockInternal(@WAKE_LOCK_TYPE int wakelock) {
+ switch (wakelock) {
+ case WAKE_LOCK_PROXIMITY_POSITIVE:
+ return releaseProxPositiveSuspendBlocker();
+ case WAKE_LOCK_PROXIMITY_NEGATIVE:
+ return releaseProxNegativeSuspendBlocker();
+ case WAKE_LOCK_PROXIMITY_DEBOUNCE:
+ return releaseProxDebounceSuspendBlocker();
+ case WAKE_LOCK_STATE_CHANGED:
+ return releaseStateChangedSuspendBlocker();
+ case WAKE_LOCK_UNFINISHED_BUSINESS:
+ return releaseUnfinishedBusinessSuspendBlocker();
+ default:
+ throw new RuntimeException("Invalid wakelock attempted to be released");
+ }
+ }
+
+ /**
+ * Acquires the proximity positive wakelock and notifies the PowerManagerService about the
+ * changes.
+ */
+ private boolean acquireProxPositiveSuspendBlocker() {
+ if (!mIsProximityPositiveAcquired) {
+ mDisplayPowerCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxPositive);
+ mIsProximityPositiveAcquired = true;
+ return true;
+ }
+ return false;
+ }
+
+ /**
* Acquires the state change wakelock and notifies the PowerManagerService about the changes.
*/
- public boolean acquireStateChangedSuspendBlocker() {
+ private boolean acquireStateChangedSuspendBlocker() {
// Grab a wake lock if we have change of the display state
if (!mOnStateChangedPending) {
if (DEBUG) {
@@ -105,18 +203,20 @@
/**
* Releases the state change wakelock and notifies the PowerManagerService about the changes.
*/
- public void releaseStateChangedSuspendBlocker() {
+ private boolean releaseStateChangedSuspendBlocker() {
if (mOnStateChangedPending) {
mDisplayPowerCallbacks.releaseSuspendBlocker(mSuspendBlockerIdOnStateChanged);
mOnStateChangedPending = false;
+ return true;
}
+ return false;
}
/**
* Acquires the unfinished business wakelock and notifies the PowerManagerService about the
* changes.
*/
- public void acquireUnfinishedBusinessSuspendBlocker() {
+ private boolean acquireUnfinishedBusinessSuspendBlocker() {
// Grab a wake lock if we have unfinished business.
if (!mUnfinishedBusiness) {
if (DEBUG) {
@@ -124,79 +224,84 @@
}
mDisplayPowerCallbacks.acquireSuspendBlocker(mSuspendBlockerIdUnfinishedBusiness);
mUnfinishedBusiness = true;
+ return true;
}
+ return false;
}
/**
* Releases the unfinished business wakelock and notifies the PowerManagerService about the
* changes.
*/
- public void releaseUnfinishedBusinessSuspendBlocker() {
+ private boolean releaseUnfinishedBusinessSuspendBlocker() {
if (mUnfinishedBusiness) {
if (DEBUG) {
Slog.d(mTag, "Finished business...");
}
mDisplayPowerCallbacks.releaseSuspendBlocker(mSuspendBlockerIdUnfinishedBusiness);
mUnfinishedBusiness = false;
+ return true;
}
- }
-
- /**
- * Acquires the proximity positive wakelock and notifies the PowerManagerService about the
- * changes.
- */
- public void acquireProxPositiveSuspendBlocker() {
- mDisplayPowerCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxPositive);
- mOnProximityPositiveMessages++;
+ return false;
}
/**
* Releases the proximity positive wakelock and notifies the PowerManagerService about the
* changes.
*/
- public void releaseProxPositiveSuspendBlocker() {
- for (int i = 0; i < mOnProximityPositiveMessages; i++) {
+ private boolean releaseProxPositiveSuspendBlocker() {
+ if (mIsProximityPositiveAcquired) {
mDisplayPowerCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxPositive);
+ mIsProximityPositiveAcquired = false;
+ return true;
}
- mOnProximityPositiveMessages = 0;
+ return false;
}
/**
* Acquires the proximity negative wakelock and notifies the PowerManagerService about the
* changes.
*/
- public void acquireProxNegativeSuspendBlocker() {
- mOnProximityNegativeMessages++;
- mDisplayPowerCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxNegative);
+ private boolean acquireProxNegativeSuspendBlocker() {
+ if (!mIsProximityNegativeAcquired) {
+ mDisplayPowerCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxNegative);
+ mIsProximityNegativeAcquired = true;
+ return true;
+ }
+ return false;
}
/**
* Releases the proximity negative wakelock and notifies the PowerManagerService about the
* changes.
*/
- public void releaseProxNegativeSuspendBlocker() {
- for (int i = 0; i < mOnProximityNegativeMessages; i++) {
+ private boolean releaseProxNegativeSuspendBlocker() {
+ if (mIsProximityNegativeAcquired) {
mDisplayPowerCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxNegative);
+ mIsProximityNegativeAcquired = false;
+ return true;
}
- mOnProximityNegativeMessages = 0;
+ return false;
}
/**
* Acquires the proximity debounce wakelock and notifies the PowerManagerService about the
* changes.
*/
- public void acquireProxDebounceSuspendBlocker() {
+ private boolean acquireProxDebounceSuspendBlocker() {
if (!mHasProximityDebounced) {
mDisplayPowerCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxDebounce);
+ mHasProximityDebounced = true;
+ return true;
}
- mHasProximityDebounced = true;
+ return false;
}
/**
* Releases the proximity debounce wakelock and notifies the PowerManagerService about the
* changes.
*/
- public boolean releaseProxDebounceSuspendBlocker() {
+ private boolean releaseProxDebounceSuspendBlocker() {
if (mHasProximityDebounced) {
mDisplayPowerCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxDebounce);
mHasProximityDebounced = false;
@@ -210,9 +315,11 @@
*/
public Runnable getOnProximityPositiveRunnable() {
return () -> {
- mOnProximityPositiveMessages--;
- mDisplayPowerCallbacks.onProximityPositive();
- mDisplayPowerCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxPositive);
+ if (mIsProximityPositiveAcquired) {
+ mIsProximityPositiveAcquired = false;
+ mDisplayPowerCallbacks.onProximityPositive();
+ mDisplayPowerCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxPositive);
+ }
};
}
@@ -221,9 +328,11 @@
*/
public Runnable getOnStateChangedRunnable() {
return () -> {
- mOnStateChangedPending = false;
- mDisplayPowerCallbacks.onStateChanged();
- mDisplayPowerCallbacks.releaseSuspendBlocker(mSuspendBlockerIdOnStateChanged);
+ if (mOnStateChangedPending) {
+ mOnStateChangedPending = false;
+ mDisplayPowerCallbacks.onStateChanged();
+ mDisplayPowerCallbacks.releaseSuspendBlocker(mSuspendBlockerIdOnStateChanged);
+ }
};
}
@@ -232,9 +341,11 @@
*/
public Runnable getOnProximityNegativeRunnable() {
return () -> {
- mOnProximityNegativeMessages--;
- mDisplayPowerCallbacks.onProximityNegative();
- mDisplayPowerCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxNegative);
+ if (mIsProximityNegativeAcquired) {
+ mIsProximityNegativeAcquired = false;
+ mDisplayPowerCallbacks.onProximityNegative();
+ mDisplayPowerCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxNegative);
+ }
};
}
@@ -246,8 +357,8 @@
pw.println(" mDisplayId=" + mDisplayId);
pw.println(" mUnfinishedBusiness=" + hasUnfinishedBusiness());
pw.println(" mOnStateChangePending=" + isOnStateChangedPending());
- pw.println(" mOnProximityPositiveMessages=" + getOnProximityPositiveMessages());
- pw.println(" mOnProximityNegativeMessages=" + getOnProximityNegativeMessages());
+ pw.println(" mOnProximityPositiveMessages=" + isProximityPositiveAcquired());
+ pw.println(" mOnProximityNegativeMessages=" + isProximityNegativeAcquired());
}
@VisibleForTesting
@@ -286,13 +397,13 @@
}
@VisibleForTesting
- int getOnProximityPositiveMessages() {
- return mOnProximityPositiveMessages;
+ boolean isProximityPositiveAcquired() {
+ return mIsProximityPositiveAcquired;
}
@VisibleForTesting
- int getOnProximityNegativeMessages() {
- return mOnProximityNegativeMessages;
+ boolean isProximityNegativeAcquired() {
+ return mIsProximityNegativeAcquired;
}
@VisibleForTesting
diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
index 346f311..58428ca 100644
--- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
+++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
@@ -43,6 +43,7 @@
import java.io.FileDescriptor;
import java.util.Objects;
+import java.util.OptionalInt;
/**
* Implementation of the service that provides a privileged API to capture and consume bugreports.
@@ -60,6 +61,9 @@
private final TelephonyManager mTelephonyManager;
private final ArraySet<String> mBugreportWhitelistedPackages;
+ @GuardedBy("mLock")
+ private OptionalInt mPreDumpedDataUid = OptionalInt.empty();
+
BugreportManagerServiceImpl(Context context) {
mContext = context;
mAppOps = context.getSystemService(AppOpsManager.class);
@@ -70,13 +74,25 @@
@Override
@RequiresPermission(android.Manifest.permission.DUMP)
+ public void preDumpUiData(String callingPackage) {
+ enforcePermission(callingPackage, Binder.getCallingUid(), true);
+
+ synchronized (mLock) {
+ preDumpUiDataLocked(callingPackage);
+ }
+ }
+
+ @Override
+ @RequiresPermission(android.Manifest.permission.DUMP)
public void startBugreport(int callingUidUnused, String callingPackage,
FileDescriptor bugreportFd, FileDescriptor screenshotFd,
- int bugreportMode, IDumpstateListener listener, boolean isScreenshotRequested) {
+ int bugreportMode, int bugreportFlags, IDumpstateListener listener,
+ boolean isScreenshotRequested) {
Objects.requireNonNull(callingPackage);
Objects.requireNonNull(bugreportFd);
Objects.requireNonNull(listener);
validateBugreportMode(bugreportMode);
+ validateBugreportFlags(bugreportFlags);
int callingUid = Binder.getCallingUid();
enforcePermission(callingPackage, callingUid, bugreportMode
@@ -90,7 +106,7 @@
synchronized (mLock) {
startBugreportLocked(callingUid, callingPackage, bugreportFd, screenshotFd,
- bugreportMode, listener, isScreenshotRequested);
+ bugreportMode, bugreportFlags, listener, isScreenshotRequested);
}
}
@@ -108,17 +124,14 @@
}
try {
// Note: this may throw SecurityException back out to the caller if they aren't
- // allowed to cancel the report, in which case we should NOT be setting ctl.stop,
- // since that would unintentionally kill some other app's bugreport, which we
- // specifically disallow.
+ // allowed to cancel the report, in which case we should NOT stop the dumpstate
+ // service, since that would unintentionally kill some other app's bugreport, which
+ // we specifically disallow.
ds.cancelBugreport(callingUid, callingPackage);
} catch (RemoteException e) {
Slog.e(TAG, "RemoteException in cancelBugreport", e);
}
- // This tells init to cancel bugreportd service. Note that this is achieved through
- // setting a system property which is not thread-safe. So the lock here offers
- // thread-safety only among callers of the API.
- SystemProperties.set("ctl.stop", BUGREPORT_SERVICE);
+ stopDumpstateBinderServiceLocked();
}
}
@@ -134,6 +147,14 @@
}
}
+ private void validateBugreportFlags(int flags) {
+ flags = clearBugreportFlag(flags, BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA);
+ if (flags != 0) {
+ Slog.w(TAG, "Unknown bugreport flags: " + flags);
+ throw new IllegalArgumentException("Unknown bugreport flags: " + flags);
+ }
+ }
+
private void enforcePermission(
String callingPackage, int callingUid, boolean checkCarrierPrivileges) {
mAppOps.checkPackage(callingUid, callingPackage);
@@ -224,17 +245,62 @@
}
@GuardedBy("mLock")
+ private void preDumpUiDataLocked(String callingPackage) {
+ mPreDumpedDataUid = OptionalInt.empty();
+
+ if (isDumpstateBinderServiceRunningLocked()) {
+ Slog.e(TAG, "'dumpstate' is already running. "
+ + "Cannot pre-dump data while another operation is currently in progress.");
+ return;
+ }
+
+ IDumpstate ds = startAndGetDumpstateBinderServiceLocked();
+ if (ds == null) {
+ Slog.e(TAG, "Unable to get bugreport service");
+ return;
+ }
+
+ try {
+ ds.preDumpUiData(callingPackage);
+ } catch (RemoteException e) {
+ return;
+ } finally {
+ // dumpstate service is already started now. We need to kill it to manage the
+ // lifecycle correctly. If we don't subsequent callers will get
+ // BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS error.
+ stopDumpstateBinderServiceLocked();
+ }
+
+
+ mPreDumpedDataUid = OptionalInt.of(Binder.getCallingUid());
+ }
+
+ @GuardedBy("mLock")
private void startBugreportLocked(int callingUid, String callingPackage,
FileDescriptor bugreportFd, FileDescriptor screenshotFd,
- int bugreportMode, IDumpstateListener listener, boolean isScreenshotRequested) {
+ int bugreportMode, int bugreportFlags, IDumpstateListener listener,
+ boolean isScreenshotRequested) {
if (isDumpstateBinderServiceRunningLocked()) {
Slog.w(TAG, "'dumpstate' is already running. Cannot start a new bugreport"
- + " while another one is currently in progress.");
- reportError(listener,
- IDumpstateListener.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS);
+ + " while another operation is currently in progress.");
+ reportError(listener, IDumpstateListener.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS);
return;
}
+ if ((bugreportFlags & BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA) != 0) {
+ if (mPreDumpedDataUid.isEmpty()) {
+ bugreportFlags = clearBugreportFlag(bugreportFlags,
+ BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA);
+ Slog.w(TAG, "Ignoring BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA."
+ + " No pre-dumped data is available.");
+ } else if (mPreDumpedDataUid.getAsInt() != callingUid) {
+ bugreportFlags = clearBugreportFlag(bugreportFlags,
+ BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA);
+ Slog.w(TAG, "Ignoring BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA."
+ + " Data was pre-dumped by a different UID.");
+ }
+ }
+
IDumpstate ds = startAndGetDumpstateBinderServiceLocked();
if (ds == null) {
Slog.w(TAG, "Unable to get bugreport service");
@@ -245,10 +311,10 @@
// Wrap the listener so we can intercept binder events directly.
IDumpstateListener myListener = new DumpstateListener(listener, ds);
try {
- ds.startBugreport(callingUid, callingPackage,
- bugreportFd, screenshotFd, bugreportMode, myListener, isScreenshotRequested);
+ ds.startBugreport(callingUid, callingPackage, bugreportFd, screenshotFd, bugreportMode,
+ bugreportFlags, myListener, isScreenshotRequested);
} catch (RemoteException e) {
- // bugreportd service is already started now. We need to kill it to manage the
+ // dumpstate service is already started now. We need to kill it to manage the
// lifecycle correctly. If we don't subsequent callers will get
// BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS error.
// Note that listener will be notified by the death recipient below.
@@ -309,6 +375,19 @@
return ds;
}
+ @GuardedBy("mLock")
+ private void stopDumpstateBinderServiceLocked() {
+ // This tells init to cancel bugreportd service. Note that this is achieved through
+ // setting a system property which is not thread-safe. So the lock here offers
+ // thread-safety only among callers of the API.
+ SystemProperties.set("ctl.stop", BUGREPORT_SERVICE);
+ }
+
+ private int clearBugreportFlag(int flags, @BugreportParams.BugreportFlag int flag) {
+ flags &= ~flag;
+ return flags;
+ }
+
private void reportError(IDumpstateListener listener, int errorCode) {
try {
listener.onError(errorCode);
diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
index be3a4da..6b31555 100644
--- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
+++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
@@ -54,6 +54,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
+import com.android.server.SystemConfig;
import com.android.server.pm.parsing.pkg.AndroidPackageUtils;
import com.android.server.pm.parsing.pkg.PackageImpl;
import com.android.server.pm.pkg.AndroidPackage;
@@ -1005,11 +1006,14 @@
/** @see ApplicationInfo#privateFlagsExt */
public static int appInfoPrivateFlagsExt(AndroidPackage pkg,
@Nullable PackageStateInternal pkgSetting) {
+ var isAllowlistedForHiddenApis = SystemConfig.getInstance().getHiddenApiWhitelistedApps()
+ .contains(pkg.getPackageName());
// @formatter:off
int pkgWithoutStateFlags = flag(pkg.isProfileable(), ApplicationInfo.PRIVATE_FLAG_EXT_PROFILEABLE)
| flag(pkg.hasRequestForegroundServiceExemption(), ApplicationInfo.PRIVATE_FLAG_EXT_REQUEST_FOREGROUND_SERVICE_EXEMPTION)
| flag(pkg.areAttributionsUserVisible(), ApplicationInfo.PRIVATE_FLAG_EXT_ATTRIBUTIONS_ARE_USER_VISIBLE)
- | flag(pkg.isOnBackInvokedCallbackEnabled(), ApplicationInfo.PRIVATE_FLAG_EXT_ENABLE_ON_BACK_INVOKED_CALLBACK);
+ | flag(pkg.isOnBackInvokedCallbackEnabled(), ApplicationInfo.PRIVATE_FLAG_EXT_ENABLE_ON_BACK_INVOKED_CALLBACK)
+ | flag(isAllowlistedForHiddenApis, ApplicationInfo.PRIVATE_FLAG_EXT_ALLOWLISTED_FOR_HIDDEN_APIS);
return appInfoPrivateFlagsExt(pkgWithoutStateFlags, pkgSetting);
// @formatter:on
}
diff --git a/services/java/com/android/server/SystemConfigService.java b/services/java/com/android/server/SystemConfigService.java
index cb52e5f..6e82907 100644
--- a/services/java/com/android/server/SystemConfigService.java
+++ b/services/java/com/android/server/SystemConfigService.java
@@ -101,6 +101,13 @@
}
return enabledComponent;
}
+
+ @Override
+ public List<ComponentName> getDefaultVrComponents() {
+ getContext().enforceCallingOrSelfPermission(Manifest.permission.QUERY_ALL_PACKAGES,
+ "Caller must hold " + Manifest.permission.QUERY_ALL_PACKAGES);
+ return new ArrayList<>(SystemConfig.getInstance().getDefaultVrComponents());
+ }
};
public SystemConfigService(Context context) {
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 2df6823a..20af02e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
@@ -177,15 +177,19 @@
advanceTime(1);
// two times, one for unfinished business and one for proximity
- verify(mWakelockController).acquireUnfinishedBusinessSuspendBlocker();
- verify(mWakelockController).acquireProxDebounceSuspendBlocker();
+ verify(mWakelockController).acquireWakelock(
+ WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
+ verify(mWakelockController).acquireWakelock(
+ WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
dpc.stop();
advanceTime(1);
// two times, one for unfinished business and one for proximity
- verify(mWakelockController).acquireUnfinishedBusinessSuspendBlocker();
- verify(mWakelockController).acquireProxDebounceSuspendBlocker();
+ verify(mWakelockController).acquireWakelock(
+ WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS);
+ verify(mWakelockController).acquireWakelock(
+ WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
}
/**
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/WakelockControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/display/WakelockControllerTest.java
index 288408c..07a81ff 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/WakelockControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/WakelockControllerTest.java
@@ -21,6 +21,7 @@
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
import android.hardware.display.DisplayManagerInternal;
@@ -33,6 +34,8 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.concurrent.Callable;
+
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class WakelockControllerTest {
@@ -64,25 +67,18 @@
}
@Test
- public void acquireStateChangedSuspendBlockerAcquiresIfNotAcquired() {
- // Acquire the suspend blocker
- assertTrue(mWakelockController.acquireStateChangedSuspendBlocker());
- assertTrue(mWakelockController.isOnStateChangedPending());
-
- // Try to reacquire
- assertFalse(mWakelockController.acquireStateChangedSuspendBlocker());
- assertTrue(mWakelockController.isOnStateChangedPending());
+ public void acquireStateChangedSuspendBlockerAcquiresIfNotAcquired() throws Exception {
+ // Acquire
+ verifyWakelockAcquisitionAndReaquisition(WakelockController.WAKE_LOCK_STATE_CHANGED,
+ () -> mWakelockController.isOnStateChangedPending());
// Verify acquire happened only once
verify(mDisplayPowerCallbacks, times(1))
.acquireSuspendBlocker(mWakelockController.getSuspendBlockerOnStateChangedId());
// Release
- mWakelockController.releaseStateChangedSuspendBlocker();
- assertFalse(mWakelockController.isOnStateChangedPending());
-
- // Try to release again
- mWakelockController.releaseStateChangedSuspendBlocker();
+ verifyWakelockReleaseAndRerelease(WakelockController.WAKE_LOCK_STATE_CHANGED,
+ () -> mWakelockController.isOnStateChangedPending());
// Verify release happened only once
verify(mDisplayPowerCallbacks, times(1))
@@ -90,25 +86,18 @@
}
@Test
- public void acquireUnfinishedBusinessSuspendBlockerAcquiresIfNotAcquired() {
- // Acquire the suspend blocker
- mWakelockController.acquireUnfinishedBusinessSuspendBlocker();
- assertTrue(mWakelockController.hasUnfinishedBusiness());
-
- // Try to reacquire
- mWakelockController.acquireUnfinishedBusinessSuspendBlocker();
- assertTrue(mWakelockController.hasUnfinishedBusiness());
+ public void acquireUnfinishedBusinessSuspendBlockerAcquiresIfNotAcquired() throws Exception {
+ // Acquire
+ verifyWakelockAcquisitionAndReaquisition(WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS,
+ () -> mWakelockController.hasUnfinishedBusiness());
// Verify acquire happened only once
verify(mDisplayPowerCallbacks, times(1))
.acquireSuspendBlocker(mWakelockController.getSuspendBlockerUnfinishedBusinessId());
- // Release the suspend blocker
- mWakelockController.releaseUnfinishedBusinessSuspendBlocker();
- assertFalse(mWakelockController.hasUnfinishedBusiness());
-
- // Try to release again
- mWakelockController.releaseUnfinishedBusinessSuspendBlocker();
+ // Release
+ verifyWakelockReleaseAndRerelease(WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS,
+ () -> mWakelockController.hasUnfinishedBusiness());
// Verify release happened only once
verify(mDisplayPowerCallbacks, times(1))
@@ -116,70 +105,56 @@
}
@Test
- public void acquireProxPositiveSuspendBlockerAcquiresIfNotAcquired() {
- // Acquire the suspend blocker
- mWakelockController.acquireProxPositiveSuspendBlocker();
- assertEquals(mWakelockController.getOnProximityPositiveMessages(), 1);
-
- // Try to reacquire
- mWakelockController.acquireProxPositiveSuspendBlocker();
- assertEquals(mWakelockController.getOnProximityPositiveMessages(), 2);
+ public void acquireProxPositiveSuspendBlockerAcquiresIfNotAcquired() throws Exception {
+ // Acquire
+ verifyWakelockAcquisitionAndReaquisition(WakelockController.WAKE_LOCK_PROXIMITY_POSITIVE,
+ () -> mWakelockController.isProximityPositiveAcquired());
// Verify acquire happened only once
- verify(mDisplayPowerCallbacks, times(2))
+ verify(mDisplayPowerCallbacks, times(1))
.acquireSuspendBlocker(mWakelockController.getSuspendBlockerProxPositiveId());
- // Release the suspend blocker
- mWakelockController.releaseProxPositiveSuspendBlocker();
- assertEquals(mWakelockController.getOnProximityPositiveMessages(), 0);
+ // Release
+ verifyWakelockReleaseAndRerelease(WakelockController.WAKE_LOCK_PROXIMITY_POSITIVE,
+ () -> mWakelockController.isProximityPositiveAcquired());
- // Verify all suspend blockers were released
- verify(mDisplayPowerCallbacks, times(2))
+ // Verify release happened only once
+ verify(mDisplayPowerCallbacks, times(1))
.releaseSuspendBlocker(mWakelockController.getSuspendBlockerProxPositiveId());
}
@Test
- public void acquireProxNegativeSuspendBlockerAcquiresIfNotAcquired() {
- // Acquire the suspend blocker
- mWakelockController.acquireProxNegativeSuspendBlocker();
- assertEquals(mWakelockController.getOnProximityNegativeMessages(), 1);
-
- // Try to reacquire
- mWakelockController.acquireProxNegativeSuspendBlocker();
- assertEquals(mWakelockController.getOnProximityNegativeMessages(), 2);
+ public void acquireProxNegativeSuspendBlockerAcquiresIfNotAcquired() throws Exception {
+ // Acquire
+ verifyWakelockAcquisitionAndReaquisition(WakelockController.WAKE_LOCK_PROXIMITY_NEGATIVE,
+ () -> mWakelockController.isProximityNegativeAcquired());
// Verify acquire happened only once
- verify(mDisplayPowerCallbacks, times(2))
+ verify(mDisplayPowerCallbacks, times(1))
.acquireSuspendBlocker(mWakelockController.getSuspendBlockerProxNegativeId());
- // Release the suspend blocker
- mWakelockController.releaseProxNegativeSuspendBlocker();
- assertEquals(mWakelockController.getOnProximityNegativeMessages(), 0);
+ // Release
+ verifyWakelockReleaseAndRerelease(WakelockController.WAKE_LOCK_PROXIMITY_NEGATIVE,
+ () -> mWakelockController.isProximityNegativeAcquired());
- // Verify all suspend blockers were released
- verify(mDisplayPowerCallbacks, times(2))
+ // Verify release happened only once
+ verify(mDisplayPowerCallbacks, times(1))
.releaseSuspendBlocker(mWakelockController.getSuspendBlockerProxNegativeId());
}
@Test
- public void acquireProxDebounceSuspendBlockerAcquiresIfNotAcquired() {
+ public void acquireProxDebounceSuspendBlockerAcquiresIfNotAcquired() throws Exception {
// Acquire the suspend blocker
- mWakelockController.acquireProxDebounceSuspendBlocker();
-
- // Try to reacquire
- mWakelockController.acquireProxDebounceSuspendBlocker();
- assertTrue(mWakelockController.hasProximitySensorDebounced());
+ verifyWakelockAcquisitionAndReaquisition(WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE,
+ () -> mWakelockController.hasProximitySensorDebounced());
// Verify acquire happened only once
verify(mDisplayPowerCallbacks, times(1))
.acquireSuspendBlocker(mWakelockController.getSuspendBlockerProxDebounceId());
// Release the suspend blocker
- assertTrue(mWakelockController.releaseProxDebounceSuspendBlocker());
-
- // Release again
- assertFalse(mWakelockController.releaseProxDebounceSuspendBlocker());
- assertFalse(mWakelockController.hasProximitySensorDebounced());
+ verifyWakelockReleaseAndRerelease(WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE,
+ () -> mWakelockController.hasProximitySensorDebounced());
// Verify suspend blocker was released only once
verify(mDisplayPowerCallbacks, times(1))
@@ -189,52 +164,126 @@
@Test
public void proximityPositiveRunnableWorksAsExpected() {
// Acquire the suspend blocker twice
- mWakelockController.acquireProxPositiveSuspendBlocker();
- mWakelockController.acquireProxPositiveSuspendBlocker();
+ assertTrue(mWakelockController.acquireWakelock(
+ WakelockController.WAKE_LOCK_PROXIMITY_POSITIVE));
// Execute the runnable
Runnable proximityPositiveRunnable = mWakelockController.getOnProximityPositiveRunnable();
proximityPositiveRunnable.run();
// Validate one suspend blocker was released
- assertEquals(mWakelockController.getOnProximityPositiveMessages(), 1);
+ assertFalse(mWakelockController.isProximityPositiveAcquired());
verify(mDisplayPowerCallbacks).onProximityPositive();
verify(mDisplayPowerCallbacks).releaseSuspendBlocker(
mWakelockController.getSuspendBlockerProxPositiveId());
}
@Test
+ public void proximityPositiveRunnableDoesNothingIfNotAcquired() {
+ // Execute the runnable
+ Runnable proximityPositiveRunnable = mWakelockController.getOnProximityPositiveRunnable();
+ proximityPositiveRunnable.run();
+
+ // Validate one suspend blocker was released
+ assertFalse(mWakelockController.isProximityPositiveAcquired());
+ verifyZeroInteractions(mDisplayPowerCallbacks);
+ }
+
+ @Test
public void proximityNegativeRunnableWorksAsExpected() {
// Acquire the suspend blocker twice
- mWakelockController.acquireProxNegativeSuspendBlocker();
- mWakelockController.acquireProxNegativeSuspendBlocker();
+ assertTrue(mWakelockController.acquireWakelock(
+ WakelockController.WAKE_LOCK_PROXIMITY_NEGATIVE));
// Execute the runnable
Runnable proximityNegativeRunnable = mWakelockController.getOnProximityNegativeRunnable();
proximityNegativeRunnable.run();
// Validate one suspend blocker was released
- assertEquals(mWakelockController.getOnProximityNegativeMessages(), 1);
+ assertFalse(mWakelockController.isProximityNegativeAcquired());
verify(mDisplayPowerCallbacks).onProximityNegative();
verify(mDisplayPowerCallbacks).releaseSuspendBlocker(
mWakelockController.getSuspendBlockerProxNegativeId());
}
@Test
+ public void proximityNegativeRunnableDoesNothingIfNotAcquired() {
+ // Execute the runnable
+ Runnable proximityNegativeRunnable = mWakelockController.getOnProximityNegativeRunnable();
+ proximityNegativeRunnable.run();
+
+ // Validate one suspend blocker was released
+ assertFalse(mWakelockController.isProximityNegativeAcquired());
+ verifyZeroInteractions(mDisplayPowerCallbacks);
+ }
+
+ @Test
public void onStateChangeRunnableWorksAsExpected() {
// Acquire the suspend blocker twice
- mWakelockController.acquireStateChangedSuspendBlocker();
+ assertTrue(mWakelockController.acquireWakelock(WakelockController.WAKE_LOCK_STATE_CHANGED));
// Execute the runnable
Runnable stateChangeRunnable = mWakelockController.getOnStateChangedRunnable();
stateChangeRunnable.run();
// Validate one suspend blocker was released
- assertEquals(mWakelockController.isOnStateChangedPending(), false);
+ assertFalse(mWakelockController.isOnStateChangedPending());
verify(mDisplayPowerCallbacks).onStateChanged();
verify(mDisplayPowerCallbacks).releaseSuspendBlocker(
mWakelockController.getSuspendBlockerOnStateChangedId());
}
+ @Test
+ public void onStateChangeRunnableDoesNothingIfNotAcquired() {
+ // Execute the runnable
+ Runnable stateChangeRunnable = mWakelockController.getOnStateChangedRunnable();
+ stateChangeRunnable.run();
+
+ // Validate one suspend blocker was released
+ assertFalse(mWakelockController.isOnStateChangedPending());
+ verifyZeroInteractions(mDisplayPowerCallbacks);
+ }
+
+ private void verifyWakelockAcquisitionAndReaquisition(int wakelockId,
+ Callable<Boolean> isWakelockAcquiredCallable)
+ throws Exception {
+ verifyWakelockAcquisition(wakelockId, isWakelockAcquiredCallable);
+ verifyWakelockReacquisition(wakelockId, isWakelockAcquiredCallable);
+ }
+
+ private void verifyWakelockReleaseAndRerelease(int wakelockId,
+ Callable<Boolean> isWakelockAcquiredCallable)
+ throws Exception {
+ verifyWakelockRelease(wakelockId, isWakelockAcquiredCallable);
+ verifyWakelockRerelease(wakelockId, isWakelockAcquiredCallable);
+ }
+
+ private void verifyWakelockAcquisition(int wakelockId,
+ Callable<Boolean> isWakelockAcquiredCallable)
+ throws Exception {
+ assertTrue(mWakelockController.acquireWakelock(wakelockId));
+ assertTrue(isWakelockAcquiredCallable.call());
+ }
+
+ private void verifyWakelockReacquisition(int wakelockId,
+ Callable<Boolean> isWakelockAcquiredCallable)
+ throws Exception {
+ assertFalse(mWakelockController.acquireWakelock(wakelockId));
+ assertTrue(isWakelockAcquiredCallable.call());
+ }
+
+ private void verifyWakelockRelease(int wakelockId, Callable<Boolean> isWakelockAcquiredCallable)
+ throws Exception {
+ assertTrue(mWakelockController.releaseWakelock(wakelockId));
+ assertFalse(isWakelockAcquiredCallable.call());
+ }
+
+ private void verifyWakelockRerelease(int wakelockId,
+ Callable<Boolean> isWakelockAcquiredCallable)
+ throws Exception {
+ assertFalse(mWakelockController.releaseWakelock(wakelockId));
+ assertFalse(isWakelockAcquiredCallable.call());
+ }
+
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java
index b354c7b..f46877e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java
@@ -16,30 +16,48 @@
package com.android.server.job;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.server.job.JobConcurrencyManager.KEY_PKG_CONCURRENCY_LIMIT_EJ;
import static com.android.server.job.JobConcurrencyManager.KEY_PKG_CONCURRENCY_LIMIT_REGULAR;
+import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
-import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import android.annotation.Nullable;
import android.app.ActivityManagerInternal;
+import android.app.AppGlobals;
import android.app.job.JobInfo;
import android.content.ComponentName;
import android.content.Context;
+import android.content.pm.IPackageManager;
import android.content.pm.UserInfo;
import android.content.res.Resources;
+import android.os.Looper;
import android.os.UserHandle;
import android.provider.DeviceConfig;
+import android.util.ArraySet;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BG;
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BGUSER;
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BGUSER_IMPORTANT;
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_EJ;
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_FGS;
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_TOP;
+
import com.android.internal.R;
+import com.android.internal.app.IBatteryStats;
import com.android.server.LocalServices;
import com.android.server.job.JobConcurrencyManager.GracePeriodObserver;
import com.android.server.job.JobConcurrencyManager.WorkTypeConfig;
@@ -52,6 +70,12 @@
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.util.ArrayList;
+import java.util.List;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -64,10 +88,24 @@
private int mDefaultUserId;
private GracePeriodObserver mGracePeriodObserver;
private Context mContext;
+ private InjectorForTest mInjector;
+ private MockitoSession mMockingSession;
private Resources mResources;
private PendingJobQueue mPendingJobQueue;
private DeviceConfig.Properties.Builder mConfigBuilder;
+ @Mock
+ private IPackageManager mIPackageManager;
+
+ static class InjectorForTest extends JobConcurrencyManager.Injector {
+ @Override
+ JobServiceContext createJobServiceContext(JobSchedulerService service,
+ JobConcurrencyManager concurrencyManager, IBatteryStats batteryStats,
+ JobPackageTracker tracker, Looper looper) {
+ return mock(JobServiceContext.class);
+ }
+ }
+
@BeforeClass
public static void setUpOnce() {
LocalServices.addService(UserManagerInternal.class, mock(UserManagerInternal.class));
@@ -83,6 +121,11 @@
@Before
public void setUp() {
+ mMockingSession = mockitoSession()
+ .initMocks(this)
+ .mockStatic(AppGlobals.class)
+ .strictness(Strictness.LENIENT)
+ .startMocking();
final JobSchedulerService jobSchedulerService = mock(JobSchedulerService.class);
mContext = mock(Context.class);
mResources = mock(Resources.class);
@@ -93,7 +136,9 @@
mConfigBuilder = new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER);
mPendingJobQueue = new PendingJobQueue();
doReturn(mPendingJobQueue).when(jobSchedulerService).getPendingJobQueue();
- mJobConcurrencyManager = new JobConcurrencyManager(jobSchedulerService);
+ doReturn(mIPackageManager).when(AppGlobals::getPackageManager);
+ mInjector = new InjectorForTest();
+ mJobConcurrencyManager = new JobConcurrencyManager(jobSchedulerService, mInjector);
mGracePeriodObserver = mock(GracePeriodObserver.class);
mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
@@ -106,6 +151,74 @@
@After
public void tearDown() throws Exception {
resetConfig();
+ if (mMockingSession != null) {
+ mMockingSession.finishMocking();
+ }
+ }
+
+ @Test
+ public void testPrepareForAssignmentDetermination_noJobs() {
+ mPendingJobQueue.clear();
+
+ final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
+ final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
+ final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>();
+ mJobConcurrencyManager
+ .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable);
+
+ assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, idle.size());
+ assertEquals(0, preferredUidOnly.size());
+ assertEquals(0, stoppable.size());
+ }
+
+ @Test
+ public void testPrepareForAssignmentDetermination_onlyPendingJobs() {
+ final ArraySet<JobStatus> jobs = new ArraySet<>();
+ for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) {
+ JobStatus job = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE + i);
+ mPendingJobQueue.add(job);
+ jobs.add(job);
+ }
+
+ final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
+ final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
+ final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>();
+ mJobConcurrencyManager
+ .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable);
+
+ assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, idle.size());
+ assertEquals(0, preferredUidOnly.size());
+ assertEquals(0, stoppable.size());
+ }
+
+ @Test
+ public void testDetermineAssignments_allRegular() throws Exception {
+ setConcurrencyConfig(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT,
+ new TypeConfig(WORK_TYPE_BG, 0, JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT));
+ final ArraySet<JobStatus> jobs = new ArraySet<>();
+ for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) {
+ final int uid = mDefaultUserId * UserHandle.PER_USER_RANGE + i;
+ final String sourcePkgName = "com.source.package." + UserHandle.getAppId(uid);
+ setPackageUid(sourcePkgName, uid);
+ final JobStatus job = createJob(uid, sourcePkgName);
+ mPendingJobQueue.add(job);
+ jobs.add(job);
+ }
+
+ final ArraySet<JobConcurrencyManager.ContextAssignment> changed = new ArraySet<>();
+ final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
+ final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
+ final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>();
+ mJobConcurrencyManager
+ .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable);
+ mJobConcurrencyManager
+ .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable);
+
+ assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, changed.size());
+ for (int i = changed.size() - 1; i >= 0; --i) {
+ jobs.remove(changed.valueAt(i).newJob);
+ }
+ assertTrue("Some jobs weren't assigned", jobs.isEmpty());
}
@Test
@@ -403,16 +516,58 @@
}
private static JobStatus createJob(int uid) {
- return createJob(uid, 1);
+ return createJob(uid, 1, null);
+ }
+
+ private static JobStatus createJob(int uid, String sourcePackageName) {
+ return createJob(uid, 1, sourcePackageName);
}
private static JobStatus createJob(int uid, int jobId) {
- return JobStatus.createFromJobInfo(
- new JobInfo.Builder(jobId, new ComponentName("foo", "bar")).build(), uid,
- null, UserHandle.getUserId(uid), "JobConcurrencyManagerTest");
+ return createJob(uid, jobId, null);
}
- private void setConcurrencyConfig(int total) throws Exception {
+ private static JobStatus createJob(int uid, int jobId, @Nullable String sourcePackageName) {
+ return JobStatus.createFromJobInfo(
+ new JobInfo.Builder(jobId, new ComponentName("foo", "bar")).build(), uid,
+ sourcePackageName, UserHandle.getUserId(uid), "JobConcurrencyManagerTest");
+ }
+
+ private static final class TypeConfig {
+ public final String workTypeString;
+ public final int min;
+ public final int max;
+
+ private TypeConfig(@JobConcurrencyManager.WorkType int workType, int min, int max) {
+ switch (workType) {
+ case WORK_TYPE_TOP:
+ workTypeString = "top";
+ break;
+ case WORK_TYPE_FGS:
+ workTypeString = "fgs";
+ break;
+ case WORK_TYPE_EJ:
+ workTypeString = "ej";
+ break;
+ case WORK_TYPE_BG:
+ workTypeString = "bg";
+ break;
+ case WORK_TYPE_BGUSER:
+ workTypeString = "bguser";
+ break;
+ case WORK_TYPE_BGUSER_IMPORTANT:
+ workTypeString = "bguser_important";
+ break;
+ case WORK_TYPE_NONE:
+ default:
+ throw new IllegalArgumentException("invalid work type: " + workType);
+ }
+ this.min = min;
+ this.max = max;
+ }
+ }
+
+ private void setConcurrencyConfig(int total, TypeConfig... typeConfigs) throws Exception {
// Set the values for all memory states so we don't have to worry about memory on the device
// during testing.
final String[] identifiers = {
@@ -422,10 +577,23 @@
for (String identifier : identifiers) {
mConfigBuilder
.setInt(WorkTypeConfig.KEY_PREFIX_MAX_TOTAL + identifier, total);
+ for (TypeConfig config : typeConfigs) {
+ mConfigBuilder.setInt(
+ WorkTypeConfig.KEY_PREFIX_MAX + config.workTypeString + "_" + identifier,
+ config.max);
+ mConfigBuilder.setInt(
+ WorkTypeConfig.KEY_PREFIX_MIN + config.workTypeString + "_" + identifier,
+ config.min);
+ }
}
updateDeviceConfig();
}
+ private void setPackageUid(final String pkgName, final int uid) throws Exception {
+ doReturn(uid).when(mIPackageManager)
+ .getPackageUid(eq(pkgName), anyLong(), eq(UserHandle.getUserId(uid)));
+ }
+
private void updateDeviceConfig() throws Exception {
DeviceConfig.setProperties(mConfigBuilder.build());
mJobConcurrencyManager.updateConfigLocked();
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
index 1d08a80..c58104a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
@@ -34,6 +34,7 @@
import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CHARGING;
import static com.android.server.job.controllers.JobStatus.CONSTRAINT_FLEXIBLE;
import static com.android.server.job.controllers.JobStatus.CONSTRAINT_IDLE;
+import static com.android.server.job.controllers.JobStatus.MIN_WINDOW_FOR_FLEXIBILITY_MS;
import static com.android.server.job.controllers.JobStatus.NO_LATEST_RUNTIME;
import static org.junit.Assert.assertArrayEquals;
@@ -143,6 +144,7 @@
mPrefetchController);
mFcConfig = mFlexibilityController.getFcConfig();
+ setDeviceConfigString(KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS, "50,60,70,80");
setDeviceConfigLong(KEY_DEADLINE_PROXIMITY_LIMIT, 0L);
setDeviceConfigBoolean(KEY_FLEXIBILITY_ENABLED, true);
}
@@ -233,21 +235,22 @@
@Test
public void testOnConstantsUpdated_PercentsToDropConstraints() {
- JobInfo.Builder jb = createJob(0).setOverrideDeadline(100L);
+ JobInfo.Builder jb = createJob(0)
+ .setOverrideDeadline(MIN_WINDOW_FOR_FLEXIBILITY_MS);
JobStatus js = createJobStatus("testPercentsToDropConstraintsConfig", jb);
- assertEquals(150L,
+ assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 5,
mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
setDeviceConfigString(KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS, "10,20,30,40");
assertArrayEquals(
mFlexibilityController.mFcConfig.PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS,
new int[] {10, 20, 30, 40});
- assertEquals(110L,
+ assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10,
mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
js.adjustNumRequiredFlexibleConstraints(-1);
- assertEquals(120L,
+ assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 2,
mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
js.adjustNumRequiredFlexibleConstraints(-1);
- assertEquals(130L,
+ assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 3,
mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
}
@@ -274,24 +277,27 @@
long nextTimeToDropNumConstraints;
// no delay, deadline
- JobInfo.Builder jb = createJob(0).setOverrideDeadline(1000);
+ JobInfo.Builder jb = createJob(0).setOverrideDeadline(MIN_WINDOW_FOR_FLEXIBILITY_MS);
JobStatus js = createJobStatus("time", jb);
assertEquals(JobStatus.NO_EARLIEST_RUNTIME, js.getEarliestRunTime());
- assertEquals(1000 + FROZEN_TIME, js.getLatestRunTimeElapsed());
+ assertEquals(MIN_WINDOW_FOR_FLEXIBILITY_MS + FROZEN_TIME, js.getLatestRunTimeElapsed());
assertEquals(FROZEN_TIME, js.enqueueTime);
nextTimeToDropNumConstraints = mFlexibilityController
.getNextConstraintDropTimeElapsedLocked(js);
- assertEquals(600L, nextTimeToDropNumConstraints);
+ assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 5,
+ nextTimeToDropNumConstraints);
js.adjustNumRequiredFlexibleConstraints(-1);
nextTimeToDropNumConstraints = mFlexibilityController
.getNextConstraintDropTimeElapsedLocked(js);
- assertEquals(700L, nextTimeToDropNumConstraints);
+ assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 6,
+ nextTimeToDropNumConstraints);
js.adjustNumRequiredFlexibleConstraints(-1);
nextTimeToDropNumConstraints = mFlexibilityController
.getNextConstraintDropTimeElapsedLocked(js);
- assertEquals(800L, nextTimeToDropNumConstraints);
+ assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 7,
+ nextTimeToDropNumConstraints);
// delay, no deadline
jb = createJob(0).setMinimumLatency(800000L);
@@ -326,20 +332,26 @@
assertEquals(181440100L, nextTimeToDropNumConstraints);
// delay, deadline
- jb = createJob(0).setOverrideDeadline(1100).setMinimumLatency(100);
+ jb = createJob(0)
+ .setOverrideDeadline(2 * MIN_WINDOW_FOR_FLEXIBILITY_MS)
+ .setMinimumLatency(MIN_WINDOW_FOR_FLEXIBILITY_MS);
js = createJobStatus("time", jb);
+ final long windowStart = FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS;
nextTimeToDropNumConstraints = mFlexibilityController
.getNextConstraintDropTimeElapsedLocked(js);
- assertEquals(700L, nextTimeToDropNumConstraints);
+ assertEquals(windowStart + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 5,
+ nextTimeToDropNumConstraints);
js.adjustNumRequiredFlexibleConstraints(-1);
nextTimeToDropNumConstraints = mFlexibilityController
.getNextConstraintDropTimeElapsedLocked(js);
- assertEquals(800L, nextTimeToDropNumConstraints);
+ assertEquals(windowStart + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 6,
+ nextTimeToDropNumConstraints);
js.adjustNumRequiredFlexibleConstraints(-1);
nextTimeToDropNumConstraints = mFlexibilityController
.getNextConstraintDropTimeElapsedLocked(js);
- assertEquals(900L, nextTimeToDropNumConstraints);
+ assertEquals(windowStart + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 7,
+ nextTimeToDropNumConstraints);
}
@Test
@@ -734,10 +746,9 @@
@Test
public void testResetJobNumDroppedConstraints() {
- JobInfo.Builder jb = createJob(22).setOverrideDeadline(100L);
+ JobInfo.Builder jb = createJob(22);
JobStatus js = createJobStatus("testResetJobNumDroppedConstraints", jb);
- js.adjustNumRequiredFlexibleConstraints(3);
- long nowElapsed;
+ long nowElapsed = FROZEN_TIME;
mFlexibilityController.mFlexibilityTracker.add(js);
@@ -746,8 +757,7 @@
assertEquals(1, mFlexibilityController
.mFlexibilityTracker.getJobsByNumRequiredConstraints(3).size());
-
- nowElapsed = 155L;
+ nowElapsed += DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS / 10 * 5;
JobSchedulerService.sElapsedRealtimeClock =
Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
@@ -766,7 +776,7 @@
assertEquals(1, mFlexibilityController
.mFlexibilityTracker.getJobsByNumRequiredConstraints(2).size());
- nowElapsed = 140L;
+ nowElapsed = FROZEN_TIME;
JobSchedulerService.sElapsedRealtimeClock =
Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
@@ -777,7 +787,7 @@
assertEquals(1, mFlexibilityController
.mFlexibilityTracker.getJobsByNumRequiredConstraints(3).size());
- nowElapsed = 175L;
+ nowElapsed += DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS / 10 * 9;
JobSchedulerService.sElapsedRealtimeClock =
Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
@@ -786,7 +796,7 @@
assertEquals(0, js.getNumRequiredFlexibleConstraints());
assertEquals(3, js.getNumDroppedFlexibleConstraints());
- nowElapsed = 165L;
+ nowElapsed = FROZEN_TIME + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS / 10 * 6;
JobSchedulerService.sElapsedRealtimeClock =
Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
index cc57b9f..dc7bcd6 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
@@ -38,6 +38,7 @@
import android.os.incremental.IncrementalManager
import android.provider.DeviceConfig
import android.util.ArrayMap
+import android.util.ArraySet
import android.util.DisplayMetrics
import android.util.EventLog
import android.view.Display
@@ -294,6 +295,8 @@
wheneverStatic { SystemConfig.getInstance() }.thenReturn(mocks.systemConfig)
whenever(mocks.systemConfig.availableFeatures).thenReturn(DEFAULT_AVAILABLE_FEATURES_MAP)
whenever(mocks.systemConfig.sharedLibraries).thenReturn(DEFAULT_SHARED_LIBRARIES_LIST)
+ whenever(mocks.systemConfig.defaultVrComponents).thenReturn(ArraySet())
+ whenever(mocks.systemConfig.hiddenApiWhitelistedApps).thenReturn(ArraySet())
wheneverStatic { SystemProperties.getBoolean("fw.free_cache_v2", true) }.thenReturn(true)
wheneverStatic { Environment.getPackageCacheDirectory() }.thenReturn(packageCacheDirectory)
wheneverStatic { SystemProperties.digestOf("ro.build.fingerprint") }.thenReturn("cacheName")
diff --git a/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
index bccd8a0b..9ae8922 100644
--- a/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -61,6 +62,7 @@
public class UserBackupManagerServiceTest {
private static final String TEST_PACKAGE = "package1";
private static final String[] TEST_PACKAGES = new String[] { TEST_PACKAGE };
+ private static final int WORKER_THREAD_TIMEOUT_MILLISECONDS = 1;
@Mock Context mContext;
@Mock IBackupManagerMonitor mBackupManagerMonitor;
@@ -179,6 +181,7 @@
mService.agentDisconnected("com.android.foo");
+ mService.waitForAsyncOperation();
verify(mOperationStorage).cancelOperation(eq(123), eq(true), any(IntConsumer.class));
verify(mOperationStorage).cancelOperation(eq(456), eq(true), any());
verify(mOperationStorage).cancelOperation(eq(789), eq(true), any());
@@ -207,6 +210,8 @@
boolean isEnabledStatePersisted = false;
boolean shouldUseNewBackupEligibilityRules = false;
+ private volatile Thread mWorkerThread = null;
+
TestBackupService(Context context, PackageManager packageManager,
LifecycleOperationStorage operationStorage) {
super(context, packageManager, operationStorage);
@@ -229,5 +234,23 @@
boolean shouldUseNewBackupEligibilityRules() {
return shouldUseNewBackupEligibilityRules;
}
+
+ @Override
+ Thread getThreadForAsyncOperation(String operationName, Runnable operation) {
+ mWorkerThread = super.getThreadForAsyncOperation(operationName, operation);
+ return mWorkerThread;
+ }
+
+ private void waitForAsyncOperation() {
+ if (mWorkerThread == null) {
+ return;
+ }
+
+ try {
+ mWorkerThread.join(/* millis */ WORKER_THREAD_TIMEOUT_MILLISECONDS);
+ } catch (InterruptedException e) {
+ fail("Failed waiting for worker thread to complete: " + e.getMessage());
+ }
+ }
}
}
diff --git a/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java b/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java
index 79ab009..5179bab 100644
--- a/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java
+++ b/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java
@@ -27,7 +27,6 @@
import android.os.Bundle;
import android.os.PersistableBundle;
import android.os.SystemProperties;
-import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import java.io.PrintWriter;
@@ -195,57 +194,20 @@
}
/**
- * Convert display name source to string.
+ * Convert mobile data policy to string.
*
- * @param source The display name source.
- * @return The display name source in string format.
+ * @param mobileDataPolicy The mobile data policy.
+ * @return The mobile data policy in string format.
*/
- @NonNull
- public static String displayNameSourceToString(
- @SubscriptionManager.SimDisplayNameSource int source) {
- switch (source) {
- case SubscriptionManager.NAME_SOURCE_UNKNOWN: return "UNKNOWN";
- case SubscriptionManager.NAME_SOURCE_CARRIER_ID: return "CARRIER_ID";
- case SubscriptionManager.NAME_SOURCE_SIM_SPN: return "SIM_SPN";
- case SubscriptionManager.NAME_SOURCE_USER_INPUT: return "USER_INPUT";
- case SubscriptionManager.NAME_SOURCE_CARRIER: return "CARRIER";
- case SubscriptionManager.NAME_SOURCE_SIM_PNN: return "SIM_PNN";
+ public static @NonNull String mobileDataPolicyToString(
+ @TelephonyManager.MobileDataPolicy int mobileDataPolicy) {
+ switch (mobileDataPolicy) {
+ case TelephonyManager.MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL:
+ return "DATA_ON_NON_DEFAULT_DURING_VOICE_CALL";
+ case TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED:
+ return "MMS_ALWAYS_ALLOWED";
default:
- return "UNKNOWN(" + source + ")";
- }
- }
-
- /**
- * Convert subscription type to string.
- *
- * @param type The subscription type.
- * @return The subscription type in string format.
- */
- @NonNull
- public static String subscriptionTypeToString(@SubscriptionManager.SubscriptionType int type) {
- switch (type) {
- case SubscriptionManager.SUBSCRIPTION_TYPE_LOCAL_SIM: return "LOCAL_SIM";
- case SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM: return "REMOTE_SIM";
- default:
- return "UNKNOWN(" + type + ")";
- }
- }
-
- /**
- * Convert usage setting to string.
- *
- * @param usageSetting Usage setting.
- * @return The usage setting in string format.
- */
- @NonNull
- public static String usageSettingToString(@SubscriptionManager.UsageSetting int usageSetting) {
- switch (usageSetting) {
- case SubscriptionManager.USAGE_SETTING_UNKNOWN: return "UNKNOWN";
- case SubscriptionManager.USAGE_SETTING_DEFAULT: return "DEFAULT";
- case SubscriptionManager.USAGE_SETTING_VOICE_CENTRIC: return "VOICE_CENTRIC";
- case SubscriptionManager.USAGE_SETTING_DATA_CENTRIC: return "DATA_CENTRIC";
- default:
- return "UNKNOWN(" + usageSetting + ")";
+ return "UNKNOWN(" + mobileDataPolicy + ")";
}
}
}
diff --git a/telephony/java/android/telephony/SmsMessage.java b/telephony/java/android/telephony/SmsMessage.java
index e0145e6..c2b65f8 100644
--- a/telephony/java/android/telephony/SmsMessage.java
+++ b/telephony/java/android/telephony/SmsMessage.java
@@ -1092,6 +1092,11 @@
if (!TextUtils.isEmpty(simOperator)) {
for (NoEmsSupportConfig currentConfig : mNoEmsSupportConfigList) {
+ if (currentConfig == null) {
+ Rlog.w("SmsMessage", "hasEmsSupport currentConfig is null");
+ continue;
+ }
+
if (simOperator.startsWith(currentConfig.mOperatorNumber) &&
(TextUtils.isEmpty(currentConfig.mGid1) ||
(!TextUtils.isEmpty(currentConfig.mGid1) &&
@@ -1155,18 +1160,21 @@
private static boolean mIsNoEmsSupportConfigListLoaded = false;
private static boolean isNoEmsSupportConfigListExisted() {
- if (!mIsNoEmsSupportConfigListLoaded) {
- Resources r = Resources.getSystem();
- if (r != null) {
- String[] listArray = r.getStringArray(
- com.android.internal.R.array.no_ems_support_sim_operators);
- if ((listArray != null) && (listArray.length > 0)) {
- mNoEmsSupportConfigList = new NoEmsSupportConfig[listArray.length];
- for (int i=0; i<listArray.length; i++) {
- mNoEmsSupportConfigList[i] = new NoEmsSupportConfig(listArray[i].split(";"));
+ synchronized (SmsMessage.class) {
+ if (!mIsNoEmsSupportConfigListLoaded) {
+ Resources r = Resources.getSystem();
+ if (r != null) {
+ String[] listArray = r.getStringArray(
+ com.android.internal.R.array.no_ems_support_sim_operators);
+ if ((listArray != null) && (listArray.length > 0)) {
+ mNoEmsSupportConfigList = new NoEmsSupportConfig[listArray.length];
+ for (int i = 0; i < listArray.length; i++) {
+ mNoEmsSupportConfigList[i] = new NoEmsSupportConfig(
+ listArray[i].split(";"));
+ }
}
+ mIsNoEmsSupportConfigListLoaded = true;
}
- mIsNoEmsSupportConfigListLoaded = true;
}
}
diff --git a/telephony/java/android/telephony/SubscriptionInfo.java b/telephony/java/android/telephony/SubscriptionInfo.java
index 0d3c80f..e055f63 100644
--- a/telephony/java/android/telephony/SubscriptionInfo.java
+++ b/telephony/java/android/telephony/SubscriptionInfo.java
@@ -974,7 +974,7 @@
+ " groupOwner=" + mGroupOwner
+ " isGroupDisabled=" + mIsGroupDisabled
+ " displayNameSource="
- + TelephonyUtils.displayNameSourceToString(mDisplayNameSource)
+ + SubscriptionManager.displayNameSourceToString(mDisplayNameSource)
+ " iconTint=" + mIconTint
+ " number=" + Rlog.pii(TelephonyUtils.IS_DEBUGGABLE, mNumber)
+ " dataRoaming=" + mDataRoaming
@@ -988,9 +988,9 @@
+ " carrierConfigAccessRules=" + Arrays.toString(mCarrierConfigAccessRules)
+ " countryIso=" + mCountryIso
+ " profileClass=" + mProfileClass
- + " mType=" + TelephonyUtils.subscriptionTypeToString(mType)
+ + " mType=" + SubscriptionManager.subscriptionTypeToString(mType)
+ " areUiccApplicationsEnabled=" + mAreUiccApplicationsEnabled
- + " usageSetting=" + TelephonyUtils.usageSettingToString(mUsageSetting)
+ + " usageSetting=" + SubscriptionManager.usageSettingToString(mUsageSetting)
+ "]";
}
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 6189b49..e6c6b62 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -4092,4 +4092,65 @@
(iSub)-> iSub.setUsageSetting(
usageSetting, subscriptionId, mContext.getOpPackageName()));
}
+
+ /**
+ * Convert display name source to string.
+ *
+ * @param source The display name source.
+ * @return The display name source in string format.
+ *
+ * @hide
+ */
+ @NonNull
+ public static String displayNameSourceToString(
+ @SubscriptionManager.SimDisplayNameSource int source) {
+ switch (source) {
+ case SubscriptionManager.NAME_SOURCE_UNKNOWN: return "UNKNOWN";
+ case SubscriptionManager.NAME_SOURCE_CARRIER_ID: return "CARRIER_ID";
+ case SubscriptionManager.NAME_SOURCE_SIM_SPN: return "SIM_SPN";
+ case SubscriptionManager.NAME_SOURCE_USER_INPUT: return "USER_INPUT";
+ case SubscriptionManager.NAME_SOURCE_CARRIER: return "CARRIER";
+ case SubscriptionManager.NAME_SOURCE_SIM_PNN: return "SIM_PNN";
+ default:
+ return "UNKNOWN(" + source + ")";
+ }
+ }
+
+ /**
+ * Convert subscription type to string.
+ *
+ * @param type The subscription type.
+ * @return The subscription type in string format.
+ *
+ * @hide
+ */
+ @NonNull
+ public static String subscriptionTypeToString(@SubscriptionManager.SubscriptionType int type) {
+ switch (type) {
+ case SubscriptionManager.SUBSCRIPTION_TYPE_LOCAL_SIM: return "LOCAL_SIM";
+ case SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM: return "REMOTE_SIM";
+ default:
+ return "UNKNOWN(" + type + ")";
+ }
+ }
+
+ /**
+ * Convert usage setting to string.
+ *
+ * @param usageSetting Usage setting.
+ * @return The usage setting in string format.
+ *
+ * @hide
+ */
+ @NonNull
+ public static String usageSettingToString(@SubscriptionManager.UsageSetting int usageSetting) {
+ switch (usageSetting) {
+ case SubscriptionManager.USAGE_SETTING_UNKNOWN: return "UNKNOWN";
+ case SubscriptionManager.USAGE_SETTING_DEFAULT: return "DEFAULT";
+ case SubscriptionManager.USAGE_SETTING_VOICE_CENTRIC: return "VOICE_CENTRIC";
+ case SubscriptionManager.USAGE_SETTING_DATA_CENTRIC: return "DATA_CENTRIC";
+ default:
+ return "UNKNOWN(" + usageSetting + ")";
+ }
+ }
}
diff --git a/telephony/java/android/telephony/ims/ImsMmTelManager.java b/telephony/java/android/telephony/ims/ImsMmTelManager.java
index e658c2e..caee4e2 100644
--- a/telephony/java/android/telephony/ims/ImsMmTelManager.java
+++ b/telephony/java/android/telephony/ims/ImsMmTelManager.java
@@ -72,6 +72,7 @@
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = "WIFI_MODE_", value = {
+ WIFI_MODE_UNKNOWN,
WIFI_MODE_WIFI_ONLY,
WIFI_MODE_CELLULAR_PREFERRED,
WIFI_MODE_WIFI_PREFERRED
@@ -79,6 +80,12 @@
public @interface WiFiCallingMode {}
/**
+ * Wifi calling mode is unknown. This is for initialization only.
+ * @hide
+ */
+ public static final int WIFI_MODE_UNKNOWN = -1;
+
+ /**
* Register for IMS over IWLAN if WiFi signal quality is high enough. Do not hand over to LTE
* registration if signal quality degrades.
*/
@@ -1573,4 +1580,24 @@
.get());
return binder;
}
+
+ /**
+ * Convert Wi-Fi calling mode to string.
+ *
+ * @param mode Wi-Fi calling mode.
+ * @return The Wi-Fi calling mode in string format.
+ *
+ * @hide
+ */
+ @NonNull
+ public static String wifiCallingModeToString(@ImsMmTelManager.WiFiCallingMode int mode) {
+ switch (mode) {
+ case ImsMmTelManager.WIFI_MODE_UNKNOWN: return "UNKNOWN";
+ case ImsMmTelManager.WIFI_MODE_WIFI_ONLY: return "WIFI_ONLY";
+ case ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED: return "CELLULAR_PREFERRED";
+ case ImsMmTelManager.WIFI_MODE_WIFI_PREFERRED: return "WIFI_PREFERRED";
+ default:
+ return "UNKNOWN(" + mode + ")";
+ }
+ }
}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt
index cb61e35..0837c00 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt
@@ -24,6 +24,7 @@
import org.junit.Assume
import org.junit.Before
import org.junit.FixMethodOrder
+import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.junit.runners.Parameterized
@@ -47,96 +48,115 @@
}
@FlakyTest
+ @Test
override fun appLayerReplacesLauncher() {
super.appLayerReplacesLauncher()
}
@FlakyTest
+ @Test
override fun appLayerBecomesVisible() {
super.appLayerBecomesVisible()
}
@FlakyTest
+ @Test
override fun appWindowBecomesTopWindow() {
super.appWindowBecomesTopWindow()
}
@FlakyTest
+ @Test
override fun appWindowBecomesVisible() {
super.appWindowBecomesVisible()
}
@FlakyTest
+ @Test
override fun appWindowIsTopWindowAtEnd() {
super.appWindowIsTopWindowAtEnd()
}
@FlakyTest
+ @Test
override fun appWindowReplacesLauncherAsTopWindow() {
super.appWindowReplacesLauncherAsTopWindow()
}
@FlakyTest
+ @Test
override fun entireScreenCovered() {
super.entireScreenCovered()
}
@FlakyTest
+ @Test
override fun navBarLayerIsVisibleAtStartAndEnd() {
super.navBarLayerIsVisibleAtStartAndEnd()
}
@FlakyTest
+ @Test
override fun navBarLayerPositionAtStartAndEnd() {
super.navBarLayerPositionAtStartAndEnd()
}
@FlakyTest
+ @Test
override fun navBarWindowIsAlwaysVisible() {
super.navBarWindowIsAlwaysVisible()
}
@FlakyTest
+ @Test
override fun statusBarLayerIsVisibleAtStartAndEnd() {
super.statusBarLayerIsVisibleAtStartAndEnd()
}
@FlakyTest
+ @Test
override fun statusBarLayerPositionAtStartAndEnd() {
super.statusBarLayerPositionAtStartAndEnd()
}
@FlakyTest
+ @Test
override fun statusBarWindowIsAlwaysVisible() {
super.statusBarWindowIsAlwaysVisible()
}
@FlakyTest
+ @Test
override fun taskBarLayerIsVisibleAtStartAndEnd() {
super.taskBarLayerIsVisibleAtStartAndEnd()
}
@FlakyTest
+ @Test
override fun taskBarWindowIsAlwaysVisible() {
super.taskBarWindowIsAlwaysVisible()
}
@FlakyTest
+ @Test
override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
super.visibleLayersShownMoreThanOneConsecutiveEntry()
}
@FlakyTest
+ @Test
override fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
super.visibleWindowsShownMoreThanOneConsecutiveEntry()
}
@FlakyTest
+ @Test
override fun focusChanges() {
super.focusChanges()
}
@FlakyTest
+ @Test
override fun appWindowAsTopWindowAtEnd() {
super.appWindowAsTopWindowAtEnd()
}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
index 06486ca..08624ee 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
@@ -18,6 +18,7 @@
import android.app.Instrumentation
import android.app.WallpaperManager
+import android.platform.test.annotations.FlakyTest
import android.platform.test.annotations.Postsubmit
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.BaseTest
@@ -26,8 +27,8 @@
import com.android.server.wm.flicker.FlickerTestParameterFactory
import com.android.server.wm.flicker.dsl.FlickerBuilder
import com.android.server.wm.flicker.helpers.NewTasksAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.flicker.testapp.ActivityOptions
import com.android.server.wm.traces.common.ComponentNameMatcher
import com.android.server.wm.traces.common.ComponentNameMatcher.Companion.SPLASH_SCREEN
import com.android.server.wm.traces.common.ComponentNameMatcher.Companion.WALLPAPER_BBQ_WRAPPER
@@ -56,7 +57,8 @@
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class TaskTransitionTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
- private val testApp: NewTasksAppHelper = NewTasksAppHelper(instrumentation)
+ private val testApp = NewTasksAppHelper(instrumentation)
+ private val simpleApp = SimpleAppHelper(instrumentation)
private val wallpaper by lazy {
getWallpaperPackage(instrumentation) ?: error("Unable to obtain wallpaper")
}
@@ -76,7 +78,7 @@
* Checks that the [wallpaper] window is never visible when performing task transitions. A solid
* color background should be shown instead.
*/
- @Postsubmit
+ @FlakyTest(bugId = 253617416)
@Test
fun wallpaperWindowIsNeverVisible() {
testSpec.assertWm { this.isNonAppWindowInvisible(wallpaper) }
@@ -86,7 +88,7 @@
* Checks that the [wallpaper] layer is never visible when performing task transitions. A solid
* color background should be shown instead.
*/
- @Postsubmit
+ @FlakyTest(bugId = 253617416)
@Test
fun wallpaperLayerIsNeverVisible() {
testSpec.assertLayers {
@@ -116,7 +118,7 @@
}
/** Checks that a color background is visible while the task transition is occurring. */
- @Postsubmit
+ @FlakyTest(bugId = 240570652)
@Test
fun colorLayerIsVisibleDuringTransition() {
val bgColorLayer = ComponentNameMatcher("", "colorBackgroundLayer")
@@ -124,7 +126,7 @@
testSpec.assertLayers {
this.invoke("LAUNCH_NEW_TASK_ACTIVITY coversExactly displayBounds") {
- it.visibleRegion(LAUNCH_NEW_TASK_ACTIVITY).coversExactly(displayBounds)
+ it.visibleRegion(testApp.componentMatcher).coversExactly(displayBounds)
}
.isInvisible(bgColorLayer)
.then()
@@ -133,7 +135,7 @@
.then()
// Fully transitioned to simple SIMPLE_ACTIVITY
.invoke("SIMPLE_ACTIVITY coversExactly displayBounds") {
- it.visibleRegion(SIMPLE_ACTIVITY).coversExactly(displayBounds)
+ it.visibleRegion(simpleApp.componentMatcher).coversExactly(displayBounds)
}
.isInvisible(bgColorLayer)
.then()
@@ -142,7 +144,7 @@
.then()
// Fully transitioned back to LAUNCH_NEW_TASK_ACTIVITY
.invoke("LAUNCH_NEW_TASK_ACTIVITY coversExactly displayBounds") {
- it.visibleRegion(LAUNCH_NEW_TASK_ACTIVITY).coversExactly(displayBounds)
+ it.visibleRegion(testApp.componentMatcher).coversExactly(displayBounds)
}
.isInvisible(bgColorLayer)
}
@@ -156,15 +158,15 @@
@Test
fun newTaskOpensOnTopAndThenCloses() {
testSpec.assertWm {
- this.isAppWindowOnTop(LAUNCH_NEW_TASK_ACTIVITY)
+ this.isAppWindowOnTop(testApp.componentMatcher)
.then()
.isAppWindowOnTop(SPLASH_SCREEN, isOptional = true)
.then()
- .isAppWindowOnTop(SIMPLE_ACTIVITY)
+ .isAppWindowOnTop(simpleApp.componentMatcher)
.then()
.isAppWindowOnTop(SPLASH_SCREEN, isOptional = true)
.then()
- .isAppWindowOnTop(LAUNCH_NEW_TASK_ACTIVITY)
+ .isAppWindowOnTop(testApp.componentMatcher)
}
}
@@ -225,10 +227,6 @@
super.visibleWindowsShownMoreThanOneConsecutiveEntry()
companion object {
- private val LAUNCH_NEW_TASK_ACTIVITY =
- ActivityOptions.LaunchNewActivity.COMPONENT.toFlickerComponent()
- private val SIMPLE_ACTIVITY = ActivityOptions.SimpleActivity.COMPONENT.toFlickerComponent()
-
private fun getWallpaperPackage(instrumentation: Instrumentation): IComponentMatcher? {
val wallpaperManager = WallpaperManager.getInstance(instrumentation.targetContext)
diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java
index c84c2bc..92ea029 100644
--- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java
+++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java
@@ -18,9 +18,8 @@
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
-import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
-import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED;
+import static com.android.inputmethod.stresstest.ImeStressTestUtil.verifyImeIsAlwaysHidden;
import static com.android.inputmethod.stresstest.ImeStressTestUtil.waitOnMainUntil;
import static com.android.inputmethod.stresstest.ImeStressTestUtil.waitOnMainUntilImeIsShown;
@@ -30,19 +29,27 @@
import android.os.Bundle;
import android.platform.test.annotations.RootPermissionTest;
import android.platform.test.rule.UnlockScreenRule;
+import android.view.WindowManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests to verify the "auto show" behavior in {@code InputMethodManagerService} when the window
+ * gaining the focus to start the input.
+ */
@RootPermissionTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(Parameterized.class)
public final class AutoShowTest {
@Rule
@@ -52,18 +59,95 @@
public ScreenCaptureRule mScreenCaptureRule =
new ScreenCaptureRule("/sdcard/InputMethodStressTest");
+ private static final int[] SOFT_INPUT_VISIBILITY_FLAGS =
+ new int[] {
+ WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED,
+ WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN,
+ WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN,
+ WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE,
+ WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE,
+ };
+
+ private static final int[] SOFT_INPUT_ADJUST_FLAGS =
+ new int[] {
+ WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED,
+ WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE,
+ WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN,
+ WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
+ };
+
+ // TODO(b/240359838): add test case {@code Configuration.SCREENLAYOUT_SIZE_LARGE}.
+ @Parameterized.Parameters(
+ name =
+ "softInputVisibility={0}, softInputAdjustment={1},"
+ + " softInputModeIsForwardNavigation={2}")
+ public static List<Object[]> softInputModeConfigs() {
+ ArrayList<Object[]> params = new ArrayList<>();
+ for (int softInputVisibility : SOFT_INPUT_VISIBILITY_FLAGS) {
+ for (int softInputAdjust : SOFT_INPUT_ADJUST_FLAGS) {
+ params.add(new Object[] {softInputVisibility, softInputAdjust, true});
+ params.add(new Object[] {softInputVisibility, softInputAdjust, false});
+ }
+ }
+ return params;
+ }
+
+ private static final String SOFT_INPUT_FLAGS = "soft_input_flags";
+
+ private final int mSoftInputVisibility;
+ private final int mSoftInputAdjustment;
+ private final boolean mSoftInputIsForwardNavigation;
+
+ public AutoShowTest(
+ int softInputVisibility,
+ int softInputAdjustment,
+ boolean softInputIsForwardNavigation) {
+ mSoftInputVisibility = softInputVisibility;
+ mSoftInputAdjustment = softInputAdjustment;
+ mSoftInputIsForwardNavigation = softInputIsForwardNavigation;
+ }
+
@Test
public void autoShow() {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- Intent intent = new Intent()
- .setAction(Intent.ACTION_MAIN)
- .setClass(instrumentation.getContext(), TestActivity.class)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ int flags = mSoftInputVisibility | mSoftInputAdjustment;
+ if (mSoftInputIsForwardNavigation) {
+ flags |= WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
+ }
+
+ Intent intent =
+ new Intent()
+ .setAction(Intent.ACTION_MAIN)
+ .setClass(instrumentation.getContext(), TestActivity.class)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ .putExtra(SOFT_INPUT_FLAGS, flags);
TestActivity activity = (TestActivity) instrumentation.startActivitySync(intent);
EditText editText = activity.getEditText();
waitOnMainUntil("activity should gain focus", editText::hasWindowFocus);
- waitOnMainUntilImeIsShown(editText);
+
+ if (mSoftInputVisibility == WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
+ || mSoftInputVisibility
+ == WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) {
+ // IME will be auto-shown if softInputMode is set with flag:
+ // SOFT_INPUT_STATE_VISIBLE or SOFT_INPUT_STATE_ALWAYS_VISIBLE
+ waitOnMainUntilImeIsShown(editText);
+ } else if (mSoftInputVisibility == WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN
+ || mSoftInputVisibility
+ == WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN) {
+ // IME will be not be shown if softInputMode is set with flag:
+ // SOFT_INPUT_STATE_HIDDEN or SOFT_INPUT_STATE_ALWAYS_HIDDEN
+ verifyImeIsAlwaysHidden(editText);
+ } else {
+ // The current system behavior will choose to show IME automatically when navigating
+ // forward to an app that has no visibility state specified (i.e.
+ // SOFT_INPUT_STATE_UNSPECIFIED) with set SOFT_INPUT_ADJUST_RESIZE flag.
+ if (mSoftInputVisibility == WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED
+ && mSoftInputAdjustment == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+ && mSoftInputIsForwardNavigation) {
+ waitOnMainUntilImeIsShown(editText);
+ }
+ }
}
public static class TestActivity extends Activity {
@@ -72,16 +156,15 @@
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- // IME will be auto-shown if the following conditions are met:
- // 1. SoftInputMode state is SOFT_INPUT_STATE_UNSPECIFIED.
- // 2. SoftInputMode adjust is SOFT_INPUT_ADJUST_RESIZE.
- getWindow().setSoftInputMode(SOFT_INPUT_STATE_UNSPECIFIED | SOFT_INPUT_ADJUST_RESIZE);
+ int flags = getIntent().getIntExtra(SOFT_INPUT_FLAGS, 0);
+ getWindow().setSoftInputMode(flags);
LinearLayout rootView = new LinearLayout(this);
rootView.setOrientation(LinearLayout.VERTICAL);
mEditText = new EditText(this);
rootView.addView(mEditText, new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
setContentView(rootView);
- // 3. The focused view is a text editor (View#onCheckIsTextEditor() returns true).
+ // Ensure the focused view is a text editor (View#onCheckIsTextEditor() returns true) to
+ // automatically display a soft input window.
mEditText.requestFocus();
}
diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestUtil.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestUtil.java
index ba2ba3c..b6d462c 100644
--- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestUtil.java
+++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestUtil.java
@@ -25,6 +25,8 @@
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.compatibility.common.util.ThrowingRunnable;
+
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
@@ -33,6 +35,7 @@
public final class ImeStressTestUtil {
private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
+ private static final long VERIFY_DURATION = TimeUnit.SECONDS.toMillis(2);
private ImeStressTestUtil() {
}
@@ -77,4 +80,41 @@
eventually(() -> assertWithMessage("IME should be hidden").that(
callOnMainSync(() -> isImeShown(view))).isFalse(), TIMEOUT);
}
+
+ /** Verify IME is always hidden within the given time duration. */
+ public static void verifyImeIsAlwaysHidden(View view) {
+ always(
+ () ->
+ assertWithMessage("IME should be hidden")
+ .that(callOnMainSync(() -> isImeShown(view)))
+ .isFalse(),
+ VERIFY_DURATION);
+ }
+
+ /**
+ * Make sure that a {@link Runnable} always finishes without throwing a {@link Exception} in the
+ * given duration
+ *
+ * @param r The {@link Runnable} to run.
+ * @param timeoutMillis The number of milliseconds to wait for {@code r} to not throw
+ */
+ public static void always(ThrowingRunnable r, long timeoutMillis) {
+ long start = System.currentTimeMillis();
+
+ while (true) {
+ try {
+ r.run();
+ if (System.currentTimeMillis() - start >= timeoutMillis) {
+ return;
+ }
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException ignored) {
+ // Do nothing
+ }
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
}