diff options
62 files changed, 1915 insertions, 573 deletions
diff --git a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java index fd8ddbcf3809..6c8af39015f5 100644 --- a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java +++ b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java @@ -59,6 +59,10 @@ public interface JobSchedulerInternal { */ void reportAppUsage(String packageName, int userId); + /** @return {@code true} if the app is considered buggy from JobScheduler's perspective. */ + boolean isAppConsideredBuggy(int callingUserId, @NonNull String callingPackageName, + int timeoutBlameUserId, @NonNull String timeoutBlamePackageName); + /** * @return {@code true} if the given notification is associated with any user-initiated jobs. */ diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobNotificationCoordinator.java b/apex/jobscheduler/service/java/com/android/server/job/JobNotificationCoordinator.java index 8a5d09404ebb..ae86afbd8380 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobNotificationCoordinator.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobNotificationCoordinator.java @@ -123,21 +123,21 @@ class JobNotificationCoordinator { if (oldDetails == null) { if (jobStatus.startedAsUserInitiatedJob) { Counter.logIncrementWithUid( - "job_scheduler.value_cntr_w_uid_initial_setNotification_call_required", + "job_scheduler.value_cntr_w_uid_initial_set_notification_call_required", jobStatus.getUid()); } else { Counter.logIncrementWithUid( - "job_scheduler.value_cntr_w_uid_initial_setNotification_call_optional", + "job_scheduler.value_cntr_w_uid_initial_set_notification_call_optional", jobStatus.getUid()); } } else { if (jobStatus.startedAsUserInitiatedJob) { Counter.logIncrementWithUid( - "job_scheduler.value_cntr_w_uid_subsequent_setNotification_call_required", + "job_scheduler.value_cntr_w_uid_subsequent_set_notification_call_required", jobStatus.getUid()); } else { Counter.logIncrementWithUid( - "job_scheduler.value_cntr_w_uid_subsequent_setNotification_call_optional", + "job_scheduler.value_cntr_w_uid_subsequent_set_notification_call_optional", jobStatus.getUid()); } if (oldDetails.notificationId != notificationId) { @@ -145,7 +145,7 @@ class JobNotificationCoordinator { removeNotificationAssociation(hostingContext, JobParameters.STOP_REASON_UNDEFINED, jobStatus); Counter.logIncrementWithUid( - "job_scheduler.value_cntr_w_uid_setNotification_changed_notification_ids", + "job_scheduler.value_cntr_w_uid_set_notification_changed_notification_ids", jobStatus.getUid()); } } 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 cbc9263a2c3d..f99bcf144b91 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -322,16 +322,25 @@ public class JobSchedulerService extends com.android.server.SystemService private static final String QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG = ".schedulePersisted()"; private static final String QUOTA_TRACKER_SCHEDULE_LOGGED = ".schedulePersisted out-of-quota logged"; + private static final String QUOTA_TRACKER_TIMEOUT_UIJ_TAG = "timeout-uij"; + private static final String QUOTA_TRACKER_TIMEOUT_EJ_TAG = "timeout-ej"; + private static final String QUOTA_TRACKER_TIMEOUT_REG_TAG = "timeout-reg"; + private static final String QUOTA_TRACKER_TIMEOUT_TOTAL_TAG = "timeout-total"; + private static final String QUOTA_TRACKER_ANR_TAG = "anr"; private static final Category QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED = new Category( ".schedulePersisted()"); private static final Category QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED = new Category( ".schedulePersisted out-of-quota logged"); - private static final Categorizer QUOTA_CATEGORIZER = (userId, packageName, tag) -> { - if (QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG.equals(tag)) { - return QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED; - } - return QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED; - }; + private static final Category QUOTA_TRACKER_CATEGORY_TIMEOUT_UIJ = + new Category(QUOTA_TRACKER_TIMEOUT_UIJ_TAG); + private static final Category QUOTA_TRACKER_CATEGORY_TIMEOUT_EJ = + new Category(QUOTA_TRACKER_TIMEOUT_EJ_TAG); + private static final Category QUOTA_TRACKER_CATEGORY_TIMEOUT_REG = + new Category(QUOTA_TRACKER_TIMEOUT_REG_TAG); + private static final Category QUOTA_TRACKER_CATEGORY_TIMEOUT_TOTAL = + new Category(QUOTA_TRACKER_TIMEOUT_TOTAL_TAG); + private static final Category QUOTA_TRACKER_CATEGORY_ANR = new Category(QUOTA_TRACKER_ANR_TAG); + private static final Category QUOTA_TRACKER_CATEGORY_DISABLED = new Category("disabled"); /** * Queue of pending jobs. The JobServiceContext class will receive jobs from this list @@ -493,10 +502,18 @@ public class JobSchedulerService extends com.android.server.SystemService } switch (name) { case Constants.KEY_ENABLE_API_QUOTAS: + case Constants.KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC: case Constants.KEY_API_QUOTA_SCHEDULE_COUNT: case Constants.KEY_API_QUOTA_SCHEDULE_WINDOW_MS: case Constants.KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT: case Constants.KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS: if (!apiQuotaScheduleUpdated) { mConstants.updateApiQuotaConstantsLocked(); updateQuotaTracker(); @@ -583,10 +600,26 @@ public class JobSchedulerService extends com.android.server.SystemService @VisibleForTesting void updateQuotaTracker() { - mQuotaTracker.setEnabled(mConstants.ENABLE_API_QUOTAS); + mQuotaTracker.setEnabled( + mConstants.ENABLE_API_QUOTAS || mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC); mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED, mConstants.API_QUOTA_SCHEDULE_COUNT, mConstants.API_QUOTA_SCHEDULE_WINDOW_MS); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_TIMEOUT_UIJ, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_TIMEOUT_EJ, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_TIMEOUT_REG, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_TIMEOUT_TOTAL, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_ANR, + mConstants.EXECUTION_SAFEGUARDS_UDC_ANR_COUNT, + mConstants.EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS); } /** @@ -616,6 +649,8 @@ public class JobSchedulerService extends com.android.server.SystemService "conn_low_signal_strength_relax_frac"; private static final String KEY_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS = "prefetch_force_batch_relax_threshold_ms"; + // This has been enabled for 3+ full releases. We're unlikely to disable it. + // TODO(141645789): remove this flag private static final String KEY_ENABLE_API_QUOTAS = "enable_api_quotas"; private static final String KEY_API_QUOTA_SCHEDULE_COUNT = "aq_schedule_count"; private static final String KEY_API_QUOTA_SCHEDULE_WINDOW_MS = "aq_schedule_window_ms"; @@ -623,6 +658,22 @@ public class JobSchedulerService extends com.android.server.SystemService "aq_schedule_throw_exception"; private static final String KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = "aq_schedule_return_failure"; + private static final String KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC = + "enable_execution_safeguards_udc"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = + "es_u_timeout_uij_count"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = + "es_u_timeout_ej_count"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = + "es_u_timeout_reg_count"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT = + "es_u_timeout_total_count"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS = + "es_u_timeout_window_ms"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT = + "es_u_anr_count"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS = + "es_u_anr_window_ms"; private static final String KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = "runtime_free_quota_max_limit_ms"; @@ -662,6 +713,17 @@ public class JobSchedulerService extends com.android.server.SystemService private static final long DEFAULT_API_QUOTA_SCHEDULE_WINDOW_MS = MINUTE_IN_MILLIS; private static final boolean DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION = true; private static final boolean DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false; + private static final boolean DEFAULT_ENABLE_EXECUTION_SAFEGUARDS_UDC = true; + private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = 2; + // EJs have a shorter timeout, so set a higher limit for them to start with. + private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = 5; + private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = 3; + private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT = 10; + private static final long DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS = + 24 * HOUR_IN_MILLIS; + private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT = 3; + private static final long DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS = + 6 * HOUR_IN_MILLIS; @VisibleForTesting public static final long DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = 30 * MINUTE_IN_MILLIS; @VisibleForTesting @@ -774,6 +836,55 @@ public class JobSchedulerService extends com.android.server.SystemService public boolean API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT; + /** + * Whether to enable the execution safeguards added in UDC. + */ + public boolean ENABLE_EXECUTION_SAFEGUARDS_UDC = DEFAULT_ENABLE_EXECUTION_SAFEGUARDS_UDC; + /** + * The maximum number of times an app can have a user-iniated job time out before the system + * begins removing some of the app's privileges. + */ + public int EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT; + /** + * The maximum number of times an app can have an expedited job time out before the system + * begins removing some of the app's privileges. + */ + public int EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT; + /** + * The maximum number of times an app can have a regular job time out before the system + * begins removing some of the app's privileges. + */ + public int EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT; + /** + * The maximum number of times an app can have jobs time out before the system + * attempts to restrict most of the app's privileges. + */ + public int EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT = + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT; + /** + * The time window that {@link #EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT}, + * {@link #EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT}, + * {@link #EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT}, and + * {@link #EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT} should be evaluated over. + */ + public long EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS = + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS; + + /** + * The maximum number of times an app can ANR from JobScheduler's perspective before + * JobScheduler will attempt to restrict the app. + */ + public int EXECUTION_SAFEGUARDS_UDC_ANR_COUNT = DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT; + /** + * The time window that {@link #EXECUTION_SAFEGUARDS_UDC_ANR_COUNT} + * should be evaluated over. + */ + public long EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS = + DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS; + /** The maximum amount of time we will let a job run for when quota is "free". */ public long RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; @@ -915,6 +1026,9 @@ public class JobSchedulerService extends com.android.server.SystemService private void updateApiQuotaConstantsLocked() { ENABLE_API_QUOTAS = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_ENABLE_API_QUOTAS, DEFAULT_ENABLE_API_QUOTAS); + ENABLE_EXECUTION_SAFEGUARDS_UDC = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC, DEFAULT_ENABLE_EXECUTION_SAFEGUARDS_UDC); // Set a minimum value on the quota limit so it's not so low that it interferes with // legitimate use cases. API_QUOTA_SCHEDULE_COUNT = Math.max(250, @@ -931,6 +1045,40 @@ public class JobSchedulerService extends com.android.server.SystemService DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT, DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT); + + // Set a minimum value on the timeout limit so it's not so low that it interferes with + // legitimate use cases. + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = Math.max(2, + DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT)); + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = Math.max(2, + DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT)); + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = Math.max(2, + DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT)); + final int highestTimeoutCount = Math.max(EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT, + Math.max(EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT, + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT)); + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT = Math.max(highestTimeoutCount, + DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT)); + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS = DeviceConfig.getLong( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS); + EXECUTION_SAFEGUARDS_UDC_ANR_COUNT = Math.max(1, + DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT)); + EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS = DeviceConfig.getLong( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS); } private void updateRuntimeConstantsLocked() { @@ -1029,6 +1177,23 @@ public class JobSchedulerService extends com.android.server.SystemService pw.print(KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT, API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT).println(); + pw.print(KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC, ENABLE_EXECUTION_SAFEGUARDS_UDC) + .println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT, + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT).println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT, + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT).println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT, + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT).println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT, + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT).println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS, + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS).println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT, + EXECUTION_SAFEGUARDS_UDC_ANR_COUNT).println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS, + EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS).println(); + pw.print(KEY_RUNTIME_MIN_GUARANTEE_MS, RUNTIME_MIN_GUARANTEE_MS).println(); pw.print(KEY_RUNTIME_MIN_EJ_GUARANTEE_MS, RUNTIME_MIN_EJ_GUARANTEE_MS).println(); pw.print(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) @@ -2252,12 +2417,52 @@ public class JobSchedulerService extends com.android.server.SystemService // Set up the app standby bucketing tracker mStandbyTracker = new StandbyTracker(); mUsageStats = LocalServices.getService(UsageStatsManagerInternal.class); - mQuotaTracker = new CountQuotaTracker(context, QUOTA_CATEGORIZER); - mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED, - mConstants.API_QUOTA_SCHEDULE_COUNT, - mConstants.API_QUOTA_SCHEDULE_WINDOW_MS); + + final Categorizer quotaCategorizer = (userId, packageName, tag) -> { + if (QUOTA_TRACKER_TIMEOUT_UIJ_TAG.equals(tag)) { + return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC + ? QUOTA_TRACKER_CATEGORY_TIMEOUT_UIJ + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + if (QUOTA_TRACKER_TIMEOUT_EJ_TAG.equals(tag)) { + return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC + ? QUOTA_TRACKER_CATEGORY_TIMEOUT_EJ + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + if (QUOTA_TRACKER_TIMEOUT_REG_TAG.equals(tag)) { + return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC + ? QUOTA_TRACKER_CATEGORY_TIMEOUT_REG + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + if (QUOTA_TRACKER_TIMEOUT_TOTAL_TAG.equals(tag)) { + return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC + ? QUOTA_TRACKER_CATEGORY_TIMEOUT_TOTAL + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + if (QUOTA_TRACKER_ANR_TAG.equals(tag)) { + return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC + ? QUOTA_TRACKER_CATEGORY_ANR + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + if (QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG.equals(tag)) { + return mConstants.ENABLE_API_QUOTAS + ? QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + if (QUOTA_TRACKER_SCHEDULE_LOGGED.equals(tag)) { + return mConstants.ENABLE_API_QUOTAS + ? QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + Slog.wtf(TAG, "Unexpected category tag: " + tag); + return QUOTA_TRACKER_CATEGORY_DISABLED; + }; + mQuotaTracker = new CountQuotaTracker(context, quotaCategorizer); + updateQuotaTracker(); // Log at most once per minute. + // Set outside updateQuotaTracker() since this is intentionally not configurable. mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED, 1, 60_000); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_DISABLED, Integer.MAX_VALUE, 60_000); mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class); mAppStandbyInternal.addListener(mStandbyTracker); @@ -2762,6 +2967,48 @@ public class JobSchedulerService extends com.android.server.SystemService 0 /* Reset cumulativeExecutionTime because of successful execution */); } + @VisibleForTesting + void maybeProcessBuggyJob(@NonNull JobStatus jobStatus, int debugStopReason) { + boolean jobTimedOut = debugStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT; + // If madeActive = 0, the job never actually started. + if (!jobTimedOut && jobStatus.madeActive > 0) { + final long executionDurationMs = sUptimeMillisClock.millis() - jobStatus.madeActive; + // The debug reason may be different if we stopped the job for some other reason + // (eg. constraints), so look at total execution time to be safe. + if (jobStatus.startedAsUserInitiatedJob) { + // TODO: factor in different min guarantees for different UI job types + jobTimedOut = executionDurationMs >= mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; + } else if (jobStatus.startedAsExpeditedJob) { + jobTimedOut = executionDurationMs >= mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS; + } else { + jobTimedOut = executionDurationMs >= mConstants.RUNTIME_MIN_GUARANTEE_MS; + } + } + if (jobTimedOut) { + final int userId = jobStatus.getTimeoutBlameUserId(); + final String pkg = jobStatus.getTimeoutBlamePackageName(); + mQuotaTracker.noteEvent(userId, pkg, + jobStatus.startedAsUserInitiatedJob + ? QUOTA_TRACKER_TIMEOUT_UIJ_TAG + : (jobStatus.startedAsExpeditedJob + ? QUOTA_TRACKER_TIMEOUT_EJ_TAG + : QUOTA_TRACKER_TIMEOUT_REG_TAG)); + if (!mQuotaTracker.noteEvent(userId, pkg, QUOTA_TRACKER_TIMEOUT_TOTAL_TAG)) { + mAppStandbyInternal.restrictApp( + pkg, userId, UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY); + } + } + + if (debugStopReason == JobParameters.INTERNAL_STOP_REASON_ANR) { + final int callingUserId = jobStatus.getUserId(); + final String callingPkg = jobStatus.getServiceComponent().getPackageName(); + if (!mQuotaTracker.noteEvent(callingUserId, callingPkg, QUOTA_TRACKER_ANR_TAG)) { + mAppStandbyInternal.restrictApp(callingPkg, callingUserId, + UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY); + } + } + } + // JobCompletedListener implementations. /** @@ -2784,6 +3031,8 @@ public class JobSchedulerService extends com.android.server.SystemService mLastCompletedJobTimeElapsed[mLastCompletedJobIndex] = sElapsedRealtimeClock.millis(); mLastCompletedJobIndex = (mLastCompletedJobIndex + 1) % NUM_COMPLETED_JOB_HISTORY; + maybeProcessBuggyJob(jobStatus, debugStopReason); + if (debugStopReason == JobParameters.INTERNAL_STOP_REASON_UNINSTALL || debugStopReason == JobParameters.INTERNAL_STOP_REASON_DATA_CLEARED) { // The job should have already been cleared from the rest of the JS tracking. No need @@ -3511,26 +3760,36 @@ public class JobSchedulerService extends com.android.server.SystemService if (job.shouldTreatAsUserInitiatedJob() && checkRunUserInitiatedJobsPermission( job.getSourceUid(), job.getSourcePackageName())) { + // The calling package is the one doing the work, so use it in the + // timeout quota checks. + final boolean isWithinTimeoutQuota = mQuotaTracker.isWithinQuota( + job.getTimeoutBlameUserId(), job.getTimeoutBlamePackageName(), + QUOTA_TRACKER_TIMEOUT_UIJ_TAG); + final long upperLimitMs = isWithinTimeoutQuota + ? mConstants.RUNTIME_UI_LIMIT_MS + : mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; if (job.getJob().getRequiredNetwork() != null) { // User-initiated data transfers. if (mConstants.RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS) { final long estimatedTransferTimeMs = mConnectivityController.getEstimatedTransferTimeMs(job); if (estimatedTransferTimeMs == ConnectivityController.UNKNOWN_TIME) { - return mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS; + return Math.min(upperLimitMs, + mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS); } // Try to give the job at least as much time as we think the transfer // will take, but cap it at the maximum limit. final long factoredTransferTimeMs = (long) (estimatedTransferTimeMs * mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR); - return Math.min(mConstants.RUNTIME_UI_LIMIT_MS, + return Math.min(upperLimitMs, Math.max(factoredTransferTimeMs, mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS)); } - return Math.max(mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, - mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS); + return Math.min(upperLimitMs, + Math.max(mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS)); } - return mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; + return Math.min(upperLimitMs, mConstants.RUNTIME_MIN_UI_GUARANTEE_MS); } else if (job.shouldTreatAsExpeditedJob()) { // Don't guarantee RESTRICTED jobs more than 5 minutes. return job.getEffectiveStandbyBucket() != RESTRICTED_INDEX @@ -3547,13 +3806,24 @@ public class JobSchedulerService extends com.android.server.SystemService synchronized (mLock) { if (job.shouldTreatAsUserInitiatedJob() && checkRunUserInitiatedJobsPermission( - job.getSourceUid(), job.getSourcePackageName())) { + job.getSourceUid(), job.getSourcePackageName()) + && mQuotaTracker.isWithinQuota(job.getTimeoutBlameUserId(), + job.getTimeoutBlamePackageName(), + QUOTA_TRACKER_TIMEOUT_UIJ_TAG)) { return mConstants.RUNTIME_UI_LIMIT_MS; } if (job.shouldTreatAsUserInitiatedJob()) { return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; } - return Math.min(mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + // Only let the app use the higher runtime if it hasn't repeatedly timed out. + final String timeoutTag = job.shouldTreatAsExpeditedJob() + ? QUOTA_TRACKER_TIMEOUT_EJ_TAG : QUOTA_TRACKER_TIMEOUT_REG_TAG; + final long upperLimitMs = + mQuotaTracker.isWithinQuota(job.getTimeoutBlameUserId(), + job.getTimeoutBlamePackageName(), timeoutTag) + ? mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS + : mConstants.RUNTIME_MIN_GUARANTEE_MS; + return Math.min(upperLimitMs, mConstants.USE_TARE_POLICY ? mTareController.getMaxJobExecutionTimeMsLocked(job) : mQuotaController.getMaxJobExecutionTimeMsLocked(job)); @@ -3797,6 +4067,17 @@ public class JobSchedulerService extends com.android.server.SystemService } @Override + public boolean isAppConsideredBuggy(int callingUserId, @NonNull String callingPackageName, + int timeoutBlameUserId, @NonNull String timeoutBlamePackageName) { + return !mQuotaTracker.isWithinQuota(callingUserId, callingPackageName, + QUOTA_TRACKER_ANR_TAG) + || !mQuotaTracker.isWithinQuota(callingUserId, callingPackageName, + QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG) + || !mQuotaTracker.isWithinQuota(timeoutBlameUserId, timeoutBlamePackageName, + QUOTA_TRACKER_TIMEOUT_TOTAL_TAG); + } + + @Override public boolean isNotificationAssociatedWithAnyUserInitiatedJobs(int notificationId, int userId, @NonNull String packageName) { if (packageName == null) { 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 0b08b6faf971..5f795b6fedd9 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -1331,7 +1331,7 @@ public final class JobServiceContext implements ServiceConnection { // FINISHED/NO-RETRY. onSlowAppResponseLocked(/* reschedule */ false, /* updateStopReasons */ true, /* texCounterMetricId */ - "job_scheduler.value_cntr_w_uid_slow_app_response_onStartJob", + "job_scheduler.value_cntr_w_uid_slow_app_response_on_start_job", /* debugReason */ "timed out while starting", /* anrMessage */ "No response to onStartJob", CompatChanges.isChangeEnabled(ANR_PRE_UDC_APIS_ON_SLOW_RESPONSES, @@ -1343,7 +1343,7 @@ public final class JobServiceContext implements ServiceConnection { // other reason. onSlowAppResponseLocked(/* reschedule */ true, /* updateStopReasons */ false, /* texCounterMetricId */ - "job_scheduler.value_cntr_w_uid_slow_app_response_onStopJob", + "job_scheduler.value_cntr_w_uid_slow_app_response_on_stop_job", /* debugReason */ "timed out while stopping", /* anrMessage */ "No response to onStopJob", CompatChanges.isChangeEnabled(ANR_PRE_UDC_APIS_ON_SLOW_RESPONSES, @@ -1400,7 +1400,7 @@ public final class JobServiceContext implements ServiceConnection { } else if (mAwaitingNotification) { onSlowAppResponseLocked(/* reschedule */ true, /* updateStopReasons */ true, /* texCounterMetricId */ - "job_scheduler.value_cntr_w_uid_slow_app_response_setNotification", + "job_scheduler.value_cntr_w_uid_slow_app_response_set_notification", /* debugReason */ "timed out while stopping", /* anrMessage */ "required notification not provided", /* triggerAnr */ true); 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 edd531d13965..3baa9e6d3de9 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 @@ -1088,13 +1088,77 @@ public final class JobStatus { return UserHandle.getUserId(callingUid); } + private boolean shouldBlameSourceForTimeout() { + // If the system scheduled the job on behalf of an app, assume the app is the one + // doing the work and blame the app directly. This is the case with things like + // syncs via SyncManager. + // If the system didn't schedule the job on behalf of an app, then + // blame the app doing the actual work. Proxied jobs are a little tricky. + // Proxied jobs scheduled by built-in system apps like DownloadManager may be fine + // and we could consider exempting those jobs. For example, in DownloadManager's + // case, all it does is download files and the code is vetted. A timeout likely + // means it's downloading a large file, which isn't an error. For now, DownloadManager + // is an exempted app, so this shouldn't be an issue. + // However, proxied jobs coming from other system apps (such as those that can + // be updated separately from an OTA) may not be fine and we would want to apply + // this policy to those jobs/apps. + // TODO(284512488): consider exempting DownloadManager or other system apps + return UserHandle.isCore(callingUid); + } + + /** + * Returns the package name that should most likely be blamed for the job timing out. + */ + public String getTimeoutBlamePackageName() { + if (shouldBlameSourceForTimeout()) { + return sourcePackageName; + } + return getServiceComponent().getPackageName(); + } + + /** + * Returns the UID that should most likely be blamed for the job timing out. + */ + public int getTimeoutBlameUid() { + if (shouldBlameSourceForTimeout()) { + return sourceUid; + } + return callingUid; + } + + /** + * Returns the userId that should most likely be blamed for the job timing out. + */ + public int getTimeoutBlameUserId() { + if (shouldBlameSourceForTimeout()) { + return sourceUserId; + } + return UserHandle.getUserId(callingUid); + } + /** * Returns an appropriate standby bucket for the job, taking into account any standby * exemptions. */ public int getEffectiveStandbyBucket() { + final JobSchedulerInternal jsi = LocalServices.getService(JobSchedulerInternal.class); + final boolean isBuggy = jsi.isAppConsideredBuggy( + getUserId(), getServiceComponent().getPackageName(), + getTimeoutBlameUserId(), getTimeoutBlamePackageName()); + final int actualBucket = getStandbyBucket(); if (actualBucket == EXEMPTED_INDEX) { + // EXEMPTED apps always have their jobs exempted, even if they're buggy, because the + // user has explicitly told the system to avoid restricting the app for power reasons. + if (isBuggy) { + final String pkg; + if (getServiceComponent().getPackageName().equals(sourcePackageName)) { + pkg = sourcePackageName; + } else { + pkg = getServiceComponent().getPackageName() + "/" + sourcePackageName; + } + Slog.w(TAG, "Exempted app " + pkg + " considered buggy"); + } return actualBucket; } if (uidActive || getJob().isExemptedFromAppStandby()) { @@ -1102,13 +1166,18 @@ public final class JobStatus { // like other ACTIVE apps. return ACTIVE_INDEX; } + // If the app is considered buggy, but hasn't yet been put in the RESTRICTED bucket + // (potentially because it's used frequently by the user), limit its effective bucket + // so that it doesn't get to run as much as a normal ACTIVE app. + final int highestBucket = isBuggy ? WORKING_INDEX : ACTIVE_INDEX; if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX && mHasMediaBackupExemption) { - // Cap it at WORKING_INDEX as media back up jobs are important to the user, and the + // Treat it as if it's at least WORKING_INDEX since media backup jobs are important + // to the user, and the // source package may not have been used directly in a while. - return Math.min(WORKING_INDEX, actualBucket); + return Math.max(highestBucket, Math.min(WORKING_INDEX, actualBucket)); } - return actualBucket; + return Math.max(highestBucket, actualBucket); } /** Returns the real standby bucket of the job. */ diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java index 07958dd0fef5..1c29982dbd48 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java @@ -773,6 +773,14 @@ public final class QuotaController extends StateController { // If quota is currently "free", then the job can run for the full amount of time, // regardless of bucket (hence using charging instead of isQuotaFreeLocked()). if (mService.isBatteryCharging() + // The top and foreground cases here were added because apps in those states + // aren't really restricted and the work could be something the user is + // waiting for. Now that user-initiated jobs are a defined concept, we may + // not need these exemptions as much. However, UIJs are currently limited + // (as of UDC) to data transfer work. There may be other work that could + // rely on this exception. Once we add more UIJ types, we can re-evaluate + // the need for these exceptions. + // TODO: re-evaluate the need for these exceptions || mTopAppCache.get(jobStatus.getSourceUid()) || isTopStartedJobLocked(jobStatus) || isUidInForeground(jobStatus.getSourceUid())) { diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java index 55e681521048..7d3837786be9 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java @@ -3025,7 +3025,7 @@ public class AppStandbyController public static final long DEFAULT_INITIAL_FOREGROUND_SERVICE_START_TIMEOUT = COMPRESS_TIME ? ONE_MINUTE : 30 * ONE_MINUTE; public static final long DEFAULT_AUTO_RESTRICTED_BUCKET_DELAY_MS = - COMPRESS_TIME ? ONE_MINUTE : ONE_DAY; + COMPRESS_TIME ? ONE_MINUTE : ONE_HOUR; public static final boolean DEFAULT_CROSS_PROFILE_APPS_SHARE_STANDBY_BUCKETS = true; public static final long DEFAULT_BROADCAST_RESPONSE_WINDOW_DURATION_MS = 2 * ONE_MINUTE; diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 57935e3bd5b1..70e924a2acfe 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -2933,62 +2933,21 @@ public class WallpaperManager { } } - if (!isComponentExist(context, cn)) { - cn = null; - } - - return cn; - } - - /** - * Return {@link ComponentName} of the CMF default wallpaper, or - * {@link #getDefaultWallpaperComponent(Context)} if none is defined. - * - * @hide - */ - public static ComponentName getCmfDefaultWallpaperComponent(Context context) { - ComponentName cn = null; - String[] cmfWallpaperMap = context.getResources().getStringArray( - com.android.internal.R.array.cmf_default_wallpaper_component); - if (cmfWallpaperMap == null || cmfWallpaperMap.length == 0) { - Log.d(TAG, "No CMF wallpaper config"); - return getDefaultWallpaperComponent(context); - } - - for (String entry : cmfWallpaperMap) { - String[] cmfWallpaper; - if (!TextUtils.isEmpty(entry)) { - cmfWallpaper = entry.split(","); - if (cmfWallpaper != null && cmfWallpaper.length == 2 && VALUE_CMF_COLOR.equals( - cmfWallpaper[0]) && !TextUtils.isEmpty(cmfWallpaper[1])) { - cn = ComponentName.unflattenFromString(cmfWallpaper[1]); - break; - } + // Check if the package exists + if (cn != null) { + try { + final PackageManager packageManager = context.getPackageManager(); + packageManager.getPackageInfo(cn.getPackageName(), + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE); + } catch (PackageManager.NameNotFoundException e) { + cn = null; } } - if (!isComponentExist(context, cn)) { - cn = null; - } - return cn; } - private static boolean isComponentExist(Context context, ComponentName cn) { - if (cn == null) { - return false; - } - try { - final PackageManager packageManager = context.getPackageManager(); - packageManager.getPackageInfo(cn.getPackageName(), - PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE); - } catch (PackageManager.NameNotFoundException e) { - return false; - } - return true; - } - /** * Register a callback for lock wallpaper observation. Only the OS may use this. * diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index b2a923043dbe..da5e40aedbd2 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -11369,7 +11369,8 @@ public class DevicePolicyManager { * @throws SecurityException if the caller is not a profile owner on an organization-owned * managed profile. * @throws IllegalStateException if called after the device setup has been completed. - * @throws UnsupportedOperationException if the api is not enabled. + * @throws UnsupportedOperationException if managed subscriptions policy is not explicitly + * enabled by the device policy management role holder during device setup. * @see ManagedSubscriptionsPolicy */ public void setManagedSubscriptionsPolicy(@Nullable ManagedSubscriptionsPolicy policy) { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 1bbb7b4bd671..a28be4be24ad 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -5459,7 +5459,7 @@ public final class ViewRootImpl implements ViewParent, } private void updateRenderHdrSdrRatio() { - mRenderHdrSdrRatio = mDisplay.getHdrSdrRatio(); + mRenderHdrSdrRatio = Math.min(mDesiredHdrSdrRatio, mDisplay.getHdrSdrRatio()); mUpdateHdrSdrRatioInfo = true; } @@ -5487,22 +5487,14 @@ public final class ViewRootImpl implements ViewParent, mHdrSdrRatioChangedListener = null; } else { mHdrSdrRatioChangedListener = display -> { - setTargetHdrSdrRatio(display.getHdrSdrRatio()); + updateRenderHdrSdrRatio(); + invalidate(); }; mDisplay.registerHdrSdrRatioChangedListener(mExecutor, mHdrSdrRatioChangedListener); } } } - /** happylint */ - public void setTargetHdrSdrRatio(float ratio) { - if (mRenderHdrSdrRatio != ratio) { - mRenderHdrSdrRatio = ratio; - mUpdateHdrSdrRatioInfo = true; - invalidate(); - } - } - @Override public void requestChildFocus(View child, View focused) { if (DEBUG_INPUT_RESIZE) { diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 00f8db0d9264..ee8c0f83304e 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -1817,16 +1817,6 @@ specified --> <string name="default_wallpaper_component" translatable="false">@null</string> - <!-- CMF colors to default wallpaper component map, the component with color matching the device - color will be the cmf default wallpapers. The default wallpaper will be default wallpaper - component if not specified. - - E.g. for SLV color, and com.android.example/com.android.example.SlVDefaultWallpaper - <item>SLV,com.android.example/com.android.example.SlVDefaultWallpaper</item> --> - <string-array name="cmf_default_wallpaper_component" translatable="false"> - <!-- Add packages here --> - </string-array> - <!-- By default a product has no distinct default lock wallpaper --> <item name="default_lock_wallpaper" type="drawable">@null</item> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index a8518afd7d3d..dc4eafd2e00e 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2116,7 +2116,6 @@ <java-symbol type="string" name="data_usage_rapid_body" /> <java-symbol type="string" name="data_usage_rapid_app_body" /> <java-symbol type="string" name="default_wallpaper_component" /> - <java-symbol type="array" name="cmf_default_wallpaper_component" /> <java-symbol type="string" name="device_storage_monitor_notification_channel" /> <java-symbol type="string" name="dlg_ok" /> <java-symbol type="string" name="dump_heap_notification" /> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java index d6e1a82a68ff..467e9c7a116b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java @@ -375,16 +375,15 @@ public class DisplayLayout { insetsState.getDisplayFrame(), WindowInsets.Type.navigationBars(), false /* ignoreVisibility */); - outInsets.set(insets.left, insets.top, insets.right, insets.bottom); int position = navigationBarPosition(res, displayWidth, displayHeight, displayRotation); int navBarSize = getNavigationBarSize(res, position, displayWidth > displayHeight, uiMode); if (position == NAV_BAR_BOTTOM) { - outInsets.bottom = Math.max(outInsets.bottom , navBarSize); + outInsets.bottom = Math.max(insets.bottom , navBarSize); } else if (position == NAV_BAR_RIGHT) { - outInsets.right = Math.max(outInsets.right , navBarSize); + outInsets.right = Math.max(insets.right , navBarSize); } else if (position == NAV_BAR_LEFT) { - outInsets.left = Math.max(outInsets.left , navBarSize); + outInsets.left = Math.max(insets.left , navBarSize); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 5a9c25f7a710..c491fed5d896 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -70,6 +70,8 @@ import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.freeform.FreeformComponents; import com.android.wm.shell.fullscreen.FullscreenTaskListener; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController; +import com.android.wm.shell.keyguard.KeyguardTransitionHandler; +import com.android.wm.shell.keyguard.KeyguardTransitions; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; @@ -77,8 +79,6 @@ import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.pip.phone.PipTouchHandler; -import com.android.wm.shell.keyguard.KeyguardTransitionHandler; -import com.android.wm.shell.keyguard.KeyguardTransitions; import com.android.wm.shell.recents.RecentTasks; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.recents.RecentsTransitionHandler; @@ -102,13 +102,13 @@ import com.android.wm.shell.unfold.UnfoldAnimationController; import com.android.wm.shell.unfold.UnfoldTransitionHandler; import com.android.wm.shell.windowdecor.WindowDecorViewModel; +import java.util.Optional; + import dagger.BindsOptionalOf; import dagger.Lazy; import dagger.Module; import dagger.Provides; -import java.util.Optional; - /** * Provides basic dependencies from {@link com.android.wm.shell}, these dependencies are only * accessible from components within the WM subcomponent (can be explicitly exposed to the @@ -548,13 +548,15 @@ public abstract class WMShellBaseModule { @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, @ShellAnimationThread ShellExecutor animExecutor, - ShellCommandHandler shellCommandHandler) { + ShellCommandHandler shellCommandHandler, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { if (!context.getResources().getBoolean(R.bool.config_registerShellTransitionsOnInit)) { // TODO(b/238217847): Force override shell init if registration is disabled shellInit = new ShellInit(mainExecutor); } return new Transitions(context, shellInit, shellController, organizer, pool, - displayController, mainExecutor, mainHandler, animExecutor, shellCommandHandler); + displayController, mainExecutor, mainHandler, animExecutor, shellCommandHandler, + rootTaskDisplayAreaOrganizer); } @WMSingleton 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 5086e2c4b05d..bf75132dde54 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 @@ -816,6 +816,12 @@ public class PipTransition extends PipTransitionController { final ActivityManager.RunningTaskInfo taskInfo = pipChange.getTaskInfo(); final SurfaceControl leash = pipChange.getLeash(); final int startRotation = pipChange.getStartRotation(); + // Check again in case some callers use startEnterAnimation directly so the flag was not + // set in startAnimation, e.g. from DefaultMixedHandler. + if (!mInFixedRotation) { + mEndFixedRotation = pipChange.getEndFixedRotation(); + mInFixedRotation = mEndFixedRotation != ROTATION_UNDEFINED; + } final int endRotation = mInFixedRotation ? mEndFixedRotation : pipChange.getEndRotation(); setBoundsStateForEntry(taskInfo.topActivity, taskInfo.pictureInPictureParams, @@ -844,7 +850,7 @@ public class PipTransition extends PipTransitionController { && taskInfo.pictureInPictureParams.isAutoEnterEnabled() && mPipTransitionState.getInSwipePipToHomeTransition()) { handleSwipePipToHomeTransition(startTransaction, finishTransaction, leash, - sourceHintRect, destinationBounds, rotationDelta, taskInfo); + sourceHintRect, destinationBounds, taskInfo); return; } @@ -935,8 +941,15 @@ public class PipTransition extends PipTransitionController { @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull SurfaceControl leash, @Nullable Rect sourceHintRect, - @NonNull Rect destinationBounds, int rotationDelta, + @NonNull Rect destinationBounds, @NonNull ActivityManager.RunningTaskInfo pipTaskInfo) { + if (mInFixedRotation) { + // If rotation changes when returning to home, the transition should contain both the + // entering PiP and the display change (PipController#startSwipePipToHome has updated + // the display layout to new rotation). So it is not expected to see fixed rotation. + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "%s: SwipePipToHome should not use fixed rotation %d", TAG, mEndFixedRotation); + } final SurfaceControl swipePipToHomeOverlay = mPipOrganizer.mSwipePipToHomeOverlay; if (swipePipToHomeOverlay != null) { // Launcher fade in the overlay on top of the fullscreen Task. It is possible we @@ -947,12 +960,7 @@ public class PipTransition extends PipTransitionController { mPipOrganizer.mSwipePipToHomeOverlay = null; } - Rect sourceBounds = pipTaskInfo.configuration.windowConfiguration.getBounds(); - if (!Transitions.SHELL_TRANSITIONS_ROTATION && rotationDelta % 2 == 1) { - // PipController#startSwipePipToHome has updated the display layout to new rotation, - // so flip the source bounds to match the same orientation. - sourceBounds = new Rect(0, 0, sourceBounds.height(), sourceBounds.width()); - } + final Rect sourceBounds = pipTaskInfo.configuration.windowConfiguration.getBounds(); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getAnimator(pipTaskInfo, leash, sourceBounds, sourceBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 8723f9b0181d..66da1253be6f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -595,8 +595,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { cancel(mWillFinishToHome, true /* withScreenshots */, "display change"); return; } - // Don't consider order-only changes as changing apps. - if (!TransitionUtil.isOrderOnly(change)) { + // Don't consider order-only & non-leaf changes as changing apps. + if (!TransitionUtil.isOrderOnly(change) && isLeafTask) { hasChangingApp = true; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 38911dbfc2b5..c8e88fcc0a6a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -2499,6 +2499,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // handling to the mixed-handler to deal with splitting it up. if (mMixedHandler.animatePendingSplitWithDisplayChange(transition, info, startTransaction, finishTransaction, finishCallback)) { + mSplitLayout.update(startTransaction); return true; } } 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 715f8354ea51..3bf278cc46cf 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 @@ -58,7 +58,6 @@ import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITI import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_OPEN; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN; -import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition; import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow; import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet; import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation; @@ -76,6 +75,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.graphics.Color; import android.graphics.Insets; import android.graphics.Point; import android.graphics.Rect; @@ -103,6 +103,7 @@ import com.android.internal.policy.AttributeCache; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; @@ -137,6 +138,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private final Rect mInsets = new Rect(0, 0, 0, 0); private float mTransitionAnimationScaleSetting = 1.0f; + private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer; private final int mCurrentUserId; private Drawable mEnterpriseThumbnailDrawable; @@ -157,7 +159,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @NonNull DisplayController displayController, @NonNull TransactionPool transactionPool, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, - @NonNull ShellExecutor animExecutor) { + @NonNull ShellExecutor animExecutor, + @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer) { mDisplayController = displayController; mTransactionPool = transactionPool; mContext = context; @@ -168,6 +171,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { mCurrentUserId = UserHandle.myUserId(); mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class); shellInit.addInitCallback(this::onInit, this); + mRootTDAOrganizer = rootTDAOrganizer; } private void onInit() { @@ -510,10 +514,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } if (backgroundColorForTransition != 0) { - for (int i = 0; i < info.getRootCount(); ++i) { - addBackgroundToTransition(info.getRoot(i).getLeash(), backgroundColorForTransition, - startTransaction, finishTransaction); - } + addBackgroundColorOnTDA(info, backgroundColorForTransition, startTransaction, + finishTransaction); } if (postStartTransactionCallbacks.size() > 0) { @@ -543,6 +545,28 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { return true; } + private void addBackgroundColorOnTDA(@NonNull TransitionInfo info, + @ColorInt int color, @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + final Color bgColor = Color.valueOf(color); + final float[] colorArray = new float[] { bgColor.red(), bgColor.green(), bgColor.blue() }; + + for (int i = 0; i < info.getRootCount(); ++i) { + final int displayId = info.getRoot(i).getDisplayId(); + final SurfaceControl.Builder colorLayerBuilder = new SurfaceControl.Builder() + .setName("animation-background") + .setCallsite("DefaultTransitionHandler") + .setColorLayer(); + + mRootTDAOrganizer.attachToDisplayArea(displayId, colorLayerBuilder); + final SurfaceControl backgroundSurface = colorLayerBuilder.build(); + startTransaction.setColor(backgroundSurface, colorArray) + .setLayer(backgroundSurface, -1) + .show(backgroundSurface); + finishTransaction.remove(backgroundSurface); + } + } + private static boolean isDreamTransition(@NonNull TransitionInfo info) { for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); @@ -696,9 +720,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { if (a != null) { if (!a.isInitialized()) { - final int width = endBounds.width(); - final int height = endBounds.height(); - a.initialize(width, height, width, height); + final Rect animationRange = TransitionUtil.isClosingType(changeMode) + ? change.getStartAbsBounds() : change.getEndAbsBounds(); + a.initialize(animationRange.width(), animationRange.height(), + endBounds.width(), endBounds.height()); } a.restrictDuration(MAX_ANIMATION_DURATION); a.scaleCurrentDuration(mTransitionAnimationScaleSetting); @@ -886,17 +911,18 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } private static void applyTransformation(long time, SurfaceControl.Transaction t, - SurfaceControl leash, Animation anim, Transformation transformation, float[] matrix, + SurfaceControl leash, Animation anim, Transformation tmpTransformation, float[] matrix, Point position, float cornerRadius, @Nullable Rect immutableClipRect) { - anim.getTransformation(time, transformation); + tmpTransformation.clear(); + anim.getTransformation(time, tmpTransformation); if (position != null) { - transformation.getMatrix().postTranslate(position.x, position.y); + tmpTransformation.getMatrix().postTranslate(position.x, position.y); } - t.setMatrix(leash, transformation.getMatrix(), matrix); - t.setAlpha(leash, transformation.getAlpha()); + t.setMatrix(leash, tmpTransformation.getMatrix(), matrix); + t.setAlpha(leash, tmpTransformation.getAlpha()); final Rect clipRect = immutableClipRect == null ? null : new Rect(immutableClipRect); - Insets extensionInsets = Insets.min(transformation.getInsets(), Insets.NONE); + Insets extensionInsets = Insets.min(tmpTransformation.getInsets(), Insets.NONE); if (!extensionInsets.equals(Insets.NONE) && clipRect != null && !clipRect.isEmpty()) { // Clip out any overflowing edge extension clipRect.inset(extensionInsets); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 2314376e8d27..cdc82eadcd90 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -69,6 +69,7 @@ import androidx.annotation.BinderThread; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; @@ -265,7 +266,8 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor) { this(context, shellInit, shellController, organizer, pool, displayController, mainExecutor, - mainHandler, animExecutor, null); + mainHandler, animExecutor, null, + new RootTaskDisplayAreaOrganizer(mainExecutor, context)); } public Transitions(@NonNull Context context, @@ -277,7 +279,8 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor, - @Nullable ShellCommandHandler shellCommandHandler) { + @Nullable ShellCommandHandler shellCommandHandler, + @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer) { mOrganizer = organizer; mContext = context; mMainExecutor = mainExecutor; @@ -285,7 +288,7 @@ public class Transitions implements RemoteCallable<Transitions>, mDisplayController = displayController; mPlayerImpl = new TransitionPlayerImpl(); mDefaultTransitionHandler = new DefaultTransitionHandler(context, shellInit, - displayController, pool, mainExecutor, mainHandler, animExecutor); + displayController, pool, mainExecutor, mainHandler, animExecutor, rootTDAOrganizer); mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor); mShellController = shellController; // The very last handler (0 in the list) should be the default one. diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt new file mode 100644 index 000000000000..f7ce87088040 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt @@ -0,0 +1,200 @@ +/* + * 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.wm.shell.flicker.pip + +import android.app.Instrumentation +import android.os.SystemClock +import android.platform.test.annotations.Presubmit +import android.tools.common.NavBar +import android.tools.common.Rotation +import android.tools.device.apphelpers.StandardAppHelper +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import android.tools.device.helpers.WindowUtils +import android.tools.device.traces.parsers.toFlickerComponent +import androidx.test.filters.RequiresDevice +import androidx.test.uiautomator.By +import androidx.test.uiautomator.BySelector +import androidx.test.uiautomator.UiObject2 +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.server.wm.flicker.testapp.ActivityOptions +import com.android.wm.shell.flicker.LAUNCHER_UI_PACKAGE_NAME +import org.junit.Assume +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test entering pip from an app via auto-enter property when navigating to home from split screen. + * + * To run this test: `atest WMShellFlickerTests:AutoEnterPipOnGoToHomeTest` + * + * Actions: + * ``` + * Launch an app in full screen + * Select "Auto-enter PiP" radio button + * Open all apps and drag another app icon to enter split screen + * Press Home button or swipe up to go Home and put [pipApp] in pip mode + * ``` + * + * Notes: + * ``` + * 1. All assertions are inherited from [EnterPipTest] + * 2. Part of the test setup occurs automatically via + * [android.tools.device.flicker.legacy.runner.TransitionRunner], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup + * ``` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class AutoEnterPipFromSplitScreenOnGoToHomeTest(flicker: FlickerTest) : + AutoEnterPipOnGoToHomeTest(flicker) { + private val portraitDisplayBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_0) + /** Second app used to enter split screen mode */ + protected val secondAppForSplitScreen = getSplitScreenApp(instrumentation) + fun getSplitScreenApp(instrumentation: Instrumentation): StandardAppHelper = + SimpleAppHelper( + instrumentation, + ActivityOptions.SplitScreen.Primary.LABEL, + ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent() + ) + + /** Defines the transition used to run the test */ + override val transition: FlickerBuilder.() -> Unit + get() = { + setup { + secondAppForSplitScreen.launchViaIntent(wmHelper) + pipApp.launchViaIntent(wmHelper) + tapl.goHome() + enterSplitScreen() + // wait until split screen is established + wmHelper + .StateSyncBuilder() + .withWindowSurfaceAppeared(pipApp) + .withWindowSurfaceAppeared(secondAppForSplitScreen) + .withSplitDividerVisible() + .waitForAndVerify() + pipApp.enableAutoEnterForPipActivity() + } + teardown { + // close gracefully so that onActivityUnpinned() can be called before force exit + pipApp.closePipWindow(wmHelper) + pipApp.exit(wmHelper) + secondAppForSplitScreen.exit(wmHelper) + } + transitions { tapl.goHome() } + } + + // TODO(b/285400227) merge the code in a common utility - this is copied from SplitScreenUtils + private val TIMEOUT_MS = 3_000L + private val overviewSnapshotSelector: BySelector + get() = By.res(LAUNCHER_UI_PACKAGE_NAME, "snapshot") + private fun enterSplitScreen() { + // Note: The initial split position in landscape is different between tablet and phone. + // In landscape, tablet will let the first app split to right side, and phone will + // split to left side. + if (tapl.isTablet) { + // TAPL's currentTask on tablet is sometimes not what we expected if the overview + // contains more than 3 task views. We need to use uiautomator directly to find the + // second task to split. + tapl.workspace.switchToOverview().overviewActions.clickSplit() + val snapshots = tapl.device.wait(Until.findObjects(overviewSnapshotSelector), + TIMEOUT_MS) + if (snapshots == null || snapshots.size < 1) { + error("Fail to find a overview snapshot to split.") + } + + // Find the second task in the upper right corner in split select mode by sorting + // 'left' in descending order and 'top' in ascending order. + snapshots.sortWith { t1: UiObject2, t2: UiObject2 -> + t2.getVisibleBounds().left - t1.getVisibleBounds().left + } + snapshots.sortWith { t1: UiObject2, t2: UiObject2 -> + t1.getVisibleBounds().top - t2.getVisibleBounds().top + } + snapshots[0].click() + } else { + tapl.workspace + .switchToOverview() + .currentTask + .tapMenu() + .tapSplitMenuItem() + .currentTask + .open() + } + SystemClock.sleep(TIMEOUT_MS) + } + + @Presubmit + @Test + override fun pipOverlayLayerAppearThenDisappear() { + // when entering from split screen we use alpha animation, without overlay + } + + @Presubmit + @Test + override fun pipLayerOrOverlayRemainInsideVisibleBounds() { + // when entering from split screen we use alpha animation, without overlay + } + + @Presubmit + @Test + override fun pipLayerReduces() { + // when entering from split screen we use alpha animation, so there is no size change + Assume.assumeFalse(flicker.scenario.isGesturalNavigation) + super.pipLayerReduces() + } + + @Presubmit + @Test + override fun pipAppLayerAlwaysVisible() { + // pip layer in gesture nav will disappear during transition with alpha animation + Assume.assumeFalse(flicker.scenario.isGesturalNavigation) + super.pipAppLayerAlwaysVisible() + } + + @Presubmit + @Test + override fun pipWindowRemainInsideVisibleBounds() { + if (tapl.isTablet) { + flicker.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) } + } else { + // on phones home does not rotate in landscape, PiP enters back to portrait + // orientation so use display bounds from that orientation for assertion + flicker.assertWmVisibleRegion(pipApp) { coversAtMost(portraitDisplayBounds) } + } + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt index ef7bedf49a92..b95732e43357 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt @@ -53,7 +53,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class AutoEnterPipOnGoToHomeTest(flicker: FlickerTest) : EnterPipViaAppUiButtonTest(flicker) { +open class AutoEnterPipOnGoToHomeTest(flicker: FlickerTest) : EnterPipViaAppUiButtonTest(flicker) { override val thisTransition: FlickerBuilder.() -> Unit = { transitions { tapl.goHome() } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt index 95121def07bc..cdbdb85a9195 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt @@ -62,7 +62,7 @@ abstract class EnterPipTransition(flicker: FlickerTest) : PipTransition(flicker) */ @Presubmit @Test - fun pipWindowRemainInsideVisibleBounds() { + open fun pipWindowRemainInsideVisibleBounds() { flicker.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java index 3fcb7f398185..ffc0479f01c3 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java @@ -116,10 +116,7 @@ public class InfoMediaManager extends MediaManager { && !TextUtils.isEmpty(mPackageName)) { RouteListingPreference routeListingPreference = mRouterManager.getRouteListingPreference(mPackageName); - if (routeListingPreference != null) { - Api34Impl.onRouteListingPreferenceUpdated(null, routeListingPreference, - mPreferenceItemMap); - } + Api34Impl.onRouteListingPreferenceUpdated(routeListingPreference, mPreferenceItemMap); } refreshDevices(); } @@ -631,7 +628,7 @@ public class InfoMediaManager extends MediaManager { } /** - * Ignore callback here since we'll also receive {@link onRequestFailed} with reason code. + * Ignore callback here since we'll also receive {@link #onRequestFailed} with reason code. */ @Override public void onTransferFailed(RoutingSessionInfo session, MediaRoute2Info route) { @@ -656,9 +653,9 @@ public class InfoMediaManager extends MediaManager { public void onRouteListingPreferenceUpdated( String packageName, RouteListingPreference routeListingPreference) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - Api34Impl.onRouteListingPreferenceUpdated(packageName, routeListingPreference, - mPreferenceItemMap); + if (TextUtils.equals(mPackageName, packageName)) { + Api34Impl.onRouteListingPreferenceUpdated( + routeListingPreference, mPreferenceItemMap); refreshDevices(); } } @@ -746,7 +743,6 @@ public class InfoMediaManager extends MediaManager { @DoNotInline static void onRouteListingPreferenceUpdated( - String packageName, RouteListingPreference routeListingPreference, Map<String, RouteListingPreference.Item> preferenceItemMap) { preferenceItemMap.clear(); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java index aa5f3df1b750..39780f3a96ed 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java @@ -73,6 +73,7 @@ import java.util.Set; public class InfoMediaManagerTest { private static final String TEST_PACKAGE_NAME = "com.test.packagename"; + private static final String TEST_PACKAGE_NAME_2 = "com.test.packagename2"; private static final String TEST_ID = "test_id"; private static final String TEST_ID_1 = "test_id_1"; private static final String TEST_ID_2 = "test_id_2"; @@ -308,7 +309,54 @@ public class InfoMediaManagerTest { } @Test - public void onRouteChanged_getAvailableRoutesWithPrefernceListExit_ordersRoutes() { + public void onRouteChanged_getAvailableRoutesWithPreferenceListExit_ordersRoutes() { + RouteListingPreference routeListingPreference = setUpPreferenceList(TEST_PACKAGE_NAME); + setUpSelectedRoutes(TEST_PACKAGE_NAME); + + final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>(); + final RoutingSessionInfo sessionInfo = mock(RoutingSessionInfo.class); + routingSessionInfos.add(sessionInfo); + + when(mRouterManager.getRoutingSessions(TEST_PACKAGE_NAME)).thenReturn(routingSessionInfos); + when(sessionInfo.getSelectedRoutes()).thenReturn(ImmutableList.of(TEST_ID)); + + setAvailableRoutesList(TEST_PACKAGE_NAME); + + mInfoMediaManager.mRouterManager = mRouterManager; + mInfoMediaManager.mMediaRouterCallback.onRouteListingPreferenceUpdated(TEST_PACKAGE_NAME, + routeListingPreference); + mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); + + assertThat(mInfoMediaManager.mMediaDevices).hasSize(3); + assertThat(mInfoMediaManager.mMediaDevices.get(0).getId()).isEqualTo(TEST_ID); + assertThat(mInfoMediaManager.mMediaDevices.get(1).getId()).isEqualTo(TEST_ID_4); + assertThat(mInfoMediaManager.mMediaDevices.get(1).isSuggestedDevice()).isTrue(); + assertThat(mInfoMediaManager.mMediaDevices.get(2).getId()).isEqualTo(TEST_ID_3); + } + + @Test + public void onRouteChanged_preferenceListUpdateWithDifferentPkg_notOrdersRoutes() { + RouteListingPreference routeListingPreference = setUpPreferenceList(TEST_PACKAGE_NAME_2); + setUpSelectedRoutes(TEST_PACKAGE_NAME); + + final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>(); + final RoutingSessionInfo sessionInfo = mock(RoutingSessionInfo.class); + routingSessionInfos.add(sessionInfo); + + when(mRouterManager.getRoutingSessions(TEST_PACKAGE_NAME)).thenReturn(routingSessionInfos); + when(sessionInfo.getSelectedRoutes()).thenReturn(ImmutableList.of(TEST_ID)); + + setAvailableRoutesList(TEST_PACKAGE_NAME); + mInfoMediaManager.mRouterManager = mRouterManager; + mInfoMediaManager.mMediaRouterCallback.onRouteListingPreferenceUpdated(TEST_PACKAGE_NAME_2, + routeListingPreference); + mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); + + assertThat(mInfoMediaManager.mMediaDevices).hasSize(1); + assertThat(mInfoMediaManager.mMediaDevices.get(0).getId()).isEqualTo(TEST_ID); + } + + private RouteListingPreference setUpPreferenceList(String packageName) { ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.UPSIDE_DOWN_CAKE); final List<RouteListingPreference.Item> preferenceItemList = new ArrayList<>(); @@ -324,57 +372,40 @@ public class InfoMediaManagerTest { RouteListingPreference routeListingPreference = new RouteListingPreference.Builder().setItems( preferenceItemList).setUseSystemOrdering(false).build(); - when(mRouterManager.getRouteListingPreference(TEST_PACKAGE_NAME)) + when(mRouterManager.getRouteListingPreference(packageName)) .thenReturn(routeListingPreference); + return routeListingPreference; + } + private void setUpSelectedRoutes(String packageName) { final List<MediaRoute2Info> selectedRoutes = new ArrayList<>(); final MediaRoute2Info info = mock(MediaRoute2Info.class); when(info.getId()).thenReturn(TEST_ID); - when(info.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME); + when(info.getClientPackageName()).thenReturn(packageName); when(info.isSystemRoute()).thenReturn(true); selectedRoutes.add(info); when(mRouterManager.getSelectedRoutes(any())).thenReturn(selectedRoutes); - - final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>(); - final RoutingSessionInfo sessionInfo = mock(RoutingSessionInfo.class); - routingSessionInfos.add(sessionInfo); - - when(mRouterManager.getRoutingSessions(TEST_PACKAGE_NAME)).thenReturn(routingSessionInfos); - when(sessionInfo.getSelectedRoutes()).thenReturn(ImmutableList.of(TEST_ID)); - - setAvailableRoutesList(); - - mInfoMediaManager.mRouterManager = mRouterManager; - mInfoMediaManager.mMediaRouterCallback.onRouteListingPreferenceUpdated(TEST_PACKAGE_NAME, - routeListingPreference); - mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); - - assertThat(mInfoMediaManager.mMediaDevices).hasSize(3); - assertThat(mInfoMediaManager.mMediaDevices.get(0).getId()).isEqualTo(TEST_ID); - assertThat(mInfoMediaManager.mMediaDevices.get(1).getId()).isEqualTo(TEST_ID_4); - assertThat(mInfoMediaManager.mMediaDevices.get(1).isSuggestedDevice()).isTrue(); - assertThat(mInfoMediaManager.mMediaDevices.get(2).getId()).isEqualTo(TEST_ID_3); } - private List<MediaRoute2Info> setAvailableRoutesList() { + private List<MediaRoute2Info> setAvailableRoutesList(String packageName) { final List<MediaRoute2Info> availableRoutes = new ArrayList<>(); final MediaRoute2Info availableInfo1 = mock(MediaRoute2Info.class); when(availableInfo1.getId()).thenReturn(TEST_ID_2); - when(availableInfo1.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME); + when(availableInfo1.getClientPackageName()).thenReturn(packageName); when(availableInfo1.getType()).thenReturn(TYPE_REMOTE_TV); availableRoutes.add(availableInfo1); final MediaRoute2Info availableInfo2 = mock(MediaRoute2Info.class); when(availableInfo2.getId()).thenReturn(TEST_ID_3); - when(availableInfo2.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME); + when(availableInfo2.getClientPackageName()).thenReturn(packageName); availableRoutes.add(availableInfo2); final MediaRoute2Info availableInfo3 = mock(MediaRoute2Info.class); when(availableInfo3.getId()).thenReturn(TEST_ID_4); - when(availableInfo3.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME); + when(availableInfo3.getClientPackageName()).thenReturn(packageName); availableRoutes.add(availableInfo3); - when(mRouterManager.getAvailableRoutes(TEST_PACKAGE_NAME)).thenReturn( + when(mRouterManager.getAvailableRoutes(packageName)).thenReturn( availableRoutes); return availableRoutes; diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 0a9a1842dd77..0aa121dfcc49 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -996,7 +996,6 @@ android:name=".notetask.shortcut.LaunchNoteTaskActivity" android:exported="true" android:excludeFromRecents="true" - android:resizeableActivity="false" android:theme="@android:style/Theme.NoDisplay" > <intent-filter> @@ -1012,7 +1011,6 @@ android:exported="false" android:enabled="true" android:excludeFromRecents="true" - android:resizeableActivity="false" android:theme="@android:style/Theme.NoDisplay" /> <activity diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index 25272ae097a1..ccfbaf1da7b6 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -215,7 +215,7 @@ constructor( debugLog { "onShowNoteTask - opened as app bubble: $info" } } is NoteTaskLaunchMode.Activity -> { - if (activityManager.isInForeground(info.packageName)) { + if (info.isKeyguardLocked && activityManager.isInForeground(info.packageName)) { // Force note task into background by calling home. val intent = createHomeIntent() context.startActivityAsUser(intent, user) diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEntryPoint.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEntryPoint.kt index fae325cc3147..442000281862 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEntryPoint.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEntryPoint.kt @@ -25,12 +25,14 @@ import com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity * An entry point represents where the note task has ben called from. In rare cases, it may * represent a "re-entry" (i.e., [APP_CLIPS]). */ -enum class -NoteTaskEntryPoint { +enum class NoteTaskEntryPoint { /** @see [LaunchNoteTaskActivity] */ WIDGET_PICKER_SHORTCUT, + /** @see [LaunchNoteTaskActivity] */ + WIDGET_PICKER_SHORTCUT_IN_MULTI_WINDOW_MODE, + /** @see [NoteTaskQuickAffordanceConfig] */ QUICK_AFFORDANCE, diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEventLogger.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEventLogger.kt index 48a5933a6030..a79057e5464b 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEventLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEventLogger.kt @@ -22,6 +22,8 @@ import com.android.systemui.notetask.NoteTaskEntryPoint.KEYBOARD_SHORTCUT import com.android.systemui.notetask.NoteTaskEntryPoint.QUICK_AFFORDANCE import com.android.systemui.notetask.NoteTaskEntryPoint.TAIL_BUTTON import com.android.systemui.notetask.NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT +import com.android.systemui.notetask.NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT_IN_MULTI_WINDOW_MODE +import com.android.systemui.notetask.NoteTaskEventLogger.NoteTaskUiEvent import com.android.systemui.notetask.NoteTaskEventLogger.NoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE import com.android.systemui.notetask.NoteTaskEventLogger.NoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT import com.android.systemui.notetask.NoteTaskEventLogger.NoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON @@ -41,40 +43,45 @@ class NoteTaskEventLogger @Inject constructor(private val uiEventLogger: UiEvent /** Logs a [NoteTaskInfo] as an **open** [NoteTaskUiEvent], including package name and uid. */ fun logNoteTaskOpened(info: NoteTaskInfo) { val event = - when (info.entryPoint) { - TAIL_BUTTON -> { - if (info.isKeyguardLocked) { - NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED - } else { - NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON + when (info.entryPoint) { + TAIL_BUTTON -> { + if (info.isKeyguardLocked) { + NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED + } else { + NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON + } } + + WIDGET_PICKER_SHORTCUT, + WIDGET_PICKER_SHORTCUT_IN_MULTI_WINDOW_MODE -> NOTE_OPENED_VIA_SHORTCUT + + QUICK_AFFORDANCE -> NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE + APP_CLIPS, + KEYBOARD_SHORTCUT, + null -> return } - WIDGET_PICKER_SHORTCUT -> NOTE_OPENED_VIA_SHORTCUT - QUICK_AFFORDANCE -> NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE - APP_CLIPS -> return - KEYBOARD_SHORTCUT -> return - null -> return - } uiEventLogger.log(event, info.uid, info.packageName) } /** Logs a [NoteTaskInfo] as a **closed** [NoteTaskUiEvent], including package name and uid. */ fun logNoteTaskClosed(info: NoteTaskInfo) { val event = - when (info.entryPoint) { - TAIL_BUTTON -> { - if (info.isKeyguardLocked) { - NoteTaskUiEvent.NOTE_CLOSED_VIA_STYLUS_TAIL_BUTTON_LOCKED - } else { - NoteTaskUiEvent.NOTE_CLOSED_VIA_STYLUS_TAIL_BUTTON + when (info.entryPoint) { + TAIL_BUTTON -> { + if (info.isKeyguardLocked) { + NoteTaskUiEvent.NOTE_CLOSED_VIA_STYLUS_TAIL_BUTTON_LOCKED + } else { + NoteTaskUiEvent.NOTE_CLOSED_VIA_STYLUS_TAIL_BUTTON + } } + + WIDGET_PICKER_SHORTCUT, + WIDGET_PICKER_SHORTCUT_IN_MULTI_WINDOW_MODE, + QUICK_AFFORDANCE, + APP_CLIPS, + KEYBOARD_SHORTCUT, + null -> return } - WIDGET_PICKER_SHORTCUT -> return - QUICK_AFFORDANCE -> return - APP_CLIPS -> return - KEYBOARD_SHORTCUT -> return - null -> return - } uiEventLogger.log(event, info.uid, info.packageName) } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfo.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfo.kt index a75834760d30..269eb870686c 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfo.kt @@ -16,6 +16,7 @@ package com.android.systemui.notetask import android.os.UserHandle +import com.android.systemui.notetask.NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT_IN_MULTI_WINDOW_MODE /** Contextual information required to launch a Note Task by [NoteTaskController]. */ data class NoteTaskInfo( @@ -27,7 +28,7 @@ data class NoteTaskInfo( ) { val launchMode: NoteTaskLaunchMode = - if (isKeyguardLocked) { + if (isKeyguardLocked || entryPoint == WIDGET_PICKER_SHORTCUT_IN_MULTI_WINDOW_MODE) { NoteTaskLaunchMode.Activity } else { NoteTaskLaunchMode.AppBubble diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt index 441b9f5d0181..754c3650a5ed 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt @@ -51,7 +51,8 @@ internal object NoteTaskRoleManagerExt { val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget) return ShortcutInfo.Builder(context, NoteTaskController.SHORTCUT_ID) - .setIntent(LaunchNoteTaskActivity.newIntent(context = context)) + .setIntent(LaunchNoteTaskActivity.createIntent(context)) + .setActivity(LaunchNoteTaskActivity.createComponent(context)) .setShortLabel(context.getString(R.string.note_task_button_label)) .setLongLived(true) .setIcon(icon) diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt index 8ca13b9776bb..7ef149de2794 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt @@ -16,6 +16,7 @@ package com.android.systemui.notetask.shortcut +import android.content.ComponentName import android.content.Context import android.content.Intent import android.os.Bundle @@ -72,7 +73,13 @@ constructor( controller.startNoteTaskProxyActivityForUser(mainUser) } } else { - controller.showNoteTask(entryPoint = NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT) + val entryPoint = + if (isInMultiWindowMode) { + NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT_IN_MULTI_WINDOW_MODE + } else { + NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT + } + controller.showNoteTask(entryPoint) } finish() } @@ -80,11 +87,14 @@ constructor( companion object { /** Creates a new [Intent] set to start [LaunchNoteTaskActivity]. */ - fun newIntent(context: Context): Intent { - return Intent(context, LaunchNoteTaskActivity::class.java).apply { + fun createIntent(context: Context): Intent = + Intent(context, LaunchNoteTaskActivity::class.java).apply { // Intent's action must be set in shortcuts, or an exception will be thrown. action = Intent.ACTION_CREATE_NOTE } - } + + /** Creates a new [ComponentName] for [LaunchNoteTaskActivity]. */ + fun createComponent(context: Context): ComponentName = + ComponentName(context, LaunchNoteTaskActivity::class.java) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index 23b5241b79ef..314566eaf8d0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -633,12 +633,17 @@ class HeadsUpCoordinator @Inject constructor( mFSIUpdateCandidates.removeAll(toRemoveForFSI) } - /** When an action is pressed on a notification, end HeadsUp lifetime extension. */ + /** + * When an action is pressed on a notification, make sure we don't lifetime-extend it in the + * future by informing the HeadsUpManager, and make sure we don't keep lifetime-extending it if + * we already are. + * + * @see HeadsUpManager.setUserActionMayIndirectlyRemove + * @see HeadsUpManager.canRemoveImmediately + */ private val mActionPressListener = Consumer<NotificationEntry> { entry -> - if (mNotifsExtendingLifetime.contains(entry)) { - val removeInMillis = mHeadsUpManager.getEarliestRemovalTime(entry.key) - mExecutor.executeDelayed({ endNotifLifetimeExtensionIfExtended(entry) }, removeInMillis) - } + mHeadsUpManager.setUserActionMayIndirectlyRemove(entry) + mExecutor.execute { endNotifLifetimeExtensionIfExtended(entry) } } private val mLifetimeExtender = object : NotifLifetimeExtender { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java index ed8050a031d8..92a78541d744 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java @@ -393,6 +393,31 @@ public abstract class HeadsUpManager extends AlertingNotificationManager { } } + /** + * Notes that the user took an action on an entry that might indirectly cause the system or the + * app to remove the notification. + * + * @param entry the entry that might be indirectly removed by the user's action + * + * @see com.android.systemui.statusbar.notification.collection.coordinator.HeadsUpCoordinator#mActionPressListener + * @see #canRemoveImmediately(String) + */ + public void setUserActionMayIndirectlyRemove(@NonNull NotificationEntry entry) { + HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey()); + if (headsUpEntry != null) { + headsUpEntry.userActionMayIndirectlyRemove = true; + } + } + + @Override + public boolean canRemoveImmediately(@NonNull String key) { + HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); + if (headsUpEntry != null && headsUpEntry.userActionMayIndirectlyRemove) { + return true; + } + return super.canRemoveImmediately(key); + } + @NonNull @Override protected HeadsUpEntry createAlertEntry() { @@ -421,6 +446,8 @@ public abstract class HeadsUpManager extends AlertingNotificationManager { */ protected class HeadsUpEntry extends AlertEntry { public boolean remoteInputActive; + public boolean userActionMayIndirectlyRemove; + protected boolean expanded; protected boolean wasUnpinned; diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 109c1cfdcf0f..b848d2e84faf 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -930,7 +930,6 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, showRingerDrawer(); } }); - updateSelectedRingerContainerDescription(mIsRingerDrawerOpen); mRingerDrawerVibrate.setOnClickListener( new RingerDrawerItemClickListener(RINGER_MODE_VIBRATE)); @@ -993,18 +992,6 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, : 0; } - @VisibleForTesting String getSelectedRingerContainerDescription() { - return mSelectedRingerContainer.getContentDescription().toString(); - } - - @VisibleForTesting void toggleRingerDrawer(boolean show) { - if (show) { - showRingerDrawer(); - } else { - hideRingerDrawer(); - } - } - /** Animates in the ringer drawer. */ private void showRingerDrawer() { if (mIsRingerDrawerOpen) { @@ -1082,7 +1069,12 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, .start(); } - updateSelectedRingerContainerDescription(true); + // When the ringer drawer is open, tapping the currently selected ringer will set the ringer + // to the current ringer mode. Change the content description to that, instead of the 'tap + // to change ringer mode' default. + mSelectedRingerContainer.setContentDescription( + mContext.getString(getStringDescriptionResourceForRingerMode( + mState.ringerModeInternal))); mIsRingerDrawerOpen = true; } @@ -1128,38 +1120,14 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, .translationY(0f) .start(); - updateSelectedRingerContainerDescription(false); + // When the drawer is closed, tapping the selected ringer drawer will open it, allowing the + // user to change the ringer. + mSelectedRingerContainer.setContentDescription( + mContext.getString(R.string.volume_ringer_change)); mIsRingerDrawerOpen = false; } - - /** - * @param open false to set the description when drawer is closed - */ - private void updateSelectedRingerContainerDescription(boolean open) { - if (mState == null) return; - - String currentMode = mContext.getString(getStringDescriptionResourceForRingerMode( - mState.ringerModeInternal)); - String tapToSelect; - - if (open) { - // When the ringer drawer is open, tapping the currently selected ringer will set the - // ringer to the current ringer mode. Change the content description to that, instead of - // the 'tap to change ringer mode' default. - tapToSelect = ""; - - } else { - // When the drawer is closed, tapping the selected ringer drawer will open it, allowing - // the user to change the ringer. The user needs to know that, and also the current mode - currentMode += ", "; - tapToSelect = mContext.getString(R.string.volume_ringer_change); - } - - mSelectedRingerContainer.setContentDescription(currentMode + tapToSelect); - } - private void initSettingsH(int lockTaskModeState) { if (mSettingsView != null) { mSettingsView.setVisibility( @@ -1735,7 +1703,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, }); } - @VisibleForTesting int getStringDescriptionResourceForRingerMode(int mode) { + private int getStringDescriptionResourceForRingerMode(int mode) { switch (mode) { case RINGER_MODE_SILENT: return R.string.volume_ringer_status_silent; @@ -1817,7 +1785,6 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, updateVolumeRowH(row); } updateRingerH(); - updateSelectedRingerContainerDescription(mIsRingerDrawerOpen); mWindow.setTitle(composeWindowTitle()); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt index e99f8b6aa47b..0954f6f0ffaf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -72,6 +72,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock +import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.doNothing import org.mockito.Mockito.never import org.mockito.Mockito.spy @@ -227,12 +228,15 @@ internal class NoteTaskControllerTest : SysuiTestCase() { // region showNoteTask @Test - fun showNoteTask_keyguardIsLocked_shouldStartActivityAndLogUiEvent() { - val expectedInfo = NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = true) + fun showNoteTaskAsUser_keyguardIsLocked_shouldStartActivityWithExpectedUserAndLogUiEvent() { + val user10 = UserHandle.of(/* userId= */ 10) + val expectedInfo = + NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = true, user = user10) whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) - createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!) + createNoteTaskController() + .showNoteTaskAsUser(entryPoint = expectedInfo.entryPoint!!, user = user10) val intentCaptor = argumentCaptor<Intent>() val userCaptor = argumentCaptor<UserHandle>() @@ -245,21 +249,18 @@ internal class NoteTaskControllerTest : SysuiTestCase() { hasFlags(FLAG_ACTIVITY_NEW_DOCUMENT) extras().bool(EXTRA_USE_STYLUS_MODE).isTrue() } - assertThat(userCaptor.value).isEqualTo(userTracker.userHandle) + assertThat(userCaptor.value).isEqualTo(user10) verify(eventLogger).logNoteTaskOpened(expectedInfo) verifyZeroInteractions(bubbles) } @Test - fun showNoteTaskWithUser_keyguardIsLocked_shouldStartActivityWithExpectedUserAndLogUiEvent() { - val user10 = UserHandle.of(/* userId= */ 10) - val expectedInfo = - NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = true, user = user10) + fun showNoteTask_keyguardIsLocked_notesIsClosed_shouldStartActivityAndLogUiEvent() { + val expectedInfo = NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = true) whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) - createNoteTaskController() - .showNoteTaskAsUser(entryPoint = expectedInfo.entryPoint!!, user = user10) + createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!) val intentCaptor = argumentCaptor<Intent>() val userCaptor = argumentCaptor<UserHandle>() @@ -272,7 +273,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { hasFlags(FLAG_ACTIVITY_NEW_DOCUMENT) extras().bool(EXTRA_USE_STYLUS_MODE).isTrue() } - assertThat(userCaptor.value).isEqualTo(user10) + assertThat(userCaptor.value).isEqualTo(userTracker.userHandle) verify(eventLogger).logNoteTaskOpened(expectedInfo) verifyZeroInteractions(bubbles) } @@ -301,15 +302,31 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } @Test - fun showNoteTask_keyguardIsUnlocked_shouldStartBubblesWithoutLoggingUiEvent() { + fun showNoteTask_keyguardIsUnlocked_noteIsClosed_shouldStartBubblesWithoutLoggingUiEvent() { + val expectedInfo = NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = false) + whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) + whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) + + createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!) + + // Context package name used to create bubble icon from drawable resource id + verify(context, atLeastOnce()).packageName + verifyNoteTaskOpenInBubbleInUser(userTracker.userHandle) + verifyZeroInteractions(eventLogger) + } + + @Test + fun showNoteTask_keyguardIsUnlocked_noteIsOpen_shouldStartBubblesWithoutLoggingUiEvent() { val expectedInfo = NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = false) whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) + whenever(activityManager.getRunningTasks(anyInt())) + .thenReturn(listOf(NOTE_RUNNING_TASK_INFO)) createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!) // Context package name used to create bubble icon from drawable resource id - verify(context).packageName + verify(context, atLeastOnce()).packageName verifyNoteTaskOpenInBubbleInUser(userTracker.userHandle) verifyZeroInteractions(eventLogger) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt index 34354504abf0..24f39d187f88 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt @@ -16,37 +16,47 @@ package com.android.systemui.notetask import android.os.UserHandle -import android.test.suitebuilder.annotation.SmallTest -import androidx.test.runner.AndroidJUnit4 +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.notetask.NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT_IN_MULTI_WINDOW_MODE import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith /** atest SystemUITests:NoteTaskInfoTest */ @SmallTest -@RunWith(AndroidJUnit4::class) +@RunWith(AndroidTestingRunner::class) internal class NoteTaskInfoTest : SysuiTestCase() { - private fun createNoteTaskInfo(): NoteTaskInfo = - NoteTaskInfo(packageName = NOTES_PACKAGE_NAME, uid = NOTES_UID, UserHandle.of(0)) - @Test fun launchMode_keyguardLocked_launchModeActivity() { - val underTest = createNoteTaskInfo().copy(isKeyguardLocked = true) + val underTest = DEFAULT_INFO.copy(isKeyguardLocked = true) assertThat(underTest.launchMode).isEqualTo(NoteTaskLaunchMode.Activity) } @Test - fun launchMode_keyguardUnlocked_launchModeActivity() { - val underTest = createNoteTaskInfo().copy(isKeyguardLocked = false) + fun launchMode_multiWindowMode_launchModeActivity() { + val underTest = DEFAULT_INFO.copy(entryPoint = WIDGET_PICKER_SHORTCUT_IN_MULTI_WINDOW_MODE) + + assertThat(underTest.launchMode).isEqualTo(NoteTaskLaunchMode.Activity) + } + + @Test + fun launchMode_keyguardUnlocked_launchModeAppBubble() { + val underTest = DEFAULT_INFO.copy(isKeyguardLocked = false) assertThat(underTest.launchMode).isEqualTo(NoteTaskLaunchMode.AppBubble) } private companion object { - const val NOTES_PACKAGE_NAME = "com.android.note.app" - const val NOTES_UID = 123456 + + val DEFAULT_INFO = + NoteTaskInfo( + packageName = "com.android.note.app", + uid = 123456, + user = UserHandle.of(0), + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index 283efe263f04..257cc5b1b85c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -221,16 +221,35 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { } @Test - fun hunExtensionCancelledWhenHunActionPressed() { + fun actionPressCancelsExistingLifetimeExtension() { whenever(headsUpManager.isSticky(anyString())).thenReturn(true) addHUN(entry) + whenever(headsUpManager.canRemoveImmediately(anyString())).thenReturn(false) whenever(headsUpManager.getEarliestRemovalTime(anyString())).thenReturn(1000L) - assertTrue(notifLifetimeExtender.maybeExtendLifetime(entry, 0)) + assertTrue(notifLifetimeExtender.maybeExtendLifetime(entry, /* reason = */ 0)) + actionPressListener.accept(entry) - executor.advanceClockToLast() executor.runAllReady() - verify(headsUpManager, times(1)).removeNotification(eq(entry.key), eq(true)) + verify(endLifetimeExtension, times(1)).onEndLifetimeExtension(notifLifetimeExtender, entry) + + collectionListener.onEntryRemoved(entry, /* reason = */ 0) + verify(headsUpManager, times(1)).removeNotification(eq(entry.key), any()) + } + + @Test + fun actionPressPreventsFutureLifetimeExtension() { + whenever(headsUpManager.isSticky(anyString())).thenReturn(true) + addHUN(entry) + + actionPressListener.accept(entry) + verify(headsUpManager, times(1)).setUserActionMayIndirectlyRemove(entry) + + whenever(headsUpManager.canRemoveImmediately(anyString())).thenReturn(true) + assertFalse(notifLifetimeExtender.maybeExtendLifetime(entry, 0)) + + collectionListener.onEntryRemoved(entry, /* reason = */ 0) + verify(headsUpManager, times(1)).removeNotification(eq(entry.key), any()) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index b0c14868822c..85b1ec108e98 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -89,6 +89,7 @@ import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController import org.junit.Assert; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -102,6 +103,7 @@ import java.util.ArrayList; /** * Tests for {@link NotificationStackScrollLayout}. */ +@Ignore("b/255552856") @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper @@ -373,6 +375,20 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test + public void testAppearFractionCalculation() { + // appear start position + when(mNotificationShelf.getIntrinsicHeight()).thenReturn(100); + // because it's the same as shelf height, appear start position equals shelf height + mStackScroller.mStatusBarHeight = 100; + // appear end position + when(mEmptyShadeView.getHeight()).thenReturn(200); + + assertEquals(0f, mStackScroller.calculateAppearFraction(100)); + assertEquals(1f, mStackScroller.calculateAppearFraction(200)); + assertEquals(0.5f, mStackScroller.calculateAppearFraction(150)); + } + + @Test public void testAppearFractionCalculationIsNotNegativeWhenShelfBecomesSmaller() { // this situation might occur if status bar height is defined in pixels while shelf height // in dp and screen density changes - appear start position @@ -574,7 +590,6 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { @Test public void testReInflatesFooterViews() { - when(mEmptyShadeView.getTextResource()).thenReturn(R.string.empty_shade_text); clearInvocations(mStackScroller); mStackScroller.reinflateViews(); verify(mStackScroller).setFooterView(any()); @@ -901,7 +916,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { mStackScroller.setHasFilteredOutSeenNotifications(true); mStackScroller.updateEmptyShadeView(true, false); - verify(mEmptyShadeView).setFooterText(not(eq(0))); + verify(mEmptyShadeView).setFooterText(not(0)); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java index 487d26d21824..a797e032a353 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java @@ -20,7 +20,6 @@ import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertNotSame; import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; @@ -346,4 +345,17 @@ public class HeadsUpManagerTest extends AlertingNotificationManagerTest { assertEquals(HeadsUpManager.NotificationPeekEvent.NOTIFICATION_PEEK.getId(), mUiEventLoggerFake.eventId(0)); } + + @Test + public void testSetUserActionMayIndirectlyRemove() { + NotificationEntry notifEntry = new NotificationEntryBuilder() + .setSbn(createNewNotification(/* id= */ 0)) + .build(); + + mHeadsUpManager.showNotification(notifEntry); + assertFalse(mHeadsUpManager.canRemoveImmediately(notifEntry.getKey())); + + mHeadsUpManager.setUserActionMayIndirectlyRemove(notifEntry); + assertTrue(mHeadsUpManager.canRemoveImmediately(notifEntry.getKey())); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index 2ea6368ab402..8f725bebfb16 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -16,16 +16,11 @@ package com.android.systemui.volume; -import static android.media.AudioManager.RINGER_MODE_NORMAL; -import static android.media.AudioManager.RINGER_MODE_SILENT; -import static android.media.AudioManager.RINGER_MODE_VIBRATE; - import static com.android.systemui.volume.Events.DISMISS_REASON_UNKNOWN; import static com.android.systemui.volume.Events.SHOW_REASON_UNKNOWN; import static com.android.systemui.volume.VolumeDialogControllerImpl.STREAMS; import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotSame; import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -298,7 +293,7 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Test public void testSelectVibrateFromDrawer() { final State initialUnsetState = new State(); - initialUnsetState.ringerModeInternal = RINGER_MODE_NORMAL; + initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL; mDialog.onStateChangedH(initialUnsetState); mActiveRinger.performClick(); @@ -312,7 +307,7 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Test public void testSelectMuteFromDrawer() { final State initialUnsetState = new State(); - initialUnsetState.ringerModeInternal = RINGER_MODE_NORMAL; + initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL; mDialog.onStateChangedH(initialUnsetState); mActiveRinger.performClick(); @@ -334,7 +329,7 @@ public class VolumeDialogImplTest extends SysuiTestCase { // Make sure we've actually changed the ringer mode. verify(mVolumeDialogController, times(1)).setRingerMode( - RINGER_MODE_NORMAL, false); + AudioManager.RINGER_MODE_NORMAL, false); } /** @@ -516,85 +511,6 @@ public class VolumeDialogImplTest extends SysuiTestCase { } } - private enum RingerDrawerState {INIT, OPEN, CLOSE} - - @Test - public void ringerModeNormal_ringerContainerDescribesItsState() { - assertRingerContainerDescribesItsState(RINGER_MODE_NORMAL, RingerDrawerState.INIT); - } - - @Test - public void ringerModeSilent_ringerContainerDescribesItsState() { - assertRingerContainerDescribesItsState(RINGER_MODE_SILENT, RingerDrawerState.INIT); - } - - @Test - public void ringerModeVibrate_ringerContainerDescribesItsState() { - assertRingerContainerDescribesItsState(RINGER_MODE_VIBRATE, RingerDrawerState.INIT); - } - - @Test - public void ringerModeNormal_openDrawer_ringerContainerDescribesItsState() { - assertRingerContainerDescribesItsState(RINGER_MODE_NORMAL, RingerDrawerState.OPEN); - } - - @Test - public void ringerModeSilent_openDrawer_ringerContainerDescribesItsState() { - assertRingerContainerDescribesItsState(RINGER_MODE_SILENT, RingerDrawerState.OPEN); - } - - @Test - public void ringerModeVibrate_openDrawer_ringerContainerDescribesItsState() { - assertRingerContainerDescribesItsState(RINGER_MODE_VIBRATE, RingerDrawerState.OPEN); - } - - @Test - public void ringerModeNormal_closeDrawer_ringerContainerDescribesItsState() { - assertRingerContainerDescribesItsState(RINGER_MODE_NORMAL, RingerDrawerState.CLOSE); - } - - @Test - public void ringerModeSilent_closeDrawer_ringerContainerDescribesItsState() { - assertRingerContainerDescribesItsState(RINGER_MODE_SILENT, RingerDrawerState.CLOSE); - } - - @Test - public void ringerModeVibrate_closeDrawer_ringerContainerDescribesItsState() { - assertRingerContainerDescribesItsState(RINGER_MODE_VIBRATE, RingerDrawerState.CLOSE); - } - - /** - * The content description should include ringer state, and the correct one. - */ - private void assertRingerContainerDescribesItsState(int ringerMode, - RingerDrawerState drawerState) { - State state = createShellState(); - state.ringerModeInternal = ringerMode; - mDialog.onStateChangedH(state); - - mDialog.show(SHOW_REASON_UNKNOWN); - - if (drawerState != RingerDrawerState.INIT) { - // in both cases we first open the drawer - mDialog.toggleRingerDrawer(true); - - if (drawerState == RingerDrawerState.CLOSE) { - mDialog.toggleRingerDrawer(false); - } - } - - String ringerContainerDescription = mDialog.getSelectedRingerContainerDescription(); - String ringerDescription = mContext.getString( - mDialog.getStringDescriptionResourceForRingerMode(ringerMode)); - - if (drawerState == RingerDrawerState.OPEN) { - assertEquals(ringerDescription, ringerContainerDescription); - } else { - assertNotSame(ringerDescription, ringerContainerDescription); - assertTrue(ringerContainerDescription.startsWith(ringerDescription)); - } - } - @After public void teardown() { if (mDialog != null) { diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 6b55d7ed4d56..cd2f844294e3 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -987,8 +987,12 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub "Virtual device doesn't have a virtual display with ID " + displayId); } - releaseOwnedVirtualDisplayResources(virtualDisplayWrapper); - + final long ident = Binder.clearCallingIdentity(); + try { + releaseOwnedVirtualDisplayResources(virtualDisplayWrapper); + } finally { + Binder.restoreCallingIdentity(ident); + } } /** diff --git a/services/core/java/com/android/server/WallpaperUpdateReceiver.java b/services/core/java/com/android/server/WallpaperUpdateReceiver.java index 2812233815a6..99178920cc52 100644 --- a/services/core/java/com/android/server/WallpaperUpdateReceiver.java +++ b/services/core/java/com/android/server/WallpaperUpdateReceiver.java @@ -88,7 +88,7 @@ public class WallpaperUpdateReceiver extends BroadcastReceiver { } else { //live wallpaper ComponentName currCN = info.getComponent(); - ComponentName defaultCN = WallpaperManager.getCmfDefaultWallpaperComponent(context); + ComponentName defaultCN = WallpaperManager.getDefaultWallpaperComponent(context); if (!currCN.equals(defaultCN)) { return true; } diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 1f8ff73b2d5d..fc72a771fa9c 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -2214,6 +2214,7 @@ import java.util.concurrent.atomic.AtomicBoolean; mDeviceInventory.removePreferredDevicesForStrategyInt(mAccessibilityStrategyId); } mDeviceInventory.applyConnectedDevicesRoles(); + mDeviceInventory.reapplyExternalDevicesRoles(); } else { mDeviceInventory.setPreferredDevicesForStrategyInt( mCommunicationStrategyId, Arrays.asList(preferredCommunicationDevice)); diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index d1cae490a31d..219dda398ebf 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -361,23 +361,34 @@ public class AudioDeviceInventory { AudioSystem.DEVICE_STATE_AVAILABLE, di.mDeviceCodecFormat); } - mAppliedStrategyRoles.clear(); mAppliedStrategyRolesInt.clear(); - mAppliedPresetRoles.clear(); mAppliedPresetRolesInt.clear(); applyConnectedDevicesRoles_l(); } + reapplyExternalDevicesRoles(); + } + + /*package*/ void reapplyExternalDevicesRoles() { + synchronized (mDevicesLock) { + mAppliedStrategyRoles.clear(); + mAppliedPresetRoles.clear(); + } synchronized (mPreferredDevices) { mPreferredDevices.forEach((strategy, devices) -> { - setPreferredDevicesForStrategy(strategy, devices); }); + setPreferredDevicesForStrategy(strategy, devices); + }); } synchronized (mNonDefaultDevices) { mNonDefaultDevices.forEach((strategy, devices) -> { addDevicesRoleForStrategy(strategy, AudioSystem.DEVICE_ROLE_DISABLED, - devices, false /* internal */); }); + devices, false /* internal */); + }); } synchronized (mPreferredDevicesForCapturePreset) { - // TODO: call audiosystem to restore + mPreferredDevicesForCapturePreset.forEach((capturePreset, devices) -> { + setDevicesRoleForCapturePreset( + capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices); + }); } } @@ -1163,6 +1174,7 @@ public class AudioDeviceInventory { return mAudioSystem.removeDevicesRoleForStrategy(s, r, d); }); purgeRoles(mAppliedPresetRolesInt, (p, r, d) -> { return mAudioSystem.removeDevicesRoleForCapturePreset(p, r, d); }); + reapplyExternalDevicesRoles(); } @GuardedBy("mDevicesLock") diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index f2d1357ac1b5..aaf13ebeff2a 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -3714,11 +3714,16 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile if (parser.getName().equals(TAG_PERMISSIONS)) { final LegacyPermissionState legacyState; if (ps.hasSharedUser()) { - legacyState = getSettingLPr(ps.getSharedUserAppId()).getLegacyPermissionState(); + final SettingBase sharedUserSettings = getSettingLPr( + ps.getSharedUserAppId()); + legacyState = sharedUserSettings != null + ? sharedUserSettings.getLegacyPermissionState() : null; } else { legacyState = ps.getLegacyPermissionState(); } - readInstallPermissionsLPr(parser, legacyState, users); + if (legacyState != null) { + readInstallPermissionsLPr(parser, legacyState, users); + } } else if (parser.getName().equals(TAG_USES_STATIC_LIB)) { readUsesStaticLibLPw(parser, ps); } else if (parser.getName().equals(TAG_USES_SDK_LIB)) { diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index a020728cc85e..5b3514c01f9f 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -2547,10 +2547,7 @@ public class ShortcutService extends IShortcutService.Stub { enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APP_PREDICTIONS, "getShareTargets"); final ComponentName chooser = injectChooserActivity(); - final String pkg = (chooser != null - && mPackageManagerInternal.getComponentEnabledSetting(chooser, - injectBinderCallingUid(), userId) == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) - ? chooser.getPackageName() : mContext.getPackageName(); + final String pkg = chooser != null ? chooser.getPackageName() : mContext.getPackageName(); synchronized (mLock) { throwIfUserLockedL(userId); final List<ShortcutManager.ShareShortcutInfo> shortcutInfoList = new ArrayList<>(); diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java index 3e7ae331d705..2499529f0fc0 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -141,6 +141,7 @@ import com.android.server.pm.parsing.pkg.AndroidPackageUtils; import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.PackageState; import com.android.server.pm.pkg.PackageStateInternal; +import com.android.server.pm.pkg.SharedUserApi; import com.android.server.pm.pkg.component.ComponentMutateUtils; import com.android.server.pm.pkg.component.ParsedPermission; import com.android.server.pm.pkg.component.ParsedPermissionGroup; @@ -4538,8 +4539,13 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt final int appId = ps.getAppId(); final LegacyPermissionState legacyState; if (ps.hasSharedUser()) { - legacyState = mPackageManagerInt.getSharedUserApi( - ps.getSharedUserAppId()).getSharedUserLegacyPermissionState(); + final int sharedUserId = ps.getSharedUserAppId(); + SharedUserApi sharedUserApi = mPackageManagerInt.getSharedUserApi(sharedUserId); + if (sharedUserApi == null) { + Slog.wtf(TAG, "Missing shared user Api for " + sharedUserId); + return; + } + legacyState = sharedUserApi.getSharedUserLegacyPermissionState(); } else { legacyState = ps.getLegacyPermissionState(); } @@ -4584,8 +4590,13 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt ps.setInstallPermissionsFixed(false); final LegacyPermissionState legacyState; if (ps.hasSharedUser()) { - legacyState = mPackageManagerInt.getSharedUserApi( - ps.getSharedUserAppId()).getSharedUserLegacyPermissionState(); + final int sharedUserId = ps.getSharedUserAppId(); + SharedUserApi sharedUserApi = mPackageManagerInt.getSharedUserApi(sharedUserId); + if (sharedUserApi == null) { + Slog.wtf(TAG, "Missing shared user Api for " + sharedUserId); + return; + } + legacyState = sharedUserApi.getSharedUserLegacyPermissionState(); } else { legacyState = ps.getLegacyPermissionState(); } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperData.java b/services/core/java/com/android/server/wallpaper/WallpaperData.java index 9ff6a0d7e6ee..d87fca4d3c71 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperData.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperData.java @@ -133,16 +133,14 @@ class WallpaperData { */ final Rect cropHint = new Rect(0, 0, 0, 0); - WallpaperData(int userId, File wallpaperDir, String inputFileName, String cropFileName) { - this.userId = userId; - wallpaperFile = new File(wallpaperDir, inputFileName); - cropFile = new File(wallpaperDir, cropFileName); - } - WallpaperData(int userId, @SetWallpaperFlags int wallpaperType) { - this(userId, getWallpaperDir(userId), - (wallpaperType == FLAG_LOCK) ? WALLPAPER_LOCK_ORIG : WALLPAPER, - (wallpaperType == FLAG_LOCK) ? WALLPAPER_LOCK_CROP : WALLPAPER_CROP); + this.userId = userId; + this.mWhich = wallpaperType; + File wallpaperDir = getWallpaperDir(userId); + String wallpaperFileName = (wallpaperType == FLAG_LOCK) ? WALLPAPER_LOCK_ORIG : WALLPAPER; + String cropFileName = (wallpaperType == FLAG_LOCK) ? WALLPAPER_LOCK_CROP : WALLPAPER_CROP; + this.wallpaperFile = new File(wallpaperDir, wallpaperFileName); + this.cropFile = new File(wallpaperDir, cropFileName); } /** diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index a079875a23e4..9e9b3444006c 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -1588,7 +1588,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub mShuttingDown = false; mImageWallpaper = ComponentName.unflattenFromString( context.getResources().getString(R.string.image_wallpaper_component)); - mDefaultWallpaperComponent = WallpaperManager.getCmfDefaultWallpaperComponent(context); + mDefaultWallpaperComponent = WallpaperManager.getDefaultWallpaperComponent(context); mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class); mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); mIPackageManager = AppGlobals.getPackageManager(); diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java index 1a322ff0196f..5e2618b00e2e 100644 --- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java @@ -454,7 +454,7 @@ class InsetsSourceProvider { if (mSource.getType() == WindowInsets.Type.ime()) { setClientVisible(target.isRequestedVisible(WindowInsets.Type.ime())); } - final Transaction t = mDisplayContent.getSyncTransaction(); + final Transaction t = mWindowContainer.getSyncTransaction(); mWindowContainer.startAnimation(t, mAdapter, !mClientVisible /* hidden */, ANIMATION_TYPE_INSETS_CONTROL); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index bb6f8056acda..7104739068e4 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3290,6 +3290,12 @@ class Task extends TaskFragment { scheduleAnimation(); } + // Let organizer manage task visibility for shell transition. So don't change it's + // visibility during collecting. + if (mTransitionController.isCollecting() && mCreatedByOrganizer) { + return; + } + // We intend to let organizer manage task visibility but it doesn't // have enough information until we finish shell transitions. // In the mean time we do an easy fix here. @@ -5687,17 +5693,28 @@ class Task extends TaskFragment { } private boolean moveTaskToBackInner(@NonNull Task task) { - moveToBack("moveTaskToBackInner", task); - - if (inPinnedWindowingMode()) { - mTaskSupervisor.removeRootTask(this); - return true; + if (mTransitionController.isShellTransitionsEnabled()) { + // Preventing from update surface position for WindowState if configuration changed, + // because the position is depends on WindowFrame, so update the position before + // relayout will only update it to "old" position. + mAtmService.deferWindowLayout(); } + try { + moveToBack("moveTaskToBackInner", task); - mRootWindowContainer.ensureVisibilityAndConfig(null /* starting */, - mDisplayContent.mDisplayId, false /* markFrozenIfConfigChanged */, - false /* deferResume */); + if (inPinnedWindowingMode()) { + mTaskSupervisor.removeRootTask(this); + return true; + } + mRootWindowContainer.ensureVisibilityAndConfig(null /* starting */, + mDisplayContent.mDisplayId, false /* markFrozenIfConfigChanged */, + false /* deferResume */); + } finally { + if (mTransitionController.isShellTransitionsEnabled()) { + mAtmService.continueWindowLayout(); + } + } ActivityRecord topActivity = getDisplayArea().topRunningActivity(); Task topRootTask = topActivity.getRootTask(); if (topRootTask != null && topRootTask != this && topActivity.isState(RESUMED)) { diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 9a5f766989ca..795d022512a3 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -690,6 +690,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if (!wc.mDisplayContent.getDisplayPolicy().isScreenOnFully() || wc.mDisplayContent.getDisplayInfo().state == Display.STATE_OFF) { mFlags |= WindowManager.TRANSIT_FLAG_INVISIBLE; + return; } if (mContainerFreezer == null) { @@ -1190,7 +1191,11 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // processed all the participants first (in particular, we want to trigger pip-enter first) for (int i = 0; i < mParticipants.size(); ++i) { final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord(); - if (ar != null) { + // If the activity was just inserted to an invisible task, it will keep INITIALIZING + // state. Then no need to notify the callback to avoid clearing some states + // unexpectedly, e.g. launch-task-behind. + if (ar != null && (ar.isVisibleRequested() + || !ar.isState(ActivityRecord.State.INITIALIZING))) { mController.dispatchLegacyAppTransitionFinished(ar); } } @@ -2244,11 +2249,17 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { WindowContainer<?> ancestor = findCommonAncestor(sortedTargets, wc); // Make leash based on highest (z-order) direct child of ancestor with a participant. + // Check whether the ancestor is belonged to last parent, shouldn't happen. + final boolean hasReparent = !wc.isDescendantOf(ancestor); WindowContainer leashReference = wc; - while (leashReference.getParent() != ancestor) { - leashReference = leashReference.getParent(); + if (hasReparent) { + Slog.e(TAG, "Did not find common ancestor! Ancestor= " + ancestor + + " target= " + wc); + } else { + while (leashReference.getParent() != ancestor) { + leashReference = leashReference.getParent(); + } } - final SurfaceControl rootLeash = leashReference.makeAnimationLeash().setName( "Transition Root: " + leashReference.getName()).build(); rootLeash.setUnreleasedWarningCallSite("Transition.calculateTransitionRoots"); @@ -2307,23 +2318,13 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { task.fillTaskInfo(tinfo); change.setTaskInfo(tinfo); change.setRotationAnimation(getTaskRotationAnimation(task)); - final ActivityRecord topMostActivity = task.getTopMostActivity(); - change.setAllowEnterPip(topMostActivity != null - && topMostActivity.checkEnterPictureInPictureAppOpsState()); final ActivityRecord topRunningActivity = task.topRunningActivity(); - if (topRunningActivity != null && task.mDisplayContent != null - // Display won't be rotated for multi window Task, so the fixed rotation - // won't be applied. This can happen when the windowing mode is changed - // before the previous fixed rotation is applied. - && (!task.inMultiWindowMode() || !topRunningActivity.inMultiWindowMode())) { - // If Activity is in fixed rotation, its will be applied with the next rotation, - // when the Task is still in the previous rotation. - final int taskRotation = task.getWindowConfiguration().getDisplayRotation(); - final int activityRotation = topRunningActivity.getWindowConfiguration() - .getDisplayRotation(); - if (taskRotation != activityRotation) { - change.setEndFixedRotation(activityRotation); + if (topRunningActivity != null) { + if (topRunningActivity.info.supportsPictureInPicture()) { + change.setAllowEnterPip( + topRunningActivity.checkEnterPictureInPictureAppOpsState()); } + setEndFixedRotationIfNeeded(change, task, topRunningActivity); } } else if ((info.mFlags & ChangeInfo.FLAG_SEAMLESS_ROTATION) != 0) { change.setRotationAnimation(ROTATION_ANIMATION_SEAMLESS); @@ -2431,6 +2432,48 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } return animOptions; } + + private static void setEndFixedRotationIfNeeded(@NonNull TransitionInfo.Change change, + @NonNull Task task, @NonNull ActivityRecord taskTopRunning) { + if (!taskTopRunning.isVisibleRequested()) { + // Fixed rotation only applies to opening or changing activity. + return; + } + if (task.inMultiWindowMode() && taskTopRunning.inMultiWindowMode()) { + // Display won't be rotated for multi window Task, so the fixed rotation won't be + // applied. This can happen when the windowing mode is changed before the previous + // fixed rotation is applied. Check both task and activity because the activity keeps + // fullscreen mode when the task is entering PiP. + return; + } + final int taskRotation = task.getWindowConfiguration().getDisplayRotation(); + final int activityRotation = taskTopRunning.getWindowConfiguration() + .getDisplayRotation(); + // If the Activity uses fixed rotation, its rotation will be applied to display after + // the current transition is done, while the Task is still in the previous rotation. + if (taskRotation != activityRotation) { + change.setEndFixedRotation(activityRotation); + return; + } + + // For example, the task is entering PiP so it no longer decides orientation. If the next + // orientation source (it could be an activity which was behind the PiP or launching to top) + // will change display rotation, then set the fixed rotation hint as well so the animation + // can consider the rotated position. + if (!task.inPinnedWindowingMode() || taskTopRunning.mDisplayContent.inTransition()) { + return; + } + final WindowContainer<?> orientationSource = + taskTopRunning.mDisplayContent.getLastOrientationSource(); + if (orientationSource == null) { + return; + } + final int nextRotation = orientationSource.getWindowConfiguration().getDisplayRotation(); + if (taskRotation != nextRotation) { + change.setEndFixedRotation(nextRotation); + } + } + /** * Finds the top-most common ancestor of app targets. * @@ -2453,6 +2496,20 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // Skip the non-app window or windows on a different display continue; } + // Re-initiate the last parent as the initial ancestor instead of the top target. + // When move a leaf task from organized task to display area, try to keep the transition + // root be the original organized task for close transition animation. + // Otherwise, shell will use wrong root layer to play animation. + // Note: Since the target is sorted, so only need to do this at the lowest target. + if (change.mStartParent != null && wc.getParent() != null + && change.mStartParent.isAttached() && wc.getParent() != change.mStartParent + && i == targets.size() - 1) { + final int transitionMode = change.getTransitMode(wc); + if (transitionMode == TRANSIT_CLOSE || transitionMode == TRANSIT_TO_BACK) { + ancestor = change.mStartParent; + continue; + } + } while (!wc.isDescendantOf(ancestor)) { ancestor = ancestor.getParent(); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index d84c85c8e823..67572bfcad18 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -88,6 +88,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import static android.view.WindowManager.REMOVE_CONTENT_MODE_UNDEFINED; import static android.view.WindowManager.TRANSIT_NONE; +import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.fixScale; import static android.view.WindowManagerGlobal.ADD_OKAY; import static android.view.WindowManagerGlobal.RELAYOUT_RES_CANCEL_AND_REDRAW; @@ -3591,6 +3592,7 @@ public class WindowManagerService extends IWindowManager.Stub public void setCurrentUser(@UserIdInt int newUserId) { synchronized (mGlobalLock) { + mAtmService.getTransitionController().requestTransitionIfNeeded(TRANSIT_OPEN, null); mCurrentUserId = newUserId; mPolicy.setCurrentUserLw(newUserId); mKeyguardDisableHandler.setCurrentUser(newUserId); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java index c918fb87154f..9c1d765fe0f9 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java @@ -101,7 +101,7 @@ final class DevicePolicyEngine { private final UserManager mUserManager; // TODO(b/256849338): add more granular locks - private final Object mLock = new Object(); + private final Object mLock; /** * Map of <userId, Map<policyKey, policyState>> @@ -122,9 +122,11 @@ final class DevicePolicyEngine { DevicePolicyEngine( @NonNull Context context, - @NonNull DeviceAdminServiceController deviceAdminServiceController) { + @NonNull DeviceAdminServiceController deviceAdminServiceController, + @NonNull Object lock) { mContext = Objects.requireNonNull(context); mDeviceAdminServiceController = Objects.requireNonNull(deviceAdminServiceController); + mLock = Objects.requireNonNull(lock); mUserManager = mContext.getSystemService(UserManager.class); mLocalPolicies = new SparseArray<>(); mGlobalPolicies = new HashMap<>(); @@ -152,8 +154,8 @@ final class DevicePolicyEngine { PolicyState<V> localPolicyState = getLocalPolicyStateLocked(policyDefinition, userId); if (policyDefinition.isNonCoexistablePolicy()) { - setNonCoexistableLocalPolicy(policyDefinition, localPolicyState, enforcingAdmin, - value, userId, skipEnforcePolicy); + setNonCoexistableLocalPolicyLocked(policyDefinition, localPolicyState, + enforcingAdmin, value, userId, skipEnforcePolicy); return; } @@ -173,7 +175,7 @@ final class DevicePolicyEngine { // the data structures. if (!skipEnforcePolicy) { if (policyChanged) { - onLocalPolicyChanged(policyDefinition, enforcingAdmin, userId); + onLocalPolicyChangedLocked(policyDefinition, enforcingAdmin, userId); } boolean policyEnforced = Objects.equals( localPolicyState.getCurrentResolvedPolicy(), value); @@ -211,7 +213,7 @@ final class DevicePolicyEngine { * * <p>Passing a {@code null} value means the policy set by this admin should be removed. */ - private <V> void setNonCoexistableLocalPolicy( + private <V> void setNonCoexistableLocalPolicyLocked( PolicyDefinition<V> policyDefinition, PolicyState<V> localPolicyState, EnforcingAdmin enforcingAdmin, @@ -266,8 +268,8 @@ final class DevicePolicyEngine { PolicyState<V> localPolicyState = getLocalPolicyStateLocked(policyDefinition, userId); if (policyDefinition.isNonCoexistablePolicy()) { - setNonCoexistableLocalPolicy(policyDefinition, localPolicyState, enforcingAdmin, - /* value= */ null, userId, /* skipEnforcePolicy= */ false); + setNonCoexistableLocalPolicyLocked(policyDefinition, localPolicyState, + enforcingAdmin, /* value= */ null, userId, /* skipEnforcePolicy= */ false); return; } @@ -282,7 +284,7 @@ final class DevicePolicyEngine { } if (policyChanged) { - onLocalPolicyChanged(policyDefinition, enforcingAdmin, userId); + onLocalPolicyChangedLocked(policyDefinition, enforcingAdmin, userId); } // For a removePolicy to be enforced, it means no current policy exists @@ -348,7 +350,7 @@ final class DevicePolicyEngine { /** * Enforces the new policy and notifies relevant admins. */ - private <V> void onLocalPolicyChanged( + private <V> void onLocalPolicyChangedLocked( @NonNull PolicyDefinition<V> policyDefinition, @NonNull EnforcingAdmin enforcingAdmin, int userId) { @@ -358,7 +360,7 @@ final class DevicePolicyEngine { policyDefinition, localPolicyState.getCurrentResolvedPolicy(), userId); // Send policy updates to admins who've set it locally - sendPolicyChangedToAdmins( + sendPolicyChangedToAdminsLocked( localPolicyState, enforcingAdmin, policyDefinition, @@ -369,7 +371,7 @@ final class DevicePolicyEngine { // Send policy updates to admins who've set it globally if (hasGlobalPolicyLocked(policyDefinition)) { PolicyState<V> globalPolicyState = getGlobalPolicyStateLocked(policyDefinition); - sendPolicyChangedToAdmins( + sendPolicyChangedToAdminsLocked( globalPolicyState, enforcingAdmin, policyDefinition, @@ -424,7 +426,7 @@ final class DevicePolicyEngine { // the data structures. if (!skipEnforcePolicy) { if (policyChanged) { - onGlobalPolicyChanged(policyDefinition, enforcingAdmin); + onGlobalPolicyChangedLocked(policyDefinition, enforcingAdmin); } boolean policyAppliedGlobally = Objects.equals( @@ -473,7 +475,7 @@ final class DevicePolicyEngine { boolean policyChanged = policyState.removePolicy(enforcingAdmin); if (policyChanged) { - onGlobalPolicyChanged(policyDefinition, enforcingAdmin); + onGlobalPolicyChangedLocked(policyDefinition, enforcingAdmin); } applyGlobalPolicyOnUsersWithLocalPoliciesLocked(policyDefinition, enforcingAdmin, @@ -499,7 +501,7 @@ final class DevicePolicyEngine { /** * Enforces the new policy globally and notifies relevant admins. */ - private <V> void onGlobalPolicyChanged( + private <V> void onGlobalPolicyChangedLocked( @NonNull PolicyDefinition<V> policyDefinition, @NonNull EnforcingAdmin enforcingAdmin) { PolicyState<V> policyState = getGlobalPolicyStateLocked(policyDefinition); @@ -507,7 +509,7 @@ final class DevicePolicyEngine { enforcePolicy(policyDefinition, policyState.getCurrentResolvedPolicy(), UserHandle.USER_ALL); - sendPolicyChangedToAdmins( + sendPolicyChangedToAdminsLocked( policyState, enforcingAdmin, policyDefinition, @@ -552,7 +554,7 @@ final class DevicePolicyEngine { policyDefinition, localPolicyState.getCurrentResolvedPolicy(), userId); - sendPolicyChangedToAdmins( + sendPolicyChangedToAdminsLocked( localPolicyState, enforcingAdmin, policyDefinition, @@ -745,34 +747,35 @@ final class DevicePolicyEngine { } <V> void transferPolicies(EnforcingAdmin oldAdmin, EnforcingAdmin newAdmin) { - Set<PolicyKey> globalPolicies = new HashSet<>(mGlobalPolicies.keySet()); - for (PolicyKey policy : globalPolicies) { - PolicyState<?> policyState = mGlobalPolicies.get(policy); - if (policyState.getPoliciesSetByAdmins().containsKey(oldAdmin)) { - PolicyDefinition<V> policyDefinition = - (PolicyDefinition<V>) policyState.getPolicyDefinition(); - PolicyValue<V> policyValue = - (PolicyValue<V>) policyState.getPoliciesSetByAdmins().get(oldAdmin); - setGlobalPolicy(policyDefinition, newAdmin, policyValue); - } - } - - for (int i = 0; i < mLocalPolicies.size(); i++) { - int userId = mLocalPolicies.keyAt(i); - Set<PolicyKey> localPolicies = new HashSet<>( - mLocalPolicies.get(userId).keySet()); - for (PolicyKey policy : localPolicies) { - PolicyState<?> policyState = mLocalPolicies.get(userId).get(policy); + synchronized (mLock) { + Set<PolicyKey> globalPolicies = new HashSet<>(mGlobalPolicies.keySet()); + for (PolicyKey policy : globalPolicies) { + PolicyState<?> policyState = mGlobalPolicies.get(policy); if (policyState.getPoliciesSetByAdmins().containsKey(oldAdmin)) { PolicyDefinition<V> policyDefinition = (PolicyDefinition<V>) policyState.getPolicyDefinition(); PolicyValue<V> policyValue = (PolicyValue<V>) policyState.getPoliciesSetByAdmins().get(oldAdmin); - setLocalPolicy(policyDefinition, newAdmin, policyValue, userId); + setGlobalPolicy(policyDefinition, newAdmin, policyValue); } } - } + for (int i = 0; i < mLocalPolicies.size(); i++) { + int userId = mLocalPolicies.keyAt(i); + Set<PolicyKey> localPolicies = new HashSet<>( + mLocalPolicies.get(userId).keySet()); + for (PolicyKey policy : localPolicies) { + PolicyState<?> policyState = mLocalPolicies.get(userId).get(policy); + if (policyState.getPoliciesSetByAdmins().containsKey(oldAdmin)) { + PolicyDefinition<V> policyDefinition = + (PolicyDefinition<V>) policyState.getPolicyDefinition(); + PolicyValue<V> policyValue = + (PolicyValue<V>) policyState.getPoliciesSetByAdmins().get(oldAdmin); + setLocalPolicy(policyDefinition, newAdmin, policyValue, userId); + } + } + } + } removePoliciesForAdmin(oldAdmin); } @@ -836,7 +839,7 @@ final class DevicePolicyEngine { mLocalPolicies.get(userId).put( policyDefinition.getPolicyKey(), new PolicyState<>(policyDefinition)); } - return getPolicyState(mLocalPolicies.get(userId), policyDefinition); + return getPolicyStateLocked(mLocalPolicies.get(userId), policyDefinition); } private <V> void removeLocalPolicyStateLocked( @@ -858,14 +861,14 @@ final class DevicePolicyEngine { mGlobalPolicies.put( policyDefinition.getPolicyKey(), new PolicyState<>(policyDefinition)); } - return getPolicyState(mGlobalPolicies, policyDefinition); + return getPolicyStateLocked(mGlobalPolicies, policyDefinition); } private <V> void removeGlobalPolicyStateLocked(PolicyDefinition<V> policyDefinition) { mGlobalPolicies.remove(policyDefinition.getPolicyKey()); } - private static <V> PolicyState<V> getPolicyState( + private static <V> PolicyState<V> getPolicyStateLocked( Map<PolicyKey, PolicyState<?>> policies, PolicyDefinition<V> policyDefinition) { try { // This will not throw an exception because policyDefinition is of type V, so unless @@ -935,7 +938,7 @@ final class DevicePolicyEngine { } // TODO(b/261430877): Finalise the decision on which admins to send the updates to. - private <V> void sendPolicyChangedToAdmins( + private <V> void sendPolicyChangedToAdminsLocked( PolicyState<V> policyState, EnforcingAdmin callingAdmin, PolicyDefinition<V> policyDefinition, @@ -1210,17 +1213,19 @@ final class DevicePolicyEngine { if (parentInfo == null || parentInfo.getUserHandle().getIdentifier() == userId) { return; } - if (!mLocalPolicies.contains(parentInfo.getUserHandle().getIdentifier())) { - return; - } - for (Map.Entry<PolicyKey, PolicyState<?>> entry : mLocalPolicies.get( - parentInfo.getUserHandle().getIdentifier()).entrySet()) { - enforcePolicyOnUser(userId, entry.getValue()); + synchronized (mLock) { + if (!mLocalPolicies.contains(parentInfo.getUserHandle().getIdentifier())) { + return; + } + for (Map.Entry<PolicyKey, PolicyState<?>> entry : mLocalPolicies.get( + parentInfo.getUserHandle().getIdentifier()).entrySet()) { + enforcePolicyOnUserLocked(userId, entry.getValue()); + } } }); } - private <V> void enforcePolicyOnUser(int userId, PolicyState<V> policyState) { + private <V> void enforcePolicyOnUserLocked(int userId, PolicyState<V> policyState) { if (!policyState.getPolicyDefinition().isInheritable()) { return; } @@ -1239,26 +1244,28 @@ final class DevicePolicyEngine { */ @NonNull DevicePolicyState getDevicePolicyState() { - Map<UserHandle, Map<PolicyKey, android.app.admin.PolicyState<?>>> policies = - new HashMap<>(); - for (int i = 0; i < mLocalPolicies.size(); i++) { - UserHandle user = UserHandle.of(mLocalPolicies.keyAt(i)); - policies.put(user, new HashMap<>()); - for (PolicyKey policyKey : mLocalPolicies.valueAt(i).keySet()) { - policies.get(user).put( - policyKey, - mLocalPolicies.valueAt(i).get(policyKey).getParcelablePolicyState()); + synchronized (mLock) { + Map<UserHandle, Map<PolicyKey, android.app.admin.PolicyState<?>>> policies = + new HashMap<>(); + for (int i = 0; i < mLocalPolicies.size(); i++) { + UserHandle user = UserHandle.of(mLocalPolicies.keyAt(i)); + policies.put(user, new HashMap<>()); + for (PolicyKey policyKey : mLocalPolicies.valueAt(i).keySet()) { + policies.get(user).put( + policyKey, + mLocalPolicies.valueAt(i).get(policyKey).getParcelablePolicyState()); + } } - } - if (!mGlobalPolicies.isEmpty()) { - policies.put(UserHandle.ALL, new HashMap<>()); - for (PolicyKey policyKey : mGlobalPolicies.keySet()) { - policies.get(UserHandle.ALL).put( - policyKey, - mGlobalPolicies.get(policyKey).getParcelablePolicyState()); + if (!mGlobalPolicies.isEmpty()) { + policies.put(UserHandle.ALL, new HashMap<>()); + for (PolicyKey policyKey : mGlobalPolicies.keySet()) { + policies.get(UserHandle.ALL).put( + policyKey, + mGlobalPolicies.get(policyKey).getParcelablePolicyState()); + } } + return new DevicePolicyState(policies); } - return new DevicePolicyState(policies); } @@ -1266,23 +1273,25 @@ final class DevicePolicyEngine { * Removes all local and global policies set by that admin. */ void removePoliciesForAdmin(EnforcingAdmin admin) { - Set<PolicyKey> globalPolicies = new HashSet<>(mGlobalPolicies.keySet()); - for (PolicyKey policy : globalPolicies) { - PolicyState<?> policyState = mGlobalPolicies.get(policy); - if (policyState.getPoliciesSetByAdmins().containsKey(admin)) { - removeGlobalPolicy(policyState.getPolicyDefinition(), admin); + synchronized (mLock) { + Set<PolicyKey> globalPolicies = new HashSet<>(mGlobalPolicies.keySet()); + for (PolicyKey policy : globalPolicies) { + PolicyState<?> policyState = mGlobalPolicies.get(policy); + if (policyState.getPoliciesSetByAdmins().containsKey(admin)) { + removeGlobalPolicy(policyState.getPolicyDefinition(), admin); + } } - } - for (int i = 0; i < mLocalPolicies.size(); i++) { - Set<PolicyKey> localPolicies = new HashSet<>( - mLocalPolicies.get(mLocalPolicies.keyAt(i)).keySet()); - for (PolicyKey policy : localPolicies) { - PolicyState<?> policyState = mLocalPolicies.get( - mLocalPolicies.keyAt(i)).get(policy); - if (policyState.getPoliciesSetByAdmins().containsKey(admin)) { - removeLocalPolicy( - policyState.getPolicyDefinition(), admin, mLocalPolicies.keyAt(i)); + for (int i = 0; i < mLocalPolicies.size(); i++) { + Set<PolicyKey> localPolicies = new HashSet<>( + mLocalPolicies.get(mLocalPolicies.keyAt(i)).keySet()); + for (PolicyKey policy : localPolicies) { + PolicyState<?> policyState = mLocalPolicies.get( + mLocalPolicies.keyAt(i)).get(policy); + if (policyState.getPoliciesSetByAdmins().containsKey(admin)) { + removeLocalPolicy( + policyState.getPolicyDefinition(), admin, mLocalPolicies.keyAt(i)); + } } } } @@ -1292,23 +1301,25 @@ final class DevicePolicyEngine { * Removes all local policies for the provided {@code userId}. */ private void removeLocalPoliciesForUser(int userId) { - if (!mLocalPolicies.contains(userId)) { - // No policies on user - return; - } + synchronized (mLock) { + if (!mLocalPolicies.contains(userId)) { + // No policies on user + return; + } - Set<PolicyKey> localPolicies = new HashSet<>(mLocalPolicies.get(userId).keySet()); - for (PolicyKey policy : localPolicies) { - PolicyState<?> policyState = mLocalPolicies.get(userId).get(policy); - Set<EnforcingAdmin> admins = new HashSet<>( - policyState.getPoliciesSetByAdmins().keySet()); - for (EnforcingAdmin admin : admins) { - removeLocalPolicy( - policyState.getPolicyDefinition(), admin, userId); + Set<PolicyKey> localPolicies = new HashSet<>(mLocalPolicies.get(userId).keySet()); + for (PolicyKey policy : localPolicies) { + PolicyState<?> policyState = mLocalPolicies.get(userId).get(policy); + Set<EnforcingAdmin> admins = new HashSet<>( + policyState.getPoliciesSetByAdmins().keySet()); + for (EnforcingAdmin admin : admins) { + removeLocalPolicy( + policyState.getPolicyDefinition(), admin, userId); + } } - } - mLocalPolicies.remove(userId); + mLocalPolicies.remove(userId); + } } /** @@ -1376,7 +1387,7 @@ final class DevicePolicyEngine { */ private void updateDeviceAdminServiceOnPolicyRemoveLocked( @NonNull EnforcingAdmin enforcingAdmin) { - if (doesAdminHavePolicies(enforcingAdmin)) { + if (doesAdminHavePoliciesLocked(enforcingAdmin)) { return; } int userId = enforcingAdmin.getUserId(); @@ -1399,7 +1410,7 @@ final class DevicePolicyEngine { /* actionForLog= */ "policy-removed"); } - private boolean doesAdminHavePolicies(@NonNull EnforcingAdmin enforcingAdmin) { + private boolean doesAdminHavePoliciesLocked(@NonNull EnforcingAdmin enforcingAdmin) { for (PolicyKey policy : mGlobalPolicies.keySet()) { PolicyState<?> policyState = mGlobalPolicies.get(policy); if (policyState.getPoliciesSetByAdmins().containsKey(enforcingAdmin)) { @@ -1420,13 +1431,17 @@ final class DevicePolicyEngine { @NonNull private Set<EnforcingAdmin> getEnforcingAdminsOnUser(int userId) { - return mEnforcingAdmins.contains(userId) - ? mEnforcingAdmins.get(userId) : Collections.emptySet(); + synchronized (mLock) { + return mEnforcingAdmins.contains(userId) + ? mEnforcingAdmins.get(userId) : Collections.emptySet(); + } } private void write() { - Log.d(TAG, "Writing device policies to file."); - new DevicePoliciesReaderWriter().writeToFileLocked(); + synchronized (mLock) { + Log.d(TAG, "Writing device policies to file."); + new DevicePoliciesReaderWriter().writeToFileLocked(); + } } // TODO(b/256852787): trigger resolving logic after loading policies as roles are recalculated @@ -1436,11 +1451,11 @@ final class DevicePolicyEngine { synchronized (mLock) { clear(); new DevicePoliciesReaderWriter().readFromFileLocked(); - reapplyAllPolicies(); + reapplyAllPoliciesLocked(); } } - private <V> void reapplyAllPolicies() { + private <V> void reapplyAllPoliciesLocked() { for (PolicyKey policy : mGlobalPolicies.keySet()) { PolicyState<?> policyState = mGlobalPolicies.get(policy); // Policy definition and value will always be of the same type @@ -1470,10 +1485,8 @@ final class DevicePolicyEngine { * <p>Note that this doesn't clear any enforcements, it only clears the data structures. */ void clearAllPolicies() { - synchronized (mLock) { - clear(); - write(); - } + clear(); + write(); } private void clear() { synchronized (mLock) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index a5b15489f292..e44b8cdbaab0 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -2093,7 +2093,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { mUserData = new SparseArray<>(); mOwners = makeOwners(injector, pathProvider); - mDevicePolicyEngine = new DevicePolicyEngine(mContext, mDeviceAdminServiceController); + mDevicePolicyEngine = new DevicePolicyEngine( + mContext, mDeviceAdminServiceController, getLockObject()); if (!mHasFeature) { // Skip the rest of the initialization @@ -7840,27 +7841,29 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { throw new SecurityException("Cannot wipe data. " + restriction + " restriction is set for user " + userId); } + }); - boolean isSystemUser = userId == UserHandle.USER_SYSTEM; - boolean wipeDevice; - if (factoryReset == null || !mInjector.isChangeEnabled(EXPLICIT_WIPE_BEHAVIOUR, - adminPackage, - userId)) { - // Legacy mode - wipeDevice = isSystemUser; + boolean isSystemUser = userId == UserHandle.USER_SYSTEM; + boolean wipeDevice; + if (factoryReset == null || !mInjector.isChangeEnabled(EXPLICIT_WIPE_BEHAVIOUR, + adminPackage, + userId)) { + // Legacy mode + wipeDevice = isSystemUser; + } else { + // Explicit behaviour + if (factoryReset) { + EnforcingAdmin enforcingAdmin = enforcePermissionsAndGetEnforcingAdmin( + /*admin=*/ null, + /*permission=*/ new String[]{MANAGE_DEVICE_POLICY_WIPE_DATA, + MASTER_CLEAR}, + USES_POLICY_WIPE_DATA, + adminPackage, + factoryReset ? UserHandle.USER_ALL : + getAffectedUser(calledOnParentInstance)); + wipeDevice = true; } else { - // Explicit behaviour - if (factoryReset) { - EnforcingAdmin enforcingAdmin = enforcePermissionsAndGetEnforcingAdmin( - /*admin=*/ null, - /*permission=*/ new String[]{MANAGE_DEVICE_POLICY_WIPE_DATA, - MASTER_CLEAR}, - USES_POLICY_WIPE_DATA, - adminPackage, - factoryReset ? UserHandle.USER_ALL : - getAffectedUser(calledOnParentInstance)); - wipeDevice = true; - } else { + mInjector.binderWithCleanCallingIdentity(() -> { Preconditions.checkCallAuthorization(!isSystemUser, "User %s is a system user and cannot be removed", userId); boolean isLastNonHeadlessUser = getUserInfo(userId).isFull() @@ -7871,9 +7874,11 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { "Removing user %s would leave the device without any active users. " + "Consider factory resetting the device instead.", userId); - wipeDevice = false; - } + }); + wipeDevice = false; } + } + mInjector.binderWithCleanCallingIdentity(() -> { if (wipeDevice) { forceWipeDeviceNoLock( (flags & WIPE_EXTERNAL_STORAGE) != 0, 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 c5ff8cc7b0d6..dd23d9f006e3 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java @@ -28,6 +28,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; import static com.android.server.job.JobSchedulerService.RARE_INDEX; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import static com.android.server.job.JobSchedulerService.sUptimeMillisClock; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -148,6 +149,9 @@ public class JobSchedulerServiceTest { // Used in JobConcurrencyManager. doReturn(mock(UserManagerInternal.class)) .when(() -> LocalServices.getService(UserManagerInternal.class)); + // Used in JobStatus. + doReturn(mock(JobSchedulerInternal.class)) + .when(() -> LocalServices.getService(JobSchedulerInternal.class)); // Called via IdleController constructor. when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class)); when(mContext.getResources()).thenReturn(mock(Resources.class)); @@ -168,6 +172,8 @@ public class JobSchedulerServiceTest { JobSchedulerService.sSystemClock = Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC); JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC); + // Make sure the uptime is at least 24 hours so that tests that rely on high uptime work. + sUptimeMillisClock = getAdvancedClock(sUptimeMillisClock, 24 * HOUR_IN_MILLIS); // Called by DeviceIdlenessTracker when(mContext.getSystemService(UiModeManager.class)).thenReturn(mock(UiModeManager.class)); @@ -313,6 +319,260 @@ public class JobSchedulerServiceTest { } @Test + public void testGetMinJobExecutionGuaranteeMs_timeoutSafeguards_disabled() { + JobStatus jobUij = createJobStatus("testGetMinJobExecutionGuaranteeMs_timeoutSafeguards", + createJobInfo(1) + .setUserInitiated(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); + JobStatus jobEj = createJobStatus("testGetMinJobExecutionGuaranteeMs_timeoutSafeguards", + createJobInfo(2).setExpedited(true)); + JobStatus jobReg = createJobStatus("testGetMinJobExecutionGuaranteeMs_timeoutSafeguards", + createJobInfo(3)); + spyOn(jobUij); + when(jobUij.shouldTreatAsUserInitiatedJob()).thenReturn(true); + jobUij.startedAsUserInitiatedJob = true; + spyOn(jobEj); + when(jobEj.shouldTreatAsExpeditedJob()).thenReturn(true); + jobEj.startedAsExpeditedJob = true; + + mService.mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC = false; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = 2; + mService.updateQuotaTracker(); + mService.resetScheduleQuota(); + + // Safeguards disabled -> no penalties. + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 1 UIJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 2 UIJ timeouts. Safeguards disabled -> no penalties. + jobUij.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 1 EJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 2 EJ timeouts. Safeguards disabled -> no penalties. + jobEj.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 1 reg timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 2 Reg timeouts. Safeguards disabled -> no penalties. + jobReg.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + } + + @Test + public void testGetMinJobExecutionGuaranteeMs_timeoutSafeguards_enabled() { + JobStatus jobUij = createJobStatus("testGetMinJobExecutionGuaranteeMs_timeoutSafeguards", + createJobInfo(1) + .setUserInitiated(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); + JobStatus jobEj = createJobStatus("testGetMinJobExecutionGuaranteeMs_timeoutSafeguards", + createJobInfo(2).setExpedited(true)); + JobStatus jobReg = createJobStatus("testGetMinJobExecutionGuaranteeMs_timeoutSafeguards", + createJobInfo(3)); + spyOn(jobUij); + when(jobUij.shouldTreatAsUserInitiatedJob()).thenReturn(true); + jobUij.startedAsUserInitiatedJob = true; + spyOn(jobEj); + when(jobEj.shouldTreatAsExpeditedJob()).thenReturn(true); + jobEj.startedAsExpeditedJob = true; + + mService.mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC = true; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = 2; + mService.updateQuotaTracker(); + mService.resetScheduleQuota(); + + // No timeouts -> no penalties. + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 1 UIJ timeout. No execution penalty yet. + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // Not a timeout -> 1 UIJ timeout. No execution penalty yet. + jobUij.madeActive = sUptimeMillisClock.millis() - 1; + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 2 UIJ timeouts. Min execution penalty only for UIJs. + jobUij.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 1 EJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 2 EJ timeouts. Max execution penalty for EJs. + jobEj.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 1 reg timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 2 Reg timeouts. Max execution penalty for regular jobs. + jobReg.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + } + + @Test public void testGetMaxJobExecutionTimeMs() { JobStatus jobUIDT = createJobStatus("testGetMaxJobExecutionTimeMs", createJobInfo(10) @@ -327,7 +587,7 @@ public class JobSchedulerServiceTest { doReturn(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) .when(quotaController).getMaxJobExecutionTimeMsLocked(any()); doReturn(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) - .when(quotaController).getMaxJobExecutionTimeMsLocked(any()); + .when(tareController).getMaxJobExecutionTimeMsLocked(any()); grantRunUserInitiatedJobsPermission(true); assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, @@ -337,6 +597,306 @@ public class JobSchedulerServiceTest { mService.getMaxJobExecutionTimeMs(jobUIDT)); } + @Test + public void testGetMaxJobExecutionTimeMs_timeoutSafeguards_disabled() { + JobStatus jobUij = createJobStatus("testGetMaxJobExecutionTimeMs_timeoutSafeguards", + createJobInfo(1) + .setUserInitiated(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); + JobStatus jobEj = createJobStatus("testGetMaxJobExecutionTimeMs_timeoutSafeguards", + createJobInfo(2).setExpedited(true)); + JobStatus jobReg = createJobStatus("testGetMaxJobExecutionTimeMs_timeoutSafeguards", + createJobInfo(3)); + spyOn(jobUij); + when(jobUij.shouldTreatAsUserInitiatedJob()).thenReturn(true); + jobUij.startedAsUserInitiatedJob = true; + spyOn(jobEj); + when(jobEj.shouldTreatAsExpeditedJob()).thenReturn(true); + jobEj.startedAsExpeditedJob = true; + + QuotaController quotaController = mService.getQuotaController(); + spyOn(quotaController); + TareController tareController = mService.getTareController(); + spyOn(tareController); + doReturn(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) + .when(quotaController).getMaxJobExecutionTimeMsLocked(any()); + doReturn(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) + .when(tareController).getMaxJobExecutionTimeMsLocked(any()); + + mService.mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC = false; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = 2; + mService.updateQuotaTracker(); + mService.resetScheduleQuota(); + + // Safeguards disabled -> no penalties. + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 1 UIJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 2 UIJ timeouts. Safeguards disabled -> no penalties. + jobUij.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 1 EJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 2 EJ timeouts. Safeguards disabled -> no penalties. + jobEj.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 1 reg timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 2 Reg timeouts. Safeguards disabled -> no penalties. + jobReg.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + } + + @Test + public void testGetMaxJobExecutionTimeMs_timeoutSafeguards_enabled() { + JobStatus jobUij = createJobStatus("testGetMaxJobExecutionTimeMs_timeoutSafeguards", + createJobInfo(1) + .setUserInitiated(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); + JobStatus jobEj = createJobStatus("testGetMaxJobExecutionTimeMs_timeoutSafeguards", + createJobInfo(2).setExpedited(true)); + JobStatus jobReg = createJobStatus("testGetMaxJobExecutionTimeMs_timeoutSafeguards", + createJobInfo(3)); + spyOn(jobUij); + when(jobUij.shouldTreatAsUserInitiatedJob()).thenReturn(true); + jobUij.startedAsUserInitiatedJob = true; + spyOn(jobEj); + when(jobEj.shouldTreatAsExpeditedJob()).thenReturn(true); + jobEj.startedAsExpeditedJob = true; + + QuotaController quotaController = mService.getQuotaController(); + spyOn(quotaController); + TareController tareController = mService.getTareController(); + spyOn(tareController); + doReturn(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) + .when(quotaController).getMaxJobExecutionTimeMsLocked(any()); + doReturn(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) + .when(tareController).getMaxJobExecutionTimeMsLocked(any()); + + mService.mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC = true; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = 2; + mService.updateQuotaTracker(); + mService.resetScheduleQuota(); + + // No timeouts -> no penalties. + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 1 UIJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // Not a timeout -> 1 UIJ timeout. No max execution penalty yet. + jobUij.madeActive = sUptimeMillisClock.millis() - 1; + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 2 UIJ timeouts. Max execution penalty only for UIJs. + jobUij.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 1 EJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // Not a timeout -> 1 EJ timeout. No max execution penalty yet. + jobEj.madeActive = sUptimeMillisClock.millis() - 1; + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 2 EJ timeouts. Max execution penalty for EJs. + jobEj.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 1 reg timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // Not a timeout -> 1 reg timeout. No max execution penalty yet. + jobReg.madeActive = sUptimeMillisClock.millis() - 1; + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 2 Reg timeouts. Max execution penalty for regular jobs. + jobReg.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + } + /** * Confirm that * {@link JobSchedulerService#getRescheduleJobForFailureLocked(JobStatus, int, int)} @@ -1226,6 +1786,7 @@ public class JobSchedulerServiceTest { mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true; mService.updateQuotaTracker(); + mService.resetScheduleQuota(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { @@ -1249,6 +1810,7 @@ public class JobSchedulerServiceTest { mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false; mService.updateQuotaTracker(); + mService.resetScheduleQuota(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { @@ -1270,6 +1832,7 @@ public class JobSchedulerServiceTest { mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true; mService.updateQuotaTracker(); + mService.resetScheduleQuota(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { @@ -1292,6 +1855,7 @@ public class JobSchedulerServiceTest { mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true; mService.updateQuotaTracker(); + mService.resetScheduleQuota(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { @@ -1315,6 +1879,7 @@ public class JobSchedulerServiceTest { mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false; mService.updateQuotaTracker(); + mService.resetScheduleQuota(); final JobInfo job = createJobInfo().setPersisted(false).build(); final JobWorkItem item = new JobWorkItem.Builder().build(); @@ -1337,6 +1902,7 @@ public class JobSchedulerServiceTest { mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false; mService.updateQuotaTracker(); + mService.resetScheduleQuota(); final JobInfo job = createJobInfo().setPersisted(true).build(); final JobWorkItem item = new JobWorkItem.Builder().build(); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java index 2180a781e437..2b56ea8bba33 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java @@ -73,6 +73,7 @@ import android.telephony.TelephonyManager; import android.util.DataUnit; import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerInternal; import com.android.server.job.JobSchedulerService; import com.android.server.job.JobSchedulerService.Constants; import com.android.server.net.NetworkPolicyManagerInternal; @@ -124,6 +125,10 @@ public class ConnectivityControllerTest { LocalServices.removeServiceForTest(NetworkPolicyManagerInternal.class); LocalServices.addService(NetworkPolicyManagerInternal.class, mNetPolicyManagerInternal); + // Used in JobStatus. + LocalServices.removeServiceForTest(JobSchedulerInternal.class); + LocalServices.addService(JobSchedulerInternal.class, mock(JobSchedulerInternal.class)); + // Freeze the clocks at this moment in time JobSchedulerService.sSystemClock = Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC); 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 05780ebe6c4b..1de7e3719112 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 @@ -21,6 +21,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; +import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX; import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX; import static com.android.server.job.JobSchedulerService.NEVER_INDEX; import static com.android.server.job.JobSchedulerService.RARE_INDEX; @@ -45,6 +46,8 @@ import static com.android.server.job.controllers.JobStatus.NO_LATEST_RUNTIME; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.when; @@ -582,6 +585,40 @@ public class JobStatusTest { } @Test + public void testGetEffectiveStandbyBucket_buggyApp() { + when(mJobSchedulerInternal.isAppConsideredBuggy( + anyInt(), anyString(), anyInt(), anyString())) + .thenReturn(true); + + final JobInfo jobInfo = new JobInfo.Builder(1234, TEST_JOB_COMPONENT).build(); + JobStatus job = createJobStatus(jobInfo); + + // Exempt apps be exempting. + job.setStandbyBucket(EXEMPTED_INDEX); + assertEquals(EXEMPTED_INDEX, job.getEffectiveStandbyBucket()); + + // Actual bucket is higher than the buggy cap, so the cap comes into effect. + job.setStandbyBucket(ACTIVE_INDEX); + assertEquals(WORKING_INDEX, job.getEffectiveStandbyBucket()); + + // Buckets at the cap or below shouldn't be affected. + job.setStandbyBucket(WORKING_INDEX); + assertEquals(WORKING_INDEX, job.getEffectiveStandbyBucket()); + + job.setStandbyBucket(FREQUENT_INDEX); + assertEquals(FREQUENT_INDEX, job.getEffectiveStandbyBucket()); + + job.setStandbyBucket(RARE_INDEX); + assertEquals(RARE_INDEX, job.getEffectiveStandbyBucket()); + + job.setStandbyBucket(RESTRICTED_INDEX); + assertEquals(RESTRICTED_INDEX, job.getEffectiveStandbyBucket()); + + job.setStandbyBucket(NEVER_INDEX); + assertEquals(NEVER_INDEX, job.getEffectiveStandbyBucket()); + } + + @Test public void testModifyingInternalFlags() { final JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar")) 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 fb59ea2bb63b..7cc01e1b4292 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 @@ -58,6 +58,7 @@ import android.util.SparseArray; import androidx.test.runner.AndroidJUnit4; import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerInternal; import com.android.server.job.JobSchedulerService; import com.android.server.job.controllers.PrefetchController.PcConstants; @@ -135,6 +136,9 @@ public class PrefetchControllerTest { when(mJobSchedulerService.getPackagesForUidLocked(anyInt())) .thenAnswer(invocationOnMock -> mPackagesForUid.get(invocationOnMock.getArgument(0))); + // Used in JobStatus. + doReturn(mock(JobSchedulerInternal.class)) + .when(() -> LocalServices.getService(JobSchedulerInternal.class)); // Freeze the clocks at 24 hours after this moment in time. Several tests create sessions // in the past, and PrefetchController sometimes floors values at 0, so if the test time 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 6f713e0fad64..dce162c58d0b 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 @@ -85,6 +85,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import com.android.server.PowerAllowlistInternal; +import com.android.server.job.JobSchedulerInternal; import com.android.server.job.JobSchedulerService; import com.android.server.job.JobStore; import com.android.server.job.controllers.QuotaController.ExecutionStats; @@ -190,6 +191,8 @@ public class QuotaControllerTest { doReturn(mPowerAllowlistInternal) .when(() -> LocalServices.getService(PowerAllowlistInternal.class)); // Used in JobStatus. + doReturn(mock(JobSchedulerInternal.class)) + .when(() -> LocalServices.getService(JobSchedulerInternal.class)); doReturn(mPackageManagerInternal) .when(() -> LocalServices.getService(PackageManagerInternal.class)); // Used in QuotaController.Handler. diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java index 51e521d8ffe4..206af5b6aea6 100644 --- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java @@ -17,8 +17,10 @@ package com.android.server.wallpaper; import static android.app.WallpaperManager.COMMAND_REAPPLY; +import static android.app.WallpaperManager.FLAG_LOCK; import static android.app.WallpaperManager.FLAG_SYSTEM; import static android.os.FileObserver.CLOSE_WRITE; +import static android.os.UserHandle.MIN_SECONDARY_USER_ID; import static android.os.UserHandle.USER_SYSTEM; import static android.view.Display.DEFAULT_DISPLAY; @@ -106,6 +108,7 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.List; /** * Tests for the {@link WallpaperManagerService} class. @@ -172,12 +175,12 @@ public class WallpaperManagerServiceTests { sImageWallpaperComponentName = ComponentName.unflattenFromString( sContext.getResources().getString(R.string.image_wallpaper_component)); // Mock default wallpaper as image wallpaper if there is no pre-defined default wallpaper. - sDefaultWallpaperComponent = WallpaperManager.getCmfDefaultWallpaperComponent(sContext); + sDefaultWallpaperComponent = WallpaperManager.getDefaultWallpaperComponent(sContext); if (sDefaultWallpaperComponent == null) { sDefaultWallpaperComponent = sImageWallpaperComponentName; doReturn(sImageWallpaperComponentName).when(() -> - WallpaperManager.getCmfDefaultWallpaperComponent(any())); + WallpaperManager.getDefaultWallpaperComponent(any())); } else { sContext.addMockService(sDefaultWallpaperComponent, sWallpaperService); } @@ -262,6 +265,25 @@ public class WallpaperManagerServiceTests { } /** + * Tests that the fundamental fields are set by the main WallpaperData constructor + */ + @Test + public void testWallpaperDataConstructor() { + final int testUserId = MIN_SECONDARY_USER_ID; + for (int which: List.of(FLAG_LOCK, FLAG_SYSTEM)) { + WallpaperData newWallpaperData = new WallpaperData(testUserId, which); + assertEquals(which, newWallpaperData.mWhich); + assertEquals(testUserId, newWallpaperData.userId); + + WallpaperData wallpaperData = mService.getWallpaperSafeLocked(testUserId, which); + assertEquals(wallpaperData.cropFile.getAbsolutePath(), + newWallpaperData.cropFile.getAbsolutePath()); + assertEquals(wallpaperData.wallpaperFile.getAbsolutePath(), + newWallpaperData.wallpaperFile.getAbsolutePath()); + } + } + + /** * Tests that internal basic data should be correct after boot up. */ @Test @@ -405,10 +427,7 @@ public class WallpaperManagerServiceTests { fail("exception occurred while writing system wallpaper attributes"); } - WallpaperData shouldMatchSystem = new WallpaperData(systemWallpaperData.userId, - systemWallpaperData.wallpaperFile.getParentFile(), - systemWallpaperData.wallpaperFile.getAbsolutePath(), - systemWallpaperData.cropFile.getAbsolutePath()); + WallpaperData shouldMatchSystem = new WallpaperData(0, FLAG_SYSTEM); try { TypedXmlPullParser parser = Xml.newBinaryPullParser(); mService.mWallpaperDataParser.parseWallpaperAttributes(parser, shouldMatchSystem, true); diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index b59f027fec83..11267268a6fa 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -1346,6 +1346,16 @@ public class TransitionTests extends WindowTestsBase { activity1.setVisible(false); abortTransition.abort(); assertTrue(activity1.isVisible()); + + // The mLaunchTaskBehind flag of an invisible initializing activity should not be cleared. + final Transition noChangeTransition = controller.createTransition(TRANSIT_OPEN); + noChangeTransition.collect(activity1); + activity1.setVisibleRequested(false); + activity1.setState(ActivityRecord.State.INITIALIZING, "test"); + activity1.mLaunchTaskBehind = true; + mWm.mSyncEngine.abort(noChangeTransition.getSyncId()); + noChangeTransition.finishTransition(); + assertTrue(activity1.mLaunchTaskBehind); } @Test |