diff options
218 files changed, 6711 insertions, 1593 deletions
diff --git a/apex/jobscheduler/framework/java/android/app/AlarmManager.java b/apex/jobscheduler/framework/java/android/app/AlarmManager.java index 5a445d476711..dade7c3d84a8 100644 --- a/apex/jobscheduler/framework/java/android/app/AlarmManager.java +++ b/apex/jobscheduler/framework/java/android/app/AlarmManager.java @@ -309,7 +309,7 @@ public class AlarmManager { /** * Callback method that is invoked by the system when the alarm time is reached. */ - public void onAlarm(); + void onAlarm(); } final class ListenerWrapper extends IAlarmListener.Stub implements Runnable { @@ -453,7 +453,7 @@ public class AlarmManager { * @see #RTC * @see #RTC_WAKEUP */ - public void set(@AlarmType int type, long triggerAtMillis, PendingIntent operation) { + public void set(@AlarmType int type, long triggerAtMillis, @NonNull PendingIntent operation) { setImpl(type, triggerAtMillis, legacyExactLength(), 0, 0, operation, null, null, (Handler) null, null, null); } @@ -480,8 +480,8 @@ public class AlarmManager { * @param targetHandler {@link Handler} on which to execute the listener's onAlarm() * callback, or {@code null} to run that callback on the main looper. */ - public void set(@AlarmType int type, long triggerAtMillis, String tag, OnAlarmListener listener, - Handler targetHandler) { + public void set(@AlarmType int type, long triggerAtMillis, @Nullable String tag, + @NonNull OnAlarmListener listener, @Nullable Handler targetHandler) { setImpl(type, triggerAtMillis, legacyExactLength(), 0, 0, null, listener, tag, targetHandler, null, null); } @@ -546,7 +546,7 @@ public class AlarmManager { * @see Intent#EXTRA_ALARM_COUNT */ public void setRepeating(@AlarmType int type, long triggerAtMillis, - long intervalMillis, PendingIntent operation) { + long intervalMillis, @NonNull PendingIntent operation) { setImpl(type, triggerAtMillis, legacyExactLength(), intervalMillis, 0, operation, null, null, (Handler) null, null, null); } @@ -602,7 +602,7 @@ public class AlarmManager { * @see #RTC_WAKEUP */ public void setWindow(@AlarmType int type, long windowStartMillis, long windowLengthMillis, - PendingIntent operation) { + @NonNull PendingIntent operation) { setImpl(type, windowStartMillis, windowLengthMillis, 0, 0, operation, null, null, (Handler) null, null, null); } @@ -625,12 +625,62 @@ public class AlarmManager { * @see #setWindow(int, long, long, PendingIntent) */ public void setWindow(@AlarmType int type, long windowStartMillis, long windowLengthMillis, - String tag, OnAlarmListener listener, Handler targetHandler) { + @Nullable String tag, @NonNull OnAlarmListener listener, + @Nullable Handler targetHandler) { setImpl(type, windowStartMillis, windowLengthMillis, 0, 0, null, listener, tag, targetHandler, null, null); } /** + * Direct callback version of {@link #setWindow(int, long, long, PendingIntent)}. Rather + * than supplying a PendingIntent to be sent when the alarm time is reached, this variant + * supplies an {@link OnAlarmListener} instance that will be invoked at that time. + * <p> + * The OnAlarmListener {@link OnAlarmListener#onAlarm() onAlarm()} method will be + * invoked via the specified target Executor. + * + * <p> + * Note: Starting with API {@link Build.VERSION_CODES#S}, apps should not pass in a window of + * less than 10 minutes. The system will try its best to accommodate smaller windows if the + * alarm is supposed to fire in the near future, but there are no guarantees and the app should + * expect any window smaller than 10 minutes to get elongated to 10 minutes. + * + * @see #setWindow(int, long, long, PendingIntent) + */ + public void setWindow(@AlarmType int type, long windowStartMillis, long windowLengthMillis, + @Nullable String tag, @NonNull Executor executor, @NonNull OnAlarmListener listener) { + setImpl(type, windowStartMillis, windowLengthMillis, 0, 0, null, listener, tag, + executor, null, null); + } + + /** + * Direct callback version of {@link #setWindow(int, long, long, PendingIntent)}. Rather + * than supplying a PendingIntent to be sent when the alarm time is reached, this variant + * supplies an {@link OnAlarmListener} instance that will be invoked at that time. + * <p> + * The OnAlarmListener {@link OnAlarmListener#onAlarm() onAlarm()} method will be + * invoked via the specified target Executor. + * + * <p> + * Note: Starting with API {@link Build.VERSION_CODES#S}, apps should not pass in a window of + * less than 10 minutes. The system will try its best to accommodate smaller windows if the + * alarm is supposed to fire in the near future, but there are no guarantees and the app should + * expect any window smaller than 10 minutes to get elongated to 10 minutes. + * + * @see #setWindow(int, long, long, PendingIntent) + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) + public void setWindow(@AlarmType int type, long windowStartMillis, long windowLengthMillis, + @Nullable String tag, @NonNull Executor executor, @Nullable WorkSource workSource, + @NonNull OnAlarmListener listener) { + setImpl(type, windowStartMillis, windowLengthMillis, 0, 0, null, listener, tag, + executor, workSource, null); + } + + /** * Schedule an alarm that is prioritized by the system while the device is in power saving modes * such as battery saver and device idle (doze). * @@ -725,7 +775,8 @@ public class AlarmManager { * @see Manifest.permission#SCHEDULE_EXACT_ALARM SCHEDULE_EXACT_ALARM */ @RequiresPermission(value = Manifest.permission.SCHEDULE_EXACT_ALARM, conditional = true) - public void setExact(@AlarmType int type, long triggerAtMillis, PendingIntent operation) { + public void setExact(@AlarmType int type, long triggerAtMillis, + @NonNull PendingIntent operation) { setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, 0, operation, null, null, (Handler) null, null, null); } @@ -756,8 +807,8 @@ public class AlarmManager { * @see Manifest.permission#SCHEDULE_EXACT_ALARM SCHEDULE_EXACT_ALARM */ @RequiresPermission(value = Manifest.permission.SCHEDULE_EXACT_ALARM, conditional = true) - public void setExact(@AlarmType int type, long triggerAtMillis, String tag, - OnAlarmListener listener, Handler targetHandler) { + public void setExact(@AlarmType int type, long triggerAtMillis, @Nullable String tag, + @NonNull OnAlarmListener listener, @Nullable Handler targetHandler) { setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, 0, null, listener, tag, targetHandler, null, null); } @@ -767,8 +818,8 @@ public class AlarmManager { * the given time. * @hide */ - public void setIdleUntil(@AlarmType int type, long triggerAtMillis, String tag, - OnAlarmListener listener, Handler targetHandler) { + public void setIdleUntil(@AlarmType int type, long triggerAtMillis, @Nullable String tag, + @NonNull OnAlarmListener listener, @Nullable Handler targetHandler) { setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_IDLE_UNTIL, null, listener, tag, targetHandler, null, null); } @@ -828,7 +879,7 @@ public class AlarmManager { * @see Manifest.permission#SCHEDULE_EXACT_ALARM SCHEDULE_EXACT_ALARM */ @RequiresPermission(Manifest.permission.SCHEDULE_EXACT_ALARM) - public void setAlarmClock(AlarmClockInfo info, PendingIntent operation) { + public void setAlarmClock(@NonNull AlarmClockInfo info, @NonNull PendingIntent operation) { setImpl(RTC_WAKEUP, info.getTriggerTime(), WINDOW_EXACT, 0, 0, operation, null, null, (Handler) null, null, info); } @@ -837,7 +888,8 @@ public class AlarmManager { @SystemApi @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public void set(@AlarmType int type, long triggerAtMillis, long windowMillis, - long intervalMillis, PendingIntent operation, WorkSource workSource) { + long intervalMillis, @NonNull PendingIntent operation, + @Nullable WorkSource workSource) { setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, operation, null, null, (Handler) null, workSource, null); } @@ -854,8 +906,8 @@ public class AlarmManager { */ @UnsupportedAppUsage public void set(@AlarmType int type, long triggerAtMillis, long windowMillis, - long intervalMillis, String tag, OnAlarmListener listener, Handler targetHandler, - WorkSource workSource) { + long intervalMillis, @Nullable String tag, @NonNull OnAlarmListener listener, + @Nullable Handler targetHandler, @Nullable WorkSource workSource) { setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, null, listener, tag, targetHandler, workSource, null); } @@ -873,8 +925,8 @@ public class AlarmManager { @SystemApi @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public void set(@AlarmType int type, long triggerAtMillis, long windowMillis, - long intervalMillis, OnAlarmListener listener, Handler targetHandler, - WorkSource workSource) { + long intervalMillis, @NonNull OnAlarmListener listener, @Nullable Handler targetHandler, + @Nullable WorkSource workSource) { setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, null, listener, null, targetHandler, workSource, null); } @@ -1072,7 +1124,7 @@ public class AlarmManager { * @see Intent#EXTRA_ALARM_COUNT */ public void setInexactRepeating(@AlarmType int type, long triggerAtMillis, - long intervalMillis, PendingIntent operation) { + long intervalMillis, @NonNull PendingIntent operation) { setImpl(type, triggerAtMillis, WINDOW_HEURISTIC, intervalMillis, 0, operation, null, null, (Handler) null, null, null); } @@ -1122,7 +1174,7 @@ public class AlarmManager { * @see #RTC_WAKEUP */ public void setAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis, - PendingIntent operation) { + @NonNull PendingIntent operation) { setImpl(type, triggerAtMillis, WINDOW_HEURISTIC, 0, FLAG_ALLOW_WHILE_IDLE, operation, null, null, (Handler) null, null, null); } @@ -1195,12 +1247,46 @@ public class AlarmManager { */ @RequiresPermission(value = Manifest.permission.SCHEDULE_EXACT_ALARM, conditional = true) public void setExactAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis, - PendingIntent operation) { + @NonNull PendingIntent operation) { setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_ALLOW_WHILE_IDLE, operation, null, null, (Handler) null, null, null); } /** + * Like {@link #setExact(int, long, String, Executor, WorkSource, OnAlarmListener)}, but this + * alarm will be allowed to execute even when the system is in low-power idle modes. + * + * <p> See {@link #setExactAndAllowWhileIdle(int, long, PendingIntent)} for more details. + * + * @param type type of alarm + * @param triggerAtMillis The exact time in milliseconds, that the alarm should be delivered, + * expressed in the appropriate clock's units (depending on the alarm + * type). + * @param listener {@link OnAlarmListener} instance whose + * {@link OnAlarmListener#onAlarm() onAlarm()} method will be called when + * the alarm time is reached. + * @param executor The {@link Executor} on which to execute the listener's onAlarm() + * callback. + * @param tag Optional. A string tag used to identify this alarm in logs and + * battery-attribution. + * @param workSource A {@link WorkSource} object to attribute this alarm to the app that + * requested this work. + * @hide + */ + @SystemApi + @RequiresPermission(allOf = { + Manifest.permission.UPDATE_DEVICE_STATS, + Manifest.permission.SCHEDULE_EXACT_ALARM}, conditional = true) + public void setExactAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis, + @Nullable String tag, @NonNull Executor executor, @Nullable WorkSource workSource, + @NonNull OnAlarmListener listener) { + Objects.requireNonNull(executor); + Objects.requireNonNull(listener); + setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_ALLOW_WHILE_IDLE, null, listener, tag, + executor, workSource, null); + } + + /** * Remove any alarms with a matching {@link Intent}. * Any alarm, of any type, whose Intent matches this one (as defined by * {@link Intent#filterEquals}), will be canceled. @@ -1210,7 +1296,7 @@ public class AlarmManager { * * @see #set */ - public void cancel(PendingIntent operation) { + public void cancel(@NonNull PendingIntent operation) { if (operation == null) { final String msg = "cancel() called with a null PendingIntent"; if (mTargetSdkVersion >= Build.VERSION_CODES.N) { @@ -1233,7 +1319,7 @@ public class AlarmManager { * * @param listener OnAlarmListener instance that is the target of a currently-set alarm. */ - public void cancel(OnAlarmListener listener) { + public void cancel(@NonNull OnAlarmListener listener) { if (listener == null) { throw new NullPointerException("cancel() called with a null OnAlarmListener"); } diff --git a/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java b/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java index 299ad66a882c..4a3a6d912966 100644 --- a/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java +++ b/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java @@ -113,7 +113,9 @@ public class EconomyManager { /** @hide */ public static final String KEY_AM_INITIAL_CONSUMPTION_LIMIT = "am_initial_consumption_limit"; /** @hide */ - public static final String KEY_AM_HARD_CONSUMPTION_LIMIT = "am_hard_consumption_limit"; + public static final String KEY_AM_MIN_CONSUMPTION_LIMIT = "am_minimum_consumption_limit"; + /** @hide */ + public static final String KEY_AM_MAX_CONSUMPTION_LIMIT = "am_maximum_consumption_limit"; // TODO: Add AlarmManager modifier keys /** @hide */ public static final String KEY_AM_REWARD_TOP_ACTIVITY_INSTANT = @@ -242,7 +244,9 @@ public class EconomyManager { /** @hide */ public static final String KEY_JS_INITIAL_CONSUMPTION_LIMIT = "js_initial_consumption_limit"; /** @hide */ - public static final String KEY_JS_HARD_CONSUMPTION_LIMIT = "js_hard_consumption_limit"; + public static final String KEY_JS_MIN_CONSUMPTION_LIMIT = "js_minimum_consumption_limit"; + /** @hide */ + public static final String KEY_JS_MAX_CONSUMPTION_LIMIT = "js_maximum_consumption_limit"; // TODO: Add JobScheduler modifier keys /** @hide */ public static final String KEY_JS_REWARD_APP_INSTALL_INSTANT = @@ -371,7 +375,9 @@ public class EconomyManager { /** @hide */ public static final long DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT_CAKES = arcToCake(2880); /** @hide */ - public static final long DEFAULT_AM_HARD_CONSUMPTION_LIMIT_CAKES = arcToCake(15_000); + public static final long DEFAULT_AM_MIN_CONSUMPTION_LIMIT_CAKES = arcToCake(1440); + /** @hide */ + public static final long DEFAULT_AM_MAX_CONSUMPTION_LIMIT_CAKES = arcToCake(15_000); // TODO: add AlarmManager modifier default values /** @hide */ public static final long DEFAULT_AM_REWARD_TOP_ACTIVITY_INSTANT_CAKES = arcToCake(0); @@ -478,8 +484,10 @@ public class EconomyManager { /** @hide */ public static final long DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES = arcToCake(29_000); /** @hide */ - // TODO: set hard limit based on device type (phone vs tablet vs etc) + battery size - public static final long DEFAULT_JS_HARD_CONSUMPTION_LIMIT_CAKES = arcToCake(250_000); + public static final long DEFAULT_JS_MIN_CONSUMPTION_LIMIT_CAKES = arcToCake(17_000); + /** @hide */ + // TODO: set maximum limit based on device type (phone vs tablet vs etc) + battery size + public static final long DEFAULT_JS_MAX_CONSUMPTION_LIMIT_CAKES = arcToCake(250_000); // TODO: add JobScheduler modifier default values /** @hide */ public static final long DEFAULT_JS_REWARD_APP_INSTALL_INSTANT_CAKES = arcToCake(408); 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 6375d0deae40..d9fb318c9335 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -432,6 +432,7 @@ public class JobSchedulerService extends com.android.server.SystemService break; case Constants.KEY_MIN_LINEAR_BACKOFF_TIME_MS: case Constants.KEY_MIN_EXP_BACKOFF_TIME_MS: + case Constants.KEY_SYSTEM_STOP_TO_FAILURE_RATIO: mConstants.updateBackoffConstantsLocked(); break; case Constants.KEY_CONN_CONGESTION_DELAY_FRAC: @@ -509,6 +510,8 @@ public class JobSchedulerService extends com.android.server.SystemService private static final String KEY_MIN_LINEAR_BACKOFF_TIME_MS = "min_linear_backoff_time_ms"; private static final String KEY_MIN_EXP_BACKOFF_TIME_MS = "min_exp_backoff_time_ms"; + private static final String KEY_SYSTEM_STOP_TO_FAILURE_RATIO = + "system_stop_to_failure_ratio"; private static final String KEY_CONN_CONGESTION_DELAY_FRAC = "conn_congestion_delay_frac"; private static final String KEY_CONN_PREFETCH_RELAX_FRAC = "conn_prefetch_relax_frac"; private static final String KEY_CONN_USE_CELL_SIGNAL_STRENGTH = @@ -540,6 +543,7 @@ public class JobSchedulerService extends com.android.server.SystemService private static final float DEFAULT_MODERATE_USE_FACTOR = .5f; private static final long DEFAULT_MIN_LINEAR_BACKOFF_TIME_MS = JobInfo.MIN_BACKOFF_MILLIS; private static final long DEFAULT_MIN_EXP_BACKOFF_TIME_MS = JobInfo.MIN_BACKOFF_MILLIS; + private static final int DEFAULT_SYSTEM_STOP_TO_FAILURE_RATIO = 3; private static final float DEFAULT_CONN_CONGESTION_DELAY_FRAC = 0.5f; private static final float DEFAULT_CONN_PREFETCH_RELAX_FRAC = 0.5f; private static final boolean DEFAULT_CONN_USE_CELL_SIGNAL_STRENGTH = true; @@ -589,6 +593,11 @@ public class JobSchedulerService extends com.android.server.SystemService * The minimum backoff time to allow for exponential backoff. */ long MIN_EXP_BACKOFF_TIME_MS = DEFAULT_MIN_EXP_BACKOFF_TIME_MS; + /** + * The ratio to use to convert number of times a job was stopped by JobScheduler to an + * incremental failure in the backoff policy calculation. + */ + int SYSTEM_STOP_TO_FAILURE_RATIO = DEFAULT_SYSTEM_STOP_TO_FAILURE_RATIO; /** * The fraction of a job's running window that must pass before we @@ -700,6 +709,9 @@ public class JobSchedulerService extends com.android.server.SystemService MIN_EXP_BACKOFF_TIME_MS = DeviceConfig.getLong(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MIN_EXP_BACKOFF_TIME_MS, DEFAULT_MIN_EXP_BACKOFF_TIME_MS); + SYSTEM_STOP_TO_FAILURE_RATIO = DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_SYSTEM_STOP_TO_FAILURE_RATIO, + DEFAULT_SYSTEM_STOP_TO_FAILURE_RATIO); } private void updateConnectivityConstantsLocked() { @@ -797,6 +809,7 @@ public class JobSchedulerService extends com.android.server.SystemService pw.print(KEY_MIN_LINEAR_BACKOFF_TIME_MS, MIN_LINEAR_BACKOFF_TIME_MS).println(); pw.print(KEY_MIN_EXP_BACKOFF_TIME_MS, MIN_EXP_BACKOFF_TIME_MS).println(); + pw.print(KEY_SYSTEM_STOP_TO_FAILURE_RATIO, SYSTEM_STOP_TO_FAILURE_RATIO).println(); pw.print(KEY_CONN_CONGESTION_DELAY_FRAC, CONN_CONGESTION_DELAY_FRAC).println(); pw.print(KEY_CONN_PREFETCH_RELAX_FRAC, CONN_PREFETCH_RELAX_FRAC).println(); pw.print(KEY_CONN_USE_CELL_SIGNAL_STRENGTH, CONN_USE_CELL_SIGNAL_STRENGTH).println(); @@ -1277,7 +1290,7 @@ public class JobSchedulerService extends com.android.server.SystemService jobStatus.getJob().isPrefetch(), jobStatus.getJob().getPriority(), jobStatus.getEffectivePriority(), - jobStatus.getNumFailures()); + jobStatus.getNumPreviousAttempts()); // If the job is immediately ready to run, then we can just immediately // put it in the pending list and try to schedule it. This is especially @@ -1476,7 +1489,7 @@ public class JobSchedulerService extends com.android.server.SystemService cancelled.getJob().isPrefetch(), cancelled.getJob().getPriority(), cancelled.getEffectivePriority(), - cancelled.getNumFailures()); + cancelled.getNumPreviousAttempts()); } // If this is a replacement, bring in the new version of the job if (incomingJob != null) { @@ -1820,12 +1833,13 @@ public class JobSchedulerService extends com.android.server.SystemService Slog.wtf(TAG, "Not yet prepared when started tracking: " + jobStatus); } jobStatus.enqueueTime = sElapsedRealtimeClock.millis(); - final boolean update = mJobs.add(jobStatus); + final boolean update = lastJob != null; + mJobs.add(jobStatus); if (mReadyToRock) { for (int i = 0; i < mControllers.size(); i++) { StateController controller = mControllers.get(i); if (update) { - controller.maybeStopTrackingJobLocked(jobStatus, null, true); + controller.maybeStopTrackingJobLocked(jobStatus, null); } controller.maybeStartTrackingJobLocked(jobStatus, lastJob); } @@ -1858,7 +1872,7 @@ public class JobSchedulerService extends com.android.server.SystemService if (mReadyToRock) { for (int i = 0; i < mControllers.size(); i++) { StateController controller = mControllers.get(i); - controller.maybeStopTrackingJobLocked(jobStatus, incomingJob, false); + controller.maybeStopTrackingJobLocked(jobStatus, incomingJob); } } return removed; @@ -1903,7 +1917,7 @@ public class JobSchedulerService extends com.android.server.SystemService * Reschedules the given job based on the job's backoff policy. It doesn't make sense to * specify an override deadline on a failed job (the failed job will run even though it's not * ready), so we reschedule it with {@link JobStatus#NO_LATEST_RUNTIME}, but specify that any - * ready job with {@link JobStatus#getNumFailures()} > 0 will be executed. + * ready job with {@link JobStatus#getNumPreviousAttempts()} > 0 will be executed. * * @param failureToReschedule Provided job status that we will reschedule. * @return A newly instantiated JobStatus with the same constraints as the last job except @@ -1911,12 +1925,24 @@ public class JobSchedulerService extends com.android.server.SystemService * @see #maybeQueueReadyJobsForExecutionLocked */ @VisibleForTesting - JobStatus getRescheduleJobForFailureLocked(JobStatus failureToReschedule) { + JobStatus getRescheduleJobForFailureLocked(JobStatus failureToReschedule, + int internalStopReason) { final long elapsedNowMillis = sElapsedRealtimeClock.millis(); final JobInfo job = failureToReschedule.getJob(); final long initialBackoffMillis = job.getInitialBackoffMillis(); - final int backoffAttempts = failureToReschedule.getNumFailures() + 1; + int numFailures = failureToReschedule.getNumFailures(); + int numSystemStops = failureToReschedule.getNumSystemStops(); + // We should back off slowly if JobScheduler keeps stopping the job, + // but back off immediately if the issue appeared to be the app's fault. + if (internalStopReason == JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH + || internalStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT) { + numFailures++; + } else { + numSystemStops++; + } + final int backoffAttempts = Math.max(1, + numFailures + numSystemStops / mConstants.SYSTEM_STOP_TO_FAILURE_RATIO); long delayMillis; switch (job.getBackoffPolicy()) { @@ -1943,7 +1969,7 @@ public class JobSchedulerService extends com.android.server.SystemService Math.min(delayMillis, JobInfo.MAX_BACKOFF_DELAY_MILLIS); JobStatus newJob = new JobStatus(failureToReschedule, elapsedNowMillis + delayMillis, - JobStatus.NO_LATEST_RUNTIME, backoffAttempts, + JobStatus.NO_LATEST_RUNTIME, numFailures, numSystemStops, failureToReschedule.getLastSuccessfulRunTime(), sSystemClock.millis()); if (job.isPeriodic()) { newJob.setOriginalLatestRunTimeElapsed( @@ -2034,7 +2060,7 @@ public class JobSchedulerService extends com.android.server.SystemService + newLatestRuntimeElapsed); return new JobStatus(periodicToReschedule, elapsedNow + period - flex, elapsedNow + period, - 0 /* backoffAttempt */, + 0 /* numFailures */, 0 /* numSystemStops */, sSystemClock.millis() /* lastSuccessfulRunTime */, periodicToReschedule.getLastFailedRunTime()); } @@ -2049,7 +2075,7 @@ public class JobSchedulerService extends com.android.server.SystemService } return new JobStatus(periodicToReschedule, newEarliestRunTimeElapsed, newLatestRuntimeElapsed, - 0 /* backoffAttempt */, + 0 /* numFailures */, 0 /* numSystemStops */, sSystemClock.millis() /* lastSuccessfulRunTime */, periodicToReschedule.getLastFailedRunTime()); } @@ -2093,7 +2119,7 @@ public class JobSchedulerService extends com.android.server.SystemService // job so we can transfer any appropriate state over from the previous job when // we stop it. final JobStatus rescheduledJob = needsReschedule - ? getRescheduleJobForFailureLocked(jobStatus) : null; + ? getRescheduleJobForFailureLocked(jobStatus, debugStopReason) : null; if (rescheduledJob != null && (debugStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT || debugStopReason == JobParameters.INTERNAL_STOP_REASON_PREEMPT)) { @@ -2427,7 +2453,7 @@ public class JobSchedulerService extends com.android.server.SystemService shouldForceBatchJob = mPrefetchController.getNextEstimatedLaunchTimeLocked(job) > relativelySoonCutoffTime; - } else if (job.getNumFailures() > 0) { + } else if (job.getNumPreviousAttempts() > 0) { shouldForceBatchJob = false; } else { final long nowElapsed = sElapsedRealtimeClock.millis(); diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java index d6456f0edea5..9e3f19de56f1 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -363,7 +363,7 @@ public final class JobServiceContext implements ServiceConnection { job.getJob().isPrefetch(), job.getJob().getPriority(), job.getEffectivePriority(), - job.getNumFailures()); + job.getNumPreviousAttempts()); if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { // Use the context's ID to distinguish traces since there'll only be one job // running per context. @@ -1032,7 +1032,7 @@ public final class JobServiceContext implements ServiceConnection { completedJob.getJob().isPrefetch(), completedJob.getJob().getPriority(), completedJob.getEffectivePriority(), - completedJob.getNumFailures()); + completedJob.getNumPreviousAttempts()); if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_SYSTEM_SERVER, "JobScheduler", completedJob.getTag(), getId()); 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 22b09683187f..68cb049af758 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java @@ -194,7 +194,7 @@ public final class JobStore { convertRtcBoundsToElapsed(utcTimes, elapsedNow); JobStatus newJob = new JobStatus(job, elapsedRuntimes.first, elapsedRuntimes.second, - 0, job.getLastSuccessfulRunTime(), job.getLastFailedRunTime()); + 0, 0, job.getLastSuccessfulRunTime(), job.getLastFailedRunTime()); newJob.prepareLocked(); toAdd.add(newJob); toRemove.add(job); @@ -203,13 +203,12 @@ public final class JobStore { } /** - * Add a job to the master list, persisting it if necessary. If the JobStatus already exists, - * it will be replaced. + * Add a job to the master list, persisting it if necessary. + * Similar jobs to the new job will not be removed. + * * @param jobStatus Job to add. - * @return Whether or not an equivalent JobStatus was replaced by this operation. */ - public boolean add(JobStatus jobStatus) { - boolean replaced = mJobSet.remove(jobStatus); + public void add(JobStatus jobStatus) { mJobSet.add(jobStatus); if (jobStatus.isPersisted()) { maybeWriteStatusToDiskAsync(); @@ -217,7 +216,6 @@ public final class JobStore { if (DEBUG) { Slog.d(TAG, "Added job status to store: " + jobStatus); } - return replaced; } /** diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java index 65d712102e94..ecee10a13c1d 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java @@ -81,8 +81,7 @@ public final class BackgroundJobsController extends StateController { } @Override - public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, - boolean forUpdate) { + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) { } @Override diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java index d284a99a4559..2ca3f8f731e9 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java @@ -133,7 +133,7 @@ public final class BatteryController extends RestrictingController { } @Override - public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) { + public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob) { if (taskStatus.clearTrackingController(JobStatus.TRACKING_BATTERY)) { mTrackedTasks.remove(taskStatus); mTopStartedJobs.remove(taskStatus); @@ -143,7 +143,7 @@ public final class BatteryController extends RestrictingController { @Override public void stopTrackingRestrictedJobLocked(JobStatus jobStatus) { if (!jobStatus.hasPowerConstraint()) { - maybeStopTrackingJobLocked(jobStatus, null, false); + maybeStopTrackingJobLocked(jobStatus, null); } } diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java index 9b5956094e48..b029e0075dc2 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java @@ -127,8 +127,7 @@ public class ComponentController extends StateController { } @Override - public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, - boolean forUpdate) { + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) { } @Override diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java index d2dc2a7e4e96..16dd16727fa6 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java @@ -290,8 +290,7 @@ public final class ConnectivityController extends RestrictingController implemen @GuardedBy("mLock") @Override - public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, - boolean forUpdate) { + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) { if (jobStatus.clearTrackingController(JobStatus.TRACKING_CONNECTIVITY)) { ArraySet<JobStatus> jobs = mTrackedJobs.get(jobStatus.getSourceUid()); if (jobs != null) { diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java index 83a756cf1e11..847a1bfe4465 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java @@ -159,8 +159,7 @@ public final class ContentObserverController extends StateController { } @Override - public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, - boolean forUpdate) { + public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob) { if (taskStatus.clearTrackingController(JobStatus.TRACKING_CONTENT)) { mTrackedTasks.remove(taskStatus); if (taskStatus.contentObserverJobInstance != null) { diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java index abbe177c5d49..bdf72b64d3e0 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java @@ -225,8 +225,7 @@ public final class DeviceIdleJobsController extends StateController { } @Override - public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, - boolean forUpdate) { + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) { if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) { mAllowInIdleJobs.remove(jobStatus); } 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 547f94badd69..4c176921ed14 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 @@ -213,7 +213,7 @@ public final class FlexibilityController extends StateController { @Override @GuardedBy("mLock") - public void maybeStopTrackingJobLocked(JobStatus js, JobStatus incomingJob, boolean forUpdate) { + public void maybeStopTrackingJobLocked(JobStatus js, JobStatus incomingJob) { if (js.clearTrackingController(JobStatus.TRACKING_FLEXIBILITY)) { mFlexibilityAlarmQueue.removeAlarmForKey(js); mFlexibilityTracker.remove(js); @@ -342,10 +342,10 @@ public final class FlexibilityController extends StateController { // There is no deadline and no estimated launch time. return NO_LIFECYCLE_END; } - if (js.getNumFailures() > 1) { - // Number of failures will not equal one as per restriction in JobStatus constructor. + // Increase the flex deadline for jobs rescheduled more than once. + if (js.getNumPreviousAttempts() > 1) { return earliest + Math.min( - (long) Math.scalb(mRescheduledJobDeadline, js.getNumFailures() - 2), + (long) Math.scalb(mRescheduledJobDeadline, js.getNumPreviousAttempts() - 2), mMaxRescheduledDeadline); } return js.getLatestRunTimeElapsed() == JobStatus.NO_LATEST_RUNTIME diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java index 926cfc192cd1..a25af7110ee5 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java @@ -76,8 +76,7 @@ public final class IdleController extends RestrictingController implements Idlen } @Override - public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, - boolean forUpdate) { + public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob) { if (taskStatus.clearTrackingController(JobStatus.TRACKING_IDLE)) { mTrackedTasks.remove(taskStatus); } @@ -86,7 +85,7 @@ public final class IdleController extends RestrictingController implements Idlen @Override public void stopTrackingRestrictedJobLocked(JobStatus jobStatus) { if (!jobStatus.hasIdleConstraint()) { - maybeStopTrackingJobLocked(jobStatus, null, false); + maybeStopTrackingJobLocked(jobStatus, null); } } 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 de602a8f7f25..f9fb0d0df002 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 @@ -241,10 +241,22 @@ public final class JobStatus { */ private long mOriginalLatestRunTimeElapsedMillis; - /** How many times this job has failed, used to compute back-off. */ + /** + * How many times this job has failed to complete on its own + * (via {@link android.app.job.JobService#jobFinished(JobParameters, boolean)} or because of + * a timeout). + * This count doesn't include most times JobScheduler decided to stop the job + * (via {@link android.app.job.JobService#onStopJob(JobParameters)}. + */ private final int numFailures; /** + * The number of times JobScheduler has forced this job to stop due to reasons mostly outside + * of the app's control. + */ + private final int mNumSystemStops; + + /** * Which app standby bucket this job's app is in. Updated when the app is moved to a * different bucket. */ @@ -488,6 +500,8 @@ public final class JobStatus { * @param tag A string associated with the job for debugging/logging purposes. * @param numFailures Count of how many times this job has requested a reschedule because * its work was not yet finished. + * @param numSystemStops Count of how many times JobScheduler has forced this job to stop due to + * factors mostly out of the app's control. * @param earliestRunTimeElapsedMillis Milestone: earliest point in time at which the job * is to be considered runnable * @param latestRunTimeElapsedMillis Milestone: point in time at which the job will be @@ -497,7 +511,7 @@ public final class JobStatus { * @param internalFlags Non-API property flags about this job */ private JobStatus(JobInfo job, int callingUid, String sourcePackageName, - int sourceUserId, int standbyBucket, String tag, int numFailures, + int sourceUserId, int standbyBucket, String tag, int numFailures, int numSystemStops, long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis, long lastSuccessfulRunTime, long lastFailedRunTime, int internalFlags, int dynamicConstraints) { @@ -535,6 +549,7 @@ public final class JobStatus { this.latestRunTimeElapsedMillis = latestRunTimeElapsedMillis; this.mOriginalLatestRunTimeElapsedMillis = latestRunTimeElapsedMillis; this.numFailures = numFailures; + mNumSystemStops = numSystemStops; int requiredConstraints = job.getConstraintFlags(); if (job.getRequiredNetwork() != null) { @@ -576,7 +591,7 @@ public final class JobStatus { // Otherwise, every consecutive reschedule increases a jobs' flexibility deadline. if (!isRequestedExpeditedJob() && satisfiesMinWindowException - && numFailures != 1 + && (numFailures + numSystemStops) != 1 && lacksSomeFlexibleConstraints) { mNumRequiredFlexibleConstraints = NUM_SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS + (mPreferUnmetered ? 1 : 0); @@ -626,7 +641,7 @@ public final class JobStatus { this(jobStatus.getJob(), jobStatus.getUid(), jobStatus.getSourcePackageName(), jobStatus.getSourceUserId(), jobStatus.getStandbyBucket(), - jobStatus.getSourceTag(), jobStatus.getNumFailures(), + jobStatus.getSourceTag(), jobStatus.getNumFailures(), jobStatus.getNumSystemStops(), jobStatus.getEarliestRunTime(), jobStatus.getLatestRunTimeElapsed(), jobStatus.getLastSuccessfulRunTime(), jobStatus.getLastFailedRunTime(), jobStatus.getInternalFlags(), jobStatus.mDynamicConstraints); @@ -654,7 +669,7 @@ public final class JobStatus { int innerFlags, int dynamicConstraints) { this(job, callingUid, sourcePkgName, sourceUserId, standbyBucket, - sourceTag, 0, + sourceTag, /* numFailures */ 0, /* numSystemStops */ 0, earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis, lastSuccessfulRunTime, lastFailedRunTime, innerFlags, dynamicConstraints); @@ -673,12 +688,13 @@ public final class JobStatus { /** Create a new job to be rescheduled with the provided parameters. */ public JobStatus(JobStatus rescheduling, long newEarliestRuntimeElapsedMillis, - long newLatestRuntimeElapsedMillis, int backoffAttempt, + long newLatestRuntimeElapsedMillis, int numFailures, int numSystemStops, long lastSuccessfulRunTime, long lastFailedRunTime) { this(rescheduling.job, rescheduling.getUid(), rescheduling.getSourcePackageName(), rescheduling.getSourceUserId(), rescheduling.getStandbyBucket(), - rescheduling.getSourceTag(), backoffAttempt, newEarliestRuntimeElapsedMillis, + rescheduling.getSourceTag(), numFailures, numSystemStops, + newEarliestRuntimeElapsedMillis, newLatestRuntimeElapsedMillis, lastSuccessfulRunTime, lastFailedRunTime, rescheduling.getInternalFlags(), rescheduling.mDynamicConstraints); @@ -715,7 +731,7 @@ public final class JobStatus { int standbyBucket = JobSchedulerService.standbyBucketForPackage(jobPackage, sourceUserId, elapsedNow); return new JobStatus(job, callingUid, sourcePkg, sourceUserId, - standbyBucket, tag, 0, + standbyBucket, tag, /* numFailures */ 0, /* numSystemStops */ 0, earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis, 0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */, /*innerFlags=*/ 0, /* dynamicConstraints */ 0); @@ -868,10 +884,27 @@ public final class JobStatus { pw.print(job.getId()); } + /** + * Returns the number of times the job stopped previously for reasons that appeared to be within + * the app's control. + */ public int getNumFailures() { return numFailures; } + /** + * Returns the number of times the system stopped a previous execution of this job for reasons + * that were likely outside the app's control. + */ + public int getNumSystemStops() { + return mNumSystemStops; + } + + /** Returns the total number of times we've attempted to run this job in the past. */ + public int getNumPreviousAttempts() { + return numFailures + mNumSystemStops; + } + public ComponentName getServiceComponent() { return job.getService(); } @@ -1857,6 +1890,10 @@ public final class JobStatus { sb.append(" failures="); sb.append(numFailures); } + if (mNumSystemStops != 0) { + sb.append(" system stops="); + sb.append(mNumSystemStops); + } if (isReady()) { sb.append(" READY"); } else { @@ -2382,6 +2419,9 @@ public final class JobStatus { if (numFailures != 0) { pw.print("Num failures: "); pw.println(numFailures); } + if (mNumSystemStops != 0) { + pw.print("Num system stops: "); pw.println(mNumSystemStops); + } if (mLastSuccessfulRunTime != 0) { pw.print("Last successful run: "); pw.println(formatTime(mLastSuccessfulRunTime)); @@ -2579,7 +2619,7 @@ public final class JobStatus { proto.write(JobStatusDumpProto.ORIGINAL_LATEST_RUNTIME_ELAPSED, mOriginalLatestRunTimeElapsedMillis); - proto.write(JobStatusDumpProto.NUM_FAILURES, numFailures); + proto.write(JobStatusDumpProto.NUM_FAILURES, numFailures + mNumSystemStops); proto.write(JobStatusDumpProto.LAST_SUCCESSFUL_RUN_TIME, mLastSuccessfulRunTime); proto.write(JobStatusDumpProto.LAST_FAILED_RUN_TIME, mLastFailedRunTime); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java index e04cec30d26b..d69b9e084c2b 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java @@ -167,8 +167,7 @@ public class PrefetchController extends StateController { @Override @GuardedBy("mLock") - public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, - boolean forUpdate) { + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) { final int userId = jobStatus.getSourceUserId(); final String pkgName = jobStatus.getSourcePackageName(); final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java index bb8d175c2375..659e7c0fde84 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java @@ -673,8 +673,7 @@ public final class QuotaController extends StateController { @Override @GuardedBy("mLock") - public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, - boolean forUpdate) { + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) { if (jobStatus.clearTrackingController(JobStatus.TRACKING_QUOTA)) { unprepareFromExecutionLocked(jobStatus); final int userId = jobStatus.getSourceUserId(); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java index 8453e53782ca..0eedcf0bfe77 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java @@ -85,8 +85,7 @@ public abstract class StateController { /** * Remove task - this will happen if the task is cancelled, completed, etc. */ - public abstract void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, - boolean forUpdate); + public abstract void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob); /** * Called when a new job is being created to reschedule an old failed job. diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java index 1ce0a7f6b4c7..11e2ff7bd77f 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java @@ -70,8 +70,7 @@ public final class StorageController extends StateController { } @Override - public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, - boolean forUpdate) { + public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob) { if (taskStatus.clearTrackingController(JobStatus.TRACKING_STORAGE)) { mTrackedTasks.remove(taskStatus); } diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java index b2ca3a051e36..cafb02dd7531 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java @@ -383,8 +383,7 @@ public class TareController extends StateController { @Override @GuardedBy("mLock") - public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, - boolean forUpdate) { + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) { final int userId = jobStatus.getSourceUserId(); final String pkgName = jobStatus.getSourcePackageName(); if (!mTopStartedJobs.remove(jobStatus) && jobStatus.madeActive > 0) { diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java index b6361ce56569..5195f28de9d2 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java @@ -79,7 +79,7 @@ public final class TimeController extends StateController { @Override public void maybeStartTrackingJobLocked(JobStatus job, JobStatus lastJob) { if (job.hasTimingDelayConstraint() || job.hasDeadlineConstraint()) { - maybeStopTrackingJobLocked(job, null, false); + maybeStopTrackingJobLocked(job, null); // First: check the constraints now, because if they are already satisfied // then there is no need to track it. This gives us a fast path for a common @@ -134,8 +134,7 @@ public final class TimeController extends StateController { * tracking was the one our alarms were based off of. */ @Override - public void maybeStopTrackingJobLocked(JobStatus job, JobStatus incomingJob, - boolean forUpdate) { + public void maybeStopTrackingJobLocked(JobStatus job, JobStatus incomingJob) { if (job.clearTrackingController(JobStatus.TRACKING_TIME)) { if (mTrackedJobs.remove(job)) { checkExpiredDelaysAndResetAlarm(); diff --git a/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java index b426f16744e3..46338fa69eb0 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java @@ -33,9 +33,10 @@ import static android.app.tare.EconomyManager.DEFAULT_AM_ACTION_ALARM_INEXACT_NO import static android.app.tare.EconomyManager.DEFAULT_AM_ACTION_ALARM_INEXACT_NONWAKEUP_CTP_CAKES; import static android.app.tare.EconomyManager.DEFAULT_AM_ACTION_ALARM_INEXACT_WAKEUP_BASE_PRICE_CAKES; import static android.app.tare.EconomyManager.DEFAULT_AM_ACTION_ALARM_INEXACT_WAKEUP_CTP_CAKES; -import static android.app.tare.EconomyManager.DEFAULT_AM_HARD_CONSUMPTION_LIMIT_CAKES; import static android.app.tare.EconomyManager.DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT_CAKES; +import static android.app.tare.EconomyManager.DEFAULT_AM_MAX_CONSUMPTION_LIMIT_CAKES; import static android.app.tare.EconomyManager.DEFAULT_AM_MAX_SATIATED_BALANCE_CAKES; +import static android.app.tare.EconomyManager.DEFAULT_AM_MIN_CONSUMPTION_LIMIT_CAKES; import static android.app.tare.EconomyManager.DEFAULT_AM_MIN_SATIATED_BALANCE_EXEMPTED_CAKES; import static android.app.tare.EconomyManager.DEFAULT_AM_MIN_SATIATED_BALANCE_OTHER_APP_CAKES; import static android.app.tare.EconomyManager.DEFAULT_AM_REWARD_NOTIFICATION_INTERACTION_INSTANT_CAKES; @@ -71,9 +72,10 @@ import static android.app.tare.EconomyManager.KEY_AM_ACTION_ALARM_INEXACT_NONWAK import static android.app.tare.EconomyManager.KEY_AM_ACTION_ALARM_INEXACT_NONWAKEUP_CTP; import static android.app.tare.EconomyManager.KEY_AM_ACTION_ALARM_INEXACT_WAKEUP_BASE_PRICE; import static android.app.tare.EconomyManager.KEY_AM_ACTION_ALARM_INEXACT_WAKEUP_CTP; -import static android.app.tare.EconomyManager.KEY_AM_HARD_CONSUMPTION_LIMIT; import static android.app.tare.EconomyManager.KEY_AM_INITIAL_CONSUMPTION_LIMIT; +import static android.app.tare.EconomyManager.KEY_AM_MAX_CONSUMPTION_LIMIT; import static android.app.tare.EconomyManager.KEY_AM_MAX_SATIATED_BALANCE; +import static android.app.tare.EconomyManager.KEY_AM_MIN_CONSUMPTION_LIMIT; import static android.app.tare.EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_EXEMPTED; import static android.app.tare.EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_OTHER_APP; import static android.app.tare.EconomyManager.KEY_AM_REWARD_NOTIFICATION_INTERACTION_INSTANT; @@ -146,7 +148,8 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy { private long mMinSatiatedBalanceOther; private long mMaxSatiatedBalance; private long mInitialSatiatedConsumptionLimit; - private long mHardSatiatedConsumptionLimit; + private long mMinSatiatedConsumptionLimit; + private long mMaxSatiatedConsumptionLimit; private final KeyValueListParser mParser = new KeyValueListParser(','); private final Injector mInjector; @@ -199,8 +202,13 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy { } @Override - long getHardSatiatedConsumptionLimit() { - return mHardSatiatedConsumptionLimit; + long getMinSatiatedConsumptionLimit() { + return mMinSatiatedConsumptionLimit; + } + + @Override + long getMaxSatiatedConsumptionLimit() { + return mMaxSatiatedConsumptionLimit; } @NonNull @@ -240,12 +248,15 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy { mMaxSatiatedBalance = getConstantAsCake(mParser, properties, KEY_AM_MAX_SATIATED_BALANCE, DEFAULT_AM_MAX_SATIATED_BALANCE_CAKES, Math.max(arcToCake(1), mMinSatiatedBalanceExempted)); + mMinSatiatedConsumptionLimit = getConstantAsCake(mParser, properties, + KEY_AM_MIN_CONSUMPTION_LIMIT, DEFAULT_AM_MIN_CONSUMPTION_LIMIT_CAKES, + arcToCake(1)); mInitialSatiatedConsumptionLimit = getConstantAsCake(mParser, properties, - KEY_AM_INITIAL_CONSUMPTION_LIMIT, DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT_CAKES, - arcToCake(1)); - mHardSatiatedConsumptionLimit = getConstantAsCake(mParser, properties, - KEY_AM_HARD_CONSUMPTION_LIMIT, DEFAULT_AM_HARD_CONSUMPTION_LIMIT_CAKES, - mInitialSatiatedConsumptionLimit); + KEY_AM_INITIAL_CONSUMPTION_LIMIT, DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT_CAKES, + mMinSatiatedConsumptionLimit); + mMaxSatiatedConsumptionLimit = getConstantAsCake(mParser, properties, + KEY_AM_MAX_CONSUMPTION_LIMIT, DEFAULT_AM_MAX_CONSUMPTION_LIMIT_CAKES, + mInitialSatiatedConsumptionLimit); final long exactAllowWhileIdleWakeupBasePrice = getConstantAsCake(mParser, properties, KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_WAKEUP_BASE_PRICE, @@ -396,9 +407,11 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy { pw.decreaseIndent(); pw.print("Max satiated balance", cakeToString(mMaxSatiatedBalance)).println(); pw.print("Consumption limits: ["); + pw.print(cakeToString(mMinSatiatedConsumptionLimit)); + pw.print(", "); pw.print(cakeToString(mInitialSatiatedConsumptionLimit)); pw.print(", "); - pw.print(cakeToString(mHardSatiatedConsumptionLimit)); + pw.print(cakeToString(mMaxSatiatedConsumptionLimit)); pw.println("]"); pw.println(); diff --git a/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java index 66f7c357d223..7a9607657972 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java @@ -43,7 +43,8 @@ public class CompleteEconomicPolicy extends EconomicPolicy { private int mEnabledEconomicPolicyIds = 0; private int[] mCostModifiers = EmptyArray.INT; private long mInitialConsumptionLimit; - private long mHardConsumptionLimit; + private long mMinConsumptionLimit; + private long mMaxConsumptionLimit; CompleteEconomicPolicy(@NonNull InternalResourceService irs) { this(irs, new CompleteInjector()); @@ -100,14 +101,17 @@ public class CompleteEconomicPolicy extends EconomicPolicy { private void updateLimits() { long initialConsumptionLimit = 0; - long hardConsumptionLimit = 0; + long minConsumptionLimit = 0; + long maxConsumptionLimit = 0; for (int i = 0; i < mEnabledEconomicPolicies.size(); ++i) { final EconomicPolicy economicPolicy = mEnabledEconomicPolicies.valueAt(i); initialConsumptionLimit += economicPolicy.getInitialSatiatedConsumptionLimit(); - hardConsumptionLimit += economicPolicy.getHardSatiatedConsumptionLimit(); + minConsumptionLimit += economicPolicy.getMinSatiatedConsumptionLimit(); + maxConsumptionLimit += economicPolicy.getMaxSatiatedConsumptionLimit(); } mInitialConsumptionLimit = initialConsumptionLimit; - mHardConsumptionLimit = hardConsumptionLimit; + mMinConsumptionLimit = minConsumptionLimit; + mMaxConsumptionLimit = maxConsumptionLimit; } @Override @@ -134,8 +138,13 @@ public class CompleteEconomicPolicy extends EconomicPolicy { } @Override - long getHardSatiatedConsumptionLimit() { - return mHardConsumptionLimit; + long getMinSatiatedConsumptionLimit() { + return mMinConsumptionLimit; + } + + @Override + long getMaxSatiatedConsumptionLimit() { + return mMaxConsumptionLimit; } @NonNull diff --git a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java index 008dcb8edf63..b52f6f11b3bf 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java @@ -232,15 +232,21 @@ public abstract class EconomicPolicy { * Returns the maximum number of cakes that should be consumed during a full 100% discharge * cycle. This is the initial limit. The system may choose to increase the limit over time, * but the increased limit should never exceed the value returned from - * {@link #getHardSatiatedConsumptionLimit()}. + * {@link #getMaxSatiatedConsumptionLimit()}. */ abstract long getInitialSatiatedConsumptionLimit(); /** - * Returns the maximum number of cakes that should be consumed during a full 100% discharge - * cycle. This is the hard limit that should never be exceeded. + * Returns the minimum number of cakes that should be available for consumption during a full + * 100% discharge cycle. + */ + abstract long getMinSatiatedConsumptionLimit(); + + /** + * Returns the maximum number of cakes that should be available for consumption during a full + * 100% discharge cycle. */ - abstract long getHardSatiatedConsumptionLimit(); + abstract long getMaxSatiatedConsumptionLimit(); /** Return the set of modifiers that should apply to this policy's costs. */ @NonNull diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java index dd0a19433683..581a545f8401 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java @@ -670,7 +670,7 @@ public class InternalResourceService extends SystemService { final long shortfall = (mCurrentBatteryLevel - QUANTITATIVE_EASING_BATTERY_THRESHOLD) * currentConsumptionLimit / 100; final long newConsumptionLimit = Math.min(currentConsumptionLimit + shortfall, - mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit()); + mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit()); if (newConsumptionLimit != currentConsumptionLimit) { Slog.i(TAG, "Increasing consumption limit from " + cakeToString(currentConsumptionLimit) + " to " + cakeToString(newConsumptionLimit)); @@ -720,12 +720,12 @@ public class InternalResourceService extends SystemService { // The stock is too low. We're doing pretty well. We can increase the stock slightly // to let apps do more work in the background. newConsumptionLimit = Math.min((long) (currentConsumptionLimit * 1.01), - mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit()); + mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit()); } else if (percentageOfTarget < 100) { // The stock is too high IMO. We're below the target. Decrease the stock to reduce // background work. newConsumptionLimit = Math.max((long) (currentConsumptionLimit * .98), - mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit()); + mCompleteEconomicPolicy.getMinSatiatedConsumptionLimit()); } else { // The stock is just right. return; @@ -957,9 +957,9 @@ public class InternalResourceService extends SystemService { } else { mScribe.loadFromDiskLocked(); if (mScribe.getSatiatedConsumptionLimitLocked() - < mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit() + < mCompleteEconomicPolicy.getMinSatiatedConsumptionLimit() || mScribe.getSatiatedConsumptionLimitLocked() - > mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit()) { + > mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit()) { // Reset the consumption limit since several factors may have changed. mScribe.setConsumptionLimitLocked( mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit()); @@ -1442,17 +1442,16 @@ public class InternalResourceService extends SystemService { private void updateEconomicPolicy() { synchronized (mLock) { - final long initialLimit = - mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit(); - final long hardLimit = mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit(); + final long minLimit = mCompleteEconomicPolicy.getMinSatiatedConsumptionLimit(); + final long maxLimit = mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit(); final int oldEnabledPolicies = mCompleteEconomicPolicy.getEnabledPolicyIds(); mCompleteEconomicPolicy.tearDown(); mCompleteEconomicPolicy = new CompleteEconomicPolicy(InternalResourceService.this); if (mIsEnabled && mBootPhase >= PHASE_THIRD_PARTY_APPS_CAN_START) { mCompleteEconomicPolicy.setup(getAllDeviceConfigProperties()); - if (initialLimit != mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit() - || hardLimit - != mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit()) { + if (minLimit != mCompleteEconomicPolicy.getMinSatiatedConsumptionLimit() + || maxLimit + != mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit()) { // Reset the consumption limit since several factors may have changed. mScribe.setConsumptionLimitLocked( mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit()); diff --git a/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java index 71c6d099ac77..7cf459c2e494 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java @@ -38,9 +38,10 @@ import static android.app.tare.EconomyManager.DEFAULT_JS_ACTION_JOB_MIN_START_BA import static android.app.tare.EconomyManager.DEFAULT_JS_ACTION_JOB_MIN_START_CTP_CAKES; import static android.app.tare.EconomyManager.DEFAULT_JS_ACTION_JOB_TIMEOUT_PENALTY_BASE_PRICE_CAKES; import static android.app.tare.EconomyManager.DEFAULT_JS_ACTION_JOB_TIMEOUT_PENALTY_CTP_CAKES; -import static android.app.tare.EconomyManager.DEFAULT_JS_HARD_CONSUMPTION_LIMIT_CAKES; import static android.app.tare.EconomyManager.DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES; +import static android.app.tare.EconomyManager.DEFAULT_JS_MAX_CONSUMPTION_LIMIT_CAKES; import static android.app.tare.EconomyManager.DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES; +import static android.app.tare.EconomyManager.DEFAULT_JS_MIN_CONSUMPTION_LIMIT_CAKES; import static android.app.tare.EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_EXEMPTED_CAKES; import static android.app.tare.EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER_CAKES; import static android.app.tare.EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_OTHER_APP_CAKES; @@ -84,9 +85,10 @@ import static android.app.tare.EconomyManager.KEY_JS_ACTION_JOB_MIN_START_BASE_P import static android.app.tare.EconomyManager.KEY_JS_ACTION_JOB_MIN_START_CTP; import static android.app.tare.EconomyManager.KEY_JS_ACTION_JOB_TIMEOUT_PENALTY_BASE_PRICE; import static android.app.tare.EconomyManager.KEY_JS_ACTION_JOB_TIMEOUT_PENALTY_CTP; -import static android.app.tare.EconomyManager.KEY_JS_HARD_CONSUMPTION_LIMIT; import static android.app.tare.EconomyManager.KEY_JS_INITIAL_CONSUMPTION_LIMIT; +import static android.app.tare.EconomyManager.KEY_JS_MAX_CONSUMPTION_LIMIT; import static android.app.tare.EconomyManager.KEY_JS_MAX_SATIATED_BALANCE; +import static android.app.tare.EconomyManager.KEY_JS_MIN_CONSUMPTION_LIMIT; import static android.app.tare.EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_EXEMPTED; import static android.app.tare.EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER; import static android.app.tare.EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_OTHER_APP; @@ -159,7 +161,8 @@ public class JobSchedulerEconomicPolicy extends EconomicPolicy { private long mMinSatiatedBalanceIncrementalAppUpdater; private long mMaxSatiatedBalance; private long mInitialSatiatedConsumptionLimit; - private long mHardSatiatedConsumptionLimit; + private long mMinSatiatedConsumptionLimit; + private long mMaxSatiatedConsumptionLimit; private final KeyValueListParser mParser = new KeyValueListParser(','); private final Injector mInjector; @@ -216,8 +219,13 @@ public class JobSchedulerEconomicPolicy extends EconomicPolicy { } @Override - long getHardSatiatedConsumptionLimit() { - return mHardSatiatedConsumptionLimit; + long getMinSatiatedConsumptionLimit() { + return mMinSatiatedConsumptionLimit; + } + + @Override + long getMaxSatiatedConsumptionLimit() { + return mMaxSatiatedConsumptionLimit; } @NonNull @@ -260,12 +268,15 @@ public class JobSchedulerEconomicPolicy extends EconomicPolicy { mMaxSatiatedBalance = getConstantAsCake(mParser, properties, KEY_JS_MAX_SATIATED_BALANCE, DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES, Math.max(arcToCake(1), mMinSatiatedBalanceExempted)); + mMinSatiatedConsumptionLimit = getConstantAsCake(mParser, properties, + KEY_JS_MIN_CONSUMPTION_LIMIT, DEFAULT_JS_MIN_CONSUMPTION_LIMIT_CAKES, + arcToCake(1)); mInitialSatiatedConsumptionLimit = getConstantAsCake(mParser, properties, - KEY_JS_INITIAL_CONSUMPTION_LIMIT, DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES, - arcToCake(1)); - mHardSatiatedConsumptionLimit = getConstantAsCake(mParser, properties, - KEY_JS_HARD_CONSUMPTION_LIMIT, DEFAULT_JS_HARD_CONSUMPTION_LIMIT_CAKES, - mInitialSatiatedConsumptionLimit); + KEY_JS_INITIAL_CONSUMPTION_LIMIT, DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES, + mMinSatiatedConsumptionLimit); + mMaxSatiatedConsumptionLimit = getConstantAsCake(mParser, properties, + KEY_JS_MAX_CONSUMPTION_LIMIT, DEFAULT_JS_MAX_CONSUMPTION_LIMIT_CAKES, + mInitialSatiatedConsumptionLimit); mActions.put(ACTION_JOB_MAX_START, new Action(ACTION_JOB_MAX_START, getConstantAsCake(mParser, properties, @@ -420,9 +431,11 @@ public class JobSchedulerEconomicPolicy extends EconomicPolicy { pw.decreaseIndent(); pw.print("Max satiated balance", cakeToString(mMaxSatiatedBalance)).println(); pw.print("Consumption limits: ["); + pw.print(cakeToString(mMinSatiatedConsumptionLimit)); + pw.print(", "); pw.print(cakeToString(mInitialSatiatedConsumptionLimit)); pw.print(", "); - pw.print(cakeToString(mHardSatiatedConsumptionLimit)); + pw.print(cakeToString(mMaxSatiatedConsumptionLimit)); pw.println("]"); pw.println(); diff --git a/core/api/current.txt b/core/api/current.txt index cfaacfe1fcc0..d1e6c8282abd 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -4604,23 +4604,24 @@ package android.app { public class AlarmManager { method public boolean canScheduleExactAlarms(); - method public void cancel(android.app.PendingIntent); - method public void cancel(android.app.AlarmManager.OnAlarmListener); + method public void cancel(@NonNull android.app.PendingIntent); + method public void cancel(@NonNull android.app.AlarmManager.OnAlarmListener); method public void cancelAll(); method public android.app.AlarmManager.AlarmClockInfo getNextAlarmClock(); - method public void set(int, long, android.app.PendingIntent); - method public void set(int, long, String, android.app.AlarmManager.OnAlarmListener, android.os.Handler); - method @RequiresPermission(android.Manifest.permission.SCHEDULE_EXACT_ALARM) public void setAlarmClock(android.app.AlarmManager.AlarmClockInfo, android.app.PendingIntent); - method public void setAndAllowWhileIdle(int, long, android.app.PendingIntent); - method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExact(int, long, android.app.PendingIntent); - method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExact(int, long, String, android.app.AlarmManager.OnAlarmListener, android.os.Handler); - method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExactAndAllowWhileIdle(int, long, android.app.PendingIntent); - method public void setInexactRepeating(int, long, long, android.app.PendingIntent); - method public void setRepeating(int, long, long, android.app.PendingIntent); + method public void set(int, long, @NonNull android.app.PendingIntent); + method public void set(int, long, @Nullable String, @NonNull android.app.AlarmManager.OnAlarmListener, @Nullable android.os.Handler); + method @RequiresPermission(android.Manifest.permission.SCHEDULE_EXACT_ALARM) public void setAlarmClock(@NonNull android.app.AlarmManager.AlarmClockInfo, @NonNull android.app.PendingIntent); + method public void setAndAllowWhileIdle(int, long, @NonNull android.app.PendingIntent); + method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExact(int, long, @NonNull android.app.PendingIntent); + method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExact(int, long, @Nullable String, @NonNull android.app.AlarmManager.OnAlarmListener, @Nullable android.os.Handler); + method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExactAndAllowWhileIdle(int, long, @NonNull android.app.PendingIntent); + method public void setInexactRepeating(int, long, long, @NonNull android.app.PendingIntent); + method public void setRepeating(int, long, long, @NonNull android.app.PendingIntent); method @RequiresPermission(android.Manifest.permission.SET_TIME) public void setTime(long); method @RequiresPermission(android.Manifest.permission.SET_TIME_ZONE) public void setTimeZone(String); - method public void setWindow(int, long, long, android.app.PendingIntent); - method public void setWindow(int, long, long, String, android.app.AlarmManager.OnAlarmListener, android.os.Handler); + method public void setWindow(int, long, long, @NonNull android.app.PendingIntent); + method public void setWindow(int, long, long, @Nullable String, @NonNull android.app.AlarmManager.OnAlarmListener, @Nullable android.os.Handler); + method public void setWindow(int, long, long, @Nullable String, @NonNull java.util.concurrent.Executor, @NonNull android.app.AlarmManager.OnAlarmListener); field public static final String ACTION_NEXT_ALARM_CLOCK_CHANGED = "android.app.action.NEXT_ALARM_CLOCK_CHANGED"; field public static final String ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED = "android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED"; field public static final int ELAPSED_REALTIME = 3; // 0x3 @@ -11654,7 +11655,7 @@ package android.content.pm { public static final class PackageInstaller.PreapprovalDetails implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.graphics.Bitmap getIcon(); - method @NonNull public String getLabel(); + method @NonNull public CharSequence getLabel(); method @NonNull public android.icu.util.ULocale getLocale(); method @NonNull public String getPackageName(); method public void writeToParcel(@NonNull android.os.Parcel, int); @@ -11665,7 +11666,7 @@ package android.content.pm { ctor public PackageInstaller.PreapprovalDetails.Builder(); method @NonNull public android.content.pm.PackageInstaller.PreapprovalDetails build(); method @NonNull public android.content.pm.PackageInstaller.PreapprovalDetails.Builder setIcon(@NonNull android.graphics.Bitmap); - method @NonNull public android.content.pm.PackageInstaller.PreapprovalDetails.Builder setLabel(@NonNull String); + method @NonNull public android.content.pm.PackageInstaller.PreapprovalDetails.Builder setLabel(@NonNull CharSequence); method @NonNull public android.content.pm.PackageInstaller.PreapprovalDetails.Builder setLocale(@NonNull android.icu.util.ULocale); method @NonNull public android.content.pm.PackageInstaller.PreapprovalDetails.Builder setPackageName(@NonNull String); } @@ -41591,7 +41592,7 @@ package android.telephony { field public static final String KEY_CARRIER_CONFIG_APPLIED_BOOL = "carrier_config_applied_bool"; field public static final String KEY_CARRIER_CONFIG_VERSION_STRING = "carrier_config_version_string"; field public static final String KEY_CARRIER_CROSS_SIM_IMS_AVAILABLE_BOOL = "carrier_cross_sim_ims_available_bool"; - field public static final String KEY_CARRIER_DATA_CALL_PERMANENT_FAILURE_STRINGS = "carrier_data_call_permanent_failure_strings"; + field @Deprecated public static final String KEY_CARRIER_DATA_CALL_PERMANENT_FAILURE_STRINGS = "carrier_data_call_permanent_failure_strings"; field public static final String KEY_CARRIER_DEFAULT_ACTIONS_ON_DCFAILURE_STRING_ARRAY = "carrier_default_actions_on_dcfailure_string_array"; field public static final String KEY_CARRIER_DEFAULT_ACTIONS_ON_DEFAULT_NETWORK_AVAILABLE = "carrier_default_actions_on_default_network_available_string_array"; field public static final String KEY_CARRIER_DEFAULT_ACTIONS_ON_REDIRECTION_STRING_ARRAY = "carrier_default_actions_on_redirection_string_array"; @@ -41728,6 +41729,7 @@ package android.telephony { field public static final String KEY_MMS_MMS_READ_REPORT_ENABLED_BOOL = "enableMMSReadReports"; field public static final String KEY_MMS_MULTIPART_SMS_ENABLED_BOOL = "enableMultipartSMS"; field public static final String KEY_MMS_NAI_SUFFIX_STRING = "naiSuffix"; + field public static final String KEY_MMS_NETWORK_RELEASE_TIMEOUT_MILLIS_INT = "mms_network_release_timeout_millis_int"; field public static final String KEY_MMS_NOTIFY_WAP_MMSC_ENABLED_BOOL = "enabledNotifyWapMMSC"; field public static final String KEY_MMS_RECIPIENT_LIMIT_INT = "recipientLimit"; field public static final String KEY_MMS_SEND_MULTIPART_SMS_AS_SEPARATE_MESSAGES_BOOL = "sendMultipartSmsAsSeparateMessages"; @@ -43416,14 +43418,18 @@ package android.telephony { field public static final int RESULT_RECEIVE_WHILE_ENCRYPTED = 504; // 0x1f8 field public static final int RESULT_REMOTE_EXCEPTION = 31; // 0x1f field public static final int RESULT_REQUEST_NOT_SUPPORTED = 24; // 0x18 + field public static final int RESULT_RIL_ABORTED = 137; // 0x89 field public static final int RESULT_RIL_ACCESS_BARRED = 122; // 0x7a field public static final int RESULT_RIL_BLOCKED_DUE_TO_CALL = 123; // 0x7b field public static final int RESULT_RIL_CANCELLED = 119; // 0x77 + field public static final int RESULT_RIL_DEVICE_IN_USE = 136; // 0x88 field public static final int RESULT_RIL_ENCODING_ERR = 109; // 0x6d field public static final int RESULT_RIL_GENERIC_ERROR = 124; // 0x7c field public static final int RESULT_RIL_INTERNAL_ERR = 113; // 0x71 field public static final int RESULT_RIL_INVALID_ARGUMENTS = 104; // 0x68 field public static final int RESULT_RIL_INVALID_MODEM_STATE = 115; // 0x73 + field public static final int RESULT_RIL_INVALID_RESPONSE = 125; // 0x7d + field public static final int RESULT_RIL_INVALID_SIM_STATE = 130; // 0x82 field public static final int RESULT_RIL_INVALID_SMSC_ADDRESS = 110; // 0x6e field public static final int RESULT_RIL_INVALID_SMS_FORMAT = 107; // 0x6b field public static final int RESULT_RIL_INVALID_STATE = 103; // 0x67 @@ -43432,14 +43438,23 @@ package android.telephony { field public static final int RESULT_RIL_NETWORK_NOT_READY = 116; // 0x74 field public static final int RESULT_RIL_NETWORK_REJECT = 102; // 0x66 field public static final int RESULT_RIL_NO_MEMORY = 105; // 0x69 + field public static final int RESULT_RIL_NO_NETWORK_FOUND = 135; // 0x87 field public static final int RESULT_RIL_NO_RESOURCES = 118; // 0x76 + field public static final int RESULT_RIL_NO_SMS_TO_ACK = 131; // 0x83 + field public static final int RESULT_RIL_NO_SUBSCRIPTION = 134; // 0x86 field public static final int RESULT_RIL_OPERATION_NOT_ALLOWED = 117; // 0x75 field public static final int RESULT_RIL_RADIO_NOT_AVAILABLE = 100; // 0x64 field public static final int RESULT_RIL_REQUEST_NOT_SUPPORTED = 114; // 0x72 field public static final int RESULT_RIL_REQUEST_RATE_LIMITED = 106; // 0x6a field public static final int RESULT_RIL_SIMULTANEOUS_SMS_AND_CALL_NOT_ALLOWED = 121; // 0x79 field public static final int RESULT_RIL_SIM_ABSENT = 120; // 0x78 + field public static final int RESULT_RIL_SIM_BUSY = 132; // 0x84 + field public static final int RESULT_RIL_SIM_ERROR = 129; // 0x81 + field public static final int RESULT_RIL_SIM_FULL = 133; // 0x85 + field public static final int RESULT_RIL_SIM_PIN2 = 126; // 0x7e + field public static final int RESULT_RIL_SIM_PUK2 = 127; // 0x7f field public static final int RESULT_RIL_SMS_SEND_FAIL_RETRY = 101; // 0x65 + field public static final int RESULT_RIL_SUBSCRIPTION_NOT_AVAILABLE = 128; // 0x80 field public static final int RESULT_RIL_SYSTEM_ERR = 108; // 0x6c field public static final int RESULT_SMS_BLOCKED_DURING_EMERGENCY = 29; // 0x1d field public static final int RESULT_SMS_SEND_RETRY_FAILED = 30; // 0x1e @@ -45568,6 +45583,14 @@ package android.text { field public static final int DONE = -1; // 0xffffffff } + public static class SegmentFinder.DefaultSegmentFinder extends android.text.SegmentFinder { + ctor public SegmentFinder.DefaultSegmentFinder(@NonNull int[]); + method public int nextEndBoundary(@IntRange(from=0) int); + method public int nextStartBoundary(@IntRange(from=0) int); + method public int previousEndBoundary(@IntRange(from=0) int); + method public int previousStartBoundary(@IntRange(from=0) int); + } + public class Selection { method public static boolean extendDown(android.text.Spannable, android.text.Layout); method public static boolean extendLeft(android.text.Spannable, android.text.Layout); @@ -48557,6 +48580,12 @@ package android.view { field public static final int VERTICAL_GRAVITY_MASK = 112; // 0x70 } + public class HandwritingDelegateConfiguration { + ctor public HandwritingDelegateConfiguration(@IdRes int, @NonNull Runnable); + method public int getDelegatorViewId(); + method @NonNull public Runnable getInitiationCallback(); + } + public class HapticFeedbackConstants { field public static final int CLOCK_TICK = 4; // 0x4 field public static final int CONFIRM = 16; // 0x10 @@ -49574,16 +49603,13 @@ package android.view { } public final class PixelCopy { - method @NonNull public static android.view.PixelCopy.Request ofSurface(@NonNull android.view.Surface, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.PixelCopy.CopyResult>); - method @NonNull public static android.view.PixelCopy.Request ofSurface(@NonNull android.view.SurfaceView, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.PixelCopy.CopyResult>); - method @NonNull public static android.view.PixelCopy.Request ofWindow(@NonNull android.view.Window, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.PixelCopy.CopyResult>); - method @NonNull public static android.view.PixelCopy.Request ofWindow(@NonNull android.view.View, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.PixelCopy.CopyResult>); method public static void request(@NonNull android.view.SurfaceView, @NonNull android.graphics.Bitmap, @NonNull android.view.PixelCopy.OnPixelCopyFinishedListener, @NonNull android.os.Handler); method public static void request(@NonNull android.view.SurfaceView, @Nullable android.graphics.Rect, @NonNull android.graphics.Bitmap, @NonNull android.view.PixelCopy.OnPixelCopyFinishedListener, @NonNull android.os.Handler); method public static void request(@NonNull android.view.Surface, @NonNull android.graphics.Bitmap, @NonNull android.view.PixelCopy.OnPixelCopyFinishedListener, @NonNull android.os.Handler); method public static void request(@NonNull android.view.Surface, @Nullable android.graphics.Rect, @NonNull android.graphics.Bitmap, @NonNull android.view.PixelCopy.OnPixelCopyFinishedListener, @NonNull android.os.Handler); method public static void request(@NonNull android.view.Window, @NonNull android.graphics.Bitmap, @NonNull android.view.PixelCopy.OnPixelCopyFinishedListener, @NonNull android.os.Handler); method public static void request(@NonNull android.view.Window, @Nullable android.graphics.Rect, @NonNull android.graphics.Bitmap, @NonNull android.view.PixelCopy.OnPixelCopyFinishedListener, @NonNull android.os.Handler); + method public static void request(@NonNull android.view.PixelCopy.Request); field public static final int ERROR_DESTINATION_INVALID = 5; // 0x5 field public static final int ERROR_SOURCE_INVALID = 4; // 0x4 field public static final int ERROR_SOURCE_NO_DATA = 3; // 0x3 @@ -49592,19 +49618,28 @@ package android.view { field public static final int SUCCESS = 0; // 0x0 } - public static final class PixelCopy.CopyResult { - method @NonNull public android.graphics.Bitmap getBitmap(); - method public int getStatus(); - } - public static interface PixelCopy.OnPixelCopyFinishedListener { method public void onPixelCopyFinished(int); } public static final class PixelCopy.Request { - method public void request(); - method @NonNull public android.view.PixelCopy.Request setDestinationBitmap(@Nullable android.graphics.Bitmap); - method @NonNull public android.view.PixelCopy.Request setSourceRect(@Nullable android.graphics.Rect); + method @Nullable public android.graphics.Bitmap getDestinationBitmap(); + method @Nullable public android.graphics.Rect getSourceRect(); + method @NonNull public static android.view.PixelCopy.Request.Builder ofSurface(@NonNull android.view.Surface, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.PixelCopy.Result>); + method @NonNull public static android.view.PixelCopy.Request.Builder ofSurface(@NonNull android.view.SurfaceView, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.PixelCopy.Result>); + method @NonNull public static android.view.PixelCopy.Request.Builder ofWindow(@NonNull android.view.Window, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.PixelCopy.Result>); + method @NonNull public static android.view.PixelCopy.Request.Builder ofWindow(@NonNull android.view.View, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.PixelCopy.Result>); + } + + public static final class PixelCopy.Request.Builder { + method @NonNull public android.view.PixelCopy.Request build(); + method @NonNull public android.view.PixelCopy.Request.Builder setDestinationBitmap(@Nullable android.graphics.Bitmap); + method @NonNull public android.view.PixelCopy.Request.Builder setSourceRect(@Nullable android.graphics.Rect); + } + + public static final class PixelCopy.Result { + method @NonNull public android.graphics.Bitmap getBitmap(); + method public int getStatus(); } public final class PointerIcon implements android.os.Parcelable { @@ -50167,6 +50202,7 @@ package android.view { method public float getHandwritingBoundsOffsetLeft(); method public float getHandwritingBoundsOffsetRight(); method public float getHandwritingBoundsOffsetTop(); + method @Nullable public android.view.HandwritingDelegateConfiguration getHandwritingDelegateConfiguration(); method public final boolean getHasOverlappingRendering(); method public final int getHeight(); method public void getHitRect(android.graphics.Rect); @@ -50533,6 +50569,7 @@ package android.view { method public void setForegroundTintList(@Nullable android.content.res.ColorStateList); method public void setForegroundTintMode(@Nullable android.graphics.PorterDuff.Mode); method public void setHandwritingBoundsOffsets(float, float, float, float); + method public void setHandwritingDelegateConfiguration(@Nullable android.view.HandwritingDelegateConfiguration); method public void setHapticFeedbackEnabled(boolean); method public void setHasTransientState(boolean); method public void setHorizontalFadingEdgeEnabled(boolean); @@ -53575,6 +53612,7 @@ package android.view.inputmethod { method public boolean reportFullscreenMode(boolean); method public boolean requestCursorUpdates(int); method public default boolean requestCursorUpdates(int, int); + method public default void requestTextBoundsInfo(@NonNull android.graphics.RectF, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.inputmethod.TextBoundsInfoResult>); method public boolean sendKeyEvent(android.view.KeyEvent); method public boolean setComposingRegion(int, int); method public default boolean setComposingRegion(int, int, @Nullable android.view.inputmethod.TextAttribute); @@ -53904,6 +53942,50 @@ package android.view.inputmethod { method @NonNull public android.view.inputmethod.TextAttribute.Builder setTextConversionSuggestions(@NonNull java.util.List<java.lang.String>); } + public final class TextBoundsInfo implements android.os.Parcelable { + method public int describeContents(); + method @IntRange(from=0, to=125) public int getCharacterBidiLevel(int); + method @NonNull public android.graphics.RectF getCharacterBounds(int); + method public int getCharacterFlags(int); + method public int getEnd(); + method @NonNull public android.text.SegmentFinder getGraphemeSegmentFinder(); + method @NonNull public android.text.SegmentFinder getLineSegmentFinder(); + method @NonNull public android.graphics.Matrix getMatrix(); + method public int getStart(); + method @NonNull public android.text.SegmentFinder getWordSegmentFinder(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.TextBoundsInfo> CREATOR; + field public static final int FLAG_CHARACTER_LINEFEED = 2; // 0x2 + field public static final int FLAG_CHARACTER_PUNCTUATION = 4; // 0x4 + field public static final int FLAG_CHARACTER_WHITESPACE = 1; // 0x1 + field public static final int FLAG_LINE_IS_RTL = 8; // 0x8 + } + + public static final class TextBoundsInfo.Builder { + ctor public TextBoundsInfo.Builder(); + method @NonNull public android.view.inputmethod.TextBoundsInfo build(); + method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder clear(); + method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setCharacterBidiLevel(@NonNull int[]); + method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setCharacterBounds(@NonNull float[]); + method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setCharacterFlags(@NonNull int[]); + method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setGraphemeSegmentFinder(@NonNull android.text.SegmentFinder); + method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setLineSegmentFinder(@NonNull android.text.SegmentFinder); + method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setMatrix(@NonNull android.graphics.Matrix); + method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setStartAndEnd(@IntRange(from=0) int, @IntRange(from=0) int); + method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setWordSegmentFinder(@NonNull android.text.SegmentFinder); + } + + public final class TextBoundsInfoResult { + ctor public TextBoundsInfoResult(int); + ctor public TextBoundsInfoResult(int, @NonNull android.view.inputmethod.TextBoundsInfo); + method public int getResultCode(); + method @Nullable public android.view.inputmethod.TextBoundsInfo getTextBoundsInfo(); + field public static final int CODE_CANCELLED = 3; // 0x3 + field public static final int CODE_FAILED = 2; // 0x2 + field public static final int CODE_SUCCESS = 1; // 0x1 + field public static final int CODE_UNSUPPORTED = 0; // 0x0 + } + public final class TextSnapshot { ctor public TextSnapshot(@NonNull android.view.inputmethod.SurroundingText, @IntRange(from=0xffffffff) int, @IntRange(from=0xffffffff) int, int); method @IntRange(from=0xffffffff) public int getCompositionEnd(); diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index 88efcced78fb..ce18745046ff 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -340,10 +340,6 @@ package android.os { method public boolean shouldBypassCache(@NonNull Q); } - public interface Parcelable { - method public default int getStability(); - } - public class Process { method public static final int getAppUidForSdkSandboxUid(int); method public static final boolean isSdkSandboxUid(int); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index a382ecfc99d3..7cbf4617d991 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -524,10 +524,12 @@ package android.app { } public class AlarmManager { - method @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public void set(int, long, long, long, android.app.PendingIntent, android.os.WorkSource); - method @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public void set(int, long, long, long, android.app.AlarmManager.OnAlarmListener, android.os.Handler, android.os.WorkSource); + method @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public void set(int, long, long, long, @NonNull android.app.PendingIntent, @Nullable android.os.WorkSource); + method @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public void set(int, long, long, long, @NonNull android.app.AlarmManager.OnAlarmListener, @Nullable android.os.Handler, @Nullable android.os.WorkSource); method @RequiresPermission(allOf={android.Manifest.permission.UPDATE_DEVICE_STATS, android.Manifest.permission.SCHEDULE_EXACT_ALARM}, conditional=true) public void setExact(int, long, @Nullable String, @NonNull java.util.concurrent.Executor, @NonNull android.os.WorkSource, @NonNull android.app.AlarmManager.OnAlarmListener); + method @RequiresPermission(allOf={android.Manifest.permission.UPDATE_DEVICE_STATS, android.Manifest.permission.SCHEDULE_EXACT_ALARM}, conditional=true) public void setExactAndAllowWhileIdle(int, long, @Nullable String, @NonNull java.util.concurrent.Executor, @Nullable android.os.WorkSource, @NonNull android.app.AlarmManager.OnAlarmListener); method @RequiresPermission(android.Manifest.permission.SCHEDULE_PRIORITIZED_ALARM) public void setPrioritized(int, long, long, @Nullable String, @NonNull java.util.concurrent.Executor, @NonNull android.app.AlarmManager.OnAlarmListener); + method @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public void setWindow(int, long, long, @Nullable String, @NonNull java.util.concurrent.Executor, @Nullable android.os.WorkSource, @NonNull android.app.AlarmManager.OnAlarmListener); } public class AppOpsManager { @@ -9670,6 +9672,7 @@ package android.os { } public interface Parcelable { + method public default int getStability(); field public static final int PARCELABLE_STABILITY_LOCAL = 0; // 0x0 field public static final int PARCELABLE_STABILITY_VINTF = 1; // 0x1 } @@ -9678,7 +9681,6 @@ package android.os { ctor public ParcelableHolder(int); method public int describeContents(); method @Nullable public <T extends android.os.Parcelable> T getParcelable(@NonNull Class<T>); - method public int getStability(); method public void readFromParcel(@NonNull android.os.Parcel); method public void setParcelable(@Nullable android.os.Parcelable); method public void writeToParcel(@NonNull android.os.Parcel, int); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index ef74a3ef6c20..6e8713413d68 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -419,7 +419,7 @@ package android.app { } public final class SyncNotedAppOp implements android.os.Parcelable { - ctor public SyncNotedAppOp(int, @IntRange(from=0L) int, @Nullable String, @NonNull String); + ctor public SyncNotedAppOp(int, @IntRange(from=0L) int, @Nullable String, @Nullable String); } public class TaskInfo { diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 308e996f9919..baa3bd96343a 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -227,6 +227,12 @@ public abstract class ActivityManagerInternal { public abstract boolean isModernQueueEnabled(); /** + * Enforce capability restrictions on use of the given BroadcastOptions + */ + public abstract void enforceBroadcastOptionsPermissions(@Nullable Bundle options, + int callingUid); + + /** * Returns package name given pid. * * @param pid The pid we are searching package name for. @@ -300,7 +306,7 @@ public abstract class ActivityManagerInternal { public abstract int handleIncomingUser(int callingPid, int callingUid, @UserIdInt int userId, boolean allowAll, int allowMode, String name, String callerPackage); - /** Checks if the calling binder pid as the permission. */ + /** Checks if the calling binder pid/uid has the given permission. */ @PermissionMethod public abstract void enforceCallingPermission(@PermissionName String permission, String func); diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java index 48638d1fdff4..45d44589b2d8 100644 --- a/core/java/android/app/BroadcastOptions.java +++ b/core/java/android/app/BroadcastOptions.java @@ -636,6 +636,7 @@ public class BroadcastOptions extends ComponentOptions { * @param broadcastIsInteractive * @see #isInteractiveBroadcast() */ + @RequiresPermission(android.Manifest.permission.BROADCAST_OPTION_INTERACTIVE) public void setInteractiveBroadcast(boolean broadcastIsInteractive) { mIsInteractiveBroadcast = broadcastIsInteractive; } diff --git a/core/java/android/app/SyncNotedAppOp.java b/core/java/android/app/SyncNotedAppOp.java index f156b30d5050..f674e882d34e 100644 --- a/core/java/android/app/SyncNotedAppOp.java +++ b/core/java/android/app/SyncNotedAppOp.java @@ -56,7 +56,7 @@ public final class SyncNotedAppOp implements Parcelable { * The package this op applies to * @hide */ - private final @NonNull String mPackageName; + private final @Nullable String mPackageName; /** * Native code relies on parcel ordering, do not change @@ -64,7 +64,7 @@ public final class SyncNotedAppOp implements Parcelable { */ @TestApi public SyncNotedAppOp(int opMode, @IntRange(from = 0L) int opCode, - @Nullable String attributionTag, @NonNull String packageName) { + @Nullable String attributionTag, @Nullable String packageName) { this.mOpCode = opCode; com.android.internal.util.AnnotationValidations.validate( IntRange.class, null, mOpCode, @@ -101,7 +101,7 @@ public final class SyncNotedAppOp implements Parcelable { * @hide */ public SyncNotedAppOp(@IntRange(from = 0L) int opCode, @Nullable String attributionTag, - @NonNull String packageName) { + @Nullable String packageName) { this(AppOpsManager.MODE_IGNORED, opCode, attributionTag, packageName); } @@ -152,7 +152,7 @@ public final class SyncNotedAppOp implements Parcelable { * @hide */ @DataClass.Generated.Member - public @NonNull String getPackageName() { + public @Nullable String getPackageName() { return mPackageName; } @@ -211,11 +211,12 @@ public final class SyncNotedAppOp implements Parcelable { byte flg = 0; if (mAttributionTag != null) flg |= 0x4; + if (mPackageName != null) flg |= 0x8; dest.writeByte(flg); dest.writeInt(mOpMode); dest.writeInt(mOpCode); if (mAttributionTag != null) dest.writeString(mAttributionTag); - dest.writeString(mPackageName); + if (mPackageName != null) dest.writeString(mPackageName); } @Override @@ -233,7 +234,7 @@ public final class SyncNotedAppOp implements Parcelable { int opMode = in.readInt(); int opCode = in.readInt(); String attributionTag = (flg & 0x4) == 0 ? null : in.readString(); - String packageName = in.readString(); + String packageName = (flg & 0x8) == 0 ? null : in.readString(); this.mOpMode = opMode; this.mOpCode = opCode; @@ -243,8 +244,6 @@ public final class SyncNotedAppOp implements Parcelable { "to", AppOpsManager._NUM_OP - 1); this.mAttributionTag = attributionTag; this.mPackageName = packageName; - com.android.internal.util.AnnotationValidations.validate( - NonNull.class, null, mPackageName); // onConstructed(); // You can define this method to get a callback } @@ -264,10 +263,10 @@ public final class SyncNotedAppOp implements Parcelable { }; @DataClass.Generated( - time = 1643320427700L, + time = 1667247337573L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/app/SyncNotedAppOp.java", - inputSignatures = "private final int mOpMode\nprivate final @android.annotation.IntRange int mOpCode\nprivate final @android.annotation.Nullable java.lang.String mAttributionTag\nprivate final @android.annotation.NonNull java.lang.String mPackageName\npublic @android.annotation.NonNull java.lang.String getOp()\npublic int getOpMode()\nprivate java.lang.String opCodeToString()\nclass SyncNotedAppOp extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genAidl=true, genConstructor=false, genToString=true)") + inputSignatures = "private final int mOpMode\nprivate final @android.annotation.IntRange int mOpCode\nprivate final @android.annotation.Nullable java.lang.String mAttributionTag\nprivate final @android.annotation.Nullable java.lang.String mPackageName\npublic @android.annotation.NonNull java.lang.String getOp()\npublic int getOpMode()\nprivate java.lang.String opCodeToString()\nclass SyncNotedAppOp extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genAidl=true, genConstructor=false, genToString=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 95e9c22373de..5e15b0a19a6a 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -2456,7 +2456,12 @@ public class WallpaperManager { public void waitForCompletion() { try { - mLatch.await(30, TimeUnit.SECONDS); + final boolean completed = mLatch.await(30, TimeUnit.SECONDS); + if (completed) { + Log.d(TAG, "Wallpaper set completion."); + } else { + Log.d(TAG, "Timeout waiting for wallpaper set completion!"); + } } catch (InterruptedException e) { // This might be legit: the crop may take a very long time. Don't sweat // it in that case; we are okay with display lagging behind in order to diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index d2fb1fb7fdf4..d7686e22756e 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -3346,7 +3346,7 @@ public class PackageInstaller { /** * The label representing the app to be installed. */ - private final @NonNull String mLabel; + private final @NonNull CharSequence mLabel; /** * The locale of the app label being used. */ @@ -3388,7 +3388,7 @@ public class PackageInstaller { @DataClass.Generated.Member public PreapprovalDetails( @Nullable Bitmap icon, - @NonNull String label, + @NonNull CharSequence label, @NonNull ULocale locale, @NonNull String packageName) { this.mIcon = icon; @@ -3417,7 +3417,7 @@ public class PackageInstaller { * The label representing the app to be installed. */ @DataClass.Generated.Member - public @NonNull String getLabel() { + public @NonNull CharSequence getLabel() { return mLabel; } @@ -3461,7 +3461,7 @@ public class PackageInstaller { if (mIcon != null) flg |= 0x1; dest.writeByte(flg); if (mIcon != null) mIcon.writeToParcel(dest, flags); - dest.writeString8(mLabel); + dest.writeCharSequence(mLabel); dest.writeString8(mLocale.toString()); dest.writeString8(mPackageName); } @@ -3479,7 +3479,7 @@ public class PackageInstaller { byte flg = in.readByte(); Bitmap icon = (flg & 0x1) == 0 ? null : Bitmap.CREATOR.createFromParcel(in); - String label = in.readString8(); + CharSequence label = (CharSequence) in.readCharSequence(); ULocale locale = new ULocale(in.readString8()); String packageName = in.readString8(); @@ -3519,7 +3519,7 @@ public class PackageInstaller { public static final class Builder { private @Nullable Bitmap mIcon; - private @NonNull String mLabel; + private @NonNull CharSequence mLabel; private @NonNull ULocale mLocale; private @NonNull String mPackageName; @@ -3545,7 +3545,7 @@ public class PackageInstaller { * The label representing the app to be installed. */ @DataClass.Generated.Member - public @NonNull Builder setLabel(@NonNull String value) { + public @NonNull Builder setLabel(@NonNull CharSequence value) { checkNotUsed(); mBuilderFieldsSet |= 0x2; mLabel = value; @@ -3596,10 +3596,10 @@ public class PackageInstaller { } @DataClass.Generated( - time = 1664257135109L, + time = 1666748098353L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/content/pm/PackageInstaller.java", - inputSignatures = "private final @android.annotation.Nullable android.graphics.Bitmap mIcon\nprivate final @android.annotation.NonNull java.lang.String mLabel\nprivate final @android.annotation.NonNull android.icu.util.ULocale mLocale\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nclass PreapprovalDetails extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genHiddenConstructor=true, genBuilder=true, genToString=true)") + inputSignatures = "private final @android.annotation.Nullable android.graphics.Bitmap mIcon\nprivate final @android.annotation.NonNull java.lang.CharSequence mLabel\nprivate final @android.annotation.NonNull android.icu.util.ULocale mLocale\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nclass PreapprovalDetails extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genHiddenConstructor=true, genBuilder=true, genToString=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/credentials/Credential.java b/core/java/android/credentials/Credential.java index fed259254239..db89170b0cd7 100644 --- a/core/java/android/credentials/Credential.java +++ b/core/java/android/credentials/Credential.java @@ -36,7 +36,8 @@ public final class Credential implements Parcelable { * * @hide */ - @NonNull public static final String TYPE_PASSWORD = "android.credentials.TYPE_PASSWORD"; + @NonNull public static final String TYPE_PASSWORD_CREDENTIAL = + "android.credentials.TYPE_PASSWORD_CREDENTIAL"; /** * The credential type. diff --git a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java index 4f09beec81dd..95aecfe4c7af 100644 --- a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java +++ b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java @@ -16,30 +16,29 @@ package android.inputmethodservice; +import static android.view.inputmethod.TextBoundsInfoResult.CODE_CANCELLED; + import android.annotation.AnyThread; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; +import android.graphics.RectF; import android.os.Bundle; import android.os.RemoteException; import android.os.ResultReceiver; import android.view.KeyEvent; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CorrectionInfo; -import android.view.inputmethod.DeleteGesture; -import android.view.inputmethod.DeleteRangeGesture; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.HandwritingGesture; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputContentInfo; -import android.view.inputmethod.InsertGesture; -import android.view.inputmethod.JoinOrSplitGesture; -import android.view.inputmethod.RemoveSpaceGesture; -import android.view.inputmethod.SelectGesture; -import android.view.inputmethod.SelectRangeGesture; +import android.view.inputmethod.ParcelableHandwritingGesture; import android.view.inputmethod.SurroundingText; import android.view.inputmethod.TextAttribute; +import android.view.inputmethod.TextBoundsInfo; +import android.view.inputmethod.TextBoundsInfoResult; import com.android.internal.infra.AndroidFuture; import com.android.internal.inputmethod.IRemoteInputConnection; @@ -47,6 +46,7 @@ import com.android.internal.inputmethod.InputConnectionCommandHeader; import java.util.Objects; import java.util.concurrent.Executor; +import java.util.function.Consumer; import java.util.function.IntConsumer; /** @@ -98,6 +98,44 @@ final class IRemoteInputConnectionInvoker { }; /** + * Subclass of {@link ResultReceiver} used by + * {@link #requestTextBoundsInfo(RectF, Executor, Consumer)} for providing + * callback. + */ + private static final class TextBoundsInfoResultReceiver extends ResultReceiver { + @Nullable + private Consumer<TextBoundsInfoResult> mConsumer; + @Nullable + private Executor mExecutor; + + TextBoundsInfoResultReceiver(@NonNull Executor executor, + @NonNull Consumer<TextBoundsInfoResult> consumer) { + super(null); + mExecutor = executor; + mConsumer = consumer; + } + + @Override + protected void onReceiveResult(@TextBoundsInfoResult.ResultCode int resultCode, + @Nullable Bundle resultData) { + synchronized (this) { + if (mExecutor != null && mConsumer != null) { + final TextBoundsInfoResult textBoundsInfoResult = new TextBoundsInfoResult( + resultCode, TextBoundsInfo.createFromBundle(resultData)); + mExecutor.execute(() -> mConsumer.accept(textBoundsInfoResult)); + // provide callback only once. + clear(); + } + } + } + + private void clear() { + mExecutor = null; + mConsumer = null; + } + } + + /** * Creates a new instance of {@link IRemoteInputConnectionInvoker} for the given * {@link IRemoteInputConnection}. * @@ -637,50 +675,19 @@ final class IRemoteInputConnectionInvoker { } /** - * Invokes one of {@link IRemoteInputConnection#performHandwritingSelectGesture}, - * {@link IRemoteInputConnection#performHandwritingSelectRangeGesture}, - * {@link IRemoteInputConnection#performHandwritingDeleteGesture}, - * {@link IRemoteInputConnection#performHandwritingDeleteRangeGesture}, - * {@link IRemoteInputConnection#performHandwritingInsertGesture}, - * {@link IRemoteInputConnection#performHandwritingRemoveSpaceGesture}, - * {@link IRemoteInputConnection#performHandwritingJoinOrSplitGesture}. + * Invokes {@link IRemoteInputConnection#performHandwritingGesture( + * InputConnectionCommandHeader, ParcelableHandwritingGesture, ResultReceiver)}. */ @AnyThread - public void performHandwritingGesture( - @NonNull HandwritingGesture gesture, @Nullable @CallbackExecutor Executor executor, - @Nullable IntConsumer consumer) { - + public void performHandwritingGesture(@NonNull ParcelableHandwritingGesture gesture, + @Nullable @CallbackExecutor Executor executor, @Nullable IntConsumer consumer) { ResultReceiver resultReceiver = null; if (consumer != null) { Objects.requireNonNull(executor); resultReceiver = new IntResultReceiver(executor, consumer); } try { - if (gesture instanceof SelectGesture) { - mConnection.performHandwritingSelectGesture( - createHeader(), (SelectGesture) gesture, resultReceiver); - } else if (gesture instanceof SelectRangeGesture) { - mConnection.performHandwritingSelectRangeGesture( - createHeader(), (SelectRangeGesture) gesture, resultReceiver); - } else if (gesture instanceof InsertGesture) { - mConnection.performHandwritingInsertGesture( - createHeader(), (InsertGesture) gesture, resultReceiver); - } else if (gesture instanceof DeleteGesture) { - mConnection.performHandwritingDeleteGesture( - createHeader(), (DeleteGesture) gesture, resultReceiver); - } else if (gesture instanceof DeleteRangeGesture) { - mConnection.performHandwritingDeleteRangeGesture( - createHeader(), (DeleteRangeGesture) gesture, resultReceiver); - } else if (gesture instanceof RemoveSpaceGesture) { - mConnection.performHandwritingRemoveSpaceGesture( - createHeader(), (RemoveSpaceGesture) gesture, resultReceiver); - } else if (gesture instanceof JoinOrSplitGesture) { - mConnection.performHandwritingJoinOrSplitGesture( - createHeader(), (JoinOrSplitGesture) gesture, resultReceiver); - } else if (consumer != null && executor != null) { - executor.execute(() - -> consumer.accept(InputConnection.HANDWRITING_GESTURE_RESULT_UNSUPPORTED)); - } + mConnection.performHandwritingGesture(createHeader(), gesture, resultReceiver); } catch (RemoteException e) { if (consumer != null && executor != null) { executor.execute(() -> consumer.accept( @@ -736,6 +743,28 @@ final class IRemoteInputConnectionInvoker { } /** + * Invokes {@link IRemoteInputConnection#requestTextBoundsInfo(InputConnectionCommandHeader, + * RectF, ResultReceiver)} + * @param rectF {@code rectF} parameter to be passed. + * @param executor {@code Executor} parameter to be passed. + * @param consumer {@code Consumer} parameter to be passed. + */ + @AnyThread + public void requestTextBoundsInfo( + @NonNull RectF rectF, @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<TextBoundsInfoResult> consumer) { + Objects.requireNonNull(executor); + Objects.requireNonNull(consumer); + + final ResultReceiver resultReceiver = new TextBoundsInfoResultReceiver(executor, consumer); + try { + mConnection.requestTextBoundsInfo(createHeader(), rectF, resultReceiver); + } catch (RemoteException e) { + executor.execute(() -> consumer.accept(new TextBoundsInfoResult(CODE_CANCELLED))); + } + } + + /** * Invokes {@link IRemoteInputConnection#commitContent(InputConnectionCommandHeader, * InputContentInfo, int, Bundle, AndroidFuture)}. * diff --git a/core/java/android/inputmethodservice/RemoteInputConnection.java b/core/java/android/inputmethodservice/RemoteInputConnection.java index 09e86c4c9650..976e71f2b85a 100644 --- a/core/java/android/inputmethodservice/RemoteInputConnection.java +++ b/core/java/android/inputmethodservice/RemoteInputConnection.java @@ -21,6 +21,7 @@ import android.annotation.CallbackExecutor; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.graphics.RectF; import android.os.Bundle; import android.os.Handler; import android.util.Log; @@ -32,8 +33,10 @@ import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.HandwritingGesture; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputContentInfo; +import android.view.inputmethod.ParcelableHandwritingGesture; import android.view.inputmethod.SurroundingText; import android.view.inputmethod.TextAttribute; +import android.view.inputmethod.TextBoundsInfoResult; import com.android.internal.inputmethod.CancellationGroup; import com.android.internal.inputmethod.CompletableFutureUtil; @@ -44,6 +47,7 @@ import com.android.internal.inputmethod.InputConnectionProtoDumper; import java.lang.ref.WeakReference; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; +import java.util.function.Consumer; import java.util.function.IntConsumer; /** @@ -418,7 +422,8 @@ final class RemoteInputConnection implements InputConnection { public void performHandwritingGesture( @NonNull HandwritingGesture gesture, @Nullable @CallbackExecutor Executor executor, @Nullable IntConsumer consumer) { - mInvoker.performHandwritingGesture(gesture, executor, consumer); + mInvoker.performHandwritingGesture(ParcelableHandwritingGesture.of(gesture), executor, + consumer); } @AnyThread @@ -460,6 +465,13 @@ final class RemoteInputConnection implements InputConnection { } @AnyThread + public void requestTextBoundsInfo( + @NonNull RectF rectF, @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<TextBoundsInfoResult> consumer) { + mInvoker.requestTextBoundsInfo(rectF, executor, consumer); + } + + @AnyThread public Handler getHandler() { // Nothing should happen when called from input method. return null; diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 2afa87980c11..1673ade04150 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -561,9 +561,11 @@ public final class Parcel { */ public final void recycle() { if (mRecycled) { - Log.w(TAG, "Recycle called on unowned Parcel. (recycle twice?) Here: " + Log.wtf(TAG, "Recycle called on unowned Parcel. (recycle twice?) Here: " + Log.getStackTraceString(new Throwable()) + " Original recycle call (if DEBUG_RECYCLE): ", mStack); + + return; } mRecycled = true; diff --git a/core/java/android/os/Parcelable.java b/core/java/android/os/Parcelable.java index 8a8045714d46..a2b0486c1df5 100644 --- a/core/java/android/os/Parcelable.java +++ b/core/java/android/os/Parcelable.java @@ -188,7 +188,7 @@ public interface Parcelable { * @return true if this parcelable is stable. * @hide */ - @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) default @Stability int getStability() { return PARCELABLE_STABILITY_LOCAL; } diff --git a/core/java/android/service/credentials/CredentialEntry.java b/core/java/android/service/credentials/CredentialEntry.java index a3fa979a6188..1d4ac25eb284 100644 --- a/core/java/android/service/credentials/CredentialEntry.java +++ b/core/java/android/service/credentials/CredentialEntry.java @@ -172,9 +172,11 @@ public final class CredentialEntry implements Parcelable { * {@code credential}, or the {@code pendingIntent}. */ public @NonNull Builder setPendingIntent(@Nullable PendingIntent pendingIntent) { - Preconditions.checkState(pendingIntent != null && mCredential != null, - "credential is already set. Cannot set both the pendingIntent " - + "and the credential"); + if (pendingIntent != null) { + Preconditions.checkState(mCredential != null, + "credential is already set. Cannot set both the pendingIntent " + + "and the credential"); + } mPendingIntent = pendingIntent; return this; } @@ -186,9 +188,11 @@ public final class CredentialEntry implements Parcelable { * the {@code pendingIntent}, or the {@code credential}. */ public @NonNull Builder setCredential(@Nullable Credential credential) { - Preconditions.checkState(credential != null && mPendingIntent != null, - "pendingIntent is already set. Cannot set both the " - + "pendingIntent and the credential"); + if (credential != null) { + Preconditions.checkState(mPendingIntent != null, + "pendingIntent is already set. Cannot set both the " + + "pendingIntent and the credential"); + } mCredential = credential; return this; } diff --git a/core/java/android/service/credentials/SaveEntry.java b/core/java/android/service/credentials/SaveEntry.java index abe51d43bc48..55ff6ff9755f 100644 --- a/core/java/android/service/credentials/SaveEntry.java +++ b/core/java/android/service/credentials/SaveEntry.java @@ -17,17 +17,11 @@ package android.service.credentials; import android.annotation.NonNull; -import android.annotation.Nullable; import android.app.PendingIntent; import android.app.slice.Slice; -import android.credentials.Credential; import android.os.Parcel; import android.os.Parcelable; -import com.android.internal.util.Preconditions; - -import java.util.Objects; - /** * An entry to be shown on the UI. This entry represents where the credential to be created will * be stored. Examples include user's account, family group etc. @@ -36,13 +30,11 @@ import java.util.Objects; */ public final class SaveEntry implements Parcelable { private final @NonNull Slice mSlice; - private final @Nullable PendingIntent mPendingIntent; - private final @Nullable Credential mCredential; + private final @NonNull PendingIntent mPendingIntent; private SaveEntry(@NonNull Parcel in) { mSlice = in.readTypedObject(Slice.CREATOR); mPendingIntent = in.readTypedObject(PendingIntent.CREATOR); - mCredential = in.readTypedObject(Credential.CREATOR); } public static final @NonNull Creator<SaveEntry> CREATOR = new Creator<SaveEntry>() { @@ -66,18 +58,23 @@ public final class SaveEntry implements Parcelable { public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeTypedObject(mSlice, flags); dest.writeTypedObject(mPendingIntent, flags); - dest.writeTypedObject(mCredential, flags); } - /* package-private */ SaveEntry( + /** + * Constructs a save entry to be displayed on the UI. + * + * @param slice the display content to be displayed on the UI, along with this entry + * @param pendingIntent the intent to be invoked when the user selects this entry + */ + public SaveEntry( @NonNull Slice slice, - @Nullable PendingIntent pendingIntent, - @Nullable Credential credential) { + @NonNull PendingIntent pendingIntent) { this.mSlice = slice; com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mSlice); this.mPendingIntent = pendingIntent; - this.mCredential = credential; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mPendingIntent); } /** Returns the content to be displayed with this save entry on the UI. */ @@ -86,76 +83,7 @@ public final class SaveEntry implements Parcelable { } /** Returns the pendingIntent to be invoked when this save entry on the UI is selectcd. */ - public @Nullable PendingIntent getPendingIntent() { + public @NonNull PendingIntent getPendingIntent() { return mPendingIntent; } - - /** Returns the credential produced by the {@link CreateCredentialRequest}. */ - public @Nullable Credential getCredential() { - return mCredential; - } - - /** - * A builder for {@link SaveEntry}. - */ - public static final class Builder { - - private @NonNull Slice mSlice; - private @Nullable PendingIntent mPendingIntent; - private @Nullable Credential mCredential; - - /** - * Builds the instance. - * @param slice the content to be displayed with this save entry - * - * @throws NullPointerException If {@code slice} is null. - */ - public Builder(@NonNull Slice slice) { - mSlice = Objects.requireNonNull(slice, "slice must not be null"); - } - - /** - * Sets the pendingIntent to be invoked when this entry is selected by the user. - * - * @throws IllegalStateException If {@code credential} is already set. Must only set either - * {@code credential}, or the {@code pendingIntent}. - */ - public @NonNull Builder setPendingIntent(@Nullable PendingIntent pendingIntent) { - Preconditions.checkState(pendingIntent != null - && mCredential != null, "credential is already set. Must only set " - + "either the pendingIntent or the credential"); - mPendingIntent = pendingIntent; - return this; - } - - /** - * Sets the credential to be returned when this entry is selected by the user. - * - * @throws IllegalStateException If {@code pendingIntent} is already set. Must only - * set either the {@code pendingIntent}, or {@code credential}. - */ - public @NonNull Builder setCredential(@Nullable Credential credential) { - Preconditions.checkState(credential != null && mPendingIntent != null, - "pendingIntent is already set. Must only set either the credential " - + "or the pendingIntent"); - mCredential = credential; - return this; - } - - /** - * Builds the instance. - * - * @throws IllegalStateException if both {@code pendingIntent} and {@code credential} - * are null. - */ - public @NonNull SaveEntry build() { - Preconditions.checkState(mPendingIntent == null && mCredential == null, - "pendingIntent and credential both must not be null. Must set " - + "either the pendingIntnet or the credential"); - return new SaveEntry( - mSlice, - mPendingIntent, - mCredential); - } - } } diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index c6cd708c5967..37fc9f288c3e 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -575,6 +575,7 @@ public abstract class WallpaperService extends Service { */ public void reportEngineShown(boolean waitForEngineShown) { if (mIWallpaperEngine.mShownReported) return; + Log.d(TAG, "reportEngineShown: shouldWait=" + waitForEngineShown); if (!waitForEngineShown) { Message message = mCaller.obtainMessage(MSG_REPORT_SHOWN); mCaller.removeMessages(MSG_REPORT_SHOWN); diff --git a/core/java/android/text/SegmentFinder.java b/core/java/android/text/SegmentFinder.java index c21c5774fa0a..be0094b28509 100644 --- a/core/java/android/text/SegmentFinder.java +++ b/core/java/android/text/SegmentFinder.java @@ -19,6 +19,13 @@ package android.text; import android.annotation.IntRange; import android.graphics.RectF; +import androidx.annotation.NonNull; + +import com.android.internal.util.Preconditions; + +import java.util.Arrays; +import java.util.Objects; + /** * Finds text segment boundaries within text. Subclasses can implement different types of text * segments. Grapheme clusters and words are examples of possible text segments. These are @@ -63,4 +70,144 @@ public abstract class SegmentFinder { * character offset, or {@code DONE} if there are none. */ public abstract int nextEndBoundary(@IntRange(from = 0) int offset); + + /** + * The default {@link SegmentFinder} implementation based on given segment ranges. + */ + public static class DefaultSegmentFinder extends SegmentFinder { + private final int[] mSegments; + + /** + * Create a SegmentFinder with segments stored in an array, where i-th segment's start is + * stored at segments[2 * i] and end is stored at segments[2 * i + 1] respectively. + * + * <p> It is required that segments do not overlap, and are already sorted by their start + * indices. </p> + * @param segments the array that stores the segment ranges. + * @throws IllegalArgumentException if the given segments array's length is not even; the + * given segments are not sorted or there are segments overlap with others. + */ + public DefaultSegmentFinder(@NonNull int[] segments) { + checkSegmentsValid(segments); + mSegments = segments; + } + + /** {@inheritDoc} */ + @Override + public int previousStartBoundary(@IntRange(from = 0) int offset) { + return findPrevious(offset, /* isStart = */ true); + } + + /** {@inheritDoc} */ + @Override + public int previousEndBoundary(@IntRange(from = 0) int offset) { + return findPrevious(offset, /* isStart = */ false); + } + + /** {@inheritDoc} */ + @Override + public int nextStartBoundary(@IntRange(from = 0) int offset) { + return findNext(offset, /* isStart = */ true); + } + + /** {@inheritDoc} */ + @Override + public int nextEndBoundary(@IntRange(from = 0) int offset) { + return findNext(offset, /* isStart = */ false); + } + + private int findNext(int offset, boolean isStart) { + if (offset < 0) return DONE; + if (mSegments.length < 1 || offset > mSegments[mSegments.length - 1]) return DONE; + + if (offset < mSegments[0]) { + return isStart ? mSegments[0] : mSegments[1]; + } + + int index = Arrays.binarySearch(mSegments, offset); + if (index >= 0) { + // mSegments may have duplicate elements (The previous segments end equals + // to the following segments start.) Move the index forwards since we are searching + // for the next segment. + if (index + 1 < mSegments.length && mSegments[index + 1] == offset) { + index = index + 1; + } + // Point the index to the first segment boundary larger than the given offset. + index += 1; + } else { + // binarySearch returns the insertion point, it's the first segment boundary larger + // than the given offset. + index = -(index + 1); + } + if (index >= mSegments.length) return DONE; + + // +---------------------------------------+ + // | | isStart | isEnd | + // |---------------+-----------+-----------| + // | indexIsStart | index | index + 1 | + // |---------------+-----------+-----------| + // | indexIsEnd | index + 1 | index | + // +---------------------------------------+ + boolean indexIsStart = index % 2 == 0; + if (isStart != indexIsStart) { + return (index + 1 < mSegments.length) ? mSegments[index + 1] : DONE; + } + return mSegments[index]; + } + + private int findPrevious(int offset, boolean isStart) { + if (mSegments.length < 1 || offset < mSegments[0]) return DONE; + + if (offset > mSegments[mSegments.length - 1]) { + return isStart ? mSegments[mSegments.length - 2] : mSegments[mSegments.length - 1]; + } + + int index = Arrays.binarySearch(mSegments, offset); + if (index >= 0) { + // mSegments may have duplicate elements (when the previous segments end equal + // to the following segments start). Move the index backwards since we are searching + // for the previous segment. + if (index > 0 && mSegments[index - 1] == offset) { + index = index - 1; + } + // Point the index to the first segment boundary smaller than the given offset. + index -= 1; + } else { + // binarySearch returns the insertion point, insertionPoint - 1 is the first + // segment boundary smaller than the given offset. + index = -(index + 1) - 1; + } + if (index < 0) return DONE; + + // +---------------------------------------+ + // | | isStart | isEnd | + // |---------------+-----------+-----------| + // | indexIsStart | index | index - 1 | + // |---------------+-----------+-----------| + // | indexIsEnd | index - 1 | index | + // +---------------------------------------+ + boolean indexIsStart = index % 2 == 0; + if (isStart != indexIsStart) { + return (index > 0) ? mSegments[index - 1] : DONE; + } + return mSegments[index]; + } + + private static void checkSegmentsValid(int[] segments) { + Objects.requireNonNull(segments); + Preconditions.checkArgument(segments.length % 2 == 0, + "the length of segments must be even"); + if (segments.length == 0) return; + int lastSegmentEnd = Integer.MIN_VALUE; + for (int index = 0; index < segments.length; index += 2) { + if (segments[index] < lastSegmentEnd) { + throw new IllegalArgumentException("segments can't overlap"); + } + if (segments[index] >= segments[index + 1]) { + throw new IllegalArgumentException("the segment range can't be empty"); + } + lastSegmentEnd = segments[index + 1]; + } + } + } } diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java index d1f05ec0c05e..7b6a6d29baf8 100644 --- a/core/java/android/util/FeatureFlagUtils.java +++ b/core/java/android/util/FeatureFlagUtils.java @@ -103,6 +103,25 @@ public class FeatureFlagUtils { public static final String SETTINGS_NEW_KEYBOARD_UI = "settings_new_keyboard_ui"; /** + * Enable new shortcut list UI + * @hide + */ + public static final String SETTINGS_NEW_KEYBOARD_SHORTCUT = "settings_new_keyboard_shortcut"; + + /** + * Enable new modifier key settings UI + * @hide + */ + public static final String SETTINGS_NEW_KEYBOARD_MODIFIER_KEY = + "settings_new_keyboard_modifier_key"; + + /** + * Enable new trackpad settings UI + * @hide + */ + public static final String SETTINGS_NEW_KEYBOARD_TRACKPAD = "settings_new_keyboard_trackpad"; + + /** * Enable the new pages which is implemented with SPA. * @hide */ @@ -143,6 +162,9 @@ public class FeatureFlagUtils { DEFAULT_FLAGS.put(SETTINGS_HIDE_SECOND_LAYER_PAGE_NAVIGATE_UP_BUTTON_IN_TWO_PANE, "true"); DEFAULT_FLAGS.put(SETTINGS_AUTO_TEXT_WRAPPING, "false"); DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_UI, "false"); + DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_SHORTCUT, "false"); + DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY, "false"); + DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_TRACKPAD, "false"); DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA, "false"); DEFAULT_FLAGS.put(SETTINGS_ADB_METRICS_WRITER, "false"); } @@ -158,6 +180,9 @@ public class FeatureFlagUtils { PERSISTENT_FLAGS.add(SETTINGS_HIDE_SECOND_LAYER_PAGE_NAVIGATE_UP_BUTTON_IN_TWO_PANE); PERSISTENT_FLAGS.add(SETTINGS_AUTO_TEXT_WRAPPING); PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_UI); + PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_SHORTCUT); + PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY); + PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_TRACKPAD); } /** diff --git a/core/java/android/view/HandwritingDelegateConfiguration.java b/core/java/android/view/HandwritingDelegateConfiguration.java new file mode 100644 index 000000000000..524bb4ca68ab --- /dev/null +++ b/core/java/android/view/HandwritingDelegateConfiguration.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.annotation.IdRes; +import android.annotation.NonNull; + +/** + * Configuration for a view to act as a handwriting initiation delegate. This allows handwriting + * mode for a delegator editor view to be initiated by stylus movement on the delegate view. + * + * <p>If a stylus {@link MotionEvent} occurs within the delegate view's bounds, the callback + * returned by {@link #getInitiationCallback()} will be called. The callback implementation is + * expected to show and focus the delegator editor view. If a view with identifier matching {@link + * #getDelegatorViewId()} creates an input connection while the same stylus {@link MotionEvent} + * sequence is ongoing, handwriting mode will be initiated for that view. + * + * <p>A common use case is a custom view which looks like a text editor but does not actually + * support text editing itself, and clicking on the custom view causes an EditText to be shown. To + * support handwriting initiation in this case, {@link View#setHandwritingDelegateConfiguration} can + * be called on the custom view to configure it as a delegate, and set the EditText as the delegator + * by passing the EditText's identifier as the {@code delegatorViewId}. The {@code + * initiationCallback} implementation is typically the same as the click listener implementation + * which shows the EditText. + */ +public class HandwritingDelegateConfiguration { + @IdRes private final int mDelegatorViewId; + @NonNull private final Runnable mInitiationCallback; + + /** + * Constructs a HandwritingDelegateConfiguration instance. + * + * @param delegatorViewId identifier of the delegator editor view for which handwriting mode + * should be initiated + * @param initiationCallback callback called when a stylus {@link MotionEvent} occurs within + * this view's bounds + */ + public HandwritingDelegateConfiguration( + @IdRes int delegatorViewId, @NonNull Runnable initiationCallback) { + mDelegatorViewId = delegatorViewId; + mInitiationCallback = initiationCallback; + } + + /** + * Returns the identifier of the delegator editor view for which handwriting mode should be + * initiated. + */ + public int getDelegatorViewId() { + return mDelegatorViewId; + } + + /** + * Returns the callback which should be called when a stylus {@link MotionEvent} occurs within + * the delegate view's bounds. + */ + @NonNull + public Runnable getInitiationCallback() { + return mInitiationCallback; + } +} diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java index a0a07b30662f..2e4073e9cfd0 100644 --- a/core/java/android/view/HandwritingInitiator.java +++ b/core/java/android/view/HandwritingInitiator.java @@ -16,6 +16,7 @@ package android.view; +import android.annotation.IdRes; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Rect; @@ -161,6 +162,15 @@ public class HandwritingInitiator { if (candidateView != null) { if (candidateView == getConnectedView()) { startHandwriting(candidateView); + } else if (candidateView.getHandwritingDelegateConfiguration() != null) { + mState.mDelegatorViewId = + candidateView + .getHandwritingDelegateConfiguration() + .getDelegatorViewId(); + candidateView + .getHandwritingDelegateConfiguration() + .getInitiationCallback() + .run(); } else { if (candidateView.getRevealOnFocusHint()) { candidateView.setRevealOnFocusHint(false); @@ -259,8 +269,10 @@ public class HandwritingInitiator { } final Rect handwritingArea = getViewHandwritingArea(connectedView); - if (isInHandwritingArea(handwritingArea, mState.mStylusDownX, - mState.mStylusDownY, connectedView)) { + if ((mState.mDelegatorViewId != View.NO_ID + && mState.mDelegatorViewId == connectedView.getId()) + || isInHandwritingArea( + handwritingArea, mState.mStylusDownX, mState.mStylusDownY, connectedView)) { startHandwriting(connectedView); } else { mState.mShouldInitHandwriting = false; @@ -287,6 +299,11 @@ public class HandwritingInitiator { if (!view.isAutoHandwritingEnabled()) { return false; } + // The view may be a handwriting initiation delegate, in which case it is not the editor + // view for which handwriting would be started. However, in almost all cases, the return + // values of View#isStylusHandwritingAvailable will be the same for the delegate view and + // the delegator editor view. So the delegate view can be used to decide whether handwriting + // should be triggered. return view.isStylusHandwritingAvailable(); } @@ -473,6 +490,13 @@ public class HandwritingInitiator { * built InputConnection. */ private boolean mExceedHandwritingSlop; + /** + * If the current ongoing stylus MotionEvent sequence started over a handwriting initiation + * delegate view, then this is the view identifier of the corresponding delegator view. If + * the delegator view creates an input connection while the MotionEvent sequence is still + * ongoing, then handwriting mode will be initiated for the delegator view. + */ + @IdRes private int mDelegatorViewId = View.NO_ID; /** The pointer id of the stylus pointer that is being tracked. */ private final int mStylusPointerId; diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 7b6ebf7cd9b8..49d9e67ce51d 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -5063,6 +5063,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private boolean mHoveringTouchDelegate = false; /** + * Configuration for this view to act as a handwriting initiation delegate. This allows + * handwriting mode for a delegator editor view to be initiated by stylus movement on this + * delegate view. + */ + private HandwritingDelegateConfiguration mHandwritingDelegateConfiguration; + + /** * Solid color to use as a background when creating the drawing cache. Enables * the cache to use 16 bit bitmaps instead of 32 bit. */ @@ -12255,6 +12262,30 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Configures this view to act as a handwriting initiation delegate. This allows handwriting + * mode for a delegator editor view to be initiated by stylus movement on this delegate view. + * + * <p>If {@code null} is passed, this view will no longer act as a handwriting initiation + * delegate. + */ + public void setHandwritingDelegateConfiguration( + @Nullable HandwritingDelegateConfiguration configuration) { + mHandwritingDelegateConfiguration = configuration; + if (configuration != null) { + setHandwritingArea(new Rect(0, 0, getWidth(), getHeight())); + } + } + + /** + * If this view has been configured as a handwriting initiation delegate, returns the delegate + * configuration. + */ + @Nullable + public HandwritingDelegateConfiguration getHandwritingDelegateConfiguration() { + return mHandwritingDelegateConfiguration; + } + + /** * Gets the coordinates of this view in the coordinate space of the * {@link Surface} that contains the view. * @@ -24205,7 +24236,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } rebuildOutline(); - if (onCheckIsTextEditor()) { + if (onCheckIsTextEditor() || mHandwritingDelegateConfiguration != null) { setHandwritingArea(new Rect(0, 0, newWidth, newHeight)); } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index ff4588a7bc9b..e664ebf4c484 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -1257,7 +1257,7 @@ public final class ViewRootImpl implements ViewParent, mTmpFrames.attachedFrame = attachedFrame; mTmpFrames.sizeCompatScale = sizeCompatScale[0]; mInvSizeCompatScale = 1f / sizeCompatScale[0]; - } catch (RemoteException e) { + } catch (RemoteException | RuntimeException e) { mAdded = false; mView = null; mAttachInfo.mRootView = null; diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java index 7d268a925f60..d6d7339b602c 100644 --- a/core/java/android/view/inputmethod/InputConnection.java +++ b/core/java/android/view/inputmethod/InputConnection.java @@ -16,11 +16,14 @@ package android.view.inputmethod; +import static android.view.inputmethod.TextBoundsInfoResult.CODE_UNSUPPORTED; + import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.graphics.RectF; import android.inputmethodservice.InputMethodService; import android.os.Bundle; import android.os.Handler; @@ -32,7 +35,9 @@ import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Objects; import java.util.concurrent.Executor; +import java.util.function.Consumer; import java.util.function.IntConsumer; /** @@ -1205,6 +1210,40 @@ public interface InputConnection { return false; } + + /** + * Called by input method to request the {@link TextBoundsInfo} for a range of text which is + * covered by or in vicinity of the given {@code RectF}. It can be used as a supplementary + * method to implement the handwriting gesture API - + * {@link #performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)}. + * + * <p><strong>Editor authors</strong>: It's preferred that the editor returns a + * {@link TextBoundsInfo} of all the text lines whose bounds intersect with the given + * {@code rectF}. + * </p> + * + * <p><strong>IME authors</strong>: This method is expensive when the text is long. Please + * consider that both the text bounds computation and IPC round-trip to send the data are time + * consuming. It's preferable to only request text bounds in smaller areas. + * </p> + * + * @param rectF the interested area where the text bounds are requested, in the screen + * coordinates. + * @param executor the executor to run the callback. + * @param consumer the callback invoked by editor to return the result. It must return a + * non-null object. + * + * @see TextBoundsInfo + * @see android.view.inputmethod.TextBoundsInfoResult + */ + default void requestTextBoundsInfo( + @NonNull RectF rectF, @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<TextBoundsInfoResult> consumer) { + Objects.requireNonNull(executor); + Objects.requireNonNull(consumer); + executor.execute(() -> consumer.accept(new TextBoundsInfoResult(CODE_UNSUPPORTED))); + } + /** * Called by the system to enable application developers to specify a dedicated thread on which * {@link InputConnection} methods are called back. diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java index 56beddf2ef38..7af96b60938c 100644 --- a/core/java/android/view/inputmethod/InputConnectionWrapper.java +++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java @@ -20,6 +20,7 @@ import android.annotation.CallbackExecutor; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.graphics.RectF; import android.os.Bundle; import android.os.Handler; import android.view.KeyEvent; @@ -27,6 +28,7 @@ import android.view.KeyEvent; import com.android.internal.util.Preconditions; import java.util.concurrent.Executor; +import java.util.function.Consumer; import java.util.function.IntConsumer; /** @@ -347,6 +349,17 @@ public class InputConnectionWrapper implements InputConnection { * @throws NullPointerException if the target is {@code null}. */ @Override + public void requestTextBoundsInfo( + @NonNull RectF rectF, @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<TextBoundsInfoResult> consumer) { + mTarget.requestTextBoundsInfo(rectF, executor, consumer); + } + + /** + * {@inheritDoc} + * @throws NullPointerException if the target is {@code null}. + */ + @Override public Handler getHandler() { return mTarget.getHandler(); } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 53d77e146d89..1697bf8bcf7b 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -1902,8 +1902,11 @@ public final class InputMethodManager { * a result receiver: explicitly request that the current input method's * soft input area be shown to the user, if needed. * - * @param view The currently focused view, which would like to receive - * soft keyboard input. + * @param view The currently focused view, which would like to receive soft keyboard input. + * Note that this view is only considered focused here if both it itself has + * {@link View#isFocused view focus}, and its containing window has + * {@link View#hasWindowFocus window focus}. Otherwise the call fails and + * returns {@code false}. * @param flags Provides additional operating flags. Currently may be * 0 or have the {@link #SHOW_IMPLICIT} bit set. */ @@ -1965,8 +1968,11 @@ public final class InputMethodManager { * can be garbage collected regardless of the lifetime of * {@link ResultReceiver}. * - * @param view The currently focused view, which would like to receive - * soft keyboard input. + * @param view The currently focused view, which would like to receive soft keyboard input. + * Note that this view is only considered focused here if both it itself has + * {@link View#isFocused view focus}, and its containing window has + * {@link View#hasWindowFocus window focus}. Otherwise the call fails and + * returns {@code false}. * @param flags Provides additional operating flags. Currently may be * 0 or have the {@link #SHOW_IMPLICIT} bit set. * @param resultReceiver If non-null, this will be called by the IME when diff --git a/core/java/android/view/inputmethod/ParcelableHandwritingGesture.aidl b/core/java/android/view/inputmethod/ParcelableHandwritingGesture.aidl new file mode 100644 index 000000000000..ffadf820325e --- /dev/null +++ b/core/java/android/view/inputmethod/ParcelableHandwritingGesture.aidl @@ -0,0 +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 android.view.inputmethod; + +parcelable ParcelableHandwritingGesture; diff --git a/core/java/android/view/inputmethod/ParcelableHandwritingGesture.java b/core/java/android/view/inputmethod/ParcelableHandwritingGesture.java new file mode 100644 index 000000000000..e4066fcfd61d --- /dev/null +++ b/core/java/android/view/inputmethod/ParcelableHandwritingGesture.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.inputmethod; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * A generic container of parcelable {@link HandwritingGesture}. + * + * @hide + */ +public final class ParcelableHandwritingGesture implements Parcelable { + @NonNull + private final HandwritingGesture mGesture; + @NonNull + private final Parcelable mGestureAsParcelable; + + private ParcelableHandwritingGesture(@NonNull HandwritingGesture gesture) { + mGesture = gesture; + // For fail-fast. + mGestureAsParcelable = (Parcelable) gesture; + } + + /** + * Creates {@link ParcelableHandwritingGesture} from {@link HandwritingGesture}, which also + * implements {@link Parcelable}. + * + * @param gesture {@link HandwritingGesture} object to be stored. + * @return {@link ParcelableHandwritingGesture} to be stored in {@link Parcel}. + */ + @NonNull + public static ParcelableHandwritingGesture of(@NonNull HandwritingGesture gesture) { + return new ParcelableHandwritingGesture(Objects.requireNonNull(gesture)); + } + + /** + * @return {@link HandwritingGesture} object stored in this container. + */ + @NonNull + public HandwritingGesture get() { + return mGesture; + } + + private static HandwritingGesture createFromParcelInternal( + @HandwritingGesture.GestureType int gestureType, @NonNull Parcel parcel) { + switch (gestureType) { + case HandwritingGesture.GESTURE_TYPE_NONE: + throw new UnsupportedOperationException("GESTURE_TYPE_NONE is not supported"); + case HandwritingGesture.GESTURE_TYPE_SELECT: + return SelectGesture.CREATOR.createFromParcel(parcel); + case HandwritingGesture.GESTURE_TYPE_SELECT_RANGE: + return SelectRangeGesture.CREATOR.createFromParcel(parcel); + case HandwritingGesture.GESTURE_TYPE_INSERT: + return InsertGesture.CREATOR.createFromParcel(parcel); + case HandwritingGesture.GESTURE_TYPE_DELETE: + return DeleteGesture.CREATOR.createFromParcel(parcel); + case HandwritingGesture.GESTURE_TYPE_DELETE_RANGE: + return DeleteRangeGesture.CREATOR.createFromParcel(parcel); + case HandwritingGesture.GESTURE_TYPE_JOIN_OR_SPLIT: + return JoinOrSplitGesture.CREATOR.createFromParcel(parcel); + case HandwritingGesture.GESTURE_TYPE_REMOVE_SPACE: + return RemoveSpaceGesture.CREATOR.createFromParcel(parcel); + default: + throw new UnsupportedOperationException("Unknown type=" + gestureType); + } + } + + public static final Creator<ParcelableHandwritingGesture> CREATOR = new Parcelable.Creator<>() { + @Override + public ParcelableHandwritingGesture createFromParcel(Parcel in) { + final int gestureType = in.readInt(); + return new ParcelableHandwritingGesture(createFromParcelInternal(gestureType, in)); + } + + @Override + public ParcelableHandwritingGesture[] newArray(int size) { + return new ParcelableHandwritingGesture[size]; + } + }; + + @Override + public int describeContents() { + return mGestureAsParcelable.describeContents(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mGesture.getGestureType()); + mGestureAsParcelable.writeToParcel(dest, flags); + } +} diff --git a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java index f2b70997de63..ead79245609a 100644 --- a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java +++ b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java @@ -28,6 +28,7 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; +import android.graphics.RectF; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -982,62 +983,9 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub { @Dispatching(cancellable = true) @Override - public void performHandwritingSelectGesture( - InputConnectionCommandHeader header, SelectGesture gesture, + public void performHandwritingGesture( + InputConnectionCommandHeader header, ParcelableHandwritingGesture gestureContainer, ResultReceiver resultReceiver) { - performHandwritingGestureInternal(header, gesture, resultReceiver); - } - - @Dispatching(cancellable = true) - @Override - public void performHandwritingSelectRangeGesture( - InputConnectionCommandHeader header, SelectRangeGesture gesture, - ResultReceiver resultReceiver) { - performHandwritingGestureInternal(header, gesture, resultReceiver); - } - - @Dispatching(cancellable = true) - @Override - public void performHandwritingInsertGesture( - InputConnectionCommandHeader header, InsertGesture gesture, - ResultReceiver resultReceiver) { - performHandwritingGestureInternal(header, gesture, resultReceiver); - } - - @Dispatching(cancellable = true) - @Override - public void performHandwritingDeleteGesture( - InputConnectionCommandHeader header, DeleteGesture gesture, - ResultReceiver resultReceiver) { - performHandwritingGestureInternal(header, gesture, resultReceiver); - } - - @Dispatching(cancellable = true) - @Override - public void performHandwritingDeleteRangeGesture( - InputConnectionCommandHeader header, DeleteRangeGesture gesture, - ResultReceiver resultReceiver) { - performHandwritingGestureInternal(header, gesture, resultReceiver); - } - - @Dispatching(cancellable = true) - @Override - public void performHandwritingRemoveSpaceGesture( - InputConnectionCommandHeader header, RemoveSpaceGesture gesture, - ResultReceiver resultReceiver) { - performHandwritingGestureInternal(header, gesture, resultReceiver); - } - - @Dispatching(cancellable = true) - @Override - public void performHandwritingJoinOrSplitGesture( - InputConnectionCommandHeader header, JoinOrSplitGesture gesture, - ResultReceiver resultReceiver) { - performHandwritingGestureInternal(header, gesture, resultReceiver); - } - - private <T extends HandwritingGesture> void performHandwritingGestureInternal( - InputConnectionCommandHeader header, T gesture, ResultReceiver resultReceiver) { dispatchWithTracing("performHandwritingGesture", () -> { if (header.mSessionId != mCurrentSessionId.get()) { if (resultReceiver != null) { @@ -1059,7 +1007,7 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub { // TODO(210039666): implement Cleaner to return HANDWRITING_GESTURE_RESULT_UNKNOWN if // editor doesn't return any type. ic.performHandwritingGesture( - gesture, + gestureContainer.get(), resultReceiver != null ? Runnable::run : null, resultReceiver != null ? (resultCode) -> resultReceiver.send(resultCode, null /* resultData */) @@ -1147,6 +1095,36 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub { @Dispatching(cancellable = true) @Override + public void requestTextBoundsInfo( + InputConnectionCommandHeader header, RectF rectF, + @NonNull ResultReceiver resultReceiver) { + dispatchWithTracing("requestTextBoundsInfo", () -> { + if (header.mSessionId != mCurrentSessionId.get()) { + resultReceiver.send(TextBoundsInfoResult.CODE_CANCELLED, null); + return; // cancelled + } + InputConnection ic = getInputConnection(); + if (ic == null || !isActive()) { + Log.w(TAG, "requestTextBoundsInfo on inactive InputConnection"); + resultReceiver.send(TextBoundsInfoResult.CODE_CANCELLED, null); + return; + } + + ic.requestTextBoundsInfo( + rectF, + Runnable::run, + (textBoundsInfoResult) -> { + final int resultCode = textBoundsInfoResult.getResultCode(); + final TextBoundsInfo textBoundsInfo = + textBoundsInfoResult.getTextBoundsInfo(); + resultReceiver.send(resultCode, + textBoundsInfo == null ? null : textBoundsInfo.toBundle()); + }); + }); + } + + @Dispatching(cancellable = true) + @Override public void commitContent(InputConnectionCommandHeader header, InputContentInfo inputContentInfo, int flags, Bundle opts, AndroidFuture future /* T=Boolean */) { diff --git a/core/java/android/view/inputmethod/TextBoundsInfo.java b/core/java/android/view/inputmethod/TextBoundsInfo.java new file mode 100644 index 000000000000..4e87405f0137 --- /dev/null +++ b/core/java/android/view/inputmethod/TextBoundsInfo.java @@ -0,0 +1,844 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.inputmethod; + +import android.annotation.IntDef; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.SegmentFinder; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * The text bounds information of a slice of text in the editor. + * + * <p> This class provides IME the layout information of the text within the range from + * {@link #getStart()} to {@link #getEnd()}. It's intended to be used by IME as a supplementary API + * to support handwriting gestures. + * </p> + */ +public final class TextBoundsInfo implements Parcelable { + /** + * The flag indicating that the character is a whitespace. + * + * @see Builder#setCharacterFlags(int[]) + * @see #getCharacterFlags(int) + */ + public static final int FLAG_CHARACTER_WHITESPACE = 1; + + /** + * The flag indicating that the character is a linefeed character. + * + * @see Builder#setCharacterFlags(int[]) + * @see #getCharacterFlags(int) + */ + public static final int FLAG_CHARACTER_LINEFEED = 1 << 1; + + /** + * The flag indicating that the character is a punctuation. + * + * @see Builder#setCharacterFlags(int[]) + * @see #getCharacterFlags(int) + */ + public static final int FLAG_CHARACTER_PUNCTUATION = 1 << 2; + + /** + * The flag indicating that the line this character belongs to has RTL line direction. It's + * required that all characters in the same line must have the same direction. + * + * @see Builder#setCharacterFlags(int[]) + * @see #getCharacterFlags(int) + */ + public static final int FLAG_LINE_IS_RTL = 1 << 3; + + + /** @hide */ + @IntDef(prefix = "FLAG_", flag = true, value = { + FLAG_CHARACTER_WHITESPACE, + FLAG_CHARACTER_LINEFEED, + FLAG_CHARACTER_PUNCTUATION, + FLAG_LINE_IS_RTL + }) + @Retention(RetentionPolicy.SOURCE) + public @interface CharacterFlags {} + + /** All the valid flags. */ + private static final int KNOWN_CHARACTER_FLAGS = FLAG_CHARACTER_WHITESPACE + | FLAG_CHARACTER_LINEFEED | FLAG_CHARACTER_PUNCTUATION | FLAG_LINE_IS_RTL; + + /** + * The amount of shift to get the character's BiDi level from the internal character flags. + */ + private static final int BIDI_LEVEL_SHIFT = 19; + + /** + * The mask used to get the character's BiDi level from the internal character flags. + */ + private static final int BIDI_LEVEL_MASK = 0x7F << BIDI_LEVEL_SHIFT; + + /** + * The flag indicating that the character at the index is the start of a line segment. + * This flag is only used internally to serialize the {@link SegmentFinder}. + * + * @see #writeToParcel(Parcel, int) + */ + private static final int FLAG_LINE_SEGMENT_START = 1 << 31; + + /** + * The flag indicating that the character at the index is the end of a line segment. + * This flag is only used internally to serialize the {@link SegmentFinder}. + * + * @see #writeToParcel(Parcel, int) + */ + private static final int FLAG_LINE_SEGMENT_END = 1 << 30; + + /** + * The flag indicating that the character at the index is the start of a word segment. + * This flag is only used internally to serialize the {@link SegmentFinder}. + * + * @see #writeToParcel(Parcel, int) + */ + private static final int FLAG_WORD_SEGMENT_START = 1 << 29; + + /** + * The flag indicating that the character at the index is the end of a word segment. + * This flag is only used internally to serialize the {@link SegmentFinder}. + * + * @see #writeToParcel(Parcel, int) + */ + private static final int FLAG_WORD_SEGMENT_END = 1 << 28; + + /** + * The flag indicating that the character at the index is the start of a grapheme segment. + * It's only used internally to serialize the {@link SegmentFinder}. + * + * @see #writeToParcel(Parcel, int) + */ + private static final int FLAG_GRAPHEME_SEGMENT_START = 1 << 27; + + /** + * The flag indicating that the character at the index is the end of a grapheme segment. + * It's only used internally to serialize the {@link SegmentFinder}. + * + * @see #writeToParcel(Parcel, int) + */ + private static final int FLAG_GRAPHEME_SEGMENT_END = 1 << 26; + + private final int mStart; + private final int mEnd; + private final float[] mMatrixValues; + private final float[] mCharacterBounds; + /** + * The array that encodes character and BiDi levels. They are stored together to save memory + * space, and it's easier during serialization. + */ + private final int[] mInternalCharacterFlags; + private final SegmentFinder mLineSegmentFinder; + private final SegmentFinder mWordSegmentFinder; + private final SegmentFinder mGraphemeSegmentFinder; + + /** + * Returns a new instance of {@link android.graphics.Matrix} that indicates the transformation + * matrix that is to be applied other positional data in this class. + * + * @return a new instance (copy) of the transformation matrix. + */ + @NonNull + public Matrix getMatrix() { + final Matrix matrix = new Matrix(); + matrix.setValues(mMatrixValues); + return matrix; + } + + /** + * Returns the index of the first character whose bounds information is available in this + * {@link TextBoundsInfo}, inclusive. + * + * @see Builder#setStartAndEnd(int, int) + */ + public int getStart() { + return mStart; + } + + /** + * Returns the index of the last character whose bounds information is available in this + * {@link TextBoundsInfo}, exclusive. + * + * @see Builder#setStartAndEnd(int, int) + */ + public int getEnd() { + return mEnd; + } + + /** + * Return the bounds of the character at the given {@code index}, in the coordinates of the + * editor. + * + * @param index the index of the queried character. + * @return the bounding box of the queried character. + * + * @throws IndexOutOfBoundsException if the given {@code index} is out of the range from + * the {@code start} to the {@code end}. + */ + @NonNull + public RectF getCharacterBounds(int index) { + if (index < mStart || index >= mEnd) { + throw new IndexOutOfBoundsException("Index is out of the bounds of " + + "[" + mStart + ", " + mEnd + ")."); + } + final int offset = 4 * (index - mStart); + return new RectF(mCharacterBounds[offset], mCharacterBounds[offset + 1], + mCharacterBounds[offset + 2], mCharacterBounds[offset + 3]); + } + + /** + * Return the flags associated with the character at the given {@code index}. + * The flags contain the following information: + * <ul> + * <li>The {@link #FLAG_CHARACTER_WHITESPACE} flag, indicating the character is a + * whitespace. </li> + * <li>The {@link #FLAG_CHARACTER_LINEFEED} flag, indicating the character is a + * linefeed. </li> + * <li>The {@link #FLAG_CHARACTER_PUNCTUATION} flag, indicating the character is a + * punctuation. </li> + * <li>The {@link #FLAG_LINE_IS_RTL} flag, indicating the line this character belongs to + * has RTL line direction. All characters in the same line must have the same line + * direction. Check {@link #getLineSegmentFinder()} for more information of + * line boundaries. </li> + * </ul> + * + * @param index the index of the queried character. + * @return the flags associated with the queried character. + * + * @throws IndexOutOfBoundsException if the given {@code index} is out of the range from + * the {@code start} to the {@code end}. + * + * @see #FLAG_CHARACTER_WHITESPACE + * @see #FLAG_CHARACTER_LINEFEED + * @see #FLAG_CHARACTER_PUNCTUATION + * @see #FLAG_LINE_IS_RTL + */ + @CharacterFlags + public int getCharacterFlags(int index) { + if (index < mStart || index >= mEnd) { + throw new IndexOutOfBoundsException("Index is out of the bounds of " + + "[" + mStart + ", " + mEnd + ")."); + } + final int offset = index - mStart; + return mInternalCharacterFlags[offset] & KNOWN_CHARACTER_FLAGS; + } + + /** + * The BiDi level of the character at the given {@code index}. <br/> + * BiDi level is defined by + * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm" >the unicode + * bidirectional algorithm </a>. One can determine whether a character's direction is + * right-to-left (RTL) or left-to-right (LTR) by checking the last bit of the BiDi level. + * If it's 1, the character is RTL, otherwise the character is LTR. The BiDi level of a + * character must be in the range of [0, 125]. + * + * @param index the index of the queried character. + * @return the BiDi level of the character, which is an integer in the range of [0, 125]. + * @throws IndexOutOfBoundsException if the given {@code index} is out of the range from + * the {@code start} to the {@code end}. + * + * @see Builder#setCharacterBidiLevel(int[]) + */ + @IntRange(from = 0, to = 125) + public int getCharacterBidiLevel(int index) { + if (index < mStart || index >= mEnd) { + throw new IndexOutOfBoundsException("Index is out of the bounds of " + + "[" + mStart + ", " + mEnd + ")."); + } + final int offset = index - mStart; + return (mInternalCharacterFlags[offset] & BIDI_LEVEL_MASK) >> BIDI_LEVEL_SHIFT; + } + + /** + * Returns the {@link SegmentFinder} that locates the word boundaries. + * + * @see Builder#setWordSegmentFinder(SegmentFinder) + */ + @NonNull + public SegmentFinder getWordSegmentFinder() { + return mWordSegmentFinder; + } + + /** + * Returns the {@link SegmentFinder} that locates the grapheme boundaries. + * + * @see Builder#setGraphemeSegmentFinder(SegmentFinder) + */ + @NonNull + public SegmentFinder getGraphemeSegmentFinder() { + return mGraphemeSegmentFinder; + } + + /** + * Returns the {@link SegmentFinder} that locates the line boundaries. + * + * @see Builder#setLineSegmentFinder(SegmentFinder) + */ + @NonNull + public SegmentFinder getLineSegmentFinder() { + return mLineSegmentFinder; + } + + /** + * Describe the kinds of special objects contained in this Parcelable + * instance's marshaled representation. For example, if the object will + * include a file descriptor in the output of {@link #writeToParcel(Parcel, int)}, + * the return value of this method must include the + * {@link #CONTENTS_FILE_DESCRIPTOR} bit. + * + * @return a bitmask indicating the set of special object types marshaled + * by this Parcelable object instance. + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Flatten this object in to a Parcel. + * + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written. + * May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}. + */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mStart); + dest.writeInt(mEnd); + dest.writeFloatArray(mMatrixValues); + dest.writeFloatArray(mCharacterBounds); + + // The end can also be a break position. We need an extra space to encode the breaks. + final int[] encodedFlags = Arrays.copyOf(mInternalCharacterFlags, mEnd - mStart + 1); + encodeSegmentFinder(encodedFlags, FLAG_GRAPHEME_SEGMENT_START, FLAG_GRAPHEME_SEGMENT_END, + mStart, mEnd, mGraphemeSegmentFinder); + encodeSegmentFinder(encodedFlags, FLAG_WORD_SEGMENT_START, FLAG_WORD_SEGMENT_END, mStart, + mEnd, mWordSegmentFinder); + encodeSegmentFinder(encodedFlags, FLAG_LINE_SEGMENT_START, FLAG_LINE_SEGMENT_END, mStart, + mEnd, mLineSegmentFinder); + dest.writeIntArray(encodedFlags); + } + + private TextBoundsInfo(Parcel source) { + mStart = source.readInt(); + mEnd = source.readInt(); + mMatrixValues = Objects.requireNonNull(source.createFloatArray()); + mCharacterBounds = Objects.requireNonNull(source.createFloatArray()); + final int[] encodedFlags = Objects.requireNonNull(source.createIntArray()); + + mGraphemeSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_GRAPHEME_SEGMENT_START, + FLAG_GRAPHEME_SEGMENT_END, mStart, mEnd); + mWordSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_WORD_SEGMENT_START, + FLAG_WORD_SEGMENT_END, mStart, mEnd); + mLineSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_LINE_SEGMENT_START, + FLAG_LINE_SEGMENT_END, mStart, mEnd); + + final int length = mEnd - mStart; + final int flagsMask = KNOWN_CHARACTER_FLAGS | BIDI_LEVEL_MASK; + mInternalCharacterFlags = new int[length]; + for (int i = 0; i < length; ++i) { + // Remove the flags used to encoded segment boundaries. + mInternalCharacterFlags[i] = encodedFlags[i] & flagsMask; + } + } + + private TextBoundsInfo(Builder builder) { + mStart = builder.mStart; + mEnd = builder.mEnd; + mMatrixValues = Arrays.copyOf(builder.mMatrixValues, 9); + final int length = mEnd - mStart; + mCharacterBounds = Arrays.copyOf(builder.mCharacterBounds, 4 * length); + // Store characterFlags and characterBidiLevels to save memory. + mInternalCharacterFlags = new int[length]; + for (int index = 0; index < length; ++index) { + mInternalCharacterFlags[index] = builder.mCharacterFlags[index] + | (builder.mCharacterBidiLevels[index] << BIDI_LEVEL_SHIFT); + } + mGraphemeSegmentFinder = builder.mGraphemeSegmentFinder; + mWordSegmentFinder = builder.mWordSegmentFinder; + mLineSegmentFinder = builder.mLineSegmentFinder; + } + + /** + * The CREATOR to make this class Parcelable. + */ + @NonNull + public static final Parcelable.Creator<TextBoundsInfo> CREATOR = new Creator<TextBoundsInfo>() { + @Override + public TextBoundsInfo createFromParcel(Parcel source) { + return new TextBoundsInfo(source); + } + + @Override + public TextBoundsInfo[] newArray(int size) { + return new TextBoundsInfo[size]; + } + }; + + private static final String TEXT_BOUNDS_INFO_KEY = "android.view.inputmethod.TextBoundsInfo"; + + /** + * Store the {@link TextBoundsInfo} into a {@link Bundle}. This method is used by + * {@link RemoteInputConnectionImpl} to transfer the {@link TextBoundsInfo} from the editor + * to IME. + * + * @see TextBoundsInfoResult + * @see InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer) + * @hide + */ + @NonNull + public Bundle toBundle() { + final Bundle bundle = new Bundle(); + bundle.putParcelable(TEXT_BOUNDS_INFO_KEY, this); + return bundle; + + } + + /** @hide */ + @Nullable + public static TextBoundsInfo createFromBundle(@Nullable Bundle bundle) { + if (bundle == null) return null; + return bundle.getParcelable(TEXT_BOUNDS_INFO_KEY, TextBoundsInfo.class); + } + + /** + * The builder class to create a {@link TextBoundsInfo} object. + */ + public static final class Builder { + private final float[] mMatrixValues = new float[9]; + private boolean mMatrixInitialized; + private int mStart; + private int mEnd; + private float[] mCharacterBounds; + private int[] mCharacterFlags; + private int[] mCharacterBidiLevels; + private SegmentFinder mLineSegmentFinder; + private SegmentFinder mWordSegmentFinder; + private SegmentFinder mGraphemeSegmentFinder; + + /** Clear all the parameters set on this {@link Builder} to reuse it. */ + @NonNull + public Builder clear() { + mMatrixInitialized = false; + mStart = -1; + mEnd = -1; + mCharacterBounds = null; + mCharacterFlags = null; + mLineSegmentFinder = null; + mWordSegmentFinder = null; + mGraphemeSegmentFinder = null; + return this; + } + + /** + * Sets the matrix that transforms local coordinates into screen coordinates. + * + * @param matrix transformation matrix from local coordinates into screen coordinates. + * @throws NullPointerException if the given {@code matrix} is {@code null}. + */ + @NonNull + public Builder setMatrix(@NonNull Matrix matrix) { + Objects.requireNonNull(matrix).getValues(mMatrixValues); + mMatrixInitialized = true; + return this; + } + + /** + * Set the start and end index of the {@link TextBoundsInfo}. It's the range of the + * characters whose information is available in the {@link TextBoundsInfo}. + * + * @param start the start index of the {@link TextBoundsInfo}, inclusive. + * @param end the end index of the {@link TextBoundsInfo}, exclusive. + * @throws IllegalArgumentException if the given {@code start} or {@code end} is negative, + * or {@code end} is smaller than the {@code start}. + */ + @NonNull + @SuppressWarnings("MissingGetterMatchingBuilder") + public Builder setStartAndEnd(@IntRange(from = 0) int start, @IntRange(from = 0) int end) { + Preconditions.checkArgument(start >= 0); + Preconditions.checkArgumentInRange(start, 0, end, "start"); + mStart = start; + mEnd = end; + return this; + } + + /** + * Set the characters bounds, in the coordinates of the editor. <br/> + * + * The given array should be divided into groups of four where each element represents + * left, top, right and bottom of the character bounds respectively. + * The bounds of the i-th character in the editor should be stored at index + * 4 * (i - start). The length of the given array must equal to 4 * (end - start). <br/> + * + * Sometimes multiple characters in a single grapheme are rendered as one symbol on the + * screen. So those characters only have one shared bounds. In this case, we recommend the + * editor to assign all the width to the bounds of the first character in the grapheme, + * and make the rest characters' bounds zero-width. <br/> + * + * For example, the string "'0xD83D' '0xDE00'" is rendered as one grapheme - a grinning face + * emoji. If the bounds of the grapheme is: Rect(5, 10, 15, 20), the character bounds of the + * string should be: [ Rect(5, 10, 15, 20), Rect(15, 10, 15, 20) ]. + * + * @param characterBounds the array of the flattened character bounds. + * @throws NullPointerException if the given {@code characterBounds} is {@code null}. + */ + @NonNull + public Builder setCharacterBounds(@NonNull float[] characterBounds) { + mCharacterBounds = Objects.requireNonNull(characterBounds); + return this; + } + + /** + * Set the flags of the characters. The flags of the i-th character in the editor is stored + * at index (i - start). The length of the given array must equal to (end - start). + * The flags contain the following information: + * <ul> + * <li>The {@link #FLAG_CHARACTER_WHITESPACE} flag, indicating the character is a + * whitespace. </li> + * <li>The {@link #FLAG_CHARACTER_LINEFEED} flag, indicating the character is a + * linefeed. </li> + * <li>The {@link #FLAG_CHARACTER_PUNCTUATION} flag, indicating the character is a + * punctuation. </li> + * <li>The {@link #FLAG_LINE_IS_RTL} flag, indicating the line this character belongs to + * is RTL. All all character in the same line must have the same line direction. Check + * {@link #getLineSegmentFinder()} for more information of line boundaries. </li> + * </ul> + * + * @param characterFlags the array of the character's flags. + * @throws NullPointerException if the given {@code characterFlags} is {@code null}. + * @throws IllegalArgumentException if the given {@code characterFlags} contains invalid + * flags. + * + * @see #getCharacterFlags(int) + */ + @NonNull + public Builder setCharacterFlags(@NonNull int[] characterFlags) { + Objects.requireNonNull(characterFlags); + for (int characterFlag : characterFlags) { + if ((characterFlag & (~KNOWN_CHARACTER_FLAGS)) != 0) { + throw new IllegalArgumentException("characterFlags contains invalid flags."); + } + } + mCharacterFlags = characterFlags; + return this; + } + + /** + * Set the BiDi levels for the character. The bidiLevel of the i-th character in the editor + * is stored at index (i - start). The length of the given array must equal to + * (end - start). <br/> + * + * BiDi level is defined by + * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm" >the unicode + * bidirectional algorithm </a>. One can determine whether a character's direction is + * right-to-left (RTL) or left-to-right (LTR) by checking the last bit of the BiDi level. + * If it's 1, the character is RTL, otherwise the character is LTR. The BiDi level of a + * character must be in the range of [0, 125]. + * @param characterBidiLevels the array of the character's BiDi level. + * + * @throws NullPointerException if the given {@code characterBidiLevels} is {@code null}. + * @throws IllegalArgumentException if the given {@code characterBidiLevels} contains an + * element that's out of the range [0, 125]. + * + * @see #getCharacterBidiLevel(int) + */ + @NonNull + public Builder setCharacterBidiLevel(@NonNull int[] characterBidiLevels) { + Objects.requireNonNull(characterBidiLevels); + for (int index = 0; index < characterBidiLevels.length; ++index) { + Preconditions.checkArgumentInRange(characterBidiLevels[index], 0, 125, + "bidiLevels[" + index + "]"); + } + mCharacterBidiLevels = characterBidiLevels; + return this; + } + + /** + * Set the {@link SegmentFinder} that locates the grapheme cluster boundaries. Grapheme is + * defined in <a href="https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries"> + * the unicode annex #29: unicode text segmentation<a/>. It's a user-perspective character. + * And it's usually the minimal unit for selection, backspace, deletion etc. <br/> + * + * Please note that only the grapheme segments within the range from start to end will + * be available to the IME. The remaining information will be discarded during serialization + * for better performance. + * + * @param graphemeSegmentFinder the {@link SegmentFinder} that locates the grapheme cluster + * boundaries. + * @throws NullPointerException if the given {@code graphemeSegmentFinder} is {@code null}. + * + * @see #getGraphemeSegmentFinder() + * @see SegmentFinder + * @see SegmentFinder.DefaultSegmentFinder + */ + @NonNull + public Builder setGraphemeSegmentFinder(@NonNull SegmentFinder graphemeSegmentFinder) { + mGraphemeSegmentFinder = Objects.requireNonNull(graphemeSegmentFinder); + return this; + } + + /** + * Set the {@link SegmentFinder} that locates the word boundaries. <br/> + * + * Please note that only the word segments within the range from start to end will + * be available to the IME. The remaining information will be discarded during serialization + * for better performance. + * @param wordSegmentFinder set the {@link SegmentFinder} that locates the word boundaries. + * @throws NullPointerException if the given {@code wordSegmentFinder} is {@code null}. + * + * @see #getWordSegmentFinder() + * @see SegmentFinder + * @see SegmentFinder.DefaultSegmentFinder + */ + @NonNull + public Builder setWordSegmentFinder(@NonNull SegmentFinder wordSegmentFinder) { + mWordSegmentFinder = Objects.requireNonNull(wordSegmentFinder); + return this; + } + + /** + * Set the {@link SegmentFinder} that locates the line boundaries. Aside from the hard + * breaks in the text, it should also locate the soft line breaks added by the editor. + * It is expected that the characters within the same line is rendered on the same baseline. + * (Except for some text formatted as subscript and superscript.) <br/> + * + * Please note that only the line segments within the range from start to end will + * be available to the IME. The remaining information will be discarded during serialization + * for better performance. + * @param lineSegmentFinder set the {@link SegmentFinder} that locates the line boundaries. + * @throws NullPointerException if the given {@code lineSegmentFinder} is {@code null}. + * + * @see #getLineSegmentFinder() + * @see SegmentFinder + * @see SegmentFinder.DefaultSegmentFinder + */ + @NonNull + public Builder setLineSegmentFinder(@NonNull SegmentFinder lineSegmentFinder) { + mLineSegmentFinder = Objects.requireNonNull(lineSegmentFinder); + return this; + } + + /** + * Create the {@link TextBoundsInfo} using the parameters in this {@link Builder}. + * + * @throws IllegalStateException in the following conditions: + * <ul> + * <li>if the {@code start} or {@code end} is not set.</li> + * <li>if the {@code matrix} is not set.</li> + * <li>if {@code characterBounds} is not set or its length doesn't equal to + * 4 * ({@code end} - {@code start}).</li> + * <li>if the {@code characterFlags} is not set or its length doesn't equal to + * ({@code end} - {@code start}).</li> + * <li>if {@code graphemeSegmentFinder}, {@code wordSegmentFinder} or + * {@code lineSegmentFinder} is not set.</li> + * <li>if characters in the same line has inconsistent {@link #FLAG_LINE_IS_RTL} + * flag.</li> + * </ul> + */ + @NonNull + public TextBoundsInfo build() { + if (mStart < 0 || mEnd < 0) { + throw new IllegalStateException("Start and end must be set."); + } + + if (!mMatrixInitialized) { + throw new IllegalStateException("Matrix must be set."); + } + + if (mCharacterBounds == null) { + throw new IllegalStateException("CharacterBounds must be set."); + } + + if (mCharacterFlags == null) { + throw new IllegalStateException("CharacterFlags must be set."); + } + + if (mCharacterBidiLevels == null) { + throw new IllegalStateException("CharacterBidiLevel must be set."); + } + + if (mCharacterBounds.length != 4 * (mEnd - mStart)) { + throw new IllegalStateException("The length of characterBounds doesn't match the " + + "length of the given start and end." + + " Expected length: " + (4 * (mEnd - mStart)) + + " characterBounds length: " + mCharacterBounds.length); + } + if (mCharacterFlags.length != mEnd - mStart) { + throw new IllegalStateException("The length of characterFlags doesn't match the " + + "length of the given start and end." + + " Expected length: " + (mEnd - mStart) + + " characterFlags length: " + mCharacterFlags.length); + } + if (mCharacterBidiLevels.length != mEnd - mStart) { + throw new IllegalStateException("The length of characterBidiLevels doesn't match" + + " the length of the given start and end." + + " Expected length: " + (mEnd - mStart) + + " characterFlags length: " + mCharacterBidiLevels.length); + } + if (mGraphemeSegmentFinder == null) { + throw new IllegalStateException("GraphemeSegmentFinder must be set."); + } + if (mWordSegmentFinder == null) { + throw new IllegalStateException("WordSegmentFinder must be set."); + } + if (mLineSegmentFinder == null) { + throw new IllegalStateException("LineSegmentFinder must be set."); + } + + if (!isLineDirectionFlagConsistent(mCharacterFlags, mLineSegmentFinder, mStart, mEnd)) { + throw new IllegalStateException("characters in the same line must have the same " + + "FLAG_LINE_IS_RTL flag value."); + } + return new TextBoundsInfo(this); + } + } + + /** + * Encode the segment start and end positions in {@link SegmentFinder} to a flags array. + * + * For example: + * Text: "A BC DE" + * Input: + * start: 2, end: 7 // substring "BC DE" + * SegmentFinder: segment ranges = [(2, 4), (5, 7)] // a word break iterator + * flags: [0x0000, 0x0000, 0x0080, 0x0000, 0x0000, 0x0000] // 0x0080 is whitespace + * segmentStartFlag: 0x0100 + * segmentEndFlag: 0x0200 + * Output: + * flags: [0x0100, 0x0000, 0x0280, 0x0100, 0x0000, 0x0200] + * The index 2 and 5 encode segment starts, the index 4 and 7 encode a segment end. + * + * @param flags the flags array to receive the results. + * @param segmentStartFlag the flag used to encode the segment start. + * @param segmentEndFlag the flag used to encode the segment end. + * @param start the start index of the encoded range, inclusive. + * @param end the end index of the encoded range, inclusive. + * @param segmentFinder the SegmentFinder to be encoded. + * + * @see #decodeSegmentFinder(int[], int, int, int, int) + */ + private static void encodeSegmentFinder(@NonNull int[] flags, int segmentStartFlag, + int segmentEndFlag, int start, int end, @NonNull SegmentFinder segmentFinder) { + if (end - start + 1 != flags.length) { + throw new IllegalStateException("The given flags array must have the same length as" + + " the given range. flags length: " + flags.length + + " range: [" + start + ", " + end + "]"); + } + + int segmentEnd = segmentFinder.nextEndBoundary(start); + if (segmentEnd == SegmentFinder.DONE) return; + int segmentStart = segmentFinder.previousStartBoundary(segmentEnd); + + while (segmentEnd != SegmentFinder.DONE && segmentEnd <= end) { + if (segmentStart >= start) { + flags[segmentStart - start] |= segmentStartFlag; + flags[segmentEnd - start] |= segmentEndFlag; + } + segmentStart = segmentFinder.nextStartBoundary(segmentStart); + segmentEnd = segmentFinder.nextEndBoundary(segmentEnd); + } + } + + /** + * Decode a {@link SegmentFinder} from a flags array. + * + * For example: + * Text: "A BC DE" + * Input: + * start: 2, end: 7 // substring "BC DE" + * flags: [0x0100, 0x0000, 0x0280, 0x0100, 0x0000, 0x0200] + * segmentStartFlag: 0x0100 + * segmentEndFlag: 0x0200 + * Output: + * SegmentFinder: segment ranges = [(2, 4), (5, 7)] + * + * @param flags the flags array to decode the SegmentFinder. + * @param segmentStartFlag the flag to decode a segment start. + * @param segmentEndFlag the flag to decode a segment end. + * @param start the start index of the interested range, inclusive. + * @param end the end index of the interested range, inclusive. + * + * @see #encodeSegmentFinder(int[], int, int, int, int, SegmentFinder) + */ + private static SegmentFinder decodeSegmentFinder(int[] flags, int segmentStartFlag, + int segmentEndFlag, int start, int end) { + if (end - start + 1 != flags.length) { + throw new IllegalStateException("The given flags array must have the same length as" + + " the given range. flags length: " + flags.length + + " range: [" + start + ", " + end + "]"); + } + int[] breaks = ArrayUtils.newUnpaddedIntArray(10); + int count = 0; + for (int offset = 0; offset < flags.length; ++offset) { + if ((flags[offset] & segmentStartFlag) == segmentStartFlag) { + breaks = GrowingArrayUtils.append(breaks, count++, start + offset); + } + if ((flags[offset] & segmentEndFlag) == segmentEndFlag) { + breaks = GrowingArrayUtils.append(breaks, count++, start + offset); + } + } + return new SegmentFinder.DefaultSegmentFinder(Arrays.copyOf(breaks, count)); + } + + /** + * Check whether the {@link #FLAG_LINE_IS_RTL} is the same for characters in the same line. + * @return true if all characters in the same line has the same {@link #FLAG_LINE_IS_RTL} flag. + */ + private static boolean isLineDirectionFlagConsistent(int[] characterFlags, + SegmentFinder lineSegmentFinder, int start, int end) { + int segmentEnd = lineSegmentFinder.nextEndBoundary(start); + if (segmentEnd == SegmentFinder.DONE) return true; + int segmentStart = lineSegmentFinder.previousStartBoundary(segmentEnd); + + while (segmentStart != SegmentFinder.DONE && segmentStart < end) { + final int lineStart = Math.max(segmentStart, start); + final int lineEnd = Math.min(segmentEnd, end); + final boolean lineIsRtl = (characterFlags[lineStart - start] & FLAG_LINE_IS_RTL) != 0; + for (int index = lineStart + 1; index < lineEnd; ++index) { + final int flags = characterFlags[index - start]; + final boolean characterLineIsRtl = (flags & FLAG_LINE_IS_RTL) != 0; + if (characterLineIsRtl != lineIsRtl) { + return false; + } + } + + segmentStart = lineSegmentFinder.nextStartBoundary(segmentStart); + segmentEnd = lineSegmentFinder.nextEndBoundary(segmentEnd); + } + return true; + } +} diff --git a/core/java/android/view/inputmethod/TextBoundsInfoResult.java b/core/java/android/view/inputmethod/TextBoundsInfoResult.java new file mode 100644 index 000000000000..62df17a3aeae --- /dev/null +++ b/core/java/android/view/inputmethod/TextBoundsInfoResult.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.inputmethod; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.RectF; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * The object that holds the result of the + * {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} call. + * + * @see InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer) + */ +public final class TextBoundsInfoResult { + private final int mResultCode; + private final TextBoundsInfo mTextBoundsInfo; + + /** + * Result for {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} when the + * editor doesn't implement the method. + */ + public static final int CODE_UNSUPPORTED = 0; + + /** + * Result for {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} when the + * editor successfully returns a {@link TextBoundsInfo}. + */ + public static final int CODE_SUCCESS = 1; + + /** + * Result for {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} when the + * request failed. This result code is returned when the editor can't provide a valid + * {@link TextBoundsInfo}. (e.g. The editor view is not laid out.) + */ + public static final int CODE_FAILED = 2; + + /** + * Result for {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} when the + * request is cancelled. This happens when the {@link InputConnection} is or becomes + * invalidated while requesting the + * {@link TextBoundsInfo}, for example because a new {@code InputConnection} was started, or + * due to {@link InputMethodManager#invalidateInput}. + */ + public static final int CODE_CANCELLED = 3; + + /** @hide */ + @IntDef(prefix = { "CODE_" }, value = { + CODE_UNSUPPORTED, + CODE_SUCCESS, + CODE_FAILED, + CODE_CANCELLED, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ResultCode {} + + /** + * Create a {@link TextBoundsInfoResult} object with no {@link TextBoundsInfo}. + * The given {@code resultCode} can't be {@link #CODE_SUCCESS}. + * @param resultCode the result code of the + * {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} call. + */ + public TextBoundsInfoResult(@ResultCode int resultCode) { + this(resultCode, null); + } + + /** + * Create a {@link TextBoundsInfoResult} object. + * + * @param resultCode the result code of the + * {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} call. + * @param textBoundsInfo the returned {@link TextBoundsInfo} of the + * {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} call. It can't be + * null if the {@code resultCode} is {@link #CODE_SUCCESS}. + * + * @throws IllegalStateException if the resultCode is + * {@link #CODE_SUCCESS} but the given {@code textBoundsInfo} + * is null. + */ + public TextBoundsInfoResult(@ResultCode int resultCode, + @NonNull TextBoundsInfo textBoundsInfo) { + if (resultCode == CODE_SUCCESS && textBoundsInfo == null) { + throw new IllegalStateException("TextBoundsInfo must be provided when the resultCode " + + "is CODE_SUCCESS."); + } + mResultCode = resultCode; + mTextBoundsInfo = textBoundsInfo; + } + + /** + * Return the result code of the + * {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} call. + * Its value is one of the {@link #CODE_UNSUPPORTED}, {@link #CODE_SUCCESS}, + * {@link #CODE_FAILED} and {@link #CODE_CANCELLED}. + */ + @ResultCode + public int getResultCode() { + return mResultCode; + } + + /** + * Return the {@link TextBoundsInfo} provided by the editor. It is non-null if the + * {@code resultCode} is {@link #CODE_SUCCESS}. + * Otherwise, it can be null in the following conditions: + * <ul> + * <li>the editor doesn't support + * {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)}.</li> + * <li>the editor doesn't have the text bounds information at the moment. (e.g. the editor + * view is not laid out yet.) </li> + * <li> the {@link InputConnection} is or become inactive during the request. </li> + * <ul/> + */ + @Nullable + public TextBoundsInfo getTextBoundsInfo() { + return mTextBoundsInfo; + } +} diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 8815ab35b671..0956a71bd92d 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -141,6 +141,10 @@ public final class TransitionInfo implements Parcelable { /** The first unused bit. This can be used by remotes to attach custom flags to this change. */ public static final int FLAG_FIRST_CUSTOM = 1 << 17; + /** The change belongs to a window that won't contain activities. */ + public static final int FLAGS_IS_NON_APP_WINDOW = + FLAG_IS_WALLPAPER | FLAG_IS_INPUT_METHOD | FLAG_IS_SYSTEM_WINDOW; + /** @hide */ @IntDef(prefix = { "FLAG_" }, value = { FLAG_NONE, @@ -579,11 +583,16 @@ public final class TransitionInfo implements Parcelable { return mFlags; } - /** Whether the given change flags has included in this change. */ + /** Whether this change contains any of the given change flags. */ public boolean hasFlags(@ChangeFlags int flags) { return (mFlags & flags) != 0; } + /** Whether this change contains all of the given change flags. */ + public boolean hasAllFlags(@ChangeFlags int flags) { + return (mFlags & flags) == flags; + } + /** * @return the bounds of the container before the change. It may be empty if the container * is coming into existence. diff --git a/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl b/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl index ea5c9a33b762..81e060d8c648 100644 --- a/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl +++ b/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl @@ -16,20 +16,15 @@ package com.android.internal.inputmethod; +import android.graphics.RectF; import android.os.Bundle; import android.os.ResultReceiver; import android.view.KeyEvent; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CorrectionInfo; -import android.view.inputmethod.DeleteGesture; -import android.view.inputmethod.DeleteRangeGesture; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputContentInfo; -import android.view.inputmethod.InsertGesture; -import android.view.inputmethod.JoinOrSplitGesture; -import android.view.inputmethod.RemoveSpaceGesture; -import android.view.inputmethod.SelectGesture; -import android.view.inputmethod.SelectRangeGesture; +import android.view.inputmethod.ParcelableHandwritingGesture; import android.view.inputmethod.TextAttribute; import com.android.internal.infra.AndroidFuture; @@ -94,26 +89,8 @@ import com.android.internal.inputmethod.InputConnectionCommandHeader; void performPrivateCommand(in InputConnectionCommandHeader header, String action, in Bundle data); - void performHandwritingSelectGesture(in InputConnectionCommandHeader header, - in SelectGesture gesture, in ResultReceiver resultReceiver); - - void performHandwritingSelectRangeGesture(in InputConnectionCommandHeader header, - in SelectRangeGesture gesture, in ResultReceiver resultReceiver); - - void performHandwritingInsertGesture(in InputConnectionCommandHeader header, - in InsertGesture gesture, in ResultReceiver resultReceiver); - - void performHandwritingDeleteGesture(in InputConnectionCommandHeader header, - in DeleteGesture gesture, in ResultReceiver resultReceiver); - - void performHandwritingDeleteRangeGesture(in InputConnectionCommandHeader header, - in DeleteRangeGesture gesture, in ResultReceiver resultReceiver); - - void performHandwritingRemoveSpaceGesture(in InputConnectionCommandHeader header, - in RemoveSpaceGesture gesture, in ResultReceiver resultReceiver); - - void performHandwritingJoinOrSplitGesture(in InputConnectionCommandHeader header, - in JoinOrSplitGesture gesture, in ResultReceiver resultReceiver); + void performHandwritingGesture(in InputConnectionCommandHeader header, + in ParcelableHandwritingGesture gesture, in ResultReceiver resultReceiver); void setComposingRegion(in InputConnectionCommandHeader header, int start, int end); @@ -130,6 +107,9 @@ import com.android.internal.inputmethod.InputConnectionCommandHeader; int cursorUpdateMode, int cursorUpdateFilter, int imeDisplayId, in AndroidFuture future /* T=Boolean */); + void requestTextBoundsInfo(in InputConnectionCommandHeader header, in RectF rect, + in ResultReceiver resultReceiver /* T=TextBoundsInfoResult */); + void commitContent(in InputConnectionCommandHeader header, in InputContentInfo inputContentInfo, int flags, in Bundle opts, in AndroidFuture future /* T=Boolean */); diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 62c584847c0f..953ea898fcf9 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3152,6 +3152,12 @@ <permission android:name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND" android:protectionLevel="signature|privileged|vendorPrivileged|oem|verifier|role"/> + <!-- Allows an application to hint that a broadcast is associated with an + "interactive" usage scenario + @hide --> + <permission android:name="android.permission.BROADCAST_OPTION_INTERACTIVE" + android:protectionLevel="signature|privileged" /> + <!-- @SystemApi Must be required by activities that handle the intent action {@link Intent#ACTION_SEND_SHOW_SUSPENDED_APP_DETAILS}. This is for use by apps that hold {@link Manifest.permission#SUSPEND_APPS} to interact with the system. diff --git a/core/tests/coretests/src/android/app/activity/BroadcastTest.java b/core/tests/coretests/src/android/app/activity/BroadcastTest.java index 0f81896692c0..7e875ada7267 100644 --- a/core/tests/coretests/src/android/app/activity/BroadcastTest.java +++ b/core/tests/coretests/src/android/app/activity/BroadcastTest.java @@ -18,6 +18,8 @@ package android.app.activity; import android.app.Activity; import android.app.ActivityManager; +import android.app.BroadcastOptions; +import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -536,4 +538,40 @@ public class BroadcastTest extends ActivityTestsBase { Log.i("foo", "Unregister exception", e); } } + + public void testBroadcastOption_interactive() throws Exception { + final BroadcastOptions options = BroadcastOptions.makeBasic(); + options.setInteractiveBroadcast(true); + final Intent intent = makeBroadcastIntent(BROADCAST_REGISTERED); + + try { + getContext().sendBroadcast(intent, null, options.toBundle()); + fail("No exception thrown with BroadcastOptions.setInteractiveBroadcast(true)"); + } catch (SecurityException se) { + // Expected, correct behavior - this case intentionally empty + } catch (Exception e) { + fail("Unexpected exception " + e.getMessage() + + " thrown with BroadcastOptions.setInteractiveBroadcast(true)"); + } + } + + public void testBroadcastOption_interactive_PendingIntent() throws Exception { + final BroadcastOptions options = BroadcastOptions.makeBasic(); + options.setInteractiveBroadcast(true); + final Intent intent = makeBroadcastIntent(BROADCAST_REGISTERED); + PendingIntent brPending = PendingIntent.getBroadcast(getContext(), + 1, intent, PendingIntent.FLAG_IMMUTABLE); + + try { + brPending.send(getContext(), 1, null, null, null, null, options.toBundle()); + fail("No exception thrown with BroadcastOptions.setInteractiveBroadcast(true)"); + } catch (SecurityException se) { + // Expected, correct behavior - this case intentionally empty + } catch (Exception e) { + fail("Unexpected exception " + e.getMessage() + + " thrown with BroadcastOptions.setInteractiveBroadcast(true)"); + } finally { + brPending.cancel(); + } + } } diff --git a/core/tests/coretests/src/android/view/inputmethod/ParcelableHandwritingGestureTest.java b/core/tests/coretests/src/android/view/inputmethod/ParcelableHandwritingGestureTest.java new file mode 100644 index 000000000000..79aeaa39bc7c --- /dev/null +++ b/core/tests/coretests/src/android/view/inputmethod/ParcelableHandwritingGestureTest.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.inputmethod; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import android.annotation.NonNull; +import android.graphics.PointF; +import android.graphics.RectF; +import android.os.Parcel; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public class ParcelableHandwritingGestureTest { + + @Test + public void testCreationFailWithNullPointerException() { + assertThrows(NullPointerException.class, () -> ParcelableHandwritingGesture.of(null)); + } + + @Test + public void testInvalidTypeHeader() { + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + // GESTURE_TYPE_NONE is not a supported header. + parcel.writeInt(HandwritingGesture.GESTURE_TYPE_NONE); + final Parcel initializedParcel = parcel; + assertThrows(UnsupportedOperationException.class, + () -> ParcelableHandwritingGesture.CREATOR.createFromParcel(initializedParcel)); + } finally { + if (parcel != null) { + parcel.recycle(); + } + } + } + + @Test + public void testSelectGesture() { + verifyEqualityAfterUnparcel(new SelectGesture.Builder() + .setGranularity(HandwritingGesture.GRANULARITY_WORD) + .setSelectionArea(new RectF(1, 2, 3, 4)) + .setFallbackText("") + .build()); + } + + @Test + public void testSelectRangeGesture() { + verifyEqualityAfterUnparcel(new SelectRangeGesture.Builder() + .setGranularity(HandwritingGesture.GRANULARITY_WORD) + .setSelectionStartArea(new RectF(1, 2, 3, 4)) + .setSelectionEndArea(new RectF(5, 6, 7, 8)) + .setFallbackText("") + .build()); + } + + @Test + public void testInsertGestureGesture() { + verifyEqualityAfterUnparcel(new InsertGesture.Builder() + .setTextToInsert("text") + .setInsertionPoint(new PointF(1, 1)).setFallbackText("") + .build()); + } + + @Test + public void testDeleteGestureGesture() { + verifyEqualityAfterUnparcel(new DeleteGesture.Builder() + .setGranularity(HandwritingGesture.GRANULARITY_WORD) + .setDeletionArea(new RectF(1, 2, 3, 4)) + .setFallbackText("") + .build()); + } + + @Test + public void testDeleteRangeGestureGesture() { + verifyEqualityAfterUnparcel(new DeleteRangeGesture.Builder() + .setGranularity(HandwritingGesture.GRANULARITY_WORD) + .setDeletionStartArea(new RectF(1, 2, 3, 4)) + .setDeletionEndArea(new RectF(5, 6, 7, 8)) + .setFallbackText("") + .build()); + } + + @Test + public void testRemoveSpaceGestureGesture() { + verifyEqualityAfterUnparcel(new RemoveSpaceGesture.Builder() + .setPoints(new PointF(1f, 2f), new PointF(3f, 4f)) + .setFallbackText("") + .build()); + } + + @Test + public void testJoinOrSplitGestureGesture() { + verifyEqualityAfterUnparcel(new JoinOrSplitGesture.Builder() + .setJoinOrSplitPoint(new PointF(1f, 2f)) + .setFallbackText("") + .build()); + } + + static void verifyEqualityAfterUnparcel(@NonNull HandwritingGesture gesture) { + assertEquals(gesture, cloneViaParcel(ParcelableHandwritingGesture.of(gesture)).get()); + } + + private static ParcelableHandwritingGesture cloneViaParcel( + @NonNull ParcelableHandwritingGesture original) { + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + original.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + return ParcelableHandwritingGesture.CREATOR.createFromParcel(parcel); + } finally { + if (parcel != null) { + parcel.recycle(); + } + } + } +} diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java index d4a663221cd7..95aa5d0de119 100644 --- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java +++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java @@ -32,6 +32,7 @@ import android.app.Instrumentation; import android.content.Context; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; +import android.view.HandwritingDelegateConfiguration; import android.view.HandwritingInitiator; import android.view.InputDevice; import android.view.MotionEvent; @@ -208,6 +209,30 @@ public class HandwritingInitiatorTest { } @Test + public void onTouchEvent_startHandwriting_delegate() { + int delegatorViewId = 234; + View delegatorView = new View(mContext); + delegatorView.setId(delegatorViewId); + + mTestView.setHandwritingDelegateConfiguration( + new HandwritingDelegateConfiguration( + delegatorViewId, + () -> mHandwritingInitiator.onInputConnectionCreated(delegatorView))); + + final int x1 = (sHwArea.left + sHwArea.right) / 2; + final int y1 = (sHwArea.top + sHwArea.bottom) / 2; + MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); + mHandwritingInitiator.onTouchEvent(stylusEvent1); + + final int x2 = x1 + mHandwritingSlop * 2; + final int y2 = y1; + MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0); + mHandwritingInitiator.onTouchEvent(stylusEvent2); + + verify(mHandwritingInitiator, times(1)).startHandwriting(delegatorView); + } + + @Test public void onTouchEvent_notStartHandwriting_whenHandwritingNotAvailable() { final Rect rect = new Rect(600, 600, 900, 900); final View testView = createView(rect, true /* autoHandwritingEnabled */, diff --git a/core/tests/coretests/src/com/android/internal/security/OWNERS b/core/tests/coretests/src/com/android/internal/security/OWNERS new file mode 100644 index 000000000000..4f4d8d7a0932 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/security/OWNERS @@ -0,0 +1,3 @@ +# Bug component: 36824 + +per-file VerityUtilsTest.java = file:platform/system/security:/fsverity/OWNERS diff --git a/graphics/java/android/view/PixelCopy.java b/graphics/java/android/view/PixelCopy.java index 0f82c8fe904a..889edb3502c4 100644 --- a/graphics/java/android/view/PixelCopy.java +++ b/graphics/java/android/view/PixelCopy.java @@ -311,11 +311,11 @@ public final class PixelCopy { /** * Contains the result of a PixelCopy request */ - public static final class CopyResult { + public static final class Result { private int mStatus; private Bitmap mBitmap; - private CopyResult(@CopyResultStatus int status, Bitmap bitmap) { + private Result(@CopyResultStatus int status, Bitmap bitmap) { mStatus = status; mBitmap = bitmap; } @@ -335,8 +335,8 @@ public final class PixelCopy { /** * If the PixelCopy {@link Request} was given a destination bitmap with - * {@link Request#setDestinationBitmap(Bitmap)} then the returned bitmap will be the same - * as the one given. If no destination bitmap was provided, then this + * {@link Request.Builder#setDestinationBitmap(Bitmap)} then the returned bitmap will be + * the same as the one given. If no destination bitmap was provided, then this * will contain the automatically allocated Bitmap to hold the result. * * @return the Bitmap the copy request was stored in. @@ -349,66 +349,199 @@ public final class PixelCopy { } /** - * A builder to create the complete PixelCopy request, which is then executed by calling - * {@link #request()} + * Represents a PixelCopy request. + * + * To create a copy request, use either of the PixelCopy.Request.ofWindow or + * PixelCopy.Request.ofSurface factories to create a {@link Request.Builder} for the + * given source content. After setting any optional parameters, such as + * {@link Builder#setSourceRect(Rect)}, build the request with {@link Builder#build()} and + * then execute it with {@link PixelCopy#request(Request)} */ public static final class Request { + private final Surface mSource; + private final Consumer<Result> mListener; + private final Executor mListenerThread; + private final Rect mSourceInsets; + private Rect mSrcRect; + private Bitmap mDest; + private Request(Surface source, Rect sourceInsets, Executor listenerThread, - Consumer<CopyResult> listener) { + Consumer<Result> listener) { this.mSource = source; this.mSourceInsets = sourceInsets; this.mListenerThread = listenerThread; this.mListener = listener; } - private final Surface mSource; - private final Consumer<CopyResult> mListener; - private final Executor mListenerThread; - private final Rect mSourceInsets; - private Rect mSrcRect; - private Bitmap mDest; + /** + * A builder to create the complete PixelCopy request, which is then executed by calling + * {@link #request(Request)} with the built request returned from {@link #build()} + */ + public static final class Builder { + private Request mRequest; + + private Builder(Request request) { + mRequest = request; + } + + private void requireNotBuilt() { + if (mRequest == null) { + throw new IllegalStateException("build() already called on this builder"); + } + } + + /** + * Sets the region of the source to copy from. By default, the entire source is copied + * to the output. If only a subset of the source is necessary to be copied, specifying + * a srcRect will improve performance by reducing + * the amount of data being copied. + * + * @param srcRect The area of the source to read from. Null or empty will be treated to + * mean the entire source + * @return this + */ + public @NonNull Builder setSourceRect(@Nullable Rect srcRect) { + requireNotBuilt(); + mRequest.mSrcRect = srcRect; + return this; + } + + /** + * Specifies the output bitmap in which to store the result. By default, a Bitmap of + * format {@link android.graphics.Bitmap.Config#ARGB_8888} with a width & height + * matching that of the {@link #setSourceRect(Rect) source area} will be created to + * place the result. + * + * @param destination The bitmap to store the result, or null to have a bitmap + * automatically created of the appropriate size. If not null, must + * not be {@link Bitmap#isRecycled() recycled} and must be + * {@link Bitmap#isMutable() mutable}. + * @return this + */ + public @NonNull Builder setDestinationBitmap(@Nullable Bitmap destination) { + requireNotBuilt(); + if (destination != null) { + validateBitmapDest(destination); + } + mRequest.mDest = destination; + return this; + } + + /** + * @return The built {@link PixelCopy.Request} + */ + public @NonNull Request build() { + requireNotBuilt(); + Request ret = mRequest; + mRequest = null; + return ret; + } + } /** - * Sets the region of the source to copy from. By default, the entire source is copied to - * the output. If only a subset of the source is necessary to be copied, specifying a - * srcRect will improve performance by reducing - * the amount of data being copied. + * Creates a PixelCopy request for the given {@link Window} + * @param source The Window to copy from + * @param callbackExecutor The executor to run the callback on + * @param listener The callback for when the copy request is completed + * @return A {@link Builder} builder to set the optional params & execute the request + */ + public static @NonNull Builder ofWindow(@NonNull Window source, + @NonNull Executor callbackExecutor, + @NonNull Consumer<Result> listener) { + final Rect insets = new Rect(); + final Surface surface = sourceForWindow(source, insets); + return new Builder(new Request(surface, insets, callbackExecutor, listener)); + } + + /** + * Creates a PixelCopy request for the {@link Window} that the given {@link View} is + * attached to. + * + * Note that this copy request is not cropped to the area the View occupies by default. If + * that behavior is desired, use {@link View#getLocationInWindow(int[])} combined with + * {@link Builder#setSourceRect(Rect)} to set a crop area to restrict the copy operation. * - * @param srcRect The area of the source to read from. Null or empty will be treated to - * mean the entire source - * @return this + * @param source A View that {@link View#isAttachedToWindow() is attached} to a window that + * will be used to retrieve the window to copy from. + * @param callbackExecutor The executor to run the callback on + * @param listener The callback for when the copy request is completed + * @return A {@link Builder} builder to set the optional params & execute the request */ - public @NonNull Request setSourceRect(@Nullable Rect srcRect) { - this.mSrcRect = srcRect; - return this; + public static @NonNull Builder ofWindow(@NonNull View source, + @NonNull Executor callbackExecutor, + @NonNull Consumer<Result> listener) { + if (source == null || !source.isAttachedToWindow()) { + throw new IllegalArgumentException( + "View must not be null & must be attached to window"); + } + final Rect insets = new Rect(); + Surface surface = null; + final ViewRootImpl root = source.getViewRootImpl(); + if (root != null) { + surface = root.mSurface; + insets.set(root.mWindowAttributes.surfaceInsets); + } + if (surface == null || !surface.isValid()) { + throw new IllegalArgumentException( + "Window doesn't have a backing surface!"); + } + return new Builder(new Request(surface, insets, callbackExecutor, listener)); } /** - * Specifies the output bitmap in which to store the result. By default, a Bitmap of format - * {@link android.graphics.Bitmap.Config#ARGB_8888} with a width & height matching that - * of the {@link #setSourceRect(Rect) source area} will be created to place the result. + * Creates a PixelCopy request for the given {@link Surface} * - * @param destination The bitmap to store the result, or null to have a bitmap - * automatically created of the appropriate size. If not null, must not - * be {@link Bitmap#isRecycled() recycled} and must be - * {@link Bitmap#isMutable() mutable}. - * @return this + * @param source The Surface to copy from. Must be {@link Surface#isValid() valid}. + * @param callbackExecutor The executor to run the callback on + * @param listener The callback for when the copy request is completed + * @return A {@link Builder} builder to set the optional params & execute the request */ - public @NonNull Request setDestinationBitmap(@Nullable Bitmap destination) { - if (destination != null) { - validateBitmapDest(destination); + public static @NonNull Builder ofSurface(@NonNull Surface source, + @NonNull Executor callbackExecutor, + @NonNull Consumer<Result> listener) { + if (source == null || !source.isValid()) { + throw new IllegalArgumentException("Source must not be null & must be valid"); } - this.mDest = destination; - return this; + return new Builder(new Request(source, null, callbackExecutor, listener)); + } + + /** + * Creates a PixelCopy request for the {@link Surface} belonging to the + * given {@link SurfaceView} + * + * @param source The SurfaceView to copy from. The backing surface must be + * {@link Surface#isValid() valid} + * @param callbackExecutor The executor to run the callback on + * @param listener The callback for when the copy request is completed + * @return A {@link Builder} builder to set the optional params & execute the request + */ + public static @NonNull Builder ofSurface(@NonNull SurfaceView source, + @NonNull Executor callbackExecutor, + @NonNull Consumer<Result> listener) { + return ofSurface(source.getHolder().getSurface(), callbackExecutor, listener); + } + + /** + * @return The destination bitmap as set by {@link Builder#setDestinationBitmap(Bitmap)} + */ + public @Nullable Bitmap getDestinationBitmap() { + return mDest; } /** - * Executes the request. + * @return The source rect to copy from as set by {@link Builder#setSourceRect(Rect)} + */ + public @Nullable Rect getSourceRect() { + return mSrcRect; + } + + /** + * @hide */ public void request() { if (!mSource.isValid()) { mListenerThread.execute(() -> mListener.accept( - new CopyResult(ERROR_SOURCE_INVALID, null))); + new Result(ERROR_SOURCE_INVALID, null))); return; } HardwareRenderer.copySurfaceInto(mSource, new HardwareRenderer.CopyRequest( @@ -416,93 +549,18 @@ public final class PixelCopy { @Override public void onCopyFinished(int result) { mListenerThread.execute(() -> mListener.accept( - new CopyResult(result, mDestinationBitmap))); + new Result(result, mDestinationBitmap))); } }); } } /** - * Creates a PixelCopy request for the given {@link Window} - * @param source The Window to copy from - * @param callbackExecutor The executor to run the callback on - * @param listener The callback for when the copy request is completed - * @return A {@link Request} builder to set the optional params & execute the request - */ - public static @NonNull Request ofWindow(@NonNull Window source, - @NonNull Executor callbackExecutor, - @NonNull Consumer<CopyResult> listener) { - final Rect insets = new Rect(); - final Surface surface = sourceForWindow(source, insets); - return new Request(surface, insets, callbackExecutor, listener); - } - - /** - * Creates a PixelCopy request for the {@link Window} that the given {@link View} is - * attached to. - * - * Note that this copy request is not cropped to the area the View occupies by default. If that - * behavior is desired, use {@link View#getLocationInWindow(int[])} combined with - * {@link Request#setSourceRect(Rect)} to set a crop area to restrict the copy operation. - * - * @param source A View that {@link View#isAttachedToWindow() is attached} to a window that - * will be used to retrieve the window to copy from. - * @param callbackExecutor The executor to run the callback on - * @param listener The callback for when the copy request is completed - * @return A {@link Request} builder to set the optional params & execute the request - */ - public static @NonNull Request ofWindow(@NonNull View source, - @NonNull Executor callbackExecutor, - @NonNull Consumer<CopyResult> listener) { - if (source == null || !source.isAttachedToWindow()) { - throw new IllegalArgumentException( - "View must not be null & must be attached to window"); - } - final Rect insets = new Rect(); - Surface surface = null; - final ViewRootImpl root = source.getViewRootImpl(); - if (root != null) { - surface = root.mSurface; - insets.set(root.mWindowAttributes.surfaceInsets); - } - if (surface == null || !surface.isValid()) { - throw new IllegalArgumentException( - "Window doesn't have a backing surface!"); - } - return new Request(surface, insets, callbackExecutor, listener); - } - - /** - * Creates a PixelCopy request for the given {@link Surface} - * - * @param source The Surface to copy from. Must be {@link Surface#isValid() valid}. - * @param callbackExecutor The executor to run the callback on - * @param listener The callback for when the copy request is completed - * @return A {@link Request} builder to set the optional params & execute the request - */ - public static @NonNull Request ofSurface(@NonNull Surface source, - @NonNull Executor callbackExecutor, - @NonNull Consumer<CopyResult> listener) { - if (source == null || !source.isValid()) { - throw new IllegalArgumentException("Source must not be null & must be valid"); - } - return new Request(source, null, callbackExecutor, listener); - } - - /** - * Creates a PixelCopy request for the {@link Surface} belonging to the - * given {@link SurfaceView} - * - * @param source The SurfaceView to copy from. The backing surface must be - * {@link Surface#isValid() valid} - * @param callbackExecutor The executor to run the callback on - * @param listener The callback for when the copy request is completed - * @return A {@link Request} builder to set the optional params & execute the request + * Executes the pixel copy request + * @param request The request to execute */ - public static @NonNull Request ofSurface(@NonNull SurfaceView source, - @NonNull Executor callbackExecutor, - @NonNull Consumer<CopyResult> listener) { - return ofSurface(source.getHolder().getSurface(), callbackExecutor, listener); + public static void request(@NonNull Request request) { + request.request(); } private PixelCopy() {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index c2ad1a98d167..5b7d141591ea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -69,6 +69,7 @@ import com.android.wm.shell.common.InteractionJankMonitorUtils; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; import java.io.PrintWriter; +import java.util.function.Consumer; /** * Records and handles layout of splits. Helps to calculate proper bounds when configuration or @@ -599,7 +600,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange /** Swich both surface position with animation. */ public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1, - SurfaceControl leash2, Runnable finishCallback) { + SurfaceControl leash2, Consumer<Rect> finishCallback) { final boolean isLandscape = isLandscape(); final Rect insets = getDisplayInsets(mContext); insets.set(isLandscape ? insets.left : 0, isLandscape ? 0 : insets.top, @@ -617,18 +618,13 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange distBounds1.offset(-mRootBounds.left, -mRootBounds.top); distBounds2.offset(-mRootBounds.left, -mRootBounds.top); distDividerBounds.offset(-mRootBounds.left, -mRootBounds.top); - // DO NOT move to insets area for smooth animation. - distBounds1.set(distBounds1.left, distBounds1.top, - distBounds1.right - insets.right, distBounds1.bottom - insets.bottom); - distBounds2.set(distBounds2.left + insets.left, distBounds2.top + insets.top, - distBounds2.right, distBounds2.bottom); ValueAnimator animator1 = moveSurface(t, leash1, getRefBounds1(), distBounds1, - false /* alignStart */); + -insets.left, -insets.top); ValueAnimator animator2 = moveSurface(t, leash2, getRefBounds2(), distBounds2, - true /* alignStart */); + insets.left, insets.top); ValueAnimator animator3 = moveSurface(t, getDividerLeash(), getRefDividerBounds(), - distDividerBounds, true /* alignStart */); + distDividerBounds, 0 /* offsetX */, 0 /* offsetY */); AnimatorSet set = new AnimatorSet(); set.playTogether(animator1, animator2, animator3); @@ -638,14 +634,14 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public void onAnimationEnd(Animator animation) { mDividePosition = dividerPos; updateBounds(mDividePosition); - finishCallback.run(); + finishCallback.accept(insets); } }); set.start(); } private ValueAnimator moveSurface(SurfaceControl.Transaction t, SurfaceControl leash, - Rect start, Rect end, boolean alignStart) { + Rect start, Rect end, float offsetX, float offsetY) { Rect tempStart = new Rect(start); Rect tempEnd = new Rect(end); final float diffX = tempEnd.left - tempStart.left; @@ -661,15 +657,15 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange final float distY = tempStart.top + scale * diffY; final int width = (int) (tempStart.width() + scale * diffWidth); final int height = (int) (tempStart.height() + scale * diffHeight); - if (alignStart) { + if (offsetX == 0 && offsetY == 0) { t.setPosition(leash, distX, distY); t.setWindowCrop(leash, width, height); } else { - final int offsetX = width - tempStart.width(); - final int offsetY = height - tempStart.height(); - t.setPosition(leash, distX + offsetX, distY + offsetY); + final int diffOffsetX = (int) (scale * offsetX); + final int diffOffsetY = (int) (scale * offsetY); + t.setPosition(leash, distX + diffOffsetX, distY + diffOffsetY); mTempRect.set(0, 0, width, height); - mTempRect.offsetTo(-offsetX, -offsetY); + mTempRect.offsetTo(-diffOffsetX, -diffOffsetY); t.setCrop(leash, mTempRect); } t.apply(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 2b36b4c0307d..85bad174194c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -335,6 +335,7 @@ public class PipTransition extends PipTransitionController { final ActivityManager.RunningTaskInfo taskInfo = mPipOrganizer.getTaskInfo(); if (taskInfo != null) { startExpandAnimation(taskInfo, mPipOrganizer.getSurfaceControl(), + mPipBoundsState.getBounds(), mPipBoundsState.getBounds(), new Rect(mExitDestinationBounds), Surface.ROTATION_0); } mExitDestinationBounds.setEmpty(); @@ -475,6 +476,20 @@ public class PipTransition extends PipTransitionController { taskInfo); return; } + + // When exiting PiP, the PiP leash may be an Activity of a multi-windowing Task, for which + // case it may not be in the screen coordinate. + // Reparent the pip leash to the root with max layer so that we can animate it outside of + // parent crop, and make sure it is not covered by other windows. + final SurfaceControl pipLeash = pipChange.getLeash(); + startTransaction.reparent(pipLeash, info.getRootLeash()); + startTransaction.setLayer(pipLeash, Integer.MAX_VALUE); + // Note: because of this, the bounds to animate should be translated to the root coordinate. + final Point offset = info.getRootOffset(); + final Rect currentBounds = mPipBoundsState.getBounds(); + currentBounds.offset(-offset.x, -offset.y); + startTransaction.setPosition(pipLeash, currentBounds.left, currentBounds.top); + mFinishCallback = (wct, wctCB) -> { mPipOrganizer.onExitPipFinished(taskInfo); finishCallback.onTransitionFinished(wct, wctCB); @@ -496,18 +511,17 @@ public class PipTransition extends PipTransitionController { if (displayRotationChange != null) { // Exiting PIP to fullscreen with orientation change. startExpandAndRotationAnimation(info, startTransaction, finishTransaction, - displayRotationChange, taskInfo, pipChange); + displayRotationChange, taskInfo, pipChange, offset); return; } } // Set the initial frame as scaling the end to the start. final Rect destinationBounds = new Rect(pipChange.getEndAbsBounds()); - final Point offset = pipChange.getEndRelOffset(); destinationBounds.offset(-offset.x, -offset.y); - startTransaction.setWindowCrop(pipChange.getLeash(), destinationBounds); - mSurfaceTransactionHelper.scale(startTransaction, pipChange.getLeash(), - destinationBounds, mPipBoundsState.getBounds()); + startTransaction.setWindowCrop(pipLeash, destinationBounds); + mSurfaceTransactionHelper.scale(startTransaction, pipLeash, destinationBounds, + currentBounds); startTransaction.apply(); // Check if it is fixed rotation. @@ -532,19 +546,21 @@ public class PipTransition extends PipTransitionController { y = destinationBounds.bottom; } mSurfaceTransactionHelper.rotateAndScaleWithCrop(finishTransaction, - pipChange.getLeash(), endBounds, endBounds, new Rect(), degree, x, y, + pipLeash, endBounds, endBounds, new Rect(), degree, x, y, true /* isExpanding */, rotationDelta == ROTATION_270 /* clockwise */); } else { rotationDelta = Surface.ROTATION_0; } - startExpandAnimation(taskInfo, pipChange.getLeash(), destinationBounds, rotationDelta); + startExpandAnimation(taskInfo, pipLeash, currentBounds, currentBounds, destinationBounds, + rotationDelta); } private void startExpandAndRotationAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull TransitionInfo.Change displayRotationChange, - @NonNull TaskInfo taskInfo, @NonNull TransitionInfo.Change pipChange) { + @NonNull TaskInfo taskInfo, @NonNull TransitionInfo.Change pipChange, + @NonNull Point offset) { final int rotateDelta = deltaRotation(displayRotationChange.getStartRotation(), displayRotationChange.getEndRotation()); @@ -556,7 +572,6 @@ public class PipTransition extends PipTransitionController { final Rect startBounds = new Rect(pipChange.getStartAbsBounds()); rotateBounds(startBounds, displayRotationChange.getStartAbsBounds(), rotateDelta); final Rect endBounds = new Rect(pipChange.getEndAbsBounds()); - final Point offset = pipChange.getEndRelOffset(); startBounds.offset(-offset.x, -offset.y); endBounds.offset(-offset.x, -offset.y); @@ -592,11 +607,12 @@ public class PipTransition extends PipTransitionController { } private void startExpandAnimation(final TaskInfo taskInfo, final SurfaceControl leash, - final Rect destinationBounds, final int rotationDelta) { + final Rect baseBounds, final Rect startBounds, final Rect endBounds, + final int rotationDelta) { final PipAnimationController.PipTransitionAnimator animator = - mPipAnimationController.getAnimator(taskInfo, leash, mPipBoundsState.getBounds(), - mPipBoundsState.getBounds(), destinationBounds, null, - TRANSITION_DIRECTION_LEAVE_PIP, 0 /* startingAngle */, rotationDelta); + mPipAnimationController.getAnimator(taskInfo, leash, baseBounds, startBounds, + endBounds, null /* sourceHintRect */, TRANSITION_DIRECTION_LEAVE_PIP, + 0 /* startingAngle */, rotationDelta); animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(mEnterExitAnimationDuration) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 943419bb8ea2..15a11334307b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -115,6 +115,7 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; @@ -846,15 +847,44 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, void setSideStagePositionAnimated(@SplitPosition int sideStagePosition) { if (mSideStagePosition == sideStagePosition) return; SurfaceControl.Transaction t = mTransactionPool.acquire(); + mTempRect1.setEmpty(); final StageTaskListener topLeftStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; + final SurfaceControl topLeftScreenshot = ScreenshotUtils.takeScreenshot(t, + topLeftStage.mRootLeash, mTempRect1, Integer.MAX_VALUE - 1); final StageTaskListener bottomRightStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; + final SurfaceControl bottomRightScreenshot = ScreenshotUtils.takeScreenshot(t, + bottomRightStage.mRootLeash, mTempRect1, Integer.MAX_VALUE - 1); mSplitLayout.splitSwitching(t, topLeftStage.mRootLeash, bottomRightStage.mRootLeash, - () -> { - setSideStagePosition(SplitLayout.reversePosition(mSideStagePosition), - null /* wct */); - mTransactionPool.release(t); + insets -> { + WindowContainerTransaction wct = new WindowContainerTransaction(); + setSideStagePosition(SplitLayout.reversePosition(mSideStagePosition), wct); + mSyncQueue.queue(wct); + mSyncQueue.runInSync(st -> { + updateSurfaceBounds(mSplitLayout, st, false /* applyResizingOffset */); + st.setPosition(topLeftScreenshot, -insets.left, -insets.top); + st.setPosition(bottomRightScreenshot, insets.left, insets.top); + + final ValueAnimator va = ValueAnimator.ofFloat(1, 0); + va.addUpdateListener(valueAnimator-> { + final float progress = (float) valueAnimator.getAnimatedValue(); + t.setAlpha(topLeftScreenshot, progress); + t.setAlpha(bottomRightScreenshot, progress); + t.apply(); + }); + va.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd( + @androidx.annotation.NonNull Animator animation) { + t.remove(topLeftScreenshot); + t.remove(bottomRightScreenshot); + t.apply(); + mTransactionPool.release(t); + } + }); + va.start(); + }); }); } 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 af79386caf9c..928e71f8d3a6 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 @@ -46,6 +46,7 @@ import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL; import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; import static android.window.TransitionInfo.FLAG_FILLS_TASK; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; +import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; @@ -319,6 +320,17 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final int wallpaperTransit = getWallpaperTransitType(info); for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); + if (change.hasAllFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY + | FLAG_IS_BEHIND_STARTING_WINDOW)) { + // Don't animate embedded activity if it is covered by the starting window. + // Non-embedded case still needs animation because the container can still animate + // the starting window together, e.g. CLOSE or CHANGE type. + continue; + } + if (change.hasFlags(TransitionInfo.FLAGS_IS_NON_APP_WINDOW)) { + // Wallpaper, IME, and system windows don't need any default animations. + continue; + } final boolean isTask = change.getTaskInfo() != null; boolean isSeamlessDisplayChange = false; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 63d31cde4715..89205a6dfb01 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -341,6 +341,15 @@ public class Transitions implements RemoteCallable<Transitions> { final SurfaceControl leash = change.getLeash(); final int mode = info.getChanges().get(i).getMode(); + if (mode == TRANSIT_TO_FRONT + && ((change.getStartAbsBounds().height() != change.getEndAbsBounds().height() + || change.getStartAbsBounds().width() != change.getEndAbsBounds().width()))) { + // When the window is moved to front with a different size, make sure the crop is + // updated to prevent it from using the old crop. + t.setWindowCrop(leash, change.getEndAbsBounds().width(), + change.getEndAbsBounds().height()); + } + // Don't move anything that isn't independent within its parents if (!TransitionInfo.isIndependent(change, info)) { if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT || mode == TRANSIT_CHANGE) { diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml index 2d6e8f54b6b6..d8ea0a52e651 100644 --- a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml +++ b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml @@ -13,6 +13,8 @@ <option name="run-command" value="cmd window tracing level all" /> <!-- set WM tracing to frame (avoid incomplete states) --> <option name="run-command" value="cmd window tracing frame" /> + <!-- set Layer tracing buffer size to 50mb --> + <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 51200" /> <!-- ensure lock screen mode is swipe --> <option name="run-command" value="locksettings set-disabled false" /> <!-- restart launcher to activate TAPL --> 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 73159c981b82..ad7a531b589d 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 @@ -145,15 +145,19 @@ class SwitchAppByDoubleTapDivider(testSpec: FlickerTestParameter) : SplitScreenB // robust enough to get the correct end state. } + @Presubmit @Test fun splitScreenDividerKeepVisible() = testSpec.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) + @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp) + @Presubmit @Test fun secondaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(secondaryApp) + @Presubmit @Test fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( primaryApp, @@ -161,6 +165,7 @@ class SwitchAppByDoubleTapDivider(testSpec: FlickerTestParameter) : SplitScreenB portraitPosTop = true ) + @Presubmit @Test fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( secondaryApp, @@ -168,9 +173,11 @@ class SwitchAppByDoubleTapDivider(testSpec: FlickerTestParameter) : SplitScreenB portraitPosTop = false ) + @Presubmit @Test fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp) + @Presubmit @Test fun secondaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(secondaryApp) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java index 55883ab2ef70..d01f3d310fc3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -39,6 +39,7 @@ import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; +import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -110,6 +111,7 @@ public class SplitScreenControllerTests extends ShellTestCase { } @Test + @UiThreadTest public void instantiateController_registerDumpCallback() { doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor(); when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout()); @@ -118,6 +120,7 @@ public class SplitScreenControllerTests extends ShellTestCase { } @Test + @UiThreadTest public void instantiateController_registerCommandCallback() { doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor(); when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout()); @@ -126,6 +129,7 @@ public class SplitScreenControllerTests extends ShellTestCase { } @Test + @UiThreadTest public void testControllerRegistersKeyguardChangeListener() { doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor(); when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout()); @@ -134,6 +138,7 @@ public class SplitScreenControllerTests extends ShellTestCase { } @Test + @UiThreadTest public void instantiateController_addExternalInterface() { doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor(); when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout()); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index 835087007b30..3569860b6128 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -50,6 +50,7 @@ import android.view.SurfaceSession; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -113,6 +114,7 @@ public class StageCoordinatorTests extends ShellTestCase { private StageCoordinator mStageCoordinator; @Before + @UiThreadTest public void setup() { MockitoAnnotations.initMocks(this); mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, diff --git a/libs/hwui/jni/android_graphics_Canvas.cpp b/libs/hwui/jni/android_graphics_Canvas.cpp index 0513447ed05e..35258a3e0774 100644 --- a/libs/hwui/jni/android_graphics_Canvas.cpp +++ b/libs/hwui/jni/android_graphics_Canvas.cpp @@ -20,6 +20,7 @@ #include <android/api-level.h> #else #define __ANDROID_API_P__ 28 +#define __ANDROID_API_U__ 34 #endif #include <androidfw/ResourceTypes.h> #include <hwui/Canvas.h> @@ -30,8 +31,9 @@ #include <nativehelper/ScopedPrimitiveArray.h> #include <nativehelper/ScopedStringChars.h> -#include "FontUtils.h" #include "Bitmap.h" +#include "FontUtils.h" +#include "SkAndroidFrameworkUtils.h" #include "SkBitmap.h" #include "SkBlendMode.h" #include "SkClipOp.h" @@ -42,10 +44,10 @@ #include "SkMatrix.h" #include "SkPath.h" #include "SkPoint.h" +#include "SkRRect.h" #include "SkRect.h" #include "SkRefCnt.h" #include "SkRegion.h" -#include "SkRRect.h" #include "SkScalar.h" #include "SkVertices.h" @@ -710,6 +712,9 @@ static void freeTextLayoutCaches(JNIEnv* env, jobject) { static void setCompatibilityVersion(JNIEnv* env, jobject, jint apiLevel) { Canvas::setCompatibilityVersion(apiLevel); + if (apiLevel < __ANDROID_API_U__) { + SkAndroidFrameworkUtils::UseLegacyLocalMatrixConcatenation(); + } } static void punchHole(JNIEnv* env, jobject, jlong canvasPtr, jfloat left, jfloat top, jfloat right, @@ -800,7 +805,6 @@ int register_android_graphics_Canvas(JNIEnv* env) { ret |= RegisterMethodsOrDie(env, "android/graphics/BaseCanvas", gDrawMethods, NELEM(gDrawMethods)); ret |= RegisterMethodsOrDie(env, "android/graphics/BaseRecordingCanvas", gDrawMethods, NELEM(gDrawMethods)); return ret; - } }; // namespace android diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index 56fb1a91aa90..0988cba2f424 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -36,7 +36,7 @@ import com.android.credentialmanager.createflow.CreateScreenState import com.android.credentialmanager.createflow.RequestDisplayInfo import com.android.credentialmanager.getflow.GetCredentialUiState import com.android.credentialmanager.getflow.GetScreenState -import com.android.credentialmanager.jetpack.CredentialEntryUi.Companion.TYPE_PUBLIC_KEY_CREDENTIAL +import com.android.credentialmanager.jetpack.provider.CredentialEntryUi.Companion.TYPE_PUBLIC_KEY_CREDENTIAL // Consider repo per screen, similar to view model? class CredentialManagerRepo( diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt index 8e30208e75d9..aeea46a85caf 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt @@ -39,8 +39,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap import com.android.credentialmanager.R -import com.android.credentialmanager.jetpack.CredentialEntryUi.Companion.TYPE_PASSWORD_CREDENTIAL -import com.android.credentialmanager.jetpack.CredentialEntryUi.Companion.TYPE_PUBLIC_KEY_CREDENTIAL +import com.android.credentialmanager.jetpack.provider.CredentialEntryUi.Companion.TYPE_PASSWORD_CREDENTIAL +import com.android.credentialmanager.jetpack.provider.CredentialEntryUi.Companion.TYPE_PUBLIC_KEY_CREDENTIAL import com.android.credentialmanager.ui.theme.Grey100 import com.android.credentialmanager.ui.theme.Shapes import com.android.credentialmanager.ui.theme.Typography diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCredentialRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCredentialRequest.kt new file mode 100644 index 000000000000..7e7dbde8655a --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCredentialRequest.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack.developer + +import android.credentials.Credential +import android.os.Bundle + +/** + * Base request class for registering a credential. + * + * @property type the credential type + * @property data the request data in the [Bundle] format + * @property requireSystemProvider true if must only be fulfilled by a system provider and false + * otherwise + */ +open class CreateCredentialRequest( + val type: String, + val data: Bundle, + val requireSystemProvider: Boolean, +) { + companion object { + @JvmStatic + fun createFrom(from: android.credentials.CreateCredentialRequest): CreateCredentialRequest { + return try { + when (from.type) { + Credential.TYPE_PASSWORD_CREDENTIAL -> + CreatePasswordRequest.createFrom(from.data) + PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL -> + CreatePublicKeyCredentialBaseRequest.createFrom(from.data) + else -> + CreateCredentialRequest(from.type, from.data, from.requireSystemProvider()) + } + } catch (e: FrameworkClassParsingException) { + CreateCredentialRequest(from.type, from.data, from.requireSystemProvider()) + } + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePasswordRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePasswordRequest.kt new file mode 100644 index 000000000000..f0da9f9d1866 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePasswordRequest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack.developer + +import android.credentials.Credential +import android.os.Bundle + +/** + * A request to save the user password credential with their password provider. + * + * @property id the user id associated with the password + * @property password the password + * @throws NullPointerException If [id] is null + * @throws NullPointerException If [password] is null + * @throws IllegalArgumentException If [password] is empty + */ +class CreatePasswordRequest constructor( + val id: String, + val password: String, +) : CreateCredentialRequest( + Credential.TYPE_PASSWORD_CREDENTIAL, + toBundle(id, password), + false, +) { + + init { + require(password.isNotEmpty()) { "password should not be empty" } + } + + companion object { + const val BUNDLE_KEY_ID = "androidx.credentials.BUNDLE_KEY_ID" + const val BUNDLE_KEY_PASSWORD = "androidx.credentials.BUNDLE_KEY_PASSWORD" + + @JvmStatic + internal fun toBundle(id: String, password: String): Bundle { + val bundle = Bundle() + bundle.putString(BUNDLE_KEY_ID, id) + bundle.putString(BUNDLE_KEY_PASSWORD, password) + return bundle + } + + @JvmStatic + fun createFrom(data: Bundle): CreatePasswordRequest { + try { + val id = data.getString(BUNDLE_KEY_ID) + val password = data.getString(BUNDLE_KEY_PASSWORD) + return CreatePasswordRequest(id!!, password!!) + } catch (e: Exception) { + throw FrameworkClassParsingException() + } + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialBaseRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialBaseRequest.kt new file mode 100644 index 000000000000..26d61f9eb7a9 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialBaseRequest.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack.developer + +import android.os.Bundle + +/** + * Base request class for registering a public key credential. + * + * @property requestJson The request in JSON format + * @throws NullPointerException If [requestJson] is null. This is handled by the Kotlin runtime + * @throws IllegalArgumentException If [requestJson] is empty + * + * @hide + */ +abstract class CreatePublicKeyCredentialBaseRequest constructor( + val requestJson: String, + type: String, + data: Bundle, + requireSystemProvider: Boolean, +) : CreateCredentialRequest(type, data, requireSystemProvider) { + + init { + require(requestJson.isNotEmpty()) { "request json must not be empty" } + } + + companion object { + const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON" + const val BUNDLE_KEY_SUBTYPE = "androidx.credentials.BUNDLE_KEY_SUBTYPE" + + @JvmStatic + fun createFrom(data: Bundle): CreatePublicKeyCredentialBaseRequest { + return when (data.getString(BUNDLE_KEY_SUBTYPE)) { + CreatePublicKeyCredentialRequest + .BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST -> + CreatePublicKeyCredentialRequestPrivileged.createFrom(data) + CreatePublicKeyCredentialRequestPrivileged + .BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIVILEGED -> + CreatePublicKeyCredentialRequestPrivileged.createFrom(data) + else -> throw FrameworkClassParsingException() + } + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequest.kt new file mode 100644 index 000000000000..2eda90b827dc --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequest.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack.developer + +import android.os.Bundle + +/** + * A request to register a passkey from the user's public key credential provider. + * + * @property requestJson the request in JSON format + * @property allowHybrid defines whether hybrid credentials are allowed to fulfill this request, + * true by default + * @throws NullPointerException If [requestJson] or [allowHybrid] is null. This is handled by the + * Kotlin runtime + * @throws IllegalArgumentException If [requestJson] is empty + * + * @hide + */ +class CreatePublicKeyCredentialRequest @JvmOverloads constructor( + requestJson: String, + @get:JvmName("allowHybrid") + val allowHybrid: Boolean = true +) : CreatePublicKeyCredentialBaseRequest( + requestJson, + PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL, + toBundle(requestJson, allowHybrid), + false, +) { + companion object { + const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID" + const val BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST = + "androidx.credentials.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST" + + @JvmStatic + internal fun toBundle(requestJson: String, allowHybrid: Boolean): Bundle { + val bundle = Bundle() + bundle.putString(BUNDLE_KEY_SUBTYPE, + BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST) + bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson) + bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid) + return bundle + } + + @JvmStatic + fun createFrom(data: Bundle): CreatePublicKeyCredentialRequest { + try { + val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON) + val allowHybrid = data.get(BUNDLE_KEY_ALLOW_HYBRID) + return CreatePublicKeyCredentialRequest(requestJson!!, (allowHybrid!!) as Boolean) + } catch (e: Exception) { + throw FrameworkClassParsingException() + } + } + } +}
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequestPrivileged.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequestPrivileged.kt new file mode 100644 index 000000000000..36324f83a7e5 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequestPrivileged.kt @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack.developer + +import android.os.Bundle + +/** + * A privileged request to register a passkey from the user’s public key credential provider, where + * the caller can modify the rp. Only callers with privileged permission, e.g. user’s default + * brower, caBLE, can use this. + * + * @property requestJson the privileged request in JSON format + * @property allowHybrid defines whether hybrid credentials are allowed to fulfill this request, + * true by default + * @property rp the expected true RP ID which will override the one in the [requestJson] + * @property clientDataHash a hash that is used to verify the [rp] Identity + * @throws NullPointerException If any of [allowHybrid], [requestJson], [rp], or [clientDataHash] is + * null. This is handled by the Kotlin runtime + * @throws IllegalArgumentException If any of [requestJson], [rp], or [clientDataHash] is empty + * + * @hide + */ +class CreatePublicKeyCredentialRequestPrivileged @JvmOverloads constructor( + requestJson: String, + val rp: String, + val clientDataHash: String, + @get:JvmName("allowHybrid") + val allowHybrid: Boolean = true +) : CreatePublicKeyCredentialBaseRequest( + requestJson, + PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL, + toBundle(requestJson, rp, clientDataHash, allowHybrid), + false, +) { + + init { + require(rp.isNotEmpty()) { "rp must not be empty" } + require(clientDataHash.isNotEmpty()) { "clientDataHash must not be empty" } + } + + /** A builder for [CreatePublicKeyCredentialRequestPrivileged]. */ + class Builder(var requestJson: String, var rp: String, var clientDataHash: String) { + + private var allowHybrid: Boolean = true + + /** + * Sets the privileged request in JSON format. + */ + fun setRequestJson(requestJson: String): Builder { + this.requestJson = requestJson + return this + } + + /** + * Sets whether hybrid credentials are allowed to fulfill this request, true by default. + */ + fun setAllowHybrid(allowHybrid: Boolean): Builder { + this.allowHybrid = allowHybrid + return this + } + + /** + * Sets the expected true RP ID which will override the one in the [requestJson]. + */ + fun setRp(rp: String): Builder { + this.rp = rp + return this + } + + /** + * Sets a hash that is used to verify the [rp] Identity. + */ + fun setClientDataHash(clientDataHash: String): Builder { + this.clientDataHash = clientDataHash + return this + } + + /** Builds a [CreatePublicKeyCredentialRequestPrivileged]. */ + fun build(): CreatePublicKeyCredentialRequestPrivileged { + return CreatePublicKeyCredentialRequestPrivileged(this.requestJson, + this.rp, this.clientDataHash, this.allowHybrid) + } + } + + companion object { + const val BUNDLE_KEY_RP = "androidx.credentials.BUNDLE_KEY_RP" + const val BUNDLE_KEY_CLIENT_DATA_HASH = + "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH" + const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID" + const val BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIVILEGED = + "androidx.credentials.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_" + + "PRIVILEGED" + + @JvmStatic + internal fun toBundle( + requestJson: String, + rp: String, + clientDataHash: String, + allowHybrid: Boolean + ): Bundle { + val bundle = Bundle() + bundle.putString(BUNDLE_KEY_SUBTYPE, + BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIVILEGED) + bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson) + bundle.putString(BUNDLE_KEY_RP, rp) + bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash) + bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid) + return bundle + } + + @JvmStatic + fun createFrom(data: Bundle): CreatePublicKeyCredentialRequestPrivileged { + try { + val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON) + val rp = data.getString(BUNDLE_KEY_RP) + val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH) + val allowHybrid = data.get(BUNDLE_KEY_ALLOW_HYBRID) + return CreatePublicKeyCredentialRequestPrivileged( + requestJson!!, + rp!!, + clientDataHash!!, + (allowHybrid!!) as Boolean, + ) + } catch (e: Exception) { + throw FrameworkClassParsingException() + } + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/Credential.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/Credential.kt new file mode 100644 index 000000000000..ee08e9e30649 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/Credential.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack.developer + +import android.os.Bundle + +/** + * Base class for a credential with which the user consented to authenticate to the app. + * + * @property type the credential type + * @property data the credential data in the [Bundle] format. + */ +open class Credential(val type: String, val data: Bundle) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt new file mode 100644 index 000000000000..497c272750ac --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack.developer + +/** + * Internal exception used to indicate a parsing error while converting from a framework type to + * a jetpack type. + * + * @hide + */ +internal class FrameworkClassParsingException : Exception()
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialOption.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialOption.kt new file mode 100644 index 000000000000..eb65241ed4df --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialOption.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack.developer + +import android.credentials.Credential +import android.os.Bundle + +/** + * Base request class for getting a registered credential. + * + * @property type the credential type + * @property data the request data in the [Bundle] format + * @property requireSystemProvider true if must only be fulfilled by a system provider and false + * otherwise + */ +open class GetCredentialOption( + val type: String, + val data: Bundle, + val requireSystemProvider: Boolean, +) { + companion object { + @JvmStatic + fun createFrom(from: android.credentials.GetCredentialOption): GetCredentialOption { + return try { + when (from.type) { + Credential.TYPE_PASSWORD_CREDENTIAL -> + GetPasswordOption.createFrom(from.data) + PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL -> + GetPublicKeyCredentialBaseOption.createFrom(from.data) + else -> + GetCredentialOption(from.type, from.data, from.requireSystemProvider()) + } + } catch (e: FrameworkClassParsingException) { + GetCredentialOption(from.type, from.data, from.requireSystemProvider()) + } + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialRequest.kt new file mode 100644 index 000000000000..7f9256ed6c75 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialRequest.kt @@ -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.credentialmanager.jetpack.developer + +/** + * Encapsulates a request to get a user credential. + * + * @property getCredentialOptions the list of [GetCredentialOption] from which the user can choose + * one to authenticate to the app + * @throws IllegalArgumentException If [getCredentialOptions] is empty + */ +class GetCredentialRequest constructor( + val getCredentialOptions: List<GetCredentialOption>, +) { + + init { + require(getCredentialOptions.isNotEmpty()) { "credentialRequests should not be empty" } + } + + /** A builder for [GetCredentialRequest]. */ + class Builder { + private var getCredentialOptions: MutableList<GetCredentialOption> = mutableListOf() + + /** Adds a specific type of [GetCredentialOption]. */ + fun addGetCredentialOption(getCredentialOption: GetCredentialOption): Builder { + getCredentialOptions.add(getCredentialOption) + return this + } + + /** Sets the list of [GetCredentialOption]. */ + fun setGetCredentialOptions(getCredentialOptions: List<GetCredentialOption>): Builder { + this.getCredentialOptions = getCredentialOptions.toMutableList() + return this + } + + /** + * Builds a [GetCredentialRequest]. + * + * @throws IllegalArgumentException If [getCredentialOptions] is empty + */ + fun build(): GetCredentialRequest { + return GetCredentialRequest(getCredentialOptions.toList()) + } + } + + companion object { + @JvmStatic + fun createFrom(from: android.credentials.GetCredentialRequest): GetCredentialRequest { + return GetCredentialRequest( + from.getCredentialOptions.map {GetCredentialOption.createFrom(it)} + ) + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPasswordOption.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPasswordOption.kt new file mode 100644 index 000000000000..2facad17b04e --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPasswordOption.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack.developer + +import android.credentials.Credential +import android.os.Bundle + +/** A request to retrieve the user's saved application password from their password provider. */ +class GetPasswordOption : GetCredentialOption( + Credential.TYPE_PASSWORD_CREDENTIAL, + Bundle(), + false, +) { + companion object { + @JvmStatic + fun createFrom(data: Bundle): GetPasswordOption { + return GetPasswordOption() + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialBaseOption.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialBaseOption.kt new file mode 100644 index 000000000000..9b51b306dd6b --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialBaseOption.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack.developer + +import android.os.Bundle + +/** + * Base request class for getting a registered public key credential. + * + * @property requestJson the request in JSON format + * @throws NullPointerException If [requestJson] is null - auto handled by the + * Kotlin runtime + * @throws IllegalArgumentException If [requestJson] is empty + * + * @hide + */ +abstract class GetPublicKeyCredentialBaseOption constructor( + val requestJson: String, + type: String, + data: Bundle, + requireSystemProvider: Boolean, +) : GetCredentialOption(type, data, requireSystemProvider) { + + init { + require(requestJson.isNotEmpty()) { "request json must not be empty" } + } + + companion object { + const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON" + const val BUNDLE_KEY_SUBTYPE = "androidx.credentials.BUNDLE_KEY_SUBTYPE" + + @JvmStatic + fun createFrom(data: Bundle): GetPublicKeyCredentialBaseOption { + return when (data.getString(BUNDLE_KEY_SUBTYPE)) { + GetPublicKeyCredentialOption + .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION -> + GetPublicKeyCredentialOption.createFrom(data) + GetPublicKeyCredentialOptionPrivileged + .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED -> + GetPublicKeyCredentialOptionPrivileged.createFrom(data) + else -> throw FrameworkClassParsingException() + } + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOption.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOption.kt new file mode 100644 index 000000000000..6f13c17f9b6e --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOption.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack.developer + +import android.os.Bundle + +/** + * A request to get passkeys from the user's public key credential provider. + * + * @property requestJson the request in JSON format + * @property allowHybrid defines whether hybrid credentials are allowed to fulfill this request, + * true by default + * @throws NullPointerException If [requestJson] or [allowHybrid] is null. It is handled by the + * Kotlin runtime + * @throws IllegalArgumentException If [requestJson] is empty + * + * @hide + */ +class GetPublicKeyCredentialOption @JvmOverloads constructor( + requestJson: String, + @get:JvmName("allowHybrid") + val allowHybrid: Boolean = true, +) : GetPublicKeyCredentialBaseOption( + requestJson, + PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL, + toBundle(requestJson, allowHybrid), + false +) { + companion object { + const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID" + const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION = + "androidx.credentials.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION" + + @JvmStatic + internal fun toBundle(requestJson: String, allowHybrid: Boolean): Bundle { + val bundle = Bundle() + bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson) + bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid) + return bundle + } + + @JvmStatic + fun createFrom(data: Bundle): GetPublicKeyCredentialOption { + try { + val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON) + val allowHybrid = data.get(BUNDLE_KEY_ALLOW_HYBRID) + return GetPublicKeyCredentialOption(requestJson!!, (allowHybrid!!) as Boolean) + } catch (e: Exception) { + throw FrameworkClassParsingException() + } + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOptionPrivileged.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOptionPrivileged.kt new file mode 100644 index 000000000000..79c62a1cdfbe --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOptionPrivileged.kt @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack.developer + +import android.os.Bundle + +/** + * A privileged request to get passkeys from the user's public key credential provider. The caller + * can modify the RP. Only callers with privileged permission (e.g. user's public browser or caBLE) + * can use this. + * + * @property requestJson the privileged request in JSON format + * @property allowHybrid defines whether hybrid credentials are allowed to fulfill this request, + * true by default + * @property rp the expected true RP ID which will override the one in the [requestJson] + * @property clientDataHash a hash that is used to verify the [rp] Identity + * @throws NullPointerException If any of [allowHybrid], [requestJson], [rp], or [clientDataHash] + * is null. This is handled by the Kotlin runtime + * @throws IllegalArgumentException If any of [requestJson], [rp], or [clientDataHash] is empty + * + * @hide + */ +class GetPublicKeyCredentialOptionPrivileged @JvmOverloads constructor( + requestJson: String, + val rp: String, + val clientDataHash: String, + @get:JvmName("allowHybrid") + val allowHybrid: Boolean = true +) : GetPublicKeyCredentialBaseOption( + requestJson, + PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL, + toBundle(requestJson, rp, clientDataHash, allowHybrid), + false, +) { + + init { + require(rp.isNotEmpty()) { "rp must not be empty" } + require(clientDataHash.isNotEmpty()) { "clientDataHash must not be empty" } + } + + /** A builder for [GetPublicKeyCredentialOptionPrivileged]. */ + class Builder(var requestJson: String, var rp: String, var clientDataHash: String) { + + private var allowHybrid: Boolean = true + + /** + * Sets the privileged request in JSON format. + */ + fun setRequestJson(requestJson: String): Builder { + this.requestJson = requestJson + return this + } + + /** + * Sets whether hybrid credentials are allowed to fulfill this request, true by default. + */ + fun setAllowHybrid(allowHybrid: Boolean): Builder { + this.allowHybrid = allowHybrid + return this + } + + /** + * Sets the expected true RP ID which will override the one in the [requestJson]. + */ + fun setRp(rp: String): Builder { + this.rp = rp + return this + } + + /** + * Sets a hash that is used to verify the [rp] Identity. + */ + fun setClientDataHash(clientDataHash: String): Builder { + this.clientDataHash = clientDataHash + return this + } + + /** Builds a [GetPublicKeyCredentialOptionPrivileged]. */ + fun build(): GetPublicKeyCredentialOptionPrivileged { + return GetPublicKeyCredentialOptionPrivileged(this.requestJson, + this.rp, this.clientDataHash, this.allowHybrid) + } + } + + companion object { + const val BUNDLE_KEY_RP = "androidx.credentials.BUNDLE_KEY_RP" + const val BUNDLE_KEY_CLIENT_DATA_HASH = + "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH" + const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID" + const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED = + "androidx.credentials.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION" + + "_PRIVILEGED" + + @JvmStatic + internal fun toBundle( + requestJson: String, + rp: String, + clientDataHash: String, + allowHybrid: Boolean + ): Bundle { + val bundle = Bundle() + bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson) + bundle.putString(BUNDLE_KEY_RP, rp) + bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash) + bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid) + return bundle + } + + @JvmStatic + fun createFrom(data: Bundle): GetPublicKeyCredentialOptionPrivileged { + try { + val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON) + val rp = data.getString(BUNDLE_KEY_RP) + val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH) + val allowHybrid = data.get(BUNDLE_KEY_ALLOW_HYBRID) + return GetPublicKeyCredentialOptionPrivileged( + requestJson!!, + rp!!, + clientDataHash!!, + (allowHybrid!!) as Boolean, + ) + } catch (e: Exception) { + throw FrameworkClassParsingException() + } + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PublicKeyCredential.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PublicKeyCredential.kt new file mode 100644 index 000000000000..b45a63bcf4ec --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PublicKeyCredential.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack.developer + +import android.os.Bundle + +/** + * Represents the user's passkey credential granted by the user for app sign-in. + * + * @property authenticationResponseJson the public key credential authentication response in + * JSON format that follows the standard webauthn json format shown at + * [this w3c link](https://w3c.github.io/webauthn/#dictdef-authenticationresponsejson) + * @throws NullPointerException If [authenticationResponseJson] is null. This is handled by the + * kotlin runtime + * @throws IllegalArgumentException If [authenticationResponseJson] is empty + * + * @hide + */ +class PublicKeyCredential constructor( + val authenticationResponseJson: String +) : Credential( + TYPE_PUBLIC_KEY_CREDENTIAL, + toBundle(authenticationResponseJson) +) { + + init { + require(authenticationResponseJson.isNotEmpty()) { + "authentication response JSON must not be empty" } + } + companion object { + /** The type value for public key credential related operations. */ + const val TYPE_PUBLIC_KEY_CREDENTIAL: String = + "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" + const val BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON = + "androidx.credentials.BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON" + + @JvmStatic + internal fun toBundle(authenticationResponseJson: String): Bundle { + val bundle = Bundle() + bundle.putString(BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON, authenticationResponseJson) + return bundle + } + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/ActionUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/ActionUi.kt index d4341b498fe0..1e639fe6bd55 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/ActionUi.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/ActionUi.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.credentialmanager.jetpack +package com.android.credentialmanager.jetpack.provider import android.app.slice.Slice import android.credentials.ui.Entry diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/CredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntryUi.kt index d6f1b5f5c8e9..12ab436e1507 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/CredentialEntryUi.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntryUi.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.credentialmanager.jetpack +package com.android.credentialmanager.jetpack.provider import android.app.slice.Slice import android.graphics.drawable.Icon diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasskeyCredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/PasskeyCredentialEntryUi.kt index bb3b206500b4..c5dbe66e8dbb 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasskeyCredentialEntryUi.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/PasskeyCredentialEntryUi.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.credentialmanager.jetpack +package com.android.credentialmanager.jetpack.provider import android.app.slice.Slice import android.credentials.ui.Entry diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasswordCredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/PasswordCredentialEntryUi.kt index 7311b7081343..5049503b32c1 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasswordCredentialEntryUi.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/PasswordCredentialEntryUi.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.credentialmanager.jetpack +package com.android.credentialmanager.jetpack.provider import android.app.slice.Slice import android.credentials.ui.Entry diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/SaveEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/SaveEntryUi.kt index fad3309fb86f..b260cf63587c 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/SaveEntryUi.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/SaveEntryUi.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.credentialmanager.jetpack +package com.android.credentialmanager.jetpack.provider import android.app.slice.Slice import android.credentials.ui.Entry diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index c659525d42a5..f170ead70b77 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -54,6 +54,7 @@ android_library { "SettingsLibSettingsTransition", "SettingsLibButtonPreference", "SettingsLibDeviceStateRotationLock", + "SettingsLibProfileSelector", "setupdesign", "zxing-core-1.7", "androidx.room_room-runtime", diff --git a/packages/SettingsLib/ProfileSelector/Android.bp b/packages/SettingsLib/ProfileSelector/Android.bp new file mode 100644 index 000000000000..250cd755a6a6 --- /dev/null +++ b/packages/SettingsLib/ProfileSelector/Android.bp @@ -0,0 +1,27 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_library { + name: "SettingsLibProfileSelector", + + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + + static_libs: [ + "com.google.android.material_material", + "SettingsLibSettingsTheme", + ], + + sdk_version: "system_current", + min_sdk_version: "23", + apex_available: [ + "//apex_available:platform", + "com.android.mediaprovider", + ], +} diff --git a/packages/SettingsLib/ProfileSelector/AndroidManifest.xml b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml new file mode 100644 index 000000000000..a57469e39eb6 --- /dev/null +++ b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2020 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.settingslib.widget"> + + <uses-sdk android:minSdkVersion="23" /> +</manifest> diff --git a/packages/SettingsLib/res/color-night-v31/settingslib_tabs_indicator_color.xml b/packages/SettingsLib/ProfileSelector/res/color-night/settingslib_tabs_indicator_color.xml index 9a093601a92c..9a093601a92c 100644 --- a/packages/SettingsLib/res/color-night-v31/settingslib_tabs_indicator_color.xml +++ b/packages/SettingsLib/ProfileSelector/res/color-night/settingslib_tabs_indicator_color.xml diff --git a/packages/SettingsLib/res/color-night-v31/settingslib_tabs_text_color.xml b/packages/SettingsLib/ProfileSelector/res/color-night/settingslib_tabs_text_color.xml index 33f96df8d5b5..33f96df8d5b5 100644 --- a/packages/SettingsLib/res/color-night-v31/settingslib_tabs_text_color.xml +++ b/packages/SettingsLib/ProfileSelector/res/color-night/settingslib_tabs_text_color.xml diff --git a/packages/SettingsLib/res/color-v31/settingslib_tabs_indicator_color.xml b/packages/SettingsLib/ProfileSelector/res/color/settingslib_tabs_indicator_color.xml index 57fef52f15bd..57fef52f15bd 100644 --- a/packages/SettingsLib/res/color-v31/settingslib_tabs_indicator_color.xml +++ b/packages/SettingsLib/ProfileSelector/res/color/settingslib_tabs_indicator_color.xml diff --git a/packages/SettingsLib/res/color-v31/settingslib_tabs_text_color.xml b/packages/SettingsLib/ProfileSelector/res/color/settingslib_tabs_text_color.xml index df2346d7175e..df2346d7175e 100644 --- a/packages/SettingsLib/res/color-v31/settingslib_tabs_text_color.xml +++ b/packages/SettingsLib/ProfileSelector/res/color/settingslib_tabs_text_color.xml diff --git a/packages/SettingsLib/res/drawable-v31/settingslib_tabs_background.xml b/packages/SettingsLib/ProfileSelector/res/drawable/settingslib_tabs_background.xml index 5378eeef97e0..5378eeef97e0 100644 --- a/packages/SettingsLib/res/drawable-v31/settingslib_tabs_background.xml +++ b/packages/SettingsLib/ProfileSelector/res/drawable/settingslib_tabs_background.xml diff --git a/packages/SettingsLib/res/drawable-v31/settingslib_tabs_indicator_background.xml b/packages/SettingsLib/ProfileSelector/res/drawable/settingslib_tabs_indicator_background.xml index 9c10a5b59281..9c10a5b59281 100644 --- a/packages/SettingsLib/res/drawable-v31/settingslib_tabs_indicator_background.xml +++ b/packages/SettingsLib/ProfileSelector/res/drawable/settingslib_tabs_indicator_background.xml diff --git a/packages/SettingsLib/ProfileSelector/res/layout/tab_fragment.xml b/packages/SettingsLib/ProfileSelector/res/layout/tab_fragment.xml new file mode 100644 index 000000000000..0448c6c4f467 --- /dev/null +++ b/packages/SettingsLib/ProfileSelector/res/layout/tab_fragment.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:theme="@style/Theme.MaterialComponents.DayNight" + android:id="@+id/tab_container" + android:clipToPadding="true" + android:clipChildren="true" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <com.google.android.material.tabs.TabLayout + android:id="@+id/tabs" + style="@style/SettingsLibTabsStyle"/> + + <androidx.viewpager2.widget.ViewPager2 + android:id="@+id/view_pager" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + </androidx.viewpager2.widget.ViewPager2> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SettingsLib/ProfileSelector/res/values/strings.xml b/packages/SettingsLib/ProfileSelector/res/values/strings.xml new file mode 100644 index 000000000000..68d4047a497c --- /dev/null +++ b/packages/SettingsLib/ProfileSelector/res/values/strings.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + + <!-- Header for items under the personal user [CHAR LIMIT=30] --> + <string name="settingslib_category_personal">Personal</string> + <!-- Header for items under the work user [CHAR LIMIT=30] --> + <string name="settingslib_category_work">Work</string> +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/res/values-v31/styles.xml b/packages/SettingsLib/ProfileSelector/res/values/styles.xml index 0b703c99884b..0b703c99884b 100644 --- a/packages/SettingsLib/res/values-v31/styles.xml +++ b/packages/SettingsLib/ProfileSelector/res/values/styles.xml diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java new file mode 100644 index 000000000000..ac426ed8b5d4 --- /dev/null +++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.widget; + +import android.app.Activity; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.fragment.app.Fragment; +import androidx.viewpager2.widget.ViewPager2; + +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; + +/** + * Base fragment class for profile settings. + */ +public abstract class ProfileSelectFragment extends Fragment { + + /** + * Personal or Work profile tab of {@link ProfileSelectFragment} + * <p>0: Personal tab. + * <p>1: Work profile tab. + */ + public static final String EXTRA_SHOW_FRAGMENT_TAB = + ":settings:show_fragment_tab"; + + /** + * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB + */ + public static final int PERSONAL_TAB = 0; + + /** + * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB + */ + public static final int WORK_TAB = 1; + + private ViewGroup mContentView; + + private ViewPager2 mViewPager; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Defines the xml file for the fragment + mContentView = (ViewGroup) inflater.inflate(R.layout.tab_fragment, container, false); + + final Activity activity = getActivity(); + final int titleResId = getTitleResId(); + if (titleResId > 0) { + activity.setTitle(titleResId); + } + final int selectedTab = getTabId(activity, getArguments()); + + final View tabContainer = mContentView.findViewById(R.id.tab_container); + mViewPager = tabContainer.findViewById(R.id.view_pager); + mViewPager.setAdapter(new ProfileViewPagerAdapter(this)); + final TabLayout tabs = tabContainer.findViewById(R.id.tabs); + new TabLayoutMediator(tabs, mViewPager, + (tab, position) -> tab.setText(getPageTitle(position)) + ).attach(); + + tabContainer.setVisibility(View.VISIBLE); + final TabLayout.Tab tab = tabs.getTabAt(selectedTab); + tab.select(); + + return mContentView; + } + + /** + * create Personal or Work profile fragment + * <p>0: Personal profile. + * <p>1: Work profile. + */ + public abstract Fragment createFragment(int position); + + /** + * Returns a resource ID of the title + * Override this if the title needs to be updated dynamically. + */ + public int getTitleResId() { + return 0; + } + + int getTabId(Activity activity, Bundle bundle) { + if (bundle != null) { + final int extraTab = bundle.getInt(EXTRA_SHOW_FRAGMENT_TAB, -1); + if (extraTab != -1) { + return extraTab; + } + } + return PERSONAL_TAB; + } + + private CharSequence getPageTitle(int position) { + if (position == WORK_TAB) { + return getContext().getString(R.string.settingslib_category_work); + } + + return getString(R.string.settingslib_category_personal); + } +} diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java new file mode 100644 index 000000000000..daf2564a674e --- /dev/null +++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.widget; + +import androidx.fragment.app.Fragment; +import androidx.viewpager2.adapter.FragmentStateAdapter; + +/** + * ViewPager Adapter to handle between TabLayout and ViewPager2 + */ +public class ProfileViewPagerAdapter extends FragmentStateAdapter { + + private final ProfileSelectFragment mParentFragments; + + ProfileViewPagerAdapter(ProfileSelectFragment fragment) { + super(fragment); + mParentFragments = fragment; + } + + @Override + public Fragment createFragment(int position) { + return mParentFragments.createFragment(position); + } + + @Override + public int getItemCount() { + return 2; + } +} diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp b/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp index bcc64d3cd234..b5a21bdae606 100644 --- a/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp +++ b/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp @@ -23,5 +23,6 @@ android_library { apex_available: [ "//apex_available:platform", "com.android.permission", + "com.android.mediaprovider", ], } diff --git a/packages/SettingsLib/SettingsTheme/Android.bp b/packages/SettingsLib/SettingsTheme/Android.bp index 82e0220997d3..939977fa7ef2 100644 --- a/packages/SettingsLib/SettingsTheme/Android.bp +++ b/packages/SettingsLib/SettingsTheme/Android.bp @@ -24,5 +24,6 @@ android_library { "com.android.permission", "com.android.adservices", "com.android.healthconnect", + "com.android.mediaprovider", ], } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt index 83e3f786c200..04d0fe03e1e8 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt @@ -18,7 +18,6 @@ package com.android.settingslib.spa.gallery.home import android.os.Bundle import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsPageProvider @@ -60,9 +59,13 @@ object HomePageProvider : SettingsPageProvider { ) } + override fun getTitle(arguments: Bundle?): String { + return SpaEnvironmentFactory.instance.appContext.getString(R.string.app_name) + } + @Composable override fun Page(arguments: Bundle?) { - HomeScaffold(title = stringResource(R.string.app_name)) { + HomeScaffold(title = getTitle(arguments)) { for (entry in buildEntry(arguments)) { if (entry.owner.isCreateBy(SettingsPageProviderEnum.ARGUMENT.name)) { entry.UiLayout(ArgumentPageModel.buildArgument(intParam = 0)) diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt index 60ff3627b33e..7958d11ad513 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt @@ -99,9 +99,13 @@ object ArgumentPageProvider : SettingsPageProvider { } } + override fun getTitle(arguments: Bundle?): String { + return ArgumentPageModel.genPageTitle() + } + @Composable override fun Page(arguments: Bundle?) { - RegularScaffold(title = ArgumentPageModel.create(arguments).genPageTitle()) { + RegularScaffold(title = getTitle(arguments)) { for (entry in buildEntry(arguments)) { if (entry.toPage != null) { entry.UiLayout(ArgumentPageModel.buildNextArgument(arguments)) diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt index e5e3c679a76a..5f15865ff7b9 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt @@ -81,6 +81,10 @@ class ArgumentPageModel : PageModel() { return EntrySearchData(title = PAGE_TITLE, keyword = ARGUMENT_PAGE_KEYWORDS) } + fun genPageTitle(): String { + return PAGE_TITLE + } + @Composable fun create(arguments: Bundle?): ArgumentPageModel { val pageModel: ArgumentPageModel = viewModel(key = arguments.toString()) @@ -89,7 +93,6 @@ class ArgumentPageModel : PageModel() { } } - private val title = PAGE_TITLE private var arguments: Bundle? = null private var stringParam: String? = null private var intParam: Int? = null @@ -104,11 +107,6 @@ class ArgumentPageModel : PageModel() { } @Composable - fun genPageTitle(): String { - return title - } - - @Composable fun genStringParamPreferenceModel(): PreferenceModel { return object : PreferenceModel { override val title = STRING_PARAM_TITLE @@ -131,7 +129,7 @@ class ArgumentPageModel : PageModel() { "$INT_PARAM_NAME=" + intParam!! ) return object : PreferenceModel { - override val title = genPageTitle() + override val title = PAGE_TITLE override val summary = stateOf(summaryArray.joinToString(", ")) override val onClick = navigator( SettingsPageProviderEnum.ARGUMENT.displayName + parameter.navLink(arguments) diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt index 0fc2a5f5fda8..c903cfd96ce3 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt @@ -67,9 +67,13 @@ object FooterPageProvider : SettingsPageProvider { } } + override fun getTitle(arguments: Bundle?): String { + return TITLE + } + @Composable override fun Page(arguments: Bundle?) { - RegularScaffold(title = TITLE) { + RegularScaffold(title = getTitle(arguments)) { for (entry in buildEntry(arguments)) { entry.UiLayout() } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt index a64d4a5d3ea6..e10cf3aa0702 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt @@ -31,7 +31,6 @@ import com.android.settingslib.spa.widget.IllustrationModel import com.android.settingslib.spa.widget.ResourceType import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel -import com.android.settingslib.spa.widget.scaffold.RegularScaffold private const val TITLE = "Sample Illustration" @@ -82,13 +81,8 @@ object IllustrationPageProvider : SettingsPageProvider { } } - @Composable - override fun Page(arguments: Bundle?) { - RegularScaffold(title = TITLE) { - for (entry in buildEntry(arguments)) { - entry.UiLayout() - } - } + override fun getTitle(arguments: Bundle?): String { + return TITLE } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt index dc45df4a0374..9136b0430c40 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt @@ -60,6 +60,10 @@ object ProgressBarPageProvider : SettingsPageProvider { } } + override fun getTitle(arguments: Bundle?): String { + return TITLE + } + @Composable override fun Page(arguments: Bundle?) { // Mocks a loading time of 2 seconds. @@ -69,7 +73,7 @@ object ProgressBarPageProvider : SettingsPageProvider { loading = false } - RegularScaffold(title = TITLE) { + RegularScaffold(title = getTitle(arguments)) { // Auto update the progress and finally jump tp 0.4f. var progress by remember { mutableStateOf(0f) } LaunchedEffect(Unit) { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt index b38178b0e6f4..cb58a95e01a6 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt @@ -49,9 +49,13 @@ object SettingsPagerPageProvider : SettingsPageProvider { } } + override fun getTitle(arguments: Bundle?): String { + return TITLE + } + @Composable override fun Page(arguments: Bundle?) { - SettingsScaffold(title = TITLE) { paddingValues -> + SettingsScaffold(title = getTitle(arguments)) { paddingValues -> Box(Modifier.padding(paddingValues)) { SettingsPager(listOf("Personal", "Work")) { PlaceholderTitle("Page $it") diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt index 7567c6daf996..73b34a50a520 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt @@ -37,7 +37,6 @@ import com.android.settingslib.spa.widget.preference.SliderPreference import com.android.settingslib.spa.widget.preference.SliderPreferenceModel import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel -import com.android.settingslib.spa.widget.scaffold.RegularScaffold private const val TITLE = "Sample Slider" @@ -119,13 +118,8 @@ object SliderPageProvider : SettingsPageProvider { } } - @Composable - override fun Page(arguments: Bundle?) { - RegularScaffold(title = TITLE) { - for (entry in buildEntry(arguments)) { - entry.UiLayout() - } - } + override fun getTitle(arguments: Bundle?): String { + return TITLE } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt index a8e49384da97..f38a8d4193b3 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt @@ -33,7 +33,6 @@ import com.android.settingslib.spa.widget.preference.MainSwitchPreference import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel -import com.android.settingslib.spa.widget.scaffold.RegularScaffold private const val TITLE = "Sample MainSwitchPreference" @@ -72,13 +71,8 @@ object MainSwitchPreferencePageProvider : SettingsPageProvider { } } - @Composable - override fun Page(arguments: Bundle?) { - RegularScaffold(title = TITLE) { - for (entry in buildEntry(arguments)) { - entry.UiLayout() - } - } + override fun getTitle(arguments: Bundle?): String { + return TITLE } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMain.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMain.kt index 165eaa05c9d5..61925a7b7164 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMain.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMain.kt @@ -17,7 +17,6 @@ package com.android.settingslib.spa.gallery.preference import android.os.Bundle -import androidx.compose.runtime.Composable import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider @@ -25,7 +24,6 @@ import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel -import com.android.settingslib.spa.widget.scaffold.RegularScaffold private const val TITLE = "Category: Preference" @@ -54,12 +52,7 @@ object PreferenceMainPageProvider : SettingsPageProvider { } } - @Composable - override fun Page(arguments: Bundle?) { - RegularScaffold(title = TITLE) { - for (entry in buildEntry(arguments)) { - entry.UiLayout() - } - } + override fun getTitle(arguments: Bundle?): String { + return TITLE } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt index 2c2782b20e35..26e59ff699a4 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt @@ -49,7 +49,6 @@ import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Compan import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SimplePreferenceMacro -import com.android.settingslib.spa.widget.scaffold.RegularScaffold import com.android.settingslib.spa.widget.ui.SettingsIcon private const val TAG = "PreferencePage" @@ -128,11 +127,11 @@ object PreferencePageProvider : SettingsPageProvider { .setStatusDataFn { EntryStatusData(isDisabled = false) } .setUiLayoutFn { val model = PreferencePageModel.create() - val asyncSummary = remember { model.getAsyncSummary() } Preference( object : PreferenceModel { override val title = ASYNC_PREFERENCE_TITLE - override val summary = asyncSummary + override val summary = model.asyncSummary + override val enabled = model.asyncEnable } ) }.build() @@ -204,13 +203,8 @@ object PreferencePageProvider : SettingsPageProvider { } } - @Composable - override fun Page(arguments: Bundle?) { - RegularScaffold(title = PAGE_TITLE) { - for (entry in buildEntry(arguments)) { - entry.UiLayout() - } - } + override fun getTitle(arguments: Bundle?): String { + return PAGE_TITLE } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt index 1e64b2edb60b..d874417c1a56 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt @@ -59,7 +59,8 @@ class PreferencePageModel : PageModel() { private val spaLogger = SpaEnvironmentFactory.instance.logger - private val asyncSummary = mutableStateOf(" ") + val asyncSummary = mutableStateOf("(loading)") + val asyncEnable = mutableStateOf(false) private val manualUpdater = mutableStateOf(0) @@ -87,16 +88,13 @@ class PreferencePageModel : PageModel() { override fun initialize(arguments: Bundle?) { spaLogger.message(TAG, "initialize with args " + arguments.toString()) viewModelScope.launch(Dispatchers.IO) { + // Loading your data here. delay(2000L) asyncSummary.value = ASYNC_PREFERENCE_SUMMARY + asyncEnable.value = true } } - fun getAsyncSummary(): State<String> { - spaLogger.message(TAG, "getAsyncSummary") - return asyncSummary - } - fun getManualUpdaterSummary(): State<String> { spaLogger.message(TAG, "getManualUpdaterSummary") return derivedStateOf { manualUpdater.value.toString() } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt index 46b44ca9d614..367766a34a3c 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt @@ -34,7 +34,6 @@ import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SwitchPreference import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel -import com.android.settingslib.spa.widget.scaffold.RegularScaffold import kotlinx.coroutines.delay private const val TITLE = "Sample SwitchPreference" @@ -88,13 +87,8 @@ object SwitchPreferencePageProvider : SettingsPageProvider { } } - @Composable - override fun Page(arguments: Bundle?) { - RegularScaffold(title = TITLE) { - for (entry in buildEntry(arguments)) { - entry.UiLayout() - } - } + override fun getTitle(arguments: Bundle?): String { + return TITLE } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt index b991f59866eb..22da99c23fc8 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt @@ -34,7 +34,6 @@ import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference -import com.android.settingslib.spa.widget.scaffold.RegularScaffold import kotlinx.coroutines.delay private const val TITLE = "Sample TwoTargetSwitchPreference" @@ -88,13 +87,8 @@ object TwoTargetSwitchPreferencePageProvider : SettingsPageProvider { } } - @Composable - override fun Page(arguments: Bundle?) { - RegularScaffold(title = TITLE) { - for (entry in buildEntry(arguments)) { - entry.UiLayout() - } - } + override fun getTitle(arguments: Bundle?): String { + return TITLE } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt index a4713b993af0..d87cbe82d9d8 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt @@ -48,41 +48,40 @@ object CategoryPageProvider : SettingsPageProvider { } } - @Composable - override fun Page(arguments: Bundle?) { - CategoryPage() + override fun getTitle(arguments: Bundle?): String { + return TITLE } -} -@Composable -private fun CategoryPage() { - RegularScaffold(title = TITLE) { - CategoryTitle("Category A") - Preference(remember { - object : PreferenceModel { - override val title = "Preference 1" - override val summary = stateOf("Summary 1") - } - }) - Preference(remember { - object : PreferenceModel { - override val title = "Preference 2" - override val summary = stateOf("Summary 2") - } - }) - Category("Category B") { + @Composable + override fun Page(arguments: Bundle?) { + RegularScaffold(title = getTitle(arguments)) { + CategoryTitle("Category A") Preference(remember { object : PreferenceModel { - override val title = "Preference 3" - override val summary = stateOf("Summary 3") + override val title = "Preference 1" + override val summary = stateOf("Summary 1") } }) Preference(remember { object : PreferenceModel { - override val title = "Preference 4" - override val summary = stateOf("Summary 4") + override val title = "Preference 2" + override val summary = stateOf("Summary 2") } }) + Category("Category B") { + Preference(remember { + object : PreferenceModel { + override val title = "Preference 3" + override val summary = stateOf("Summary 3") + } + }) + Preference(remember { + object : PreferenceModel { + override val title = "Preference 4" + override val summary = stateOf("Summary 4") + } + }) + } } } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt index 03b72d348d40..ec2f436c9e98 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt @@ -49,9 +49,13 @@ object SpinnerPageProvider : SettingsPageProvider { } } + override fun getTitle(arguments: Bundle?): String { + return TITLE + } + @Composable override fun Page(arguments: Bundle?) { - RegularScaffold(title = TITLE) { + RegularScaffold(title = getTitle(arguments)) { val selectedIndex = rememberSaveable { mutableStateOf(0) } Spinner( options = (1..3).map { "Option $it" }, diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt index f8963b2a8837..151b50cdb5c4 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt @@ -19,6 +19,7 @@ package com.android.settingslib.spa.framework.common import android.os.Bundle import androidx.compose.runtime.Composable import androidx.navigation.NamedNavArgument +import com.android.settingslib.spa.widget.scaffold.RegularScaffold /** * An SettingsPageProvider which is used to create Settings page instances. @@ -36,13 +37,19 @@ interface SettingsPageProvider { val parameter: List<NamedNavArgument> get() = emptyList() - /** The [Composable] used to render this page. */ - @Composable - fun Page(arguments: Bundle?) + fun getTitle(arguments: Bundle?): String = displayName ?: name fun buildEntry(arguments: Bundle?): List<SettingsEntry> = emptyList() - fun getTitle(arguments: Bundle?): String = displayName ?: name + /** The [Composable] used to render this page. */ + @Composable + fun Page(arguments: Bundle?) { + RegularScaffold(title = getTitle(arguments)) { + for (entry in buildEntry(arguments)) { + entry.UiLayout() + } + } + } } fun SettingsPageProvider.createSettingsPage(arguments: Bundle? = null): SettingsPage { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt index 60734254a58b..b83104360260 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt @@ -59,7 +59,8 @@ abstract class SpaEnvironment(context: Context) { val entryRepository = lazy { SettingsEntryRepository(pageProviderRepository.value) } - val appContext: Context = context.applicationContext + // In Robolectric test, applicationContext is not available. Use context as fallback. + val appContext: Context = context.applicationContext ?: context open val browseActivityClass: Class<out Activity>? = null diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/ParameterTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/ParameterTest.kt new file mode 100644 index 000000000000..21ff08515ee6 --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/ParameterTest.kt @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.framework.util + +import androidx.core.os.bundleOf +import androidx.navigation.NamedNavArgument +import androidx.navigation.NavType +import androidx.navigation.navArgument +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class ParameterTest { + @Test + fun navRouteTest() { + val navArguments = listOf( + navArgument("string_param") { type = NavType.StringType }, + navArgument("int_param") { type = NavType.IntType }, + ) + + val route = navArguments.navRoute() + assertThat(route).isEqualTo("/{string_param}/{int_param}") + } + + @Test + fun navLinkTest() { + val navArguments = listOf( + navArgument("string_param") { type = NavType.StringType }, + navArgument("int_param") { type = NavType.IntType }, + ) + + val unsetAllLink = navArguments.navLink() + assertThat(unsetAllLink).isEqualTo("/[unset]/[unset]") + + val setAllLink = navArguments.navLink( + bundleOf( + "string_param" to "myStr", + "int_param" to 10, + ) + ) + assertThat(setAllLink).isEqualTo("/myStr/10") + + val setUnknownLink = navArguments.navLink( + bundleOf( + "string_param" to "myStr", + "int_param" to 10, + "unknown_param" to "unknown", + ) + ) + assertThat(setUnknownLink).isEqualTo("/myStr/10") + + val setWrongTypeLink = navArguments.navLink( + bundleOf( + "string_param" to "myStr", + "int_param" to "wrongStr", + ) + ) + assertThat(setWrongTypeLink).isEqualTo("/myStr/0") + } + + @Test + fun normalizeTest() { + val emptyArguments = emptyList<NamedNavArgument>() + assertThat(emptyArguments.normalize()).isNull() + + val navArguments = listOf( + navArgument("string_param") { type = NavType.StringType }, + navArgument("int_param") { type = NavType.IntType }, + navArgument("rt_param") { type = NavType.StringType }, + ) + + val emptyParam = navArguments.normalize() + assertThat(emptyParam).isNotNull() + assertThat(emptyParam.toString()).isEqualTo( + "Bundle[{rt_param=null, unset_string_param=null, unset_int_param=null}]" + ) + + val setParialParam = navArguments.normalize( + bundleOf( + "string_param" to "myStr", + "rt_param" to "rtStr", + ) + ) + assertThat(setParialParam).isNotNull() + assertThat(setParialParam.toString()).isEqualTo( + "Bundle[{rt_param=null, string_param=myStr, unset_int_param=null}]" + ) + + val setAllParam = navArguments.normalize( + bundleOf( + "string_param" to "myStr", + "int_param" to 10, + "rt_param" to "rtStr", + ) + ) + assertThat(setAllParam).isNotNull() + assertThat(setAllParam.toString()).isEqualTo( + "Bundle[{rt_param=null, int_param=10, string_param=myStr}]" + ) + } + + @Test + fun getArgTest() { + val navArguments = listOf( + navArgument("string_param") { type = NavType.StringType }, + navArgument("int_param") { type = NavType.IntType }, + ) + + assertThat( + navArguments.getStringArg( + "string_param", bundleOf( + "string_param" to "myStr", + ) + ) + ).isEqualTo("myStr") + + assertThat( + navArguments.getStringArg( + "string_param", bundleOf( + "string_param" to 10, + ) + ) + ).isNull() + + assertThat( + navArguments.getStringArg( + "unknown_param", bundleOf( + "string_param" to "myStr", + ) + ) + ).isNull() + + assertThat(navArguments.getStringArg("string_param")).isNull() + + assertThat( + navArguments.getIntArg( + "int_param", bundleOf( + "int_param" to 10, + ) + ) + ).isEqualTo(10) + + assertThat( + navArguments.getIntArg( + "int_param", bundleOf( + "int_param" to "10", + ) + ) + ).isEqualTo(0) + + assertThat( + navArguments.getIntArg( + "unknown_param", bundleOf( + "int_param" to 10, + ) + ) + ).isNull() + + assertThat(navArguments.getIntArg("int_param")).isNull() + } + + @Test + fun isRuntimeParamTest() { + val regularParam = navArgument("regular_param") { type = NavType.StringType } + val rtParam = navArgument("rt_param") { type = NavType.StringType } + assertThat(regularParam.isRuntimeParam()).isFalse() + assertThat(rtParam.isRuntimeParam()).isTrue() + } +} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt index fd723dd067d7..bb1cd6e44867 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt @@ -22,6 +22,7 @@ import android.app.admin.DevicePolicyManager import android.app.usage.StorageStatsManager import android.apphibernation.AppHibernationManager import android.content.Context +import android.content.pm.CrossProfileApps import android.content.pm.verify.domain.DomainVerificationManager import android.os.UserHandle import android.os.UserManager @@ -36,6 +37,9 @@ val Context.appHibernationManager get() = getSystemService(AppHibernationManager /** The [AppOpsManager] instance. */ val Context.appOpsManager get() = getSystemService(AppOpsManager::class.java)!! +/** The [CrossProfileApps] instance. */ +val Context.crossProfileApps get() = getSystemService(CrossProfileApps::class.java)!! + /** The [DevicePolicyManager] instance. */ val Context.devicePolicyManager get() = getSystemService(DevicePolicyManager::class.java)!! diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt index 0f037e40b997..06ea381d0c1d 100644 --- a/packages/SystemUI/ktfmt_includes.txt +++ b/packages/SystemUI/ktfmt_includes.txt @@ -27,8 +27,6 @@ -packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt -packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt -packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt --packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionDarkness.kt --packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt -packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonPositionCalculator.kt -packages/SystemUI/shared/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerManager.kt -packages/SystemUI/shared/src/com/android/systemui/shared/system/smartspace/SmartspaceState.kt @@ -683,8 +681,6 @@ -packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt -packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimatorTest.kt -packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt --packages/SystemUI/tests/src/com/android/systemui/shared/navigationbar/RegionSamplingHelperTest.kt --packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplingInstanceTest.kt -packages/SystemUI/tests/src/com/android/systemui/shared/rotation/RotationButtonControllerTest.kt -packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt -packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt diff --git a/packages/SystemUI/res/layout/user_switcher_fullscreen.xml b/packages/SystemUI/res/layout/user_switcher_fullscreen.xml index c2c79cb0f34b..78884ffbe1a2 100644 --- a/packages/SystemUI/res/layout/user_switcher_fullscreen.xml +++ b/packages/SystemUI/res/layout/user_switcher_fullscreen.xml @@ -14,58 +14,84 @@ See the License for the specific language governing permissions and limitations under the License. --> -<com.android.systemui.user.UserSwitcherRootView +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/user_switcher_root" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginVertical="40dp" - android:layout_marginHorizontal="60dp"> + android:orientation="vertical"> - <androidx.constraintlayout.helper.widget.Flow - android:id="@+id/flow" - android:layout_width="0dp" - android:layout_height="wrap_content" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:flow_horizontalBias="0.5" - app:flow_verticalAlign="center" - app:flow_wrapMode="chain" - app:flow_horizontalGap="@dimen/user_switcher_fullscreen_horizontal_gap" - app:flow_verticalGap="44dp" - app:flow_horizontalStyle="packed"/> + <ScrollView + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:fillViewport="true"> - <TextView - android:id="@+id/cancel" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:gravity="center" - app:layout_constraintHeight_min="48dp" - app:layout_constraintEnd_toStartOf="@+id/add" - app:layout_constraintBottom_toBottomOf="parent" - android:paddingHorizontal="@dimen/user_switcher_fullscreen_button_padding" - android:textSize="@dimen/user_switcher_fullscreen_button_text_size" - android:textColor="?androidprv:attr/colorAccentPrimary" - android:text="@string/cancel" /> + <com.android.systemui.user.UserSwitcherRootView + android:id="@+id/user_switcher_grid_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="40dp" + android:paddingHorizontal="60dp"> - <TextView - android:id="@+id/add" - style="@style/Widget.Dialog.Button.BorderButton" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:gravity="center" - android:paddingHorizontal="@dimen/user_switcher_fullscreen_button_padding" - android:text="@string/add" - android:textColor="?androidprv:attr/colorAccentPrimary" - android:textSize="@dimen/user_switcher_fullscreen_button_text_size" - android:visibility="gone" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintHeight_min="48dp" /> -</com.android.systemui.user.UserSwitcherRootView> + <androidx.constraintlayout.helper.widget.Flow + android:id="@+id/flow" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:flow_horizontalBias="0.5" + app:flow_verticalAlign="center" + app:flow_wrapMode="chain" + app:flow_horizontalGap="@dimen/user_switcher_fullscreen_horizontal_gap" + app:flow_verticalGap="44dp" + app:flow_horizontalStyle="packed"/> + </com.android.systemui.user.UserSwitcherRootView> + + </ScrollView> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="96dp" + android:orientation="horizontal" + android:gravity="center_vertical|end" + android:paddingEnd="48dp"> + + <TextView + android:id="@+id/cancel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:gravity="center" + android:minHeight="48dp" + android:paddingHorizontal="@dimen/user_switcher_fullscreen_button_padding" + android:textSize="@dimen/user_switcher_fullscreen_button_text_size" + android:textColor="?androidprv:attr/colorAccentPrimary" + android:text="@string/cancel" /> + + <Space + android:layout_width="24dp" + android:layout_height="0dp" + /> + + <TextView + android:id="@+id/add" + style="@style/Widget.Dialog.Button.BorderButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:gravity="center" + android:paddingHorizontal="@dimen/user_switcher_fullscreen_button_padding" + android:text="@string/add" + android:textColor="?androidprv:attr/colorAccentPrimary" + android:textSize="@dimen/user_switcher_fullscreen_button_text_size" + android:visibility="gone" + android:minHeight="48dp" /> + + </LinearLayout> + +</LinearLayout> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt index da1d233949cf..3961438ff591 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -237,6 +237,9 @@ class DefaultClockController( ) { var isActive: Boolean = fraction < 0.5f fun update(newFraction: Float): Pair<Boolean, Boolean> { + if (newFraction == fraction) { + return Pair(isActive, false) + } val wasActive = isActive val hasJumped = (fraction == 0f && newFraction == 1f) || (fraction == 1f && newFraction == 0f) diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl index 4613e8b1060f..e743ec87bd1c 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl @@ -110,4 +110,9 @@ oneway interface IOverviewProxy { * Sent when screen started turning off. */ void onScreenTurningOff() = 24; + + /** + * Sent when split keyboard shortcut is triggered to enter stage split. + */ + void enterStageSplitFromRunningApp(boolean leftOrTop) = 25; } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt index cd4b9994ccca..0ee813b84402 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt @@ -24,15 +24,13 @@ import com.android.systemui.shared.navigationbar.RegionSamplingHelper.SamplingCa import java.io.PrintWriter import java.util.concurrent.Executor -/** - * Class for instance of RegionSamplingHelper - */ -open class RegionSamplingInstance( - sampledView: View?, - mainExecutor: Executor?, - bgExecutor: Executor?, - regionSamplingEnabled: Boolean, - updateFun: UpdateColorCallback +/** Class for instance of RegionSamplingHelper */ +open class RegionSampler( + sampledView: View?, + mainExecutor: Executor?, + bgExecutor: Executor?, + regionSamplingEnabled: Boolean, + updateFun: UpdateColorCallback ) { private var regionDarkness = RegionDarkness.DEFAULT private var samplingBounds = Rect() @@ -40,23 +38,13 @@ open class RegionSamplingInstance( @VisibleForTesting var regionSampler: RegionSamplingHelper? = null private var lightForegroundColor = Color.WHITE private var darkForegroundColor = Color.BLACK - /** - * Interface for method to be passed into RegionSamplingHelper - */ - @FunctionalInterface - interface UpdateColorCallback { - /** - * Method to update the foreground colors after clock darkness changed. - */ - fun updateColors() - } @VisibleForTesting open fun createRegionSamplingHelper( - sampledView: View, - callback: SamplingCallback, - mainExecutor: Executor?, - bgExecutor: Executor? + sampledView: View, + callback: SamplingCallback, + mainExecutor: Executor?, + bgExecutor: Executor? ): RegionSamplingHelper { return RegionSamplingHelper(sampledView, callback, mainExecutor, bgExecutor) } @@ -77,7 +65,7 @@ open class RegionSamplingInstance( * * @return the determined foreground color */ - fun currentForegroundColor(): Int{ + fun currentForegroundColor(): Int { return if (regionDarkness.isDark) { lightForegroundColor } else { @@ -97,41 +85,37 @@ open class RegionSamplingInstance( return regionDarkness } - /** - * Start region sampler - */ + /** Start region sampler */ fun startRegionSampler() { regionSampler?.start(samplingBounds) } - /** - * Stop region sampler - */ + /** Stop region sampler */ fun stopRegionSampler() { regionSampler?.stop() } - /** - * Dump region sampler - */ + /** Dump region sampler */ fun dump(pw: PrintWriter) { regionSampler?.dump(pw) } init { if (regionSamplingEnabled && sampledView != null) { - regionSampler = createRegionSamplingHelper(sampledView, + regionSampler = + createRegionSamplingHelper( + sampledView, object : SamplingCallback { override fun onRegionDarknessChanged(isRegionDark: Boolean) { regionDarkness = convertToClockDarkness(isRegionDark) - updateFun.updateColors() + updateFun() } /** - * The method getLocationOnScreen is used to obtain the view coordinates - * relative to its left and top edges on the device screen. - * Directly accessing the X and Y coordinates of the view returns the - * location relative to its parent view instead. - */ + * The method getLocationOnScreen is used to obtain the view coordinates + * relative to its left and top edges on the device screen. Directly + * accessing the X and Y coordinates of the view returns the location + * relative to its parent view instead. + */ override fun getSampledRegion(sampledView: View): Rect { val screenLocation = tmpScreenLocation sampledView.getLocationOnScreen(screenLocation) @@ -147,8 +131,13 @@ open class RegionSamplingInstance( override fun isSamplingEnabled(): Boolean { return regionSamplingEnabled } - }, mainExecutor, bgExecutor) + }, + mainExecutor, + bgExecutor + ) } regionSampler?.setWindowVisible(true) } } + +typealias UpdateColorCallback = () -> Unit diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 40a96b060bc0..d48d7ffcd824 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -34,25 +34,27 @@ import com.android.systemui.flags.Flags.DOZING_MIGRATION_1 import com.android.systemui.flags.Flags.REGION_SAMPLING import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.log.dagger.KeyguardClockLog import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.log.LogBuffer -import com.android.systemui.shared.regionsampling.RegionSamplingInstance +import com.android.systemui.shared.regionsampling.RegionSampler import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback import com.android.systemui.statusbar.policy.ConfigurationController -import java.io.PrintWriter -import java.util.Locale -import java.util.TimeZone -import java.util.concurrent.Executor -import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch +import java.io.PrintWriter +import java.util.Locale +import java.util.TimeZone +import java.util.concurrent.Executor +import javax.inject.Inject /** * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by @@ -69,14 +71,17 @@ open class ClockEventController @Inject constructor( private val context: Context, @Main private val mainExecutor: Executor, @Background private val bgExecutor: Executor, - @KeyguardClockLog private val logBuffer: LogBuffer, + @KeyguardClockLog private val logBuffer: LogBuffer?, private val featureFlags: FeatureFlags ) { var clock: ClockController? = null set(value) { field = value if (value != null) { - value.setLogBuffer(logBuffer) + if (logBuffer != null) { + value.setLogBuffer(logBuffer) + } + value.initialize(resources, dozeAmount, 0f) updateRegionSamplers(value) } @@ -139,21 +144,17 @@ open class ClockEventController @Inject constructor( bgExecutor: Executor?, regionSamplingEnabled: Boolean, updateColors: () -> Unit - ): RegionSamplingInstance { - return RegionSamplingInstance( + ): RegionSampler { + return RegionSampler( sampledView, mainExecutor, bgExecutor, regionSamplingEnabled, - object : RegionSamplingInstance.UpdateColorCallback { - override fun updateColors() { - updateColors() - } - }) + updateFun = { updateColors() } ) } - var smallRegionSampler: RegionSamplingInstance? = null - var largeRegionSampler: RegionSamplingInstance? = null + var smallRegionSampler: RegionSampler? = null + var largeRegionSampler: RegionSampler? = null private var smallClockIsDark = true private var largeClockIsDark = true @@ -161,6 +162,7 @@ open class ClockEventController @Inject constructor( private val configListener = object : ConfigurationController.ConfigurationListener { override fun onThemeChanged() { clock?.events?.onColorPaletteChanged(resources) + updateColors() } override fun onDensityOrFontScaleChanged() { @@ -186,8 +188,10 @@ open class ClockEventController @Inject constructor( private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() { override fun onKeyguardVisibilityChanged(visible: Boolean) { isKeyguardVisible = visible - if (!isKeyguardVisible) { - clock?.animations?.doze(if (isDozing) 1f else 0f) + if (!featureFlags.isEnabled(DOZING_MIGRATION_1)) { + if (!isKeyguardVisible) { + clock?.animations?.doze(if (isDozing) 1f else 0f) + } } } @@ -224,6 +228,7 @@ open class ClockEventController @Inject constructor( listenForDozing(this) if (featureFlags.isEnabled(DOZING_MIGRATION_1)) { listenForDozeAmountTransition(this) + listenForGoneToAodTransition(this) } else { listenForDozeAmount(this) } @@ -276,6 +281,22 @@ open class ClockEventController @Inject constructor( } } + /** + * When keyguard is displayed again after being gone, the clock must be reset to full + * dozing. + */ + @VisibleForTesting + internal fun listenForGoneToAodTransition(scope: CoroutineScope): Job { + return scope.launch { + keyguardTransitionInteractor.goneToAodTransition.filter { + it.transitionState == TransitionState.STARTED + }.collect { + dozeAmount = 1f + clock?.animations?.doze(dozeAmount) + } + } + } + @VisibleForTesting internal fun listenForDozing(scope: CoroutineScope): Job { return scope.launch { diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt index 46f3d4e5f6aa..32ce537ea25a 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt @@ -21,6 +21,7 @@ import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogLevel import com.android.systemui.plugins.log.LogLevel.DEBUG import com.android.systemui.plugins.log.LogLevel.ERROR +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.plugins.log.LogLevel.VERBOSE import com.android.systemui.plugins.log.LogLevel.WARNING import com.android.systemui.plugins.log.MessageInitializer @@ -50,6 +51,14 @@ class KeyguardLogger @Inject constructor(@KeyguardLog private val buffer: LogBuf buffer.log(TAG, DEBUG, messageInitializer, messagePrinter) } + fun v(msg: String, arg: Any) { + buffer.log(TAG, VERBOSE, { str1 = arg.toString() }, { "$msg: $str1" }) + } + + fun i(msg: String, arg: Any) { + buffer.log(TAG, INFO, { str1 = arg.toString() }, { "$msg: $str1" }) + } + // TODO: remove after b/237743330 is fixed fun logStatusBarCalculatedAlpha(alpha: Float) { debugLog({ double1 = alpha.toDouble() }, { "Calculated new alpha: $double1" }) diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index d9f44cdecf40..47ee71e8982f 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -43,6 +43,7 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.systemui.dagger.GlobalRootComponent; import com.android.systemui.dagger.SysUIComponent; import com.android.systemui.dump.DumpManager; +import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.util.NotificationChannels; import java.util.Comparator; @@ -137,7 +138,7 @@ public class SystemUIApplication extends Application implements if (mServicesStarted) { final int N = mServices.length; for (int i = 0; i < N; i++) { - mServices[i].onBootCompleted(); + notifyBootCompleted(mServices[i]); } } } @@ -256,7 +257,7 @@ public class SystemUIApplication extends Application implements for (i = 0; i < mServices.length; i++) { if (mBootCompleteCache.isBootComplete()) { - mServices[i].onBootCompleted(); + notifyBootCompleted(mServices[i]); } mDumpManager.registerDumpable(mServices[i].getClass().getName(), mServices[i]); @@ -267,7 +268,13 @@ public class SystemUIApplication extends Application implements mServicesStarted = true; } - private void timeInitialization(String clsName, Runnable init, TimingsTraceLog log, + private static void notifyBootCompleted(CoreStartable coreStartable) { + Trace.beginSection(coreStartable.getClass().getSimpleName() + ".onBootCompleted()"); + coreStartable.onBootCompleted(); + Trace.endSection(); + } + + private static void timeInitialization(String clsName, Runnable init, TimingsTraceLog log, String metricsPrefix) { long ti = System.currentTimeMillis(); log.traceBegin(metricsPrefix + " " + clsName); @@ -281,11 +288,13 @@ public class SystemUIApplication extends Application implements } } - private CoreStartable startAdditionalStartable(String clsName) { + private static CoreStartable startAdditionalStartable(String clsName) { CoreStartable startable; if (DEBUG) Log.d(TAG, "loading: " + clsName); try { + Trace.beginSection(clsName + ".newInstance()"); startable = (CoreStartable) Class.forName(clsName).newInstance(); + Trace.endSection(); } catch (ClassNotFoundException | IllegalAccessException | InstantiationException ex) { @@ -295,14 +304,19 @@ public class SystemUIApplication extends Application implements return startStartable(startable); } - private CoreStartable startStartable(String clsName, Provider<CoreStartable> provider) { + private static CoreStartable startStartable(String clsName, Provider<CoreStartable> provider) { if (DEBUG) Log.d(TAG, "loading: " + clsName); - return startStartable(provider.get()); + Trace.beginSection("Provider<" + clsName + ">.get()"); + CoreStartable startable = provider.get(); + Trace.endSection(); + return startStartable(startable); } - private CoreStartable startStartable(CoreStartable startable) { + private static CoreStartable startStartable(CoreStartable startable) { if (DEBUG) Log.d(TAG, "running: " + startable); + Trace.beginSection(startable.getClass().getSimpleName() + ".start()"); startable.start(); + Trace.endSection(); return startable; } @@ -340,11 +354,18 @@ public class SystemUIApplication extends Application implements @Override public void onConfigurationChanged(Configuration newConfig) { if (mServicesStarted) { - mSysUIComponent.getConfigurationController().onConfigurationChanged(newConfig); + ConfigurationController configController = mSysUIComponent.getConfigurationController(); + Trace.beginSection( + configController.getClass().getSimpleName() + ".onConfigurationChanged()"); + configController.onConfigurationChanged(newConfig); + Trace.endSection(); int len = mServices.length; for (int i = 0; i < len; i++) { if (mServices[i] != null) { + Trace.beginSection( + mServices[i].getClass().getSimpleName() + ".onConfigurationChanged()"); mServices[i].onConfigurationChanged(newConfig); + Trace.endSection(); } } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index a4d3399eeb5d..40b5b0ac0e7d 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -122,7 +122,8 @@ object Flags { * Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository * will occur in stages. This is one stage of many to come. */ - @JvmField val DOZING_MIGRATION_1 = UnreleasedFlag(213, teamfood = true) + // TODO(b/255607168): Tracking Bug + @JvmField val DOZING_MIGRATION_1 = UnreleasedFlag(213) @JvmField val NEW_ELLIPSE_DETECTION = UnreleasedFlag(214) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index b186ae0ceec4..6baaf5f10024 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -21,7 +21,10 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.common.shared.model.Position import com.android.systemui.dagger.SysUISingleton import com.android.systemui.doze.DozeHost +import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.keyguard.WakefulnessLifecycle.Wakefulness import com.android.systemui.keyguard.shared.model.StatusBarState +import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject @@ -89,6 +92,9 @@ interface KeyguardRepository { /** Observable for the [StatusBarState] */ val statusBarState: Flow<StatusBarState> + /** Observable for device wake/sleep state */ + val wakefulnessState: Flow<WakefulnessModel> + /** * Returns `true` if the keyguard is showing; `false` otherwise. * @@ -118,6 +124,7 @@ constructor( statusBarStateController: StatusBarStateController, private val keyguardStateController: KeyguardStateController, dozeHost: DozeHost, + wakefulnessLifecycle: WakefulnessLifecycle, ) : KeyguardRepository { private val _animateBottomAreaDozingTransitions = MutableStateFlow(false) override val animateBottomAreaDozingTransitions = @@ -207,6 +214,40 @@ constructor( awaitClose { statusBarStateController.removeCallback(callback) } } + override val wakefulnessState: Flow<WakefulnessModel> = conflatedCallbackFlow { + val callback = + object : WakefulnessLifecycle.Observer { + override fun onStartedWakingUp() { + trySendWithFailureLogging( + WakefulnessModel.STARTING_TO_WAKE, + TAG, + "Wakefulness: starting to wake" + ) + } + override fun onFinishedWakingUp() { + trySendWithFailureLogging(WakefulnessModel.AWAKE, TAG, "Wakefulness: awake") + } + override fun onStartedGoingToSleep() { + trySendWithFailureLogging( + WakefulnessModel.STARTING_TO_SLEEP, + TAG, + "Wakefulness: starting to sleep" + ) + } + override fun onFinishedGoingToSleep() { + trySendWithFailureLogging(WakefulnessModel.ASLEEP, TAG, "Wakefulness: asleep") + } + } + wakefulnessLifecycle.addObserver(callback) + trySendWithFailureLogging( + wakefulnessIntToObject(wakefulnessLifecycle.getWakefulness()), + TAG, + "initial wakefulness state" + ) + + awaitClose { wakefulnessLifecycle.removeObserver(callback) } + } + override fun setAnimateDozingTransitions(animate: Boolean) { _animateBottomAreaDozingTransitions.value = animate } @@ -228,6 +269,16 @@ constructor( } } + private fun wakefulnessIntToObject(@Wakefulness value: Int): WakefulnessModel { + return when (value) { + 0 -> WakefulnessModel.ASLEEP + 1 -> WakefulnessModel.STARTING_TO_WAKE + 2 -> WakefulnessModel.AWAKE + 3 -> WakefulnessModel.STARTING_TO_SLEEP + else -> throw IllegalArgumentException("Invalid Wakefulness value: $value") + } + } + companion object { private const val TAG = "KeyguardRepositoryImpl" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt index d15d7f25bbde..0c725208e22d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt @@ -22,4 +22,9 @@ import dagger.Module @Module interface KeyguardRepositoryModule { @Binds fun keyguardRepository(impl: KeyguardRepositoryImpl): KeyguardRepository + + @Binds + fun keyguardTransitionRepository( + impl: KeyguardTransitionRepositoryImpl + ): KeyguardTransitionRepository } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index ab25597b077c..e3d1a27dad2b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -29,27 +29,33 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import java.util.UUID import javax.inject.Inject +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter -@SysUISingleton -class KeyguardTransitionRepository @Inject constructor() { - /* - * Each transition between [KeyguardState]s will have an associated Flow. - * In order to collect these events, clients should call [transition]. - */ - private val _transitions = MutableStateFlow(TransitionStep()) - val transitions = _transitions.asStateFlow() - - /* Information about the active transition. */ - private var currentTransitionInfo: TransitionInfo? = null - /* - * When manual control of the transition is requested, a unique [UUID] is used as the handle - * to permit calls to [updateTransition] +/** + * The source of truth for all keyguard transitions. + * + * While the keyguard component is visible, it can undergo a number of transitions between different + * UI screens, such as AOD (Always-on Display), Bouncer, and others mentioned in [KeyguardState]. + * These UI elements should listen to events emitted by [transitions], to ensure a centrally + * coordinated experience. + * + * To create or modify logic that controls when and how transitions get created, look at + * [TransitionInteractor]. These interactors will call [startTransition] and [updateTransition] on + * this repository. + */ +interface KeyguardTransitionRepository { + /** + * All events regarding transitions, as they start, run, and complete. [TransitionStep#value] is + * a float between [0, 1] representing progress towards completion. If this is a user driven + * transition, that value may not be a monotonic progression, as the user may swipe in any + * direction. */ - private var updateTransitionId: UUID? = null + val transitions: Flow<TransitionStep> /** * Interactors that require information about changes between [KeyguardState]s will call this to @@ -60,22 +66,56 @@ class KeyguardTransitionRepository @Inject constructor() { } /** - * Begin a transition from one state to another. The [info.from] must match - * [currentTransitionInfo.to], or the request will be denied. This is enforced to avoid - * unplanned transitions. + * Begin a transition from one state to another. Will not start if another transition is in + * progress. + */ + fun startTransition(info: TransitionInfo): UUID? + + /** + * Allows manual control of a transition. When calling [startTransition], the consumer must pass + * in a null animator. In return, it will get a unique [UUID] that will be validated to allow + * further updates. + * + * When the transition is over, TransitionState.FINISHED must be passed into the [state] + * parameter. + */ + fun updateTransition( + transitionId: UUID, + @FloatRange(from = 0.0, to = 1.0) value: Float, + state: TransitionState + ) +} + +@SysUISingleton +class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitionRepository { + /* + * Each transition between [KeyguardState]s will have an associated Flow. + * In order to collect these events, clients should call [transition]. + */ + private val _transitions = + MutableSharedFlow<TransitionStep>( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + override val transitions = _transitions.asSharedFlow().distinctUntilChanged() + private var lastStep: TransitionStep = TransitionStep() + + /* + * When manual control of the transition is requested, a unique [UUID] is used as the handle + * to permit calls to [updateTransition] */ - fun startTransition(info: TransitionInfo): UUID? { - if (currentTransitionInfo != null) { + private var updateTransitionId: UUID? = null + + override fun startTransition(info: TransitionInfo): UUID? { + if (lastStep.transitionState != TransitionState.FINISHED) { // Open questions: // * Queue of transitions? buffer of 1? // * Are transitions cancellable if a new one is triggered? // * What validation does this need to do? - Log.wtf(TAG, "Transition still active: $currentTransitionInfo") + Log.wtf(TAG, "Transition still active: $lastStep") return null } - currentTransitionInfo?.animator?.cancel() - currentTransitionInfo = info info.animator?.let { animator -> // An animator was provided, so use it to run the transition animator.setFloatValues(0f, 1f) @@ -83,24 +123,24 @@ class KeyguardTransitionRepository @Inject constructor() { object : AnimatorUpdateListener { override fun onAnimationUpdate(animation: ValueAnimator) { emitTransition( - info, - (animation.getAnimatedValue() as Float), - TransitionState.RUNNING + TransitionStep( + info, + (animation.getAnimatedValue() as Float), + TransitionState.RUNNING + ) ) } } val adapter = object : AnimatorListenerAdapter() { override fun onAnimationStart(animation: Animator) { - Log.i(TAG, "Starting transition: $info") - emitTransition(info, 0f, TransitionState.STARTED) + emitTransition(TransitionStep(info, 0f, TransitionState.STARTED)) } override fun onAnimationCancel(animation: Animator) { Log.i(TAG, "Cancelling transition: $info") } override fun onAnimationEnd(animation: Animator) { - Log.i(TAG, "Ending transition: $info") - emitTransition(info, 1f, TransitionState.FINISHED) + emitTransition(TransitionStep(info, 1f, TransitionState.FINISHED)) animator.removeListener(this) animator.removeUpdateListener(updateListener) } @@ -111,8 +151,7 @@ class KeyguardTransitionRepository @Inject constructor() { return@startTransition null } ?: run { - Log.i(TAG, "Starting transition (manual): $info") - emitTransition(info, 0f, TransitionState.STARTED) + emitTransition(TransitionStep(info, 0f, TransitionState.STARTED)) // No animator, so it's manual. Provide a mechanism to callback updateTransitionId = UUID.randomUUID() @@ -120,15 +159,7 @@ class KeyguardTransitionRepository @Inject constructor() { } } - /** - * Allows manual control of a transition. When calling [startTransition], the consumer must pass - * in a null animator. In return, it will get a unique [UUID] that will be validated to allow - * further updates. - * - * When the transition is over, TransitionState.FINISHED must be passed into the [state] - * parameter. - */ - fun updateTransition( + override fun updateTransition( transitionId: UUID, @FloatRange(from = 0.0, to = 1.0) value: Float, state: TransitionState @@ -138,52 +169,41 @@ class KeyguardTransitionRepository @Inject constructor() { return } - if (currentTransitionInfo == null) { - Log.wtf(TAG, "Attempting to update with null 'currentTransitionInfo'") - return + if (state == TransitionState.FINISHED) { + updateTransitionId = null } - currentTransitionInfo?.let { info -> - if (state == TransitionState.FINISHED) { - updateTransitionId = null - Log.i(TAG, "Ending transition: $info") - } - - emitTransition(info, value, state) - } + val nextStep = lastStep.copy(value = value, transitionState = state) + emitTransition(nextStep, isManual = true) } - private fun emitTransition( - info: TransitionInfo, - value: Float, - transitionState: TransitionState - ) { - trace(info, transitionState) - - if (transitionState == TransitionState.FINISHED) { - currentTransitionInfo = null + private fun emitTransition(nextStep: TransitionStep, isManual: Boolean = false) { + trace(nextStep, isManual) + val emitted = _transitions.tryEmit(nextStep) + if (!emitted) { + Log.w(TAG, "Failed to emit next value without suspending") } - _transitions.value = TransitionStep(info.from, info.to, value, transitionState) + lastStep = nextStep } - private fun trace(info: TransitionInfo, transitionState: TransitionState) { + private fun trace(step: TransitionStep, isManual: Boolean) { if ( - transitionState != TransitionState.STARTED && - transitionState != TransitionState.FINISHED + step.transitionState != TransitionState.STARTED && + step.transitionState != TransitionState.FINISHED ) { return } val traceName = - "Transition: ${info.from} -> ${info.to} " + - if (info.animator == null) { + "Transition: ${step.from} -> ${step.to} " + + if (isManual) { "(manual)" } else { "" } val traceCookie = traceName.hashCode() - if (transitionState == TransitionState.STARTED) { + if (step.transitionState == TransitionState.STARTED) { Trace.beginAsyncSection(traceName, traceCookie) - } else if (transitionState == TransitionState.FINISHED) { + } else if (step.transitionState == TransitionState.FINISHED) { Trace.endAsyncSection(traceName, traceCookie) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt index 400376663f1a..0aeff7fc69fd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt @@ -24,6 +24,7 @@ import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect @@ -36,31 +37,35 @@ constructor( @Application private val scope: CoroutineScope, private val keyguardRepository: KeyguardRepository, private val keyguardTransitionRepository: KeyguardTransitionRepository, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, ) : TransitionInteractor("AOD<->LOCKSCREEN") { override fun start() { scope.launch { - keyguardRepository.isDozing.collect { isDozing -> - if (isDozing) { - keyguardTransitionRepository.startTransition( - TransitionInfo( - name, - KeyguardState.LOCKSCREEN, - KeyguardState.AOD, - getAnimator(), + keyguardRepository.isDozing + .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) }) + .collect { pair -> + val (isDozing, keyguardState) = pair + if (isDozing && keyguardState == KeyguardState.LOCKSCREEN) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.LOCKSCREEN, + KeyguardState.AOD, + getAnimator(), + ) ) - ) - } else { - keyguardTransitionRepository.startTransition( - TransitionInfo( - name, - KeyguardState.AOD, - KeyguardState.LOCKSCREEN, - getAnimator(), + } else if (!isDozing && keyguardState == KeyguardState.AOD) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.AOD, + KeyguardState.LOCKSCREEN, + getAnimator(), + ) ) - ) + } } - } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt new file mode 100644 index 000000000000..0e2a54c57bdb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt @@ -0,0 +1,76 @@ +/* + * 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.domain.interactor + +import android.animation.ValueAnimator +import com.android.systemui.animation.Interpolators +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.WakefulnessModel +import com.android.systemui.util.kotlin.sample +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +@SysUISingleton +class GoneAodTransitionInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val keyguardInteractor: KeyguardInteractor, + private val keyguardTransitionRepository: KeyguardTransitionRepository, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, +) : TransitionInteractor("GONE->AOD") { + + override fun start() { + scope.launch { + keyguardInteractor.wakefulnessState + .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) }) + .collect { pair -> + val (wakefulnessState, keyguardState) = pair + if ( + keyguardState == KeyguardState.GONE && + wakefulnessState == WakefulnessModel.STARTING_TO_SLEEP + ) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.GONE, + KeyguardState.AOD, + getAnimator(), + ) + ) + } + } + } + } + + private fun getAnimator(): ValueAnimator { + return ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(TRANSITION_DURATION_MS) + } + } + + companion object { + private const val TRANSITION_DURATION_MS = 500L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index fc2269c6b01c..03c6a789326e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.keyguard.shared.model.WakefulnessModel import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -40,6 +41,8 @@ constructor( val isDozing: Flow<Boolean> = repository.isDozing /** Whether the keyguard is showing to not. */ val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing + /** The device wake/sleep state */ + val wakefulnessState: Flow<WakefulnessModel> = repository.wakefulnessState fun isKeyguardShowing(): Boolean { return repository.isKeyguardShowing() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt new file mode 100644 index 000000000000..83d94325b9d4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt @@ -0,0 +1,51 @@ +/* + * 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.domain.interactor + +import com.android.keyguard.logging.KeyguardLogger +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +/** Collect flows of interest for auditing keyguard transitions. */ +@SysUISingleton +class KeyguardTransitionAuditLogger +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val interactor: KeyguardTransitionInteractor, + private val keyguardInteractor: KeyguardInteractor, + private val logger: KeyguardLogger, +) { + + fun start() { + scope.launch { + keyguardInteractor.wakefulnessState.collect { logger.v("WakefulnessState", it) } + } + + scope.launch { + interactor.finishedKeyguardState.collect { logger.i("Finished transition to", it) } + } + + scope.launch { + interactor.startedKeyguardState.collect { logger.i("Started transition to", it) } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt index b166681433a8..d5ea77b8e729 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt @@ -26,6 +26,7 @@ class KeyguardTransitionCoreStartable @Inject constructor( private val interactors: Set<TransitionInteractor>, + private val auditLogger: KeyguardTransitionAuditLogger, ) : CoreStartable { override fun start() { @@ -38,9 +39,12 @@ constructor( when (it) { is LockscreenBouncerTransitionInteractor -> Log.d(TAG, "Started $it") is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it") + is GoneAodTransitionInteractor -> Log.d(TAG, "Started $it") + is LockscreenGoneTransitionInteractor -> Log.d(TAG, "Started $it") } it.start() } + auditLogger.start() } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 7409aec57b4c..dffd097a77c5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -19,11 +19,15 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge @@ -40,6 +44,9 @@ constructor( /** LOCKSCREEN->AOD transition information. */ val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD) + /** GONE->AOD information. */ + val goneToAodTransition: Flow<TransitionStep> = repository.transition(GONE, AOD) + /** * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <-> * Lockscreen (0f). @@ -49,4 +56,16 @@ constructor( aodToLockscreenTransition.map { step -> step.copy(value = 1f - step.value) }, lockscreenToAodTransition, ) + + /* The last completed [KeyguardState] transition */ + val finishedKeyguardState: Flow<KeyguardState> = + repository.transitions + .filter { step -> step.transitionState == TransitionState.FINISHED } + .map { step -> step.to } + + /* The last started [KeyguardState] transition */ + val startedKeyguardState: Flow<KeyguardState> = + repository.transitions + .filter { step -> step.transitionState == TransitionState.STARTED } + .map { step -> step.to } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt index 3c2a12e3836a..761f3fd9d9f9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt @@ -29,6 +29,7 @@ import com.android.systemui.util.kotlin.sample import java.util.UUID import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @@ -40,59 +41,63 @@ constructor( private val keyguardRepository: KeyguardRepository, private val shadeRepository: ShadeRepository, private val keyguardTransitionRepository: KeyguardTransitionRepository, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor ) : TransitionInteractor("LOCKSCREEN<->BOUNCER") { private var transitionId: UUID? = null override fun start() { scope.launch { - shadeRepository.shadeModel.sample( - combine( - keyguardTransitionRepository.transitions, - keyguardRepository.statusBarState, - ) { transitions, statusBarState -> - Pair(transitions, statusBarState) - } - ) { shadeModel, pair -> - val (transitions, statusBarState) = pair + shadeRepository.shadeModel + .sample( + combine( + keyguardTransitionInteractor.finishedKeyguardState, + keyguardRepository.statusBarState, + ) { keyguardState, statusBarState -> + Pair(keyguardState, statusBarState) + }, + { shadeModel, pair -> Triple(shadeModel, pair.first, pair.second) } + ) + .collect { triple -> + val (shadeModel, keyguardState, statusBarState) = triple - val id = transitionId - if (id != null) { - // An existing `id` means a transition is started, and calls to - // `updateTransition` will control it until FINISHED - keyguardTransitionRepository.updateTransition( - id, - shadeModel.expansionAmount, - if (shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f) { - transitionId = null - TransitionState.FINISHED - } else { - TransitionState.RUNNING - } - ) - } else { - // TODO (b/251849525): Remove statusbarstate check when that state is integrated - // into KeyguardTransitionRepository - val isOnLockscreen = - transitions.transitionState == TransitionState.FINISHED && - transitions.to == KeyguardState.LOCKSCREEN - if ( - isOnLockscreen && - shadeModel.isUserDragging && - statusBarState != SHADE_LOCKED - ) { - transitionId = - keyguardTransitionRepository.startTransition( - TransitionInfo( - ownerName = name, - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.BOUNCER, - animator = null, + val id = transitionId + if (id != null) { + // An existing `id` means a transition is started, and calls to + // `updateTransition` will control it until FINISHED + keyguardTransitionRepository.updateTransition( + id, + shadeModel.expansionAmount, + if ( + shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f + ) { + transitionId = null + TransitionState.FINISHED + } else { + TransitionState.RUNNING + } + ) + } else { + // TODO (b/251849525): Remove statusbarstate check when that state is + // integrated + // into KeyguardTransitionRepository + if ( + keyguardState == KeyguardState.LOCKSCREEN && + shadeModel.isUserDragging && + statusBarState != SHADE_LOCKED + ) { + transitionId = + keyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = name, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.BOUNCER, + animator = null, + ) ) - ) + } } } - } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt new file mode 100644 index 000000000000..6c1adbd68ef2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt @@ -0,0 +1,67 @@ +/* + * 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.domain.interactor + +import android.animation.ValueAnimator +import com.android.systemui.animation.Interpolators +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +@SysUISingleton +class LockscreenGoneTransitionInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val keyguardInteractor: KeyguardInteractor, + private val keyguardTransitionRepository: KeyguardTransitionRepository, +) : TransitionInteractor("LOCKSCREEN->GONE") { + + override fun start() { + scope.launch { + keyguardInteractor.isKeyguardShowing.collect { isShowing -> + if (!isShowing) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.LOCKSCREEN, + KeyguardState.GONE, + getAnimator(), + ) + ) + } + } + } + } + + private fun getAnimator(): ValueAnimator { + return ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(TRANSITION_DURATION_MS) + } + } + + companion object { + private const val TRANSITION_DURATION_MS = 10L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt index 74c542c0043f..728bafae2a4c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt @@ -39,4 +39,10 @@ abstract class StartKeyguardTransitionModule { @Binds @IntoSet abstract fun aodLockscreen(impl: AodLockscreenTransitionInteractor): TransitionInteractor + + @Binds @IntoSet abstract fun goneAod(impl: GoneAodTransitionInteractor): TransitionInteractor + + @Binds + @IntoSet + abstract fun lockscreenGone(impl: LockscreenGoneTransitionInteractor): TransitionInteractor } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt index f66d5d3650c8..7958033ba017 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt @@ -17,7 +17,10 @@ package com.android.systemui.keyguard.shared.model /** List of all possible states to transition to/from */ enum class KeyguardState { - /** For initialization only */ + /** + * For initialization as well as when the security method is set to NONE, indicating that + * the keyguard should never be shown. + */ NONE, /* Always-on Display. The device is in a low-power mode with a minimal UI visible */ AOD, @@ -31,4 +34,11 @@ enum class KeyguardState { * unlocked if SWIPE security method is used, or if face lockscreen bypass is false. */ LOCKSCREEN, + + /* + * Keyguard is no longer visible. In most cases the user has just authenticated and keyguard + * is being removed, but there are other cases where the user is swiping away keyguard, such as + * with SWIPE security method or face unlock without bypass. + */ + GONE, } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt index d8691c17f53d..0e0465bb5207 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt @@ -17,7 +17,6 @@ package com.android.systemui.keyguard.shared.model /** Possible states for a running transition between [State] */ enum class TransitionState { - NONE, STARTED, RUNNING, FINISHED diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt index 688ec912aac8..0ca358210813 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt @@ -20,5 +20,11 @@ data class TransitionStep( val from: KeyguardState = KeyguardState.NONE, val to: KeyguardState = KeyguardState.NONE, val value: Float = 0f, // constrained [0.0, 1.0] - val transitionState: TransitionState = TransitionState.NONE, -) + val transitionState: TransitionState = TransitionState.FINISHED, +) { + constructor( + info: TransitionInfo, + value: Float, + transitionState: TransitionState, + ) : this(info.from, info.to, value, transitionState) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt new file mode 100644 index 000000000000..64f834d6c5ab --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.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.model + +/** Model device wakefulness states. */ +enum class WakefulnessModel { + /** The device is asleep and not interactive. */ + ASLEEP, + /** Received a signal that the device is beginning to wake up. */ + STARTING_TO_WAKE, + /** Device is now fully awake and interactive. */ + AWAKE, + /** Signal that the device is now going to sleep. */ + STARTING_TO_SLEEP, +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt index ed649b1c0265..f006442906e7 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt @@ -181,6 +181,7 @@ constructor( return enabled == other.enabled && name == other.name && intent == other.intent && - id == other.id + id == other.id && + showBroadcastButton == other.showBroadcastButton } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 1c0f05745280..d584fa9895fd 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -41,7 +41,6 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_N import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE; -import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED; import static com.android.systemui.statusbar.VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL; import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD; @@ -1954,7 +1953,7 @@ public final class NotificationPanelViewController { public void closeQs() { cancelQsAnimation(); - setQsExpansion(mQsMinExpansionHeight); + setQsExpansionHeight(mQsMinExpansionHeight); } @VisibleForTesting @@ -1992,7 +1991,7 @@ public final class NotificationPanelViewController { } float height = mQsExpansionHeight; mQsExpansionAnimator.cancel(); - setQsExpansion(height); + setQsExpansionHeight(height); } flingSettings(0 /* vel */, animateAway ? FLING_HIDE : FLING_COLLAPSE); } @@ -2205,7 +2204,7 @@ public final class NotificationPanelViewController { // Already tracking because onOverscrolled was called. We need to update here // so we don't stop for a frame until the next touch event gets handled in // onTouchEvent. - setQsExpansion(h + mInitialHeightOnTouch); + setQsExpansionHeight(h + mInitialHeightOnTouch); trackMovement(event); return true; } else { @@ -2616,7 +2615,7 @@ public final class NotificationPanelViewController { case MotionEvent.ACTION_MOVE: if (DEBUG_LOGCAT) Log.d(TAG, "onQSTouch move"); mShadeLog.logMotionEvent(event, "onQsTouch: move action, setting QS expansion"); - setQsExpansion(h + mInitialHeightOnTouch); + setQsExpansionHeight(h + mInitialHeightOnTouch); if (h >= getFalsingThreshold()) { mQsTouchAboveFalsingThreshold = true; } @@ -2663,7 +2662,7 @@ public final class NotificationPanelViewController { // Reset scroll position and apply that position to the expanded height. float height = mQsExpansionHeight; - setQsExpansion(height); + setQsExpansionHeight(height); updateExpandedHeightToMaxHeight(); mNotificationStackScrollLayoutController.checkSnoozeLeavebehind(); @@ -2735,7 +2734,7 @@ public final class NotificationPanelViewController { mQs.setExpanded(mQsExpanded); } - void setQsExpansion(float height) { + void setQsExpansionHeight(float height) { height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight); mQsFullyExpanded = height == mQsMaxExpansionHeight && mQsMaxExpansionHeight != 0; boolean qsAnimatingAway = !mQsAnimatorExpand && mAnimatingQS; @@ -3157,12 +3156,13 @@ public final class NotificationPanelViewController { delay); mIsQsTranslationResetAnimator = mQsTranslationForFullShadeTransition > 0.0f; } - - if (mSplitShadeEnabled) { - updateQsExpansionForLockscreenToShadeTransition(pxAmount); - } float endPosition = 0; if (pxAmount > 0.0f) { + if (mSplitShadeEnabled) { + float qsHeight = MathUtils.lerp(mQsMinExpansionHeight, mQsMaxExpansionHeight, + mLockscreenShadeTransitionController.getQSDragProgress()); + setQsExpansionHeight(qsHeight); + } if (mNotificationStackScrollLayoutController.getVisibleNotificationCount() == 0 && !mMediaDataManager.hasActiveMediaOrRecommendation()) { // No notifications are visible, let's animate to the height of qs instead @@ -3200,18 +3200,6 @@ public final class NotificationPanelViewController { updateQsExpansion(); } - private void updateQsExpansionForLockscreenToShadeTransition(float pxAmount) { - float qsExpansion = 0; - if (pxAmount > 0.0f) { - qsExpansion = MathUtils.lerp(mQsMinExpansionHeight, mQsMaxExpansionHeight, - mLockscreenShadeTransitionController.getQSDragProgress()); - } - // SHADE_LOCKED means transition is over and we don't want further updates - if (mBarState != SHADE_LOCKED) { - setQsExpansion(qsExpansion); - } - } - /** * Notify the panel that the pulse expansion has finished and that we're going to the full * shade @@ -3329,7 +3317,7 @@ public final class NotificationPanelViewController { animator.setDuration(350); } animator.addUpdateListener( - animation -> setQsExpansion((Float) animation.getAnimatedValue())); + animation -> setQsExpansionHeight((Float) animation.getAnimatedValue())); animator.addListener(new AnimatorListenerAdapter() { private boolean mIsCanceled; @@ -3470,7 +3458,7 @@ public final class NotificationPanelViewController { } float targetHeight = mQsMinExpansionHeight + qsExpansionFraction * (mQsMaxExpansionHeight - mQsMinExpansionHeight); - setQsExpansion(targetHeight); + setQsExpansionHeight(targetHeight); } updateExpandedHeight(expandedHeight); updateHeader(); @@ -4729,8 +4717,6 @@ public final class NotificationPanelViewController { private void startOpening(MotionEvent event) { updatePanelExpansionAndVisibility(); - // Reset at start so haptic can be triggered as soon as panel starts to open. - mHasVibratedOnOpen = false; //TODO: keyguard opens QS a different way; log that too? // Log the position of the swipe that opened the panel @@ -5035,7 +5021,7 @@ public final class NotificationPanelViewController { } public boolean isFullyExpanded() { - return mExpandedHeight >= getMaxPanelHeight(); + return mExpandedHeight >= getMaxPanelTransitionDistance(); } public boolean isFullyCollapsed() { @@ -5341,7 +5327,7 @@ public final class NotificationPanelViewController { mQsExpansionFromOverscroll = rounded != 0f; mLastOverscroll = rounded; updateQsState(); - setQsExpansion(mQsMinExpansionHeight + rounded); + setQsExpansionHeight(mQsMinExpansionHeight + rounded); } @Override @@ -5358,7 +5344,7 @@ public final class NotificationPanelViewController { // make sure we can expand setOverScrolling(false); } - setQsExpansion(mQsExpansionHeight); + setQsExpansionHeight(mQsExpansionHeight); boolean canExpand = isQsExpansionEnabled(); flingSettings(!canExpand && open ? 0f : velocity, open && canExpand ? FLING_EXPAND : FLING_COLLAPSE, () -> { @@ -5551,7 +5537,7 @@ public final class NotificationPanelViewController { } } else { // this else branch means we are doing one of: - // - from KEYGUARD and SHADE (but not expanded shade) + // - from KEYGUARD to SHADE (but not fully expanded as when swiping from the top) // - from SHADE to KEYGUARD // - from SHADE_LOCKED to SHADE // - getting notified again about the current SHADE or KEYGUARD state @@ -5720,7 +5706,7 @@ public final class NotificationPanelViewController { startQsSizeChangeAnimation(oldMaxHeight, mQsMaxExpansionHeight); } } else if (!mQsExpanded && mQsExpansionAnimator == null) { - setQsExpansion(mQsMinExpansionHeight + mLastOverscroll); + setQsExpansionHeight(mQsMinExpansionHeight + mLastOverscroll); } else { mShadeLog.v("onLayoutChange: qs expansion not set"); } @@ -6214,6 +6200,10 @@ public final class NotificationPanelViewController { } break; case MotionEvent.ACTION_MOVE: + if (isFullyCollapsed()) { + // If panel is fully collapsed, reset haptic effect before adding movement. + mHasVibratedOnOpen = false; + } addMovement(event); if (!isFullyCollapsed()) { maybeVibrateOnOpening(true /* openingWithTouch */); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt index fc984618f1b0..58738377a3db 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -48,7 +48,8 @@ import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.settings.UserTracker -import com.android.systemui.shared.regionsampling.RegionSamplingInstance +import com.android.systemui.shared.regionsampling.RegionSampler +import com.android.systemui.shared.regionsampling.UpdateColorCallback import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.DeviceProvisionedController @@ -90,8 +91,8 @@ class LockscreenSmartspaceController @Inject constructor( // Smartspace can be used on multiple displays, such as when the user casts their screen private var smartspaceViews = mutableSetOf<SmartspaceView>() - private var regionSamplingInstances = - mutableMapOf<SmartspaceView, RegionSamplingInstance>() + private var regionSamplers = + mutableMapOf<SmartspaceView, RegionSampler>() private val regionSamplingEnabled = featureFlags.isEnabled(Flags.REGION_SAMPLING) @@ -101,27 +102,23 @@ class LockscreenSmartspaceController @Inject constructor( private var showSensitiveContentForManagedUser = false private var managedUserHandle: UserHandle? = null - private val updateFun = object : RegionSamplingInstance.UpdateColorCallback { - override fun updateColors() { - updateTextColorFromRegionSampler() - } - } + private val updateFun: UpdateColorCallback = { updateTextColorFromRegionSampler() } // TODO: Move logic into SmartspaceView var stateChangeListener = object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { smartspaceViews.add(v as SmartspaceView) - var regionSamplingInstance = RegionSamplingInstance( + var regionSampler = RegionSampler( v, uiExecutor, bgExecutor, regionSamplingEnabled, updateFun ) - initializeTextColors(regionSamplingInstance) - regionSamplingInstance.startRegionSampler() - regionSamplingInstances.put(v, regionSamplingInstance) + initializeTextColors(regionSampler) + regionSampler.startRegionSampler() + regionSamplers.put(v, regionSampler) connectSession() updateTextColorFromWallpaper() @@ -131,9 +128,9 @@ class LockscreenSmartspaceController @Inject constructor( override fun onViewDetachedFromWindow(v: View) { smartspaceViews.remove(v as SmartspaceView) - var regionSamplingInstance = regionSamplingInstances.getValue(v) - regionSamplingInstance.stopRegionSampler() - regionSamplingInstances.remove(v) + var regionSampler = regionSamplers.getValue(v) + regionSampler.stopRegionSampler() + regionSamplers.remove(v) if (smartspaceViews.isEmpty()) { disconnect() @@ -363,19 +360,19 @@ class LockscreenSmartspaceController @Inject constructor( } } - private fun initializeTextColors(regionSamplingInstance: RegionSamplingInstance) { + private fun initializeTextColors(regionSampler: RegionSampler) { val lightThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_LightWallpaper) val darkColor = Utils.getColorAttrDefaultColor(lightThemeContext, R.attr.wallpaperTextColor) val darkThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI) val lightColor = Utils.getColorAttrDefaultColor(darkThemeContext, R.attr.wallpaperTextColor) - regionSamplingInstance.setForegroundColors(lightColor, darkColor) + regionSampler.setForegroundColors(lightColor, darkColor) } private fun updateTextColorFromRegionSampler() { smartspaceViews.forEach { - val textColor = regionSamplingInstances.getValue(it).currentForegroundColor() + val textColor = regionSamplers.getValue(it).currentForegroundColor() it.setPrimaryTextColor(textColor) } } diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index 3ecb15b9d79c..5894fd385ee3 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -115,7 +115,6 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { private final SecureSettings mSecureSettings; private final Executor mMainExecutor; private final Handler mBgHandler; - private final boolean mIsMonochromaticEnabled; private final Context mContext; private final boolean mIsMonetEnabled; private final UserTracker mUserTracker; @@ -365,7 +364,6 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { UserTracker userTracker, DumpManager dumpManager, FeatureFlags featureFlags, @Main Resources resources, WakefulnessLifecycle wakefulnessLifecycle) { mContext = context; - mIsMonochromaticEnabled = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEMES); mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET); mDeviceProvisionedController = deviceProvisionedController; mBroadcastDispatcher = broadcastDispatcher; @@ -669,11 +667,8 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { // used as a system-wide theme. // - Content intentionally excluded, intended for media player, not system-wide List<Style> validStyles = new ArrayList<>(Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, - Style.TONAL_SPOT, Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT)); - - if (mIsMonochromaticEnabled) { - validStyles.add(Style.MONOCHROMATIC); - } + Style.TONAL_SPOT, Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT, + Style.MONOCHROMATIC)); Style style = mThemeStyle; final String overlayPackageJson = mSecureSettings.getStringForUser( diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index b16dc5403a57..6a2326036ec0 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -62,6 +62,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext /** @@ -136,7 +137,7 @@ constructor( private val isNewImpl: Boolean get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) - private val _userSwitcherSettings = MutableStateFlow<UserSwitcherSettingsModel?>(null) + private val _userSwitcherSettings = MutableStateFlow(runBlocking { getSettings() }) override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> = _userSwitcherSettings.asStateFlow().filterNotNull() @@ -235,7 +236,7 @@ constructor( } override fun isSimpleUserSwitcher(): Boolean { - return checkNotNull(_userSwitcherSettings.value?.isSimpleUserSwitcher) + return _userSwitcherSettings.value.isSimpleUserSwitcher } private fun observeSelectedUser() { diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt index 07e5cf9d9df2..f9d14cd1b7ca 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt @@ -208,7 +208,12 @@ constructor( if (newGuestId == UserHandle.USER_NULL) { Log.e(TAG, "Could not create new guest, switching back to system user") switchUser(UserHandle.USER_SYSTEM) - withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) } + withContext(backgroundDispatcher) { + manager.removeUserWhenPossible( + UserHandle.of(currentUser.id), + /* overrideDevicePolicy= */ false + ) + } try { WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null) } catch (e: RemoteException) { @@ -222,13 +227,21 @@ constructor( switchUser(newGuestId) - withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) } + withContext(backgroundDispatcher) { + manager.removeUserWhenPossible( + UserHandle.of(currentUser.id), + /* overrideDevicePolicy= */ false + ) + } } else { if (repository.isGuestUserAutoCreated) { repository.isGuestUserResetting = true } switchUser(targetUserId) - manager.removeUser(currentUser.id) + manager.removeUserWhenPossible( + UserHandle.of(currentUser.id), + /* overrideDevicePolicy= */ false + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt index 968af59e6c45..ad09ee3c10d9 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt @@ -61,14 +61,15 @@ object UserSwitcherViewBinder { falsingCollector: FalsingCollector, onFinish: () -> Unit, ) { - val rootView: UserSwitcherRootView = view.requireViewById(R.id.user_switcher_root) - val flowWidget: FlowWidget = view.requireViewById(R.id.flow) + val gridContainerView: UserSwitcherRootView = + view.requireViewById(R.id.user_switcher_grid_container) + val flowWidget: FlowWidget = gridContainerView.requireViewById(R.id.flow) val addButton: View = view.requireViewById(R.id.add) val cancelButton: View = view.requireViewById(R.id.cancel) val popupMenuAdapter = MenuAdapter(layoutInflater) var popupMenu: UserSwitcherPopupMenu? = null - rootView.touchHandler = + gridContainerView.touchHandler = object : Gefingerpoken { override fun onTouchEvent(ev: MotionEvent?): Boolean { falsingCollector.onTouchEvent(ev) @@ -134,7 +135,7 @@ object UserSwitcherViewBinder { val viewPool = view.children.filter { it.tag == USER_VIEW_TAG }.toMutableList() viewPool.forEach { - view.removeView(it) + gridContainerView.removeView(it) flowWidget.removeView(it) } users.forEach { userViewModel -> @@ -152,7 +153,7 @@ object UserSwitcherViewBinder { inflatedView } userView.id = View.generateViewId() - view.addView(userView) + gridContainerView.addView(userView) flowWidget.addView(userView) UserViewBinder.bind( view = userView, diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java index 44f6d03207b1..ad97ef4a79bc 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java @@ -31,6 +31,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.SystemClock; import android.os.Trace; +import android.os.UserHandle; import android.service.wallpaper.WallpaperService; import android.util.ArraySet; import android.util.Log; @@ -598,7 +599,6 @@ public class ImageWallpaper extends WallpaperService { getDisplayContext().getSystemService(DisplayManager.class) .unregisterDisplayListener(this); mWallpaperLocalColorExtractor.cleanUp(); - unloadBitmap(); } @Override @@ -676,9 +676,14 @@ public class ImageWallpaper extends WallpaperService { void drawFrameOnCanvas(Bitmap bitmap) { Trace.beginSection("ImageWallpaper.CanvasEngine#drawFrame"); Surface surface = mSurfaceHolder.getSurface(); - Canvas canvas = mWideColorGamut - ? surface.lockHardwareWideColorGamutCanvas() - : surface.lockHardwareCanvas(); + Canvas canvas = null; + try { + canvas = mWideColorGamut + ? surface.lockHardwareWideColorGamutCanvas() + : surface.lockHardwareCanvas(); + } catch (IllegalStateException e) { + Log.w(TAG, "Unable to lock canvas", e); + } if (canvas != null) { Rect dest = mSurfaceHolder.getSurfaceFrame(); try { @@ -709,17 +714,6 @@ public class ImageWallpaper extends WallpaperService { } } - private void unloadBitmap() { - mBackgroundExecutor.execute(this::unloadBitmapSynchronized); - } - - private void unloadBitmapSynchronized() { - synchronized (mLock) { - mBitmapUsages = 0; - unloadBitmapInternal(); - } - } - private void unloadBitmapInternal() { Trace.beginSection("ImageWallpaper.CanvasEngine#unloadBitmap"); if (mBitmap != null) { @@ -738,7 +732,7 @@ public class ImageWallpaper extends WallpaperService { boolean loadSuccess = false; Bitmap bitmap; try { - bitmap = mWallpaperManager.getBitmap(false); + bitmap = mWallpaperManager.getBitmapAsUser(UserHandle.USER_CURRENT, false); if (bitmap != null && bitmap.getByteCount() > RecordingCanvas.MAX_BITMAP_SIZE) { throw new RuntimeException("Wallpaper is too large to draw!"); @@ -757,7 +751,7 @@ public class ImageWallpaper extends WallpaperService { } try { - bitmap = mWallpaperManager.getBitmap(false); + bitmap = mWallpaperManager.getBitmapAsUser(UserHandle.USER_CURRENT, false); } catch (RuntimeException | OutOfMemoryError e) { Log.w(TAG, "Unable to load default wallpaper!", e); bitmap = null; @@ -770,9 +764,6 @@ public class ImageWallpaper extends WallpaperService { Log.e(TAG, "Attempt to load a recycled bitmap"); } else if (mBitmap == bitmap) { Log.e(TAG, "Loaded a bitmap that was already loaded"); - } else if (bitmap.getWidth() < 1 || bitmap.getHeight() < 1) { - Log.e(TAG, "Attempt to load an invalid wallpaper of length " - + bitmap.getWidth() + "x" + bitmap.getHeight()); } else { // at this point, loading is done correctly. loadSuccess = true; diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index 1c3656d71d82..52b6b38ca8ef 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -65,7 +65,6 @@ import org.mockito.junit.MockitoJUnit class ClockEventControllerTest : SysuiTestCase() { @JvmField @Rule val mockito = MockitoJUnit.rule() - @Mock private lateinit var keyguardInteractor: KeyguardInteractor @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher @Mock private lateinit var batteryController: BatteryController @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java index 8ef65dcb2c3a..a36105e11514 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java @@ -16,6 +16,7 @@ package com.android.systemui.accessibility.floatingmenu; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import android.testing.AndroidTestingRunner; @@ -47,7 +48,7 @@ public class DismissAnimationControllerTest extends SysuiTestCase { stubWindowManager); final MenuView stubMenuView = new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance); - mDismissView = new DismissView(mContext); + mDismissView = spy(new DismissView(mContext)); mDismissAnimationController = new DismissAnimationController(mDismissView, stubMenuView); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index 7a1568098e4a..b9ab9d37a805 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -20,6 +20,8 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Position import com.android.systemui.doze.DozeHost +import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.mockito.argumentCaptor @@ -43,6 +45,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var statusBarStateController: StatusBarStateController @Mock private lateinit var dozeHost: DozeHost @Mock private lateinit var keyguardStateController: KeyguardStateController + @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle private lateinit var underTest: KeyguardRepositoryImpl @@ -55,6 +58,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { statusBarStateController, keyguardStateController, dozeHost, + wakefulnessLifecycle, ) } @@ -184,4 +188,33 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { job.cancel() verify(statusBarStateController).removeCallback(captor.value) } + + @Test + fun wakefullness() = runBlockingTest { + val values = mutableListOf<WakefulnessModel>() + val job = underTest.wakefulnessState.onEach(values::add).launchIn(this) + + val captor = argumentCaptor<WakefulnessLifecycle.Observer>() + verify(wakefulnessLifecycle).addObserver(captor.capture()) + + captor.value.onStartedWakingUp() + captor.value.onFinishedWakingUp() + captor.value.onStartedGoingToSleep() + captor.value.onFinishedGoingToSleep() + + assertThat(values) + .isEqualTo( + listOf( + // Initial value will be ASLEEP + WakefulnessModel.ASLEEP, + WakefulnessModel.STARTING_TO_WAKE, + WakefulnessModel.AWAKE, + WakefulnessModel.STARTING_TO_SLEEP, + WakefulnessModel.ASLEEP, + ) + ) + + job.cancel() + verify(wakefulnessLifecycle).removeObserver(captor.value) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt index 1b34100b1cef..64913c7c28c2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt @@ -63,7 +63,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { @Before fun setUp() { - underTest = KeyguardTransitionRepository() + underTest = KeyguardTransitionRepositoryImpl() wtfHandler = WtfHandler() oldWtfHandler = Log.setWtfHandler(wtfHandler) } @@ -174,9 +174,6 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { } private fun assertSteps(steps: List<TransitionStep>, fractions: List<BigDecimal>) { - // + 2 accounts for start and finish of automated transition - assertThat(steps.size).isEqualTo(fractions.size + 2) - assertThat(steps[0]).isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED)) fractions.forEachIndexed { index, fraction -> assertThat(steps[index + 1]) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt new file mode 100644 index 000000000000..0424c28a1c24 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt @@ -0,0 +1,149 @@ +/* + * 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.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.GONE +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING +import com.android.systemui.keyguard.shared.model.TransitionState.STARTED +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class KeyguardTransitionInteractorTest : SysuiTestCase() { + + private lateinit var underTest: KeyguardTransitionInteractor + private lateinit var repository: FakeKeyguardTransitionRepository + + @Before + fun setUp() { + repository = FakeKeyguardTransitionRepository() + underTest = KeyguardTransitionInteractor(repository) + } + + @Test + fun `transition collectors receives only appropriate events`() = + runBlocking(IMMEDIATE) { + var goneToAodSteps = mutableListOf<TransitionStep>() + val job1 = + underTest.goneToAodTransition.onEach { goneToAodSteps.add(it) }.launchIn(this) + + var aodToLockscreenSteps = mutableListOf<TransitionStep>() + val job2 = + underTest.aodToLockscreenTransition + .onEach { aodToLockscreenSteps.add(it) } + .launchIn(this) + + val steps = mutableListOf<TransitionStep>() + steps.add(TransitionStep(AOD, GONE, 0f, STARTED)) + steps.add(TransitionStep(AOD, GONE, 1f, FINISHED)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED)) + steps.add(TransitionStep(GONE, AOD, 0f, STARTED)) + steps.add(TransitionStep(GONE, AOD, 0.1f, RUNNING)) + steps.add(TransitionStep(GONE, AOD, 0.2f, RUNNING)) + + steps.forEach { repository.sendTransitionStep(it) } + + assertThat(aodToLockscreenSteps).isEqualTo(steps.subList(2, 5)) + assertThat(goneToAodSteps).isEqualTo(steps.subList(5, 8)) + + job1.cancel() + job2.cancel() + } + + @Test + fun dozeAmountTransitionTest() = + runBlocking(IMMEDIATE) { + var dozeAmountSteps = mutableListOf<TransitionStep>() + val job = + underTest.dozeAmountTransition.onEach { dozeAmountSteps.add(it) }.launchIn(this) + + val steps = mutableListOf<TransitionStep>() + + steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 0.8f, RUNNING)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED)) + + steps.forEach { repository.sendTransitionStep(it) } + + assertThat(dozeAmountSteps.subList(0, 3)) + .isEqualTo( + listOf( + steps[0].copy(value = 1f - steps[0].value), + steps[1].copy(value = 1f - steps[1].value), + steps[2].copy(value = 1f - steps[2].value), + ) + ) + assertThat(dozeAmountSteps.subList(3, 7)).isEqualTo(steps.subList(3, 7)) + + job.cancel() + } + + @Test + fun keyguardStateTests() = + runBlocking(IMMEDIATE) { + var finishedSteps = mutableListOf<KeyguardState>() + val job1 = + underTest.finishedKeyguardState.onEach { finishedSteps.add(it) }.launchIn(this) + var startedSteps = mutableListOf<KeyguardState>() + val job2 = underTest.startedKeyguardState.onEach { startedSteps.add(it) }.launchIn(this) + + val steps = mutableListOf<TransitionStep>() + + steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED)) + steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) + + steps.forEach { repository.sendTransitionStep(it) } + + assertThat(finishedSteps).isEqualTo(listOf(LOCKSCREEN, AOD)) + assertThat(startedSteps).isEqualTo(listOf(LOCKSCREEN, AOD, GONE)) + + job1.cancel() + job2.cancel() + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index ac4dd49208c2..dbf9e67dfe19 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -1552,7 +1552,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { /* delay= */ 0 ); - mNotificationPanelViewController.setQsExpansion(/* height= */ 123); + mNotificationPanelViewController.setQsExpansionHeight(/* height= */ 123); // First for setTransitionToFullShadeAmount and then setQsExpansion verify(mQs, times(2)).setQsExpansion( @@ -1573,7 +1573,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { when(mNotificationStackScrollLayoutController.getNotificationSquishinessFraction()) .thenReturn(nsslSquishinessFraction); - mNotificationPanelViewController.setQsExpansion(/* height= */ 123); + mNotificationPanelViewController.setQsExpansionHeight(/* height= */ 123); verify(mQs).setQsExpansion( /* expansion= */ anyFloat(), @@ -1680,6 +1680,15 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { inOrder.verify(mNotificationStackScrollLayoutController).onExpansionStopped(); } + @Test + public void inUnlockedSplitShade_transitioningMaxTransitionDistance_makesShadeFullyExpanded() { + mStatusBarStateController.setState(SHADE); + enableSplitShade(true); + int transitionDistance = mNotificationPanelViewController.getMaxPanelTransitionDistance(); + mNotificationPanelViewController.setExpandedHeight(transitionDistance); + assertThat(mNotificationPanelViewController.isFullyExpanded()).isTrue(); + } + private static MotionEvent createMotionEvent(int x, int y, int action) { return MotionEvent.obtain( /* downTime= */ 0, /* eventTime= */ 0, action, x, y, /* metaState= */ 0); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplingInstanceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplerTest.kt index 09d51f6447b0..5a62cc18cae3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplingInstanceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplerTest.kt @@ -21,61 +21,55 @@ import org.mockito.junit.MockitoJUnit @RunWith(AndroidTestingRunner::class) @SmallTest -class RegionSamplingInstanceTest : SysuiTestCase() { +class RegionSamplerTest : SysuiTestCase() { - @JvmField @Rule - val mockito = MockitoJUnit.rule() + @JvmField @Rule val mockito = MockitoJUnit.rule() @Mock private lateinit var sampledView: View @Mock private lateinit var mainExecutor: Executor @Mock private lateinit var bgExecutor: Executor @Mock private lateinit var regionSampler: RegionSamplingHelper - @Mock private lateinit var updateFun: RegionSamplingInstance.UpdateColorCallback @Mock private lateinit var pw: PrintWriter @Mock private lateinit var callback: RegionSamplingHelper.SamplingCallback - private lateinit var regionSamplingInstance: RegionSamplingInstance + private lateinit var mRegionSampler: RegionSampler + private var updateFun: UpdateColorCallback = {} @Before fun setUp() { whenever(sampledView.isAttachedToWindow).thenReturn(true) - whenever(regionSampler.callback).thenReturn(this@RegionSamplingInstanceTest.callback) - - regionSamplingInstance = object : RegionSamplingInstance( - sampledView, - mainExecutor, - bgExecutor, - true, - updateFun - ) { - override fun createRegionSamplingHelper( + whenever(regionSampler.callback).thenReturn(this@RegionSamplerTest.callback) + + mRegionSampler = + object : RegionSampler(sampledView, mainExecutor, bgExecutor, true, updateFun) { + override fun createRegionSamplingHelper( sampledView: View, callback: RegionSamplingHelper.SamplingCallback, mainExecutor: Executor?, bgExecutor: Executor? - ): RegionSamplingHelper { - return this@RegionSamplingInstanceTest.regionSampler + ): RegionSamplingHelper { + return this@RegionSamplerTest.regionSampler + } } - } } @Test fun testStartRegionSampler() { - regionSamplingInstance.startRegionSampler() + mRegionSampler.startRegionSampler() verify(regionSampler).start(Rect(0, 0, 0, 0)) } @Test fun testStopRegionSampler() { - regionSamplingInstance.stopRegionSampler() + mRegionSampler.stopRegionSampler() verify(regionSampler).stop() } @Test fun testDump() { - regionSamplingInstance.dump(pw) + mRegionSampler.dump(pw) verify(regionSampler).dump(pw) } @@ -91,23 +85,18 @@ class RegionSamplingInstanceTest : SysuiTestCase() { @Test fun testFlagFalse() { - regionSamplingInstance = object : RegionSamplingInstance( - sampledView, - mainExecutor, - bgExecutor, - false, - updateFun - ) { - override fun createRegionSamplingHelper( + mRegionSampler = + object : RegionSampler(sampledView, mainExecutor, bgExecutor, false, updateFun) { + override fun createRegionSamplingHelper( sampledView: View, callback: RegionSamplingHelper.SamplingCallback, mainExecutor: Executor?, bgExecutor: Executor? - ): RegionSamplingHelper { - return this@RegionSamplingInstanceTest.regionSampler + ): RegionSamplingHelper { + return this@RegionSamplerTest.regionSampler + } } - } - Assert.assertEquals(regionSamplingInstance.regionSampler, null) + Assert.assertEquals(mRegionSampler.regionSampler, null) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt index 120bf791c462..e49652148f3a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt @@ -219,6 +219,7 @@ class GuestUserInteractorTest : SysuiTestCase() { repository.setUserInfos(listOf(NON_GUEST_USER_INFO, EPHEMERAL_GUEST_USER_INFO)) repository.setSelectedUserInfo(EPHEMERAL_GUEST_USER_INFO) val targetUserId = NON_GUEST_USER_INFO.id + val ephemeralGuestUserHandle = UserHandle.of(EPHEMERAL_GUEST_USER_INFO.id) underTest.exit( guestUserId = GUEST_USER_INFO.id, @@ -230,7 +231,7 @@ class GuestUserInteractorTest : SysuiTestCase() { ) verify(manager).markGuestForDeletion(EPHEMERAL_GUEST_USER_INFO.id) - verify(manager).removeUser(EPHEMERAL_GUEST_USER_INFO.id) + verify(manager).removeUserWhenPossible(ephemeralGuestUserHandle, false) verify(switchUser).invoke(targetUserId) } @@ -240,6 +241,7 @@ class GuestUserInteractorTest : SysuiTestCase() { whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true) repository.setSelectedUserInfo(GUEST_USER_INFO) val targetUserId = NON_GUEST_USER_INFO.id + val guestUserHandle = UserHandle.of(GUEST_USER_INFO.id) underTest.exit( guestUserId = GUEST_USER_INFO.id, @@ -251,7 +253,7 @@ class GuestUserInteractorTest : SysuiTestCase() { ) verify(manager).markGuestForDeletion(GUEST_USER_INFO.id) - verify(manager).removeUser(GUEST_USER_INFO.id) + verify(manager).removeUserWhenPossible(guestUserHandle, false) verify(switchUser).invoke(targetUserId) } @@ -296,6 +298,7 @@ class GuestUserInteractorTest : SysuiTestCase() { repository.setSelectedUserInfo(GUEST_USER_INFO) val targetUserId = NON_GUEST_USER_INFO.id + val guestUserHandle = UserHandle.of(GUEST_USER_INFO.id) underTest.remove( guestUserId = GUEST_USER_INFO.id, targetUserId = targetUserId, @@ -305,7 +308,7 @@ class GuestUserInteractorTest : SysuiTestCase() { ) verify(manager).markGuestForDeletion(GUEST_USER_INFO.id) - verify(manager).removeUser(GUEST_USER_INFO.id) + verify(manager).removeUserWhenPossible(guestUserHandle, false) verify(switchUser).invoke(targetUserId) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java index c2543589bfb8..379bb28ae032 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java @@ -26,8 +26,8 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -44,6 +44,7 @@ import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerGlobal; import android.os.Handler; +import android.os.UserHandle; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.Display; @@ -135,9 +136,10 @@ public class ImageWallpaperTest extends SysuiTestCase { when(mWallpaperBitmap.getHeight()).thenReturn(mBitmapHeight); // set up wallpaper manager - when(mWallpaperManager.peekBitmapDimensions()).thenReturn( - new Rect(0, 0, mBitmapWidth, mBitmapHeight)); - when(mWallpaperManager.getBitmap(false)).thenReturn(mWallpaperBitmap); + when(mWallpaperManager.peekBitmapDimensions()) + .thenReturn(new Rect(0, 0, mBitmapWidth, mBitmapHeight)); + when(mWallpaperManager.getBitmapAsUser(eq(UserHandle.USER_CURRENT), anyBoolean())) + .thenReturn(mWallpaperBitmap); when(mMockContext.getSystemService(WallpaperManager.class)).thenReturn(mWallpaperManager); // set up surface @@ -286,9 +288,6 @@ public class ImageWallpaperTest extends SysuiTestCase { testMinSurfaceHelper(8, 8); testMinSurfaceHelper(100, 2000); testMinSurfaceHelper(200, 1); - testMinSurfaceHelper(0, 1); - testMinSurfaceHelper(1, 0); - testMinSurfaceHelper(0, 0); } private void testMinSurfaceHelper(int bitmapWidth, int bitmapHeight) { @@ -307,28 +306,6 @@ public class ImageWallpaperTest extends SysuiTestCase { } @Test - public void testZeroBitmap() { - // test that a frame is never drawn with a 0 bitmap - testZeroBitmapHelper(0, 1); - testZeroBitmapHelper(1, 0); - testZeroBitmapHelper(0, 0); - } - - private void testZeroBitmapHelper(int bitmapWidth, int bitmapHeight) { - - clearInvocations(mSurfaceHolder); - setBitmapDimensions(bitmapWidth, bitmapHeight); - - ImageWallpaper imageWallpaper = createImageWallpaperCanvas(); - ImageWallpaper.CanvasEngine engine = - (ImageWallpaper.CanvasEngine) imageWallpaper.onCreateEngine(); - ImageWallpaper.CanvasEngine spyEngine = spy(engine); - spyEngine.onCreate(mSurfaceHolder); - spyEngine.onSurfaceRedrawNeeded(mSurfaceHolder); - verify(spyEngine, never()).drawFrameOnCanvas(any()); - } - - @Test public void testLoadDrawAndUnloadBitmap() { setBitmapDimensions(LOW_BMP_WIDTH, LOW_BMP_HEIGHT); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 0c126805fb78..11178db8afd2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.data.repository import com.android.systemui.common.shared.model.Position import com.android.systemui.keyguard.shared.model.StatusBarState +import com.android.systemui.keyguard.shared.model.WakefulnessModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -48,6 +49,9 @@ class FakeKeyguardRepository : KeyguardRepository { private val _statusBarState = MutableStateFlow(StatusBarState.SHADE) override val statusBarState: Flow<StatusBarState> = _statusBarState + private val _wakefulnessState = MutableStateFlow(WakefulnessModel.ASLEEP) + override val wakefulnessState: Flow<WakefulnessModel> = _wakefulnessState + override fun isKeyguardShowing(): Boolean { return _isKeyguardShowing.value } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt new file mode 100644 index 000000000000..6c4424470c1c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt @@ -0,0 +1,47 @@ +/* + * 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.data.repository + +import android.annotation.FloatRange +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import java.util.UUID +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow + +/** Fake implementation of [KeyguardTransitionRepository] */ +class FakeKeyguardTransitionRepository : KeyguardTransitionRepository { + + private val _transitions = MutableSharedFlow<TransitionStep>() + override val transitions: SharedFlow<TransitionStep> = _transitions + + suspend fun sendTransitionStep(step: TransitionStep) { + _transitions.emit(step) + } + + override fun startTransition(info: TransitionInfo): UUID? { + return null + } + + override fun updateTransition( + transitionId: UUID, + @FloatRange(from = 0.0, to = 1.0) value: Float, + state: TransitionState + ) = Unit +} diff --git a/services/core/java/android/os/BatteryStatsInternal.java b/services/core/java/android/os/BatteryStatsInternal.java index 41044bfc70ca..b70cbe329f3a 100644 --- a/services/core/java/android/os/BatteryStatsInternal.java +++ b/services/core/java/android/os/BatteryStatsInternal.java @@ -27,7 +27,6 @@ import java.lang.annotation.RetentionPolicy; import java.util.Collection; import java.util.List; - /** * Battery stats local system service interface. This is used to pass internal data out of * BatteryStatsImpl, as well as make unchecked calls into BatteryStatsImpl. diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index ea6688442fa4..2eaddb1c37f1 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -470,6 +470,7 @@ import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; public class ActivityManagerService extends IActivityManager.Stub @@ -3455,13 +3456,13 @@ public class ActivityManagerService extends IActivityManager.Stub } /** - * @param firstPidOffsets Optional, when it's set, it receives the start/end offset + * @param firstPidEndOffset Optional, when it's set, it receives the start/end offset * of the very first pid to be dumped. */ /* package */ static File dumpStackTraces(ArrayList<Integer> firstPids, ProcessCpuTracker processCpuTracker, SparseArray<Boolean> lastPids, ArrayList<Integer> nativePids, StringWriter logExceptionCreatingFile, - long[] firstPidOffsets, String subject, String criticalEventSection, + AtomicLong firstPidEndOffset, String subject, String criticalEventSection, AnrLatencyTracker latencyTracker) { try { if (latencyTracker != null) { @@ -3534,15 +3535,10 @@ public class ActivityManagerService extends IActivityManager.Stub + (criticalEventSection != null ? criticalEventSection : "")); } - Pair<Long, Long> offsets = dumpStackTraces( + long firstPidEndPos = dumpStackTraces( tracesFile.getAbsolutePath(), firstPids, nativePids, extraPids, latencyTracker); - if (firstPidOffsets != null) { - if (offsets == null) { - firstPidOffsets[0] = firstPidOffsets[1] = -1; - } else { - firstPidOffsets[0] = offsets.first; // Start offset to the ANR trace file - firstPidOffsets[1] = offsets.second; // End offset to the ANR trace file - } + if (firstPidEndOffset != null) { + firstPidEndOffset.set(firstPidEndPos); } return tracesFile; @@ -3661,9 +3657,9 @@ public class ActivityManagerService extends IActivityManager.Stub /** - * @return The start/end offset of the trace of the very first PID + * @return The end offset of the trace of the very first PID */ - public static Pair<Long, Long> dumpStackTraces(String tracesFile, + public static long dumpStackTraces(String tracesFile, ArrayList<Integer> firstPids, ArrayList<Integer> nativePids, ArrayList<Integer> extraPids, AnrLatencyTracker latencyTracker) { @@ -3679,7 +3675,6 @@ public class ActivityManagerService extends IActivityManager.Stub // As applications are usually interested with the ANR stack traces, but we can't share with // them the stack traces other than their own stacks. So after the very first PID is // dumped, remember the current file size. - long firstPidStart = -1; long firstPidEnd = -1; // First collect all of the stacks of the most important pids. @@ -3692,11 +3687,6 @@ public class ActivityManagerService extends IActivityManager.Stub final int pid = firstPids.get(i); // We don't copy ANR traces from the system_server intentionally. final boolean firstPid = i == 0 && MY_PID != pid; - File tf = null; - if (firstPid) { - tf = new File(tracesFile); - firstPidStart = tf.exists() ? tf.length() : 0; - } if (latencyTracker != null) { latencyTracker.dumpingPidStarted(pid); } @@ -3712,11 +3702,11 @@ public class ActivityManagerService extends IActivityManager.Stub if (remainingTime <= 0) { Slog.e(TAG, "Aborting stack trace dump (current firstPid=" + pid + "); deadline exceeded."); - return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null; + return firstPidEnd; } if (firstPid) { - firstPidEnd = tf.length(); + firstPidEnd = new File(tracesFile).length(); // Full latency dump if (latencyTracker != null) { appendtoANRFile(tracesFile, @@ -3755,7 +3745,7 @@ public class ActivityManagerService extends IActivityManager.Stub if (remainingTime <= 0) { Slog.e(TAG, "Aborting stack trace dump (current native pid=" + pid + "); deadline exceeded."); - return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null; + return firstPidEnd; } if (DEBUG_ANR) { @@ -3785,7 +3775,7 @@ public class ActivityManagerService extends IActivityManager.Stub if (remainingTime <= 0) { Slog.e(TAG, "Aborting stack trace dump (current extra pid=" + pid + "); deadline exceeded."); - return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null; + return firstPidEnd; } if (DEBUG_ANR) { @@ -3800,7 +3790,7 @@ public class ActivityManagerService extends IActivityManager.Stub appendtoANRFile(tracesFile, "----- dumping ended at " + SystemClock.uptimeMillis() + "\n"); Slog.i(TAG, "Done dumping"); - return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null; + return firstPidEnd; } @Override @@ -13836,6 +13826,25 @@ public class ActivityManagerService extends IActivityManager.Stub } } + // Apply permission policy around the use of specific broadcast options + void enforceBroadcastOptionPermissionsInternal(@Nullable Bundle options, int callingUid) { + if (options != null && callingUid != Process.SYSTEM_UID) { + if (options.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)) { + if (DEBUG_BROADCAST_LIGHT) { + Slog.w(TAG, "Non-system caller " + callingUid + + " may not flag broadcast as alarm"); + } + throw new SecurityException( + "Non-system callers may not flag broadcasts as alarm"); + } + if (options.containsKey(BroadcastOptions.KEY_INTERACTIVE_BROADCAST)) { + enforceCallingPermission( + android.Manifest.permission.BROADCAST_OPTION_INTERACTIVE, + "setInteractiveBroadcast"); + } + } + } + @GuardedBy("this") final int broadcastIntentLocked(ProcessRecord callerApp, String callerPackage, String callerFeatureId, Intent intent, String resolvedType, @@ -14703,19 +14712,8 @@ public class ActivityManagerService extends IActivityManager.Stub // We're delivering the result to the caller final ProcessRecord resultToApp = callerApp; - // Non-system callers can't declare that a broadcast is alarm-related. - // The PendingIntent invocation case is handled in PendingIntentRecord. - if (bOptions != null && callingUid != SYSTEM_UID) { - if (bOptions.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST) - || bOptions.containsKey(BroadcastOptions.KEY_INTERACTIVE_BROADCAST)) { - if (DEBUG_BROADCAST) { - Slog.w(TAG, "Non-system caller " + callingUid - + " may not flag broadcast as alarm or interactive"); - } - throw new SecurityException( - "Non-system callers may not flag broadcasts as alarm or interactive"); - } - } + // Permission regimes around sender-supplied broadcast options. + enforceBroadcastOptionPermissionsInternal(bOptions, callingUid); final long origId = Binder.clearCallingIdentity(); try { @@ -16909,6 +16907,11 @@ public class ActivityManagerService extends IActivityManager.Stub return mEnableModernQueue; } + @Override + public void enforceBroadcastOptionsPermissions(Bundle options, int callingUid) { + enforceBroadcastOptionPermissionsInternal(options, callingUid); + } + /** * Returns package name by pid. */ diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index 14a169737b38..3f5f3bce3014 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -18,7 +18,6 @@ package com.android.server.am; import static android.app.ActivityManager.START_SUCCESS; -import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST_LIGHT; import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; @@ -36,7 +35,6 @@ import android.os.Bundle; import android.os.IBinder; import android.os.PowerWhitelistManager; import android.os.PowerWhitelistManager.ReasonCode; -import android.os.Process; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.TransactionTooLargeException; @@ -441,17 +439,8 @@ public final class PendingIntentRecord extends IIntentSender.Stub { // Only system senders can declare a broadcast to be alarm-originated. We check // this here rather than in the general case handling below to fail before the other // invocation side effects such as allowlisting. - if (options != null && callingUid != Process.SYSTEM_UID - && key.type == ActivityManager.INTENT_SENDER_BROADCAST) { - if (options.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST) - || options.containsKey(BroadcastOptions.KEY_INTERACTIVE_BROADCAST)) { - if (DEBUG_BROADCAST_LIGHT) { - Slog.w(TAG, "Non-system caller " + callingUid - + " may not flag broadcast as alarm or interactive"); - } - throw new SecurityException( - "Non-system callers may not flag broadcasts as alarm or interactive"); - } + if (key.type == ActivityManager.INTENT_SENDER_BROADCAST) { + controller.mAmInternal.enforceBroadcastOptionsPermissions(options, callingUid); } final long origId = Binder.clearCallingIdentity(); diff --git a/services/core/java/com/android/server/am/ProcessErrorStateRecord.java b/services/core/java/com/android/server/am/ProcessErrorStateRecord.java index 71d39964ed72..f461f3d24b5f 100644 --- a/services/core/java/com/android/server/am/ProcessErrorStateRecord.java +++ b/services/core/java/com/android/server/am/ProcessErrorStateRecord.java @@ -62,6 +62,8 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + /** * The error state of the process, such as if it's crashing/ANR etc. */ @@ -458,10 +460,10 @@ class ProcessErrorStateRecord { // avoid spending 1/2 second collecting stats to rank lastPids. StringWriter tracesFileException = new StringWriter(); // To hold the start and end offset to the ANR trace file respectively. - final long[] offsets = new long[2]; + final AtomicLong firstPidEndOffset = new AtomicLong(-1); File tracesFile = ActivityManagerService.dumpStackTraces(firstPids, isSilentAnr ? null : processCpuTracker, isSilentAnr ? null : lastPids, - nativePids, tracesFileException, offsets, annotation, criticalEventLog, + nativePids, tracesFileException, firstPidEndOffset, annotation, criticalEventLog, latencyTracker); if (isMonitorCpuUsage()) { @@ -478,10 +480,14 @@ class ProcessErrorStateRecord { if (tracesFile == null) { // There is no trace file, so dump (only) the alleged culprit's threads to the log Process.sendSignal(pid, Process.SIGNAL_QUIT); - } else if (offsets[1] > 0) { + } else if (firstPidEndOffset.get() > 0) { // We've dumped into the trace file successfully + // We pass the start and end offsets of the first section of + // the ANR file (the headers and first process dump) + final long startOffset = 0L; + final long endOffset = firstPidEndOffset.get(); mService.mProcessList.mAppExitInfoTracker.scheduleLogAnrTrace( - pid, mApp.uid, mApp.getPackageList(), tracesFile, offsets[0], offsets[1]); + pid, mApp.uid, mApp.getPackageList(), tracesFile, startOffset, endOffset); } // Check if package is still being loaded diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index a3b1a420b180..523a2dca7a18 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -699,7 +699,6 @@ public class DisplayDeviceConfig { */ public float getNitsFromBacklight(float backlight) { if (mBacklightToNitsSpline == null) { - Slog.wtf(TAG, "requesting nits when no mapping exists."); return NITS_INVALID; } backlight = Math.max(backlight, mBacklightMinimum); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 9cb8f43c452c..0eaa5e452a58 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -144,6 +144,7 @@ import android.view.inputmethod.InputMethodSubtype; import android.window.ImeOnBackInvokedDispatcher; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.PackageMonitor; import com.android.internal.infra.AndroidFuture; import com.android.internal.inputmethod.DirectBootAwareness; @@ -1592,8 +1593,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private final InputMethodManagerService mService; public Lifecycle(Context context) { + this(context, new InputMethodManagerService(context)); + } + + public Lifecycle( + Context context, @NonNull InputMethodManagerService inputMethodManagerService) { super(context); - mService = new InputMethodManagerService(context); + mService = inputMethodManagerService; } @Override @@ -1668,12 +1674,25 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } public InputMethodManagerService(Context context) { + this(context, null, null); + } + + @VisibleForTesting + InputMethodManagerService( + Context context, + @Nullable ServiceThread serviceThreadForTesting, + @Nullable InputMethodBindingController bindingControllerForTesting) { mContext = context; mRes = context.getResources(); // TODO(b/196206770): Disallow I/O on this thread. Currently it's needed for loading // additional subtypes in switchUserOnHandlerLocked(). - final ServiceThread thread = new ServiceThread( - HANDLER_THREAD_NAME, Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */); + final ServiceThread thread = + serviceThreadForTesting != null + ? serviceThreadForTesting + : new ServiceThread( + HANDLER_THREAD_NAME, + Process.THREAD_PRIORITY_FOREGROUND, + true /* allowIo */); thread.start(); mHandler = Handler.createAsync(thread.getLooper(), this); // Note: SettingsObserver doesn't register observers in its constructor. @@ -1701,10 +1720,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub updateCurrentProfileIds(); AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId); - mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked( - mSettings, context); + mSwitchingController = + InputMethodSubtypeSwitchingController.createInstanceLocked(mSettings, context); mMenuController = new InputMethodMenuController(this); - mBindingController = new InputMethodBindingController(this); + mBindingController = + bindingControllerForTesting != null + ? bindingControllerForTesting + : new InputMethodBindingController(this); mAutofillController = new AutofillSuggestionsController(this); mPreventImeStartupUnlessTextEditor = mRes.getBoolean( com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor); @@ -3673,6 +3695,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // UI for input. if (isTextEditor && editorInfo != null && shouldRestoreImeVisibility(windowToken, softInputMode)) { + if (DEBUG) Slog.v(TAG, "Will show input to restore visibility"); res = startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection, editorInfo, startInputFlags, startInputReason, unverifiedTargetSdkVersion, imeDispatcher); @@ -3719,11 +3742,17 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub imeDispatcher); didStart = true; } - showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null, + showCurrentInputLocked( + windowToken, + InputMethodManager.SHOW_IMPLICIT, + null, SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV); } break; case LayoutParams.SOFT_INPUT_STATE_UNCHANGED: + if (DEBUG) { + Slog.v(TAG, "Window asks to keep the input in whatever state it was last in"); + } // Do nothing. break; case LayoutParams.SOFT_INPUT_STATE_HIDDEN: @@ -3794,6 +3823,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // To maintain compatibility, we are now hiding the IME when we don't have // an editor upon refocusing a window. if (startInputByWinGainedFocus) { + if (DEBUG) Slog.v(TAG, "Same window without editor will hide input"); hideCurrentInputLocked(mCurFocusedWindow, 0, null, SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR); } @@ -3807,6 +3837,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // 1) SOFT_INPUT_STATE_UNCHANGED state without an editor // 2) SOFT_INPUT_STATE_VISIBLE state without an editor // 3) SOFT_INPUT_STATE_ALWAYS_VISIBLE state without an editor + if (DEBUG) Slog.v(TAG, "Window without editor will hide input"); hideCurrentInputLocked(mCurFocusedWindow, 0, null, SoftInputShowHideReason.HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR); } diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index 439e9bd2624e..c67d54f79307 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -151,7 +151,7 @@ class MediaRouter2ServiceImpl { mContext.registerReceiver(mScreenOnOffReceiver, screenOnOffIntentFilter); } - // Methods that implement MediaRouter2 operations. + // Start of methods that implement MediaRouter2 operations. @NonNull public void enforceMediaContentControlPermission() { @@ -199,46 +199,6 @@ class MediaRouter2ServiceImpl { } } - @NonNull - public RoutingSessionInfo getSystemSessionInfo( - @Nullable String packageName, boolean setDeviceRouteSelected) { - final int uid = Binder.getCallingUid(); - final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier(); - final boolean hasModifyAudioRoutingPermission = mContext.checkCallingOrSelfPermission( - android.Manifest.permission.MODIFY_AUDIO_ROUTING) - == PackageManager.PERMISSION_GRANTED; - - final long token = Binder.clearCallingIdentity(); - try { - RoutingSessionInfo systemSessionInfo = null; - synchronized (mLock) { - UserRecord userRecord = getOrCreateUserRecordLocked(userId); - List<RoutingSessionInfo> sessionInfos; - if (hasModifyAudioRoutingPermission) { - if (setDeviceRouteSelected) { - systemSessionInfo = userRecord.mHandler.mSystemProvider - .generateDeviceRouteSelectedSessionInfo(packageName); - } else { - sessionInfos = userRecord.mHandler.mSystemProvider.getSessionInfos(); - if (sessionInfos != null && !sessionInfos.isEmpty()) { - systemSessionInfo = new RoutingSessionInfo.Builder(sessionInfos.get(0)) - .setClientPackageName(packageName).build(); - } else { - Slog.w(TAG, "System provider does not have any session info."); - } - } - } else { - systemSessionInfo = new RoutingSessionInfo.Builder( - userRecord.mHandler.mSystemProvider.getDefaultSessionInfo()) - .setClientPackageName(packageName).build(); - } - } - return systemSessionInfo; - } finally { - Binder.restoreCallingIdentity(token); - } - } - public void registerRouter2(@NonNull IMediaRouter2 router, @NonNull String packageName) { Objects.requireNonNull(router, "router must not be null"); if (TextUtils.isEmpty(packageName)) { @@ -418,7 +378,9 @@ class MediaRouter2ServiceImpl { } } - // Methods that implement MediaRouter2Manager operations. + // End of methods that implement MediaRouter2 operations. + + // Start of methods that implement MediaRouter2Manager operations. @NonNull public List<RoutingSessionInfo> getRemoteSessions(@NonNull IMediaRouter2Manager manager) { @@ -610,6 +572,52 @@ class MediaRouter2ServiceImpl { } } + // End of methods that implement MediaRouter2Manager operations. + + // Start of methods that implements operations for both MediaRouter2 and MediaRouter2Manager. + + @NonNull + public RoutingSessionInfo getSystemSessionInfo( + @Nullable String packageName, boolean setDeviceRouteSelected) { + final int uid = Binder.getCallingUid(); + final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier(); + final boolean hasModifyAudioRoutingPermission = mContext.checkCallingOrSelfPermission( + android.Manifest.permission.MODIFY_AUDIO_ROUTING) + == PackageManager.PERMISSION_GRANTED; + + final long token = Binder.clearCallingIdentity(); + try { + RoutingSessionInfo systemSessionInfo = null; + synchronized (mLock) { + UserRecord userRecord = getOrCreateUserRecordLocked(userId); + List<RoutingSessionInfo> sessionInfos; + if (hasModifyAudioRoutingPermission) { + if (setDeviceRouteSelected) { + systemSessionInfo = userRecord.mHandler.mSystemProvider + .generateDeviceRouteSelectedSessionInfo(packageName); + } else { + sessionInfos = userRecord.mHandler.mSystemProvider.getSessionInfos(); + if (sessionInfos != null && !sessionInfos.isEmpty()) { + systemSessionInfo = new RoutingSessionInfo.Builder(sessionInfos.get(0)) + .setClientPackageName(packageName).build(); + } else { + Slog.w(TAG, "System provider does not have any session info."); + } + } + } else { + systemSessionInfo = new RoutingSessionInfo.Builder( + userRecord.mHandler.mSystemProvider.getDefaultSessionInfo()) + .setClientPackageName(packageName).build(); + } + } + return systemSessionInfo; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + // End of methods that implements operations for both MediaRouter2 and MediaRouter2Manager. + public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { pw.println(prefix + "MediaRouter2ServiceImpl"); @@ -678,6 +686,8 @@ class MediaRouter2ServiceImpl { return mUserManagerInternal.getProfileParentId(userId) == mCurrentActiveUserId; } + // Start of locked methods that are used by MediaRouter2. + @GuardedBy("mLock") private void registerRouter2Locked(@NonNull IMediaRouter2 router, int uid, int pid, @NonNull String packageName, int userId, boolean hasConfigureWifiDisplayPermission, @@ -952,6 +962,10 @@ class MediaRouter2ServiceImpl { DUMMY_REQUEST_ID, routerRecord, uniqueSessionId)); } + // End of locked methods that are used by MediaRouter2. + + // Start of locked methods that are used by MediaRouter2Manager. + private List<RoutingSessionInfo> getRemoteSessionsLocked( @NonNull IMediaRouter2Manager manager) { final IBinder binder = manager.asBinder(); @@ -1260,6 +1274,10 @@ class MediaRouter2ServiceImpl { uniqueRequestId, routerRecord, uniqueSessionId)); } + // End of locked methods that are used by MediaRouter2Manager. + + // Start of locked methods that are used by both MediaRouter2 and MediaRouter2Manager. + @GuardedBy("mLock") private UserRecord getOrCreateUserRecordLocked(int userId) { UserRecord userRecord = mUserRecords.get(userId); @@ -1294,6 +1312,8 @@ class MediaRouter2ServiceImpl { } } + // End of locked methods that are used by both MediaRouter2 and MediaRouter2Manager. + static long toUniqueRequestId(int requesterId, int originalRequestId) { return ((long) requesterId << 32) | originalRequestId; } diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java index e3a2fb2a2dee..2e67bf2fcdf7 100644 --- a/services/core/java/com/android/server/pm/AppsFilterImpl.java +++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java @@ -492,9 +492,11 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, * * @param newPkgSetting the new setting being added * @param isReplace if the package is being replaced and may need extra cleanup. + * @param retainImplicitGrantOnReplace {@code true} to retain implicit grant access if + * the package is being replaced. */ public void addPackage(Computer snapshot, PackageStateInternal newPkgSetting, - boolean isReplace) { + boolean isReplace, boolean retainImplicitGrantOnReplace) { final long currentTimeUs = SystemClock.currentTimeMicro(); final int logType = isReplace ? PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_REPLACED @@ -505,7 +507,8 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, try { if (isReplace) { // let's first remove any prior rules for this package - removePackageInternal(snapshot, newPkgSetting, true /*isReplace*/); + removePackageInternal(snapshot, newPkgSetting, + true /*isReplace*/, retainImplicitGrantOnReplace); } final ArrayMap<String, ? extends PackageStateInternal> settings = snapshot.getPackageStates(); @@ -1016,13 +1019,14 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, } /** - * Equivalent to calling {@link #addPackage(Computer, PackageStateInternal, boolean)} - * with {@code isReplace} equal to {@code false}. + * Equivalent to calling {@link #addPackage(Computer, PackageStateInternal, boolean, boolean)} + * with {@code isReplace} and {@code retainImplicitGrantOnReplace} equal to {@code false}. * - * @see AppsFilterImpl#addPackage(Computer, PackageStateInternal, boolean) + * @see AppsFilterImpl#addPackage(Computer, PackageStateInternal, boolean, boolean) */ public void addPackage(Computer snapshot, PackageStateInternal newPkgSetting) { - addPackage(snapshot, newPkgSetting, false /* isReplace */); + addPackage(snapshot, newPkgSetting, false /* isReplace */, + false /* retainImplicitGrantOnReplace */); } /** @@ -1032,7 +1036,8 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, */ public void removePackage(Computer snapshot, PackageStateInternal setting) { final long currentTimeUs = SystemClock.currentTimeMicro(); - removePackageInternal(snapshot, setting, false /* isReplace */); + removePackageInternal(snapshot, setting, + false /* isReplace */, false /* retainImplicitGrantOnReplace */); logCacheUpdated( PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_DELETED, SystemClock.currentTimeMicro() - currentTimeUs, @@ -1046,33 +1051,37 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, * * @param setting the setting of the package being removed. * @param isReplace if the package is being replaced. + * @param retainImplicitGrantOnReplace {@code true} to retain implicit grant access if + * the package is being replaced. */ private void removePackageInternal(Computer snapshot, PackageStateInternal setting, - boolean isReplace) { + boolean isReplace, boolean retainImplicitGrantOnReplace) { final ArraySet<String> additionalChangedPackages; final ArrayMap<String, ? extends PackageStateInternal> settings = snapshot.getPackageStates(); final UserInfo[] users = snapshot.getUserInfos(); final Collection<SharedUserSetting> sharedUserSettings = snapshot.getAllSharedUsers(); final int userCount = users.length; - synchronized (mImplicitlyQueryableLock) { - for (int u = 0; u < userCount; u++) { - final int userId = users[u].id; - final int removingUid = UserHandle.getUid(userId, setting.getAppId()); - mImplicitlyQueryable.remove(removingUid); - for (int i = mImplicitlyQueryable.size() - 1; i >= 0; i--) { - mImplicitlyQueryable.remove(mImplicitlyQueryable.keyAt(i), - removingUid); - } + if (!isReplace || !retainImplicitGrantOnReplace) { + synchronized (mImplicitlyQueryableLock) { + for (int u = 0; u < userCount; u++) { + final int userId = users[u].id; + final int removingUid = UserHandle.getUid(userId, setting.getAppId()); + mImplicitlyQueryable.remove(removingUid); + for (int i = mImplicitlyQueryable.size() - 1; i >= 0; i--) { + mImplicitlyQueryable.remove(mImplicitlyQueryable.keyAt(i), + removingUid); + } - if (isReplace) { - continue; - } + if (isReplace) { + continue; + } - mRetainedImplicitlyQueryable.remove(removingUid); - for (int i = mRetainedImplicitlyQueryable.size() - 1; i >= 0; i--) { - mRetainedImplicitlyQueryable.remove( - mRetainedImplicitlyQueryable.keyAt(i), removingUid); + mRetainedImplicitlyQueryable.remove(removingUid); + for (int i = mRetainedImplicitlyQueryable.size() - 1; i >= 0; i--) { + mRetainedImplicitlyQueryable.remove( + mRetainedImplicitlyQueryable.keyAt(i), removingUid); + } } } } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 9d007c92bcc5..7ea0c04562e2 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -463,7 +463,8 @@ final class InstallPackageHelper { final Computer snapshot = mPm.snapshotComputer(); mPm.mComponentResolver.addAllComponents(pkg, chatty, mPm.mSetupWizardPackage, snapshot); - mPm.mAppsFilter.addPackage(snapshot, pkgSetting, isReplace); + mPm.mAppsFilter.addPackage(snapshot, pkgSetting, isReplace, + (scanFlags & SCAN_DONT_KILL_APP) != 0 /* retainImplicitGrantOnReplace */); mPm.addAllPackageProperties(pkg); if (oldPkgSetting == null || oldPkgSetting.getPkg() == null) { diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 5df73a6d1fbd..0369a83ec9c1 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -3326,7 +3326,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { "Failure to obtain package info."); } final List<String> filePaths = packageLite.getAllApkPaths(); - final String appLabel = mPreapprovalDetails.getLabel(); + final CharSequence appLabel = mPreapprovalDetails.getLabel(); final ULocale appLocale = mPreapprovalDetails.getLocale(); final ApplicationInfo appInfo = packageInfo.applicationInfo; boolean appLabelMatched = false; diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 12c9d4b37eed..45c0d6ea6c11 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -455,19 +455,24 @@ public final class Settings implements Watchable, Snappable { // The user's preferred activities associated with particular intent // filters. @Watched - private final WatchedSparseArray<PreferredIntentResolver> - mPreferredActivities = new WatchedSparseArray<>(); + private final WatchedSparseArray<PreferredIntentResolver> mPreferredActivities; + private final SnapshotCache<WatchedSparseArray<PreferredIntentResolver>> + mPreferredActivitiesSnapshot; // The persistent preferred activities of the user's profile/device owner // associated with particular intent filters. @Watched private final WatchedSparseArray<PersistentPreferredIntentResolver> - mPersistentPreferredActivities = new WatchedSparseArray<>(); + mPersistentPreferredActivities; + private final SnapshotCache<WatchedSparseArray<PersistentPreferredIntentResolver>> + mPersistentPreferredActivitiesSnapshot; + // For every user, it is used to find to which other users the intent can be forwarded. @Watched - private final WatchedSparseArray<CrossProfileIntentResolver> - mCrossProfileIntentResolvers = new WatchedSparseArray<>(); + private final WatchedSparseArray<CrossProfileIntentResolver> mCrossProfileIntentResolvers; + private final SnapshotCache<WatchedSparseArray<CrossProfileIntentResolver>> + mCrossProfileIntentResolversSnapshot; @Watched final WatchedArrayMap<String, SharedUserSetting> mSharedUsers = new WatchedArrayMap<>(); @@ -476,11 +481,12 @@ public final class Settings implements Watchable, Snappable { // For reading/writing settings file. @Watched - private final WatchedArrayList<Signature> mPastSignatures = - new WatchedArrayList<Signature>(); + private final WatchedArrayList<Signature> mPastSignatures; + private final SnapshotCache<WatchedArrayList<Signature>> mPastSignaturesSnapshot; + @Watched - private final WatchedArrayMap<Long, Integer> mKeySetRefs = - new WatchedArrayMap<Long, Integer>(); + private final WatchedArrayMap<Long, Integer> mKeySetRefs; + private final SnapshotCache<WatchedArrayMap<Long, Integer>> mKeySetRefsSnapshot; // Packages that have been renamed since they were first installed. // Keys are the new names of the packages, values are the original @@ -511,7 +517,8 @@ public final class Settings implements Watchable, Snappable { * scanning to make it less confusing. */ @Watched - private final WatchedArrayList<PackageSetting> mPendingPackages = new WatchedArrayList<>(); + private final WatchedArrayList<PackageSetting> mPendingPackages; + private final SnapshotCache<WatchedArrayList<PackageSetting>> mPendingPackagesSnapshot; private final File mSystemDir; @@ -583,6 +590,26 @@ public final class Settings implements Watchable, Snappable { mInstallerPackagesSnapshot = new SnapshotCache.Auto<>(mInstallerPackages, mInstallerPackages, "Settings.mInstallerPackages"); + mPreferredActivities = new WatchedSparseArray<>(); + mPreferredActivitiesSnapshot = new SnapshotCache.Auto<>(mPreferredActivities, + mPreferredActivities, "Settings.mPreferredActivities"); + mPersistentPreferredActivities = new WatchedSparseArray<>(); + mPersistentPreferredActivitiesSnapshot = new SnapshotCache.Auto<>( + mPersistentPreferredActivities, mPersistentPreferredActivities, + "Settings.mPersistentPreferredActivities"); + mCrossProfileIntentResolvers = new WatchedSparseArray<>(); + mCrossProfileIntentResolversSnapshot = new SnapshotCache.Auto<>( + mCrossProfileIntentResolvers, mCrossProfileIntentResolvers, + "Settings.mCrossProfileIntentResolvers"); + mPastSignatures = new WatchedArrayList<>(); + mPastSignaturesSnapshot = new SnapshotCache.Auto<>(mPastSignatures, mPastSignatures, + "Settings.mPastSignatures"); + mKeySetRefs = new WatchedArrayMap<>(); + mKeySetRefsSnapshot = new SnapshotCache.Auto<>(mKeySetRefs, mKeySetRefs, + "Settings.mKeySetRefs"); + mPendingPackages = new WatchedArrayList<>(); + mPendingPackagesSnapshot = new SnapshotCache.Auto<>(mPendingPackages, mPendingPackages, + "Settings.mPendingPackages"); mKeySetManagerService = new KeySetManagerService(mPackages); // Test-only handler working on background thread. @@ -623,6 +650,26 @@ public final class Settings implements Watchable, Snappable { mInstallerPackagesSnapshot = new SnapshotCache.Auto<>(mInstallerPackages, mInstallerPackages, "Settings.mInstallerPackages"); + mPreferredActivities = new WatchedSparseArray<>(); + mPreferredActivitiesSnapshot = new SnapshotCache.Auto<>(mPreferredActivities, + mPreferredActivities, "Settings.mPreferredActivities"); + mPersistentPreferredActivities = new WatchedSparseArray<>(); + mPersistentPreferredActivitiesSnapshot = new SnapshotCache.Auto<>( + mPersistentPreferredActivities, mPersistentPreferredActivities, + "Settings.mPersistentPreferredActivities"); + mCrossProfileIntentResolvers = new WatchedSparseArray<>(); + mCrossProfileIntentResolversSnapshot = new SnapshotCache.Auto<>( + mCrossProfileIntentResolvers, mCrossProfileIntentResolvers, + "Settings.mCrossProfileIntentResolvers"); + mPastSignatures = new WatchedArrayList<>(); + mPastSignaturesSnapshot = new SnapshotCache.Auto<>(mPastSignatures, mPastSignatures, + "Settings.mPastSignatures"); + mKeySetRefs = new WatchedArrayMap<>(); + mKeySetRefsSnapshot = new SnapshotCache.Auto<>(mKeySetRefs, mKeySetRefs, + "Settings.mKeySetRefs"); + mPendingPackages = new WatchedArrayList<>(); + mPendingPackagesSnapshot = new SnapshotCache.Auto<>(mPendingPackages, mPendingPackages, + "Settings.mPendingPackages"); mKeySetManagerService = new KeySetManagerService(mPackages); mHandler = handler; @@ -699,24 +746,27 @@ public final class Settings implements Watchable, Snappable { mBlockUninstallPackages.snapshot(r.mBlockUninstallPackages); mVersion.putAll(r.mVersion); mVerifierDeviceIdentity = r.mVerifierDeviceIdentity; - WatchedSparseArray.snapshot( - mPreferredActivities, r.mPreferredActivities); - WatchedSparseArray.snapshot( - mPersistentPreferredActivities, r.mPersistentPreferredActivities); - WatchedSparseArray.snapshot( - mCrossProfileIntentResolvers, r.mCrossProfileIntentResolvers); + mPreferredActivities = r.mPreferredActivitiesSnapshot.snapshot(); + mPreferredActivitiesSnapshot = new SnapshotCache.Sealed<>(); + mPersistentPreferredActivities = r.mPersistentPreferredActivitiesSnapshot.snapshot(); + mPersistentPreferredActivitiesSnapshot = new SnapshotCache.Sealed<>(); + mCrossProfileIntentResolvers = r.mCrossProfileIntentResolversSnapshot.snapshot(); + mCrossProfileIntentResolversSnapshot = new SnapshotCache.Sealed<>(); + mSharedUsers.snapshot(r.mSharedUsers); mAppIds = r.mAppIds.snapshot(); - WatchedArrayList.snapshot( - mPastSignatures, r.mPastSignatures); - WatchedArrayMap.snapshot( - mKeySetRefs, r.mKeySetRefs); + + mPastSignatures = r.mPastSignaturesSnapshot.snapshot(); + mPastSignaturesSnapshot = new SnapshotCache.Sealed<>(); + mKeySetRefs = r.mKeySetRefsSnapshot.snapshot(); + mKeySetRefsSnapshot = new SnapshotCache.Sealed<>(); + mRenamedPackages.snapshot(r.mRenamedPackages); mNextAppLinkGeneration.snapshot(r.mNextAppLinkGeneration); mDefaultBrowserApp.snapshot(r.mDefaultBrowserApp); // mReadMessages - WatchedArrayList.snapshot( - mPendingPackages, r.mPendingPackages); + mPendingPackages = r.mPendingPackagesSnapshot.snapshot(); + mPendingPackagesSnapshot = new SnapshotCache.Sealed<>(); mSystemDir = null; // mKeySetManagerService; mPermissions = r.mPermissions; diff --git a/services/core/java/com/android/server/power/stats/CpuWakeupStats.java b/services/core/java/com/android/server/power/stats/CpuWakeupStats.java index 5f76fbc0ec36..79e35c2daf6f 100644 --- a/services/core/java/com/android/server/power/stats/CpuWakeupStats.java +++ b/services/core/java/com/android/server/power/stats/CpuWakeupStats.java @@ -384,7 +384,7 @@ public class CpuWakeupStats { private static final class Wakeup { private static final String PARSER_TAG = "CpuWakeupStats.Wakeup"; private static final String ABORT_REASON_PREFIX = "Abort"; - private static final Pattern sIrqPattern = Pattern.compile("(\\d+)\\s+(\\S+)"); + private static final Pattern sIrqPattern = Pattern.compile("^(\\d+)\\s+(\\S+)"); String mRawReason; long mElapsedMillis; @@ -409,7 +409,7 @@ public class CpuWakeupStats { IrqDevice[] parsedDevices = new IrqDevice[components.length]; for (String component : components) { - final Matcher matcher = sIrqPattern.matcher(component); + final Matcher matcher = sIrqPattern.matcher(component.trim()); if (matcher.find()) { final int line; final String device; diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index d378b11f02bf..c59b90fe1ec5 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -16,12 +16,15 @@ package com.android.server.statusbar; +import static android.Manifest.permission.INTERACT_ACROSS_USERS; +import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; import static android.app.StatusBarManager.DISABLE2_GLOBAL_ACTIONS; import static android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE; import static android.app.StatusBarManager.NAV_BAR_MODE_DEFAULT; import static android.app.StatusBarManager.NAV_BAR_MODE_KIDS; import static android.app.StatusBarManager.NavBarMode; import static android.app.StatusBarManager.SessionFlags; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY; @@ -1304,18 +1307,23 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D "StatusBarManagerService"); } + private boolean doesCallerHoldInteractAcrossUserPermission() { + return mContext.checkCallingPermission(INTERACT_ACROSS_USERS_FULL) == PERMISSION_GRANTED + || mContext.checkCallingPermission(INTERACT_ACROSS_USERS) == PERMISSION_GRANTED; + } + /** * For targetSdk S+ we require STATUS_BAR. For targetSdk < S, we only require EXPAND_STATUS_BAR * but also require that it falls into one of the allowed use-cases to lock down abuse vector. */ private boolean checkCanCollapseStatusBar(String method) { int uid = Binder.getCallingUid(); - int pid = Binder.getCallingUid(); + int pid = Binder.getCallingPid(); if (CompatChanges.isChangeEnabled(LOCK_DOWN_COLLAPSE_STATUS_BAR, uid)) { enforceStatusBar(); } else { if (mContext.checkPermission(Manifest.permission.STATUS_BAR, pid, uid) - != PackageManager.PERMISSION_GRANTED) { + != PERMISSION_GRANTED) { enforceExpandStatusBar(); if (!mActivityTaskManager.canCloseSystemDialogs(pid, uid)) { Slog.e(TAG, "Permission Denial: Method " + method + "() requires permission " @@ -2021,6 +2029,11 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } final int userId = mCurrentUserId; + final int callingUserId = UserHandle.getUserId(Binder.getCallingUid()); + if (mCurrentUserId != callingUserId && !doesCallerHoldInteractAcrossUserPermission()) { + throw new SecurityException("Calling user id: " + callingUserId + + ", cannot call on behalf of current user id: " + mCurrentUserId + "."); + } final long userIdentity = Binder.clearCallingIdentity(); try { Settings.Secure.putIntForUser(mContext.getContentResolver(), diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index abb57bca3704..6a3553315e5a 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -320,9 +320,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub IRemoteCallback.Stub callback = new IRemoteCallback.Stub() { @Override public void sendResult(Bundle data) throws RemoteException { - if (DEBUG) { - Slog.d(TAG, "publish system wallpaper changed!"); - } + Slog.d(TAG, "publish system wallpaper changed!"); notifyWallpaperChanged(wallpaper); } }; @@ -1552,6 +1550,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub mReply.sendResult(null); } catch (RemoteException e) { Binder.restoreCallingIdentity(ident); + Slog.d(TAG, "failed to send callback!", e); } mReply = null; } diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index b9fa80cf2c0f..7e56dbf5602b 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -26,7 +26,6 @@ import static android.view.InsetsController.ANIMATION_TYPE_HIDE; import static android.view.InsetsController.ANIMATION_TYPE_SHOW; import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_HIDDEN; import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_SHOWN; -import static android.view.InsetsState.ITYPE_CAPTION_BAR; import static android.view.InsetsState.ITYPE_CLIMATE_BAR; import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR; import static android.view.InsetsState.ITYPE_IME; @@ -289,9 +288,8 @@ class InsetsPolicy { final boolean alwaysOnTop = token != null && token.isAlwaysOnTop(); // Always use windowing mode fullscreen when get insets for window metrics to make sure it // contains all insets types. - final InsetsState originalState = mDisplayContent.getInsetsPolicy() - .enforceInsetsPolicyForTarget(type, WINDOWING_MODE_FULLSCREEN, alwaysOnTop, - attrs.type, mStateController.getRawInsetsState()); + final InsetsState originalState = enforceInsetsPolicyForTarget(WINDOWING_MODE_FULLSCREEN, + alwaysOnTop, attrs, mStateController.getRawInsetsState()); InsetsState state = adjustVisibilityForTransientTypes(originalState); return adjustInsetsForRoundedCorners(token, state, state == originalState); } @@ -343,56 +341,42 @@ class InsetsPolicy { /** - * Modifies the given {@code state} according to the {@code type} (Inset type) provided by - * the target. - * When performing layout of the target or dispatching insets to the target, we need to exclude - * sources which should not be visible to the target. e.g., the source which represents the - * target window itself, and the IME source when the target is above IME. We also need to - * exclude certain types of insets source for client within specific windowing modes. + * Modifies the given {@code state} according to the target's window state. + * When performing layout of the target or dispatching insets to the target, we need to adjust + * sources based on the target. e.g., the floating window will not receive system bars other + * than caption, and some insets provider may request to override sizes for given window types. + * Since the window type and the insets types provided by the window shall not change at + * runtime, rotation doesn't matter in the layout params. * - * @param type the inset type provided by the target * @param windowingMode the windowing mode of the target * @param isAlwaysOnTop is the target always on top - * @param windowType the type of the target + * @param attrs the layout params of the target * @param state the input inset state containing all the sources * @return The state stripped of the necessary information. */ - InsetsState enforceInsetsPolicyForTarget(@InternalInsetsType int type, - @WindowConfiguration.WindowingMode int windowingMode, boolean isAlwaysOnTop, - int windowType, InsetsState state) { + InsetsState enforceInsetsPolicyForTarget(@WindowConfiguration.WindowingMode int windowingMode, + boolean isAlwaysOnTop, WindowManager.LayoutParams attrs, InsetsState state) { boolean stateCopied = false; - if (type != ITYPE_INVALID) { + if (attrs.providedInsets != null && attrs.providedInsets.length > 0) { state = new InsetsState(state); stateCopied = true; - state.removeSource(type); - - // Navigation bar doesn't get influenced by anything else - if (type == ITYPE_NAVIGATION_BAR || type == ITYPE_EXTRA_NAVIGATION_BAR) { - state.removeSource(ITYPE_STATUS_BAR); - state.removeSource(ITYPE_CLIMATE_BAR); - state.removeSource(ITYPE_CAPTION_BAR); - state.removeSource(ITYPE_NAVIGATION_BAR); - state.removeSource(ITYPE_EXTRA_NAVIGATION_BAR); - } - - // Status bar doesn't get influenced by caption bar - if (type == ITYPE_STATUS_BAR || type == ITYPE_CLIMATE_BAR) { - state.removeSource(ITYPE_CAPTION_BAR); + for (int i = attrs.providedInsets.length - 1; i >= 0; i--) { + state.removeSource(attrs.providedInsets[i].type); } } ArrayMap<Integer, WindowContainerInsetsSourceProvider> providers = mStateController .getSourceProviders(); for (int i = providers.size() - 1; i >= 0; i--) { WindowContainerInsetsSourceProvider otherProvider = providers.valueAt(i); - if (otherProvider.overridesFrame(windowType)) { + if (otherProvider.overridesFrame(attrs.type)) { if (!stateCopied) { state = new InsetsState(state); stateCopied = true; } InsetsSource override = new InsetsSource(state.getSource(otherProvider.getSource().getType())); - override.setFrame(otherProvider.getOverriddenFrame(windowType)); + override.setFrame(otherProvider.getOverriddenFrame(attrs.type)); state.addSource(override); } } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 6d750a469f8c..f30c4355306c 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -29,7 +29,6 @@ import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS; import static android.os.PowerManager.DRAW_WAKE_LOCK; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.InsetsState.ITYPE_IME; -import static android.view.InsetsState.ITYPE_INVALID; import static android.view.SurfaceControl.Transaction; import static android.view.SurfaceControl.getGlobalTransaction; import static android.view.ViewRootImpl.LOCAL_LAYOUT; @@ -1673,14 +1672,11 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (rotatedState != null) { return insetsPolicy.adjustInsetsForWindow(this, rotatedState); } - final InsetsSourceProvider provider = getControllableInsetProvider(); - final @InternalInsetsType int insetTypeProvidedByWindow = provider != null - ? provider.getSource().getType() : ITYPE_INVALID; final InsetsState rawInsetsState = mFrozenInsetsState != null ? mFrozenInsetsState : getMergedInsetsState(); final InsetsState insetsStateForWindow = insetsPolicy - .enforceInsetsPolicyForTarget(insetTypeProvidedByWindow, - getWindowingMode(), isAlwaysOnTop(), mAttrs.type, rawInsetsState); + .enforceInsetsPolicyForTarget( + getWindowingMode(), isAlwaysOnTop(), mAttrs, rawInsetsState); return insetsPolicy.adjustInsetsForWindow(this, insetsStateForWindow, includeTransient); } @@ -5713,6 +5709,15 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return super.getAnimationLeashParent(); } + @Override + public void onAnimationLeashCreated(Transaction t, SurfaceControl leash) { + super.onAnimationLeashCreated(t, leash); + if (isStartingWindowAssociatedToTask()) { + // Make sure the animation leash is still on top of the task. + t.setLayer(leash, Integer.MAX_VALUE); + } + } + // TODO(b/70040778): We should aim to eliminate the last user of TYPE_APPLICATION_MEDIA // then we can drop all negative layering on the windowing side and simply inherit // the default implementation here. diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp new file mode 100644 index 000000000000..939fb6a9d170 --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/Android.bp @@ -0,0 +1,62 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "FrameworksInputMethodSystemServerTests", + defaults: [ + "modules-utils-testable-device-config-defaults", + ], + + srcs: [ + "src/**/*.java", + ], + + static_libs: [ + "androidx.test.core", + "androidx.test.runner", + "androidx.test.espresso.core", + "androidx.test.espresso.contrib", + "androidx.test.ext.truth", + "frameworks-base-testutils", + "mockito-target-extended-minus-junit4", + "platform-test-annotations", + "services.core", + "servicestests-core-utils", + "servicestests-utils-mockito-extended", + "truth-prebuilt", + ], + + libs: [ + "android.test.mock", + "android.test.base", + "android.test.runner", + ], + + certificate: "platform", + platform_apis: true, + test_suites: ["device-tests"], + + optimize: { + enabled: false, + }, +} diff --git a/services/tests/InputMethodSystemServerTests/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/AndroidManifest.xml new file mode 100644 index 000000000000..12e7cfc28a56 --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/AndroidManifest.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.frameworks.inputmethodtests"> + + <uses-sdk android:targetSdkVersion="31" /> + + <!-- Permissions required for granting and logging --> + <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/> + <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG"/> + <uses-permission android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG"/> + <uses-permission android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD"/> + + <!-- Permissions for reading system info --> + <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" /> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> + + <application android:testOnly="true" + android:debuggable="true"> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.frameworks.inputmethodtests" + android:label="Frameworks InputMethod System Service Tests" /> + +</manifest> diff --git a/services/tests/InputMethodSystemServerTests/AndroidTest.xml b/services/tests/InputMethodSystemServerTests/AndroidTest.xml new file mode 100644 index 000000000000..92be78060da8 --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/AndroidTest.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="Runs Frameworks InputMethod System Services Tests."> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-instrumentation" /> + + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="install-arg" value="-t" /> + <option name="test-file-name" value="FrameworksInputMethodSystemServerTests.apk" /> + </target_preparer> + + <option name="test-tag" value="FrameworksInputMethodSystemServerTests" /> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.frameworks.inputmethodtests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> + + <!-- Collect the files in the dump directory for debugging --> + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="directory-keys" value="/sdcard/FrameworksInputMethodSystemServerTests/" /> + <option name="collect-on-run-ended-only" value="true" /> + </metrics_collector> +</configuration> diff --git a/services/tests/InputMethodSystemServerTests/OWNERS b/services/tests/InputMethodSystemServerTests/OWNERS new file mode 100644 index 000000000000..1f2c036773a4 --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/OWNERS @@ -0,0 +1 @@ +include /services/core/java/com/android/server/inputmethod/OWNERS diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java new file mode 100644 index 000000000000..3fbc4004785d --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.inputmethod; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManagerInternal; +import android.content.Context; +import android.content.pm.PackageManagerInternal; +import android.hardware.display.DisplayManagerInternal; +import android.hardware.input.IInputManager; +import android.hardware.input.InputManager; +import android.os.Binder; +import android.os.IBinder; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.view.inputmethod.EditorInfo; +import android.window.ImeOnBackInvokedDispatcher; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.internal.compat.IPlatformCompat; +import com.android.internal.inputmethod.IInputMethod; +import com.android.internal.inputmethod.IInputMethodClient; +import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; +import com.android.internal.inputmethod.IRemoteInputConnection; +import com.android.internal.inputmethod.InputBindResult; +import com.android.internal.view.IInputMethodManager; +import com.android.server.LocalServices; +import com.android.server.ServiceThread; +import com.android.server.SystemServerInitThreadPool; +import com.android.server.SystemService; +import com.android.server.input.InputManagerInternal; +import com.android.server.pm.UserManagerInternal; +import com.android.server.wm.WindowManagerInternal; + +import org.junit.After; +import org.junit.Before; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +/** Base class for testing {@link InputMethodManagerService}. */ +public class InputMethodManagerServiceTestBase { + protected static final String TEST_SELECTED_IME_ID = "test.ime"; + protected static final String TEST_EDITOR_PKG_NAME = "test.editor"; + protected static final String TEST_FOCUSED_WINDOW_NAME = "test.editor/activity"; + protected static final WindowManagerInternal.ImeTargetInfo TEST_IME_TARGET_INFO = + new WindowManagerInternal.ImeTargetInfo( + TEST_FOCUSED_WINDOW_NAME, + TEST_FOCUSED_WINDOW_NAME, + TEST_FOCUSED_WINDOW_NAME, + TEST_FOCUSED_WINDOW_NAME, + TEST_FOCUSED_WINDOW_NAME); + protected static final InputBindResult SUCCESS_WAITING_IME_BINDING_RESULT = + new InputBindResult( + InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING, + null, + null, + null, + "0", + 0, + null, + false); + + @Mock protected WindowManagerInternal mMockWindowManagerInternal; + @Mock protected ActivityManagerInternal mMockActivityManagerInternal; + @Mock protected PackageManagerInternal mMockPackageManagerInternal; + @Mock protected InputManagerInternal mMockInputManagerInternal; + @Mock protected DisplayManagerInternal mMockDisplayManagerInternal; + @Mock protected UserManagerInternal mMockUserManagerInternal; + @Mock protected InputMethodBindingController mMockInputMethodBindingController; + @Mock protected IInputMethodClient mMockInputMethodClient; + @Mock protected IBinder mWindowToken; + @Mock protected IRemoteInputConnection mMockRemoteInputConnection; + @Mock protected IRemoteAccessibilityInputConnection mMockRemoteAccessibilityInputConnection; + @Mock protected ImeOnBackInvokedDispatcher mMockImeOnBackInvokedDispatcher; + @Mock protected IInputMethodManager.Stub mMockIInputMethodManager; + @Mock protected IPlatformCompat.Stub mMockIPlatformCompat; + @Mock protected IInputMethod mMockInputMethod; + @Mock protected IBinder mMockInputMethodBinder; + @Mock protected IInputManager mMockIInputManager; + + protected Context mContext; + protected MockitoSession mMockingSession; + protected int mTargetSdkVersion; + protected int mCallingUserId; + protected EditorInfo mEditorInfo; + protected IInputMethodInvoker mMockInputMethodInvoker; + protected InputMethodManagerService mInputMethodManagerService; + protected ServiceThread mServiceThread; + + @Before + public void setUp() throws RemoteException { + mMockingSession = + mockitoSession() + .initMocks(this) + .strictness(Strictness.LENIENT) + .mockStatic(LocalServices.class) + .mockStatic(ServiceManager.class) + .mockStatic(SystemServerInitThreadPool.class) + .startMocking(); + + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + spyOn(mContext); + + mTargetSdkVersion = mContext.getApplicationInfo().targetSdkVersion; + mCallingUserId = UserHandle.getCallingUserId(); + mEditorInfo = new EditorInfo(); + mEditorInfo.packageName = TEST_EDITOR_PKG_NAME; + + // Injecting and mocking local services. + doReturn(mMockWindowManagerInternal) + .when(() -> LocalServices.getService(WindowManagerInternal.class)); + doReturn(mMockActivityManagerInternal) + .when(() -> LocalServices.getService(ActivityManagerInternal.class)); + doReturn(mMockPackageManagerInternal) + .when(() -> LocalServices.getService(PackageManagerInternal.class)); + doReturn(mMockInputManagerInternal) + .when(() -> LocalServices.getService(InputManagerInternal.class)); + doReturn(mMockDisplayManagerInternal) + .when(() -> LocalServices.getService(DisplayManagerInternal.class)); + doReturn(mMockUserManagerInternal) + .when(() -> LocalServices.getService(UserManagerInternal.class)); + doReturn(mMockIInputMethodManager) + .when(() -> ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE)); + doReturn(mMockIPlatformCompat) + .when(() -> ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); + + // Stubbing out context related methods to avoid the system holding strong references to + // InputMethodManagerService. + doNothing().when(mContext).enforceCallingPermission(anyString(), anyString()); + doNothing().when(mContext).sendBroadcastAsUser(any(), any()); + doReturn(null).when(mContext).registerReceiver(any(), any()); + doReturn(null) + .when(mContext) + .registerReceiverAsUser(any(), any(), any(), anyString(), any(), anyInt()); + + // Injecting and mocked InputMethodBindingController and InputMethod. + mMockInputMethodInvoker = IInputMethodInvoker.create(mMockInputMethod); + InputManager.resetInstance(mMockIInputManager); + synchronized (ImfLock.class) { + when(mMockInputMethodBindingController.getCurMethod()) + .thenReturn(mMockInputMethodInvoker); + when(mMockInputMethodBindingController.bindCurrentMethod()) + .thenReturn(SUCCESS_WAITING_IME_BINDING_RESULT); + doNothing().when(mMockInputMethodBindingController).unbindCurrentMethod(); + when(mMockInputMethodBindingController.getSelectedMethodId()) + .thenReturn(TEST_SELECTED_IME_ID); + } + + // Shuffling around all other initialization to make the test runnable. + when(mMockIInputManager.getInputDeviceIds()).thenReturn(new int[0]); + when(mMockIInputMethodManager.isImeTraceEnabled()).thenReturn(false); + when(mMockIPlatformCompat.isChangeEnabledByUid(anyLong(), anyInt())).thenReturn(true); + when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(true); + when(mMockUserManagerInternal.getProfileIds(anyInt(), anyBoolean())) + .thenReturn(new int[] {0}); + when(mMockActivityManagerInternal.isSystemReady()).thenReturn(true); + when(mMockPackageManagerInternal.getPackageUid(anyString(), anyLong(), anyInt())) + .thenReturn(Binder.getCallingUid()); + when(mMockWindowManagerInternal.onToggleImeRequested(anyBoolean(), any(), any(), anyInt())) + .thenReturn(TEST_IME_TARGET_INFO); + when(mMockInputMethodClient.asBinder()).thenReturn(mMockInputMethodBinder); + + // Used by lazy initializing draw IMS nav bar at InputMethodManagerService#systemRunning(), + // which is ok to be mocked out for now. + doReturn(null).when(() -> SystemServerInitThreadPool.submit(any(), anyString())); + + mServiceThread = + new ServiceThread( + "TestServiceThread", + Process.THREAD_PRIORITY_FOREGROUND, /* allowIo */ + false); + mInputMethodManagerService = + new InputMethodManagerService( + mContext, mServiceThread, mMockInputMethodBindingController); + + // Start a InputMethodManagerService.Lifecycle to publish and manage the lifecycle of + // InputMethodManagerService, which is closer to the real situation. + InputMethodManagerService.Lifecycle lifecycle = + new InputMethodManagerService.Lifecycle(mContext, mInputMethodManagerService); + + // Public local InputMethodManagerService. + lifecycle.onStart(); + try { + // After this boot phase, services can broadcast Intents. + lifecycle.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + } catch (SecurityException e) { + // Security exception to permission denial is expected in test, mocking out to ensure + // InputMethodManagerService as system ready state. + if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) { + throw e; + } + } + + // Call InputMethodManagerService#addClient() as a preparation to start interacting with it. + mInputMethodManagerService.addClient(mMockInputMethodClient, mMockRemoteInputConnection, 0); + } + + @After + public void tearDown() { + if (mServiceThread != null) { + mServiceThread.quitSafely(); + } + + if (mMockingSession != null) { + mMockingSession.finishMocking(); + } + } + + protected void verifyShowSoftInput(boolean setVisible, boolean showSoftInput) + throws RemoteException { + synchronized (ImfLock.class) { + verify(mMockInputMethodBindingController, times(setVisible ? 1 : 0)) + .setCurrentMethodVisible(); + } + verify(mMockInputMethod, times(showSoftInput ? 1 : 0)) + .showSoftInput(any(), anyInt(), any()); + } + + protected void verifyHideSoftInput(boolean setNotVisible, boolean hideSoftInput) + throws RemoteException { + synchronized (ImfLock.class) { + verify(mMockInputMethodBindingController, times(setNotVisible ? 1 : 0)) + .setCurrentMethodNotVisible(); + } + verify(mMockInputMethod, times(hideSoftInput ? 1 : 0)) + .hideSoftInput(any(), anyInt(), any()); + } +} diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java new file mode 100644 index 000000000000..ffa272943455 --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.inputmethod; + +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING; +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN; +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED; +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION; +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN; +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE; +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN; +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED; +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED; +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; + +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.view.inputmethod.EditorInfo; +import android.window.ImeOnBackInvokedDispatcher; + +import com.android.internal.inputmethod.IInputMethodClient; +import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; +import com.android.internal.inputmethod.IRemoteInputConnection; +import com.android.internal.inputmethod.InputBindResult; +import com.android.internal.inputmethod.InputMethodDebug; +import com.android.internal.inputmethod.StartInputFlags; +import com.android.internal.inputmethod.StartInputReason; +import com.android.server.wm.WindowManagerInternal; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.ArrayList; +import java.util.List; + +/** + * Test the behavior of {@link InputMethodManagerService#startInputOrWindowGainedFocus(int, + * IInputMethodClient, IBinder, int, int, int, EditorInfo, IRemoteInputConnection, + * IRemoteAccessibilityInputConnection, int, int, ImeOnBackInvokedDispatcher)}. + */ +@RunWith(Parameterized.class) +public class InputMethodManagerServiceWindowGainedFocusTest + extends InputMethodManagerServiceTestBase { + private static final String TAG = "IMMSWindowGainedFocusTest"; + + private static final int[] SOFT_INPUT_STATE_FLAGS = + new int[] { + SOFT_INPUT_STATE_UNSPECIFIED, + SOFT_INPUT_STATE_UNCHANGED, + SOFT_INPUT_STATE_HIDDEN, + SOFT_INPUT_STATE_ALWAYS_HIDDEN, + SOFT_INPUT_STATE_VISIBLE, + SOFT_INPUT_STATE_ALWAYS_VISIBLE + }; + private static final int[] SOFT_INPUT_ADJUST_FLAGS = + new int[] { + SOFT_INPUT_ADJUST_UNSPECIFIED, + SOFT_INPUT_ADJUST_RESIZE, + SOFT_INPUT_ADJUST_PAN, + SOFT_INPUT_ADJUST_NOTHING + }; + private static final int DEFAULT_SOFT_INPUT_FLAG = + StartInputFlags.VIEW_HAS_FOCUS | StartInputFlags.IS_TEXT_EDITOR; + + @Parameterized.Parameters(name = "softInputState={0}, softInputAdjustment={1}") + public static List<Object[]> softInputModeConfigs() { + ArrayList<Object[]> params = new ArrayList<>(); + for (int softInputState : SOFT_INPUT_STATE_FLAGS) { + for (int softInputAdjust : SOFT_INPUT_ADJUST_FLAGS) { + params.add(new Object[] {softInputState, softInputAdjust}); + } + } + return params; + } + + private final int mSoftInputState; + private final int mSoftInputAdjustment; + + public InputMethodManagerServiceWindowGainedFocusTest( + int softInputState, int softInputAdjustment) { + mSoftInputState = softInputState; + mSoftInputAdjustment = softInputAdjustment; + } + + @Test + public void startInputOrWindowGainedFocus_forwardNavigation() throws RemoteException { + mockHasImeFocusAndRestoreImeVisibility(false /* restoreImeVisibility */); + + assertThat( + startInputOrWindowGainedFocus( + DEFAULT_SOFT_INPUT_FLAG, true /* forwardNavigation */)) + .isEqualTo(SUCCESS_WAITING_IME_BINDING_RESULT); + + switch (mSoftInputState) { + case SOFT_INPUT_STATE_UNSPECIFIED: + boolean showSoftInput = mSoftInputAdjustment == SOFT_INPUT_ADJUST_RESIZE; + verifyShowSoftInput( + showSoftInput /* setVisible */, showSoftInput /* showSoftInput */); + // Soft input was hidden by default, so it doesn't need to call + // {@code IMS#hideSoftInput()}. + verifyHideSoftInput(!showSoftInput /* setNotVisible */, false /* hideSoftInput */); + break; + case SOFT_INPUT_STATE_VISIBLE: + case SOFT_INPUT_STATE_ALWAYS_VISIBLE: + verifyShowSoftInput(true /* setVisible */, true /* showSoftInput */); + verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */); + break; + case SOFT_INPUT_STATE_UNCHANGED: // Do nothing + verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */); + verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */); + break; + case SOFT_INPUT_STATE_HIDDEN: + case SOFT_INPUT_STATE_ALWAYS_HIDDEN: + verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */); + // Soft input was hidden by default, so it doesn't need to call + // {@code IMS#hideSoftInput()}. + verifyHideSoftInput(true /* setNotVisible */, false /* hideSoftInput */); + break; + default: + throw new IllegalStateException( + "Unhandled soft input mode: " + + InputMethodDebug.softInputModeToString(mSoftInputState)); + } + } + + @Test + public void startInputOrWindowGainedFocus_notForwardNavigation() throws RemoteException { + mockHasImeFocusAndRestoreImeVisibility(false /* restoreImeVisibility */); + + assertThat( + startInputOrWindowGainedFocus( + DEFAULT_SOFT_INPUT_FLAG, false /* forwardNavigation */)) + .isEqualTo(SUCCESS_WAITING_IME_BINDING_RESULT); + + switch (mSoftInputState) { + case SOFT_INPUT_STATE_UNSPECIFIED: + boolean hideSoftInput = mSoftInputAdjustment != SOFT_INPUT_ADJUST_RESIZE; + verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */); + // Soft input was hidden by default, so it doesn't need to call + // {@code IMS#hideSoftInput()}. + verifyHideSoftInput(hideSoftInput /* setNotVisible */, false /* hideSoftInput */); + break; + case SOFT_INPUT_STATE_VISIBLE: + case SOFT_INPUT_STATE_HIDDEN: + case SOFT_INPUT_STATE_UNCHANGED: // Do nothing + verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */); + verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */); + break; + case SOFT_INPUT_STATE_ALWAYS_VISIBLE: + verifyShowSoftInput(true /* setVisible */, true /* showSoftInput */); + verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */); + break; + case SOFT_INPUT_STATE_ALWAYS_HIDDEN: + verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */); + // Soft input was hidden by default, so it doesn't need to call + // {@code IMS#hideSoftInput()}. + verifyHideSoftInput(true /* setNotVisible */, false /* hideSoftInput */); + break; + default: + throw new IllegalStateException( + "Unhandled soft input mode: " + + InputMethodDebug.softInputModeToString(mSoftInputState)); + } + } + + @Test + public void startInputOrWindowGainedFocus_userNotRunning() throws RemoteException { + when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(false); + + assertThat( + startInputOrWindowGainedFocus( + DEFAULT_SOFT_INPUT_FLAG, true /* forwardNavigation */)) + .isEqualTo(InputBindResult.INVALID_USER); + verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */); + verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */); + } + + @Test + public void startInputOrWindowGainedFocus_invalidFocusStatus() throws RemoteException { + int[] invalidImeClientFocus = + new int[] { + WindowManagerInternal.ImeClientFocusResult.NOT_IME_TARGET_WINDOW, + WindowManagerInternal.ImeClientFocusResult.DISPLAY_ID_MISMATCH, + WindowManagerInternal.ImeClientFocusResult.INVALID_DISPLAY_ID + }; + InputBindResult[] inputBingResult = + new InputBindResult[] { + InputBindResult.NOT_IME_TARGET_WINDOW, + InputBindResult.DISPLAY_ID_MISMATCH, + InputBindResult.INVALID_DISPLAY_ID + }; + + for (int i = 0; i < invalidImeClientFocus.length; i++) { + when(mMockWindowManagerInternal.hasInputMethodClientFocus( + any(), anyInt(), anyInt(), anyInt())) + .thenReturn(invalidImeClientFocus[i]); + + assertThat( + startInputOrWindowGainedFocus( + DEFAULT_SOFT_INPUT_FLAG, true /* forwardNavigation */)) + .isEqualTo(inputBingResult[i]); + verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */); + verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */); + } + } + + private InputBindResult startInputOrWindowGainedFocus( + int startInputFlag, boolean forwardNavigation) { + int softInputMode = mSoftInputState | mSoftInputAdjustment; + if (forwardNavigation) { + softInputMode |= SOFT_INPUT_IS_FORWARD_NAVIGATION; + } + + Log.i( + TAG, + "startInputOrWindowGainedFocus() softInputStateFlag=" + + InputMethodDebug.softInputModeToString(mSoftInputState) + + ", softInputAdjustFlag=" + + InputMethodDebug.softInputModeToString(mSoftInputAdjustment)); + + return mInputMethodManagerService.startInputOrWindowGainedFocus( + StartInputReason.WINDOW_FOCUS_GAIN /* startInputReason */, + mMockInputMethodClient /* client */, + mWindowToken /* windowToken */, + startInputFlag /* startInputFlags */, + softInputMode /* softInputMode */, + 0 /* windowFlags */, + mEditorInfo /* editorInfo */, + mMockRemoteInputConnection /* inputConnection */, + mMockRemoteAccessibilityInputConnection /* remoteAccessibilityInputConnection */, + mTargetSdkVersion /* unverifiedTargetSdkVersion */, + mCallingUserId /* userId */, + mMockImeOnBackInvokedDispatcher /* imeDispatcher */); + } + + private void mockHasImeFocusAndRestoreImeVisibility(boolean restoreImeVisibility) { + when(mMockWindowManagerInternal.hasInputMethodClientFocus( + any(), anyInt(), anyInt(), anyInt())) + .thenReturn(WindowManagerInternal.ImeClientFocusResult.HAS_IME_FOCUS); + when(mMockWindowManagerInternal.shouldRestoreImeVisibility(any())) + .thenReturn(restoreImeVisibility); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java index cb14864876ab..2583f44787ad 100644 --- a/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java @@ -325,7 +325,7 @@ public class DeviceIdleControllerTest { doNothing().when(mAlarmManager).set(anyInt(), anyLong(), anyString(), any(), any()); doNothing().when(mAlarmManager).setExact(anyInt(), anyLong(), anyString(), any(), any()); doNothing().when(mAlarmManager) - .setWindow(anyInt(), anyLong(), anyLong(), anyString(), any(), any()); + .setWindow(anyInt(), anyLong(), anyLong(), anyString(), any(), any(Handler.class)); doReturn(mock(Sensor.class)).when(mSensorManager) .getDefaultSensor(eq(Sensor.TYPE_SIGNIFICANT_MOTION), eq(true)); doReturn(true).when(mSensorManager).registerListener(any(), any(), anyInt()); @@ -1111,12 +1111,12 @@ public class DeviceIdleControllerTest { alarmManagerInOrder.verify(mAlarmManager).setWindow( eq(AlarmManager.ELAPSED_REALTIME), eq(idleAfterInactiveExpiryTime), - anyLong(), anyString(), any(), any()); + anyLong(), anyString(), any(), any(Handler.class)); // Maintenance alarm alarmManagerInOrder.verify(mAlarmManager).setWindow( eq(AlarmManager.ELAPSED_REALTIME_WAKEUP), eq(idleAfterInactiveExpiryTime + idlingTimeMs), - anyLong(), anyString(), any(), any()); + anyLong(), anyString(), any(), any(Handler.class)); final AlarmManager.OnAlarmListener progressionListener = alarmListenerCaptor.getAllValues().get(0); @@ -1130,7 +1130,7 @@ public class DeviceIdleControllerTest { alarmManagerInOrder.verify(mAlarmManager).setWindow( eq(AlarmManager.ELAPSED_REALTIME_WAKEUP), eq(mInjector.nowElapsed + idlingTimeMs), - anyLong(), anyString(), any(), any()); + anyLong(), anyString(), any(), any(Handler.class)); for (int i = 0; i < 2; ++i) { // IDLE->MAINTENANCE alarm @@ -1144,12 +1144,12 @@ public class DeviceIdleControllerTest { alarmManagerInOrder.verify(mAlarmManager).setWindow( eq(AlarmManager.ELAPSED_REALTIME), eq(maintenanceExpiryTime), - anyLong(), anyString(), any(), any()); + anyLong(), anyString(), any(), any(Handler.class)); // Set IDLE->MAINTENANCE alarmManagerInOrder.verify(mAlarmManager).setWindow( eq(AlarmManager.ELAPSED_REALTIME_WAKEUP), eq(maintenanceExpiryTime + idlingTimeMs), - anyLong(), anyString(), any(), any()); + anyLong(), anyString(), any(), any(Handler.class)); // MAINTENANCE->IDLE alarm mInjector.nowElapsed = mDeviceIdleController.getNextLightAlarmTimeForTesting(); @@ -1159,7 +1159,7 @@ public class DeviceIdleControllerTest { alarmManagerInOrder.verify(mAlarmManager).setWindow( eq(AlarmManager.ELAPSED_REALTIME_WAKEUP), eq(mInjector.nowElapsed + idlingTimeMs), - anyLong(), anyString(), any(), any()); + anyLong(), anyString(), any(), any(Handler.class)); } } @@ -2019,7 +2019,8 @@ public class DeviceIdleControllerTest { final ArgumentCaptor<AlarmManager.OnAlarmListener> alarmListener = ArgumentCaptor .forClass(AlarmManager.OnAlarmListener.class); doNothing().when(mAlarmManager).setWindow( - anyInt(), anyLong(), anyLong(), eq("DeviceIdleController.motion"), any(), any()); + anyInt(), anyLong(), anyLong(), eq("DeviceIdleController.motion"), any(), + any(Handler.class)); doNothing().when(mAlarmManager).setWindow(anyInt(), anyLong(), anyLong(), eq("DeviceIdleController.motion_registration"), alarmListener.capture(), any()); @@ -2063,7 +2064,8 @@ public class DeviceIdleControllerTest { final ArgumentCaptor<AlarmManager.OnAlarmListener> alarmListener = ArgumentCaptor .forClass(AlarmManager.OnAlarmListener.class); doNothing().when(mAlarmManager).setWindow( - anyInt(), anyLong(), anyLong(), eq("DeviceIdleController.motion"), any(), any()); + anyInt(), anyLong(), anyLong(), eq("DeviceIdleController.motion"), any(), + any(Handler.class)); doNothing().when(mAlarmManager).setWindow(anyInt(), anyLong(), anyLong(), eq("DeviceIdleController.motion_registration"), alarmListener.capture(), any()); @@ -2130,7 +2132,7 @@ public class DeviceIdleControllerTest { eq(SensorManager.SENSOR_DELAY_NORMAL)); inOrder.verify(mAlarmManager).setWindow( anyInt(), eq(mInjector.nowElapsed + mConstants.MOTION_INACTIVE_TIMEOUT), anyLong(), - eq("DeviceIdleController.motion"), any(), any()); + eq("DeviceIdleController.motion"), any(), any(Handler.class)); final SensorEventListener listener = listenerCaptor.getValue(); // Trigger motion @@ -2140,7 +2142,7 @@ public class DeviceIdleControllerTest { final ArgumentCaptor<Long> registrationTimeCaptor = ArgumentCaptor.forClass(Long.class); inOrder.verify(mAlarmManager).setWindow( anyInt(), registrationTimeCaptor.capture(), anyLong(), - eq("DeviceIdleController.motion_registration"), any(), any()); + eq("DeviceIdleController.motion_registration"), any(), any(Handler.class)); // Make sure the listener is re-registered. mInjector.nowElapsed = registrationTimeCaptor.getValue(); @@ -2150,7 +2152,7 @@ public class DeviceIdleControllerTest { eq(SensorManager.SENSOR_DELAY_NORMAL)); final ArgumentCaptor<Long> timeoutCaptor = ArgumentCaptor.forClass(Long.class); inOrder.verify(mAlarmManager).setWindow(anyInt(), timeoutCaptor.capture(), anyLong(), - eq("DeviceIdleController.motion"), any(), any()); + eq("DeviceIdleController.motion"), any(), any(Handler.class)); // No motion before timeout stationaryListener.motionExpected = false; diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java index 1753fc7a61e4..fc737d06059d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java @@ -41,6 +41,7 @@ import android.app.ActivityManagerInternal; import android.app.IActivityManager; import android.app.UiModeManager; import android.app.job.JobInfo; +import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.app.usage.UsageStatsManagerInternal; import android.content.ComponentName; @@ -235,6 +236,59 @@ public class JobSchedulerServiceTest { mService.getMinJobExecutionGuaranteeMs(jobDef)); } + + /** + * Confirm that {@link JobSchedulerService#getRescheduleJobForFailureLocked(JobStatus, int)} + * returns a job with the correct delay and deadline constraints. + */ + @Test + public void testGetRescheduleJobForFailure() { + final long nowElapsed = sElapsedRealtimeClock.millis(); + final long initialBackoffMs = MINUTE_IN_MILLIS; + mService.mConstants.SYSTEM_STOP_TO_FAILURE_RATIO = 3; + + JobStatus originalJob = createJobStatus("testGetRescheduleJobForFailure", + createJobInfo() + .setBackoffCriteria(initialBackoffMs, JobInfo.BACKOFF_POLICY_LINEAR)); + assertEquals(JobStatus.NO_EARLIEST_RUNTIME, originalJob.getEarliestRunTime()); + assertEquals(JobStatus.NO_LATEST_RUNTIME, originalJob.getLatestRunTimeElapsed()); + + // failure = 0, systemStop = 1 + JobStatus rescheduledJob = mService.getRescheduleJobForFailureLocked(originalJob, + JobParameters.INTERNAL_STOP_REASON_DEVICE_THERMAL); + assertEquals(nowElapsed + initialBackoffMs, rescheduledJob.getEarliestRunTime()); + assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed()); + + // failure = 0, systemStop = 2 + rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob, + JobParameters.INTERNAL_STOP_REASON_PREEMPT); + // failure = 0, systemStop = 3 + rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob, + JobParameters.INTERNAL_STOP_REASON_CONSTRAINTS_NOT_SATISFIED); + assertEquals(nowElapsed + initialBackoffMs, rescheduledJob.getEarliestRunTime()); + assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed()); + + // failure = 0, systemStop = 2 * SYSTEM_STOP_TO_FAILURE_RATIO + for (int i = 0; i < mService.mConstants.SYSTEM_STOP_TO_FAILURE_RATIO; ++i) { + rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob, + JobParameters.INTERNAL_STOP_REASON_RTC_UPDATED); + } + assertEquals(nowElapsed + 2 * initialBackoffMs, rescheduledJob.getEarliestRunTime()); + assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed()); + + // failure = 1, systemStop = 2 * SYSTEM_STOP_TO_FAILURE_RATIO + rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob, + JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + assertEquals(nowElapsed + 3 * initialBackoffMs, rescheduledJob.getEarliestRunTime()); + assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed()); + + // failure = 2, systemStop = 2 * SYSTEM_STOP_TO_FAILURE_RATIO + rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob, + JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH); + assertEquals(nowElapsed + 4 * initialBackoffMs, rescheduledJob.getEarliestRunTime()); + assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed()); + } + /** * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job * with the correct delay and deadline constraints if the periodic job is scheduled with the @@ -544,14 +598,16 @@ public class JobSchedulerServiceTest { final long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS; JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow_failedJob", createJobInfo().setPeriodic(HOUR_IN_MILLIS)); - JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job); + JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job, + JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH); JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob); assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime()); assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); advanceElapsedClock(5 * MINUTE_IN_MILLIS); // now + 5 minutes - failedJob = mService.getRescheduleJobForFailureLocked(job); + failedJob = mService.getRescheduleJobForFailureLocked(job, + JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH); advanceElapsedClock(5 * MINUTE_IN_MILLIS); // now + 10 minutes rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob); @@ -559,7 +615,8 @@ public class JobSchedulerServiceTest { assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); advanceElapsedClock(35 * MINUTE_IN_MILLIS); // now + 45 minutes - failedJob = mService.getRescheduleJobForFailureLocked(job); + failedJob = mService.getRescheduleJobForFailureLocked(job, + JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH); advanceElapsedClock(10 * MINUTE_IN_MILLIS); // now + 55 minutes rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob); @@ -569,7 +626,8 @@ public class JobSchedulerServiceTest { assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed()); advanceElapsedClock(2 * MINUTE_IN_MILLIS); // now + 57 minutes - failedJob = mService.getRescheduleJobForFailureLocked(job); + failedJob = mService.getRescheduleJobForFailureLocked(job, + JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH); advanceElapsedClock(2 * MINUTE_IN_MILLIS); // now + 59 minutes rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob); @@ -665,7 +723,8 @@ public class JobSchedulerServiceTest { public void testGetRescheduleJobForPeriodic_outsideWindow_failedJob() { JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_outsideWindow_failedJob", createJobInfo().setPeriodic(HOUR_IN_MILLIS)); - JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job); + JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job, + JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH); long now = sElapsedRealtimeClock.millis(); long nextWindowStartTime = now + HOUR_IN_MILLIS; long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS; @@ -701,7 +760,8 @@ public class JobSchedulerServiceTest { JobStatus job = createJobStatus( "testGetRescheduleJobForPeriodic_outsideWindow_flex_failedJob", createJobInfo().setPeriodic(HOUR_IN_MILLIS, 30 * MINUTE_IN_MILLIS)); - JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job); + JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job, + JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH); // First window starts 30 minutes from now. advanceElapsedClock(30 * MINUTE_IN_MILLIS); long now = sElapsedRealtimeClock.millis(); @@ -742,7 +802,8 @@ public class JobSchedulerServiceTest { JobStatus job = createJobStatus( "testGetRescheduleJobForPeriodic_outsideWindow_flex_failedJob_longPeriod", createJobInfo().setPeriodic(7 * DAY_IN_MILLIS, 9 * HOUR_IN_MILLIS)); - JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job); + JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job, + JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH); // First window starts 6.625 days from now. advanceElapsedClock(6 * DAY_IN_MILLIS + 15 * HOUR_IN_MILLIS); long now = sElapsedRealtimeClock.millis(); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java index 59cb43f16ef6..7c435be5ebdd 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java @@ -432,10 +432,10 @@ public class BatteryControllerTest { assertFalse(topStartedJobs.contains(unrelatedJob)); // Job cleanup - mBatteryController.maybeStopTrackingJobLocked(batteryJob, null, false); - mBatteryController.maybeStopTrackingJobLocked(chargingJob, null, false); - mBatteryController.maybeStopTrackingJobLocked(bothPowerJob, null, false); - mBatteryController.maybeStopTrackingJobLocked(unrelatedJob, null, false); + mBatteryController.maybeStopTrackingJobLocked(batteryJob, null); + mBatteryController.maybeStopTrackingJobLocked(chargingJob, null); + mBatteryController.maybeStopTrackingJobLocked(bothPowerJob, null); + mBatteryController.maybeStopTrackingJobLocked(unrelatedJob, null); assertFalse(trackedJobs.contains(batteryJob)); assertFalse(trackedJobs.contains(chargingJob)); assertFalse(trackedJobs.contains(bothPowerJob)); 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 674e500d2e41..3bee6871b4b9 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 @@ -489,19 +489,22 @@ public class FlexibilityControllerTest { JobInfo.Builder jb = createJob(0).setOverrideDeadline(1000L); JobStatus js = createJobStatus("time", jb); js = new JobStatus( - js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 2, FROZEN_TIME, FROZEN_TIME); + js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 2, /* numSystemStops */ 0, + FROZEN_TIME, FROZEN_TIME); assertEquals(mFcConfig.RESCHEDULED_JOB_DEADLINE_MS, mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0)); js = new JobStatus( - js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 3, FROZEN_TIME, FROZEN_TIME); + js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 2, /* numSystemStops */ 1, + FROZEN_TIME, FROZEN_TIME); assertEquals(2 * mFcConfig.RESCHEDULED_JOB_DEADLINE_MS, mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0)); js = new JobStatus( - js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 10, FROZEN_TIME, FROZEN_TIME); + js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 0, /* numSystemStops */ 10, + FROZEN_TIME, FROZEN_TIME); assertEquals(mFcConfig.MAX_RESCHEDULED_DEADLINE_MS, mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0)); } @@ -637,7 +640,12 @@ public class FlexibilityControllerTest { JobInfo.Builder jb = createJob(0); JobStatus js = createJobStatus("time", jb); js = new JobStatus( - js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 1, FROZEN_TIME, FROZEN_TIME); + js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 1, /* numSystemStops */ 0, + FROZEN_TIME, FROZEN_TIME); + assertFalse(js.hasFlexibilityConstraint()); + js = new JobStatus( + js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 0, /* numSystemStops */ 1, + FROZEN_TIME, FROZEN_TIME); assertFalse(js.hasFlexibilityConstraint()); } diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java index 149ae0b81740..7f522b0a1af5 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java @@ -248,20 +248,32 @@ public class JobStatusTest { // Less than 2 failures, priority shouldn't be affected. assertEquals(JobInfo.PRIORITY_MAX, job.getEffectivePriority()); - int backoffAttempt = 1; - job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0); + int numFailures = 1; + int numSystemStops = 0; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); assertEquals(JobInfo.PRIORITY_MAX, job.getEffectivePriority()); // 2+ failures, priority should be lowered as much as possible. - backoffAttempt = 2; - job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0); + numFailures = 2; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); assertEquals(JobInfo.PRIORITY_HIGH, job.getEffectivePriority()); - backoffAttempt = 5; - job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0); + numFailures = 5; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); assertEquals(JobInfo.PRIORITY_HIGH, job.getEffectivePriority()); - backoffAttempt = 8; - job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0); + numFailures = 8; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); assertEquals(JobInfo.PRIORITY_HIGH, job.getEffectivePriority()); + + // System stops shouldn't factor in the downgrade. + numSystemStops = 10; + numFailures = 0; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); + assertEquals(JobInfo.PRIORITY_MAX, job.getEffectivePriority()); } @Test @@ -274,33 +286,48 @@ public class JobStatusTest { // Less than 2 failures, priority shouldn't be affected. assertEquals(JobInfo.PRIORITY_HIGH, job.getEffectivePriority()); - int backoffAttempt = 1; - job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0); + int numFailures = 1; + int numSystemStops = 0; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); assertEquals(JobInfo.PRIORITY_HIGH, job.getEffectivePriority()); // Failures in [2,4), priority should be lowered slightly. - backoffAttempt = 2; - job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0); + numFailures = 2; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); assertEquals(JobInfo.PRIORITY_DEFAULT, job.getEffectivePriority()); - backoffAttempt = 3; - job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0); + numFailures = 3; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); assertEquals(JobInfo.PRIORITY_DEFAULT, job.getEffectivePriority()); // Failures in [4,6), priority should be lowered more. - backoffAttempt = 4; - job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0); + numFailures = 4; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority()); - backoffAttempt = 5; - job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0); + numFailures = 5; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority()); // 6+ failures, priority should be lowered as much as possible. - backoffAttempt = 6; - job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0); + numFailures = 6; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); assertEquals(JobInfo.PRIORITY_MIN, job.getEffectivePriority()); - backoffAttempt = 12; - job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0); + numFailures = 12; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); assertEquals(JobInfo.PRIORITY_MIN, job.getEffectivePriority()); + + // System stops shouldn't factor in the downgrade. + numSystemStops = 10; + numFailures = 0; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); + assertEquals(JobInfo.PRIORITY_HIGH, job.getEffectivePriority()); } /** @@ -317,23 +344,36 @@ public class JobStatusTest { // Less than 6 failures, priority shouldn't be affected. assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority()); - int backoffAttempt = 1; - job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0); + int numFailures = 1; + int numSystemStops = 0; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority()); - backoffAttempt = 4; - job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0); + numFailures = 4; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority()); - backoffAttempt = 5; - job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0); + numFailures = 5; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority()); // 6+ failures, priority should be lowered as much as possible. - backoffAttempt = 6; - job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0); + numFailures = 6; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); assertEquals(JobInfo.PRIORITY_MIN, job.getEffectivePriority()); - backoffAttempt = 12; - job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0); + numFailures = 12; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); assertEquals(JobInfo.PRIORITY_MIN, job.getEffectivePriority()); + + // System stops shouldn't factor in the downgrade. + numSystemStops = 10; + numFailures = 0; + job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures, + numSystemStops, 0, 0); + assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority()); } /** diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java index bb477b18d5fa..b949b3b265af 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java @@ -47,6 +47,7 @@ import android.app.usage.UsageStatsManagerInternal.EstimatedLaunchTimeChangedLis import android.appwidget.AppWidgetManager; import android.content.ComponentName; import android.content.Context; +import android.os.Handler; import android.os.Looper; import android.os.Process; import android.os.SystemClock; @@ -276,13 +277,13 @@ public class PrefetchControllerTest { inOrder.verify(mAlarmManager, timeout(DEFAULT_WAIT_MS).times(1)) .setWindow( anyInt(), eq(sElapsedRealtimeClock.millis() + 4 * HOUR_IN_MILLIS), - anyLong(), eq(TAG_PREFETCH), any(), any()); + anyLong(), eq(TAG_PREFETCH), any(), any(Handler.class)); setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 3 * HOUR_IN_MILLIS); inOrder.verify(mAlarmManager, timeout(DEFAULT_WAIT_MS).times(1)) .setWindow( anyInt(), eq(sElapsedRealtimeClock.millis() + 8 * HOUR_IN_MILLIS), - anyLong(), eq(TAG_PREFETCH), any(), any()); + anyLong(), eq(TAG_PREFETCH), any(), any(Handler.class)); } @Test @@ -414,7 +415,7 @@ public class PrefetchControllerTest { verify(mAlarmManager, timeout(DEFAULT_WAIT_MS).times(1)) .setWindow( anyInt(), eq(sElapsedRealtimeClock.millis() + 3 * HOUR_IN_MILLIS), - anyLong(), eq(TAG_PREFETCH), any(), any()); + anyLong(), eq(TAG_PREFETCH), any(), any(Handler.class)); assertFalse(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH)); assertFalse(jobStatus.isReady()); } @@ -464,7 +465,7 @@ public class PrefetchControllerTest { verify(mAlarmManager, timeout(DEFAULT_WAIT_MS).times(1)) .setWindow( anyInt(), eq(sElapsedRealtimeClock.millis() + 3 * HOUR_IN_MILLIS), - anyLong(), eq(TAG_PREFETCH), any(), any()); + anyLong(), eq(TAG_PREFETCH), any(), any(Handler.class)); assertFalse(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH)); assertFalse(jobStatus.isReady()); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java index 9407968dbb56..17822c6d5004 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java @@ -1356,7 +1356,7 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mQuotaController.getMaxJobExecutionTimeMsLocked((job))); - mQuotaController.maybeStopTrackingJobLocked(job, null, false); + mQuotaController.maybeStopTrackingJobLocked(job, null); } setProcessState(ActivityManager.PROCESS_STATE_RECEIVER); @@ -1441,7 +1441,7 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { assertEquals(mQcConstants.EJ_LIMIT_ACTIVE_MS / 2, mQuotaController.getMaxJobExecutionTimeMsLocked(job)); - mQuotaController.maybeStopTrackingJobLocked(job, null, false); + mQuotaController.maybeStopTrackingJobLocked(job, null); } setProcessState(ActivityManager.PROCESS_STATE_RECEIVER); @@ -1474,7 +1474,7 @@ public class QuotaControllerTest { synchronized (mQuotaController.mLock) { assertEquals(mQcConstants.EJ_LIMIT_ACTIVE_MS / 2, mQuotaController.getMaxJobExecutionTimeMsLocked(job)); - mQuotaController.maybeStopTrackingJobLocked(job, null, false); + mQuotaController.maybeStopTrackingJobLocked(job, null); } setProcessState(ActivityManager.PROCESS_STATE_RECEIVER); @@ -2017,7 +2017,7 @@ public class QuotaControllerTest { setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); } synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null); } advanceElapsedClock(15 * SECOND_IN_MILLIS); @@ -2032,7 +2032,7 @@ public class QuotaControllerTest { setProcessState(ActivityManager.PROCESS_STATE_RECEIVER); } synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null); } advanceElapsedClock(10 * MINUTE_IN_MILLIS + 30 * SECOND_IN_MILLIS); @@ -2090,7 +2090,7 @@ public class QuotaControllerTest { setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING, fgChangerUid); } synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(fgStateChanger, null, false); + mQuotaController.maybeStopTrackingJobLocked(fgStateChanger, null); } advanceElapsedClock(15 * SECOND_IN_MILLIS); @@ -2105,9 +2105,9 @@ public class QuotaControllerTest { setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING, fgChangerUid); } synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(fgStateChanger, null, false); + mQuotaController.maybeStopTrackingJobLocked(fgStateChanger, null); - mQuotaController.maybeStopTrackingJobLocked(unaffected, null, false); + mQuotaController.maybeStopTrackingJobLocked(unaffected, null); assertTrue(mQuotaController.isWithinQuotaLocked(unaffected)); assertTrue(unaffected.isReady()); @@ -2226,8 +2226,8 @@ public class QuotaControllerTest { advanceElapsedClock(MINUTE_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job1, null, false); - mQuotaController.maybeStopTrackingJobLocked(job2, null, false); + mQuotaController.maybeStopTrackingJobLocked(job1, null); + mQuotaController.maybeStopTrackingJobLocked(job2, null); assertFalse(mQuotaController .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); assertEquals(7 * MINUTE_IN_MILLIS, stats.allowedTimePerPeriodMs); @@ -2312,8 +2312,8 @@ public class QuotaControllerTest { advanceElapsedClock(MINUTE_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job1, null, false); - mQuotaController.maybeStopTrackingJobLocked(job2, null, false); + mQuotaController.maybeStopTrackingJobLocked(job1, null); + mQuotaController.maybeStopTrackingJobLocked(job2, null); assertFalse(mQuotaController .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); assertEquals(12, stats.jobCountLimit); @@ -2390,7 +2390,7 @@ public class QuotaControllerTest { advanceElapsedClock(MINUTE_IN_MILLIS); mAppIdleStateChangeListener.triggerTemporaryQuotaBump(SOURCE_PACKAGE, SOURCE_USER_ID); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job1, null, false); + mQuotaController.maybeStopTrackingJobLocked(job1, null); assertTrue(mQuotaController .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); assertEquals(4, stats.sessionCountLimit); @@ -2404,7 +2404,7 @@ public class QuotaControllerTest { advanceElapsedClock(MINUTE_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job2, null, false); + mQuotaController.maybeStopTrackingJobLocked(job2, null); assertFalse(mQuotaController .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX)); assertEquals(4, stats.sessionCountLimit); @@ -2708,7 +2708,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); // Test with timing sessions out of window but still under max execution limit. @@ -2725,7 +2725,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - 2 * HOUR_IN_MILLIS, 55 * MINUTE_IN_MILLIS, 1), false); @@ -2734,7 +2734,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); synchronized (mQuotaController.mLock) { mQuotaController.prepareForExecutionLocked(jobStatus); @@ -2749,7 +2749,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(1)).setWindow( - anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); } @Test @@ -2771,7 +2772,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test with timing sessions out of window. final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); @@ -2782,7 +2783,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test with timing sessions in window but still in quota. final long end = now - (2 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS); @@ -2796,7 +2797,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Add some more sessions, but still in quota. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, @@ -2808,7 +2809,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test when out of quota. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, @@ -2818,7 +2819,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(1)).setWindow( - anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); // Alarm already scheduled, so make sure it's not scheduled again. synchronized (mQuotaController.mLock) { @@ -2826,7 +2828,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(1)).setWindow( - anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); } @Test @@ -2850,7 +2853,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test with timing sessions out of window. final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); @@ -2861,7 +2864,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test with timing sessions in window but still in quota. final long start = now - (6 * HOUR_IN_MILLIS); @@ -2873,7 +2876,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Add some more sessions, but still in quota. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, @@ -2885,7 +2888,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test when out of quota. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, @@ -2895,7 +2898,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(1)).setWindow( - anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); // Alarm already scheduled, so make sure it's not scheduled again. synchronized (mQuotaController.mLock) { @@ -2903,7 +2907,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(1)).setWindow( - anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); } /** @@ -2932,7 +2937,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test with timing sessions out of window. final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); @@ -2943,7 +2948,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test with timing sessions in window but still in quota. final long start = now - (6 * HOUR_IN_MILLIS); @@ -2955,7 +2960,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Add some more sessions, but still in quota. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, @@ -2967,7 +2972,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test when out of quota. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, @@ -2977,7 +2982,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } verify(mAlarmManager, timeout(1000).times(1)).setWindow( - anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); // Alarm already scheduled, so make sure it's not scheduled again. synchronized (mQuotaController.mLock) { @@ -2985,7 +2991,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket); } verify(mAlarmManager, timeout(1000).times(1)).setWindow( - anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); } @Test @@ -3013,7 +3020,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test with timing sessions out of window. final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); @@ -3023,7 +3030,7 @@ public class QuotaControllerTest { mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test with timing sessions in window but still in quota. final long start = now - (6 * HOUR_IN_MILLIS); @@ -3039,7 +3046,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Add some more sessions, but still in quota. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, @@ -3051,7 +3058,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test when out of quota. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, @@ -3061,7 +3068,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(1)).setWindow( - anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); // Alarm already scheduled, so make sure it's not scheduled again. synchronized (mQuotaController.mLock) { @@ -3069,7 +3077,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(1)).setWindow( - anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); } /** Tests that the start alarm is properly rescheduled if the app's bucket is changed. */ @@ -3108,7 +3117,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX); } inOrder.verify(mAlarmManager, timeout(1000).times(0)) - .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); inOrder.verify(mAlarmManager, timeout(1000).times(0)) .cancel(any(AlarmManager.OnAlarmListener.class)); @@ -3123,7 +3133,7 @@ public class QuotaControllerTest { } inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedWorkingAlarmTime), anyLong(), - eq(TAG_QUOTA_CHECK), any(), any()); + eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); final long expectedFrequentAlarmTime = outOfQuotaTime + (8 * HOUR_IN_MILLIS) @@ -3135,7 +3145,7 @@ public class QuotaControllerTest { } inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedFrequentAlarmTime), anyLong(), - eq(TAG_QUOTA_CHECK), any(), any()); + eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); final long expectedRareAlarmTime = outOfQuotaTime + (24 * HOUR_IN_MILLIS) @@ -3146,7 +3156,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, RARE_INDEX); } inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( - anyInt(), eq(expectedRareAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), eq(expectedRareAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); // And back up again. setStandbyBucket(FREQUENT_INDEX, jobStatus); @@ -3156,7 +3167,7 @@ public class QuotaControllerTest { } inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedFrequentAlarmTime), anyLong(), - eq(TAG_QUOTA_CHECK), any(), any()); + eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); setStandbyBucket(WORKING_INDEX, jobStatus); synchronized (mQuotaController.mLock) { @@ -3165,7 +3176,7 @@ public class QuotaControllerTest { } inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedWorkingAlarmTime), anyLong(), - eq(TAG_QUOTA_CHECK), any(), any()); + eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); setStandbyBucket(ACTIVE_INDEX, jobStatus); synchronized (mQuotaController.mLock) { @@ -3173,7 +3184,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX); } inOrder.verify(mAlarmManager, timeout(1000).times(0)) - .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .cancel(any(AlarmManager.OnAlarmListener.class)); } @@ -3210,7 +3222,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Valid time in the future, so the count should be used. stats.jobRateLimitExpirationTimeElapsed = now + 5 * MINUTE_IN_MILLIS / 2; @@ -3221,7 +3233,7 @@ public class QuotaControllerTest { } verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedWorkingAlarmTime), anyLong(), - eq(TAG_QUOTA_CHECK), any(), any()); + eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); } /** @@ -3318,7 +3330,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(1)).setWindow( - anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); } private void runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck() { @@ -3355,7 +3368,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(1)).setWindow( - anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); } @Test @@ -3711,7 +3725,7 @@ public class QuotaControllerTest { } advanceElapsedClock(5 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null); } assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); } @@ -3737,7 +3751,7 @@ public class QuotaControllerTest { } advanceElapsedClock(5 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null); } expected.add(createTimingSession(start, 5 * SECOND_IN_MILLIS, 1)); assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -3766,7 +3780,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { @@ -3774,11 +3788,11 @@ public class QuotaControllerTest { } advanceElapsedClock(20 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null); } expected.add(createTimingSession(start, MINUTE_IN_MILLIS, 3)); assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -3819,7 +3833,7 @@ public class QuotaControllerTest { long start = JobSchedulerService.sElapsedRealtimeClock.millis(); advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus, jobStatus, true); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, jobStatus); } expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -3850,18 +3864,18 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); setDischarging(); start = JobSchedulerService.sElapsedRealtimeClock.millis(); advanceElapsedClock(20 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null); } expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 1)); assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -3879,7 +3893,7 @@ public class QuotaControllerTest { setCharging(); advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null); } assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); } @@ -3905,7 +3919,7 @@ public class QuotaControllerTest { } advanceElapsedClock(5 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null); } expected.add(createTimingSession(start, 5 * SECOND_IN_MILLIS, 1)); assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -3934,7 +3948,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { @@ -3942,11 +3956,11 @@ public class QuotaControllerTest { } advanceElapsedClock(20 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null); } expected.add(createTimingSession(start, MINUTE_IN_MILLIS, 3)); assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -3973,7 +3987,7 @@ public class QuotaControllerTest { setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); advanceElapsedClock(5 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null); } assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); } @@ -4005,7 +4019,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true); + mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1); } expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -4030,11 +4044,11 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobFg3, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobFg3, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobBg2, null); } assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -4063,7 +4077,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true); + mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1); } advanceElapsedClock(10 * SECOND_IN_MILLIS); // UID "inactive" now start = JobSchedulerService.sElapsedRealtimeClock.millis(); @@ -4073,11 +4087,11 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobFg3, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobFg3, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobBg2, null); } expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 2)); assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -4116,11 +4130,11 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobFg1, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobFg1, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobFg2, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobFg2, null); } assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -4155,11 +4169,11 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobBg1, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobBg1, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobBg2, null); } assertEquals(2, stats.jobCountInRateLimitingWindow); @@ -4193,7 +4207,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true); + mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1); } expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -4218,11 +4232,11 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobTop, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobTop, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobBg2, null); } assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -4253,7 +4267,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true); + mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1); } advanceElapsedClock(5 * SECOND_IN_MILLIS); setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); @@ -4270,12 +4284,12 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobTop, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobTop, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false); - mQuotaController.maybeStopTrackingJobLocked(jobFg1, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobBg2, null); + mQuotaController.maybeStopTrackingJobLocked(jobFg1, null); } expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 2)); assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -4312,7 +4326,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job1, job1, true); + mQuotaController.maybeStopTrackingJobLocked(job1, job1); } expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); assertEquals(expected, @@ -4329,7 +4343,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job2, null, false); + mQuotaController.maybeStopTrackingJobLocked(job2, null); } expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); assertEquals(expected, @@ -4348,7 +4362,7 @@ public class QuotaControllerTest { long elapsedGracePeriodMs = 2 * SECOND_IN_MILLIS; advanceElapsedClock(elapsedGracePeriodMs); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job3, null, false); + mQuotaController.maybeStopTrackingJobLocked(job3, null); } expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS + elapsedGracePeriodMs, 1)); assertEquals(expected, @@ -4373,7 +4387,7 @@ public class QuotaControllerTest { advanceElapsedClock(10 * SECOND_IN_MILLIS); expected.add(createTimingSession(start, remainingGracePeriod + 10 * SECOND_IN_MILLIS, 1)); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job4, job4, true); + mQuotaController.maybeStopTrackingJobLocked(job4, job4); } assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -4387,7 +4401,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job5, job5, true); + mQuotaController.maybeStopTrackingJobLocked(job5, job5); } expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); assertEquals(expected, @@ -4611,7 +4625,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Ran jobs up to the job limit. All of them should be allowed to run. for (int i = 0; i < mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; ++i) { @@ -4624,13 +4638,13 @@ public class QuotaControllerTest { } advanceElapsedClock(SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job, null, false); + mQuotaController.maybeStopTrackingJobLocked(job, null); } advanceElapsedClock(SECOND_IN_MILLIS); } // Start alarm shouldn't have been scheduled since the app was in quota up until this point. verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // The app is now out of job count quota JobStatus throttledJob = createJobStatus( @@ -4649,7 +4663,7 @@ public class QuotaControllerTest { final long expectedWorkingAlarmTime = stats.jobRateLimitExpirationTimeElapsed; verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedWorkingAlarmTime), anyLong(), - eq(TAG_QUOTA_CHECK), any(), any()); + eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); } /** @@ -4680,7 +4694,7 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Ran jobs up to the job limit. All of them should be allowed to run. for (int i = 0; i < mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW; ++i) { @@ -4695,13 +4709,13 @@ public class QuotaControllerTest { } advanceElapsedClock(SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job, null, false); + mQuotaController.maybeStopTrackingJobLocked(job, null); } advanceElapsedClock(SECOND_IN_MILLIS); } // Start alarm shouldn't have been scheduled since the app was in quota up until this point. verify(mAlarmManager, timeout(1000).times(0)).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // The app is now out of session count quota JobStatus throttledJob = createJobStatus( @@ -4721,7 +4735,7 @@ public class QuotaControllerTest { final long expectedWorkingAlarmTime = stats.sessionRateLimitExpirationTimeElapsed; verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedWorkingAlarmTime), anyLong(), - eq(TAG_QUOTA_CHECK), any(), any()); + eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); } @Test @@ -5185,7 +5199,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } inOrder.verify(mAlarmManager, timeout(1000).times(0)) - .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); // Test with timing sessions out of window. final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); @@ -5196,7 +5211,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } inOrder.verify(mAlarmManager, timeout(1000).times(0)) - .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); // Test with timing sessions in window but still in quota. final long end = now - (22 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS); @@ -5208,7 +5224,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } inOrder.verify(mAlarmManager, timeout(1000).times(0)) - .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); // Add some more sessions, but still in quota. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, @@ -5220,7 +5237,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } inOrder.verify(mAlarmManager, timeout(1000).times(0)) - .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); // Test when out of quota. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, @@ -5230,7 +5248,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( - anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); // Alarm already scheduled, so make sure it's not scheduled again. synchronized (mQuotaController.mLock) { @@ -5238,7 +5257,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket); } inOrder.verify(mAlarmManager, timeout(1000).times(0)) - .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); } /** Tests that the start alarm is properly rescheduled if the app's bucket is changed. */ @@ -5282,7 +5302,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX); } inOrder.verify(mAlarmManager, timeout(1000).times(0)) - .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); inOrder.verify(mAlarmManager, timeout(1000).times(0)) .cancel(any(AlarmManager.OnAlarmListener.class)); @@ -5297,7 +5318,7 @@ public class QuotaControllerTest { } inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedWorkingAlarmTime), anyLong(), - eq(TAG_QUOTA_CHECK), any(), any()); + eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); setStandbyBucket(FREQUENT_INDEX); final long expectedFrequentAlarmTime = @@ -5308,7 +5329,7 @@ public class QuotaControllerTest { } inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedFrequentAlarmTime), anyLong(), - eq(TAG_QUOTA_CHECK), any(), any()); + eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); setStandbyBucket(RARE_INDEX); final long expectedRareAlarmTime = @@ -5319,7 +5340,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, RARE_INDEX); } inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( - anyInt(), eq(expectedRareAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), eq(expectedRareAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); // And back up again. setStandbyBucket(FREQUENT_INDEX); @@ -5329,7 +5351,7 @@ public class QuotaControllerTest { } inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedFrequentAlarmTime), anyLong(), - eq(TAG_QUOTA_CHECK), any(), any()); + eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); setStandbyBucket(WORKING_INDEX); synchronized (mQuotaController.mLock) { @@ -5338,7 +5360,7 @@ public class QuotaControllerTest { } inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedWorkingAlarmTime), anyLong(), - eq(TAG_QUOTA_CHECK), any(), any()); + eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); setStandbyBucket(ACTIVE_INDEX); synchronized (mQuotaController.mLock) { @@ -5346,7 +5368,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX); } inOrder.verify(mAlarmManager, timeout(1000).times(0)) - .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .cancel(any(AlarmManager.OnAlarmListener.class)); } @@ -5388,7 +5411,8 @@ public class QuotaControllerTest { SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX); } verify(mAlarmManager, timeout(1000).times(1)).setWindow( - anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); } /** Tests that TimingSessions aren't saved when the device is charging. */ @@ -5408,7 +5432,7 @@ public class QuotaControllerTest { } advanceElapsedClock(5 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null); } assertNull(mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); } @@ -5434,7 +5458,7 @@ public class QuotaControllerTest { } advanceElapsedClock(5 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null); } expected.add(createTimingSession(start, 5 * SECOND_IN_MILLIS, 1)); assertEquals(expected, @@ -5464,7 +5488,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { @@ -5472,11 +5496,11 @@ public class QuotaControllerTest { } advanceElapsedClock(20 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null); } expected.add(createTimingSession(start, MINUTE_IN_MILLIS, 3)); assertEquals(expected, @@ -5521,7 +5545,7 @@ public class QuotaControllerTest { long start = JobSchedulerService.sElapsedRealtimeClock.millis(); advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus, jobStatus, true); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, jobStatus); } expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); assertEquals(expected, @@ -5553,18 +5577,18 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); setDischarging(); start = JobSchedulerService.sElapsedRealtimeClock.millis(); advanceElapsedClock(20 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null); } expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 1)); assertEquals(expected, @@ -5583,7 +5607,7 @@ public class QuotaControllerTest { setCharging(); advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null); } assertEquals(expected, mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -5610,7 +5634,7 @@ public class QuotaControllerTest { } advanceElapsedClock(5 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null); } expected.add(createTimingSession(start, 5 * SECOND_IN_MILLIS, 1)); assertEquals(expected, @@ -5640,7 +5664,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { @@ -5648,11 +5672,11 @@ public class QuotaControllerTest { } advanceElapsedClock(20 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null); } expected.add(createTimingSession(start, MINUTE_IN_MILLIS, 3)); assertEquals(expected, @@ -5680,7 +5704,7 @@ public class QuotaControllerTest { setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); advanceElapsedClock(5 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null); } assertNull(mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); } @@ -5715,7 +5739,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true); + mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1); } expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); assertEquals(expected, @@ -5741,11 +5765,11 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobFg3, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobFg3, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobBg2, null); } assertEquals(expected, mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -5775,7 +5799,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true); + mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1); } advanceElapsedClock(10 * SECOND_IN_MILLIS); // UID "inactive" now start = JobSchedulerService.sElapsedRealtimeClock.millis(); @@ -5785,11 +5809,11 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobFg3, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobFg3, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobBg2, null); } expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 2)); assertEquals(expected, @@ -5825,7 +5849,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true); + mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1); } expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); assertEquals(expected, @@ -5851,11 +5875,11 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobTop, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobTop, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobBg2, null); } assertEquals(expected, mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -5887,7 +5911,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true); + mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1); } advanceElapsedClock(5 * SECOND_IN_MILLIS); setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); @@ -5904,12 +5928,12 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobTop, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobTop, null); } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false); - mQuotaController.maybeStopTrackingJobLocked(jobFg1, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobBg2, null); + mQuotaController.maybeStopTrackingJobLocked(jobFg1, null); } expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 2)); assertEquals(expected, @@ -5946,7 +5970,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job1, job1, true); + mQuotaController.maybeStopTrackingJobLocked(job1, job1); } expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); assertEquals(expected, @@ -5963,7 +5987,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job2, null, false); + mQuotaController.maybeStopTrackingJobLocked(job2, null); } assertEquals(expected, mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -5981,7 +6005,7 @@ public class QuotaControllerTest { long elapsedGracePeriodMs = 2 * SECOND_IN_MILLIS; advanceElapsedClock(elapsedGracePeriodMs); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job3, null, false); + mQuotaController.maybeStopTrackingJobLocked(job3, null); } assertEquals(expected, mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -6005,7 +6029,7 @@ public class QuotaControllerTest { advanceElapsedClock(10 * SECOND_IN_MILLIS); expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job4, job4, true); + mQuotaController.maybeStopTrackingJobLocked(job4, job4); } assertEquals(expected, mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -6019,7 +6043,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job5, job5, true); + mQuotaController.maybeStopTrackingJobLocked(job5, job5); } expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); assertEquals(expected, @@ -6049,7 +6073,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job, job, true); + mQuotaController.maybeStopTrackingJobLocked(job, job); } expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); assertEquals(expected, @@ -6066,7 +6090,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job, null, false); + mQuotaController.maybeStopTrackingJobLocked(job, null); } expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); assertEquals(expected, @@ -6085,7 +6109,7 @@ public class QuotaControllerTest { long elapsedGracePeriodMs = 2 * SECOND_IN_MILLIS; advanceElapsedClock(elapsedGracePeriodMs); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job, null, false); + mQuotaController.maybeStopTrackingJobLocked(job, null); } expected.add(createTimingSession(start, 12 * SECOND_IN_MILLIS, 1)); assertEquals(expected, @@ -6110,7 +6134,7 @@ public class QuotaControllerTest { advanceElapsedClock(10 * SECOND_IN_MILLIS); expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS + remainingGracePeriod, 1)); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job, job, true); + mQuotaController.maybeStopTrackingJobLocked(job, job); } assertEquals(expected, mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -6124,7 +6148,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job, job, true); + mQuotaController.maybeStopTrackingJobLocked(job, job); } expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); assertEquals(expected, @@ -6169,7 +6193,7 @@ public class QuotaControllerTest { // Wait for the grace period to expire so the handler can process the message. Thread.sleep(gracePeriodMs); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job1, job1, true); + mQuotaController.maybeStopTrackingJobLocked(job1, job1); } assertNull(mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -6189,7 +6213,7 @@ public class QuotaControllerTest { // Wait for the grace period to expire so the handler can process the message. Thread.sleep(gracePeriodMs); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job2, null, false); + mQuotaController.maybeStopTrackingJobLocked(job2, null); } assertNull(mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); @@ -6215,7 +6239,7 @@ public class QuotaControllerTest { Thread.sleep(2 * gracePeriodMs); advanceElapsedClock(gracePeriodMs); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job3, job3, true); + mQuotaController.maybeStopTrackingJobLocked(job3, job3); } expected.add(createTimingSession(start, gracePeriodMs, 1)); assertEquals(expected, @@ -6243,7 +6267,7 @@ public class QuotaControllerTest { Thread.sleep(2 * gracePeriodMs); advanceElapsedClock(gracePeriodMs); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job4, job4, true); + mQuotaController.maybeStopTrackingJobLocked(job4, job4); } expected.add(createTimingSession(start, gracePeriodMs, 1)); assertEquals(expected, @@ -6270,7 +6294,7 @@ public class QuotaControllerTest { Thread.sleep(2 * gracePeriodMs); advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(job5, job5, true); + mQuotaController.maybeStopTrackingJobLocked(job5, job5); } expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); assertEquals(expected, @@ -6424,7 +6448,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobReg1, jobReg1, true); + mQuotaController.maybeStopTrackingJobLocked(jobReg1, jobReg1); } expectedRegular.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); assertEquals(expectedRegular, @@ -6440,7 +6464,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobEJ1, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobEJ1, null); } expectedEJ.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); assertEquals(expectedRegular, @@ -6461,12 +6485,12 @@ public class QuotaControllerTest { } advanceElapsedClock(5 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobEJ2, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobEJ2, null); } expectedEJ.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); advanceElapsedClock(5 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(jobReg2, null, false); + mQuotaController.maybeStopTrackingJobLocked(jobReg2, null); } expectedRegular.add( createTimingSession(start + 5 * SECOND_IN_MILLIS, 10 * SECOND_IN_MILLIS, 1)); @@ -6556,7 +6580,7 @@ public class QuotaControllerTest { } advanceElapsedClock(5000); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(regJob, null, false); + mQuotaController.maybeStopTrackingJobLocked(regJob, null); } assertEquals(0, debit.getTallyLocked()); assertEquals(10 * MINUTE_IN_MILLIS, @@ -6570,7 +6594,7 @@ public class QuotaControllerTest { } advanceElapsedClock(5 * MINUTE_IN_MILLIS); synchronized (mQuotaController.mLock) { - mQuotaController.maybeStopTrackingJobLocked(eJob, null, false); + mQuotaController.maybeStopTrackingJobLocked(eJob, null); } assertEquals(5 * MINUTE_IN_MILLIS, debit.getTallyLocked()); assertEquals(5 * MINUTE_IN_MILLIS, diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java index 612e90671345..51d641bfb80d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java @@ -81,8 +81,7 @@ public class StateControllerTest { public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { } - public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, - boolean forUpdate) { + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) { } public void dumpControllerStateLocked(IndentingPrintWriter pw, diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java index 2fac31e9e6fd..d477cb6f356c 100644 --- a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java @@ -73,7 +73,12 @@ public class AgentTrendCalculatorTest { } @Override - long getHardSatiatedConsumptionLimit() { + long getMinSatiatedConsumptionLimit() { + return 0; + } + + @Override + long getMaxSatiatedConsumptionLimit() { return 0; } diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/AlarmManagerEconomicPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/AlarmManagerEconomicPolicyTest.java index fb3e8f298424..84a61c7a21e5 100644 --- a/services/tests/mockingservicestests/src/com/android/server/tare/AlarmManagerEconomicPolicyTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/tare/AlarmManagerEconomicPolicyTest.java @@ -132,8 +132,10 @@ public class AlarmManagerEconomicPolicyTest { public void testDefaults() { assertEquals(EconomyManager.DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT_CAKES, mEconomicPolicy.getInitialSatiatedConsumptionLimit()); - assertEquals(EconomyManager.DEFAULT_AM_HARD_CONSUMPTION_LIMIT_CAKES, - mEconomicPolicy.getHardSatiatedConsumptionLimit()); + assertEquals(EconomyManager.DEFAULT_AM_MIN_CONSUMPTION_LIMIT_CAKES, + mEconomicPolicy.getMinSatiatedConsumptionLimit()); + assertEquals(EconomyManager.DEFAULT_AM_MAX_CONSUMPTION_LIMIT_CAKES, + mEconomicPolicy.getMaxSatiatedConsumptionLimit()); final String pkgRestricted = "com.pkg.restricted"; when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); assertEquals(0, mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); @@ -150,13 +152,15 @@ public class AlarmManagerEconomicPolicyTest { @Test public void testConstantsUpdating_ValidValues() { setDeviceConfigCakes(EconomyManager.KEY_AM_INITIAL_CONSUMPTION_LIMIT, arcToCake(5)); - setDeviceConfigCakes(EconomyManager.KEY_AM_HARD_CONSUMPTION_LIMIT, arcToCake(25)); + setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_CONSUMPTION_LIMIT, arcToCake(3)); + setDeviceConfigCakes(EconomyManager.KEY_AM_MAX_CONSUMPTION_LIMIT, arcToCake(25)); setDeviceConfigCakes(EconomyManager.KEY_AM_MAX_SATIATED_BALANCE, arcToCake(10)); setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_EXEMPTED, arcToCake(9)); setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_OTHER_APP, arcToCake(7)); assertEquals(arcToCake(5), mEconomicPolicy.getInitialSatiatedConsumptionLimit()); - assertEquals(arcToCake(25), mEconomicPolicy.getHardSatiatedConsumptionLimit()); + assertEquals(arcToCake(3), mEconomicPolicy.getMinSatiatedConsumptionLimit()); + assertEquals(arcToCake(25), mEconomicPolicy.getMaxSatiatedConsumptionLimit()); final String pkgRestricted = "com.pkg.restricted"; when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); @@ -171,13 +175,15 @@ public class AlarmManagerEconomicPolicyTest { public void testConstantsUpdating_InvalidValues() { // Test negatives. setDeviceConfigCakes(EconomyManager.KEY_AM_INITIAL_CONSUMPTION_LIMIT, arcToCake(-5)); - setDeviceConfigCakes(EconomyManager.KEY_AM_HARD_CONSUMPTION_LIMIT, arcToCake(-5)); + setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_CONSUMPTION_LIMIT, arcToCake(-5)); + setDeviceConfigCakes(EconomyManager.KEY_AM_MAX_CONSUMPTION_LIMIT, arcToCake(-5)); setDeviceConfigCakes(EconomyManager.KEY_AM_MAX_SATIATED_BALANCE, arcToCake(-1)); setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_EXEMPTED, arcToCake(-2)); setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_OTHER_APP, arcToCake(-3)); assertEquals(arcToCake(1), mEconomicPolicy.getInitialSatiatedConsumptionLimit()); - assertEquals(arcToCake(1), mEconomicPolicy.getHardSatiatedConsumptionLimit()); + assertEquals(arcToCake(1), mEconomicPolicy.getMinSatiatedConsumptionLimit()); + assertEquals(arcToCake(1), mEconomicPolicy.getMaxSatiatedConsumptionLimit()); final String pkgRestricted = "com.pkg.restricted"; when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); @@ -188,14 +194,16 @@ public class AlarmManagerEconomicPolicyTest { assertEquals(arcToCake(0), mEconomicPolicy.getMinSatiatedBalance(0, "com.any.other.app")); // Test min+max reversed. - setDeviceConfigCakes(EconomyManager.KEY_AM_INITIAL_CONSUMPTION_LIMIT, arcToCake(5)); - setDeviceConfigCakes(EconomyManager.KEY_AM_HARD_CONSUMPTION_LIMIT, arcToCake(3)); + setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_CONSUMPTION_LIMIT, arcToCake(5)); + setDeviceConfigCakes(EconomyManager.KEY_AM_INITIAL_CONSUMPTION_LIMIT, arcToCake(4)); + setDeviceConfigCakes(EconomyManager.KEY_AM_MAX_CONSUMPTION_LIMIT, arcToCake(3)); setDeviceConfigCakes(EconomyManager.KEY_AM_MAX_SATIATED_BALANCE, arcToCake(10)); setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_EXEMPTED, arcToCake(11)); setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_OTHER_APP, arcToCake(13)); assertEquals(arcToCake(5), mEconomicPolicy.getInitialSatiatedConsumptionLimit()); - assertEquals(arcToCake(5), mEconomicPolicy.getHardSatiatedConsumptionLimit()); + assertEquals(arcToCake(5), mEconomicPolicy.getMinSatiatedConsumptionLimit()); + assertEquals(arcToCake(5), mEconomicPolicy.getMaxSatiatedConsumptionLimit()); assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); assertEquals(arcToCake(13), mEconomicPolicy.getMaxSatiatedBalance(0, "com.any.other.app")); assertEquals(arcToCake(13), mEconomicPolicy.getMinSatiatedBalance(0, pkgExempted)); diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/CompleteEconomicPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/CompleteEconomicPolicyTest.java index 6da4ab714d7a..cad608f8ff59 100644 --- a/services/tests/mockingservicestests/src/com/android/server/tare/CompleteEconomicPolicyTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/tare/CompleteEconomicPolicyTest.java @@ -155,9 +155,12 @@ public class CompleteEconomicPolicyTest { assertEquals(EconomyManager.DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES + EconomyManager.DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT_CAKES, mEconomicPolicy.getInitialSatiatedConsumptionLimit()); - assertEquals(EconomyManager.DEFAULT_JS_HARD_CONSUMPTION_LIMIT_CAKES - + EconomyManager.DEFAULT_AM_HARD_CONSUMPTION_LIMIT_CAKES, - mEconomicPolicy.getHardSatiatedConsumptionLimit()); + assertEquals(EconomyManager.DEFAULT_JS_MIN_CONSUMPTION_LIMIT_CAKES + + EconomyManager.DEFAULT_AM_MIN_CONSUMPTION_LIMIT_CAKES, + mEconomicPolicy.getMinSatiatedConsumptionLimit()); + assertEquals(EconomyManager.DEFAULT_JS_MAX_CONSUMPTION_LIMIT_CAKES + + EconomyManager.DEFAULT_AM_MAX_CONSUMPTION_LIMIT_CAKES, + mEconomicPolicy.getMaxSatiatedConsumptionLimit()); final String pkgRestricted = "com.pkg.restricted"; when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); assertEquals(0, mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); @@ -178,8 +181,10 @@ public class CompleteEconomicPolicyTest { public void testConstantsUpdated() { setDeviceConfigCakes(EconomyManager.KEY_JS_INITIAL_CONSUMPTION_LIMIT, arcToCake(4)); setDeviceConfigCakes(EconomyManager.KEY_AM_INITIAL_CONSUMPTION_LIMIT, arcToCake(6)); - setDeviceConfigCakes(EconomyManager.KEY_JS_HARD_CONSUMPTION_LIMIT, arcToCake(24)); - setDeviceConfigCakes(EconomyManager.KEY_AM_HARD_CONSUMPTION_LIMIT, arcToCake(26)); + setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_CONSUMPTION_LIMIT, arcToCake(2)); + setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_CONSUMPTION_LIMIT, arcToCake(3)); + setDeviceConfigCakes(EconomyManager.KEY_JS_MAX_CONSUMPTION_LIMIT, arcToCake(24)); + setDeviceConfigCakes(EconomyManager.KEY_AM_MAX_CONSUMPTION_LIMIT, arcToCake(26)); setDeviceConfigCakes(EconomyManager.KEY_JS_MAX_SATIATED_BALANCE, arcToCake(9)); setDeviceConfigCakes(EconomyManager.KEY_AM_MAX_SATIATED_BALANCE, arcToCake(11)); setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_EXEMPTED, arcToCake(8)); @@ -188,7 +193,8 @@ public class CompleteEconomicPolicyTest { setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_OTHER_APP, arcToCake(2)); assertEquals(arcToCake(10), mEconomicPolicy.getInitialSatiatedConsumptionLimit()); - assertEquals(arcToCake(50), mEconomicPolicy.getHardSatiatedConsumptionLimit()); + assertEquals(arcToCake(5), mEconomicPolicy.getMinSatiatedConsumptionLimit()); + assertEquals(arcToCake(50), mEconomicPolicy.getMaxSatiatedConsumptionLimit()); final String pkgRestricted = "com.pkg.restricted"; when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); @@ -206,8 +212,10 @@ public class CompleteEconomicPolicyTest { setDeviceConfigBoolean(EconomyManager.KEY_ENABLE_POLICY_JOB_SCHEDULER, false); assertEquals(EconomyManager.DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT_CAKES, mEconomicPolicy.getInitialSatiatedConsumptionLimit()); - assertEquals(EconomyManager.DEFAULT_AM_HARD_CONSUMPTION_LIMIT_CAKES, - mEconomicPolicy.getHardSatiatedConsumptionLimit()); + assertEquals(EconomyManager.DEFAULT_AM_MIN_CONSUMPTION_LIMIT_CAKES, + mEconomicPolicy.getMinSatiatedConsumptionLimit()); + assertEquals(EconomyManager.DEFAULT_AM_MAX_CONSUMPTION_LIMIT_CAKES, + mEconomicPolicy.getMaxSatiatedConsumptionLimit()); final String pkgRestricted = "com.pkg.restricted"; when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); assertEquals(0, mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); @@ -229,8 +237,10 @@ public class CompleteEconomicPolicyTest { setDeviceConfigBoolean(EconomyManager.KEY_ENABLE_POLICY_JOB_SCHEDULER, true); assertEquals(EconomyManager.DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES, mEconomicPolicy.getInitialSatiatedConsumptionLimit()); - assertEquals(EconomyManager.DEFAULT_JS_HARD_CONSUMPTION_LIMIT_CAKES, - mEconomicPolicy.getHardSatiatedConsumptionLimit()); + assertEquals(EconomyManager.DEFAULT_JS_MIN_CONSUMPTION_LIMIT_CAKES, + mEconomicPolicy.getMinSatiatedConsumptionLimit()); + assertEquals(EconomyManager.DEFAULT_JS_MAX_CONSUMPTION_LIMIT_CAKES, + mEconomicPolicy.getMaxSatiatedConsumptionLimit()); when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); assertEquals(0, mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); assertEquals(EconomyManager.DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES, diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/JobSchedulerEconomicPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/JobSchedulerEconomicPolicyTest.java index b7bbcd7562cd..ebf760cdf857 100644 --- a/services/tests/mockingservicestests/src/com/android/server/tare/JobSchedulerEconomicPolicyTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/tare/JobSchedulerEconomicPolicyTest.java @@ -132,8 +132,10 @@ public class JobSchedulerEconomicPolicyTest { public void testDefaults() { assertEquals(EconomyManager.DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES, mEconomicPolicy.getInitialSatiatedConsumptionLimit()); - assertEquals(EconomyManager.DEFAULT_JS_HARD_CONSUMPTION_LIMIT_CAKES, - mEconomicPolicy.getHardSatiatedConsumptionLimit()); + assertEquals(EconomyManager.DEFAULT_JS_MIN_CONSUMPTION_LIMIT_CAKES, + mEconomicPolicy.getMinSatiatedConsumptionLimit()); + assertEquals(EconomyManager.DEFAULT_JS_MAX_CONSUMPTION_LIMIT_CAKES, + mEconomicPolicy.getMaxSatiatedConsumptionLimit()); final String pkgRestricted = "com.pkg.restricted"; when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); @@ -171,7 +173,8 @@ public class JobSchedulerEconomicPolicyTest { @Test public void testConstantsUpdating_ValidValues() { setDeviceConfigCakes(EconomyManager.KEY_JS_INITIAL_CONSUMPTION_LIMIT, arcToCake(5)); - setDeviceConfigCakes(EconomyManager.KEY_JS_HARD_CONSUMPTION_LIMIT, arcToCake(25)); + setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_CONSUMPTION_LIMIT, arcToCake(2)); + setDeviceConfigCakes(EconomyManager.KEY_JS_MAX_CONSUMPTION_LIMIT, arcToCake(25)); setDeviceConfigCakes(EconomyManager.KEY_JS_MAX_SATIATED_BALANCE, arcToCake(10)); setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_EXEMPTED, arcToCake(6)); setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_OTHER_APP, arcToCake(4)); @@ -179,7 +182,8 @@ public class JobSchedulerEconomicPolicyTest { arcToCake(1)); assertEquals(arcToCake(5), mEconomicPolicy.getInitialSatiatedConsumptionLimit()); - assertEquals(arcToCake(25), mEconomicPolicy.getHardSatiatedConsumptionLimit()); + assertEquals(arcToCake(2), mEconomicPolicy.getMinSatiatedConsumptionLimit()); + assertEquals(arcToCake(25), mEconomicPolicy.getMaxSatiatedConsumptionLimit()); final String pkgRestricted = "com.pkg.restricted"; when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); @@ -198,7 +202,8 @@ public class JobSchedulerEconomicPolicyTest { public void testConstantsUpdating_InvalidValues() { // Test negatives. setDeviceConfigCakes(EconomyManager.KEY_JS_INITIAL_CONSUMPTION_LIMIT, arcToCake(-5)); - setDeviceConfigCakes(EconomyManager.KEY_JS_HARD_CONSUMPTION_LIMIT, arcToCake(-5)); + setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_CONSUMPTION_LIMIT, arcToCake(-5)); + setDeviceConfigCakes(EconomyManager.KEY_JS_MAX_CONSUMPTION_LIMIT, arcToCake(-5)); setDeviceConfigCakes(EconomyManager.KEY_JS_MAX_SATIATED_BALANCE, arcToCake(-1)); setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_EXEMPTED, arcToCake(-2)); setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_OTHER_APP, arcToCake(-3)); @@ -206,7 +211,8 @@ public class JobSchedulerEconomicPolicyTest { arcToCake(-4)); assertEquals(arcToCake(1), mEconomicPolicy.getInitialSatiatedConsumptionLimit()); - assertEquals(arcToCake(1), mEconomicPolicy.getHardSatiatedConsumptionLimit()); + assertEquals(arcToCake(1), mEconomicPolicy.getMinSatiatedConsumptionLimit()); + assertEquals(arcToCake(1), mEconomicPolicy.getMaxSatiatedConsumptionLimit()); final String pkgRestricted = "com.pkg.restricted"; when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); @@ -221,14 +227,16 @@ public class JobSchedulerEconomicPolicyTest { mEconomicPolicy.getMinSatiatedBalance(0, pkgUpdater)); // Test min+max reversed. - setDeviceConfigCakes(EconomyManager.KEY_JS_INITIAL_CONSUMPTION_LIMIT, arcToCake(5)); - setDeviceConfigCakes(EconomyManager.KEY_JS_HARD_CONSUMPTION_LIMIT, arcToCake(3)); + setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_CONSUMPTION_LIMIT, arcToCake(5)); + setDeviceConfigCakes(EconomyManager.KEY_JS_INITIAL_CONSUMPTION_LIMIT, arcToCake(4)); + setDeviceConfigCakes(EconomyManager.KEY_JS_MAX_CONSUMPTION_LIMIT, arcToCake(3)); setDeviceConfigCakes(EconomyManager.KEY_JS_MAX_SATIATED_BALANCE, arcToCake(10)); setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_EXEMPTED, arcToCake(11)); setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_OTHER_APP, arcToCake(13)); assertEquals(arcToCake(5), mEconomicPolicy.getInitialSatiatedConsumptionLimit()); - assertEquals(arcToCake(5), mEconomicPolicy.getHardSatiatedConsumptionLimit()); + assertEquals(arcToCake(5), mEconomicPolicy.getMinSatiatedConsumptionLimit()); + assertEquals(arcToCake(5), mEconomicPolicy.getMaxSatiatedConsumptionLimit()); assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); assertEquals(arcToCake(13), mEconomicPolicy.getMaxSatiatedBalance(0, "com.any.other.app")); assertEquals(arcToCake(13), mEconomicPolicy.getMinSatiatedBalance(0, pkgExempted)); diff --git a/services/tests/mockingservicestests/src/com/android/server/utils/AlarmQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/utils/AlarmQueueTest.java index 00d7541a79dc..a3a49d7035d9 100644 --- a/services/tests/mockingservicestests/src/com/android/server/utils/AlarmQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/utils/AlarmQueueTest.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.when; import android.app.AlarmManager; import android.content.Context; +import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.util.ArraySet; @@ -222,7 +223,8 @@ public class AlarmQueueTest { alarmQueue.addAlarm("com.android.test.1", nowElapsed + HOUR_IN_MILLIS); verify(mAlarmManager, timeout(1000).times(1)).setWindow( - anyInt(), eq(nowElapsed + HOUR_IN_MILLIS), anyLong(), eq(ALARM_TAG), any(), any()); + anyInt(), eq(nowElapsed + HOUR_IN_MILLIS), anyLong(), eq(ALARM_TAG), any(), any( + Handler.class)); } @Test diff --git a/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java index 608b64e7d12a..0d14c9f02677 100644 --- a/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java @@ -589,14 +589,14 @@ public class CountQuotaTrackerTest { // No sessions saved yet. mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); verify(mAlarmManager, never()).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test with timing sessions out of window. final long now = mInjector.getElapsedRealtime(); logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 10 * HOUR_IN_MILLIS, 20); mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); verify(mAlarmManager, never()).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test with timing sessions in window but still in quota. final long start = now - (6 * HOUR_IN_MILLIS); @@ -604,25 +604,27 @@ public class CountQuotaTrackerTest { logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, start, 5); mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); verify(mAlarmManager, never()).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Add some more sessions, but still in quota. logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 3 * HOUR_IN_MILLIS, 1); logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - HOUR_IN_MILLIS, 3); mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); verify(mAlarmManager, never()).setWindow( - anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test when out of quota. logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - HOUR_IN_MILLIS, 1); mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); verify(mAlarmManager, timeout(1000).times(1)).setWindow( - anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); // Alarm already scheduled, so make sure it's not scheduled again. mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); verify(mAlarmManager, times(1)).setWindow( - anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); } /** Tests that the start alarm is properly rescheduled if the app's category is changed. */ @@ -656,7 +658,8 @@ public class CountQuotaTrackerTest { mCategorizer.mCategoryToUse = ACTIVE_BUCKET_CATEGORY; mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, never()) - .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); inOrder.verify(mAlarmManager, never()).cancel(any(AlarmManager.OnAlarmListener.class)); // And down from there. @@ -665,41 +668,42 @@ public class CountQuotaTrackerTest { mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .setWindow(anyInt(), eq(expectedWorkingAlarmTime), anyLong(), - eq(TAG_QUOTA_CHECK), any(), any()); + eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); final long expectedFrequentAlarmTime = outOfQuotaTime + (8 * HOUR_IN_MILLIS); mCategorizer.mCategoryToUse = FREQUENT_BUCKET_CATEGORY; mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .setWindow(anyInt(), eq(expectedFrequentAlarmTime), anyLong(), - eq(TAG_QUOTA_CHECK), any(), any()); + eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); final long expectedRareAlarmTime = outOfQuotaTime + (24 * HOUR_IN_MILLIS); mCategorizer.mCategoryToUse = RARE_BUCKET_CATEGORY; mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .setWindow(anyInt(), eq(expectedRareAlarmTime), anyLong(), - eq(TAG_QUOTA_CHECK), any(), any()); + eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // And back up again. mCategorizer.mCategoryToUse = FREQUENT_BUCKET_CATEGORY; mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .setWindow(anyInt(), eq(expectedFrequentAlarmTime), anyLong(), - eq(TAG_QUOTA_CHECK), any(), any()); + eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); mCategorizer.mCategoryToUse = WORKING_SET_BUCKET_CATEGORY; mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .setWindow(anyInt(), eq(expectedWorkingAlarmTime), anyLong(), - eq(TAG_QUOTA_CHECK), any(), any()); + eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); mCategorizer.mCategoryToUse = ACTIVE_BUCKET_CATEGORY; mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .cancel(any(AlarmManager.OnAlarmListener.class)); inOrder.verify(mAlarmManager, timeout(1000).times(0)) - .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any()); + .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), + any(Handler.class)); } @Test diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java index 1b867be81669..8f6bee170b0e 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java @@ -92,7 +92,6 @@ public class HdmiControlServiceTest { HdmiCecConfig hdmiCecConfig = new FakeHdmiCecConfig(mContextSpy); mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_PLAYBACK); - mLocalDeviceTypes.add(DEVICE_AUDIO_SYSTEM); mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, mLocalDeviceTypes, new FakeAudioDeviceVolumeManagerWrapper())); @@ -480,6 +479,7 @@ public class HdmiControlServiceTest { HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION, HdmiControlManager.HDMI_CEC_VERSION_2_0); mHdmiControlServiceSpy.setControlEnabled(HdmiControlManager.HDMI_CEC_CONTROL_ENABLED); + mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mTestLooper.dispatchAll(); mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildGiveFeatures(Constants.ADDR_TV, @@ -501,6 +501,7 @@ public class HdmiControlServiceTest { HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION, HdmiControlManager.HDMI_CEC_VERSION_1_4_B); mHdmiControlServiceSpy.setControlEnabled(HdmiControlManager.HDMI_CEC_CONTROL_ENABLED); + mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mTestLooper.dispatchAll(); HdmiCecMessage reportFeatures = ReportFeaturesMessage.build( @@ -517,6 +518,7 @@ public class HdmiControlServiceTest { HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION, HdmiControlManager.HDMI_CEC_VERSION_2_0); mHdmiControlServiceSpy.setControlEnabled(HdmiControlManager.HDMI_CEC_CONTROL_ENABLED); + mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mTestLooper.dispatchAll(); HdmiCecMessage reportFeatures = ReportFeaturesMessage.build( diff --git a/services/tests/servicestests/src/com/android/server/power/stats/CpuWakeupStatsTest.java b/services/tests/servicestests/src/com/android/server/power/stats/CpuWakeupStatsTest.java index 7731a326cf61..c2556e9267d5 100644 --- a/services/tests/servicestests/src/com/android/server/power/stats/CpuWakeupStatsTest.java +++ b/services/tests/servicestests/src/com/android/server/power/stats/CpuWakeupStatsTest.java @@ -44,7 +44,9 @@ import java.util.concurrent.ThreadLocalRandom; public class CpuWakeupStatsTest { private static final String KERNEL_REASON_ALARM_IRQ = "120 test.alarm.device"; private static final String KERNEL_REASON_UNKNOWN_IRQ = "140 test.unknown.device"; - private static final String KERNEL_REASON_UNKNOWN = "unsupported-free-form-reason"; + private static final String KERNEL_REASON_UNKNOWN = "free-form-reason test.alarm.device"; + private static final String KERNEL_REASON_UNSUPPORTED = "-1 test.alarm.device"; + private static final String KERNEL_REASON_ABORT = "Abort: due to test.alarm.device"; private static final int TEST_UID_1 = 13239823; private static final int TEST_UID_2 = 25268423; @@ -57,6 +59,7 @@ public class CpuWakeupStatsTest { @Test public void removesOldWakeups() { + // The xml resource doesn't matter for this test. final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_1); final Set<Long> timestamps = new HashSet<>(); @@ -165,11 +168,36 @@ public class CpuWakeupStatsTest { obj.noteWakeupTimeAndReason(wakeupTime, 34, KERNEL_REASON_UNKNOWN); - // Unrelated subsystems, should be ignored. + // Should be ignored as this type of wakeup is unsupported. obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3); obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime - 3, TEST_UID_4); // There should be nothing in the attribution map. assertThat(obj.mWakeupAttribution.size()).isEqualTo(0); } + + @Test + public void unsupportedAttribution() { + final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3); + + long wakeupTime = 970934; + obj.noteWakeupTimeAndReason(wakeupTime, 34, KERNEL_REASON_UNSUPPORTED); + + // Should be ignored as this type of wakeup is unsupported. + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3); + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime - 3, TEST_UID_4); + + // There should be nothing in the attribution map. + assertThat(obj.mWakeupAttribution.size()).isEqualTo(0); + + wakeupTime = 883124; + obj.noteWakeupTimeAndReason(wakeupTime, 3, KERNEL_REASON_ABORT); + + // Should be ignored as this type of wakeup is unsupported. + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 2, TEST_UID_1, TEST_UID_4); + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime - 5, TEST_UID_3); + + // There should be nothing in the attribution map. + assertThat(obj.mWakeupAttribution.size()).isEqualTo(0); + } } diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index d314a6579b58..2c7867cfd1a2 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -1147,8 +1147,12 @@ public class CarrierConfigManager { "carrier_data_call_apn_retry_after_disconnect_long"; /** - * Data call setup permanent failure causes by the carrier + * Data call setup permanent failure causes by the carrier. + * + * @deprecated This API key was added in mistake and is not used anymore by the telephony data + * frameworks. */ + @Deprecated public static final String KEY_CARRIER_DATA_CALL_PERMANENT_FAILURE_STRINGS = "carrier_data_call_permanent_failure_strings"; @@ -2103,6 +2107,16 @@ public class CarrierConfigManager { * is immediately closed (disabling keep-alive). */ public static final String KEY_MMS_CLOSE_CONNECTION_BOOL = "mmsCloseConnection"; + /** + * Waiting time in milliseconds used before releasing an MMS data call. Not tearing down an MMS + * data connection immediately helps to reduce the message delivering latency if messaging + * continues between all parties in the conversation since the same data connection can be + * reused for further messages. + * + * This timer will control how long the data call will be kept alive before being torn down. + */ + public static final String KEY_MMS_NETWORK_RELEASE_TIMEOUT_MILLIS_INT = + "mms_network_release_timeout_millis_int"; /** * The flatten {@link android.content.ComponentName componentName} of the activity that can @@ -8436,7 +8450,8 @@ public class CarrierConfigManager { * * The syntax of the retry rule: * 1. Retry based on {@link NetworkCapabilities}. Note that only APN-type network capabilities - * are supported. + * are supported. If the capabilities are not specified, then the retry rule only applies + * to the current failed APN used in setup data call request. * "capabilities=[netCaps1|netCaps2|...], [retry_interval=n1|n2|n3|n4...], [maximum_retries=n]" * * 2. Retry based on {@link DataFailCause} @@ -8447,15 +8462,16 @@ public class CarrierConfigManager { * "capabilities=[netCaps1|netCaps2|...], fail_causes=[cause1|cause2|cause3|...], * [retry_interval=n1|n2|n3|n4...], [maximum_retries=n]" * + * 4. Permanent fail causes (no timer-based retry) on the current failed APN. Retry interval + * is specified for retrying the next available APN. + * "permanent_fail_causes=8|27|28|29|30|32|33|35|50|51|111|-5|-6|65537|65538|-3|65543|65547| + * 2252|2253|2254, retry_interval=2500" + * * For example, * "capabilities=eims, retry_interval=1000, maximum_retries=20" means if the attached * network request is emergency, then retry data network setup every 1 second for up to 20 * times. * - * "fail_causes=8|27|28|29|30|32|33|35|50|51|111|-5|-6|65537|65538|-3|2253|2254 - * , maximum_retries=0" means for those fail causes, never retry with timers. Note that - * when environment changes, retry can still happen. - * * "capabilities=internet|enterprise|dun|ims|fota, retry_interval=2500|3000|" * "5000|10000|15000|20000|40000|60000|120000|240000|600000|1200000|1800000" * "1800000, maximum_retries=20" means for those capabilities, retry happens in 2.5s, 3s, 5s, @@ -9069,6 +9085,7 @@ public class CarrierConfigManager { sDefaults.putInt(KEY_MMS_SMS_TO_MMS_TEXT_LENGTH_THRESHOLD_INT, -1); sDefaults.putInt(KEY_MMS_SMS_TO_MMS_TEXT_THRESHOLD_INT, -1); sDefaults.putInt(KEY_MMS_SUBJECT_MAX_LENGTH_INT, 40); + sDefaults.putInt(KEY_MMS_NETWORK_RELEASE_TIMEOUT_MILLIS_INT, 5 * 1000); sDefaults.putString(KEY_MMS_EMAIL_GATEWAY_NUMBER_STRING, ""); sDefaults.putString(KEY_MMS_HTTP_PARAMS_STRING, ""); sDefaults.putString(KEY_MMS_NAI_SUFFIX_STRING, ""); @@ -9432,8 +9449,13 @@ public class CarrierConfigManager { sDefaults.putStringArray( KEY_TELEPHONY_DATA_SETUP_RETRY_RULES_STRING_ARRAY, new String[] { "capabilities=eims, retry_interval=1000, maximum_retries=20", - "fail_causes=8|27|28|29|30|32|33|35|50|51|111|-5|-6|65537|65538|-3|2252|" - + "2253|2254, maximum_retries=0", // No retry for those causes + // Permanent fail causes. When setup data call fails with the following + // fail causes, telephony data frameworks will stop timer-based retry on + // the failed APN until power cycle, APM, or some special events. Note that + // even timer-based retry is not performed, condition-based (RAT changes, + // registration state changes) retry can still happen. + "permanent_fail_causes=8|27|28|29|30|32|33|35|50|51|111|-5|-6|65537|65538|" + + "-3|65543|65547|2252|2253|2254, retry_interval=2500", "capabilities=mms|supl|cbs, retry_interval=2000", "capabilities=internet|enterprise|dun|ims|fota, retry_interval=2500|3000|" + "5000|10000|15000|20000|40000|60000|120000|240000|" diff --git a/telephony/java/android/telephony/SmsManager.java b/telephony/java/android/telephony/SmsManager.java index d670e5592c42..1f301c1b2279 100644 --- a/telephony/java/android/telephony/SmsManager.java +++ b/telephony/java/android/telephony/SmsManager.java @@ -2268,7 +2268,21 @@ public final class SmsManager { RESULT_RIL_SIM_ABSENT, RESULT_RIL_SIMULTANEOUS_SMS_AND_CALL_NOT_ALLOWED, RESULT_RIL_ACCESS_BARRED, - RESULT_RIL_BLOCKED_DUE_TO_CALL + RESULT_RIL_BLOCKED_DUE_TO_CALL, + RESULT_RIL_GENERIC_ERROR, + RESULT_RIL_INVALID_RESPONSE, + RESULT_RIL_SIM_PIN2, + RESULT_RIL_SIM_PUK2, + RESULT_RIL_SUBSCRIPTION_NOT_AVAILABLE, + RESULT_RIL_SIM_ERROR, + RESULT_RIL_INVALID_SIM_STATE, + RESULT_RIL_NO_SMS_TO_ACK, + RESULT_RIL_SIM_BUSY, + RESULT_RIL_SIM_FULL, + RESULT_RIL_NO_SUBSCRIPTION, + RESULT_RIL_NO_NETWORK_FOUND, + RESULT_RIL_DEVICE_IN_USE, + RESULT_RIL_ABORTED }) @Retention(RetentionPolicy.SOURCE) public @interface Result {} @@ -2534,7 +2548,7 @@ public final class SmsManager { public static final int RESULT_RIL_SIM_ABSENT = 120; /** - * 1X voice and SMS are not allowed simulteneously. + * 1X voice and SMS are not allowed simultaneously. */ public static final int RESULT_RIL_SIMULTANEOUS_SMS_AND_CALL_NOT_ALLOWED = 121; @@ -2553,6 +2567,73 @@ public final class SmsManager { */ public static final int RESULT_RIL_GENERIC_ERROR = 124; + /** + * A RIL internal error when one of the RIL layers receives an unrecognized response from a + * lower layer. + */ + public static final int RESULT_RIL_INVALID_RESPONSE = 125; + + /** + * Operation requires SIM PIN2 to be entered + */ + public static final int RESULT_RIL_SIM_PIN2 = 126; + + /** + * Operation requires SIM PUK2 to be entered + */ + public static final int RESULT_RIL_SIM_PUK2 = 127; + + /** + * Fail to find CDMA subscription from specified location + */ + public static final int RESULT_RIL_SUBSCRIPTION_NOT_AVAILABLE = 128; + + /** + * Received error from SIM card + */ + public static final int RESULT_RIL_SIM_ERROR = 129; + + /** + * Cannot process the request in current SIM state + */ + public static final int RESULT_RIL_INVALID_SIM_STATE = 130; + + /** + * ACK received when there is no SMS to ack + */ + public static final int RESULT_RIL_NO_SMS_TO_ACK = 131; + + /** + * SIM is busy + */ + public static final int RESULT_RIL_SIM_BUSY = 132; + + /** + * The target EF is full + */ + public static final int RESULT_RIL_SIM_FULL = 133; + + /** + * Device does not have subscription + */ + public static final int RESULT_RIL_NO_SUBSCRIPTION = 134; + + /** + * Network cannot be found + */ + public static final int RESULT_RIL_NO_NETWORK_FOUND = 135; + + /** + * Operation cannot be performed because the device is currently in use + */ + public static final int RESULT_RIL_DEVICE_IN_USE = 136; + + /** + * Operation aborted + */ + public static final int RESULT_RIL_ABORTED = 137; + + // SMS receiving results sent as a "result" extra in {@link Intents.SMS_REJECTED_ACTION} /** diff --git a/telephony/java/android/telephony/ims/ImsService.java b/telephony/java/android/telephony/ims/ImsService.java index e8642fef04f5..a3cbb4a436bc 100644 --- a/telephony/java/android/telephony/ims/ImsService.java +++ b/telephony/java/android/telephony/ims/ImsService.java @@ -458,8 +458,8 @@ public class ImsService extends Service { } } - private IImsRcsFeature createRcsFeatureInternal(int slotId, int subI) { - RcsFeature f = createRcsFeatureForSubscription(slotId, subI); + private IImsRcsFeature createRcsFeatureInternal(int slotId, int subId) { + RcsFeature f = createRcsFeatureForSubscription(slotId, subId); if (f != null) { f.setDefaultExecutor(getCachedExecutor()); setupFeature(f, slotId, ImsFeature.FEATURE_RCS); diff --git a/telephony/java/android/telephony/ims/feature/ImsFeature.java b/telephony/java/android/telephony/ims/feature/ImsFeature.java index f5b158fedd37..a42327b8a1a9 100644 --- a/telephony/java/android/telephony/ims/feature/ImsFeature.java +++ b/telephony/java/android/telephony/ims/feature/ImsFeature.java @@ -394,10 +394,12 @@ public abstract class ImsFeature { @VisibleForTesting public void addImsFeatureStatusCallback(@NonNull IImsFeatureStatusCallback c) { try { - // If we have just connected, send queued status. - c.notifyImsFeatureStatus(getFeatureState()); - // Add the callback if the callback completes successfully without a RemoteException. - mStatusCallbacks.register(c); + synchronized (mStatusCallbacks) { + // Add the callback if the callback completes successfully without a RemoteException + mStatusCallbacks.register(c); + // If we have just connected, send queued status. + c.notifyImsFeatureStatus(getFeatureState()); + } } catch (RemoteException e) { Log.w(LOG_TAG, "Couldn't notify feature state: " + e.getMessage()); } @@ -409,7 +411,9 @@ public abstract class ImsFeature { */ @VisibleForTesting public void removeImsFeatureStatusCallback(@NonNull IImsFeatureStatusCallback c) { - mStatusCallbacks.unregister(c); + synchronized (mStatusCallbacks) { + mStatusCallbacks.unregister(c); + } } /** diff --git a/tests/FlickerTests/AndroidTest.xml b/tests/FlickerTests/AndroidTest.xml index d91aa1ebe8f5..03eb462267cb 100644 --- a/tests/FlickerTests/AndroidTest.xml +++ b/tests/FlickerTests/AndroidTest.xml @@ -13,6 +13,8 @@ <option name="run-command" value="cmd window tracing level all" /> <!-- set WM tracing to frame (avoid incomplete states) --> <option name="run-command" value="cmd window tracing frame" /> + <!-- set Layer tracing buffer size to 50mb --> + <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 51200" /> <!-- ensure lock screen mode is swipe --> <option name="run-command" value="locksettings set-disabled false" /> <!-- restart launcher to activate TAPL --> |