diff options
187 files changed, 6441 insertions, 1636 deletions
diff --git a/Android.bp b/Android.bp index 0315c12f95a1..0a1456514010 100644 --- a/Android.bp +++ b/Android.bp @@ -214,6 +214,7 @@ java_library { "android.hardware.radio-V1.5-java", "android.hardware.radio-V1.6-java", "android.hardware.radio.data-V1-java", + "android.hardware.radio.ims-V1-java", "android.hardware.radio.messaging-V1-java", "android.hardware.radio.modem-V1-java", "android.hardware.radio.network-V2-java", 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/JobSchedulerBackgroundThread.java b/apex/jobscheduler/service/java/com/android/server/JobSchedulerBackgroundThread.java index a413f7b1f3ca..6b01a9f446af 100644 --- a/apex/jobscheduler/service/java/com/android/server/JobSchedulerBackgroundThread.java +++ b/apex/jobscheduler/service/java/com/android/server/JobSchedulerBackgroundThread.java @@ -22,6 +22,8 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.Trace; +import com.android.server.am.BroadcastLoopers; + import java.util.concurrent.Executor; /** @@ -45,6 +47,7 @@ public final class JobSchedulerBackgroundThread extends HandlerThread { sInstance = new JobSchedulerBackgroundThread(); sInstance.start(); final Looper looper = sInstance.getLooper(); + BroadcastLoopers.addLooper(looper); looper.setTraceTag(Trace.TRACE_TAG_SYSTEM_SERVER); looper.setSlowLogThresholdMs( SLOW_DISPATCH_THRESHOLD_MS, SLOW_DELIVERY_THRESHOLD_MS); 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..f659dbff72c7 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) { @@ -1903,7 +1916,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 +1924,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 +1968,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 +2059,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 +2074,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 +2118,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 +2452,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..df8f729b7ec8 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); 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..0eb93367e5e2 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 @@ -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/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/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/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/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/provider/Settings.java b/core/java/android/provider/Settings.java index 897b7c3afe54..fab6f7b97790 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -9876,6 +9876,13 @@ public final class Settings { "fingerprint_side_fps_auth_downtime"; /** + * Whether or not a SFPS device is required to be interactive for auth to unlock the device. + * @hide + */ + public static final String SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED = + "sfps_require_screen_on_to_auth_enabled"; + + /** * Whether or not debugging is enabled. * @hide */ 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/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/res/values/config.xml b/core/res/res/values/config.xml index 23dd1b407b00..9733f4e8a663 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4945,6 +4945,11 @@ <!-- If face auth sends the user directly to home/last open app, or stays on keyguard --> <bool name="config_faceAuthDismissesKeyguard">true</bool> + <!-- Default value for whether a SFPS device is required to be + {@link KeyguardUpdateMonitor#isDeviceInteractive()} for fingerprint auth + to unlock the device. --> + <bool name="config_requireScreenOnToAuthEnabled">false</bool> + <!-- The component name for the default profile supervisor, which can be set as a profile owner even after user setup is complete. The defined component should be used for supervision purposes only. The component must be part of a system app. --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 476d36d0c207..9e3235b885ab 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2725,6 +2725,7 @@ <java-symbol type="array" name="config_face_acquire_vendor_biometricprompt_ignorelist" /> <java-symbol type="bool" name="config_faceAuthSupportsSelfIllumination" /> <java-symbol type="bool" name="config_faceAuthDismissesKeyguard" /> + <java-symbol type="bool" name="config_requireScreenOnToAuthEnabled" /> <!-- Face config --> <java-symbol type="integer" name="config_faceMaxTemplatesPerUser" /> 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/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/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/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/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 26cb9f8e9ee1..b4b908dbff16 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -104,8 +104,8 @@ public final class MediaRouter2 { private final String mPackageName; /** - * Stores the latest copy of all routes received from {@link MediaRouter2ServiceImpl}, without - * any filtering, sorting, or deduplication. + * Stores the latest copy of all routes received from the system server, without any filtering, + * sorting, or deduplication. * * <p>Uses {@link MediaRoute2Info#getId()} to set each entry's key. */ 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 7fd49db93748..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,10 +18,10 @@ 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 +import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.gallery.R @@ -59,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)) @@ -76,6 +80,7 @@ object HomePageProvider : SettingsPageProvider { @Preview(showBackground = true) @Composable private fun HomeScreenPreview() { + SpaEnvironmentFactory.resetForPreview() SettingsTheme { HomePageProvider.Page(null) } 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 82073104a3a7..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 @@ -23,6 +23,7 @@ import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPage import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.gallery.SettingsPageProviderEnum @@ -98,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)) @@ -115,6 +120,7 @@ object ArgumentPageProvider : SettingsPageProvider { @Preview(showBackground = true) @Composable private fun ArgumentPagePreview() { + SpaEnvironmentFactory.resetForPreview() SettingsTheme { ArgumentPageProvider.Page( ArgumentPageModel.buildArgument(stringParam = "foo", intParam = 0) 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 fa8d51c3561f..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,19 +203,15 @@ 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 } } @Preview(showBackground = true) @Composable private fun PreferencePagePreview() { + SpaEnvironmentFactory.resetForPreview() SettingsTheme { PreferencePageProvider.Page(null) } 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 0973ba593904..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 @@ -19,6 +19,8 @@ package com.android.settingslib.spa.framework.common import android.app.Activity import android.content.Context import android.util.Log +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext private const val TAG = "SpaEnvironment" @@ -30,6 +32,20 @@ object SpaEnvironmentFactory { Log.d(TAG, "reset") } + @Composable + fun resetForPreview() { + val context = LocalContext.current + spaEnvironment = object : SpaEnvironment(context) { + override val pageProviderRepository = lazy { + SettingsPageProviderRepository( + allPageProviders = emptyList(), + rootPages = emptyList() + ) + } + } + Log.d(TAG, "resetForPreview") + } + val instance: SpaEnvironment get() { if (spaEnvironment == null) @@ -38,11 +54,14 @@ object SpaEnvironmentFactory { } } -abstract class SpaEnvironment(val context: Context) { +abstract class SpaEnvironment(context: Context) { abstract val pageProviderRepository: Lazy<SettingsPageProviderRepository> val entryRepository = lazy { SettingsEntryRepository(pageProviderRepository.value) } + // In Robolectric test, applicationContext is not available. Use context as fallback. + val appContext: Context = context.applicationContext ?: context + open val browseActivityClass: Class<out Activity>? = null open val entryProviderAuthorities: String? = 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/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExt.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExt.kt index a22d1feade0a..2b2f11c7e23f 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExt.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExt.kt @@ -1,6 +1,26 @@ +/* + * 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.spaprivileged.model.app +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo import android.content.pm.PackageManager +import android.content.pm.PackageManager.ResolveInfoFlags /** * Checks if a package is system module. @@ -12,3 +32,18 @@ fun PackageManager.isSystemModule(packageName: String): Boolean = try { // Expected, not system module false } + +/** + * Resolves the activity to start for a given application and action. + */ +fun PackageManager.resolveActionForApp( + app: ApplicationInfo, + action: String, + flags: Int = 0, +): ActivityInfo? { + val intent = Intent(action).apply { + `package` = app.packageName + } + return resolveActivityAsUser(intent, ResolveInfoFlags.of(flags.toLong()), app.userId) + ?.activityInfo +} diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExtTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExtTest.kt new file mode 100644 index 000000000000..4207490cba80 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExtTest.kt @@ -0,0 +1,129 @@ +/* + * 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.spaprivileged.model.app + +import android.content.ComponentName +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.ModuleInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.NameNotFoundException +import android.content.pm.PackageManager.ResolveInfoFlags +import android.content.pm.ResolveInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.eq +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.Mockito.`when` as whenever + +@RunWith(AndroidJUnit4::class) +class PackageManagerExtTest { + @JvmField + @Rule + val mockito: MockitoRule = MockitoJUnit.rule() + + @Mock + private lateinit var packageManager: PackageManager + + private fun mockResolveActivityAsUser(resolveInfo: ResolveInfo?) { + whenever( + packageManager.resolveActivityAsUser(any(), any<ResolveInfoFlags>(), eq(APP.userId)) + ).thenReturn(resolveInfo) + } + + @Test + fun isSystemModule_whenSystemModule_returnTrue() { + whenever(packageManager.getModuleInfo(PACKAGE_NAME, 0)).thenReturn(ModuleInfo()) + + val isSystemModule = packageManager.isSystemModule(PACKAGE_NAME) + + assertThat(isSystemModule).isTrue() + } + + @Test + fun isSystemModule_whenNotSystemModule_returnFalse() { + whenever(packageManager.getModuleInfo(PACKAGE_NAME, 0)).thenThrow(NameNotFoundException()) + + val isSystemModule = packageManager.isSystemModule(PACKAGE_NAME) + + assertThat(isSystemModule).isFalse() + } + + @Test + fun resolveActionForApp_noResolveInfo() { + mockResolveActivityAsUser(null) + + val activityInfo = packageManager.resolveActionForApp(APP, ACTION) + + assertThat(activityInfo).isNull() + } + + @Test + fun resolveActionForApp_noActivityInfo() { + mockResolveActivityAsUser(ResolveInfo()) + + val activityInfo = packageManager.resolveActionForApp(APP, ACTION) + + assertThat(activityInfo).isNull() + } + + @Test + fun resolveActionForApp_hasActivityInfo() { + mockResolveActivityAsUser(ResolveInfo().apply { + activityInfo = ActivityInfo().apply { + packageName = PACKAGE_NAME + name = ACTIVITY_NAME + } + }) + + val activityInfo = packageManager.resolveActionForApp(APP, ACTION)!! + + assertThat(activityInfo.componentName).isEqualTo(ComponentName(PACKAGE_NAME, ACTIVITY_NAME)) + } + + @Test + fun resolveActionForApp_withFlags() { + packageManager.resolveActionForApp( + app = APP, + action = ACTION, + flags = PackageManager.GET_META_DATA, + ) + + val flagsCaptor = ArgumentCaptor.forClass(ResolveInfoFlags::class.java) + verify(packageManager).resolveActivityAsUser(any(), flagsCaptor.capture(), eq(APP.userId)) + assertThat(flagsCaptor.value.value).isEqualTo(PackageManager.GET_META_DATA.toLong()) + } + + private companion object { + const val PACKAGE_NAME = "package.name" + const val ACTIVITY_NAME = "ActivityName" + const val ACTION = "action" + const val UID = 123 + val APP = ApplicationInfo().apply { + packageName = PACKAGE_NAME + uid = UID + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/OWNERS b/packages/SettingsLib/src/com/android/settingslib/qrcode/OWNERS new file mode 100644 index 000000000000..61c73fb733a9 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/qrcode/OWNERS @@ -0,0 +1,8 @@ +# Default reviewers for this and subdirectories. +bonianchen@google.com +changbetty@google.com +goldmanj@google.com +wengsu@google.com +zoeychen@google.com + +# Emergency approvers in case the above are not available diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index def7ddc86556..98af15a85238 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -122,6 +122,7 @@ public class SecureSettings { Settings.Secure.FINGERPRINT_SIDE_FPS_BP_POWER_WINDOW, Settings.Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW, Settings.Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME, + Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, Settings.Secure.ACTIVE_UNLOCK_ON_WAKE, Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT, Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index cde4bc42b225..80af69c32b05 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -177,6 +177,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME, NON_NEGATIVE_INTEGER_VALIDATOR); + VALIDATORS.put(Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SHOW_MEDIA_WHEN_BYPASSING, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.FACE_UNLOCK_APP_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION, BOOLEAN_VALIDATOR); 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/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..71e04468c07a 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -38,21 +38,21 @@ 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.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 +69,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 +142,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 +160,7 @@ open class ClockEventController @Inject constructor( private val configListener = object : ConfigurationController.ConfigurationListener { override fun onThemeChanged() { clock?.events?.onColorPaletteChanged(resources) + updateColors() } override fun onDensityOrFontScaleChanged() { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt index 71470e8870de..a0206f1f1e70 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt @@ -35,6 +35,7 @@ data class KeyguardFingerprintListenModel( val keyguardOccluded: Boolean, val occludingAppRequestingFp: Boolean, val primaryUser: Boolean, + val shouldListenSfpsState: Boolean, val shouldListenForFingerprintAssistant: Boolean, val switchingUser: Boolean, val udfps: Boolean, diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 39dc609c9bb7..b84c98d71a28 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -150,6 +150,7 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.util.Assert; +import com.android.systemui.util.settings.SecureSettings; import com.google.android.collect.Lists; @@ -321,17 +322,20 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final ArrayList<WeakReference<KeyguardUpdateMonitorCallback>> mCallbacks = Lists.newArrayList(); private ContentObserver mDeviceProvisionedObserver; + private ContentObserver mSfpsRequireScreenOnToAuthPrefObserver; private final ContentObserver mTimeFormatChangeObserver; private boolean mSwitchingUser; private boolean mDeviceInteractive; + private boolean mSfpsRequireScreenOnToAuthPrefEnabled; private final SubscriptionManager mSubscriptionManager; private final TelephonyListenerManager mTelephonyListenerManager; private final TrustManager mTrustManager; private final UserManager mUserManager; private final DevicePolicyManager mDevicePolicyManager; private final BroadcastDispatcher mBroadcastDispatcher; + private final SecureSettings mSecureSettings; private final InteractionJankMonitor mInteractionJankMonitor; private final LatencyTracker mLatencyTracker; private final StatusBarStateController mStatusBarStateController; @@ -1930,6 +1934,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab Context context, @Main Looper mainLooper, BroadcastDispatcher broadcastDispatcher, + SecureSettings secureSettings, DumpManager dumpManager, @Background Executor backgroundExecutor, @Main Executor mainExecutor, @@ -1972,6 +1977,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mStatusBarState = mStatusBarStateController.getState(); mLockPatternUtils = lockPatternUtils; mAuthController = authController; + mSecureSettings = secureSettings; dumpManager.registerDumpable(getClass().getName(), this); mSensorPrivacyManager = sensorPrivacyManager; mActiveUnlockConfig = activeUnlockConfiguration; @@ -2214,9 +2220,37 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab Settings.System.TIME_12_24))); } }; + mContext.getContentResolver().registerContentObserver( Settings.System.getUriFor(Settings.System.TIME_12_24), false, mTimeFormatChangeObserver, UserHandle.USER_ALL); + + if (isSfpsSupported() && isSfpsEnrolled()) { + updateSfpsRequireScreenOnToAuthPref(); + mSfpsRequireScreenOnToAuthPrefObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + updateSfpsRequireScreenOnToAuthPref(); + } + }; + + mContext.getContentResolver().registerContentObserver( + mSecureSettings.getUriFor( + Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED), + false, + mSfpsRequireScreenOnToAuthPrefObserver, + getCurrentUser()); + } + } + + protected void updateSfpsRequireScreenOnToAuthPref() { + final int defaultSfpsRequireScreenOnToAuthValue = + mContext.getResources().getBoolean( + com.android.internal.R.bool.config_requireScreenOnToAuthEnabled) ? 1 : 0; + mSfpsRequireScreenOnToAuthPrefEnabled = mSecureSettings.getIntForUser( + Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, + defaultSfpsRequireScreenOnToAuthValue, + getCurrentUser()) != 0; } private void initializeSimState() { @@ -2261,6 +2295,22 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } /** + * @return true if there's at least one sfps enrollment for the current user. + */ + public boolean isSfpsEnrolled() { + return mAuthController.isSfpsEnrolled(getCurrentUser()); + } + + /** + * @return true if sfps HW is supported on this device. Can return true even if the user has + * not enrolled sfps. This may be false if called before onAllAuthenticatorsRegistered. + */ + public boolean isSfpsSupported() { + return mAuthController.getSfpsProps() != null + && !mAuthController.getSfpsProps().isEmpty(); + } + + /** * @return true if there's at least one face enrolled */ public boolean isFaceEnrolled() { @@ -2588,8 +2638,22 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab && !isEncryptedOrLockdownForUser && userDoesNotHaveTrust); - final boolean shouldListen = shouldListenKeyguardState && shouldListenUserState - && shouldListenBouncerState && shouldListenUdfpsState && !isFingerprintLockedOut(); + boolean shouldListenSfpsState = true; + // If mSfpsRequireScreenOnToAuthPrefEnabled, require screen on to listen to SFPS + if (isSfpsSupported() && isSfpsEnrolled() && mSfpsRequireScreenOnToAuthPrefEnabled) { + shouldListenSfpsState = isDeviceInteractive(); + } + + boolean shouldListen = shouldListenKeyguardState && shouldListenUserState + && shouldListenBouncerState && !isFingerprintLockedOut(); + + if (isUdfpsSupported()) { + shouldListen = shouldListen && shouldListenUdfpsState; + } + + if (isSfpsSupported()) { + shouldListen = shouldListen && shouldListenSfpsState; + } maybeLogListenerModelData( new KeyguardFingerprintListenModel( @@ -2611,6 +2675,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mKeyguardOccluded, mOccludingAppRequestingFp, mIsPrimaryUser, + shouldListenSfpsState, shouldListenForFingerprintAssistant, mSwitchingUser, isUdfps, @@ -3712,6 +3777,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mContext.getContentResolver().unregisterContentObserver(mTimeFormatChangeObserver); } + if (mSfpsRequireScreenOnToAuthPrefObserver != null) { + mContext.getContentResolver().unregisterContentObserver( + mSfpsRequireScreenOnToAuthPrefObserver); + } + try { ActivityManager.getService().unregisterUserSwitchObserver(mUserSwitchObserver); } catch (RemoteException e) { @@ -3784,6 +3854,13 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab pw.println(" mBouncerIsOrWillBeShowing=" + mBouncerIsOrWillBeShowing); pw.println(" mStatusBarState=" + StatusBarState.toString(mStatusBarState)); pw.println(" mUdfpsBouncerShowing=" + mUdfpsBouncerShowing); + } else if (isSfpsSupported()) { + pw.println(" sfpsEnrolled=" + isSfpsEnrolled()); + pw.println(" shouldListenForSfps=" + shouldListenForFingerprint(false)); + if (isSfpsEnrolled()) { + pw.println(" mSfpsRequireScreenOnToAuthPrefEnabled=" + + mSfpsRequireScreenOnToAuthPrefEnabled); + } } } if (mFaceManager != null && mFaceManager.isHardwareDetected()) { 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/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 313ff4157155..709e94fa466d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -160,6 +160,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, @NonNull private final SparseBooleanArray mUdfpsEnrolledForUser; @NonNull private final SparseBooleanArray mFaceEnrolledForUser; + @NonNull private final SparseBooleanArray mSfpsEnrolledForUser; @NonNull private final SensorPrivacyManager mSensorPrivacyManager; private final WakefulnessLifecycle mWakefulnessLifecycle; private boolean mAllFingerprintAuthenticatorsRegistered; @@ -365,6 +366,15 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, } } } + if (mSidefpsProps == null) { + Log.d(TAG, "handleEnrollmentsChanged, mSidefpsProps is null"); + } else { + for (FingerprintSensorPropertiesInternal prop : mSidefpsProps) { + if (prop.sensorId == sensorId) { + mSfpsEnrolledForUser.put(userId, hasEnrollments); + } + } + } for (Callback cb : mCallbacks) { cb.onEnrollmentsChanged(modality); } @@ -722,6 +732,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, mWindowManager = windowManager; mInteractionJankMonitor = jankMonitor; mUdfpsEnrolledForUser = new SparseBooleanArray(); + mSfpsEnrolledForUser = new SparseBooleanArray(); mFaceEnrolledForUser = new SparseBooleanArray(); mVibratorHelper = vibrator; @@ -964,6 +975,11 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, return mUdfpsProps; } + @Nullable + public List<FingerprintSensorPropertiesInternal> getSfpsProps() { + return mSidefpsProps; + } + private String getErrorString(@Modality int modality, int error, int vendorCode) { switch (modality) { case TYPE_FACE: @@ -1090,6 +1106,17 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, return mUdfpsEnrolledForUser.get(userId); } + /** + * Whether the passed userId has enrolled SFPS. + */ + public boolean isSfpsEnrolled(int userId) { + if (mSidefpsController == null) { + return false; + } + + return mSfpsEnrolledForUser.get(userId); + } + /** If BiometricPrompt is currently being shown to the user. */ public boolean isShowing() { return mCurrentDialog != null; 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..2aa70642ac09 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -4729,8 +4729,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 +5033,7 @@ public final class NotificationPanelViewController { } public boolean isFullyExpanded() { - return mExpandedHeight >= getMaxPanelHeight(); + return mExpandedHeight >= getMaxPanelTransitionDistance(); } public boolean isFullyCollapsed() { @@ -6214,6 +6212,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/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/keyguard/KeyguardListenQueueTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt index aca60c033bac..131cf7d33e3a 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt @@ -72,6 +72,7 @@ private fun fingerprintModel(user: Int) = KeyguardFingerprintListenModel( keyguardOccluded = false, occludingAppRequestingFp = false, primaryUser = false, + shouldListenSfpsState = false, shouldListenForFingerprintAssistant = false, switchingUser = false, udfps = false, diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index 680c3b8f546b..ee480b61ecb4 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -21,6 +21,7 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRIN import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT_PERMANENT; +import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON; import static android.telephony.SubscriptionManager.DATA_ROAMING_DISABLE; import static android.telephony.SubscriptionManager.NAME_SOURCE_CARRIER_ID; @@ -39,6 +40,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -54,6 +56,7 @@ import android.app.trust.IStrongAuthTracker; import android.app.trust.TrustManager; import android.content.BroadcastReceiver; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -61,18 +64,21 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; +import android.database.ContentObserver; import android.hardware.SensorPrivacyManager; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricSourceType; import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; +import android.hardware.biometrics.SensorProperties; import android.hardware.face.FaceManager; import android.hardware.face.FaceSensorProperties; import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorProperties; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.net.Uri; import android.nfc.NfcAdapter; import android.os.Bundle; import android.os.CancellationSignal; @@ -111,6 +117,7 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.util.settings.GlobalSettings; +import com.android.systemui.util.settings.SecureSettings; import org.junit.After; import org.junit.Assert; @@ -182,6 +189,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Mock private BroadcastDispatcher mBroadcastDispatcher; @Mock + private SecureSettings mSecureSettings; + @Mock private TelephonyManager mTelephonyManager; @Mock private SensorPrivacyManager mSensorPrivacyManager; @@ -215,6 +224,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { private GlobalSettings mGlobalSettings; private FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig; + private final int mCurrentUserId = 100; private final UserInfo mCurrentUserInfo = new UserInfo(mCurrentUserId, "Test user", 0); @@ -224,6 +234,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Captor private ArgumentCaptor<FaceManager.AuthenticationCallback> mAuthenticationCallbackCaptor; + @Mock + private Uri mURI; + // Direct executor private final Executor mBackgroundExecutor = Runnable::run; private final Executor mMainExecutor = Runnable::run; @@ -305,6 +318,15 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mTestableLooper = TestableLooper.get(this); allowTestableLooperAsMainThread(); + + when(mSecureSettings.getUriFor(anyString())).thenReturn(mURI); + + final ContentResolver contentResolver = mContext.getContentResolver(); + ExtendedMockito.spyOn(contentResolver); + doNothing().when(contentResolver) + .registerContentObserver(any(Uri.class), anyBoolean(), any(ContentObserver.class), + anyInt()); + mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext); verify(mBiometricManager) @@ -1136,6 +1158,67 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test + public void testStartsListeningForSfps_whenKeyguardIsVisible_ifRequireScreenOnToAuthEnabled() + throws RemoteException { + // SFPS supported and enrolled + setup_SfpsProps(); + + // WHEN require screen on to auth is disabled, and keyguard is not awake + when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).thenReturn(0); + mKeyguardUpdateMonitor.updateSfpsRequireScreenOnToAuthPref(); + + mContext.getOrCreateTestableResources().addOverride( + com.android.internal.R.bool.config_requireScreenOnToAuthEnabled, true); + + // Preconditions for sfps auth to run + keyguardNotGoingAway(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + + statusBarShadeIsLocked(); + mTestableLooper.processAllMessages(); + + // THEN we should listen for sfps when screen off, because require screen on is disabled + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); + + // WHEN require screen on to auth is enabled, and keyguard is not awake + when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).thenReturn(1); + mKeyguardUpdateMonitor.updateSfpsRequireScreenOnToAuthPref(); + + // THEN we shouldn't listen for sfps when screen off, because require screen on is enabled + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse(); + + // Device now awake & keyguard is now interactive + deviceNotGoingToSleep(); + deviceIsInteractive(); + keyguardIsVisible(); + + // THEN we should listen for sfps when screen on, and require screen on is enabled + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); + } + + private void setup_SfpsProps() { + final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); + props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON)); + when(mAuthController.getSfpsProps()).thenReturn(props); + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + } + + private FingerprintSensorPropertiesInternal newFingerprintSensorPropertiesInternal( + @FingerprintSensorProperties.SensorType int sensorType) { + return new FingerprintSensorPropertiesInternal( + 0 /* sensorId */, + SensorProperties.STRENGTH_STRONG, + 1 /* maxEnrollmentsPerUser */, + new ArrayList<ComponentInfoInternal>(), + sensorType, + true /* resetLockoutRequiresHardwareAuthToken */); + } + + @Test public void testShouldNotListenForUdfps_whenTrustEnabled() { // GIVEN a "we should listen for udfps" state mStatusBarStateListener.onStateChanged(StatusBarState.KEYGUARD); @@ -1804,7 +1887,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { protected TestableKeyguardUpdateMonitor(Context context) { super(context, TestableLooper.get(KeyguardUpdateMonitorTest.this).getLooper(), - mBroadcastDispatcher, mDumpManager, + mBroadcastDispatcher, mSecureSettings, mDumpManager, mBackgroundExecutor, mMainExecutor, mStatusBarStateController, mLockPatternUtils, mAuthController, mTelephonyListenerManager, 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..89c5e59dfb8f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -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/statusbar/notification/collection/render/ShadeViewDifferTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.java deleted file mode 100644 index ab712649a90f..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.java +++ /dev/null @@ -1,317 +0,0 @@ -/* - * 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. - */ - -package com.android.systemui.statusbar.notification.collection.render; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -import android.content.Context; -import android.testing.AndroidTestingRunner; -import android.view.View; -import android.widget.FrameLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.List; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -public class ShadeViewDifferTest extends SysuiTestCase { - private ShadeViewDiffer mDiffer; - - private FakeController mRootController = new FakeController(mContext, "RootController"); - private FakeController mController1 = new FakeController(mContext, "Controller1"); - private FakeController mController2 = new FakeController(mContext, "Controller2"); - private FakeController mController3 = new FakeController(mContext, "Controller3"); - private FakeController mController4 = new FakeController(mContext, "Controller4"); - private FakeController mController5 = new FakeController(mContext, "Controller5"); - private FakeController mController6 = new FakeController(mContext, "Controller6"); - private FakeController mController7 = new FakeController(mContext, "Controller7"); - - @Mock - ShadeViewDifferLogger mLogger; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - - mDiffer = new ShadeViewDiffer(mRootController, mLogger); - } - - @Test - public void testAddInitialViews() { - // WHEN a spec is applied to an empty root - // THEN the final tree matches the spec - applySpecAndCheck( - node(mController1), - node(mController2, - node(mController3), - node(mController4) - ), - node(mController5) - ); - } - - @Test - public void testDetachViews() { - // GIVEN a preexisting tree of controllers - applySpecAndCheck( - node(mController1), - node(mController2, - node(mController3), - node(mController4) - ), - node(mController5) - ); - - // WHEN the new spec removes nodes - // THEN the final tree matches the spec - applySpecAndCheck( - node(mController5) - ); - } - - @Test - public void testReparentChildren() { - // GIVEN a preexisting tree of controllers - applySpecAndCheck( - node(mController1), - node(mController2, - node(mController3), - node(mController4) - ), - node(mController5) - ); - - // WHEN the parents of the controllers are all shuffled around - // THEN the final tree matches the spec - applySpecAndCheck( - node(mController1), - node(mController4), - node(mController3, - node(mController2) - ) - ); - } - - @Test - public void testReorderChildren() { - // GIVEN a preexisting tree of controllers - applySpecAndCheck( - node(mController1), - node(mController2), - node(mController3), - node(mController4) - ); - - // WHEN the children change order - // THEN the final tree matches the spec - applySpecAndCheck( - node(mController3), - node(mController2), - node(mController4), - node(mController1) - ); - } - - @Test - public void testRemovedGroupsAreBrokenApart() { - // GIVEN a preexisting tree with a group - applySpecAndCheck( - node(mController1), - node(mController2, - node(mController3), - node(mController4), - node(mController5) - ) - ); - - // WHEN the new spec removes the entire group - applySpecAndCheck( - node(mController1) - ); - - // THEN the group children are no longer attached to their parent - assertNull(mController3.getView().getParent()); - assertNull(mController4.getView().getParent()); - assertNull(mController5.getView().getParent()); - } - - @Test - public void testUnmanagedViews() { - // GIVEN a preexisting tree of controllers - applySpecAndCheck( - node(mController1), - node(mController2, - node(mController3), - node(mController4) - ), - node(mController5) - ); - - // GIVEN some additional unmanaged views attached to the tree - View unmanagedView1 = new View(mContext); - View unmanagedView2 = new View(mContext); - - mRootController.getView().addView(unmanagedView1, 1); - mController2.getView().addView(unmanagedView2, 0); - - // WHEN a new spec is applied with additional nodes - // THEN the final tree matches the spec - applySpecAndCheck( - node(mController1), - node(mController2, - node(mController3), - node(mController4), - node(mController6) - ), - node(mController5), - node(mController7) - ); - - // THEN the unmanaged views have been pushed to the end of their parents - assertEquals(unmanagedView1, mRootController.view.getChildAt(4)); - assertEquals(unmanagedView2, mController2.view.getChildAt(3)); - } - - private void applySpecAndCheck(NodeSpec spec) { - mDiffer.applySpec(spec); - checkMatchesSpec(spec); - } - - private void applySpecAndCheck(SpecBuilder... children) { - applySpecAndCheck(node(mRootController, children).build()); - } - - private void checkMatchesSpec(NodeSpec spec) { - final NodeController parent = spec.getController(); - final List<NodeSpec> children = spec.getChildren(); - - for (int i = 0; i < children.size(); i++) { - NodeSpec childSpec = children.get(i); - View view = parent.getChildAt(i); - - assertEquals( - "Child " + i + " of parent " + parent.getNodeLabel() + " should be " - + childSpec.getController().getNodeLabel() + " but is instead " - + (view != null ? mDiffer.getViewLabel(view) : "null"), - view, - childSpec.getController().getView()); - - if (!childSpec.getChildren().isEmpty()) { - checkMatchesSpec(childSpec); - } - } - } - - private static class FakeController implements NodeController { - - public final FrameLayout view; - private final String mLabel; - - FakeController(Context context, String label) { - view = new FrameLayout(context); - mLabel = label; - } - - @NonNull - @Override - public String getNodeLabel() { - return mLabel; - } - - @NonNull - @Override - public FrameLayout getView() { - return view; - } - - @Override - public int getChildCount() { - return view.getChildCount(); - } - - @Override - public View getChildAt(int index) { - return view.getChildAt(index); - } - - @Override - public void addChildAt(@NonNull NodeController child, int index) { - view.addView(child.getView(), index); - } - - @Override - public void moveChildTo(@NonNull NodeController child, int index) { - view.removeView(child.getView()); - view.addView(child.getView(), index); - } - - @Override - public void removeChild(@NonNull NodeController child, boolean isTransfer) { - view.removeView(child.getView()); - } - - @Override - public void onViewAdded() { - } - - @Override - public void onViewMoved() { - } - - @Override - public void onViewRemoved() { - } - } - - private static class SpecBuilder { - private final NodeController mController; - private final SpecBuilder[] mChildren; - - SpecBuilder(NodeController controller, SpecBuilder... children) { - mController = controller; - mChildren = children; - } - - public NodeSpec build() { - return build(null); - } - - public NodeSpec build(@Nullable NodeSpec parent) { - final NodeSpecImpl spec = new NodeSpecImpl(parent, mController); - for (SpecBuilder childBuilder : mChildren) { - spec.getChildren().add(childBuilder.build(spec)); - } - return spec; - } - } - - private static SpecBuilder node(NodeController controller, SpecBuilder... children) { - return new SpecBuilder(controller, children); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.kt new file mode 100644 index 000000000000..15cf17dbcf86 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.kt @@ -0,0 +1,234 @@ +/* + * 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. + */ +package com.android.systemui.statusbar.notification.collection.render + +import android.content.Context +import android.testing.AndroidTestingRunner +import android.view.View +import android.widget.FrameLayout +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.mock +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class ShadeViewDifferTest : SysuiTestCase() { + private lateinit var differ: ShadeViewDiffer + private val rootController = FakeController(mContext, "RootController") + private val controller1 = FakeController(mContext, "Controller1") + private val controller2 = FakeController(mContext, "Controller2") + private val controller3 = FakeController(mContext, "Controller3") + private val controller4 = FakeController(mContext, "Controller4") + private val controller5 = FakeController(mContext, "Controller5") + private val controller6 = FakeController(mContext, "Controller6") + private val controller7 = FakeController(mContext, "Controller7") + private val logger: ShadeViewDifferLogger = mock() + + @Before + fun setUp() { + differ = ShadeViewDiffer(rootController, logger) + } + + @Test + fun testAddInitialViews() { + // WHEN a spec is applied to an empty root + // THEN the final tree matches the spec + applySpecAndCheck( + node(controller1), + node(controller2, node(controller3), node(controller4)), + node(controller5) + ) + } + + @Test + fun testDetachViews() { + // GIVEN a preexisting tree of controllers + applySpecAndCheck( + node(controller1), + node(controller2, node(controller3), node(controller4)), + node(controller5) + ) + + // WHEN the new spec removes nodes + // THEN the final tree matches the spec + applySpecAndCheck(node(controller5)) + } + + @Test + fun testReparentChildren() { + // GIVEN a preexisting tree of controllers + applySpecAndCheck( + node(controller1), + node(controller2, node(controller3), node(controller4)), + node(controller5) + ) + + // WHEN the parents of the controllers are all shuffled around + // THEN the final tree matches the spec + applySpecAndCheck( + node(controller1), + node(controller4), + node(controller3, node(controller2)) + ) + } + + @Test + fun testReorderChildren() { + // GIVEN a preexisting tree of controllers + applySpecAndCheck( + node(controller1), + node(controller2), + node(controller3), + node(controller4) + ) + + // WHEN the children change order + // THEN the final tree matches the spec + applySpecAndCheck( + node(controller3), + node(controller2), + node(controller4), + node(controller1) + ) + } + + @Test + fun testRemovedGroupsAreBrokenApart() { + // GIVEN a preexisting tree with a group + applySpecAndCheck( + node(controller1), + node(controller2, node(controller3), node(controller4), node(controller5)) + ) + + // WHEN the new spec removes the entire group + applySpecAndCheck(node(controller1)) + + // THEN the group children are no longer attached to their parent + Assert.assertNull(controller3.view.parent) + Assert.assertNull(controller4.view.parent) + Assert.assertNull(controller5.view.parent) + } + + @Test + fun testUnmanagedViews() { + // GIVEN a preexisting tree of controllers + applySpecAndCheck( + node(controller1), + node(controller2, node(controller3), node(controller4)), + node(controller5) + ) + + // GIVEN some additional unmanaged views attached to the tree + val unmanagedView1 = View(mContext) + val unmanagedView2 = View(mContext) + rootController.view.addView(unmanagedView1, 1) + controller2.view.addView(unmanagedView2, 0) + + // WHEN a new spec is applied with additional nodes + // THEN the final tree matches the spec + applySpecAndCheck( + node(controller1), + node(controller2, node(controller3), node(controller4), node(controller6)), + node(controller5), + node(controller7) + ) + + // THEN the unmanaged views have been pushed to the end of their parents + Assert.assertEquals(unmanagedView1, rootController.view.getChildAt(4)) + Assert.assertEquals(unmanagedView2, controller2.view.getChildAt(3)) + } + + private fun applySpecAndCheck(spec: NodeSpec) { + differ.applySpec(spec) + checkMatchesSpec(spec) + } + + private fun applySpecAndCheck(vararg children: SpecBuilder) { + applySpecAndCheck(node(rootController, *children).build()) + } + + private fun checkMatchesSpec(spec: NodeSpec) { + val parent = spec.controller + val children = spec.children + for (i in children.indices) { + val childSpec = children[i] + val view = parent.getChildAt(i) + Assert.assertEquals( + "Child $i of parent ${parent.nodeLabel} " + + "should be ${childSpec.controller.nodeLabel} " + + "but instead " + + view?.let(differ::getViewLabel), + view, + childSpec.controller.view + ) + if (childSpec.children.isNotEmpty()) { + checkMatchesSpec(childSpec) + } + } + } + + private class FakeController(context: Context, label: String) : NodeController { + override val view: FrameLayout = FrameLayout(context) + override val nodeLabel: String = label + override fun getChildCount(): Int = view.childCount + + override fun getChildAt(index: Int): View? { + return view.getChildAt(index) + } + + override fun addChildAt(child: NodeController, index: Int) { + view.addView(child.view, index) + } + + override fun moveChildTo(child: NodeController, index: Int) { + view.removeView(child.view) + view.addView(child.view, index) + } + + override fun removeChild(child: NodeController, isTransfer: Boolean) { + view.removeView(child.view) + } + + override fun onViewAdded() {} + override fun onViewMoved() {} + override fun onViewRemoved() {} + } + + private class SpecBuilder( + private val mController: NodeController, + private val children: Array<out SpecBuilder> + ) { + + @JvmOverloads + fun build(parent: NodeSpec? = null): NodeSpec { + val spec = NodeSpecImpl(parent, mController) + for (childBuilder in children) { + spec.children.add(childBuilder.build(spec)) + } + return spec + } + } + + companion object { + private fun node(controller: NodeController, vararg children: SpecBuilder): SpecBuilder { + return SpecBuilder(controller, children) + } + } +} 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/services/core/java/com/android/server/ServiceThread.java b/services/core/java/com/android/server/ServiceThread.java index 6d8e49c7c869..3ea4b86d5296 100644 --- a/services/core/java/com/android/server/ServiceThread.java +++ b/services/core/java/com/android/server/ServiceThread.java @@ -22,6 +22,8 @@ import android.os.Looper; import android.os.Process; import android.os.StrictMode; +import com.android.server.am.BroadcastLoopers; + /** * Special handler thread that we create for system services that require their own loopers. */ @@ -46,6 +48,14 @@ public class ServiceThread extends HandlerThread { super.run(); } + @Override + protected void onLooperPrepared() { + // Almost all service threads are used for dispatching broadcast + // intents, so register ourselves to ensure that "wait-for-broadcast" + // shell commands are able to drain any pending broadcasts + BroadcastLoopers.addLooper(getLooper()); + } + protected static Handler makeSharedHandler(Looper looper) { return new Handler(looper, /*callback=*/ null, /* async=*/ false, /* shared=*/ true); } diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 72876f669f01..c280719b06a3 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -147,6 +147,7 @@ import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.Preconditions; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import com.android.server.am.BroadcastLoopers; import com.android.server.pm.Installer; import com.android.server.pm.UserManagerInternal; import com.android.server.storage.AppFuseBridge; @@ -1809,6 +1810,7 @@ class StorageManagerService extends IStorageManager.Stub HandlerThread hthread = new HandlerThread(TAG); hthread.start(); + BroadcastLoopers.addLooper(hthread.getLooper()); mHandler = new StorageManagerServiceHandler(hthread.getLooper()); // Add OBB Action Handler to StorageManagerService thread. diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index ea6688442fa4..c95495747ead 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 @@ -2508,6 +2509,8 @@ public class ActivityManagerService extends IActivityManager.Stub Watchdog.getInstance().addMonitor(this); Watchdog.getInstance().addThread(mHandler); + BroadcastLoopers.addLooper(BackgroundThread.getHandler().getLooper()); + // bind background threads to little cores // this is expected to fail inside of framework tests because apps can't touch cpusets directly // make sure we've already adjusted system_server's internal view of itself first @@ -3455,13 +3458,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 +3537,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 +3659,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 +3677,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 +3689,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 +3704,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 +3747,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 +3777,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 +3792,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 diff --git a/services/core/java/com/android/server/am/BroadcastLoopers.java b/services/core/java/com/android/server/am/BroadcastLoopers.java index bebb48473fc3..b828720c9162 100644 --- a/services/core/java/com/android/server/am/BroadcastLoopers.java +++ b/services/core/java/com/android/server/am/BroadcastLoopers.java @@ -25,6 +25,8 @@ import android.os.SystemClock; import android.util.ArraySet; import android.util.Slog; +import com.android.internal.annotations.GuardedBy; + import java.io.PrintWriter; import java.util.Objects; import java.util.concurrent.CountDownLatch; @@ -37,6 +39,7 @@ import java.util.concurrent.CountDownLatch; public class BroadcastLoopers { private static final String TAG = "BroadcastLoopers"; + @GuardedBy("sLoopers") private static final ArraySet<Looper> sLoopers = new ArraySet<>(); /** diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index 9e9eb71db4e5..95cefea532a6 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -144,14 +144,6 @@ class BroadcastQueueModernImpl extends BroadcastQueue { mRunning = new BroadcastProcessQueue[mConstants.MAX_RUNNING_PROCESS_QUEUES]; } - // TODO: add support for replacing pending broadcasts - // TODO: add support for merging pending broadcasts - - // TODO: consider reordering foreground broadcasts within queue - - // TODO: pause queues when background services are running - // TODO: pause queues when processes are frozen - /** * Map from UID to per-process broadcast queues. If a UID hosts more than * one process, each additional process is stored as a linked list using @@ -412,21 +404,24 @@ class BroadcastQueueModernImpl extends BroadcastQueue { queue.runningTraceTrackName = TAG + ".mRunning[" + queueIndex + "]"; queue.runningOomAdjusted = queue.isPendingManifest(); + // If already warm, we can make OOM adjust request immediately; + // otherwise we need to wait until process becomes warm + if (processWarm) { + notifyStartedRunning(queue); + updateOomAdj |= queue.runningOomAdjusted; + } + // If we're already warm, schedule next pending broadcast now; // otherwise we'll wait for the cold start to circle back around queue.makeActiveNextPending(); if (processWarm) { queue.traceProcessRunningBegin(); - notifyStartedRunning(queue); scheduleReceiverWarmLocked(queue); } else { queue.traceProcessStartingBegin(); scheduleReceiverColdLocked(queue); } - // Only kick off an OOM adjustment pass if needed - updateOomAdj |= queue.runningOomAdjusted; - // Move to considering next runnable queue queue = nextQueue; } @@ -464,9 +459,13 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // now; dispatch its next broadcast and clear the slot mRunningColdStart = null; + // Now that we're running warm, we can finally request that OOM + // adjust we've been waiting for + notifyStartedRunning(queue); + mService.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_START_RECEIVER); + queue.traceProcessEnd(); queue.traceProcessRunningBegin(); - notifyStartedRunning(queue); scheduleReceiverWarmLocked(queue); // We might be willing to kick off another cold start @@ -509,7 +508,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { if (queue != null) { // If queue was running a broadcast, fail it if (queue.isActive()) { - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE); + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE, + "onApplicationCleanupLocked"); } // Skip any pending registered receivers, since the old process @@ -661,7 +661,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // Ignore registered receivers from a previous PID if (receiver instanceof BroadcastFilter) { mRunningColdStart = null; - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED); + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED, + "BroadcastFilter for cold app"); return; } @@ -683,7 +684,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { hostingRecord, zygotePolicyFlags, allowWhileBooting, false); if (queue.app == null) { mRunningColdStart = null; - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE); + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE, + "startProcessLocked failed"); return; } } @@ -714,33 +716,37 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // If someone already finished this broadcast, finish immediately final int oldDeliveryState = getDeliveryState(r, index); if (isDeliveryStateTerminal(oldDeliveryState)) { - finishReceiverLocked(queue, oldDeliveryState); + finishReceiverLocked(queue, oldDeliveryState, "already terminal state"); return; } // Consider additional cases where we'd want to finish immediately if (app.isInFullBackup()) { - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED); + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED, "isInFullBackup"); return; } if (mSkipPolicy.shouldSkip(r, receiver)) { - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED); + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED, "mSkipPolicy"); return; } final Intent receiverIntent = r.getReceiverIntent(receiver); if (receiverIntent == null) { - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED); + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED, "isInFullBackup"); return; } // Ignore registered receivers from a previous PID if ((receiver instanceof BroadcastFilter) && ((BroadcastFilter) receiver).receiverList.pid != app.getPid()) { - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED); + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED, + "BroadcastFilter for mismatched PID"); return; } - if (mService.mProcessesReady && !r.timeoutExempt) { + // Skip ANR tracking early during boot, when requested, or when we + // immediately assume delivery success + final boolean assumeDelivered = (receiver instanceof BroadcastFilter) && !r.ordered; + if (mService.mProcessesReady && !r.timeoutExempt && !assumeDelivered) { queue.lastCpuDelayTime = queue.app.getCpuDelayTime(); final long timeout = r.isForeground() ? mFgConstants.TIMEOUT : mBgConstants.TIMEOUT; @@ -768,7 +774,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } if (DEBUG_BROADCAST) logv("Scheduling " + r + " to warm " + app); - setDeliveryState(queue, app, r, index, receiver, BroadcastRecord.DELIVERY_SCHEDULED); + setDeliveryState(queue, app, r, index, receiver, BroadcastRecord.DELIVERY_SCHEDULED, + "scheduleReceiverWarmLocked"); final IApplicationThread thread = app.getOnewayThread(); if (thread != null) { @@ -782,8 +789,9 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // TODO: consider making registered receivers of unordered // broadcasts report results to detect ANRs - if (!r.ordered) { - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED); + if (assumeDelivered) { + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED, + "assuming delivered"); } } else { notifyScheduleReceiver(app, r, (ResolveInfo) receiver); @@ -797,10 +805,11 @@ class BroadcastQueueModernImpl extends BroadcastQueue { logw(msg); app.scheduleCrashLocked(msg, CannotDeliverBroadcastException.TYPE_ID, null); app.setKilled(true); - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE); + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE, "remote app"); } } else { - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE); + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE, + "missing IApplicationThread"); } } @@ -809,9 +818,9 @@ class BroadcastQueueModernImpl extends BroadcastQueue { * ordered broadcast; assumes the sender is still a warm process. */ private void scheduleResultTo(@NonNull BroadcastRecord r) { - if ((r.resultToApp == null) || (r.resultTo == null)) return; + if (r.resultTo == null) return; final ProcessRecord app = r.resultToApp; - final IApplicationThread thread = app.getOnewayThread(); + final IApplicationThread thread = (app != null) ? app.getOnewayThread() : null; if (thread != null) { mService.mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily( app, OOM_ADJ_REASON_FINISH_RECEIVER); @@ -844,7 +853,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } private void deliveryTimeoutHardLocked(@NonNull BroadcastProcessQueue queue) { - finishReceiverLocked(queue, BroadcastRecord.DELIVERY_TIMEOUT); + finishReceiverLocked(queue, BroadcastRecord.DELIVERY_TIMEOUT, + "deliveryTimeoutHardLocked"); } @Override @@ -871,16 +881,16 @@ class BroadcastQueueModernImpl extends BroadcastQueue { if (r.resultAbort) { for (int i = r.terminalCount + 1; i < r.receivers.size(); i++) { setDeliveryState(null, null, r, i, r.receivers.get(i), - BroadcastRecord.DELIVERY_SKIPPED); + BroadcastRecord.DELIVERY_SKIPPED, "resultAbort"); } } } - return finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED); + return finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED, "remote app"); } private boolean finishReceiverLocked(@NonNull BroadcastProcessQueue queue, - @DeliveryState int deliveryState) { + @DeliveryState int deliveryState, @NonNull String reason) { checkState(queue.isActive(), "isActive"); final ProcessRecord app = queue.app; @@ -888,7 +898,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { final int index = queue.getActiveIndex(); final Object receiver = r.receivers.get(index); - setDeliveryState(queue, app, r, index, receiver, deliveryState); + setDeliveryState(queue, app, r, index, receiver, deliveryState, reason); if (deliveryState == BroadcastRecord.DELIVERY_TIMEOUT) { r.anrCount++; @@ -935,7 +945,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { */ private void setDeliveryState(@Nullable BroadcastProcessQueue queue, @Nullable ProcessRecord app, @NonNull BroadcastRecord r, int index, - @NonNull Object receiver, @DeliveryState int newDeliveryState) { + @NonNull Object receiver, @DeliveryState int newDeliveryState, String reason) { final int oldDeliveryState = getDeliveryState(r, index); // Only apply state when we haven't already reached a terminal state; @@ -963,7 +973,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { logw("Delivery state of " + r + " to " + receiver + " via " + app + " changed from " + deliveryStateToString(oldDeliveryState) + " to " - + deliveryStateToString(newDeliveryState)); + + deliveryStateToString(newDeliveryState) + " because " + reason); } r.terminalCount++; @@ -1053,7 +1063,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { * of it matching a predicate. */ private final BroadcastConsumer mBroadcastConsumerSkip = (r, i) -> { - setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_SKIPPED); + setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_SKIPPED, + "mBroadcastConsumerSkip"); }; /** @@ -1061,7 +1072,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { * cancelled, usually as a result of it matching a predicate. */ private final BroadcastConsumer mBroadcastConsumerSkipAndCanceled = (r, i) -> { - setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_SKIPPED); + setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_SKIPPED, + "mBroadcastConsumerSkipAndCanceled"); r.resultCode = Activity.RESULT_CANCELED; r.resultData = null; r.resultExtras = null; diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java index 60fddf0c7f22..481ab17b609e 100644 --- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java +++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java @@ -21,6 +21,7 @@ import static com.android.server.am.ActivityManagerService.checkComponentPermiss import static com.android.server.am.BroadcastQueue.TAG; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.AppGlobals; import android.app.AppOpsManager; @@ -42,6 +43,8 @@ import android.util.Slog; import com.android.internal.util.ArrayUtils; +import java.util.Objects; + /** * Policy logic that decides if delivery of a particular {@link BroadcastRecord} * should be skipped for a given {@link ResolveInfo} or {@link BroadcastFilter}. @@ -51,8 +54,8 @@ import com.android.internal.util.ArrayUtils; public class BroadcastSkipPolicy { private final ActivityManagerService mService; - public BroadcastSkipPolicy(ActivityManagerService service) { - mService = service; + public BroadcastSkipPolicy(@NonNull ActivityManagerService service) { + mService = Objects.requireNonNull(service); } /** @@ -60,18 +63,39 @@ public class BroadcastSkipPolicy { * the given {@link BroadcastFilter} or {@link ResolveInfo}. */ public boolean shouldSkip(@NonNull BroadcastRecord r, @NonNull Object target) { + final String msg = shouldSkipMessage(r, target); + if (msg != null) { + Slog.w(TAG, msg); + return true; + } else { + return false; + } + } + + /** + * Determine if the given {@link BroadcastRecord} is eligible to be sent to + * the given {@link BroadcastFilter} or {@link ResolveInfo}. + * + * @return message indicating why the argument should be skipped, otherwise + * {@code null} if it can proceed. + */ + public @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, @NonNull Object target) { if (target instanceof BroadcastFilter) { - return shouldSkip(r, (BroadcastFilter) target); + return shouldSkipMessage(r, (BroadcastFilter) target); } else { - return shouldSkip(r, (ResolveInfo) target); + return shouldSkipMessage(r, (ResolveInfo) target); } } /** * Determine if the given {@link BroadcastRecord} is eligible to be sent to * the given {@link ResolveInfo}. + * + * @return message indicating why the argument should be skipped, otherwise + * {@code null} if it can proceed. */ - public boolean shouldSkip(@NonNull BroadcastRecord r, @NonNull ResolveInfo info) { + private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, + @NonNull ResolveInfo info) { final BroadcastOptions brOptions = r.options; final ComponentName component = new ComponentName( info.activityInfo.applicationInfo.packageName, @@ -82,58 +106,52 @@ public class BroadcastSkipPolicy { < brOptions.getMinManifestReceiverApiLevel() || info.activityInfo.applicationInfo.targetSdkVersion > brOptions.getMaxManifestReceiverApiLevel())) { - Slog.w(TAG, "Target SDK mismatch: receiver " + info.activityInfo + return "Target SDK mismatch: receiver " + info.activityInfo + " targets " + info.activityInfo.applicationInfo.targetSdkVersion + " but delivery restricted to [" + brOptions.getMinManifestReceiverApiLevel() + ", " + brOptions.getMaxManifestReceiverApiLevel() - + "] broadcasting " + broadcastDescription(r, component)); - return true; + + "] broadcasting " + broadcastDescription(r, component); } if (brOptions != null && !brOptions.testRequireCompatChange(info.activityInfo.applicationInfo.uid)) { - Slog.w(TAG, "Compat change filtered: broadcasting " + broadcastDescription(r, component) + return "Compat change filtered: broadcasting " + broadcastDescription(r, component) + " to uid " + info.activityInfo.applicationInfo.uid + " due to compat change " - + r.options.getRequireCompatChangeId()); - return true; + + r.options.getRequireCompatChangeId(); } if (!mService.validateAssociationAllowedLocked(r.callerPackage, r.callingUid, component.getPackageName(), info.activityInfo.applicationInfo.uid)) { - Slog.w(TAG, "Association not allowed: broadcasting " - + broadcastDescription(r, component)); - return true; + return "Association not allowed: broadcasting " + + broadcastDescription(r, component); } if (!mService.mIntentFirewall.checkBroadcast(r.intent, r.callingUid, r.callingPid, r.resolvedType, info.activityInfo.applicationInfo.uid)) { - Slog.w(TAG, "Firewall blocked: broadcasting " - + broadcastDescription(r, component)); - return true; + return "Firewall blocked: broadcasting " + + broadcastDescription(r, component); } int perm = checkComponentPermission(info.activityInfo.permission, r.callingPid, r.callingUid, info.activityInfo.applicationInfo.uid, info.activityInfo.exported); if (perm != PackageManager.PERMISSION_GRANTED) { if (!info.activityInfo.exported) { - Slog.w(TAG, "Permission Denial: broadcasting " + return "Permission Denial: broadcasting " + broadcastDescription(r, component) - + " is not exported from uid " + info.activityInfo.applicationInfo.uid); + + " is not exported from uid " + info.activityInfo.applicationInfo.uid; } else { - Slog.w(TAG, "Permission Denial: broadcasting " + return "Permission Denial: broadcasting " + broadcastDescription(r, component) - + " requires " + info.activityInfo.permission); + + " requires " + info.activityInfo.permission; } - return true; } else if (info.activityInfo.permission != null) { final int opCode = AppOpsManager.permissionToOpCode(info.activityInfo.permission); if (opCode != AppOpsManager.OP_NONE && mService.getAppOpsManager().noteOpNoThrow(opCode, r.callingUid, r.callerPackage, r.callerFeatureId, "Broadcast delivered to " + info.activityInfo.name) != AppOpsManager.MODE_ALLOWED) { - Slog.w(TAG, "Appop Denial: broadcasting " + return "Appop Denial: broadcasting " + broadcastDescription(r, component) + " requires appop " + AppOpsManager.permissionToOp( - info.activityInfo.permission)); - return true; + info.activityInfo.permission); } } @@ -142,38 +160,34 @@ public class BroadcastSkipPolicy { android.Manifest.permission.INTERACT_ACROSS_USERS, info.activityInfo.applicationInfo.uid) != PackageManager.PERMISSION_GRANTED) { - Slog.w(TAG, "Permission Denial: Receiver " + component.flattenToShortString() + return "Permission Denial: Receiver " + component.flattenToShortString() + " requests FLAG_SINGLE_USER, but app does not hold " - + android.Manifest.permission.INTERACT_ACROSS_USERS); - return true; + + android.Manifest.permission.INTERACT_ACROSS_USERS; } } if (info.activityInfo.applicationInfo.isInstantApp() && r.callingUid != info.activityInfo.applicationInfo.uid) { - Slog.w(TAG, "Instant App Denial: receiving " + return "Instant App Denial: receiving " + r.intent + " to " + component.flattenToShortString() + " due to sender " + r.callerPackage + " (uid " + r.callingUid + ")" - + " Instant Apps do not support manifest receivers"); - return true; + + " Instant Apps do not support manifest receivers"; } if (r.callerInstantApp && (info.activityInfo.flags & ActivityInfo.FLAG_VISIBLE_TO_INSTANT_APP) == 0 && r.callingUid != info.activityInfo.applicationInfo.uid) { - Slog.w(TAG, "Instant App Denial: receiving " + return "Instant App Denial: receiving " + r.intent + " to " + component.flattenToShortString() + " requires receiver have visibleToInstantApps set" + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } if (r.curApp != null && r.curApp.mErrorState.isCrashing()) { // If the target process is crashing, just skip it. - Slog.w(TAG, "Skipping deliver ordered [" + r.queue.toString() + "] " + r - + " to " + r.curApp + ": process crashing"); - return true; + return "Skipping deliver ordered [" + r.queue.toString() + "] " + r + + " to " + r.curApp + ": process crashing"; } boolean isAvailable = false; @@ -183,15 +197,13 @@ public class BroadcastSkipPolicy { UserHandle.getUserId(info.activityInfo.applicationInfo.uid)); } catch (Exception e) { // all such failures mean we skip this receiver - Slog.w(TAG, "Exception getting recipient info for " - + info.activityInfo.packageName, e); + return "Exception getting recipient info for " + + info.activityInfo.packageName; } if (!isAvailable) { - Slog.w(TAG, - "Skipping delivery to " + info.activityInfo.packageName + " / " + return "Skipping delivery to " + info.activityInfo.packageName + " / " + info.activityInfo.applicationInfo.uid - + " : package no longer available"); - return true; + + " : package no longer available"; } // If permissions need a review before any of the app components can run, we drop @@ -201,10 +213,8 @@ public class BroadcastSkipPolicy { if (!requestStartTargetPermissionsReviewIfNeededLocked(r, info.activityInfo.packageName, UserHandle.getUserId( info.activityInfo.applicationInfo.uid))) { - Slog.w(TAG, - "Skipping delivery: permission review required for " - + broadcastDescription(r, component)); - return true; + return "Skipping delivery: permission review required for " + + broadcastDescription(r, component); } final int allowed = mService.getAppStartModeLOSP( @@ -216,10 +226,9 @@ public class BroadcastSkipPolicy { // to it and the app is in a state that should not receive it // (depending on how getAppStartModeLOSP has determined that). if (allowed == ActivityManager.APP_START_MODE_DISABLED) { - Slog.w(TAG, "Background execution disabled: receiving " + return "Background execution disabled: receiving " + r.intent + " to " - + component.flattenToShortString()); - return true; + + component.flattenToShortString(); } else if (((r.intent.getFlags()&Intent.FLAG_RECEIVER_EXCLUDE_BACKGROUND) != 0) || (r.intent.getComponent() == null && r.intent.getPackage() == null @@ -228,10 +237,9 @@ public class BroadcastSkipPolicy { && !isSignaturePerm(r.requiredPermissions))) { mService.addBackgroundCheckViolationLocked(r.intent.getAction(), component.getPackageName()); - Slog.w(TAG, "Background execution not allowed: receiving " + return "Background execution not allowed: receiving " + r.intent + " to " - + component.flattenToShortString()); - return true; + + component.flattenToShortString(); } } @@ -239,10 +247,8 @@ public class BroadcastSkipPolicy { && !mService.mUserController .isUserRunning(UserHandle.getUserId(info.activityInfo.applicationInfo.uid), 0 /* flags */)) { - Slog.w(TAG, - "Skipping delivery to " + info.activityInfo.packageName + " / " - + info.activityInfo.applicationInfo.uid + " : user is not running"); - return true; + return "Skipping delivery to " + info.activityInfo.packageName + " / " + + info.activityInfo.applicationInfo.uid + " : user is not running"; } if (r.excludedPermissions != null && r.excludedPermissions.length > 0) { @@ -268,13 +274,15 @@ public class BroadcastSkipPolicy { info.activityInfo.applicationInfo.uid, info.activityInfo.packageName) == AppOpsManager.MODE_ALLOWED)) { - return true; + return "Skipping delivery to " + info.activityInfo.packageName + + " due to excluded permission " + excludedPermission; } } else { // When there is no app op associated with the permission, // skip when permission is granted. if (perm == PackageManager.PERMISSION_GRANTED) { - return true; + return "Skipping delivery to " + info.activityInfo.packageName + + " due to excluded permission " + excludedPermission; } } } @@ -283,13 +291,12 @@ public class BroadcastSkipPolicy { // Check that the receiver does *not* belong to any of the excluded packages if (r.excludedPackages != null && r.excludedPackages.length > 0) { if (ArrayUtils.contains(r.excludedPackages, component.getPackageName())) { - Slog.w(TAG, "Skipping delivery of excluded package " + return "Skipping delivery of excluded package " + r.intent + " to " + component.flattenToShortString() + " excludes package " + component.getPackageName() + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } } @@ -307,95 +314,94 @@ public class BroadcastSkipPolicy { perm = PackageManager.PERMISSION_DENIED; } if (perm != PackageManager.PERMISSION_GRANTED) { - Slog.w(TAG, "Permission Denial: receiving " + return "Permission Denial: receiving " + r.intent + " to " + component.flattenToShortString() + " requires " + requiredPermission + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } int appOp = AppOpsManager.permissionToOpCode(requiredPermission); if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp) { if (!noteOpForManifestReceiver(appOp, r, info, component)) { - return true; + return "Skipping delivery to " + info.activityInfo.packageName + + " due to required appop " + appOp; } } } } if (r.appOp != AppOpsManager.OP_NONE) { if (!noteOpForManifestReceiver(r.appOp, r, info, component)) { - return true; + return "Skipping delivery to " + info.activityInfo.packageName + + " due to required appop " + r.appOp; } } - return false; + return null; } /** * Determine if the given {@link BroadcastRecord} is eligible to be sent to * the given {@link BroadcastFilter}. + * + * @return message indicating why the argument should be skipped, otherwise + * {@code null} if it can proceed. */ - public boolean shouldSkip(@NonNull BroadcastRecord r, @NonNull BroadcastFilter filter) { + private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, + @NonNull BroadcastFilter filter) { if (r.options != null && !r.options.testRequireCompatChange(filter.owningUid)) { - Slog.w(TAG, "Compat change filtered: broadcasting " + r.intent.toString() + return "Compat change filtered: broadcasting " + r.intent.toString() + " to uid " + filter.owningUid + " due to compat change " - + r.options.getRequireCompatChangeId()); - return true; + + r.options.getRequireCompatChangeId(); } if (!mService.validateAssociationAllowedLocked(r.callerPackage, r.callingUid, filter.packageName, filter.owningUid)) { - Slog.w(TAG, "Association not allowed: broadcasting " + return "Association not allowed: broadcasting " + r.intent.toString() + " from " + r.callerPackage + " (pid=" + r.callingPid + ", uid=" + r.callingUid + ") to " + filter.packageName + " through " - + filter); - return true; + + filter; } if (!mService.mIntentFirewall.checkBroadcast(r.intent, r.callingUid, r.callingPid, r.resolvedType, filter.receiverList.uid)) { - Slog.w(TAG, "Firewall blocked: broadcasting " + return "Firewall blocked: broadcasting " + r.intent.toString() + " from " + r.callerPackage + " (pid=" + r.callingPid + ", uid=" + r.callingUid + ") to " + filter.packageName + " through " - + filter); - return true; + + filter; } // Check that the sender has permission to send to this receiver if (filter.requiredPermission != null) { int perm = checkComponentPermission(filter.requiredPermission, r.callingPid, r.callingUid, -1, true); if (perm != PackageManager.PERMISSION_GRANTED) { - Slog.w(TAG, "Permission Denial: broadcasting " + return "Permission Denial: broadcasting " + r.intent.toString() + " from " + r.callerPackage + " (pid=" + r.callingPid + ", uid=" + r.callingUid + ")" + " requires " + filter.requiredPermission - + " due to registered receiver " + filter); - return true; + + " due to registered receiver " + filter; } else { final int opCode = AppOpsManager.permissionToOpCode(filter.requiredPermission); if (opCode != AppOpsManager.OP_NONE && mService.getAppOpsManager().noteOpNoThrow(opCode, r.callingUid, r.callerPackage, r.callerFeatureId, "Broadcast sent to protected receiver") != AppOpsManager.MODE_ALLOWED) { - Slog.w(TAG, "Appop Denial: broadcasting " + return "Appop Denial: broadcasting " + r.intent.toString() + " from " + r.callerPackage + " (pid=" + r.callingPid + ", uid=" + r.callingUid + ")" + " requires appop " + AppOpsManager.permissionToOp( filter.requiredPermission) - + " due to registered receiver " + filter); - return true; + + " due to registered receiver " + filter; } } } if ((filter.receiverList.app == null || filter.receiverList.app.isKilled() || filter.receiverList.app.mErrorState.isCrashing())) { - Slog.w(TAG, "Skipping deliver [" + r.queue.toString() + "] " + r - + " to " + filter.receiverList + ": process gone or crashing"); - return true; + return "Skipping deliver [" + r.queue.toString() + "] " + r + + " to " + filter.receiverList + ": process gone or crashing"; } // Ensure that broadcasts are only sent to other Instant Apps if they are marked as @@ -405,28 +411,26 @@ public class BroadcastSkipPolicy { if (!visibleToInstantApps && filter.instantApp && filter.receiverList.uid != r.callingUid) { - Slog.w(TAG, "Instant App Denial: receiving " + return "Instant App Denial: receiving " + r.intent.toString() + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid + ", uid=" + filter.receiverList.uid + ")" + " due to sender " + r.callerPackage + " (uid " + r.callingUid + ")" - + " not specifying FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS"); - return true; + + " not specifying FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS"; } if (!filter.visibleToInstantApp && r.callerInstantApp && filter.receiverList.uid != r.callingUid) { - Slog.w(TAG, "Instant App Denial: receiving " + return "Instant App Denial: receiving " + r.intent.toString() + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid + ", uid=" + filter.receiverList.uid + ")" + " requires receiver be visible to instant apps" + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } // Check that the receiver has the required permission(s) to receive this broadcast. @@ -436,15 +440,14 @@ public class BroadcastSkipPolicy { int perm = checkComponentPermission(requiredPermission, filter.receiverList.pid, filter.receiverList.uid, -1, true); if (perm != PackageManager.PERMISSION_GRANTED) { - Slog.w(TAG, "Permission Denial: receiving " + return "Permission Denial: receiving " + r.intent.toString() + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid + ", uid=" + filter.receiverList.uid + ")" + " requires " + requiredPermission + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } int appOp = AppOpsManager.permissionToOpCode(requiredPermission); if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp @@ -452,7 +455,7 @@ public class BroadcastSkipPolicy { filter.receiverList.uid, filter.packageName, filter.featureId, "Broadcast delivered to registered receiver " + filter.receiverId) != AppOpsManager.MODE_ALLOWED) { - Slog.w(TAG, "Appop Denial: receiving " + return "Appop Denial: receiving " + r.intent.toString() + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid @@ -460,8 +463,7 @@ public class BroadcastSkipPolicy { + " requires appop " + AppOpsManager.permissionToOp( requiredPermission) + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } } } @@ -469,14 +471,13 @@ public class BroadcastSkipPolicy { int perm = checkComponentPermission(null, filter.receiverList.pid, filter.receiverList.uid, -1, true); if (perm != PackageManager.PERMISSION_GRANTED) { - Slog.w(TAG, "Permission Denial: security check failed when receiving " + return "Permission Denial: security check failed when receiving " + r.intent.toString() + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid + ", uid=" + filter.receiverList.uid + ")" + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } } // Check that the receiver does *not* have any excluded permissions @@ -496,7 +497,7 @@ public class BroadcastSkipPolicy { filter.receiverList.uid, filter.packageName) == AppOpsManager.MODE_ALLOWED)) { - Slog.w(TAG, "Appop Denial: receiving " + return "Appop Denial: receiving " + r.intent.toString() + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid @@ -504,22 +505,20 @@ public class BroadcastSkipPolicy { + " excludes appop " + AppOpsManager.permissionToOp( excludedPermission) + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } } else { // When there is no app op associated with the permission, // skip when permission is granted. if (perm == PackageManager.PERMISSION_GRANTED) { - Slog.w(TAG, "Permission Denial: receiving " + return "Permission Denial: receiving " + r.intent.toString() + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid + ", uid=" + filter.receiverList.uid + ")" + " excludes " + excludedPermission + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } } } @@ -528,15 +527,14 @@ public class BroadcastSkipPolicy { // Check that the receiver does *not* belong to any of the excluded packages if (r.excludedPackages != null && r.excludedPackages.length > 0) { if (ArrayUtils.contains(r.excludedPackages, filter.packageName)) { - Slog.w(TAG, "Skipping delivery of excluded package " + return "Skipping delivery of excluded package " + r.intent.toString() + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid + ", uid=" + filter.receiverList.uid + ")" + " excludes package " + filter.packageName + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } } @@ -546,15 +544,14 @@ public class BroadcastSkipPolicy { filter.receiverList.uid, filter.packageName, filter.featureId, "Broadcast delivered to registered receiver " + filter.receiverId) != AppOpsManager.MODE_ALLOWED) { - Slog.w(TAG, "Appop Denial: receiving " + return "Appop Denial: receiving " + r.intent.toString() + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid + ", uid=" + filter.receiverList.uid + ")" + " requires appop " + AppOpsManager.opToName(r.appOp) + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - return true; + + " (uid " + r.callingUid + ")"; } // Ensure that broadcasts are only sent to other apps if they are explicitly marked as @@ -562,15 +559,14 @@ public class BroadcastSkipPolicy { if (!filter.exported && checkComponentPermission(null, r.callingPid, r.callingUid, filter.receiverList.uid, filter.exported) != PackageManager.PERMISSION_GRANTED) { - Slog.w(TAG, "Exported Denial: sending " + return "Exported Denial: sending " + r.intent.toString() + ", action: " + r.intent.getAction() + " from " + r.callerPackage + " (uid=" + r.callingUid + ")" + " due to receiver " + filter.receiverList.app + " (uid " + filter.receiverList.uid + ")" - + " not specifying RECEIVER_EXPORTED"); - return true; + + " not specifying RECEIVER_EXPORTED"; } // If permissions need a review before any of the app components can run, we drop @@ -579,10 +575,10 @@ public class BroadcastSkipPolicy { // broadcast. if (!requestStartTargetPermissionsReviewIfNeededLocked(r, filter.packageName, filter.owningUserId)) { - return true; + return "Skipping delivery to " + filter.packageName + " due to permissions review"; } - return false; + return null; } private static String broadcastDescription(BroadcastRecord r, ComponentName component) { diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index 740efbc658ba..14a169737b38 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -490,6 +490,11 @@ public final class PendingIntentRecord extends IIntentSender.Stub { final IApplicationThread finishedReceiverThread = caller; boolean sendFinish = finishedReceiver != null; + if ((finishedReceiver != null) && (finishedReceiverThread == null)) { + Slog.w(TAG, "Sending of " + intent + " from " + Binder.getCallingUid() + + " requested resultTo without an IApplicationThread!", new Throwable()); + } + int userId = key.userId; if (userId == UserHandle.USER_CURRENT) { userId = controller.mUserController.getCurrentOrTargetUserId(); 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/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java index 4c21195e2890..2e67bf2fcdf7 100644 --- a/services/core/java/com/android/server/pm/AppsFilterImpl.java +++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java @@ -22,6 +22,15 @@ import static android.os.UserHandle.USER_NULL; import static android.provider.DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE; import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; +import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED; +import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED__EVENT_TYPE__BOOT; +import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED__EVENT_TYPE__USER_CREATED; +import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED__EVENT_TYPE__USER_DELETED; +import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED; +import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__COMPAT_CHANGED; +import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_ADDED; +import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_DELETED; +import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_REPLACED; import static com.android.server.pm.AppsFilterUtils.canQueryAsInstaller; import static com.android.server.pm.AppsFilterUtils.canQueryViaComponents; import static com.android.server.pm.AppsFilterUtils.canQueryViaPackage; @@ -36,6 +45,7 @@ import android.content.pm.PackageManagerInternal; import android.content.pm.SigningDetails; import android.content.pm.UserInfo; import android.os.Handler; +import android.os.SystemClock; import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; @@ -49,6 +59,7 @@ import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.FrameworkStatsLog; import com.android.server.FgThread; import com.android.server.compat.CompatChange; import com.android.server.om.OverlayReferenceMapper; @@ -351,8 +362,15 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, if (pkg == null) { return; } + final long currentTimeUs = SystemClock.currentTimeMicro(); updateEnabledState(pkg); mAppsFilter.updateShouldFilterCacheForPackage(snapshot, packageName); + mAppsFilter.logCacheUpdated( + PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__COMPAT_CHANGED, + SystemClock.currentTimeMicro() - currentTimeUs, + snapshot.getUserInfos().length, + snapshot.getPackageStates().size(), + pkg.getUid()); } private void updateEnabledState(@NonNull AndroidPackage pkg) { @@ -465,7 +483,8 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, mOverlayReferenceMapper.rebuildIfDeferred(); mFeatureConfig.onSystemReady(); - updateEntireShouldFilterCacheAsync(pmInternal); + updateEntireShouldFilterCacheAsync(pmInternal, + PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED__EVENT_TYPE__BOOT); } /** @@ -473,16 +492,23 @@ 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 + : PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_ADDED; if (DEBUG_TRACING) { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "filter.addPackage"); } try { if (isReplace) { // let's first remove any prior rules for this package - removePackage(snapshot, newPkgSetting, true /*isReplace*/); + removePackageInternal(snapshot, newPkgSetting, + true /*isReplace*/, retainImplicitGrantOnReplace); } final ArrayMap<String, ? extends PackageStateInternal> settings = snapshot.getPackageStates(); @@ -508,6 +534,8 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, } } } + logCacheUpdated(logType, SystemClock.currentTimeMicro() - currentTimeUs, + users.length, settings.size(), newPkgSetting.getAppId()); } else { invalidateCache("addPackage: " + newPkgSetting.getPackageName()); } @@ -757,18 +785,19 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, } } - private void updateEntireShouldFilterCacheAsync(PackageManagerInternal pmInternal) { - updateEntireShouldFilterCacheAsync(pmInternal, CACHE_REBUILD_DELAY_MIN_MS); + private void updateEntireShouldFilterCacheAsync(PackageManagerInternal pmInternal, int reason) { + updateEntireShouldFilterCacheAsync(pmInternal, CACHE_REBUILD_DELAY_MIN_MS, reason); } private void updateEntireShouldFilterCacheAsync(PackageManagerInternal pmInternal, - long delayMs) { + long delayMs, int reason) { mBackgroundHandler.postDelayed(() -> { if (!mCacheValid.compareAndSet(CACHE_INVALID, CACHE_VALID)) { // Cache is already valid. return; } + final long currentTimeUs = SystemClock.currentTimeMicro(); final ArrayMap<String, AndroidPackage> packagesCache = new ArrayMap<>(); final UserInfo[][] usersRef = new UserInfo[1][]; final Computer snapshot = (Computer) pmInternal.snapshot(); @@ -787,11 +816,13 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, updateEntireShouldFilterCacheInner(snapshot, settings, usersRef[0], USER_ALL); onChanged(); + logCacheRebuilt(reason, SystemClock.currentTimeMicro() - currentTimeUs, + users.length, settings.size()); if (!mCacheValid.compareAndSet(CACHE_VALID, CACHE_VALID)) { Slog.i(TAG, "Cache invalidated while building, retrying."); updateEntireShouldFilterCacheAsync(pmInternal, - Math.min(delayMs * 2, CACHE_REBUILD_DELAY_MAX_MS)); + Math.min(delayMs * 2, CACHE_REBUILD_DELAY_MAX_MS), reason); return; } @@ -803,15 +834,27 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, if (!mCacheReady) { return; } + final long currentTimeUs = SystemClock.currentTimeMicro(); updateEntireShouldFilterCache(snapshot, newUserId); + logCacheRebuilt( + PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED__EVENT_TYPE__USER_CREATED, + SystemClock.currentTimeMicro() - currentTimeUs, + snapshot.getUserInfos().length, + snapshot.getPackageStates().size()); } - public void onUserDeleted(@UserIdInt int userId) { + public void onUserDeleted(Computer snapshot, @UserIdInt int userId) { if (!mCacheReady) { return; } + final long currentTimeUs = SystemClock.currentTimeMicro(); removeShouldFilterCacheForUser(userId); onChanged(); + logCacheRebuilt( + PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED__EVENT_TYPE__USER_DELETED, + SystemClock.currentTimeMicro() - currentTimeUs, + snapshot.getUserInfos().length, + snapshot.getPackageStates().size()); } private void updateShouldFilterCacheForPackage(Computer snapshot, @@ -976,13 +1019,31 @@ 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 */); + } + + /** + * Removes a package for consideration when filtering visibility between apps. + * + * @param setting the setting of the package being removed. + */ + public void removePackage(Computer snapshot, PackageStateInternal setting) { + final long currentTimeUs = SystemClock.currentTimeMicro(); + removePackageInternal(snapshot, setting, + false /* isReplace */, false /* retainImplicitGrantOnReplace */); + logCacheUpdated( + PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_DELETED, + SystemClock.currentTimeMicro() - currentTimeUs, + snapshot.getUserInfos().length, + snapshot.getPackageStates().size(), + setting.getAppId()); } /** @@ -990,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. */ - public void removePackage(Computer snapshot, PackageStateInternal setting, - boolean isReplace) { + private void removePackageInternal(Computer snapshot, PackageStateInternal setting, + 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); + } } } } @@ -1174,4 +1239,18 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, } } } + + private void logCacheRebuilt(int eventId, long latency, int userCount, int packageCount) { + FrameworkStatsLog.write(PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED, + eventId, latency, userCount, packageCount, mShouldFilterCache.size()); + } + + private void logCacheUpdated(int eventId, long latency, int userCount, int packageCount, + int appId) { + if (!mCacheReady) { + return; + } + FrameworkStatsLog.write(PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED, + eventId, appId, latency, userCount, packageCount, mShouldFilterCache.size()); + } } diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptService.java b/services/core/java/com/android/server/pm/BackgroundDexOptService.java index e7412c5e52ec..d856d54cb4e0 100644 --- a/services/core/java/com/android/server/pm/BackgroundDexOptService.java +++ b/services/core/java/com/android/server/pm/BackgroundDexOptService.java @@ -410,7 +410,7 @@ public final class BackgroundDexOptService { job.jobFinished(params, !completed); } else { // Periodic job - job.jobFinished(params, true); + job.jobFinished(params, false /* reschedule */); } markDexOptCompleted(); } 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/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index dfbe68a8e997..23cf26236e87 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -1147,7 +1147,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService var done = SystemClock.currentTimeMicro(); if (mSnapshotStatistics != null) { - mSnapshotStatistics.rebuild(now, done, hits); + mSnapshotStatistics.rebuild(now, done, hits, newSnapshot.getPackageStates().size()); } return newSnapshot; } @@ -4220,7 +4220,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService mSettings.removeUserLPw(userId); mPendingBroadcasts.remove(userId); mDeletePackageHelper.removeUnusedPackagesLPw(userManager, userId); - mAppsFilter.onUserDeleted(userId); + mAppsFilter.onUserDeleted(snapshotComputer(), userId); } mInstantAppRegistry.onUserRemoved(userId); } diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java index bbc4fdeb36bb..7e936735ef7d 100644 --- a/services/core/java/com/android/server/pm/RemovePackageHelper.java +++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java @@ -308,7 +308,7 @@ final class RemovePackageHelper { mPm.mSettings.getKeySetManagerService().removeAppKeySetDataLPw(packageName); final Computer snapshot = mPm.snapshotComputer(); mPm.mAppsFilter.removePackage(snapshot, - snapshot.getPackageStateInternal(packageName), false /* isReplace */); + snapshot.getPackageStateInternal(packageName)); removedAppId = mPm.mSettings.removePackageLPw(packageName); if (outInfo != null) { outInfo.mRemovedAppId = removedAppId; diff --git a/services/core/java/com/android/server/pm/SnapshotStatistics.java b/services/core/java/com/android/server/pm/SnapshotStatistics.java index 2cfc8946ad02..e04a1e5b3569 100644 --- a/services/core/java/com/android/server/pm/SnapshotStatistics.java +++ b/services/core/java/com/android/server/pm/SnapshotStatistics.java @@ -24,11 +24,13 @@ import android.os.SystemClock; import android.text.TextUtils; import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.FrameworkStatsLog; import com.android.server.EventLogTags; import java.io.PrintWriter; import java.util.Arrays; import java.util.Locale; +import java.util.concurrent.TimeUnit; /** * This class records statistics about PackageManagerService snapshots. It maintains two sets of @@ -59,9 +61,9 @@ public class SnapshotStatistics { public static final int SNAPSHOT_TICK_INTERVAL_MS = 60 * 1000; /** - * The number of ticks for long statistics. This is one week. + * The interval of the snapshot statistics logging. */ - public static final int SNAPSHOT_LONG_TICKS = 7 * 24 * 60; + private static final long SNAPSHOT_LOG_INTERVAL_US = TimeUnit.DAYS.toMicros(1); /** * The number snapshot event logs that can be generated in a single logging interval. @@ -93,6 +95,28 @@ public class SnapshotStatistics { public static final int SNAPSHOT_SHORT_LIFETIME = 5; /** + * Buckets to represent a range of the rebuild latency for the histogram of + * snapshot rebuild latency. + */ + private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_1_MILLIS = 1; + private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_2_MILLIS = 2; + private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_5_MILLIS = 5; + private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_10_MILLIS = 10; + private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_20_MILLIS = 20; + private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_50_MILLIS = 50; + private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_100_MILLIS = 100; + + /** + * Buckets to represent a range of the reuse count for the histogram of + * snapshot reuse counts. + */ + private static final int REUSE_COUNT_BUCKET_LESS_THAN_1 = 1; + private static final int REUSE_COUNT_BUCKET_LESS_THAN_10 = 10; + private static final int REUSE_COUNT_BUCKET_LESS_THAN_100 = 100; + private static final int REUSE_COUNT_BUCKET_LESS_THAN_1000 = 1000; + private static final int REUSE_COUNT_BUCKET_LESS_THAN_10000 = 10000; + + /** * The lock to control access to this object. */ private final Object mLock = new Object(); @@ -113,11 +137,6 @@ public class SnapshotStatistics { private int mEventsReported = 0; /** - * The tick counter. At the default tick interval, this wraps every 4000 years or so. - */ - private int mTicks = 0; - - /** * The handler used for the periodic ticks. */ private Handler mHandler = null; @@ -139,8 +158,6 @@ public class SnapshotStatistics { // The number of bins private int mCount; - // The mapping of low integers to bins - private int[] mBinMap; // The maximum mapped value. Values at or above this are mapped to the // top bin. private int mMaxBin; @@ -158,16 +175,6 @@ public class SnapshotStatistics { mCount = mUserKey.length + 1; // The maximum value is one more than the last one in the map. mMaxBin = mUserKey[mUserKey.length - 1] + 1; - mBinMap = new int[mMaxBin + 1]; - - int j = 0; - for (int i = 0; i < mUserKey.length; i++) { - while (j <= mUserKey[i]) { - mBinMap[j] = i; - j++; - } - } - mBinMap[mMaxBin] = mUserKey.length; } /** @@ -175,9 +182,14 @@ public class SnapshotStatistics { */ public int getBin(int x) { if (x >= 0 && x < mMaxBin) { - return mBinMap[x]; + for (int i = 0; i < mUserKey.length; i++) { + if (x <= mUserKey[i]) { + return i; + } + } + return 0; // should not happen } else if (x >= mMaxBin) { - return mBinMap[mMaxBin]; + return mUserKey.length; } else { // x is negative. The bin will not be used. return 0; @@ -263,6 +275,11 @@ public class SnapshotStatistics { public int mMaxBuildTimeUs = 0; /** + * The maximum used count since the last log. + */ + public int mMaxUsedCount = 0; + + /** * Record the rebuild. The parameters are the length of time it took to build the * latest snapshot, and the number of times the _previous_ snapshot was used. A * negative value for used signals an invalid value, which is the case the first @@ -279,7 +296,6 @@ public class SnapshotStatistics { } mTotalTimeUs += duration; - boolean reportIt = false; if (big) { mBigBuilds++; @@ -290,6 +306,9 @@ public class SnapshotStatistics { if (mMaxBuildTimeUs < duration) { mMaxBuildTimeUs = duration; } + if (mMaxUsedCount < used) { + mMaxUsedCount = used; + } } private Stats(long now) { @@ -313,6 +332,7 @@ public class SnapshotStatistics { mShortLived = orig.mShortLived; mTotalTimeUs = orig.mTotalTimeUs; mMaxBuildTimeUs = orig.mMaxBuildTimeUs; + mMaxUsedCount = orig.mMaxUsedCount; } /** @@ -443,18 +463,19 @@ public class SnapshotStatistics { } /** - * Report the object via an event. Presumably the record indicates an anomalous - * incident. + * Report the snapshot statistics to FrameworkStatsLog. */ - private void report() { - EventLogTags.writePmSnapshotStats( - mTotalBuilds, mTotalUsed, mBigBuilds, mShortLived, - mMaxBuildTimeUs / US_IN_MS, mTotalTimeUs / US_IN_MS); + private void logSnapshotStatistics(int packageCount) { + final long avgLatencyUs = (mTotalBuilds == 0 ? 0 : mTotalTimeUs / mTotalBuilds); + final int avgUsedCount = (mTotalBuilds == 0 ? 0 : mTotalUsed / mTotalBuilds); + FrameworkStatsLog.write( + FrameworkStatsLog.PACKAGE_MANAGER_SNAPSHOT_REPORTED, mTimes, mUsed, + mMaxBuildTimeUs, mMaxUsedCount, avgLatencyUs, avgUsedCount, packageCount); } } /** - * Long statistics. These roll over approximately every week. + * Long statistics. These roll over approximately one day. */ private Stats[] mLong; @@ -464,10 +485,14 @@ public class SnapshotStatistics { private Stats[] mShort; /** - * The time of the last build. This can be used to compute the length of time a - * snapshot existed before being replaced. + * The time of last logging to the FrameworkStatsLog. */ - private long mLastBuildTime = 0; + private long mLastLogTimeUs; + + /** + * The number of packages on the device. + */ + private int mPackageCount; /** * Create a snapshot object. Initialize the bin levels. The last bin catches @@ -475,8 +500,20 @@ public class SnapshotStatistics { */ public SnapshotStatistics() { // Create the bin thresholds. The time bins are in units of us. - mTimeBins = new BinMap(new int[] { 1, 2, 5, 10, 20, 50, 100 }); - mUseBins = new BinMap(new int[] { 1, 2, 5, 10, 20, 50, 100 }); + mTimeBins = new BinMap(new int[] { + REBUILD_LATENCY_BUCKET_LESS_THAN_1_MILLIS, + REBUILD_LATENCY_BUCKET_LESS_THAN_2_MILLIS, + REBUILD_LATENCY_BUCKET_LESS_THAN_5_MILLIS, + REBUILD_LATENCY_BUCKET_LESS_THAN_10_MILLIS, + REBUILD_LATENCY_BUCKET_LESS_THAN_20_MILLIS, + REBUILD_LATENCY_BUCKET_LESS_THAN_50_MILLIS, + REBUILD_LATENCY_BUCKET_LESS_THAN_100_MILLIS }); + mUseBins = new BinMap(new int[] { + REUSE_COUNT_BUCKET_LESS_THAN_1, + REUSE_COUNT_BUCKET_LESS_THAN_10, + REUSE_COUNT_BUCKET_LESS_THAN_100, + REUSE_COUNT_BUCKET_LESS_THAN_1000, + REUSE_COUNT_BUCKET_LESS_THAN_10000 }); // Create the raw statistics final long now = SystemClock.currentTimeMicro(); @@ -484,6 +521,7 @@ public class SnapshotStatistics { mLong[0] = new Stats(now); mShort = new Stats[10]; mShort[0] = new Stats(now); + mLastLogTimeUs = now; // Create the message handler for ticks and start the ticker. mHandler = new Handler(Looper.getMainLooper()) { @@ -516,13 +554,14 @@ public class SnapshotStatistics { * @param now The time at which the snapshot rebuild began, in ns. * @param done The time at which the snapshot rebuild completed, in ns. * @param hits The number of times the previous snapshot was used. + * @param packageCount The number of packages on the device. */ - public final void rebuild(long now, long done, int hits) { + public final void rebuild(long now, long done, int hits, int packageCount) { // The duration has a span of about 2000s final int duration = (int) (done - now); boolean reportEvent = false; synchronized (mLock) { - mLastBuildTime = now; + mPackageCount = packageCount; final int timeBin = mTimeBins.getBin(duration / 1000); final int useBin = mUseBins.getBin(hits); @@ -570,10 +609,12 @@ public class SnapshotStatistics { private void tick() { synchronized (mLock) { long now = SystemClock.currentTimeMicro(); - mTicks++; - if (mTicks % SNAPSHOT_LONG_TICKS == 0) { + if (now - mLastLogTimeUs > SNAPSHOT_LOG_INTERVAL_US) { shift(mLong, now); + mLastLogTimeUs = now; + mLong[mLong.length - 1].logSnapshotStatistics(mPackageCount); } + shift(mShort, now); mEventsReported = 0; } 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/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index ecc43f7b938b..b153a85a4048 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -1316,7 +1316,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mAppSwitchesState = APP_SWITCH_ALLOW; } } - return pir.sendInner(0, fillInIntent, resolvedType, allowlistToken, null, null, + return pir.sendInner(caller, 0, fillInIntent, resolvedType, allowlistToken, null, null, resultTo, resultWho, requestCode, flagsMask, flagsValues, bOptions); } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index ccfb9299b565..f30c4355306c 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -5709,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/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index e1a4c1dd7256..de5960363fa5 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -280,13 +280,13 @@ public class BroadcastQueueTest { constants.TIMEOUT = 100; constants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0; final BroadcastSkipPolicy emptySkipPolicy = new BroadcastSkipPolicy(mAms) { - public boolean shouldSkip(BroadcastRecord r, ResolveInfo info) { + public boolean shouldSkip(BroadcastRecord r, Object o) { // Ignored return false; } - public boolean shouldSkip(BroadcastRecord r, BroadcastFilter filter) { + public String shouldSkipMessage(BroadcastRecord r, Object o) { // Ignored - return false; + return null; } }; final BroadcastHistory emptyHistory = new BroadcastHistory(constants) { 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/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..bc7757b434ea 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 @@ -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 @@ -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) { @@ -4630,7 +4644,7 @@ public class QuotaControllerTest { } // 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) { @@ -4701,7 +4715,7 @@ public class QuotaControllerTest { } // 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. */ diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java index 47f449c7f897..1be7e2e6e30e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java @@ -223,7 +223,7 @@ public final class BackgroundDexOptServiceUnitTest { /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK, /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null); runFullJob(mJobServiceForIdle, mJobParametersForIdle, - /* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK, + /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK, /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null); } @@ -241,7 +241,7 @@ public final class BackgroundDexOptServiceUnitTest { assertThat(getFailedPackageNamesSecondary()).isEmpty(); runFullJob(mJobServiceForIdle, mJobParametersForIdle, - /* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK, + /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK, /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA); assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA); @@ -256,7 +256,7 @@ public final class BackgroundDexOptServiceUnitTest { mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_PERFORMED; runFullJob(mJobServiceForIdle, mJobParametersForIdle, - /* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK, + /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK, /* totalJobFinishedWithParams= */ 2, /* expectedSkippedPackage= */ null); assertThat(getFailedPackageNamesPrimary()).isEmpty(); @@ -393,7 +393,7 @@ public final class BackgroundDexOptServiceUnitTest { mCancelThread.join(TEST_WAIT_TIMEOUT_MS); // Always reschedule for periodic job - verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, true); + verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, false); verifyLastControlDexOptBlockingCall(false); } @@ -421,7 +421,7 @@ public final class BackgroundDexOptServiceUnitTest { mCancelThread.join(TEST_WAIT_TIMEOUT_MS); // Always reschedule for periodic job - verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, true); + verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, false); verify(mDexOptHelper, never()).controlDexOptBlocking(true); } 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/pm/AppsFilterImplTest.java b/services/tests/servicestests/src/com/android/server/pm/AppsFilterImplTest.java index c321639ffe2e..1a8ef9e53593 100644 --- a/services/tests/servicestests/src/com/android/server/pm/AppsFilterImplTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/AppsFilterImplTest.java @@ -387,7 +387,7 @@ public class AppsFilterImplTest { // delete user when(mSnapshot.getUserInfos()).thenReturn(USER_INFO_LIST); - appsFilter.onUserDeleted(ADDED_USER); + appsFilter.onUserDeleted(mSnapshot, ADDED_USER); for (int subjectUserId : USER_ARRAY) { for (int otherUserId : USER_ARRAY) { @@ -925,7 +925,7 @@ public class AppsFilterImplTest { assertTrue(appsFilter.shouldFilterApplication(mSnapshot, DUMMY_OVERLAY_APPID, overlaySetting, actorSetting, SYSTEM_USER)); - appsFilter.removePackage(mSnapshot, targetSetting, false /* isReplace */); + appsFilter.removePackage(mSnapshot, targetSetting); // Actor loses visibility to the overlay via removal of the target assertTrue(appsFilter.shouldFilterApplication(mSnapshot, DUMMY_ACTOR_APPID, actorSetting, @@ -1267,7 +1267,7 @@ public class AppsFilterImplTest { watcher.verifyNoChangeReported("get"); // remove a package - appsFilter.removePackage(mSnapshot, seesNothing, false /* isReplace */); + appsFilter.removePackage(mSnapshot, seesNothing); watcher.verifyChangeReported("removePackage"); } @@ -1337,7 +1337,7 @@ public class AppsFilterImplTest { target.getPackageName())); // New changes don't affect the snapshot - appsFilter.removePackage(mSnapshot, target, false); + appsFilter.removePackage(mSnapshot, target); assertTrue( appsFilter.shouldFilterApplication(mSnapshot, DUMMY_CALLING_APPID, instrumentation, target, 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/com/android/internal/telephony/RILConstants.java b/telephony/java/com/android/internal/telephony/RILConstants.java index 98926710e053..9f612e6d7dd9 100644 --- a/telephony/java/com/android/internal/telephony/RILConstants.java +++ b/telephony/java/com/android/internal/telephony/RILConstants.java @@ -536,6 +536,12 @@ public interface RILConstants { int RIL_REQUEST_TRIGGER_EMERGENCY_NETWORK_SCAN = 230; int RIL_REQUEST_CANCEL_EMERGENCY_NETWORK_SCAN = 231; int RIL_REQUEST_EXIT_EMERGENCY_MODE = 232; + int RIL_REQUEST_SET_SRVCC_CALL_INFO = 233; + int RIL_REQUEST_UPDATE_IMS_REGISTRATION_INFO = 234; + int RIL_REQUEST_START_IMS_TRAFFIC = 235; + int RIL_REQUEST_STOP_IMS_TRAFFIC = 236; + int RIL_REQUEST_SEND_ANBR_QUERY = 237; + int RIL_REQUEST_TRIGGER_EPS_FALLBACK = 238; /* Responses begin */ int RIL_RESPONSE_ACKNOWLEDGEMENT = 800; @@ -607,4 +613,7 @@ public interface RILConstants { int RIL_UNSOL_REGISTRATION_FAILED = 1104; int RIL_UNSOL_BARRING_INFO_CHANGED = 1105; int RIL_UNSOL_EMERGENCY_NETWORK_SCAN_RESULT = 1106; + int RIL_UNSOL_TRIGGER_IMS_DEREGISTRATION = 1107; + int RIL_UNSOL_CONNECTION_SETUP_FAILURE = 1108; + int RIL_UNSOL_NOTIFY_ANBR = 1109; } |