diff options
92 files changed, 3149 insertions, 539 deletions
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index 7f02cb36dfc1..2f6b6897addb 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -432,6 +432,7 @@ public class JobInfo implements Parcelable { @UnsupportedAppUsage private final ComponentName service; private final int constraintFlags; + private final int mPreferredConstraintFlags; private final TriggerContentUri[] triggerContentUris; private final long triggerContentUpdateDelay; private final long triggerContentMaxDelay; @@ -522,6 +523,30 @@ public class JobInfo implements Parcelable { } /** + * @hide + * @see JobInfo.Builder#setPrefersBatteryNotLow(boolean) + */ + public boolean isPreferBatteryNotLow() { + return (mPreferredConstraintFlags & CONSTRAINT_FLAG_BATTERY_NOT_LOW) != 0; + } + + /** + * @hide + * @see JobInfo.Builder#setPrefersCharging(boolean) + */ + public boolean isPreferCharging() { + return (mPreferredConstraintFlags & CONSTRAINT_FLAG_CHARGING) != 0; + } + + /** + * @hide + * @see JobInfo.Builder#setPrefersDeviceIdle(boolean) + */ + public boolean isPreferDeviceIdle() { + return (mPreferredConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0; + } + + /** * @see JobInfo.Builder#setRequiresCharging(boolean) */ public boolean isRequireCharging() { @@ -557,6 +582,13 @@ public class JobInfo implements Parcelable { } /** + * @hide + */ + public int getPreferredConstraintFlags() { + return mPreferredConstraintFlags; + } + + /** * Which content: URIs must change for the job to be scheduled. Returns null * if there are none required. * @see JobInfo.Builder#addTriggerContentUri(TriggerContentUri) @@ -800,6 +832,9 @@ public class JobInfo implements Parcelable { if (constraintFlags != j.constraintFlags) { return false; } + if (mPreferredConstraintFlags != j.mPreferredConstraintFlags) { + return false; + } if (!Arrays.equals(triggerContentUris, j.triggerContentUris)) { return false; } @@ -880,6 +915,7 @@ public class JobInfo implements Parcelable { hashCode = 31 * hashCode + service.hashCode(); } hashCode = 31 * hashCode + constraintFlags; + hashCode = 31 * hashCode + mPreferredConstraintFlags; if (triggerContentUris != null) { hashCode = 31 * hashCode + Arrays.hashCode(triggerContentUris); } @@ -922,6 +958,7 @@ public class JobInfo implements Parcelable { } service = in.readParcelable(null); constraintFlags = in.readInt(); + mPreferredConstraintFlags = in.readInt(); triggerContentUris = in.createTypedArray(TriggerContentUri.CREATOR); triggerContentUpdateDelay = in.readLong(); triggerContentMaxDelay = in.readLong(); @@ -956,6 +993,7 @@ public class JobInfo implements Parcelable { clipGrantFlags = b.mClipGrantFlags; service = b.mJobService; constraintFlags = b.mConstraintFlags; + mPreferredConstraintFlags = b.mPreferredConstraintFlags; triggerContentUris = b.mTriggerContentUris != null ? b.mTriggerContentUris.toArray(new TriggerContentUri[b.mTriggerContentUris.size()]) : null; @@ -999,6 +1037,7 @@ public class JobInfo implements Parcelable { } out.writeParcelable(service, flags); out.writeInt(constraintFlags); + out.writeInt(mPreferredConstraintFlags); out.writeTypedArray(triggerContentUris, flags); out.writeLong(triggerContentUpdateDelay); out.writeLong(triggerContentMaxDelay); @@ -1146,6 +1185,7 @@ public class JobInfo implements Parcelable { private int mFlags; // Requirements. private int mConstraintFlags; + private int mPreferredConstraintFlags; private NetworkRequest mNetworkRequest; private long mNetworkDownloadBytes = NETWORK_BYTES_UNKNOWN; private long mNetworkUploadBytes = NETWORK_BYTES_UNKNOWN; @@ -1199,6 +1239,7 @@ public class JobInfo implements Parcelable { mBias = job.getBias(); mFlags = job.getFlags(); mConstraintFlags = job.getConstraintFlags(); + mPreferredConstraintFlags = job.getPreferredConstraintFlags(); mNetworkRequest = job.getRequiredNetwork(); mNetworkDownloadBytes = job.getEstimatedNetworkDownloadBytes(); mNetworkUploadBytes = job.getEstimatedNetworkUploadBytes(); @@ -1341,9 +1382,6 @@ public class JobInfo implements Parcelable { * Calling this method will override any requirements previously defined * by {@link #setRequiredNetwork(NetworkRequest)}; you typically only * want to call one of these methods. - * <p> Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, - * {@link JobScheduler} may try to shift the execution of jobs requiring - * {@link #NETWORK_TYPE_ANY} to when there is access to an un-metered network. * <p class="note"> * When your job executes in * {@link JobService#onStartJob(JobParameters)}, be sure to use the @@ -1505,10 +1543,105 @@ public class JobInfo implements Parcelable { } /** - * Specify that to run this job, the device must be charging (or be a + * Specify that this job would prefer to be run when the device's battery is not low. + * This defaults to {@code false}. + * + * <p>The system may attempt to delay this job until the device's battery is not low, + * but may choose to run it even if the device's battery is low. JobScheduler will not stop + * this job if this constraint is no longer satisfied after the job has started running. + * If this job must only run when the device's battery is not low, + * use {@link #setRequiresBatteryNotLow(boolean)} instead. + * + * <p> + * Because it doesn't make sense for a constraint to be both preferred and required, + * calling both this and {@link #setRequiresBatteryNotLow(boolean)} with {@code true} + * will result in an {@link java.lang.IllegalArgumentException} when + * {@link android.app.job.JobInfo.Builder#build()} is called. + * + * @param prefersBatteryNotLow Pass {@code true} to prefer that the device's battery level + * not be low in order to run the job. + * @return This object for method chaining + * @see JobInfo#isPreferBatteryNotLow() + * @hide + */ + @NonNull + public Builder setPrefersBatteryNotLow(boolean prefersBatteryNotLow) { + mPreferredConstraintFlags = + (mPreferredConstraintFlags & ~CONSTRAINT_FLAG_BATTERY_NOT_LOW) + | (prefersBatteryNotLow ? CONSTRAINT_FLAG_BATTERY_NOT_LOW : 0); + return this; + } + + /** + * Specify that this job would prefer to be run when the device is charging (or be a * non-battery-powered device connected to permanent power, such as Android TV * devices). This defaults to {@code false}. * + * <p> + * The system may attempt to delay this job until the device is charging, but may + * choose to run it even if the device is not charging. JobScheduler will not stop + * this job if this constraint is no longer satisfied after the job has started running. + * If this job must only run when the device is charging, + * use {@link #setRequiresCharging(boolean)} instead. + * + * <p> + * Because it doesn't make sense for a constraint to be both preferred and required, + * calling both this and {@link #setRequiresCharging(boolean)} with {@code true} + * will result in an {@link java.lang.IllegalArgumentException} when + * {@link android.app.job.JobInfo.Builder#build()} is called. + * + * @param prefersCharging Pass {@code true} to prefer that the device be + * charging in order to run the job. + * @return This object for method chaining + * @see JobInfo#isPreferCharging() + * @hide + */ + @NonNull + public Builder setPrefersCharging(boolean prefersCharging) { + mPreferredConstraintFlags = (mPreferredConstraintFlags & ~CONSTRAINT_FLAG_CHARGING) + | (prefersCharging ? CONSTRAINT_FLAG_CHARGING : 0); + return this; + } + + /** + * Specify that this job would prefer to be run when the device is not in active use. + * This defaults to {@code false}. + * + * <p>The system may attempt to delay this job until the device is not in active use, + * but may choose to run it even if the device is not idle. JobScheduler will not stop + * this job if this constraint is no longer satisfied after the job has started running. + * If this job must only run when the device is not in active use, + * use {@link #setRequiresDeviceIdle(boolean)} instead. + * + * <p> + * Because it doesn't make sense for a constraint to be both preferred and required, + * calling both this and {@link #setRequiresDeviceIdle(boolean)} with {@code true} + * will result in an {@link java.lang.IllegalArgumentException} when + * {@link android.app.job.JobInfo.Builder#build()} is called. + * + * <p class="note">Despite the similar naming, this job constraint is <em>not</em> + * related to the system's "device idle" or "doze" states. This constraint only + * determines whether a job is allowed to run while the device is directly in use. + * + * @param prefersDeviceIdle Pass {@code true} to prefer that the device not be in active + * use when running this job. + * @return This object for method chaining + * @see JobInfo#isRequireDeviceIdle() + * @hide + */ + @NonNull + public Builder setPrefersDeviceIdle(boolean prefersDeviceIdle) { + mPreferredConstraintFlags = (mPreferredConstraintFlags & ~CONSTRAINT_FLAG_DEVICE_IDLE) + | (prefersDeviceIdle ? CONSTRAINT_FLAG_DEVICE_IDLE : 0); + return this; + } + + /** + * Specify that to run this job, the device must be charging (or be a + * non-battery-powered device connected to permanent power, such as Android TV + * devices). This defaults to {@code false}. Setting this to {@code false} <b>DOES NOT</b> + * mean the job will only run when the device is not charging. + * * <p class="note">For purposes of running jobs, a battery-powered device * "charging" is not quite the same as simply being connected to power. If the * device is so busy that the battery is draining despite a power connection, jobs @@ -1530,7 +1663,9 @@ public class JobInfo implements Parcelable { * Specify that to run this job, the device's battery level must not be low. * This defaults to false. If true, the job will only run when the battery level * is not low, which is generally the point where the user is given a "low battery" - * warning. + * warning. Setting this to {@code false} <b>DOES NOT</b> mean the job will only run + * when the battery is low. + * * @param batteryNotLow Whether or not the device's battery level must not be low. * @see JobInfo#isRequireBatteryNotLow() */ @@ -1543,7 +1678,8 @@ public class JobInfo implements Parcelable { /** * When set {@code true}, ensure that this job will not run if the device is in active use. * The default state is {@code false}: that is, the for the job to be runnable even when - * someone is interacting with the device. + * someone is interacting with the device. Setting this to {@code false} <b>DOES NOT</b> + * mean the job will only run when the device is not idle. * * <p>This state is a loose definition provided by the system. In general, it means that * the device is not currently being used interactively, and has not been in use for some @@ -2156,6 +2292,29 @@ public class JobInfo implements Parcelable { } } + if ((constraintFlags & mPreferredConstraintFlags) != 0) { + // Something is marked as both preferred and required. Try to give a clear exception + // reason. + if ((constraintFlags & CONSTRAINT_FLAG_BATTERY_NOT_LOW) != 0 + && (mPreferredConstraintFlags & CONSTRAINT_FLAG_BATTERY_NOT_LOW) != 0) { + throw new IllegalArgumentException( + "battery-not-low constraint cannot be both preferred and required"); + } + if ((constraintFlags & CONSTRAINT_FLAG_CHARGING) != 0 + && (mPreferredConstraintFlags & CONSTRAINT_FLAG_CHARGING) != 0) { + throw new IllegalArgumentException( + "charging constraint cannot be both preferred and required"); + } + if ((constraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0 + && (mPreferredConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) { + throw new IllegalArgumentException( + "device idle constraint cannot be both preferred and required"); + } + // Couldn't figure out what the overlap was. Just use a generic message. + throw new IllegalArgumentException( + "constraints cannot be both preferred and required"); + } + if (isUserInitiated) { if (hasEarlyConstraint) { throw new IllegalArgumentException("A user-initiated job cannot have a time delay"); @@ -2173,7 +2332,8 @@ public class JobInfo implements Parcelable { if (mPriority != PRIORITY_MAX) { throw new IllegalArgumentException("A user-initiated job must be max priority."); } - if ((constraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) { + if ((constraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0 + || (mPreferredConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) { throw new IllegalArgumentException( "A user-initiated job cannot have a device-idle constraint"); } diff --git a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java index 9b64edf53d8c..f50a90248030 100644 --- a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java +++ b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java @@ -225,6 +225,8 @@ public interface AppStandbyInternal { void setActiveAdminApps(Set<String> adminPkgs, int userId); + void setAdminProtectedPackages(Set<String> packageNames, int userId); + /** * @return {@code true} if the given package is an active device admin app. */ 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 056b6b913255..08810b549e62 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -483,7 +483,7 @@ public class JobSchedulerService extends com.android.server.SystemService case Constants.KEY_RUNTIME_UI_LIMIT_MS: case Constants.KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR: case Constants.KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS: - case Constants.KEY_RUNTIME_UI_DATA_TRANSFER_LIMIT_MS: + case Constants.KEY_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS: if (!runtimeUpdated) { mConstants.updateRuntimeConstantsLocked(); runtimeUpdated = true; @@ -583,8 +583,8 @@ public class JobSchedulerService extends com.android.server.SystemService "runtime_min_ui_data_transfer_guarantee_buffer_factor"; private static final String KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS = "runtime_min_ui_data_transfer_guarantee_ms"; - private static final String KEY_RUNTIME_UI_DATA_TRANSFER_LIMIT_MS = - "runtime_ui_data_transfer_limit_ms"; + private static final String KEY_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS = + "runtime_use_data_estimates_for_limits"; private static final String KEY_PERSIST_IN_SPLIT_FILES = "persist_in_split_files"; @@ -616,15 +616,14 @@ public class JobSchedulerService extends com.android.server.SystemService @VisibleForTesting public static final long DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS = 3 * MINUTE_IN_MILLIS; public static final long DEFAULT_RUNTIME_MIN_UI_GUARANTEE_MS = - Math.max(10 * MINUTE_IN_MILLIS, DEFAULT_RUNTIME_MIN_GUARANTEE_MS); + Math.max(6 * HOUR_IN_MILLIS, DEFAULT_RUNTIME_MIN_GUARANTEE_MS); public static final long DEFAULT_RUNTIME_UI_LIMIT_MS = - Math.max(60 * MINUTE_IN_MILLIS, DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS); + Math.max(12 * HOUR_IN_MILLIS, DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS); public static final float DEFAULT_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR = 1.35f; public static final long DEFAULT_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS = Math.max(10 * MINUTE_IN_MILLIS, DEFAULT_RUNTIME_MIN_UI_GUARANTEE_MS); - public static final long DEFAULT_RUNTIME_UI_DATA_TRANSFER_LIMIT_MS = - Math.min(Long.MAX_VALUE, DEFAULT_RUNTIME_UI_LIMIT_MS); + public static final boolean DEFAULT_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS = false; static final boolean DEFAULT_PERSIST_IN_SPLIT_FILES = true; static final int DEFAULT_MAX_NUM_PERSISTED_JOB_WORK_ITEMS = 100_000; @@ -755,13 +754,17 @@ public class JobSchedulerService extends com.android.server.SystemService /** * The minimum amount of time we try to guarantee user-initiated data transfer jobs - * will run for. + * will run for. This is only considered when using data estimates to calculate + * execution limits. */ public long RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS = DEFAULT_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS; - /** The maximum amount of time we will let a user-initiated data transfer job run for. */ - public long RUNTIME_UI_DATA_TRANSFER_LIMIT_MS = DEFAULT_RUNTIME_UI_DATA_TRANSFER_LIMIT_MS; + /** + * Whether to use data estimates to determine execution limits for execution limits. + */ + public boolean RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS = + DEFAULT_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS; /** * Whether to persist jobs in split files (by UID). If false, all persisted jobs will be @@ -879,7 +882,7 @@ public class JobSchedulerService extends com.android.server.SystemService KEY_RUNTIME_MIN_UI_GUARANTEE_MS, KEY_RUNTIME_UI_LIMIT_MS, KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS, - KEY_RUNTIME_UI_DATA_TRANSFER_LIMIT_MS); + KEY_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS); // Make sure min runtime for regular jobs is at least 10 minutes. RUNTIME_MIN_GUARANTEE_MS = Math.max(10 * MINUTE_IN_MILLIS, @@ -913,15 +916,10 @@ public class JobSchedulerService extends com.android.server.SystemService properties.getLong( KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS, DEFAULT_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS)); - // User-initiated requires RUN_USER_INITIATED_JOBS permission, so the upper limit will - // be higher than other jobs. - // Max limit should be the min guarantee and the max of other user-initiated jobs. - RUNTIME_UI_DATA_TRANSFER_LIMIT_MS = Math.max( - RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS, - Math.max(RUNTIME_UI_LIMIT_MS, - properties.getLong( - KEY_RUNTIME_UI_DATA_TRANSFER_LIMIT_MS, - DEFAULT_RUNTIME_UI_DATA_TRANSFER_LIMIT_MS))); + + RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS = properties.getBoolean( + KEY_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS, + DEFAULT_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS); } private boolean updateTareSettingsLocked(@EconomyManager.EnabledMode int enabledMode) { @@ -975,8 +973,8 @@ public class JobSchedulerService extends com.android.server.SystemService RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR).println(); pw.print(KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS, RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS).println(); - pw.print(KEY_RUNTIME_UI_DATA_TRANSFER_LIMIT_MS, - RUNTIME_UI_DATA_TRANSFER_LIMIT_MS).println(); + pw.print(KEY_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS, + RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS).println(); pw.print(KEY_PERSIST_IN_SPLIT_FILES, PERSIST_IN_SPLIT_FILES).println(); pw.print(KEY_MAX_NUM_PERSISTED_JOB_WORK_ITEMS, MAX_NUM_PERSISTED_JOB_WORK_ITEMS) @@ -3306,20 +3304,24 @@ public class JobSchedulerService extends com.android.server.SystemService if (job.shouldTreatAsUserInitiatedJob() && checkRunUserInitiatedJobsPermission( job.getSourceUid(), job.getSourcePackageName())) { - if (job.getJob().getRequiredNetwork() != null) { // UI+DT - final long estimatedTransferTimeMs = - mConnectivityController.getEstimatedTransferTimeMs(job); - if (estimatedTransferTimeMs == ConnectivityController.UNKNOWN_TIME) { - return mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_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; + } + // 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, + Math.max(factoredTransferTimeMs, + 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_DATA_TRANSFER_LIMIT_MS, - 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 mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; } else if (job.shouldTreatAsExpeditedJob()) { @@ -3336,13 +3338,9 @@ public class JobSchedulerService extends com.android.server.SystemService /** Returns the maximum amount of time this job could run for. */ public long getMaxJobExecutionTimeMs(JobStatus job) { synchronized (mLock) { - final boolean allowLongerJob = job.shouldTreatAsUserInitiatedJob() + if (job.shouldTreatAsUserInitiatedJob() && checkRunUserInitiatedJobsPermission( - job.getSourceUid(), job.getSourcePackageName()); - if (job.getJob().getRequiredNetwork() != null && allowLongerJob) { // UI+DT - return mConstants.RUNTIME_UI_DATA_TRANSFER_LIMIT_MS; - } - if (allowLongerJob) { // UI with LRJ permission + job.getSourceUid(), job.getSourcePackageName())) { return mConstants.RUNTIME_UI_LIMIT_MS; } if (job.shouldTreatAsUserInitiatedJob()) { diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java index 0dcb0b2456d6..a96a4ef951ea 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java @@ -883,6 +883,15 @@ public final class JobStore { if (job.isRequireStorageNotLow()) { out.attribute(null, "storage-not-low", Boolean.toString(true)); } + if (job.isPreferBatteryNotLow()) { + out.attributeBoolean(null, "prefer-battery-not-low", true); + } + if (job.isPreferCharging()) { + out.attributeBoolean(null, "prefer-charging", true); + } + if (job.isPreferDeviceIdle()) { + out.attributeBoolean(null, "prefer-idle", true); + } out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS); } @@ -1538,6 +1547,13 @@ public final class JobStore { if (val != null) { jobBuilder.setRequiresStorageNotLow(true); } + + jobBuilder.setPrefersBatteryNotLow( + parser.getAttributeBoolean(null, "prefer-battery-not-low", false)); + jobBuilder.setPrefersCharging( + parser.getAttributeBoolean(null, "prefer-charging", false)); + jobBuilder.setPrefersDeviceIdle( + parser.getAttributeBoolean(null, "prefer-idle", false)); } /** diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java index e55bda7fab02..3859d89c22cd 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java @@ -1147,10 +1147,9 @@ public final class ConnectivityController extends RestrictingController implemen final boolean changed = jobStatus.setConnectivityConstraintSatisfied(nowElapsed, satisfied); + jobStatus.setHasAccessToUnmetered(satisfied && capabilities != null + && capabilities.hasCapability(NET_CAPABILITY_NOT_METERED)); if (jobStatus.getPreferUnmetered()) { - jobStatus.setHasAccessToUnmetered(satisfied && capabilities != null - && capabilities.hasCapability(NET_CAPABILITY_NOT_METERED)); - jobStatus.setFlexibilityConstraintSatisfied(nowElapsed, mFlexibilityController.isFlexibilitySatisfiedLocked(jobStatus)); } diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java index 620c48d2c343..234a93c8d168 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java @@ -239,14 +239,14 @@ public final class FlexibilityController extends StateController { return !mFlexibilityEnabled || mService.getUidBias(js.getSourceUid()) == JobInfo.BIAS_TOP_APP || mService.isCurrentlyRunningLocked(js) - || getNumSatisfiedRequiredConstraintsLocked(js) + || getNumSatisfiedFlexibleConstraintsLocked(js) >= js.getNumRequiredFlexibleConstraints(); } @VisibleForTesting @GuardedBy("mLock") - int getNumSatisfiedRequiredConstraintsLocked(JobStatus js) { - return Integer.bitCount(mSatisfiedFlexibleConstraints) + int getNumSatisfiedFlexibleConstraintsLocked(JobStatus js) { + return Integer.bitCount(mSatisfiedFlexibleConstraints & js.getPreferredConstraintFlags()) + (js.getHasAccessToUnmetered() ? 1 : 0); } @@ -651,7 +651,7 @@ public final class FlexibilityController extends StateController { static final String KEY_RESCHEDULED_JOB_DEADLINE_MS = FC_CONFIG_PREFIX + "rescheduled_job_deadline_ms"; - private static final boolean DEFAULT_FLEXIBILITY_ENABLED = false; + private static final boolean DEFAULT_FLEXIBILITY_ENABLED = true; @VisibleForTesting static final long DEFAULT_DEADLINE_PROXIMITY_LIMIT_MS = 15 * MINUTE_IN_MILLIS; @VisibleForTesting 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 0cc775870f88..17dde9098926 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 @@ -16,7 +16,6 @@ package com.android.server.job.controllers; -import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; @@ -25,8 +24,6 @@ import static com.android.server.job.JobSchedulerService.NEVER_INDEX; import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX; import static com.android.server.job.JobSchedulerService.WORKING_INDEX; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; -import static com.android.server.job.controllers.FlexibilityController.NUM_SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS; -import static com.android.server.job.controllers.FlexibilityController.SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS; import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; @@ -115,12 +112,11 @@ public final class JobStatus { static final int CONSTRAINT_WITHIN_QUOTA = 1 << 24; // Implicit constraint static final int CONSTRAINT_PREFETCH = 1 << 23; static final int CONSTRAINT_BACKGROUND_NOT_RESTRICTED = 1 << 22; // Implicit constraint - static final int CONSTRAINT_FLEXIBLE = 1 << 21; // Implicit constraint + static final int CONSTRAINT_FLEXIBLE = 1 << 21; private static final int IMPLICIT_CONSTRAINTS = 0 | CONSTRAINT_BACKGROUND_NOT_RESTRICTED | CONSTRAINT_DEVICE_NOT_DOZING - | CONSTRAINT_FLEXIBLE | CONSTRAINT_TARE_WEALTH | CONSTRAINT_WITHIN_QUOTA; @@ -298,6 +294,7 @@ public final class JobStatus { // Constraints. final int requiredConstraints; + private final int mPreferredConstraints; private final int mRequiredConstraintsOfInterest; int satisfiedConstraints = 0; private int mSatisfiedConstraintsOfInterest = 0; @@ -618,24 +615,26 @@ public final class JobStatus { } mHasExemptedMediaUrisOnly = exemptedMediaUrisOnly; - mPreferUnmetered = job.getRequiredNetwork() != null - && !job.getRequiredNetwork().hasCapability(NET_CAPABILITY_NOT_METERED); + mPreferredConstraints = job.getPreferredConstraintFlags(); + + // Exposing a preferredNetworkRequest API requires that we make sure that the preferred + // NetworkRequest is a subset of the required NetworkRequest. We currently don't have the + // code to ensure that, so disable this part for now. + // TODO(236261941): look into enabling flexible network constraint requests + mPreferUnmetered = false; + // && job.getRequiredNetwork() != null + // && !job.getRequiredNetwork().hasCapability(NET_CAPABILITY_NOT_METERED); - final boolean lacksSomeFlexibleConstraints = - ((~requiredConstraints) & SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS) != 0 - || mPreferUnmetered; final boolean satisfiesMinWindowException = (latestRunTimeElapsedMillis - earliestRunTimeElapsedMillis) >= MIN_WINDOW_FOR_FLEXIBILITY_MS; // The first time a job is rescheduled it will not be subject to flexible constraints. // Otherwise, every consecutive reschedule increases a jobs' flexibility deadline. - if (!isRequestedExpeditedJob() && !job.isUserInitiated() + if (mPreferredConstraints != 0 && !isRequestedExpeditedJob() && !job.isUserInitiated() && satisfiesMinWindowException - && (numFailures + numSystemStops) != 1 - && lacksSomeFlexibleConstraints) { - mNumRequiredFlexibleConstraints = - NUM_SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS + (mPreferUnmetered ? 1 : 0); + && (numFailures + numSystemStops) != 1) { + mNumRequiredFlexibleConstraints = Integer.bitCount(mPreferredConstraints); requiredConstraints |= CONSTRAINT_FLEXIBLE; } else { mNumRequiredFlexibleConstraints = 0; @@ -1142,6 +1141,10 @@ public final class JobStatus { mInternalFlags |= flags; } + int getPreferredConstraintFlags() { + return mPreferredConstraints; + } + public int getSatisfiedConstraintFlags() { return satisfiedConstraints; } @@ -2501,6 +2504,9 @@ public final class JobStatus { pw.print("Required constraints:"); dumpConstraints(pw, requiredConstraints); pw.println(); + pw.print("Preferred constraints:"); + dumpConstraints(pw, mPreferredConstraints); + pw.println(); pw.print("Dynamic constraints:"); dumpConstraints(pw, mDynamicConstraints); pw.println(); 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 c3118ff96413..ab0a8adb5daf 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java @@ -268,6 +268,10 @@ public class AppStandbyController @GuardedBy("mActiveAdminApps") private final SparseArray<Set<String>> mActiveAdminApps = new SparseArray<>(); + /** List of admin protected packages. Can contain {@link android.os.UserHandle#USER_ALL}. */ + @GuardedBy("mAdminProtectedPackages") + private final SparseArray<Set<String>> mAdminProtectedPackages = new SparseArray<>(); + /** * Set of system apps that are headless (don't have any "front door" activities, enabled or * disabled). Presence in this map indicates that the app is a headless system app. @@ -1380,6 +1384,9 @@ public class AppStandbyController synchronized (mActiveAdminApps) { mActiveAdminApps.remove(userId); } + synchronized (mAdminProtectedPackages) { + mAdminProtectedPackages.remove(userId); + } } } @@ -1469,6 +1476,10 @@ public class AppStandbyController return STANDBY_BUCKET_EXEMPTED; } + if (isAdminProtectedPackages(packageName, userId)) { + return STANDBY_BUCKET_EXEMPTED; + } + if (isActiveNetworkScorer(packageName)) { return STANDBY_BUCKET_EXEMPTED; } @@ -1948,6 +1959,17 @@ public class AppStandbyController } } + private boolean isAdminProtectedPackages(String packageName, int userId) { + synchronized (mAdminProtectedPackages) { + if (mAdminProtectedPackages.contains(UserHandle.USER_ALL) + && mAdminProtectedPackages.get(UserHandle.USER_ALL).contains(packageName)) { + return true; + } + return mAdminProtectedPackages.contains(userId) + && mAdminProtectedPackages.get(userId).contains(packageName); + } + } + @Override public void addActiveDeviceAdmin(String adminPkg, int userId) { synchronized (mActiveAdminApps) { @@ -1972,6 +1994,17 @@ public class AppStandbyController } @Override + public void setAdminProtectedPackages(Set<String> packageNames, int userId) { + synchronized (mAdminProtectedPackages) { + if (packageNames == null || packageNames.isEmpty()) { + mAdminProtectedPackages.remove(userId); + } else { + mAdminProtectedPackages.put(userId, packageNames); + } + } + } + + @Override public void onAdminDataAvailable() { mAdminDataAvailableLatch.countDown(); } @@ -1993,6 +2026,13 @@ public class AppStandbyController } } + @VisibleForTesting + Set<String> getAdminProtectedPackagesForTest(int userId) { + synchronized (mAdminProtectedPackages) { + return mAdminProtectedPackages.get(userId); + } + } + /** * Returns {@code true} if the supplied package is the device provisioning app. Otherwise, * returns {@code false}. diff --git a/core/api/current.txt b/core/api/current.txt index 771d56cd1987..2acbc35a387c 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -27348,9 +27348,13 @@ package android.media.tv { field public static final int TIME_SHIFT_STATUS_UNAVAILABLE = 2; // 0x2 field public static final int TIME_SHIFT_STATUS_UNKNOWN = 0; // 0x0 field public static final int TIME_SHIFT_STATUS_UNSUPPORTED = 1; // 0x1 + field public static final long TV_MESSAGE_GROUP_ID_NONE = -1L; // 0xffffffffffffffffL + field public static final String TV_MESSAGE_KEY_GROUP_ID = "android.media.tv.TvInputManager.group_id"; field public static final String TV_MESSAGE_KEY_RAW_DATA = "android.media.tv.TvInputManager.raw_data"; field public static final String TV_MESSAGE_KEY_STREAM_ID = "android.media.tv.TvInputManager.stream_id"; field public static final String TV_MESSAGE_KEY_SUBTYPE = "android.media.tv.TvInputManager.subtype"; + field public static final String TV_MESSAGE_SUBTYPE_CC_608E = "CTA 608-E"; + field public static final String TV_MESSAGE_SUBTYPE_WATERMARKING_A335 = "ATSC A/335"; field public static final int TV_MESSAGE_TYPE_CLOSED_CAPTION = 2; // 0x2 field public static final int TV_MESSAGE_TYPE_OTHER = 1000; // 0x3e8 field public static final int TV_MESSAGE_TYPE_WATERMARK = 1; // 0x1 diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 6bfaa352f3f7..658a25dd1faa 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1411,8 +1411,12 @@ package android.hardware.biometrics { package android.hardware.camera2 { public final class CameraManager { + method @NonNull public android.hardware.camera2.CameraCharacteristics getCameraCharacteristics(@NonNull String, boolean) throws android.hardware.camera2.CameraAccessException; method public String[] getCameraIdListNoLazy() throws android.hardware.camera2.CameraAccessException; + method @RequiresPermission(android.Manifest.permission.CAMERA) public void openCamera(@NonNull String, boolean, @Nullable android.os.Handler, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException; method @RequiresPermission(allOf={android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA}) public void openCamera(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException; + method public static boolean shouldOverrideToPortrait(@Nullable android.content.pm.PackageManager, @Nullable String); + field public static final String LANDSCAPE_TO_PORTRAIT_PROP = "camera.enable_landscape_to_portrait"; field public static final long OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT = 250678880L; // 0xef10e60L } diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 90427cb51c26..b96f8c94aaa3 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -994,6 +994,16 @@ public abstract class ActivityManagerInternal { */ public abstract void logFgsApiEnd(int apiType, int uid, int pid); + /** + * Temporarily allow foreground service started by an uid to have while-in-use permission + * for durationMs. + * + * @param uid The UID of the app that starts the foreground service. + * @param durationMs elapsedRealTime duration in milliseconds. + * @hide + */ + public abstract void tempAllowWhileInUsePermissionInFgs(int uid, long durationMs); + /** * The list of the events about the {@link android.media.projection.IMediaProjection} itself. * diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 6bbbfe1ef4b0..5be43fc8e8f7 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -15840,9 +15840,8 @@ public class DevicePolicyManager { * Called by a device owner or a profile owner or holder of the permission * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APPS_CONTROL} to disable user * control over apps. User will not be able to clear app data or force-stop packages. When - * called by a device owner, applies to all users on the device. Starting from Android 13, - * packages with user control disabled are exempted from being put in the "restricted" App - * Standby Bucket. + * called by a device owner, applies to all users on the device. Packages with user control + * disabled are exempted from App Standby Buckets. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. Null if the * caller is not a device admin. diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index e6b306955ef0..696873f7e5e4 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -121,6 +121,7 @@ public final class CameraManager { * System property for allowing the above * @hide */ + @TestApi public static final String LANDSCAPE_TO_PORTRAIT_PROP = "camera.enable_landscape_to_portrait"; @@ -622,6 +623,16 @@ public final class CameraManager { @NonNull public CameraCharacteristics getCameraCharacteristics(@NonNull String cameraId) throws CameraAccessException { + return getCameraCharacteristics(cameraId, shouldOverrideToPortrait(mContext)); + } + + /** + * @hide + */ + @TestApi + @NonNull + public CameraCharacteristics getCameraCharacteristics(@NonNull String cameraId, + boolean overrideToPortrait) throws CameraAccessException { CameraCharacteristics characteristics = null; if (CameraManagerGlobal.sCameraServiceDisabled) { throw new IllegalArgumentException("No cameras available on device"); @@ -635,7 +646,6 @@ public final class CameraManager { try { Size displaySize = getDisplaySize(); - boolean overrideToPortrait = shouldOverrideToPortrait(mContext); CameraMetadataNative info = cameraService.getCameraCharacteristics(cameraId, mContext.getApplicationInfo().targetSdkVersion, overrideToPortrait); try { @@ -727,7 +737,7 @@ public final class CameraManager { */ private CameraDevice openCameraDeviceUserAsync(String cameraId, CameraDevice.StateCallback callback, Executor executor, final int uid, - final int oomScoreOffset) throws CameraAccessException { + final int oomScoreOffset, boolean overrideToPortrait) throws CameraAccessException { CameraCharacteristics characteristics = getCameraCharacteristics(cameraId); CameraDevice device = null; Map<String, CameraCharacteristics> physicalIdsToChars = @@ -755,7 +765,6 @@ public final class CameraManager { "Camera service is currently unavailable"); } - boolean overrideToPortrait = shouldOverrideToPortrait(mContext); cameraUser = cameraService.connectDevice(callbacks, cameraId, mContext.getOpPackageName(), mContext.getAttributionTag(), uid, oomScoreOffset, mContext.getApplicationInfo().targetSdkVersion, @@ -891,6 +900,43 @@ public final class CameraManager { } /** + * Open a connection to a camera with the given ID. Also specify overrideToPortrait for testing. + * + * @param cameraId + * The unique identifier of the camera device to open + * @param handler + * The handler on which the callback should be invoked, or + * {@code null} to use the current thread's {@link android.os.Looper looper}. + * @param callback + * The callback which is invoked once the camera is opened + * @param overrideToPortrait + * Whether to apply the landscape to portrait override, using rotate and crop. + * + * @throws CameraAccessException if the camera is disabled by device policy, + * has been disconnected, or is being used by a higher-priority camera API client. + * + * @throws IllegalArgumentException if cameraId, the callback or the executor was null, + * or the cameraId does not match any currently or previously available + * camera device. + * + * @throws SecurityException if the application does not have permission to + * access the camera + * + * @see #getCameraIdList + * @see android.app.admin.DevicePolicyManager#setCameraDisabled + * + * @hide + */ + @TestApi + @RequiresPermission(android.Manifest.permission.CAMERA) + public void openCamera(@NonNull String cameraId, boolean overrideToPortrait, + @Nullable Handler handler, + @NonNull final CameraDevice.StateCallback callback) throws CameraAccessException { + openCameraForUid(cameraId, callback, CameraDeviceImpl.checkAndWrapHandler(handler), + USE_CALLING_UID, /*oomScoreOffset*/0, overrideToPortrait); + } + + /** * Open a connection to a camera with the given ID. * * <p>The behavior of this method matches that of @@ -994,7 +1040,8 @@ public final class CameraManager { throw new IllegalArgumentException( "oomScoreOffset < 0, cannot increase priority of camera client"); } - openCameraForUid(cameraId, callback, executor, USE_CALLING_UID, oomScoreOffset); + openCameraForUid(cameraId, callback, executor, USE_CALLING_UID, oomScoreOffset, + shouldOverrideToPortrait(mContext)); } /** @@ -1016,7 +1063,8 @@ public final class CameraManager { */ public void openCameraForUid(@NonNull String cameraId, @NonNull final CameraDevice.StateCallback callback, @NonNull Executor executor, - int clientUid, int oomScoreOffset) throws CameraAccessException { + int clientUid, int oomScoreOffset, boolean overrideToPortrait) + throws CameraAccessException { if (cameraId == null) { throw new IllegalArgumentException("cameraId was null"); @@ -1027,7 +1075,8 @@ public final class CameraManager { throw new IllegalArgumentException("No cameras available on device"); } - openCameraDeviceUserAsync(cameraId, callback, executor, clientUid, oomScoreOffset); + openCameraDeviceUserAsync(cameraId, callback, executor, clientUid, oomScoreOffset, + overrideToPortrait); } /** @@ -1048,7 +1097,8 @@ public final class CameraManager { public void openCameraForUid(@NonNull String cameraId, @NonNull final CameraDevice.StateCallback callback, @NonNull Executor executor, int clientUid) throws CameraAccessException { - openCameraForUid(cameraId, callback, executor, clientUid, /*oomScoreOffset*/0); + openCameraForUid(cameraId, callback, executor, clientUid, /*oomScoreOffset*/0, + shouldOverrideToPortrait(mContext)); } /** @@ -1191,17 +1241,32 @@ public final class CameraManager { * @hide */ public static boolean shouldOverrideToPortrait(@Nullable Context context) { + PackageManager packageManager = null; + String packageName = null; + + if (context != null) { + packageManager = context.getPackageManager(); + packageName = context.getOpPackageName(); + } + + return shouldOverrideToPortrait(packageManager, packageName); + } + + /** + * @hide + */ + @TestApi + public static boolean shouldOverrideToPortrait(@Nullable PackageManager packageManager, + @Nullable String packageName) { if (!CameraManagerGlobal.sLandscapeToPortrait) { return false; } - if (context != null) { - PackageManager packageManager = context.getPackageManager(); - + if (packageManager != null && packageName != null) { try { return packageManager.getProperty( PackageManager.PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT, - context.getOpPackageName()).getBoolean(); + packageName).getBoolean(); } catch (PackageManager.NameNotFoundException e) { // No such property } diff --git a/core/java/android/hardware/radio/IRadioService.aidl b/core/java/android/hardware/radio/IRadioService.aidl index c7131a7fe9c9..9349cf7d0c32 100644 --- a/core/java/android/hardware/radio/IRadioService.aidl +++ b/core/java/android/hardware/radio/IRadioService.aidl @@ -31,7 +31,7 @@ interface IRadioService { List<RadioManager.ModuleProperties> listModules(); ITuner openTuner(int moduleId, in RadioManager.BandConfig bandConfig, boolean withAudio, - in ITunerCallback callback, int targetSdkVersion); + in ITunerCallback callback); ICloseHandle addAnnouncementListener(in int[] enabledTypes, in IAnnouncementListener listener); diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java index f072e3b43b4a..8c6083ce49b6 100644 --- a/core/java/android/hardware/radio/RadioManager.java +++ b/core/java/android/hardware/radio/RadioManager.java @@ -1796,7 +1796,7 @@ public class RadioManager { ITuner tuner; TunerCallbackAdapter halCallback = new TunerCallbackAdapter(callback, handler); try { - tuner = mService.openTuner(moduleId, config, withAudio, halCallback, mTargetSdkVersion); + tuner = mService.openTuner(moduleId, config, withAudio, halCallback); } catch (RemoteException | IllegalArgumentException | IllegalStateException ex) { Log.e(TAG, "Failed to open tuner", ex); return null; @@ -1873,7 +1873,6 @@ public class RadioManager { @NonNull private final Context mContext; @NonNull private final IRadioService mService; - private final int mTargetSdkVersion; /** * @hide @@ -1890,6 +1889,5 @@ public class RadioManager { public RadioManager(Context context, IRadioService service) { mContext = context; mService = service; - mTargetSdkVersion = mContext.getApplicationInfo().targetSdkVersion; } } diff --git a/core/java/android/window/TaskConstants.java b/core/java/android/window/TaskConstants.java index c40384027adf..3a04198a3add 100644 --- a/core/java/android/window/TaskConstants.java +++ b/core/java/android/window/TaskConstants.java @@ -80,14 +80,6 @@ public class TaskConstants { public static final int TASK_CHILD_LAYER_TASK_OVERLAY = 5 * TASK_CHILD_LAYER_REGION_SIZE; /** - * Legacy machanism to force an activity to the top of the task (i.e. for work profile - * comfirmation). - * @hide - */ - public static final int TASK_CHILD_LAYER_TASK_OVERLAY_ACTIVITIES = - 6 * TASK_CHILD_LAYER_REGION_SIZE; - - /** * Z-orders of task child layers other than activities, task fragments and layers interleaved * with them, e.g. IME windows. [-10000, 10000) is reserved for these layers. * @hide @@ -99,8 +91,7 @@ public class TaskConstants { TASK_CHILD_LAYER_LETTERBOX_EDUCATION, TASK_CHILD_LAYER_WINDOW_DECORATIONS, TASK_CHILD_LAYER_RECENTS_ANIMATION_PIP_OVERLAY, - TASK_CHILD_LAYER_TASK_OVERLAY, - TASK_CHILD_LAYER_TASK_OVERLAY_ACTIVITIES + TASK_CHILD_LAYER_TASK_OVERLAY }) public @interface TaskChildLayer {} } diff --git a/core/tests/BroadcastRadioTests/AndroidManifest.xml b/core/tests/BroadcastRadioTests/AndroidManifest.xml index 8f655efe4f07..fef3d16a183b 100644 --- a/core/tests/BroadcastRadioTests/AndroidManifest.xml +++ b/core/tests/BroadcastRadioTests/AndroidManifest.xml @@ -18,6 +18,8 @@ package="com.android.frameworks.broadcastradiotests"> <uses-permission android:name="android.permission.ACCESS_BROADCAST_RADIO" /> + <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE" /> + <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" /> <application android:debuggable="true"> <uses-library android:name="android.test.runner" /> diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramListTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramListTest.java index 75f8c954be5e..7c3d2f2138a4 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramListTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramListTest.java @@ -580,7 +580,7 @@ public final class ProgramListTest { doAnswer(invocation -> { mTunerCallback = (ITunerCallback) invocation.getArguments()[3]; return mTunerMock; - }).when(mRadioServiceMock).openTuner(anyInt(), any(), anyBoolean(), any(), anyInt()); + }).when(mRadioServiceMock).openTuner(anyInt(), any(), anyBoolean(), any()); mRadioTuner = radioManager.openTuner(/* moduleId= */ 0, band, /* withAudio= */ true, mTunerCallbackMock, /* handler= */ null); diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java index ce3e019520c8..b9f4c3fa0a77 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java @@ -30,7 +30,6 @@ import static org.mockito.Mockito.when; import android.annotation.Nullable; import android.content.Context; import android.content.pm.ApplicationInfo; -import android.os.Build; import android.os.Parcel; import android.os.RemoteException; import android.util.ArrayMap; @@ -50,8 +49,6 @@ import java.util.Set; @RunWith(MockitoJUnitRunner.class) public final class RadioManagerTest { - private static final int TEST_TARGET_SDK_VERSION = Build.VERSION_CODES.CUR_DEVELOPMENT; - private static final int REGION = RadioManager.REGION_ITU_2; private static final int FM_LOWER_LIMIT = 87500; private static final int FM_UPPER_LIMIT = 108000; @@ -1043,14 +1040,13 @@ public final class RadioManagerTest { mRadioManager.openTuner(moduleId, FM_BAND_CONFIG, withAudio, mCallbackMock, /* handler= */ null); - verify(mRadioServiceMock).openTuner(eq(moduleId), eq(FM_BAND_CONFIG), eq(withAudio), any(), - anyInt()); + verify(mRadioServiceMock).openTuner(eq(moduleId), eq(FM_BAND_CONFIG), eq(withAudio), any()); } @Test public void openTuner_whenServiceDied_returnsNull() throws Exception { createRadioManager(); - when(mRadioServiceMock.openTuner(anyInt(), any(), anyBoolean(), any(), anyInt())) + when(mRadioServiceMock.openTuner(anyInt(), any(), anyBoolean(), any())) .thenThrow(new RemoteException()); RadioTuner nullTuner = mRadioManager.openTuner(/* moduleId= */ 0, FM_BAND_CONFIG, @@ -1166,7 +1162,6 @@ public final class RadioManagerTest { } private void createRadioManager() throws RemoteException { - mApplicationInfo.targetSdkVersion = TEST_TARGET_SDK_VERSION; when(mContextMock.getApplicationInfo()).thenReturn(mApplicationInfo); when(mRadioServiceMock.listModules()).thenReturn(Arrays.asList(AMFM_PROPERTIES)); when(mRadioServiceMock.addAnnouncementListener(any(), any())).thenReturn(mCloseHandleMock); diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java index 8b257e832e43..c7b82b1caca6 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java @@ -86,7 +86,7 @@ public final class TunerAdapterTest { doAnswer(invocation -> { mTunerCallback = (ITunerCallback) invocation.getArguments()[3]; return mTunerMock; - }).when(mRadioServiceMock).openTuner(anyInt(), any(), anyBoolean(), any(), anyInt()); + }).when(mRadioServiceMock).openTuner(anyInt(), any(), anyBoolean(), any()); doAnswer(invocation -> { ProgramSelector program = (ProgramSelector) invocation.getArguments()[0]; diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java index 16c1499a2775..da51ba45174c 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java @@ -24,7 +24,6 @@ import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -35,7 +34,6 @@ import android.hardware.radio.ICloseHandle; import android.hardware.radio.ITuner; import android.hardware.radio.ITunerCallback; import android.hardware.radio.RadioManager; -import android.os.Build; import android.os.IBinder; import android.os.ServiceManager; @@ -58,7 +56,6 @@ public final class IRadioServiceAidlImplTest extends ExtendedRadioMockitoTestCas "android.hardware.broadcastradio.IBroadcastRadio/amfm"; private static final String DAB_SERVICE_NAME = "android.hardware.broadcastradio.IBroadcastRadio/dab"; - private static final int TARGET_SDK_VERSION = Build.VERSION_CODES.CUR_DEVELOPMENT; private IRadioServiceAidlImpl mAidlImpl; @@ -86,7 +83,7 @@ public final class IRadioServiceAidlImplTest extends ExtendedRadioMockitoTestCas doNothing().when(mServiceMock).enforcePolicyAccess(); when(mHalMock.listModules()).thenReturn(List.of(mModuleMock)); - when(mHalMock.openSession(anyInt(), any(), anyBoolean(), any(), eq(TARGET_SDK_VERSION))) + when(mHalMock.openSession(anyInt(), any(), anyBoolean(), any())) .thenReturn(mTunerMock); when(mHalMock.addAnnouncementListener(any(), any())).thenReturn(mICloseHandle); @@ -118,7 +115,7 @@ public final class IRadioServiceAidlImplTest extends ExtendedRadioMockitoTestCas @Test public void openTuner_forAidlImpl() throws Exception { ITuner tuner = mAidlImpl.openTuner(/* moduleId= */ 0, mBandConfigMock, - /* withAudio= */ true, mTunerCallbackMock, TARGET_SDK_VERSION); + /* withAudio= */ true, mTunerCallbackMock); assertWithMessage("Tuner opened in AIDL HAL") .that(tuner).isEqualTo(mTunerMock); @@ -128,7 +125,7 @@ public final class IRadioServiceAidlImplTest extends ExtendedRadioMockitoTestCas public void openTuner_withNullCallbackForAidlImpl_fails() throws Exception { IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> mAidlImpl.openTuner(/* moduleId= */ 0, mBandConfigMock, - /* withAudio= */ true, /* callback= */ null, TARGET_SDK_VERSION)); + /* withAudio= */ true, /* callback= */ null)); assertWithMessage("Exception for opening tuner with null callback") .that(thrown).hasMessageThat().contains("Callback must not be null"); diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java index 164c9aff127a..20bc8d404d76 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java @@ -33,7 +33,6 @@ import android.hardware.radio.ICloseHandle; import android.hardware.radio.ITuner; import android.hardware.radio.ITunerCallback; import android.hardware.radio.RadioManager; -import android.os.Build; import org.junit.Before; import org.junit.Test; @@ -51,7 +50,6 @@ public final class IRadioServiceHidlImplTest { private static final int HAL1_MODULE_ID = 0; private static final int[] ENABLE_TYPES = new int[]{Announcement.TYPE_TRAFFIC}; - private static final int TARGET_SDK_VERSION = Build.VERSION_CODES.CUR_DEVELOPMENT; private IRadioServiceHidlImpl mHidlImpl; @@ -106,7 +104,7 @@ public final class IRadioServiceHidlImplTest { @Test public void openTuner_withHal1ModuleId_forHidlImpl() throws Exception { ITuner tuner = mHidlImpl.openTuner(HAL1_MODULE_ID, mBandConfigMock, - /* withAudio= */ true, mTunerCallbackMock, TARGET_SDK_VERSION); + /* withAudio= */ true, mTunerCallbackMock); assertWithMessage("Tuner opened in HAL 1") .that(tuner).isEqualTo(mHal1TunerMock); @@ -115,7 +113,7 @@ public final class IRadioServiceHidlImplTest { @Test public void openTuner_withHal2ModuleId_forHidlImpl() throws Exception { ITuner tuner = mHidlImpl.openTuner(HAL1_MODULE_ID + 1, mBandConfigMock, - /* withAudio= */ true, mTunerCallbackMock, TARGET_SDK_VERSION); + /* withAudio= */ true, mTunerCallbackMock); assertWithMessage("Tuner opened in HAL 2") .that(tuner).isEqualTo(mHal2TunerMock); @@ -125,7 +123,7 @@ public final class IRadioServiceHidlImplTest { public void openTuner_withNullCallbackForHidlImpl_fails() throws Exception { NullPointerException thrown = assertThrows(NullPointerException.class, () -> mHidlImpl.openTuner(/* moduleId= */ 0, mBandConfigMock, - /* withAudio= */ true, /* callback= */ null, TARGET_SDK_VERSION)); + /* withAudio= */ true, /* callback= */ null)); assertWithMessage("Exception for opening tuner with null callback") .that(thrown).hasMessageThat().contains("Callback must not be null"); diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/RadioServiceUserControllerTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/RadioServiceUserControllerTest.java index 161ac2dc6aa0..3e9e9922431b 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/RadioServiceUserControllerTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/RadioServiceUserControllerTest.java @@ -24,6 +24,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.app.compat.CompatChanges; import android.os.Binder; import android.os.UserHandle; @@ -46,8 +47,8 @@ public final class RadioServiceUserControllerTest extends ExtendedRadioMockitoTe @Override protected void initializeSession(StaticMockitoSessionBuilder builder) { - builder.spyStatic(ActivityManager.class) - .spyStatic(Binder.class); + builder.spyStatic(ActivityManager.class).spyStatic(Binder.class) + .spyStatic(CompatChanges.class); } @Before diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java index 98103f6eddd3..22f3bd4abe11 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java @@ -36,7 +36,6 @@ import android.hardware.radio.ITuner; import android.hardware.radio.ITunerCallback; import android.hardware.radio.RadioManager; import android.hardware.radio.RadioTuner; -import android.os.Build; import android.os.IBinder; import android.os.IServiceCallback; import android.os.RemoteException; @@ -55,8 +54,6 @@ import java.util.Arrays; public final class BroadcastRadioServiceImplTest extends ExtendedRadioMockitoTestCase { - private static final int TARGET_SDK_VERSION = Build.VERSION_CODES.CUR_DEVELOPMENT; - private static final int FM_RADIO_MODULE_ID = 0; private static final int DAB_RADIO_MODULE_ID = 1; private static final ArrayList<String> SERVICE_LIST = @@ -140,8 +137,7 @@ public final class BroadcastRadioServiceImplTest extends ExtendedRadioMockitoTes createBroadcastRadioService(); ITuner session = mBroadcastRadioService.openSession(FM_RADIO_MODULE_ID, - /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock, - TARGET_SDK_VERSION); + /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock); assertWithMessage("Session opened in FM radio module") .that(session).isEqualTo(mFmTunerSessionMock); @@ -152,8 +148,7 @@ public final class BroadcastRadioServiceImplTest extends ExtendedRadioMockitoTes createBroadcastRadioService(); ITuner session = mBroadcastRadioService.openSession(DAB_RADIO_MODULE_ID + 1, - /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock, - TARGET_SDK_VERSION); + /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock); assertWithMessage("Session opened with id not found").that(session).isNull(); } @@ -165,8 +160,7 @@ public final class BroadcastRadioServiceImplTest extends ExtendedRadioMockitoTes IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> mBroadcastRadioService.openSession(FM_RADIO_MODULE_ID, - /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock, - TARGET_SDK_VERSION)); + /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock)); assertWithMessage("Exception for opening session by non-current user") .that(thrown).hasMessageThat().contains("Cannot open session for non-current user"); @@ -178,8 +172,7 @@ public final class BroadcastRadioServiceImplTest extends ExtendedRadioMockitoTes IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> mBroadcastRadioService.openSession(FM_RADIO_MODULE_ID, - /* legacyConfig= */ null, /* withAudio= */ false, mTunerCallbackMock, - TARGET_SDK_VERSION)); + /* legacyConfig= */ null, /* withAudio= */ false, mTunerCallbackMock)); assertWithMessage("Exception for opening session without audio") .that(thrown).hasMessageThat().contains("not supported"); @@ -247,7 +240,6 @@ public final class BroadcastRadioServiceImplTest extends ExtendedRadioMockitoTes return null; }).when(mFmBinderMock).linkToDeath(any(), anyInt()); - when(mFmRadioModuleMock.openSession(eq(mTunerCallbackMock), eq(TARGET_SDK_VERSION))) - .thenReturn(mFmTunerSessionMock); + when(mFmRadioModuleMock.openSession(mTunerCallbackMock)).thenReturn(mFmTunerSessionMock); } } diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java index 5d0e07613a98..ba0579171c43 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java @@ -16,6 +16,9 @@ package com.android.server.broadcastradio.aidl; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; + +import android.app.compat.CompatChanges; import android.hardware.broadcastradio.AmFmBandRange; import android.hardware.broadcastradio.AmFmRegionConfig; import android.hardware.broadcastradio.DabTableEntry; @@ -29,17 +32,23 @@ import android.hardware.radio.Announcement; import android.hardware.radio.ProgramList; import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; -import android.os.Build; + +import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; +import com.android.server.broadcastradio.ExtendedRadioMockitoTestCase; import com.google.common.truth.Expect; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import java.util.Map; import java.util.Set; -public final class ConversionUtilsTest { +public final class ConversionUtilsTest extends ExtendedRadioMockitoTestCase { + + private static final int U_APP_UID = 1001; + private static final int T_APP_UID = 1002; private static final int FM_LOWER_LIMIT = 87_500; private static final int FM_UPPER_LIMIT = 108_000; @@ -118,16 +127,29 @@ public final class ConversionUtilsTest { @Rule public final Expect expect = Expect.create(); + @Override + protected void initializeSession(StaticMockitoSessionBuilder builder) { + builder.spyStatic(CompatChanges.class); + } + + @Before + public void setUp() { + doReturn(true).when(() -> CompatChanges.isChangeEnabled( + ConversionUtils.RADIO_U_VERSION_REQUIRED, U_APP_UID)); + doReturn(false).when(() -> CompatChanges.isChangeEnabled( + ConversionUtils.RADIO_U_VERSION_REQUIRED, T_APP_UID)); + } + @Test public void isAtLeastU_withTSdkVersion_returnsFalse() { expect.withMessage("Target SDK version of T") - .that(ConversionUtils.isAtLeastU(Build.VERSION_CODES.TIRAMISU)).isFalse(); + .that(ConversionUtils.isAtLeastU(T_APP_UID)).isFalse(); } @Test public void isAtLeastU_withCurrentSdkVersion_returnsTrue() { expect.withMessage("Target SDK version of U") - .that(ConversionUtils.isAtLeastU(Build.VERSION_CODES.CUR_DEVELOPMENT)).isTrue(); + .that(ConversionUtils.isAtLeastU(U_APP_UID)).isTrue(); } @Test @@ -372,14 +394,14 @@ public final class ConversionUtilsTest { public void programSelectorMeetsSdkVersionRequirement_withLowerVersionId_returnsFalse() { expect.withMessage("Selector %s without required SDK version", TEST_DAB_SELECTOR) .that(ConversionUtils.programSelectorMeetsSdkVersionRequirement(TEST_DAB_SELECTOR, - Build.VERSION_CODES.TIRAMISU)).isFalse(); + T_APP_UID)).isFalse(); } @Test public void programSelectorMeetsSdkVersionRequirement_withRequiredVersionId_returnsTrue() { expect.withMessage("Selector %s with required SDK version", TEST_FM_SELECTOR) .that(ConversionUtils.programSelectorMeetsSdkVersionRequirement(TEST_FM_SELECTOR, - Build.VERSION_CODES.TIRAMISU)).isTrue(); + T_APP_UID)).isTrue(); } @Test @@ -389,7 +411,7 @@ public final class ConversionUtilsTest { expect.withMessage("Program info %s without required SDK version", dabProgramInfo) .that(ConversionUtils.programInfoMeetsSdkVersionRequirement(dabProgramInfo, - Build.VERSION_CODES.TIRAMISU)).isFalse(); + T_APP_UID)).isFalse(); } @Test @@ -399,7 +421,7 @@ public final class ConversionUtilsTest { expect.withMessage("Program info %s with required SDK version", fmProgramInfo) .that(ConversionUtils.programInfoMeetsSdkVersionRequirement(fmProgramInfo, - Build.VERSION_CODES.TIRAMISU)).isTrue(); + T_APP_UID)).isTrue(); } @Test @@ -413,7 +435,7 @@ public final class ConversionUtilsTest { Set.of(TEST_DAB_SID_EXT_ID, TEST_DAB_ENSEMBLE_ID, TEST_VENDOR_ID)); ProgramList.Chunk convertedChunk = ConversionUtils.convertChunkToTargetSdkVersion(chunk, - Build.VERSION_CODES.TIRAMISU); + T_APP_UID); expect.withMessage( "Purged state of the converted program list chunk with lower SDK version") @@ -441,7 +463,7 @@ public final class ConversionUtilsTest { Set.of(TEST_DAB_SID_EXT_ID, TEST_DAB_ENSEMBLE_ID, TEST_VENDOR_ID)); ProgramList.Chunk convertedChunk = ConversionUtils.convertChunkToTargetSdkVersion(chunk, - Build.VERSION_CODES.CUR_DEVELOPMENT); + U_APP_UID); expect.withMessage("Converted program list chunk with required SDK version") .that(convertedChunk).isEqualTo(chunk); diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java index 464ecb2b50a1..78b5a4ad71de 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java @@ -33,6 +33,7 @@ import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.compat.CompatChanges; import android.graphics.Bitmap; import android.hardware.broadcastradio.IBroadcastRadio; import android.hardware.broadcastradio.ITunerCallback; @@ -46,7 +47,6 @@ import android.hardware.radio.ProgramList; import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; import android.hardware.radio.RadioTuner; -import android.os.Build; import android.os.ParcelableException; import android.os.RemoteException; import android.os.ServiceSpecificException; @@ -73,7 +73,6 @@ import java.util.Set; */ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { - private static final int TARGET_SDK_VERSION = Build.VERSION_CODES.CUR_DEVELOPMENT; private static final VerificationWithTimeout CALLBACK_TIMEOUT = timeout(/* millis= */ 200); private static final int SIGNAL_QUALITY = 90; @@ -125,11 +124,13 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { @Override protected void initializeSession(StaticMockitoSessionBuilder builder) { - builder.spyStatic(RadioServiceUserController.class); + builder.spyStatic(RadioServiceUserController.class).spyStatic(CompatChanges.class); } @Before public void setup() throws Exception { + doReturn(true).when(() -> CompatChanges.isChangeEnabled( + eq(ConversionUtils.RADIO_U_VERSION_REQUIRED), anyInt())); doReturn(true).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); mRadioModule = new RadioModule(mBroadcastRadioMock, @@ -341,7 +342,9 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { @Test public void tune_withLowerSdkVersion() throws Exception { - openAidlClients(/* numClients= */ 1, Build.VERSION_CODES.TIRAMISU); + doReturn(false).when(() -> CompatChanges.isChangeEnabled( + eq(ConversionUtils.RADIO_U_VERSION_REQUIRED), anyInt())); + openAidlClients(/* numClients= */ 1); ProgramSelector initialSel = AidlTestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]); RadioManager.ProgramInfo tuneInfo = AidlTestUtils.makeProgramInfo(initialSel, SIGNAL_QUALITY); @@ -1175,17 +1178,13 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { .onParametersUpdated(parametersExpected); } } - private void openAidlClients(int numClients) throws Exception { - openAidlClients(numClients, TARGET_SDK_VERSION); - } - private void openAidlClients(int numClients, int targetSdkVersion) throws Exception { + private void openAidlClients(int numClients) throws Exception { mAidlTunerCallbackMocks = new android.hardware.radio.ITunerCallback[numClients]; mTunerSessions = new TunerSession[numClients]; for (int index = 0; index < numClients; index++) { mAidlTunerCallbackMocks[index] = mock(android.hardware.radio.ITunerCallback.class); - mTunerSessions[index] = mRadioModule.openSession(mAidlTunerCallbackMocks[index], - targetSdkVersion); + mTunerSessions[index] = mRadioModule.openSession(mAidlTunerCallbackMocks[index]); } } diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index f9b6ce0a199c..8efd180b6c93 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -6752,7 +6752,7 @@ public class AudioManager { /** * @hide - * Lower media volume to RS1 + * Lower media volume to RS1 interval */ public void lowerVolumeToRs1() { try { @@ -6764,13 +6764,13 @@ public class AudioManager { /** * @hide - * @return the RS2 value used for momentary exposure warnings + * @return the RS2 upper bound used for momentary exposure warnings */ @TestApi @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public float getRs2Value() { try { - return getService().getRs2Value(); + return getService().getOutputRs2UpperBound(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -6778,13 +6778,13 @@ public class AudioManager { /** * @hide - * Sets the RS2 value used for momentary exposure warnings + * Sets the RS2 upper bound used for momentary exposure warnings */ @TestApi @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public void setRs2Value(float rs2Value) { try { - getService().setRs2Value(rs2Value); + getService().setOutputRs2UpperBound(rs2Value); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index cebdc820cd02..f9d4efea57e2 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -296,10 +296,10 @@ interface IAudioService { void lowerVolumeToRs1(String callingPackage); @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") - float getRs2Value(); + float getOutputRs2UpperBound(); @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") - oneway void setRs2Value(float rs2Value); + oneway void setOutputRs2UpperBound(float rs2Value); @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") float getCsd(); diff --git a/media/java/android/media/tv/OWNERS b/media/java/android/media/tv/OWNERS index fa0429350a25..0b022e514662 100644 --- a/media/java/android/media/tv/OWNERS +++ b/media/java/android/media/tv/OWNERS @@ -1,6 +1,7 @@ quxiangfang@google.com shubang@google.com hgchen@google.com +qingxun@google.com # For android remote service per-file ITvRemoteServiceInput.aidl = file:/media/lib/tvremote/OWNERS diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java index ef2b5a5b6e50..7364daa53f89 100644 --- a/media/java/android/media/tv/TvInputManager.java +++ b/media/java/android/media/tv/TvInputManager.java @@ -156,20 +156,51 @@ public final class TvInputManager { "android.media.tv.TvInputManager.stream_id"; /** + * This value for {@link #TV_MESSAGE_KEY_GROUP_ID} denotes that the message doesn't + * belong to any group. + */ + public static final long TV_MESSAGE_GROUP_ID_NONE = -1; + + /** + * This constant is used as a {@link Bundle} key for TV messages. This is used to + * optionally identify messages that belong together, such as headers and bodies + * of the same event. For messages that do not have a group, this value + * should be {@link #TV_MESSAGE_GROUP_ID_NONE}. + * + * <p> As -1 is a reserved value, -1 should not be used as a valid groupId. + * + * <p> Type: long + */ + public static final String TV_MESSAGE_KEY_GROUP_ID = + "android.media.tv.TvInputManager.group_id"; + + /** + * This is a subtype for TV messages that can be potentially found as a value + * at {@link #TV_MESSAGE_KEY_SUBTYPE}. It identifies the subtype of the message + * as the watermarking format ATSC A/335. + */ + public static final String TV_MESSAGE_SUBTYPE_WATERMARKING_A335 = "ATSC A/335"; + + /** + * This is a subtype for TV messages that can be potentially found as a value + * at {@link #TV_MESSAGE_KEY_SUBTYPE}. It identifies the subtype of the message + * as the CC format CTA 608-E. + */ + public static final String TV_MESSAGE_SUBTYPE_CC_608E = "CTA 608-E"; + + /** * This constant is used as a {@link Bundle} key for TV messages. The value of the key * identifies the subtype of the data, such as the format of the CC data. The format * found at this key can then be used to identify how to parse the data at * {@link #TV_MESSAGE_KEY_RAW_DATA}. * - * To parse the raw data bsed on the subtype, please refer to the official documentation of the - * concerning subtype. For example, for the subtype "ATSC A/335" for watermarking, the - * document for A/335 from the ATSC standard details how this data is formatted. - * - * Some other examples of common formats include: - * <ul> - * <li>Watermarking - ATSC A/336</li> - * <li>Closed Captioning - CTA 608-E</li> - * </ul> + * <p> To parse the raw data based on the subtype, please refer to the official + * documentation of the concerning subtype. For example, for the subtype + * {@link #TV_MESSAGE_SUBTYPE_WATERMARKING_A335}, the document for A/335 from the ATSC + * standard details how this data is formatted. Similarly, the subtype + * {@link #TV_MESSAGE_SUBTYPE_CC_608E} is documented in the ANSI/CTA standard for + * 608-E. These subtypes are examples of common formats for their respective uses + * and other subtypes may exist. * * <p> Type: String */ @@ -178,7 +209,7 @@ public final class TvInputManager { /** * This constant is used as a {@link Bundle} key for TV messages. The value of the key - * stores the raw data contained in this TV Message. The format of this data is determined + * stores the raw data contained in this TV message. The format of this data is determined * by the format defined by the subtype, found using the key at * {@link #TV_MESSAGE_KEY_SUBTYPE}. See {@link #TV_MESSAGE_KEY_SUBTYPE} for more * information on how to parse this data. @@ -839,6 +870,7 @@ public final class TvInputManager { * @param type The type of message received, such as {@link #TV_MESSAGE_TYPE_WATERMARK} * @param data The raw data of the message. The bundle keys are: * {@link TvInputManager#TV_MESSAGE_KEY_STREAM_ID}, + * {@link TvInputManager#TV_MESSAGE_KEY_GROUP_ID}, * {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE}, * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}. * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java index 0d283fa87c19..85b02ad54718 100644 --- a/media/java/android/media/tv/TvInputService.java +++ b/media/java/android/media/tv/TvInputService.java @@ -1032,6 +1032,7 @@ public abstract class TvInputService extends Service { * {@link TvInputManager#TV_MESSAGE_TYPE_WATERMARK} * @param data The raw data of the message. The bundle keys are: * {@link TvInputManager#TV_MESSAGE_KEY_STREAM_ID}, + * {@link TvInputManager#TV_MESSAGE_KEY_GROUP_ID}, * {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE}, * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}. * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on @@ -1507,6 +1508,7 @@ public abstract class TvInputService extends Service { * {@link TvInputManager#TV_MESSAGE_TYPE_WATERMARK} * @param data The raw data of the message. The bundle keys are: * {@link TvInputManager#TV_MESSAGE_KEY_STREAM_ID}, + * {@link TvInputManager#TV_MESSAGE_KEY_GROUP_ID}, * {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE}, * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}. * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on diff --git a/media/java/android/media/tv/TvView.java b/media/java/android/media/tv/TvView.java index 8a886832e88e..c91f62db95c4 100644 --- a/media/java/android/media/tv/TvView.java +++ b/media/java/android/media/tv/TvView.java @@ -1256,6 +1256,7 @@ public class TvView extends ViewGroup { * {@link TvInputManager#TV_MESSAGE_TYPE_WATERMARK} * @param data The raw data of the message. The bundle keys are: * {@link TvInputManager#TV_MESSAGE_KEY_STREAM_ID}, + * {@link TvInputManager#TV_MESSAGE_KEY_GROUP_ID}, * {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE}, * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}. * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java index 69ff9c63b32b..06dfe4fffefe 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java @@ -921,6 +921,7 @@ public abstract class TvInteractiveAppService extends Service { * {@link TvInputManager#TV_MESSAGE_TYPE_WATERMARK} * @param data The raw data of the message. The bundle keys are: * {@link TvInputManager#TV_MESSAGE_KEY_STREAM_ID}, + * {@link TvInputManager#TV_MESSAGE_KEY_GROUP_ID}, * {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE}, * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}. * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java index 80a14354ef7a..cbaf5e482faa 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java @@ -946,6 +946,7 @@ public class TvInteractiveAppView extends ViewGroup { * {@link TvInputManager#TV_MESSAGE_TYPE_WATERMARK} * @param data The raw data of the message. The bundle keys are: * {@link TvInputManager#TV_MESSAGE_KEY_STREAM_ID}, + * {@link TvInputManager#TV_MESSAGE_KEY_GROUP_ID}, * {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE}, * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}. * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp index 904fa7484027..4b63fbf14d4c 100644 --- a/native/android/surface_control.cpp +++ b/native/android/surface_control.cpp @@ -628,14 +628,15 @@ void ASurfaceTransaction_setExtendedRangeBrightness(ASurfaceTransaction* aSurfac CHECK_NOT_NULL(aSurfaceControl); if (!isfinite(currentBufferRatio) || currentBufferRatio < 1.0f) { - ALOGE("Ignore setExtendedRangeBrightness, currentBufferRatio %f isn't finite or >= 1.0f", - currentBufferRatio); + LOG_ALWAYS_FATAL("setExtendedRangeBrightness, currentBufferRatio %f isn't finite or >= " + "1.0f", + currentBufferRatio); return; } if (!isfinite(desiredRatio) || desiredRatio < 1.0f) { - ALOGE("Ignore setExtendedRangeBrightness, desiredRatio %f isn't finite or >= 1.0f", - desiredRatio); + LOG_ALWAYS_FATAL("setExtendedRangeBrightness, desiredRatio %f isn't finite or >= 1.0f", + desiredRatio); return; } diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values/strings.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values/strings.xml index 0747ef089a8d..81fa8e6bba21 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/values/strings.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values/strings.xml @@ -49,7 +49,6 @@ <!-- Short summary of app that appears as subtext on the service preference in Settings --> <string name="accessibility_menu_summary">Control device via large menu</string> - <!-- TODO(b/113371047): string need to be reviewed --> <!-- String defining the settings name --> <string name="accessibility_menu_settings_name">Accessibility Menu Settings</string> @@ -57,8 +56,6 @@ <string name="accessibility_menu_large_buttons_title">Large buttons</string> <!-- String defining the summary of Large button setting --> <string name="accessibility_menu_large_buttons_summary">Increase size of Accessibility Menu Buttons</string> - <!-- String defining the title of the preference to show help and feedback menu [CHAR LIMIT=40] --> - <string name="pref_help_and_feedback_title">Help & feedback</string> <!-- String defining the title of the preference to show help menu [CHAR LIMIT=40] --> <string name="pref_help_title">Help</string> @@ -67,7 +64,4 @@ <!-- The percentage of the music volume, and double "%" is required to represent the symbol "%" --> <string name="music_volume_percentage_label">Music volume <xliff:g id="percentage">%1$s</xliff:g> %%</string> - <!-- The label of a settings item that displays legal information about the licenses used in this app. [CHAR LIMIT=NONE] --> - <string name="pref_item_licenses">Open Source Licenses</string> - </resources> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/xml/accessibilitymenu_preferences.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/xml/accessibilitymenu_preferences.xml index 3b79287ad202..e42c3cd5a368 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/xml/accessibilitymenu_preferences.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/xml/accessibilitymenu_preferences.xml @@ -28,6 +28,6 @@ <Preference android:key="@string/pref_help" - android:title="@string/pref_help_and_feedback_title"/> + android:title="@string/pref_help_title"/> </androidx.preference.PreferenceScreen>
\ No newline at end of file diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java index 8f2934882ede..4b6f9a430390 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java @@ -71,8 +71,6 @@ public class A11yMenuSettingsActivity extends FragmentActivity { private void initializeHelpAndFeedbackPreference() { final Preference prefHelp = findPreference(getString(R.string.pref_help)); if (prefHelp != null) { - prefHelp.setTitle(R.string.pref_help_title); - // Do not allow access to web during setup. if (Settings.Secure.getInt( getContext().getContentResolver(), diff --git a/packages/SystemUI/res/layout/activity_rear_display_education.xml b/packages/SystemUI/res/layout/activity_rear_display_education.xml index 094807e0fad4..c295cfe7a2e0 100644 --- a/packages/SystemUI/res/layout/activity_rear_display_education.xml +++ b/packages/SystemUI/res/layout/activity_rear_display_education.xml @@ -30,6 +30,7 @@ <com.airbnb.lottie.LottieAnimationView android:id="@+id/rear_display_folded_animation" + android:importantForAccessibility="no" android:layout_width="@dimen/rear_display_animation_width" android:layout_height="@dimen/rear_display_animation_height" android:layout_gravity="center" diff --git a/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml b/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml index e970bc573b01..c12bfcca4eff 100644 --- a/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml +++ b/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml @@ -31,6 +31,7 @@ <com.airbnb.lottie.LottieAnimationView android:id="@+id/rear_display_folded_animation" + android:importantForAccessibility="no" android:layout_width="@dimen/rear_display_animation_width" android:layout_height="@dimen/rear_display_animation_height" android:layout_gravity="center" diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index e5cd0c55027b..082f3855affc 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -99,6 +99,7 @@ <item>accessibility_display_daltonizer_enabled:color_correction</item> <item>accessibility_display_inversion_enabled:inversion</item> <item>one_handed_mode_enabled:onehanded</item> + <item>accessibility_font_scaling_has_been_changed:font_scaling</item> </string-array> <!-- Use collapsed layout for media player in landscape QQS --> diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt index 53a421d9eccc..1836ce857783 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.accessibility.fontscaling +import android.annotation.WorkerThread import android.content.Context import android.content.pm.ActivityInfo import android.content.res.Configuration @@ -27,18 +28,26 @@ import android.widget.SeekBar.OnSeekBarChangeListener import android.widget.TextView import com.android.systemui.R import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SystemSettings +import java.util.concurrent.Executor import kotlin.math.roundToInt /** The Dialog that contains a seekbar for changing the font size. */ -class FontScalingDialog(context: Context, private val systemSettings: SystemSettings) : - SystemUIDialog(context) { +class FontScalingDialog( + context: Context, + private val systemSettings: SystemSettings, + private val secureSettings: SecureSettings, + @Background private val backgroundExecutor: Executor +) : SystemUIDialog(context) { private val strEntryValues: Array<String> = context.resources.getStringArray(com.android.settingslib.R.array.entryvalues_font_size) private lateinit var title: TextView private lateinit var doneButton: Button private lateinit var seekBarWithIconButtonsView: SeekBarWithIconButtonsView + private var lastProgress: Int = -1 private val configuration: Configuration = Configuration(context.getResources().getConfiguration()) @@ -70,12 +79,22 @@ class FontScalingDialog(context: Context, private val systemSettings: SystemSett seekBarWithIconButtonsView.setMax((strEntryValues).size - 1) val currentScale = systemSettings.getFloat(Settings.System.FONT_SCALE, 1.0f) - seekBarWithIconButtonsView.setProgress(fontSizeValueToIndex(currentScale)) + lastProgress = fontSizeValueToIndex(currentScale) + seekBarWithIconButtonsView.setProgress(lastProgress) seekBarWithIconButtonsView.setOnSeekBarChangeListener( object : OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { - systemSettings.putString(Settings.System.FONT_SCALE, strEntryValues[progress]) + if (progress != lastProgress) { + if (!fontSizeHasBeenChangedFromTile) { + backgroundExecutor.execute { updateSecureSettingsIfNeeded() } + fontSizeHasBeenChangedFromTile = true + } + + backgroundExecutor.execute { updateFontScale(strEntryValues[progress]) } + + lastProgress = progress + } } override fun onStartTrackingTouch(seekBar: SeekBar) { @@ -115,4 +134,28 @@ class FontScalingDialog(context: Context, private val systemSettings: SystemSett } } } + + @WorkerThread + fun updateFontScale(newScale: String) { + systemSettings.putString(Settings.System.FONT_SCALE, newScale) + } + + @WorkerThread + fun updateSecureSettingsIfNeeded() { + if ( + secureSettings.getString(Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED) != + ON + ) { + secureSettings.putString( + Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED, + ON + ) + } + } + + companion object { + private const val ON = "1" + private const val OFF = "0" + private var fontSizeHasBeenChangedFromTile = false + } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index bb35355ba03a..19bd86aba177 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -42,6 +42,7 @@ import android.hardware.fingerprint.FingerprintSensorProperties; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IUdfpsOverlayController; import android.hardware.fingerprint.IUdfpsOverlayControllerCallback; +import android.hardware.input.InputManager; import android.os.Build; import android.os.Handler; import android.os.PowerManager; @@ -169,6 +170,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull private final AlternateBouncerInteractor mAlternateBouncerInteractor; @NonNull private final SecureSettings mSecureSettings; @NonNull private final UdfpsUtils mUdfpsUtils; + @NonNull private final InputManager mInputManager; // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple // sensors, this, in addition to a lot of the code here, will be updated. @@ -576,6 +578,10 @@ public class UdfpsController implements DozeReceiver, Dumpable { data.getTime(), data.getGestureStart(), mStatusBarStateController.isDozing()); + + // Pilfer if valid overlap, don't allow following events to reach keyguard + mInputManager.pilferPointers( + mOverlay.getOverlayView().getViewRootImpl().getInputToken()); break; case UP: @@ -599,7 +605,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { break; case UNCHANGED: - if (!isWithinSensorArea(mOverlay.getOverlayView(), event.getX(), event.getY(), + if (!isWithinSensorArea(mOverlay.getOverlayView(), event.getRawX(), event.getRawY(), true) && mActivePointerId == MotionEvent.INVALID_POINTER_ID && event.getActionMasked() == MotionEvent.ACTION_DOWN && mAlternateBouncerInteractor.isVisibleState()) { @@ -612,6 +618,13 @@ public class UdfpsController implements DozeReceiver, Dumpable { } logBiometricTouch(processedTouch.getEvent(), data); + // Always pilfer pointers that are within sensor area + if (isWithinSensorArea(mOverlay.getOverlayView(), event.getRawX(), event.getRawY(), true)) { + Log.d("Austin", "pilferTouch invalid overlap"); + mInputManager.pilferPointers( + mOverlay.getOverlayView().getViewRootImpl().getInputToken()); + } + return processedTouch.getTouchData().isWithinSensor(mOverlayParams.getNativeSensorBounds()); } @@ -798,6 +811,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull SessionTracker sessionTracker, @NonNull AlternateBouncerInteractor alternateBouncerInteractor, @NonNull SecureSettings secureSettings, + @NonNull InputManager inputManager, @NonNull UdfpsUtils udfpsUtils) { mContext = context; mExecution = execution; @@ -841,6 +855,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { mAlternateBouncerInteractor = alternateBouncerInteractor; mSecureSettings = secureSettings; mUdfpsUtils = udfpsUtils; + mInputManager = inputManager; mTouchProcessor = mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION) ? singlePointerTouchProcessor : null; diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt index b4724ddebb9a..1a85a0f47749 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt @@ -89,6 +89,9 @@ constructor( R.id.turbulence_noise_view, R.id.touch_ripple_view, ) + + // Sizing view id for recommendation card view. + val recSizingViewId = R.id.sizing_view } /** A listener when the current dimensions of the player change */ @@ -176,7 +179,21 @@ constructor( // Layout dimensions are possibly changing, so we need to update them. (at // least on large screen devices) lastOrientation = newOrientation - loadLayoutForType(type) + // Update the height of media controls for the expanded layout. it is needed + // for large screen devices. + if (type == TYPE.PLAYER) { + backgroundIds.forEach { id -> + expandedLayout.getConstraint(id).layout.mHeight = + context.resources.getDimensionPixelSize( + R.dimen.qs_media_session_height_expanded + ) + } + } else { + expandedLayout.getConstraint(recSizingViewId).layout.mHeight = + context.resources.getDimensionPixelSize( + R.dimen.qs_media_session_height_expanded + ) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt index a915ddba4f1c..3f514344cee1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.tiles import android.content.Intent import android.os.Handler +import android.os.HandlerExecutor import android.os.Looper import android.provider.Settings import android.view.View @@ -38,6 +39,7 @@ import com.android.systemui.qs.QSHost import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SystemSettings import javax.inject.Inject @@ -54,6 +56,7 @@ constructor( qsLogger: QSLogger, private val dialogLaunchAnimator: DialogLaunchAnimator, private val systemSettings: SystemSettings, + private val secureSettings: SecureSettings, private val featureFlags: FeatureFlags ) : QSTileImpl<QSTile.State?>( @@ -78,7 +81,13 @@ constructor( override fun handleClick(view: View?) { mUiHandler.post { - val dialog: SystemUIDialog = FontScalingDialog(mContext, systemSettings) + val dialog: SystemUIDialog = + FontScalingDialog( + mContext, + systemSettings, + secureSettings, + HandlerExecutor(mHandler) + ) if (view != null) { dialogLaunchAnimator.showFromView( dialog, diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 30865aa0c45d..32c59afc3794 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -2805,6 +2805,7 @@ public final class NotificationPanelViewController implements Dumpable { public void setIsLaunchAnimationRunning(boolean running) { boolean wasRunning = mIsLaunchAnimationRunning; mIsLaunchAnimationRunning = running; + mCentralSurfaces.updateIsKeyguard(); if (wasRunning != mIsLaunchAnimationRunning) { mShadeExpansionStateManager.notifyLaunchingActivityChanged(running); } @@ -3862,6 +3863,10 @@ public final class NotificationPanelViewController implements Dumpable { return mClosing || mIsLaunchAnimationRunning; } + public boolean isLaunchAnimationRunning() { + return mIsLaunchAnimationRunning; + } + public boolean isTracking() { return mTracking; } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 156e4fd1889f..e7759df6e81b 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -301,9 +301,11 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } private void applyKeyguardFlags(NotificationShadeWindowState state) { - final boolean keyguardOrAod = state.keyguardShowing + // Keyguard is visible if it's showing or if it's fading away (in which case we're animating + // it out, but the wallpaper should remain visible as a backdrop for the animation); + final boolean keyguardOrAodVisible = (state.keyguardShowing || state.keyguardFadingAway) || (state.dozing && mDozeParameters.getAlwaysOn()); - if ((keyguardOrAod && !state.mediaBackdropShowing && !state.lightRevealScrimOpaque) + if ((keyguardOrAodVisible && !state.mediaBackdropShowing && !state.lightRevealScrimOpaque) || mKeyguardViewMediator.isAnimatingBetweenKeyguardAndSurfaceBehind()) { // Show the wallpaper if we're on keyguard/AOD and the wallpaper is not occluded by a // solid backdrop. Also, show it if we are currently animating between the diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 5438a59fadc2..5e3c1c59666d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -1052,6 +1052,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // The light reveal scrim should always be fully revealed by the time the keyguard // is done going away. Double check that this is true. if (!mKeyguardStateController.isKeyguardGoingAway()) { + updateIsKeyguard(); + if (mLightRevealScrim.getRevealAmount() != 1f) { Log.e(TAG, "Keyguard is done going away, but someone left the light reveal " + "scrim at reveal amount: " + mLightRevealScrim.getRevealAmount()); @@ -2943,6 +2945,10 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { showKeyguardImpl(); } } else { + final boolean isLaunchingOrGoingAway = + mNotificationPanelViewController.isLaunchAnimationRunning() + || mKeyguardStateController.isKeyguardGoingAway(); + // During folding a foldable device this might be called as a result of // 'onScreenTurnedOff' call for the inner display. // In this case: @@ -2954,7 +2960,14 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { if (!mScreenOffAnimationController.isKeyguardHideDelayed() // If we're animating occluded, there's an activity launching over the keyguard // UI. Wait to hide it until after the animation concludes. - && !mKeyguardViewMediator.isOccludeAnimationPlaying()) { + && !mKeyguardViewMediator.isOccludeAnimationPlaying() + // If we're occluded, but playing an animation (launch or going away animations) + // the keyguard is visible behind the animation. + && !(mKeyguardStateController.isOccluded() && isLaunchingOrGoingAway)) { + // If we're going away and occluded, it means we are launching over the + // unsecured keyguard, which will subsequently go away. Wait to hide it until + // after the animation concludes to avoid the lockscreen UI changing into the + // shade UI behind the launch animation. return hideKeyguardImpl(forceStateChange); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt index ca6f42618e2a..eb8295653199 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt @@ -25,12 +25,19 @@ import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView +import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SystemSettings +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations + +private const val ON: Int = 1 +private const val OFF: Int = 0 /** Tests for [FontScalingDialog]. */ @SmallTest @@ -39,6 +46,8 @@ import org.junit.runner.RunWith class FontScalingDialogTest : SysuiTestCase() { private lateinit var fontScalingDialog: FontScalingDialog private lateinit var systemSettings: SystemSettings + private lateinit var secureSettings: SecureSettings + private lateinit var backgroundExecutor: FakeExecutor private val fontSizeValueArray: Array<String> = mContext .getResources() @@ -46,9 +55,13 @@ class FontScalingDialogTest : SysuiTestCase() { @Before fun setUp() { + MockitoAnnotations.initMocks(this) val mainHandler = Handler(TestableLooper.get(this).getLooper()) systemSettings = FakeSettings() - fontScalingDialog = FontScalingDialog(mContext, systemSettings as FakeSettings) + secureSettings = FakeSettings() + backgroundExecutor = FakeExecutor(FakeSystemClock()) + fontScalingDialog = + FontScalingDialog(mContext, systemSettings, secureSettings, backgroundExecutor) } @Test @@ -76,6 +89,7 @@ class FontScalingDialogTest : SysuiTestCase() { seekBarWithIconButtonsView.setProgress(0) iconEndFrame.performClick() + backgroundExecutor.runAllReady() val currentScale = systemSettings.getFloat(Settings.System.FONT_SCALE, /* def = */ 1.0f) assertThat(seekBar.getProgress()).isEqualTo(1) @@ -96,6 +110,7 @@ class FontScalingDialogTest : SysuiTestCase() { seekBarWithIconButtonsView.setProgress(fontSizeValueArray.size - 1) iconStartFrame.performClick() + backgroundExecutor.runAllReady() val currentScale = systemSettings.getFloat(Settings.System.FONT_SCALE, /* def = */ 1.0f) assertThat(seekBar.getProgress()).isEqualTo(fontSizeValueArray.size - 2) @@ -104,4 +119,26 @@ class FontScalingDialogTest : SysuiTestCase() { fontScalingDialog.dismiss() } + + @Test + fun progressChanged_keyWasNotSetBefore_fontScalingHasBeenChangedIsOn() { + fontScalingDialog.show() + + val seekBarWithIconButtonsView: SeekBarWithIconButtonsView = + fontScalingDialog.findViewById(R.id.font_scaling_slider)!! + secureSettings.putInt(Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED, OFF) + + // Default seekbar progress for font size is 1, set it to another progress 0 + seekBarWithIconButtonsView.setProgress(0) + backgroundExecutor.runAllReady() + + val currentSettings = + secureSettings.getInt( + Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED, + /* def = */ OFF + ) + assertThat(currentSettings).isEqualTo(ON) + + fontScalingDialog.dismiss() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index 9a73898ca7c7..445cc8739463 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -53,6 +53,7 @@ import android.hardware.fingerprint.FingerprintSensorProperties; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IUdfpsOverlayController; import android.hardware.fingerprint.IUdfpsOverlayControllerCallback; +import android.hardware.input.InputManager; import android.os.Handler; import android.os.PowerManager; import android.os.RemoteException; @@ -63,6 +64,7 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.Surface; import android.view.View; +import android.view.ViewRootImpl; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; @@ -231,6 +233,10 @@ public class UdfpsControllerTest extends SysuiTestCase { private FingerprintSensorPropertiesInternal mOpticalProps; private FingerprintSensorPropertiesInternal mUltrasonicProps; private UdfpsUtils mUdfpsUtils; + @Mock + private InputManager mInputManager; + @Mock + private ViewRootImpl mViewRootImpl; @Before public void setUp() { @@ -248,6 +254,7 @@ public class UdfpsControllerTest extends SysuiTestCase { when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true); when(mSessionTracker.getSessionId(anyInt())).thenReturn( (new InstanceIdSequence(1 << 20)).newInstanceId()); + when(mUdfpsView.getViewRootImpl()).thenReturn(mViewRootImpl); final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */, @@ -304,7 +311,7 @@ public class UdfpsControllerTest extends SysuiTestCase { mUnlockedScreenOffAnimationController, mSystemUIDialogManager, mLatencyTracker, mActivityLaunchAnimator, alternateTouchProvider, mBiometricExecutor, mPrimaryBouncerInteractor, mSinglePointerTouchProcessor, mSessionTracker, - mAlternateBouncerInteractor, mSecureSettings, mUdfpsUtils); + mAlternateBouncerInteractor, mSecureSettings, mInputManager, mUdfpsUtils); verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture()); mOverlayController = mOverlayCaptor.getValue(); verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture()); @@ -1262,6 +1269,81 @@ public class UdfpsControllerTest extends SysuiTestCase { } @Test + public void onTouch_withNewTouchDetection_pilferPointer() throws RemoteException { + final NormalizedTouchData touchData = new NormalizedTouchData(0, 0f, 0f, 0f, 0f, 0f, 0L, + 0L); + final TouchProcessorResult processorResultDown = new TouchProcessorResult.ProcessedTouch( + InteractionEvent.DOWN, 1 /* pointerId */, touchData); + + // Enable new touch detection. + when(mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)).thenReturn(true); + + // Configure UdfpsController to use FingerprintManager as opposed to AlternateTouchProvider. + initUdfpsController(mOpticalProps, false /* hasAlternateTouchProvider */); + + // Configure UdfpsView to accept the ACTION_DOWN event + when(mUdfpsView.isDisplayConfigured()).thenReturn(false); + when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); + + // GIVEN that the overlay is showing and a11y touch exploration NOT enabled + when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false); + mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, + BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); + mFgExecutor.runAllReady(); + + verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); + + // WHEN ACTION_DOWN is received + when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn( + processorResultDown); + MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); + mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); + mBiometricExecutor.runAllReady(); + downEvent.recycle(); + + // THEN the touch is pilfered, expected twice (valid overlap and touch on sensor) + verify(mInputManager, times(2)).pilferPointers(any()); + } + + @Test + public void onTouch_withNewTouchDetection_doNotPilferPointer() throws RemoteException { + final NormalizedTouchData touchData = new NormalizedTouchData(0, 0f, 0f, 0f, 0f, 0f, 0L, + 0L); + final TouchProcessorResult processorResultUnchanged = + new TouchProcessorResult.ProcessedTouch(InteractionEvent.UNCHANGED, + 1 /* pointerId */, touchData); + + // Enable new touch detection. + when(mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)).thenReturn(true); + + // Configure UdfpsController to use FingerprintManager as opposed to AlternateTouchProvider. + initUdfpsController(mOpticalProps, false /* hasAlternateTouchProvider */); + + // Configure UdfpsView to not accept the ACTION_DOWN event + when(mUdfpsView.isDisplayConfigured()).thenReturn(false); + when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(false); + + // GIVEN that the overlay is showing and a11y touch exploration NOT enabled + when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false); + mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, + BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); + mFgExecutor.runAllReady(); + + verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); + + // WHEN ACTION_DOWN is received and touch is not within sensor + when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn( + processorResultUnchanged); + MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); + mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); + mBiometricExecutor.runAllReady(); + downEvent.recycle(); + + // THEN the touch is NOT pilfered + verify(mInputManager, times(0)).pilferPointers(any()); + } + + @Test public void onAodInterrupt_onAcquiredGood_fingerNoLongerDown() throws RemoteException { // GIVEN UDFPS overlay is showing mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, @@ -1285,6 +1367,5 @@ public class UdfpsControllerTest extends SysuiTestCase { // THEN is fingerDown should be FALSE assertFalse(mUdfpsController.isFingerDown()); - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt index 0fac3db2dc1f..4565762929d7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt @@ -21,7 +21,6 @@ import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View -import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase @@ -61,8 +60,6 @@ class MediaViewControllerTest : SysuiTestCase() { @Mock private lateinit var mediaSubTitleWidgetState: WidgetState @Mock private lateinit var mediaContainerWidgetState: WidgetState @Mock private lateinit var mediaFlags: MediaFlags - @Mock private lateinit var expandedLayout: ConstraintSet - @Mock private lateinit var collapsedLayout: ConstraintSet val delta = 0.1F @@ -82,16 +79,47 @@ class MediaViewControllerTest : SysuiTestCase() { } @Test - fun testOrientationChanged_layoutsAreLoaded() { - mediaViewController.expandedLayout = expandedLayout - mediaViewController.collapsedLayout = collapsedLayout + fun testOrientationChanged_heightOfPlayerIsUpdated() { + val newConfig = Configuration() + + mediaViewController.attach(player, MediaViewController.TYPE.PLAYER) + // Change the height to see the effect of orientation change. + MediaViewController.backgroundIds.forEach { id -> + mediaViewController.expandedLayout.getConstraint(id).layout.mHeight = 10 + } + newConfig.orientation = ORIENTATION_LANDSCAPE + configurationController.onConfigurationChanged(newConfig) + MediaViewController.backgroundIds.forEach { id -> + assertTrue( + mediaViewController.expandedLayout.getConstraint(id).layout.mHeight == + context.resources.getDimensionPixelSize( + R.dimen.qs_media_session_height_expanded + ) + ) + } + } + + @Test + fun testOrientationChanged_heightOfRecCardIsUpdated() { val newConfig = Configuration() + + mediaViewController.attach(recommendation, MediaViewController.TYPE.RECOMMENDATION) + // Change the height to see the effect of orientation change. + mediaViewController.expandedLayout + .getConstraint(MediaViewController.recSizingViewId) + .layout + .mHeight = 10 newConfig.orientation = ORIENTATION_LANDSCAPE configurationController.onConfigurationChanged(newConfig) - verify(expandedLayout).load(context, R.xml.media_session_expanded) - verify(collapsedLayout).load(context, R.xml.media_session_collapsed) + assertTrue( + mediaViewController.expandedLayout + .getConstraint(MediaViewController.recSizingViewId) + .layout + .mHeight == + context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded) + ) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt index bd99cd482859..eeebd4fb7792 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt @@ -84,6 +84,7 @@ class FontScalingTileTest : SysuiTestCase() { qsLogger, dialogLaunchAnimator, FakeSettings(), + FakeSettings(), featureFlags ) fontScalingTile.initialize() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index 526dc8d150fe..dd7929771bb2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -223,6 +223,16 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { } @Test + public void attach_fadingAway_wallpaperVisible() { + clearInvocations(mWindowManager); + mNotificationShadeWindowController.attach(); + mNotificationShadeWindowController.setKeyguardFadingAway(true); + + verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture()); + assertThat((mLayoutParameters.getValue().flags & FLAG_SHOW_WALLPAPER) != 0).isTrue(); + } + + @Test public void setBackgroundBlurRadius_expandedWithBlurs() { mNotificationShadeWindowController.setBackgroundBlurRadius(10); verify(mNotificationShadeWindowView).setVisibility(eq(View.VISIBLE)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index 7db219719bf0..1aba1fc37b64 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -1342,6 +1342,18 @@ public class CentralSurfacesImplTest extends SysuiTestCase { } @Test + public void keyguard_notHidden_ifGoingAwayAndOccluded() { + setKeyguardShowingAndOccluded(true /* showing */, false /* occluded */); + + when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true); + when(mKeyguardStateController.isOccluded()).thenReturn(true); + + mCentralSurfaces.updateIsKeyguard(false); + + verify(mStatusBarStateController, never()).setState(eq(SHADE), anyBoolean()); + } + + @Test public void frpLockedDevice_shadeDisabled() { when(mDeviceProvisionedController.isFrpActive()).thenReturn(true); when(mDozeServiceHost.isPulsing()).thenReturn(true); diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index d422f9a1ab63..0edb8f27d413 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -2263,6 +2263,15 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } if (userState.mEnabledServices.contains(componentName) && !mUiAutomationManager.suppressingAccessibilityServicesLocked()) { + // Skip the enabling service disallowed by device admin policy. + if (!isAccessibilityTargetAllowed(componentName.getPackageName(), + installedService.getResolveInfo().serviceInfo.applicationInfo.uid, + userState.mUserId)) { + Slog.d(LOG_TAG, "Skipping enabling service disallowed by device admin policy: " + + componentName); + disableAccessibilityServiceLocked(componentName, userState.mUserId); + continue; + } if (service == null) { service = new AccessibilityServiceConnection(userState, mContext, componentName, installedService, sIdCounter++, mMainHandler, mLock, mSecurityPolicy, @@ -3875,32 +3884,29 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } - @Override - @RequiresPermission(anyOf = { - android.Manifest.permission.MANAGE_USERS, - android.Manifest.permission.QUERY_ADMIN_POLICY}) public boolean isAccessibilityTargetAllowed(String packageName, int uid, int userId) { - final DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class); - final List<String> permittedServices = dpm.getPermittedAccessibilityServices(userId); - - // permittedServices null means all accessibility services are allowed. - boolean allowed = permittedServices == null || permittedServices.contains(packageName); - if (allowed) { - final AppOpsManager appOps = mContext.getSystemService(AppOpsManager.class); - final int mode = appOps.noteOpNoThrow( - AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS, - uid, packageName, /* attributionTag= */ null, /* message= */ null); - final boolean ecmEnabled = mContext.getResources().getBoolean( - R.bool.config_enhancedConfirmationModeEnabled); - return !ecmEnabled || mode == AppOpsManager.MODE_ALLOWED; + final long identity = Binder.clearCallingIdentity(); + try { + final DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class); + final List<String> permittedServices = dpm.getPermittedAccessibilityServices(userId); + + // permittedServices null means all accessibility services are allowed. + boolean allowed = permittedServices == null || permittedServices.contains(packageName); + if (allowed) { + final AppOpsManager appOps = mContext.getSystemService(AppOpsManager.class); + final int mode = appOps.noteOpNoThrow( + AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS, + uid, packageName, /* attributionTag= */ null, /* message= */ null); + final boolean ecmEnabled = mContext.getResources().getBoolean( + R.bool.config_enhancedConfirmationModeEnabled); + return !ecmEnabled || mode == AppOpsManager.MODE_ALLOWED; + } + return false; + } finally { + Binder.restoreCallingIdentity(identity); } - return false; } - @Override - @RequiresPermission(anyOf = { - android.Manifest.permission.MANAGE_USERS, - android.Manifest.permission.QUERY_ADMIN_POLICY}) public boolean sendRestrictedDialogIntent(String packageName, int uid, int userId) { // The accessibility service is allowed. Don't show the restricted dialog. if (isAccessibilityTargetAllowed(packageName, uid, userId)) { diff --git a/services/core/java/android/app/usage/UsageStatsManagerInternal.java b/services/core/java/android/app/usage/UsageStatsManagerInternal.java index 70eeb7fecc8f..fc565111dbe8 100644 --- a/services/core/java/android/app/usage/UsageStatsManagerInternal.java +++ b/services/core/java/android/app/usage/UsageStatsManagerInternal.java @@ -203,6 +203,16 @@ public abstract class UsageStatsManagerInternal { public abstract void setActiveAdminApps(Set<String> adminApps, int userId); /** + * Called by DevicePolicyManagerService to inform about the protected packages for a user. + * User control will be disabled for protected packages. + * + * @param packageNames the set of protected packages for {@code userId}. + * @param userId the userId to which the protected packages belong. + */ + public abstract void setAdminProtectedPackages(@Nullable Set<String> packageNames, + @UserIdInt int userId); + + /** * Called by DevicePolicyManagerService during boot to inform that admin data is loaded and * pushed to UsageStatsService. */ diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 3e86c454fae8..0e4331ee0d38 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -10471,16 +10471,16 @@ public class AudioService extends IAudioService.Stub @Override @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) - public float getRs2Value() { - super.getRs2Value_enforcePermission(); - return mSoundDoseHelper.getRs2Value(); + public float getOutputRs2UpperBound() { + super.getOutputRs2UpperBound_enforcePermission(); + return mSoundDoseHelper.getOutputRs2UpperBound(); } @Override @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) - public void setRs2Value(float rs2Value) { - super.setRs2Value_enforcePermission(); - mSoundDoseHelper.setRs2Value(rs2Value); + public void setOutputRs2UpperBound(float rs2Value) { + super.setOutputRs2UpperBound_enforcePermission(); + mSoundDoseHelper.setOutputRs2UpperBound(rs2Value); } @Override diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java index c2483671a065..d6a9d3aa8ce3 100644 --- a/services/core/java/com/android/server/audio/SoundDoseHelper.java +++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java @@ -300,7 +300,7 @@ public class SoundDoseHelper { SAFE_MEDIA_VOLUME_UNINITIALIZED); } - float getRs2Value() { + float getOutputRs2UpperBound() { if (!mEnableCsd) { return 0.f; } @@ -312,14 +312,14 @@ public class SoundDoseHelper { } try { - return soundDose.getOutputRs2(); + return soundDose.getOutputRs2UpperBound(); } catch (RemoteException e) { Log.e(TAG, "Exception while getting the RS2 exposure value", e); return 0.f; } } - void setRs2Value(float rs2Value) { + void setOutputRs2UpperBound(float rs2Value) { if (!mEnableCsd) { return; } @@ -331,7 +331,7 @@ public class SoundDoseHelper { } try { - soundDose.setOutputRs2(rs2Value); + soundDose.setOutputRs2UpperBound(rs2Value); } catch (RemoteException e) { Log.e(TAG, "Exception while setting the RS2 exposure value", e); } diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java index b66120d428c3..6a010424db13 100644 --- a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java +++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java @@ -86,8 +86,7 @@ final class IRadioServiceAidlImpl extends IRadioService.Stub { @Override public ITuner openTuner(int moduleId, RadioManager.BandConfig bandConfig, - boolean withAudio, ITunerCallback callback, int targetSdkVersion) - throws RemoteException { + boolean withAudio, ITunerCallback callback) throws RemoteException { if (isDebugEnabled()) { Slogf.d(TAG, "Opening module %d", moduleId); } @@ -95,7 +94,7 @@ final class IRadioServiceAidlImpl extends IRadioService.Stub { if (callback == null) { throw new IllegalArgumentException("Callback must not be null"); } - return mHalAidl.openSession(moduleId, bandConfig, withAudio, callback, targetSdkVersion); + return mHalAidl.openSession(moduleId, bandConfig, withAudio, callback); } @Override diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java index 8a1ba19e24ab..408fba1bff3b 100644 --- a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java +++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java @@ -92,8 +92,7 @@ final class IRadioServiceHidlImpl extends IRadioService.Stub { @Override public ITuner openTuner(int moduleId, RadioManager.BandConfig bandConfig, - boolean withAudio, ITunerCallback callback, int targetSdkVersion) - throws RemoteException { + boolean withAudio, ITunerCallback callback) throws RemoteException { if (isDebugEnabled()) { Slog.d(TAG, "Opening module " + moduleId); } diff --git a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java index 772cd4144f11..03acf72725e7 100644 --- a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java +++ b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java @@ -199,8 +199,7 @@ public final class BroadcastRadioServiceImpl { */ @Nullable public ITuner openSession(int moduleId, @Nullable RadioManager.BandConfig legacyConfig, - boolean withAudio, ITunerCallback callback, int targetSdkVersion) - throws RemoteException { + boolean withAudio, ITunerCallback callback) throws RemoteException { if (DEBUG) { Slogf.d(TAG, "Open AIDL radio session"); } @@ -223,7 +222,7 @@ public final class BroadcastRadioServiceImpl { } } - TunerSession tunerSession = radioModule.openSession(callback, targetSdkVersion); + TunerSession tunerSession = radioModule.openSession(callback); if (legacyConfig != null) { tunerSession.setConfiguration(legacyConfig); } diff --git a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java index c0a238feb9b5..4f2bfd1c1e52 100644 --- a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java +++ b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java @@ -17,6 +17,10 @@ package com.android.server.broadcastradio.aidl; import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.app.compat.CompatChanges; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; import android.hardware.broadcastradio.AmFmRegionConfig; import android.hardware.broadcastradio.Announcement; import android.hardware.broadcastradio.DabTableEntry; @@ -57,16 +61,24 @@ import java.util.Set; * {@link android.hardware.radio} */ final class ConversionUtils { - // TODO(b/241118988): Add unit test for ConversionUtils class private static final String TAG = "BcRadioAidlSrv.convert"; + /** + * With RADIO_U_VERSION_REQUIRED enabled, 44-bit DAB identifier + * {@link IdentifierType#DAB_SID_EXT} from broadcast radio HAL can be passed as + * {@link ProgramSelector#IDENTIFIER_TYPE_DAB_DMB_SID_EXT} to {@link RadioTuner}. + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + public static final long RADIO_U_VERSION_REQUIRED = 261770108L; + private ConversionUtils() { throw new UnsupportedOperationException("ConversionUtils class is noninstantiable"); } - static boolean isAtLeastU(int targetSdkVersion) { - // TODO(b/261770108): Use version code for U. - return targetSdkVersion >= Build.VERSION_CODES.CUR_DEVELOPMENT; + @SuppressLint("AndroidFrameworkRequiresPermission") + static boolean isAtLeastU(int uid) { + return CompatChanges.isChangeEnabled(RADIO_U_VERSION_REQUIRED, uid); } static RuntimeException throwOnError(RuntimeException halException, String action) { @@ -584,9 +596,8 @@ final class ConversionUtils { return id.getType() == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT; } - static boolean programSelectorMeetsSdkVersionRequirement(ProgramSelector sel, - int targetSdkVersion) { - if (isAtLeastU(targetSdkVersion)) { + static boolean programSelectorMeetsSdkVersionRequirement(ProgramSelector sel, int uid) { + if (isAtLeastU(uid)) { return true; } if (sel.getPrimaryId().getType() == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT) { @@ -601,12 +612,11 @@ final class ConversionUtils { return true; } - static boolean programInfoMeetsSdkVersionRequirement(RadioManager.ProgramInfo info, - int targetSdkVersion) { - if (isAtLeastU(targetSdkVersion)) { + static boolean programInfoMeetsSdkVersionRequirement(RadioManager.ProgramInfo info, int uid) { + if (isAtLeastU(uid)) { return true; } - if (!programSelectorMeetsSdkVersionRequirement(info.getSelector(), targetSdkVersion)) { + if (!programSelectorMeetsSdkVersionRequirement(info.getSelector(), uid)) { return false; } if (isNewIdentifierInU(info.getLogicallyTunedTo()) @@ -622,16 +632,15 @@ final class ConversionUtils { return true; } - static ProgramList.Chunk convertChunkToTargetSdkVersion(ProgramList.Chunk chunk, - int targetSdkVersion) { - if (isAtLeastU(targetSdkVersion)) { + static ProgramList.Chunk convertChunkToTargetSdkVersion(ProgramList.Chunk chunk, int uid) { + if (isAtLeastU(uid)) { return chunk; } Set<RadioManager.ProgramInfo> modified = new ArraySet<>(); Iterator<RadioManager.ProgramInfo> modifiedIterator = chunk.getModified().iterator(); while (modifiedIterator.hasNext()) { RadioManager.ProgramInfo info = modifiedIterator.next(); - if (programInfoMeetsSdkVersionRequirement(info, targetSdkVersion)) { + if (programInfoMeetsSdkVersionRequirement(info, uid)) { modified.add(info); } } diff --git a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java index f8c19ae2b8f1..132fb8ef8a95 100644 --- a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java +++ b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java @@ -101,9 +101,9 @@ final class RadioModule { ConversionUtils.programSelectorFromHalProgramSelector(programSelector); int tunerResult = ConversionUtils.halResultToTunerResult(result); synchronized (mLock) { - fanoutAidlCallbackLocked((cb, sdkVersion) -> { + fanoutAidlCallbackLocked((cb, uid) -> { if (csel != null && !ConversionUtils - .programSelectorMeetsSdkVersionRequirement(csel, sdkVersion)) { + .programSelectorMeetsSdkVersionRequirement(csel, uid)) { Slogf.e(TAG, "onTuneFailed: cannot send program selector " + "requiring higher target SDK version"); return; @@ -123,9 +123,9 @@ final class RadioModule { "Program info from AIDL HAL is invalid"); synchronized (mLock) { mCurrentProgramInfo = currentProgramInfo; - fanoutAidlCallbackLocked((cb, sdkVersion) -> { + fanoutAidlCallbackLocked((cb, uid) -> { if (!ConversionUtils.programInfoMeetsSdkVersionRequirement( - currentProgramInfo, sdkVersion)) { + currentProgramInfo, uid)) { Slogf.e(TAG, "onCurrentProgramInfoChanged: cannot send " + "program info requiring higher target SDK version"); return; @@ -156,7 +156,7 @@ final class RadioModule { fireLater(() -> { synchronized (mLock) { mAntennaConnected = connected; - fanoutAidlCallbackLocked((cb, sdkVersion) -> cb.onAntennaState(connected)); + fanoutAidlCallbackLocked((cb, uid) -> cb.onAntennaState(connected)); } }); } @@ -165,7 +165,7 @@ final class RadioModule { public void onConfigFlagUpdated(int flag, boolean value) { fireLater(() -> { synchronized (mLock) { - fanoutAidlCallbackLocked((cb, sdkVersion) -> { + fanoutAidlCallbackLocked((cb, uid) -> { cb.onConfigFlagUpdated(flag, value); }); } @@ -178,7 +178,7 @@ final class RadioModule { synchronized (mLock) { Map<String, String> cparam = ConversionUtils.vendorInfoFromHalVendorKeyValues(parameters); - fanoutAidlCallbackLocked((cb, sdkVersion) -> { + fanoutAidlCallbackLocked((cb, uid) -> { cb.onParametersUpdated(cparam); }); } @@ -244,14 +244,14 @@ final class RadioModule { mService.setTunerCallback(mHalTunerCallback); } - TunerSession openSession(android.hardware.radio.ITunerCallback userCb, int targetSdkVersion) + TunerSession openSession(android.hardware.radio.ITunerCallback userCb) throws RemoteException { mLogger.logRadioEvent("Open TunerSession"); TunerSession tunerSession; Boolean antennaConnected; RadioManager.ProgramInfo currentProgramInfo; synchronized (mLock) { - tunerSession = new TunerSession(this, mService, userCb, targetSdkVersion); + tunerSession = new TunerSession(this, mService, userCb); mAidlTunerSessions.add(tunerSession); antennaConnected = mAntennaConnected; currentProgramInfo = mCurrentProgramInfo; @@ -404,7 +404,7 @@ final class RadioModule { } interface AidlCallbackRunnable { - void run(android.hardware.radio.ITunerCallback callback, int targetSdkVersion) + void run(android.hardware.radio.ITunerCallback callback, int uid) throws RemoteException; } @@ -423,7 +423,7 @@ final class RadioModule { for (int i = 0; i < mAidlTunerSessions.size(); i++) { try { runnable.run(mAidlTunerSessions.valueAt(i).mCallback, - mAidlTunerSessions.valueAt(i).getTargetSdkVersion()); + mAidlTunerSessions.valueAt(i).getUid()); } catch (DeadObjectException ex) { // The other side died without calling close(), so just purge it from our records. Slogf.e(TAG, "Removing dead TunerSession"); diff --git a/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java b/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java index fe8c23845d2a..0a3823f038fa 100644 --- a/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java +++ b/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java @@ -24,6 +24,7 @@ import android.hardware.radio.ITuner; import android.hardware.radio.ProgramList; import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; +import android.os.Binder; import android.os.RemoteException; import android.util.ArrayMap; import android.util.ArraySet; @@ -46,7 +47,7 @@ final class TunerSession extends ITuner.Stub { private final RadioLogger mLogger; private final RadioModule mModule; final android.hardware.radio.ITunerCallback mCallback; - private final int mTargetSdkVersion; + private final int mUid; private final IBroadcastRadio mService; @GuardedBy("mLock") @@ -61,11 +62,11 @@ final class TunerSession extends ITuner.Stub { private RadioManager.BandConfig mPlaceHolderConfig; TunerSession(RadioModule radioModule, IBroadcastRadio service, - android.hardware.radio.ITunerCallback callback, int targetSdkVersion) { + android.hardware.radio.ITunerCallback callback) { mModule = Objects.requireNonNull(radioModule, "radioModule cannot be null"); mService = Objects.requireNonNull(service, "service cannot be null"); mCallback = Objects.requireNonNull(callback, "callback cannot be null"); - mTargetSdkVersion = targetSdkVersion; + mUid = Binder.getCallingUid(); mLogger = new RadioLogger(TAG, TUNER_EVENT_LOGGER_QUEUE_SIZE); } @@ -130,7 +131,7 @@ final class TunerSession extends ITuner.Stub { mPlaceHolderConfig = Objects.requireNonNull(config, "config cannot be null"); } Slogf.i(TAG, "Ignoring setConfiguration - not applicable for broadcastradio HAL AIDL"); - mModule.fanoutAidlCallback((cb, sdkVersion) -> cb.onConfigurationChanged(config)); + mModule.fanoutAidlCallback((cb, mUid) -> cb.onConfigurationChanged(config)); } @Override @@ -254,7 +255,7 @@ final class TunerSession extends ITuner.Stub { Slogf.w(TAG, "Cannot start background scan on AIDL HAL client from non-current user"); return false; } - mModule.fanoutAidlCallback((cb, sdkVersion) -> { + mModule.fanoutAidlCallback((cb, mUid) -> { cb.onBackgroundScanComplete(); }); return true; @@ -284,8 +285,8 @@ final class TunerSession extends ITuner.Stub { mModule.onTunerSessionProgramListFilterChanged(this); } - int getTargetSdkVersion() { - return mTargetSdkVersion; + int getUid() { + return mUid; } ProgramList.Filter getProgramListFilter() { @@ -323,10 +324,9 @@ final class TunerSession extends ITuner.Stub { } for (int i = 0; i < chunks.size(); i++) { try { - if (!ConversionUtils.isAtLeastU(getTargetSdkVersion())) { + if (!ConversionUtils.isAtLeastU(getUid())) { ProgramList.Chunk downgradedChunk = - ConversionUtils.convertChunkToTargetSdkVersion(chunks.get(i), - getTargetSdkVersion()); + ConversionUtils.convertChunkToTargetSdkVersion(chunks.get(i), getUid()); mCallback.onProgramListUpdated(downgradedChunk); } else { mCallback.onProgramListUpdated(chunks.get(i)); diff --git a/services/core/java/com/android/server/cpu/CpuAvailabilityInfo.java b/services/core/java/com/android/server/cpu/CpuAvailabilityInfo.java index 06b45bf0fb4b..97507be96d84 100644 --- a/services/core/java/com/android/server/cpu/CpuAvailabilityInfo.java +++ b/services/core/java/com/android/server/cpu/CpuAvailabilityInfo.java @@ -21,6 +21,8 @@ import static com.android.server.cpu.CpuAvailabilityMonitoringConfig.CPUSET_BACK import com.android.internal.util.Preconditions; +import java.util.Objects; + /** CPU availability information. */ public final class CpuAvailabilityInfo { /** Constant to indicate missing CPU availability percent. */ @@ -35,29 +37,64 @@ public final class CpuAvailabilityInfo { @CpuAvailabilityMonitoringConfig.Cpuset public final int cpuset; + /** Uptime (in milliseconds) when the data in this object was captured. */ + public final long dataTimestampUptimeMillis; + /** The latest average CPU availability percent. */ public final int latestAvgAvailabilityPercent; - /** The past N-second average CPU availability percent. */ - public final int pastNSecAvgAvailabilityPercent; + /** + * The past N-millisecond average CPU availability percent. + * + * <p>When there is not enough data to calculate the past N-millisecond average, this field will + * contain the value {@link MISSING_CPU_AVAILABILITY_PERCENT}. + */ + public final int pastNMillisAvgAvailabilityPercent; - /** The duration over which the {@link pastNSecAvgAvailabilityPercent} was calculated. */ - public final int avgAvailabilityDurationSec; + /** The duration over which the {@link pastNMillisAvgAvailabilityPercent} was calculated. */ + public final long pastNMillisDuration; @Override public String toString() { - return "CpuAvailabilityInfo{" + "cpuset=" + cpuset + ", latestAvgAvailabilityPercent=" - + latestAvgAvailabilityPercent + ", pastNSecAvgAvailabilityPercent=" - + pastNSecAvgAvailabilityPercent + ", avgAvailabilityDurationSec=" - + avgAvailabilityDurationSec + '}'; + return "CpuAvailabilityInfo{" + "cpuset = " + cpuset + ", dataTimestampUptimeMillis = " + + dataTimestampUptimeMillis + ", latestAvgAvailabilityPercent = " + + latestAvgAvailabilityPercent + ", pastNMillisAvgAvailabilityPercent = " + + pastNMillisAvgAvailabilityPercent + ", pastNMillisDuration = " + + pastNMillisDuration + '}'; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof CpuAvailabilityInfo)) { + return false; + } + CpuAvailabilityInfo info = (CpuAvailabilityInfo) obj; + return cpuset == info.cpuset && dataTimestampUptimeMillis == info.dataTimestampUptimeMillis + && latestAvgAvailabilityPercent == info.latestAvgAvailabilityPercent + && pastNMillisAvgAvailabilityPercent == info.pastNMillisAvgAvailabilityPercent + && pastNMillisDuration == info.pastNMillisDuration; + } + + @Override + public int hashCode() { + return Objects.hash(cpuset, dataTimestampUptimeMillis, latestAvgAvailabilityPercent, + pastNMillisAvgAvailabilityPercent, pastNMillisDuration); } - CpuAvailabilityInfo(int cpuset, int latestAvgAvailabilityPercent, - int pastNSecAvgAvailabilityPercent, int avgAvailabilityDurationSec) { + CpuAvailabilityInfo(int cpuset, long dataTimestampUptimeMillis, + int latestAvgAvailabilityPercent, int pastNMillisAvgAvailabilityPercent, + long pastNMillisDuration) { this.cpuset = Preconditions.checkArgumentInRange(cpuset, CPUSET_ALL, CPUSET_BACKGROUND, "cpuset"); - this.latestAvgAvailabilityPercent = latestAvgAvailabilityPercent; - this.pastNSecAvgAvailabilityPercent = pastNSecAvgAvailabilityPercent; - this.avgAvailabilityDurationSec = avgAvailabilityDurationSec; + this.dataTimestampUptimeMillis = + Preconditions.checkArgumentNonnegative(dataTimestampUptimeMillis); + this.latestAvgAvailabilityPercent = Preconditions.checkArgumentNonnegative( + latestAvgAvailabilityPercent); + this.pastNMillisAvgAvailabilityPercent = pastNMillisAvgAvailabilityPercent; + this.pastNMillisDuration = Preconditions.checkArgumentNonnegative( + pastNMillisDuration); } } diff --git a/services/core/java/com/android/server/cpu/CpuAvailabilityMonitoringConfig.java b/services/core/java/com/android/server/cpu/CpuAvailabilityMonitoringConfig.java index a3c4c9e828b4..cbe02fc09d84 100644 --- a/services/core/java/com/android/server/cpu/CpuAvailabilityMonitoringConfig.java +++ b/services/core/java/com/android/server/cpu/CpuAvailabilityMonitoringConfig.java @@ -90,8 +90,19 @@ public final class CpuAvailabilityMonitoringConfig { @Override public String toString() { - return "CpuAvailabilityMonitoringConfig{cpuset=" + cpuset + ", mThresholds=" + mThresholds - + ')'; + return "CpuAvailabilityMonitoringConfig{cpuset=" + toCpusetString(cpuset) + ", mThresholds=" + + mThresholds + ')'; + } + + /** Returns the string equivalent of the provided cpuset. */ + public static String toCpusetString(int cpuset) { + switch (cpuset) { + case CPUSET_ALL: + return "CPUSET_ALL"; + case CPUSET_BACKGROUND: + return "CPUSET_BACKGROUND"; + } + return "Invalid cpuset: " + cpuset; } private CpuAvailabilityMonitoringConfig(Builder builder) { diff --git a/services/core/java/com/android/server/cpu/CpuInfoReader.java b/services/core/java/com/android/server/cpu/CpuInfoReader.java index ca97a9847b39..ce68edbb9fa1 100644 --- a/services/core/java/com/android/server/cpu/CpuInfoReader.java +++ b/services/core/java/com/android/server/cpu/CpuInfoReader.java @@ -21,8 +21,10 @@ import static com.android.server.cpu.CpuMonitorService.TAG; import android.annotation.IntDef; import android.annotation.Nullable; +import android.os.SystemClock; import android.system.Os; import android.system.OsConstants; +import android.util.IndentingPrintWriter; import android.util.IntArray; import android.util.LongSparseLongArray; import android.util.SparseArray; @@ -50,6 +52,9 @@ public final class CpuInfoReader { private static final String POLICY_DIR_PREFIX = "policy"; private static final String RELATED_CPUS_FILE = "related_cpus"; private static final String AFFECTED_CPUS_FILE = "affected_cpus"; + // TODO(b/263154344): Avoid reading from cpuinfo_cur_freq because non-root users don't have + // read permission for this file. The file permissions are set by the Kernel. Instead, read + // the current frequency only from scaling_cur_freq. private static final String CUR_CPUFREQ_FILE = "cpuinfo_cur_freq"; private static final String MAX_CPUFREQ_FILE = "cpuinfo_max_freq"; private static final String CUR_SCALING_FREQ_FILE = "scaling_cur_freq"; @@ -70,16 +75,18 @@ public final class CpuInfoReader { private static final Pattern TIME_IN_STATE_PATTERN = Pattern.compile("(?<freqKHz>[0-9]+)\\s(?<time>[0-9]+)"); private static final long MILLIS_PER_CLOCK_TICK = 1000L / Os.sysconf(OsConstants._SC_CLK_TCK); + private static final long MIN_READ_INTERVAL_MILLISECONDS = 500; @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = {"FLAG_CPUSET_CATEGORY_"}, flag = true, value = { FLAG_CPUSET_CATEGORY_TOP_APP, FLAG_CPUSET_CATEGORY_BACKGROUND }) - private @interface CpusetCategory{} + /** package **/ @interface CpusetCategory{} // TODO(b/242722241): Protect updatable variables with a local lock. private final File mCpusetDir; + private final long mMinReadIntervalMillis; private final SparseIntArray mCpusetCategoriesByCpus = new SparseIntArray(); private final SparseArray<File> mCpuFreqPolicyDirsById = new SparseArray<>(); private final SparseArray<StaticPolicyInfo> mStaticPolicyInfoById = new SparseArray<>(); @@ -90,16 +97,20 @@ public final class CpuInfoReader { private SparseArray<CpuUsageStats> mCumulativeCpuUsageStats = new SparseArray<>(); private boolean mIsEnabled; private boolean mHasTimeInStateFile; + private long mLastReadUptimeMillis; + private SparseArray<CpuInfo> mLastReadCpuInfos; public CpuInfoReader() { - this(new File(CPUSET_DIR_PATH), new File(CPUFREQ_DIR_PATH), new File(PROC_STAT_FILE_PATH)); + this(new File(CPUSET_DIR_PATH), new File(CPUFREQ_DIR_PATH), new File(PROC_STAT_FILE_PATH), + MIN_READ_INTERVAL_MILLISECONDS); } @VisibleForTesting - CpuInfoReader(File cpusetDir, File cpuFreqDir, File procStatFile) { + CpuInfoReader(File cpusetDir, File cpuFreqDir, File procStatFile, long minReadIntervalMillis) { mCpusetDir = cpusetDir; mCpuFreqDir = cpuFreqDir; mProcStatFile = procStatFile; + mMinReadIntervalMillis = minReadIntervalMillis; } /** @@ -167,6 +178,16 @@ public final class CpuInfoReader { if (!mIsEnabled) { return null; } + long uptimeMillis = SystemClock.uptimeMillis(); + if (mLastReadUptimeMillis > 0 + && uptimeMillis - mLastReadUptimeMillis < mMinReadIntervalMillis) { + Slogf.w(TAG, "Skipping reading from device and returning the last read CpuInfos. " + + "Last read was %d ms ago, min read interval is %d ms", + uptimeMillis - mLastReadUptimeMillis, mMinReadIntervalMillis); + return mLastReadCpuInfos; + } + mLastReadUptimeMillis = uptimeMillis; + mLastReadCpuInfos = null; SparseArray<CpuUsageStats> cpuUsageStatsByCpus = readLatestCpuUsageStats(); if (cpuUsageStatsByCpus == null || cpuUsageStatsByCpus.size() == 0) { Slogf.e(TAG, "Failed to read latest CPU usage stats"); @@ -202,6 +223,12 @@ public final class CpuInfoReader { + " policy ID %d", policyId); continue; } + if (curFreqKHz > maxFreqKHz) { + Slogf.w(TAG, "Current CPU frequency (%d) is greater than maximum CPU frequency" + + " (%d) for policy ID (%d). Skipping CPU frequency policy", curFreqKHz, + maxFreqKHz, policyId); + continue; + } for (int coreIdx = 0; coreIdx < staticPolicyInfo.relatedCpuCores.size(); coreIdx++) { int relatedCpuCore = staticPolicyInfo.relatedCpuCores.get(coreIdx); CpuInfo prevCpuInfo = cpuInfoByCpus.get(relatedCpuCore); @@ -241,9 +268,73 @@ public final class CpuInfoReader { } } } + mLastReadCpuInfos = cpuInfoByCpus; return cpuInfoByCpus; } + /** Dumps the current state. */ + public void dump(IndentingPrintWriter writer) { + writer.printf("*%s*\n", getClass().getSimpleName()); + writer.increaseIndent(); // Add intend for the outermost block. + + writer.printf("mCpusetDir = %s\n", mCpusetDir.getAbsolutePath()); + writer.printf("mCpuFreqDir = %s\n", mCpuFreqDir.getAbsolutePath()); + writer.printf("mProcStatFile = %s\n", mProcStatFile.getAbsolutePath()); + writer.printf("mIsEnabled = %s\n", mIsEnabled); + writer.printf("mHasTimeInStateFile = %s\n", mHasTimeInStateFile); + writer.printf("mLastReadUptimeMillis = %d\n", mLastReadUptimeMillis); + writer.printf("mMinReadIntervalMillis = %d\n", mMinReadIntervalMillis); + + writer.printf("Cpuset categories by CPU core:\n"); + writer.increaseIndent(); + for (int i = 0; i < mCpusetCategoriesByCpus.size(); i++) { + writer.printf("CPU core id = %d, %s\n", mCpusetCategoriesByCpus.keyAt(i), + toCpusetCategoriesStr(mCpusetCategoriesByCpus.valueAt(i))); + } + writer.decreaseIndent(); + + writer.println("Cpu frequency policy directories by policy id:"); + writer.increaseIndent(); + for (int i = 0; i < mCpuFreqPolicyDirsById.size(); i++) { + writer.printf("Policy id = %d, Dir = %s\n", mCpuFreqPolicyDirsById.keyAt(i), + mCpuFreqPolicyDirsById.valueAt(i)); + } + writer.decreaseIndent(); + + writer.println("Static cpu frequency policy infos by policy id:"); + writer.increaseIndent(); + for (int i = 0; i < mStaticPolicyInfoById.size(); i++) { + writer.printf("Policy id = %d, %s\n", mStaticPolicyInfoById.keyAt(i), + mStaticPolicyInfoById.valueAt(i)); + } + writer.decreaseIndent(); + + writer.println("Cpu time in frequency state by policy id:"); + writer.increaseIndent(); + for (int i = 0; i < mTimeInStateByPolicyId.size(); i++) { + writer.printf("Policy id = %d, Time(millis) in state by CPU frequency(KHz) = %s\n", + mTimeInStateByPolicyId.keyAt(i), mTimeInStateByPolicyId.valueAt(i)); + } + writer.decreaseIndent(); + + writer.println("Last read CPU infos:"); + writer.increaseIndent(); + for (int i = 0; i < mLastReadCpuInfos.size(); i++) { + writer.printf("%s\n", mLastReadCpuInfos.valueAt(i)); + } + writer.decreaseIndent(); + + writer.println("Latest cumulative CPU usage stats by CPU core:"); + writer.increaseIndent(); + for (int i = 0; i < mCumulativeCpuUsageStats.size(); i++) { + writer.printf("CPU core id = %d, %s\n", mCumulativeCpuUsageStats.keyAt(i), + mCumulativeCpuUsageStats.valueAt(i)); + } + writer.decreaseIndent(); + + writer.decreaseIndent(); // Remove intend for the outermost block. + } + /** * Sets the CPU frequency for testing. * @@ -496,6 +587,9 @@ public final class CpuInfoReader { for (int i = 0; i < timeInState.size(); i++) { totalTimeInState += timeInState.valueAt(i); } + if (totalTimeInState == 0) { + return CpuInfo.MISSING_FREQUENCY; + } double avgFreqKHz = 0; for (int i = 0; i < timeInState.size(); i++) { avgFreqKHz += (timeInState.keyAt(i) * timeInState.valueAt(i)) / totalTimeInState; @@ -624,16 +718,29 @@ public final class CpuInfoReader { @CpusetCategory public final int cpusetCategories; public final boolean isOnline; + public final long maxCpuFreqKHz; // Values in the below fields may be missing when a CPU core is offline. public final long curCpuFreqKHz; - public final long maxCpuFreqKHz; public final long avgTimeInStateCpuFreqKHz; @Nullable public final CpuUsageStats latestCpuUsageStats; + private long mNormalizedAvailableCpuFreqKHz; + CpuInfo(int cpuCore, @CpusetCategory int cpusetCategories, boolean isOnline, long curCpuFreqKHz, long maxCpuFreqKHz, long avgTimeInStateCpuFreqKHz, CpuUsageStats latestCpuUsageStats) { + this(cpuCore, cpusetCategories, isOnline, curCpuFreqKHz, maxCpuFreqKHz, + avgTimeInStateCpuFreqKHz, /* normalizedAvailableCpuFreqKHz= */ 0, + latestCpuUsageStats); + this.mNormalizedAvailableCpuFreqKHz = computeNormalizedAvailableCpuFreqKHz(); + } + + // Should be used only for testing. + @VisibleForTesting + CpuInfo(int cpuCore, @CpusetCategory int cpusetCategories, boolean isOnline, + long curCpuFreqKHz, long maxCpuFreqKHz, long avgTimeInStateCpuFreqKHz, + long normalizedAvailableCpuFreqKHz, CpuUsageStats latestCpuUsageStats) { this.cpuCore = cpuCore; this.cpusetCategories = cpusetCategories; this.isOnline = isOnline; @@ -641,6 +748,11 @@ public final class CpuInfoReader { this.maxCpuFreqKHz = maxCpuFreqKHz; this.avgTimeInStateCpuFreqKHz = avgTimeInStateCpuFreqKHz; this.latestCpuUsageStats = latestCpuUsageStats; + this.mNormalizedAvailableCpuFreqKHz = normalizedAvailableCpuFreqKHz; + } + + public long getNormalizedAvailableCpuFreqKHz() { + return mNormalizedAvailableCpuFreqKHz; } @Override @@ -657,6 +769,8 @@ public final class CpuInfoReader { .append(avgTimeInStateCpuFreqKHz == MISSING_FREQUENCY ? "missing" : avgTimeInStateCpuFreqKHz) .append(", latestCpuUsageStats = ").append(latestCpuUsageStats) + .append(", mNormalizedAvailableCpuFreqKHz = ") + .append(mNormalizedAvailableCpuFreqKHz) .append(" }").toString(); } @@ -673,13 +787,32 @@ public final class CpuInfoReader { && isOnline == other.isOnline && curCpuFreqKHz == other.curCpuFreqKHz && maxCpuFreqKHz == other.maxCpuFreqKHz && avgTimeInStateCpuFreqKHz == other.avgTimeInStateCpuFreqKHz - && latestCpuUsageStats.equals(other.latestCpuUsageStats); + && latestCpuUsageStats.equals(other.latestCpuUsageStats) + && mNormalizedAvailableCpuFreqKHz == other.mNormalizedAvailableCpuFreqKHz; } @Override public int hashCode() { return Objects.hash(cpuCore, cpusetCategories, isOnline, curCpuFreqKHz, maxCpuFreqKHz, - avgTimeInStateCpuFreqKHz, latestCpuUsageStats); + avgTimeInStateCpuFreqKHz, latestCpuUsageStats, mNormalizedAvailableCpuFreqKHz); + } + + private long computeNormalizedAvailableCpuFreqKHz() { + if (!isOnline) { + return MISSING_FREQUENCY; + } + long totalTimeMillis = latestCpuUsageStats.getTotalTimeMillis(); + if (totalTimeMillis == 0) { + Slogf.wtf(TAG, "Total CPU time millis is 0. This shouldn't happen unless stats are" + + " polled too frequently"); + return MISSING_FREQUENCY; + } + double nonIdlePercent = 100.0 * (totalTimeMillis + - (double) latestCpuUsageStats.idleTimeMillis) / totalTimeMillis; + long curFreqKHz = avgTimeInStateCpuFreqKHz == MISSING_FREQUENCY + ? curCpuFreqKHz : avgTimeInStateCpuFreqKHz; + double availablePercent = 100.0 - (nonIdlePercent * curFreqKHz / maxCpuFreqKHz); + return (long) ((availablePercent * maxCpuFreqKHz) / 100.0); } } @@ -712,7 +845,7 @@ public final class CpuInfoReader { this.guestNiceTimeMillis = guestNiceTimeMillis; } - public long getTotalTime() { + public long getTotalTimeMillis() { return userTimeMillis + niceTimeMillis + systemTimeMillis + idleTimeMillis + iowaitTimeMillis + irqTimeMillis + softirqTimeMillis + stealTimeMillis + guestTimeMillis + guestNiceTimeMillis; @@ -796,8 +929,8 @@ public final class CpuInfoReader { @Override public String toString() { - return "FrequencyPair{cpuFreqKHz=" + cpuFreqKHz + ", scalingFreqKHz=" + scalingFreqKHz - + '}'; + return "FrequencyPair{cpuFreqKHz = " + cpuFreqKHz + ", scalingFreqKHz = " + + scalingFreqKHz + '}'; } } @@ -812,7 +945,7 @@ public final class CpuInfoReader { @Override public String toString() { - return "StaticPolicyInfo{maxCpuFreqPair=" + maxCpuFreqPair + ", relatedCpuCores=" + return "StaticPolicyInfo{maxCpuFreqPair = " + maxCpuFreqPair + ", relatedCpuCores = " + relatedCpuCores + '}'; } } @@ -831,9 +964,9 @@ public final class CpuInfoReader { @Override public String toString() { - return "DynamicPolicyInfo{curCpuFreqPair=" + curCpuFreqPair - + ", avgTimeInStateCpuFreqKHz=" + avgTimeInStateCpuFreqKHz - + ", affectedCpuCores=" + affectedCpuCores + '}'; + return "DynamicPolicyInfo{curCpuFreqPair = " + curCpuFreqPair + + ", avgTimeInStateCpuFreqKHz = " + avgTimeInStateCpuFreqKHz + + ", affectedCpuCores = " + affectedCpuCores + '}'; } } } diff --git a/services/core/java/com/android/server/cpu/CpuMonitorService.java b/services/core/java/com/android/server/cpu/CpuMonitorService.java index 4eefe5c8cad5..df8cfad4ab03 100644 --- a/services/core/java/com/android/server/cpu/CpuMonitorService.java +++ b/services/core/java/com/android/server/cpu/CpuMonitorService.java @@ -18,15 +18,33 @@ package com.android.server.cpu; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL; +import static com.android.server.cpu.CpuAvailabilityMonitoringConfig.CPUSET_ALL; +import static com.android.server.cpu.CpuAvailabilityMonitoringConfig.CPUSET_BACKGROUND; +import static com.android.server.cpu.CpuInfoReader.FLAG_CPUSET_CATEGORY_BACKGROUND; +import static com.android.server.cpu.CpuInfoReader.FLAG_CPUSET_CATEGORY_TOP_APP; + +import android.annotation.Nullable; import android.content.Context; import android.os.Binder; -import android.util.ArrayMap; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; +import android.os.SystemClock; import android.util.IndentingPrintWriter; +import android.util.IntArray; import android.util.Log; +import android.util.LongSparseArray; +import android.util.SparseArray; +import android.util.SparseArrayMap; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DumpUtils; +import com.android.internal.util.Preconditions; +import com.android.server.ServiceThread; import com.android.server.SystemService; +import com.android.server.Watchdog; import com.android.server.utils.PriorityDump; import com.android.server.utils.Slogf; @@ -34,28 +52,55 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Objects; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; /** Service to monitor CPU availability and usage. */ public final class CpuMonitorService extends SystemService { static final String TAG = CpuMonitorService.class.getSimpleName(); static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - // TODO(b/242722241): Make this a resource overlay property. - // Maintain 3 monitoring intervals: - // * One to poll very frequently when mCpuAvailabilityCallbackInfoByCallbacks are available and - // CPU availability is above a threshold (such as at least 10% of CPU is available). - // * One to poll less frequently when mCpuAvailabilityCallbackInfoByCallbacks are available - // and CPU availability is below a threshold (such as less than 10% of CPU is available). - // * One to poll very less frequently when no callbacks are available and the build is either - // user-debug or eng. This will be useful for debugging in development environment. - static final int DEFAULT_CPU_MONITORING_INTERVAL_MILLISECONDS = 5_000; + // TODO(b/267500110): Make these constants resource overlay properties. + /** Default monitoring interval when no monitoring is in progress. */ + static final long DEFAULT_MONITORING_INTERVAL_MILLISECONDS = -1; + /** Monitoring interval when callbacks are registered and the CPU load is normal. */ + private static final long NORMAL_MONITORING_INTERVAL_MILLISECONDS = + TimeUnit.SECONDS.toMillis(5); + + /** + * Monitoring interval when no registered callbacks and the build is either user-debug or eng. + */ + private static final long DEBUG_MONITORING_INTERVAL_MILLISECONDS = TimeUnit.MINUTES.toMillis(1); + /** + * Size of the in-memory cache relative to the current uptime. + * + * On user-debug or eng builds, continuously cache stats with a bigger cache size for debugging + * purposes. + */ + private static final long CACHE_DURATION_MILLISECONDS = Build.IS_USERDEBUG || Build.IS_ENG + ? TimeUnit.MINUTES.toMillis(30) : TimeUnit.MINUTES.toMillis(10); + // TODO(b/267500110): Investigate whether this duration should change when the monitoring + // interval is updated. When the CPU is under heavy load, the monitoring will happen less + // frequently. Should this duration be increased as well when this happens? + private static final long LATEST_AVAILABILITY_DURATION_MILLISECONDS = + TimeUnit.SECONDS.toMillis(30); private final Context mContext; + private final HandlerThread mHandlerThread; + private final CpuInfoReader mCpuInfoReader; + private final boolean mShouldDebugMonitor; + private final long mNormalMonitoringIntervalMillis; + private final long mDebugMonitoringIntervalMillis; + private final long mLatestAvailabilityDurationMillis; private final Object mLock = new Object(); @GuardedBy("mLock") - private final ArrayMap<CpuMonitorInternal.CpuAvailabilityCallback, CpuAvailabilityCallbackInfo> - mCpuAvailabilityCallbackInfoByCallbacks = new ArrayMap<>(); + private final SparseArrayMap<CpuMonitorInternal.CpuAvailabilityCallback, + CpuAvailabilityCallbackInfo> mAvailabilityCallbackInfosByCallbacksByCpuset; @GuardedBy("mLock") - private long mMonitoringIntervalMilliseconds = DEFAULT_CPU_MONITORING_INTERVAL_MILLISECONDS; + private final SparseArray<CpusetInfo> mCpusetInfosByCpuset; + private final Runnable mMonitorCpuStats = this::monitorCpuStats; + + @GuardedBy("mLock") + private long mCurrentMonitoringIntervalMillis = DEFAULT_MONITORING_INTERVAL_MILLISECONDS; + private Handler mHandler; private final CpuMonitorInternal mLocalService = new CpuMonitorInternal() { @Override @@ -63,89 +108,389 @@ public final class CpuMonitorService extends SystemService { CpuAvailabilityMonitoringConfig config, CpuAvailabilityCallback callback) { Objects.requireNonNull(callback, "Callback must be non-null"); Objects.requireNonNull(config, "Config must be non-null"); + CpuAvailabilityCallbackInfo callbackInfo; synchronized (mLock) { - if (mCpuAvailabilityCallbackInfoByCallbacks.containsKey(callback)) { - Slogf.i(TAG, "Overwriting the existing CpuAvailabilityCallback %s", - mCpuAvailabilityCallbackInfoByCallbacks.get(callback)); - // TODO(b/242722241): Overwrite any internal cache (will be added in future CLs) - // that maps callbacks based on the CPU availability thresholds. - } - CpuAvailabilityCallbackInfo info = new CpuAvailabilityCallbackInfo(config, - executor); - mCpuAvailabilityCallbackInfoByCallbacks.put(callback, info); - if (DEBUG) { - Slogf.d(TAG, "Added a CPU availability callback: %s", info); + // Verify all CPUSET entries before adding the callback because this will help + // delete any previously added callback for a different CPUSET. + for (int i = 0; i < mAvailabilityCallbackInfosByCallbacksByCpuset.numMaps(); i++) { + int cpuset = mAvailabilityCallbackInfosByCallbacksByCpuset.keyAt(i); + callbackInfo = mAvailabilityCallbackInfosByCallbacksByCpuset.delete(cpuset, + callback); + if (callbackInfo != null) { + Slogf.i(TAG, "Overwriting the existing %s", callbackInfo); + } } + callbackInfo = newCallbackInfoLocked(config, callback, executor); + } + asyncNotifyMonitoringIntervalChangeToClient(callbackInfo); + if (DEBUG) { + Slogf.d(TAG, "Successfully added %s", callbackInfo); } - // TODO(b/242722241): - // * On the executor or on the handler thread, call the callback with the latest CPU - // availability info and monitoring interval. - // * Monitor the CPU stats more frequently when the first callback is added. } @Override public void removeCpuAvailabilityCallback(CpuAvailabilityCallback callback) { synchronized (mLock) { - if (!mCpuAvailabilityCallbackInfoByCallbacks.containsKey(callback)) { - Slogf.i(TAG, "CpuAvailabilityCallback was not previously added." - + " Ignoring the remove request"); - return; - } - CpuAvailabilityCallbackInfo info = - mCpuAvailabilityCallbackInfoByCallbacks.remove(callback); - if (DEBUG) { - Slogf.d(TAG, "Removed a CPU availability callback: %s", info); + for (int i = 0; i < mAvailabilityCallbackInfosByCallbacksByCpuset.numMaps(); i++) { + int cpuset = mAvailabilityCallbackInfosByCallbacksByCpuset.keyAt(i); + CpuAvailabilityCallbackInfo callbackInfo = + mAvailabilityCallbackInfosByCallbacksByCpuset.delete(cpuset, callback); + if (callbackInfo != null) { + if (DEBUG) { + Slogf.d(TAG, "Successfully removed %s", callbackInfo); + } + checkAndStopMonitoringLocked(); + return; + } } + Slogf.w(TAG, "CpuAvailabilityCallback was not previously added. Ignoring the remove" + + " request"); } - // TODO(b/242722241): Increase CPU monitoring interval when all callbacks are removed. } }; public CpuMonitorService(Context context) { + this(context, new CpuInfoReader(), new ServiceThread(TAG, + Process.THREAD_PRIORITY_BACKGROUND, /* allowIo= */ true), + Build.IS_USERDEBUG || Build.IS_ENG, NORMAL_MONITORING_INTERVAL_MILLISECONDS, + DEBUG_MONITORING_INTERVAL_MILLISECONDS, LATEST_AVAILABILITY_DURATION_MILLISECONDS); + } + + @VisibleForTesting + CpuMonitorService(Context context, CpuInfoReader cpuInfoReader, HandlerThread handlerThread, + boolean shouldDebugMonitor, long normalMonitoringIntervalMillis, + long debugMonitoringIntervalMillis, long latestAvailabilityDurationMillis) { super(context); mContext = context; + mHandlerThread = handlerThread; + mShouldDebugMonitor = shouldDebugMonitor; + mNormalMonitoringIntervalMillis = normalMonitoringIntervalMillis; + mDebugMonitoringIntervalMillis = debugMonitoringIntervalMillis; + mLatestAvailabilityDurationMillis = latestAvailabilityDurationMillis; + mCpuInfoReader = cpuInfoReader; + mCpusetInfosByCpuset = new SparseArray<>(2); + mCpusetInfosByCpuset.append(CPUSET_ALL, new CpusetInfo(CPUSET_ALL)); + mCpusetInfosByCpuset.append(CPUSET_BACKGROUND, new CpusetInfo(CPUSET_BACKGROUND)); + mAvailabilityCallbackInfosByCallbacksByCpuset = new SparseArrayMap<>(); } @Override public void onStart() { + // Initialize CPU info reader and perform the first read to make sure the CPU stats are + // readable without any issues. + if (!mCpuInfoReader.init() || mCpuInfoReader.readCpuInfos() == null) { + Slogf.wtf(TAG, "Failed to initialize CPU info reader. This happens when the CPU " + + "frequency stats are not available or the sysfs interface has changed in " + + "the Kernel. Cannot monitor CPU without these stats. Terminating CPU monitor " + + "service"); + return; + } + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); publishLocalService(CpuMonitorInternal.class, mLocalService); publishBinderService("cpu_monitor", new CpuMonitorBinder(), /* allowIsolated= */ false, DUMP_FLAG_PRIORITY_CRITICAL); + Watchdog.getInstance().addThread(mHandler); + synchronized (mLock) { + if (mShouldDebugMonitor && !mHandler.hasCallbacks(mMonitorCpuStats)) { + mCurrentMonitoringIntervalMillis = mDebugMonitoringIntervalMillis; + Slogf.i(TAG, "Starting debug monitoring"); + mHandler.post(mMonitorCpuStats); + } + } + } + + @VisibleForTesting + long getCurrentMonitoringIntervalMillis() { + synchronized (mLock) { + return mCurrentMonitoringIntervalMillis; + } } private void doDump(IndentingPrintWriter writer) { writer.printf("*%s*\n", getClass().getSimpleName()); writer.increaseIndent(); + mCpuInfoReader.dump(writer); + writer.printf("mShouldDebugMonitor = %s\n", mShouldDebugMonitor ? "Yes" : "No"); + writer.printf("mNormalMonitoringIntervalMillis = %d\n", mNormalMonitoringIntervalMillis); + writer.printf("mDebugMonitoringIntervalMillis = %d\n", mDebugMonitoringIntervalMillis); + writer.printf("mLatestAvailabilityDurationMillis = %d\n", + mLatestAvailabilityDurationMillis); synchronized (mLock) { - writer.printf("CPU monitoring interval: %d ms\n", mMonitoringIntervalMilliseconds); - if (!mCpuAvailabilityCallbackInfoByCallbacks.isEmpty()) { + writer.printf("mCurrentMonitoringIntervalMillis = %d\n", + mCurrentMonitoringIntervalMillis); + if (hasClientCallbacksLocked()) { writer.println("CPU availability change callbacks:"); writer.increaseIndent(); - for (int i = 0; i < mCpuAvailabilityCallbackInfoByCallbacks.size(); i++) { - writer.printf("%s: %s\n", mCpuAvailabilityCallbackInfoByCallbacks.keyAt(i), - mCpuAvailabilityCallbackInfoByCallbacks.valueAt(i)); + mAvailabilityCallbackInfosByCallbacksByCpuset.forEach( + (callbackInfo) -> writer.printf("%s\n", callbackInfo)); + writer.decreaseIndent(); + } + if (mCpusetInfosByCpuset.size() > 0) { + writer.println("Cpuset infos:"); + writer.increaseIndent(); + for (int i = 0; i < mCpusetInfosByCpuset.size(); i++) { + writer.printf("%s\n", mCpusetInfosByCpuset.valueAt(i)); } writer.decreaseIndent(); } } - // TODO(b/242722241): Print the recent past CPU stats. writer.decreaseIndent(); } + private void monitorCpuStats() { + long uptimeMillis = SystemClock.uptimeMillis(); + // Remove duplicate callbacks caused by switching form debug to normal monitoring. + // The removal of the duplicate callback done in the {@link newCallbackInfoLocked} method + // may result in a no-op when a duplicate execution of this callback has already started + // on the handler thread. + mHandler.removeCallbacks(mMonitorCpuStats); + SparseArray<CpuInfoReader.CpuInfo> cpuInfosByCoreId = mCpuInfoReader.readCpuInfos(); + if (cpuInfosByCoreId == null) { + // This shouldn't happen because the CPU infos are read & verified during + // the {@link onStart} call. + Slogf.wtf(TAG, "Failed to read CPU info from device"); + synchronized (mLock) { + stopMonitoringCpuStatsLocked(); + } + // Monitoring is stopped but no client callback is removed. + // TODO(b/267500110): Identify whether the clients should be notified about this state. + return; + } + + synchronized (mLock) { + // 1. Populate the {@link mCpusetInfosByCpuset} with the latest cpuInfo. + for (int i = 0; i < cpuInfosByCoreId.size(); i++) { + CpuInfoReader.CpuInfo cpuInfo = cpuInfosByCoreId.valueAt(i); + for (int j = 0; j < mCpusetInfosByCpuset.size(); j++) { + mCpusetInfosByCpuset.valueAt(j).appendCpuInfo(uptimeMillis, cpuInfo); + } + } + + // 2. Verify whether any monitoring thresholds are crossed and notify the corresponding + // clients. + for (int i = 0; i < mCpusetInfosByCpuset.size(); i++) { + CpusetInfo cpusetInfo = mCpusetInfosByCpuset.valueAt(i); + cpusetInfo.populateLatestCpuAvailabilityInfo(uptimeMillis, + mLatestAvailabilityDurationMillis); + checkClientThresholdsAndNotifyLocked(cpusetInfo); + } + + // TODO(b/267500110): Detect heavy CPU load. On detecting heavy CPU load, increase + // the monitoring interval and notify the clients. + + // 3. Continue monitoring only when either there is at least one registered client + // callback or debug monitoring is enabled. + if (mCurrentMonitoringIntervalMillis > 0 + && (hasClientCallbacksLocked() || mShouldDebugMonitor)) { + mHandler.postAtTime(mMonitorCpuStats, + uptimeMillis + mCurrentMonitoringIntervalMillis); + } else { + stopMonitoringCpuStatsLocked(); + } + } + } + + @GuardedBy("mLock") + private void checkClientThresholdsAndNotifyLocked(CpusetInfo cpusetInfo) { + int prevAvailabilityPercent = cpusetInfo.getPrevCpuAvailabilityPercent(); + CpuAvailabilityInfo latestAvailabilityInfo = cpusetInfo.getLatestCpuAvailabilityInfo(); + if (latestAvailabilityInfo == null || prevAvailabilityPercent < 0 + || mAvailabilityCallbackInfosByCallbacksByCpuset.numElementsForKey( + cpusetInfo.cpuset) == 0) { + // When either the current or the previous CPU availability percents are + // missing, skip the current cpuset as there is not enough data to verify + // whether the CPU availability has crossed any monitoring threshold. + return; + } + for (int i = 0; i < mAvailabilityCallbackInfosByCallbacksByCpuset.numMaps(); i++) { + for (int j = 0; j < mAvailabilityCallbackInfosByCallbacksByCpuset.numElementsForKeyAt( + i); j++) { + CpuAvailabilityCallbackInfo callbackInfo = + mAvailabilityCallbackInfosByCallbacksByCpuset.valueAt(i, j); + if (callbackInfo.config.cpuset != cpusetInfo.cpuset) { + continue; + } + if (didCrossAnyThreshold(prevAvailabilityPercent, + latestAvailabilityInfo.latestAvgAvailabilityPercent, + callbackInfo.config.getThresholds())) { + asyncNotifyCpuAvailabilityToClient(latestAvailabilityInfo, callbackInfo); + } + } + } + } + + private void asyncNotifyMonitoringIntervalChangeToClient( + CpuAvailabilityCallbackInfo callbackInfo) { + if (callbackInfo.executor == null) { + mHandler.post(callbackInfo.notifyMonitoringIntervalChangeRunnable); + } else { + callbackInfo.executor.execute(callbackInfo.notifyMonitoringIntervalChangeRunnable); + } + } + + private void asyncNotifyCpuAvailabilityToClient(CpuAvailabilityInfo availabilityInfo, + CpuAvailabilityCallbackInfo callbackInfo) { + callbackInfo.notifyCpuAvailabilityChangeRunnable.prepare(availabilityInfo); + if (callbackInfo.executor == null) { + mHandler.post(callbackInfo.notifyCpuAvailabilityChangeRunnable); + } else { + callbackInfo.executor.execute(callbackInfo.notifyCpuAvailabilityChangeRunnable); + } + } + + @GuardedBy("mLock") + private CpuAvailabilityCallbackInfo newCallbackInfoLocked( + CpuAvailabilityMonitoringConfig config, + CpuMonitorInternal.CpuAvailabilityCallback callback, Executor executor) { + CpuAvailabilityCallbackInfo callbackInfo = new CpuAvailabilityCallbackInfo(this, config, + callback, executor); + String cpusetStr = CpuAvailabilityMonitoringConfig.toCpusetString( + callbackInfo.config.cpuset); + CpusetInfo cpusetInfo = mCpusetInfosByCpuset.get(callbackInfo.config.cpuset); + Preconditions.checkState(cpusetInfo != null, "Missing cpuset info for cpuset %s", + cpusetStr); + boolean hasExistingClientCallbacks = hasClientCallbacksLocked(); + mAvailabilityCallbackInfosByCallbacksByCpuset.add(callbackInfo.config.cpuset, + callbackInfo.callback, callbackInfo); + if (DEBUG) { + Slogf.d(TAG, "Added a CPU availability callback: %s", callbackInfo); + } + CpuAvailabilityInfo latestInfo = cpusetInfo.getLatestCpuAvailabilityInfo(); + if (latestInfo != null) { + asyncNotifyCpuAvailabilityToClient(latestInfo, callbackInfo); + } + if (hasExistingClientCallbacks && mHandler.hasCallbacks(mMonitorCpuStats)) { + return callbackInfo; + } + // Remove existing callbacks to ensure any debug monitoring (if started) is stopped before + // starting normal monitoring. + mHandler.removeCallbacks(mMonitorCpuStats); + mCurrentMonitoringIntervalMillis = mNormalMonitoringIntervalMillis; + mHandler.post(mMonitorCpuStats); + return callbackInfo; + } + + @GuardedBy("mLock") + private void checkAndStopMonitoringLocked() { + if (hasClientCallbacksLocked()) { + return; + } + if (mShouldDebugMonitor) { + if (DEBUG) { + Slogf.e(TAG, "Switching to debug monitoring"); + } + mCurrentMonitoringIntervalMillis = mDebugMonitoringIntervalMillis; + } else { + stopMonitoringCpuStatsLocked(); + } + } + + @GuardedBy("mLock") + private boolean hasClientCallbacksLocked() { + for (int i = 0; i < mAvailabilityCallbackInfosByCallbacksByCpuset.numMaps(); i++) { + if (mAvailabilityCallbackInfosByCallbacksByCpuset.numElementsForKeyAt(i) > 0) { + return true; + } + } + return false; + } + + @GuardedBy("mLock") + private void stopMonitoringCpuStatsLocked() { + mHandler.removeCallbacks(mMonitorCpuStats); + mCurrentMonitoringIntervalMillis = DEFAULT_MONITORING_INTERVAL_MILLISECONDS; + // When the monitoring is stopped, the latest CPU availability info and the snapshots in + // {@code mCpusetInfosByCpuset} will become obsolete soon. So, remove them. + for (int i = 0; i < mCpusetInfosByCpuset.size(); i++) { + mCpusetInfosByCpuset.valueAt(i).clear(); + } + } + + private static boolean containsCpuset(@CpuInfoReader.CpusetCategory int cpusetCategories, + @CpuAvailabilityMonitoringConfig.Cpuset int expectedCpuset) { + switch (expectedCpuset) { + case CPUSET_ALL: + return (cpusetCategories & FLAG_CPUSET_CATEGORY_TOP_APP) != 0; + case CPUSET_BACKGROUND: + return (cpusetCategories & FLAG_CPUSET_CATEGORY_BACKGROUND) != 0; + default: + Slogf.wtf(TAG, "Provided invalid expectedCpuset %d", expectedCpuset); + } + return false; + } + + private static boolean didCrossAnyThreshold(int prevAvailabilityPercent, + int curAvailabilityPercent, IntArray thresholds) { + if (prevAvailabilityPercent == curAvailabilityPercent) { + return false; + } + for (int i = 0; i < thresholds.size(); i++) { + int threshold = thresholds.get(i); + // TODO(b/267500110): Identify whether or not the clients need to be notified when + // the CPU availability jumps too frequently around the provided thresholds. + // A. Should the client be notified twice - once when the availability reaches + // the threshold and once when it moves away (increase/decrease) from the threshold + // immediately? + // B. Should there be some sort of rate-limiting to avoid notifying the client too + // frequently? Should the client be able to config the rate-limit? + if (prevAvailabilityPercent < threshold && curAvailabilityPercent >= threshold) { + return true; + } + if (prevAvailabilityPercent >= threshold && curAvailabilityPercent < threshold) { + return true; + } + } + return false; + } + private static final class CpuAvailabilityCallbackInfo { + public final CpuMonitorService service; public final CpuAvailabilityMonitoringConfig config; + public final CpuMonitorInternal.CpuAvailabilityCallback callback; + @Nullable public final Executor executor; + public final Runnable notifyMonitoringIntervalChangeRunnable = new Runnable() { + @Override + public void run() { + callback.onMonitoringIntervalChanged(service.getCurrentMonitoringIntervalMillis()); + } + }; + public final NotifyCpuAvailabilityChangeRunnable notifyCpuAvailabilityChangeRunnable = + new NotifyCpuAvailabilityChangeRunnable(); - CpuAvailabilityCallbackInfo(CpuAvailabilityMonitoringConfig config, - Executor executor) { + CpuAvailabilityCallbackInfo(CpuMonitorService service, + CpuAvailabilityMonitoringConfig config, + CpuMonitorInternal.CpuAvailabilityCallback callback, @Nullable Executor executor) { + this.service = service; this.config = config; + this.callback = callback; this.executor = executor; } @Override public String toString() { - return "CpuAvailabilityCallbackInfo{" + "config=" + config + ", mExecutor=" + executor - + '}'; + return "CpuAvailabilityCallbackInfo{config = " + config + ", callback = " + callback + + ", mExecutor = " + executor + '}'; + } + + private final class NotifyCpuAvailabilityChangeRunnable implements Runnable { + private final Object mLock = new Object(); + @GuardedBy("mLock") + private CpuAvailabilityInfo mCpuAvailabilityInfo; + + public void prepare(CpuAvailabilityInfo cpuAvailabilityInfo) { + synchronized (mLock) { + mCpuAvailabilityInfo = cpuAvailabilityInfo; + } + } + + @Override + public void run() { + synchronized (mLock) { + callback.onAvailabilityChanged(mCpuAvailabilityInfo); + } + } } } @@ -170,4 +515,157 @@ public final class CpuMonitorService extends SystemService { PriorityDump.dump(mPriorityDumper, fd, pw, args); } } + + private static final class CpusetInfo { + @CpuAvailabilityMonitoringConfig.Cpuset + public final int cpuset; + private final LongSparseArray<Snapshot> mSnapshotsByUptime; + @Nullable + private CpuAvailabilityInfo mLatestCpuAvailabilityInfo; + + CpusetInfo(int cpuset) { + this.cpuset = cpuset; + mSnapshotsByUptime = new LongSparseArray<>(); + } + + public void appendCpuInfo(long uptimeMillis, CpuInfoReader.CpuInfo cpuInfo) { + if (!containsCpuset(cpuInfo.cpusetCategories, cpuset)) { + return; + } + Snapshot currentSnapshot = mSnapshotsByUptime.get(uptimeMillis); + if (currentSnapshot == null) { + currentSnapshot = new Snapshot(uptimeMillis); + mSnapshotsByUptime.append(uptimeMillis, currentSnapshot); + if (mSnapshotsByUptime.size() > 0 + && (uptimeMillis - mSnapshotsByUptime.valueAt(0).uptimeMillis) + > CACHE_DURATION_MILLISECONDS) { + mSnapshotsByUptime.removeAt(0); + } + } + currentSnapshot.appendCpuInfo(cpuInfo); + } + + @Nullable + public CpuAvailabilityInfo getLatestCpuAvailabilityInfo() { + return mLatestCpuAvailabilityInfo; + } + + public void populateLatestCpuAvailabilityInfo(long currentUptimeMillis, + long latestAvailabilityDurationMillis) { + int numSnapshots = mSnapshotsByUptime.size(); + if (numSnapshots == 0) { + mLatestCpuAvailabilityInfo = null; + return; + } + Snapshot latestSnapshot = mSnapshotsByUptime.valueAt(numSnapshots - 1); + if (latestSnapshot.uptimeMillis != currentUptimeMillis) { + // When the cpuset has no stats available for the current polling, the uptime will + // mismatch. When this happens, return {@code null} to avoid returning stale + // information. + if (DEBUG) { + Slogf.d(TAG, "Skipping stale CPU availability information for cpuset %s", + CpuAvailabilityMonitoringConfig.toCpusetString(cpuset)); + } + mLatestCpuAvailabilityInfo = null; + return; + } + // Avoid constructing {@link mLatestCpuAvailabilityInfo} if the uptime hasn't changed. + if (mLatestCpuAvailabilityInfo != null + && mLatestCpuAvailabilityInfo.dataTimestampUptimeMillis + == latestSnapshot.uptimeMillis) { + return; + } + long earliestUptimeMillis = currentUptimeMillis - latestAvailabilityDurationMillis; + mLatestCpuAvailabilityInfo = new CpuAvailabilityInfo(cpuset, + latestSnapshot.uptimeMillis, latestSnapshot.getAverageAvailableCpuFreqPercent(), + getCumulativeAvgAvailabilityPercent(earliestUptimeMillis), + latestAvailabilityDurationMillis); + } + + public int getPrevCpuAvailabilityPercent() { + int numSnapshots = mSnapshotsByUptime.size(); + if (numSnapshots < 2) { + return -1; + } + return mSnapshotsByUptime.valueAt(numSnapshots - 2).getAverageAvailableCpuFreqPercent(); + } + + private int getCumulativeAvgAvailabilityPercent(long earliestUptimeMillis) { + long totalAvailableCpuFreqKHz = 0; + long totalOnlineMaxCpuFreqKHz = 0; + int totalAccountedSnapshots = 0; + long earliestSeenUptimeMillis = Long.MAX_VALUE; + for (int i = mSnapshotsByUptime.size() - 1; i >= 0; i--) { + Snapshot snapshot = mSnapshotsByUptime.valueAt(i); + earliestSeenUptimeMillis = snapshot.uptimeMillis; + if (snapshot.uptimeMillis <= earliestUptimeMillis) { + break; + } + totalAccountedSnapshots++; + totalAvailableCpuFreqKHz += snapshot.totalNormalizedAvailableCpuFreqKHz; + totalOnlineMaxCpuFreqKHz += snapshot.totalOnlineMaxCpuFreqKHz; + } + // The cache must have at least 2 snapshots within the given duration and + // the {@link earliestSeenUptimeMillis} must be earlier than (i,e., less than) the given + // {@link earliestUptimeMillis}. Otherwise, the cache doesn't have enough data to + // calculate the cumulative average for the given duration. + // TODO(b/267500110): Investigate whether the cumulative average duration should be + // shrunk when not enough data points are available. + if (earliestSeenUptimeMillis > earliestUptimeMillis || totalAccountedSnapshots < 2) { + return CpuAvailabilityInfo.MISSING_CPU_AVAILABILITY_PERCENT; + } + return (int) ((totalAvailableCpuFreqKHz * 100.0) / totalOnlineMaxCpuFreqKHz); + } + + public void clear() { + mLatestCpuAvailabilityInfo = null; + mSnapshotsByUptime.clear(); + } + + @Override + public String toString() { + return "CpusetInfo{cpuset = " + CpuAvailabilityMonitoringConfig.toCpusetString(cpuset) + + ", mSnapshotsByUptime = " + mSnapshotsByUptime + + ", mLatestCpuAvailabilityInfo = " + mLatestCpuAvailabilityInfo + '}'; + } + + private static final class Snapshot { + public final long uptimeMillis; + public int totalOnlineCpus; + public int totalOfflineCpus; + public long totalNormalizedAvailableCpuFreqKHz; + public long totalOnlineMaxCpuFreqKHz; + public long totalOfflineMaxCpuFreqKHz; + + Snapshot(long uptimeMillis) { + this.uptimeMillis = uptimeMillis; + } + + public void appendCpuInfo(CpuInfoReader.CpuInfo cpuInfo) { + if (!cpuInfo.isOnline) { + totalOfflineCpus++; + totalOfflineMaxCpuFreqKHz += cpuInfo.maxCpuFreqKHz; + return; + } + ++totalOnlineCpus; + totalNormalizedAvailableCpuFreqKHz += cpuInfo.getNormalizedAvailableCpuFreqKHz(); + totalOnlineMaxCpuFreqKHz += cpuInfo.maxCpuFreqKHz; + } + + public int getAverageAvailableCpuFreqPercent() { + return (int) ((totalNormalizedAvailableCpuFreqKHz * 100.0) + / totalOnlineMaxCpuFreqKHz); + } + + @Override + public String toString() { + return "Snapshot{uptimeMillis = " + uptimeMillis + ", totalOnlineCpus = " + + totalOnlineCpus + ", totalOfflineCpus = " + totalOfflineCpus + + ", totalNormalizedAvailableCpuFreqKHz = " + + totalNormalizedAvailableCpuFreqKHz + + ", totalOnlineMaxCpuFreqKHz = " + totalOnlineMaxCpuFreqKHz + + ", totalOfflineMaxCpuFreqKHz = " + totalOfflineMaxCpuFreqKHz + '}'; + } + } + } } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index fdc2affda8e7..741464072fd8 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -4737,9 +4737,9 @@ public class UserManagerService extends IUserManager.Stub { UserManager.USER_OPERATION_ERROR_MAX_USERS); } // Keep logic in sync with getRemainingCreatableUserCount() - if (!isGuest && !isProfile && !isDemo && isUserLimitReached()) { + if (!isGuest && !isManagedProfile && !isDemo && isUserLimitReached()) { // If the user limit has been reached, we cannot add a user (except guest/demo). - // Note that profiles can bypass it in certain circumstances (taken + // Note that managed profiles can bypass it in certain circumstances (taken // into account in the profile check below). throwCheckedUserOperationException( "Cannot add user. Maximum user limit is reached.", diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 321924c79c58..746672968da5 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -3579,6 +3579,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { mInjector.binderWithCleanCallingIdentity(() -> mInjector.getPackageManagerInternal().setOwnerProtectedPackages( targetUserId, protectedPackages)); + mUsageStatsManagerInternal.setAdminProtectedPackages(new ArraySet(protectedPackages), + targetUserId); } void handleUnlockUser(int userId) { @@ -11777,6 +11779,20 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final CallerIdentity caller = getCallerIdentity(); Preconditions.checkCallAuthorization(canManageUsers(caller) || canQueryAdminPolicy(caller)); + // Move AccessibilityManager out of lock to prevent potential deadlock + final List<AccessibilityServiceInfo> installedServices; + long id = mInjector.binderClearCallingIdentity(); + try { + UserInfo user = getUserInfo(userId); + if (user.isManagedProfile()) { + userId = user.profileGroupId; + } + installedServices = withAccessibilityManager(userId, + AccessibilityManager::getInstalledAccessibilityServiceList); + } finally { + mInjector.binderRestoreCallingIdentity(id); + } + synchronized (getLockObject()) { List<String> result = null; // If we have multiple profiles we return the intersection of the @@ -11803,27 +11819,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { // If we have a permitted list add all system accessibility services. if (result != null) { - long id = mInjector.binderClearCallingIdentity(); - try { - UserInfo user = getUserInfo(userId); - if (user.isManagedProfile()) { - userId = user.profileGroupId; - } - final List<AccessibilityServiceInfo> installedServices = - withAccessibilityManager(userId, - AccessibilityManager::getInstalledAccessibilityServiceList); - - if (installedServices != null) { - for (AccessibilityServiceInfo service : installedServices) { - ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo; - ApplicationInfo applicationInfo = serviceInfo.applicationInfo; - if ((applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { - result.add(serviceInfo.packageName); - } + if (installedServices != null) { + for (AccessibilityServiceInfo service : installedServices) { + ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo; + ApplicationInfo applicationInfo = serviceInfo.applicationInfo; + if ((applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + result.add(serviceInfo.packageName); } } - } finally { - mInjector.binderRestoreCallingIdentity(id); } } diff --git a/services/tests/mockingservicestests/src/com/android/server/cpu/CpuInfoReaderTest.java b/services/tests/mockingservicestests/src/com/android/server/cpu/CpuInfoReaderTest.java index b21478753301..04f6f8b2e9f0 100644 --- a/services/tests/mockingservicestests/src/com/android/server/cpu/CpuInfoReaderTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/cpu/CpuInfoReaderTest.java @@ -92,6 +92,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(0, new CpuInfoReader.CpuInfo(/* cpuCore= */ 0, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_230_000, /* maxCpuFreqKHz= */ 2_500_000, /* avgTimeInStateCpuFreqKHz= */ 488_095, + /* normalizedAvailableCpuFreqKHz= */ 2_402_267, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 32_249_610, /* niceTimeMillis= */ 7_950_930, /* systemTimeMillis= */ 52_227_050, /* idleTimeMillis= */ 409_036_950, /* iowaitTimeMillis= */ 1_322_810, @@ -101,6 +102,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_450_000, /* maxCpuFreqKHz= */ 2_800_000, /* avgTimeInStateCpuFreqKHz= */ 502_380, + /* normalizedAvailableCpuFreqKHz= */ 2_693_525, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_949_280, /* niceTimeMillis= */ 7_799_450, /* systemTimeMillis= */ 54_004_020, /* idleTimeMillis= */ 402_707_120, /* iowaitTimeMillis= */ 1_186_960, @@ -111,6 +113,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000, /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ 464_285, + /* normalizedAvailableCpuFreqKHz= */ 1_901_608, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_959_280, /* niceTimeMillis= */ 7_789_450, /* systemTimeMillis= */ 54_014_020, /* idleTimeMillis= */ 402_717_120, /* iowaitTimeMillis= */ 1_166_960, @@ -121,6 +124,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000, /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ 464_285, + /* normalizedAvailableCpuFreqKHz= */ 1_907_125, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 32_349_610, /* niceTimeMillis= */ 7_850_930, /* systemTimeMillis= */ 52_127_050, /* idleTimeMillis= */ 409_136_950, /* iowaitTimeMillis= */ 1_332_810, @@ -139,6 +143,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(0, new CpuInfoReader.CpuInfo(/* cpuCore= */ 0, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000, /* maxCpuFreqKHz= */ 2_500_000, /* avgTimeInStateCpuFreqKHz= */ 419_354, + /* normalizedAvailableCpuFreqKHz= */ 2_425_919, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 10_000_000, /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 10_000_000, /* idleTimeMillis= */ 110_000_000, /* iowaitTimeMillis= */ 1_100_000, @@ -148,6 +153,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 2_800_000, /* maxCpuFreqKHz= */ 2_800_000, /* avgTimeInStateCpuFreqKHz= */ 429_032, + /* normalizedAvailableCpuFreqKHz= */ 2_403_009, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 900_000, /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 10_000_000, /* idleTimeMillis= */ 1_000_000, /* iowaitTimeMillis= */ 90_000, @@ -158,6 +164,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, /* isOnline= */ true, /* curCpuFreqKHz= */ 2_000_000, /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ 403_225, + /* normalizedAvailableCpuFreqKHz= */ 1_688_209, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 10_000_000, /* niceTimeMillis= */ 2_000_000, /* systemTimeMillis= */ 0, /* idleTimeMillis= */ 10_000_000, /* iowaitTimeMillis= */ 1_000_000, @@ -168,6 +175,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, /* isOnline= */ false, /* curCpuFreqKHz= */ MISSING_FREQUENCY, /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ MISSING_FREQUENCY, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 2_000_000, /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 1_000_000, /* idleTimeMillis= */ 100_000, /* iowaitTimeMillis= */ 100_000, @@ -189,6 +197,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(0, new CpuInfoReader.CpuInfo(/* cpuCore= */ 0, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_230_000, /* maxCpuFreqKHz= */ 2_500_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ 2_253_713, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 32_249_610, /* niceTimeMillis= */ 7_950_930, /* systemTimeMillis= */ 52_227_050, /* idleTimeMillis= */ 409_036_950, /* iowaitTimeMillis= */ 1_322_810, @@ -198,6 +207,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_450_000, /* maxCpuFreqKHz= */ 2_800_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ 2_492_687, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_949_280, /* niceTimeMillis= */ 7_799_450, /* systemTimeMillis= */ 54_004_020, /* idleTimeMillis= */ 402_707_120, /* iowaitTimeMillis= */ 1_186_960, @@ -208,6 +218,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000, /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ 1_788_079, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_959_280, /* niceTimeMillis= */ 7_789_450, /* systemTimeMillis= */ 54_014_020, /* idleTimeMillis= */ 402_717_120, /* iowaitTimeMillis= */ 1_166_960, @@ -218,6 +229,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000, /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ 1_799_962, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 32_349_610, /* niceTimeMillis= */ 7_850_930, /* systemTimeMillis= */ 52_127_050, /* idleTimeMillis= */ 409_136_950, /* iowaitTimeMillis= */ 1_332_810, @@ -237,6 +249,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(0, new CpuInfoReader.CpuInfo(/* cpuCore= */ 0, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000, /* maxCpuFreqKHz= */ 2_500_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ 2323347, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 10_000_000, /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 10_000_000, /* idleTimeMillis= */ 110_000_000, /* iowaitTimeMillis= */ 1_100_000, @@ -246,6 +259,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 2_800_000, /* maxCpuFreqKHz= */ 2_800_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ 209111, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 900_000, /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 10_000_000, /* idleTimeMillis= */ 1_000_000, /* iowaitTimeMillis= */ 90_000, @@ -256,6 +270,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, /* isOnline= */ true, /* curCpuFreqKHz= */ 2_000_000, /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ 453514, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 10_000_000, /* niceTimeMillis= */ 2_000_000, /* systemTimeMillis= */ 0, /* idleTimeMillis= */ 10_000_000, /* iowaitTimeMillis= */ 1_000_000, @@ -266,6 +281,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, /* isOnline= */ true, /* curCpuFreqKHz= */ 2_000_000, /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + /* normalizedAvailableCpuFreqKHz= */ 37728, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 2_000_000, /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 1_000_000, /* idleTimeMillis= */ 100_000, /* iowaitTimeMillis= */ 100_000, @@ -323,38 +339,8 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { SparseArray<CpuInfoReader.CpuInfo> actualCpuInfos = cpuInfoReader.readCpuInfos(); SparseArray<CpuInfoReader.CpuInfo> expectedCpuInfos = new SparseArray<>(); - expectedCpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1, - FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 3_000_000, - /* maxCpuFreqKHz= */ 1_000_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, - new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_949_280, - /* niceTimeMillis= */ 7_799_450, /* systemTimeMillis= */ 54_004_020, - /* idleTimeMillis= */ 402_707_120, /* iowaitTimeMillis= */ 1_186_960, - /* irqTimeMillis= */ 14_786_940, /* softirqTimeMillis= */ 1_498_130, - /* stealTimeMillis= */ 78_780, /* guestTimeMillis= */ 0, - /* guestNiceTimeMillis= */ 0))); - expectedCpuInfos.append(2, new CpuInfoReader.CpuInfo(/* cpuCore= */ 2, - FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, - /* isOnline= */ true, /* curCpuFreqKHz= */ 9, /* maxCpuFreqKHz= */ 2, - /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, - new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_959_280, - /* niceTimeMillis= */ 7_789_450, /* systemTimeMillis= */ 54_014_020, - /* idleTimeMillis= */ 402_717_120, /* iowaitTimeMillis= */ 1_166_960, - /* irqTimeMillis= */ 14_796_940, /* softirqTimeMillis= */ 1_478_130, - /* stealTimeMillis= */ 88_780, /* guestTimeMillis= */ 0, - /* guestNiceTimeMillis= */ 0))); - expectedCpuInfos.append(3, new CpuInfoReader.CpuInfo(/* cpuCore= */ 3, - FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND, - /* isOnline= */ true, /* curCpuFreqKHz= */ 9, /* maxCpuFreqKHz= */ 2, - /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, - new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 32_349_610, - /* niceTimeMillis= */ 7_850_930, /* systemTimeMillis= */ 52_127_050, - /* idleTimeMillis= */ 409_136_950, /* iowaitTimeMillis= */ 1_332_810, - /* irqTimeMillis= */ 8_136_740, /* softirqTimeMillis= */ 438_970, - /* stealTimeMillis= */ 71_950, /* guestTimeMillis= */ 0, - /* guestNiceTimeMillis= */ 0))); - compareCpuInfos("CPU infos with corrupted CPU frequency", expectedCpuInfos, - actualCpuInfos); + compareCpuInfos("CPU infos with corrupted CPU frequency", expectedCpuInfos, actualCpuInfos); } @Test @@ -368,6 +354,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(0, new CpuInfoReader.CpuInfo(/* cpuCore= */ 0, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_230_000, /* maxCpuFreqKHz= */ 2_500_000, /* avgTimeInStateCpuFreqKHz= */ 488_095, + /* normalizedAvailableCpuFreqKHz= */ 2_402_267, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 32_249_610, /* niceTimeMillis= */ 7_950_930, /* systemTimeMillis= */ 52_227_050, /* idleTimeMillis= */ 409_036_950, /* iowaitTimeMillis= */ 1_322_810, @@ -377,6 +364,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { expectedCpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1, FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_450_000, /* maxCpuFreqKHz= */ 2_800_000, /* avgTimeInStateCpuFreqKHz= */ 502_380, + /* normalizedAvailableCpuFreqKHz= */ 2_693_525, new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_949_280, /* niceTimeMillis= */ 7_799_450, /* systemTimeMillis= */ 54_004_020, /* idleTimeMillis= */ 402_707_120, /* iowaitTimeMillis= */ 1_186_960, @@ -393,7 +381,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { assertWithMessage("Make empty dir %s", emptyDir).that(emptyDir.mkdir()).isTrue(); CpuInfoReader cpuInfoReader = new CpuInfoReader(emptyDir, getCacheFile( VALID_CPUFREQ_WITH_TIME_IN_STATE_DIR), - getCacheFile(VALID_PROC_STAT)); + getCacheFile(VALID_PROC_STAT), /* minReadIntervalMillis= */0); assertWithMessage("Init CPU reader info").that(cpuInfoReader.init()).isFalse(); @@ -406,7 +394,7 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { File emptyDir = getCacheFile(EMPTY_DIR); assertWithMessage("Make empty dir %s", emptyDir).that(emptyDir.mkdir()).isTrue(); CpuInfoReader cpuInfoReader = new CpuInfoReader(getCacheFile(VALID_CPUSET_DIR), emptyDir, - getCacheFile(VALID_PROC_STAT)); + getCacheFile(VALID_PROC_STAT), /* minReadIntervalMillis= */0); assertWithMessage("Init CPU reader info").that(cpuInfoReader.init()).isFalse(); @@ -420,12 +408,32 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { assertWithMessage("Create empty file %s", emptyFile).that(emptyFile.createNewFile()) .isTrue(); CpuInfoReader cpuInfoReader = new CpuInfoReader(getCacheFile(VALID_CPUSET_DIR), - getCacheFile(VALID_CPUFREQ_WITH_TIME_IN_STATE_DIR), getCacheFile(EMPTY_FILE)); + getCacheFile(VALID_CPUFREQ_WITH_TIME_IN_STATE_DIR), getCacheFile(EMPTY_FILE), + /* minReadIntervalMillis= */0); assertWithMessage("Cpu infos with empty proc stat").that(cpuInfoReader.readCpuInfos()) .isNull(); } + @Test + public void testReadingTooFrequentlyReturnsLastReadCpuInfos() throws Exception { + CpuInfoReader cpuInfoReader = new CpuInfoReader(getCacheFile(VALID_CPUSET_DIR), + getCacheFile(VALID_CPUFREQ_WITH_TIME_IN_STATE_DIR), getCacheFile(VALID_PROC_STAT), + /* minReadIntervalMillis= */ 60_000); + assertWithMessage("Initialize CPU info reader").that(cpuInfoReader.init()).isTrue(); + + SparseArray<CpuInfoReader.CpuInfo> firstCpuInfos = cpuInfoReader.readCpuInfos(); + assertWithMessage("CPU infos first snapshot").that(firstCpuInfos).isNotNull(); + assertWithMessage("CPU infos first snapshot size").that(firstCpuInfos.size()) + .isGreaterThan(0); + + SparseArray<CpuInfoReader.CpuInfo> secondCpuInfos = cpuInfoReader.readCpuInfos(); + compareCpuInfos("CPU infos second snapshot", firstCpuInfos, secondCpuInfos); + + SparseArray<CpuInfoReader.CpuInfo> thirdCpuInfos = cpuInfoReader.readCpuInfos(); + compareCpuInfos("CPU infos third snapshot", firstCpuInfos, thirdCpuInfos); + } + private void compareCpuInfos(String message, SparseArray<CpuInfoReader.CpuInfo> expected, SparseArray<CpuInfoReader.CpuInfo> actual) { @@ -462,7 +470,8 @@ public final class CpuInfoReaderTest extends ExpectableTestCase { private static CpuInfoReader newCpuInfoReader(File cpusetDir, File cpuFreqDir, File procStatFile) { - CpuInfoReader cpuInfoReader = new CpuInfoReader(cpusetDir, cpuFreqDir, procStatFile); + CpuInfoReader cpuInfoReader = new CpuInfoReader(cpusetDir, cpuFreqDir, procStatFile, + /* minReadIntervalMillis= */ 0); assertWithMessage("Initialize CPU info reader").that(cpuInfoReader.init()).isTrue(); return cpuInfoReader; } diff --git a/services/tests/mockingservicestests/src/com/android/server/cpu/CpuMonitorServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/cpu/CpuMonitorServiceTest.java index 49a2cc696744..5a5f5256e37d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/cpu/CpuMonitorServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/cpu/CpuMonitorServiceTest.java @@ -17,105 +17,659 @@ package com.android.server.cpu; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.server.cpu.CpuAvailabilityInfo.MISSING_CPU_AVAILABILITY_PERCENT; import static com.android.server.cpu.CpuAvailabilityMonitoringConfig.CPUSET_ALL; +import static com.android.server.cpu.CpuAvailabilityMonitoringConfig.CPUSET_BACKGROUND; +import static com.android.server.cpu.CpuInfoReader.CpuInfo.MISSING_FREQUENCY; +import static com.android.server.cpu.CpuInfoReader.FLAG_CPUSET_CATEGORY_BACKGROUND; +import static com.android.server.cpu.CpuInfoReader.FLAG_CPUSET_CATEGORY_TOP_APP; +import static com.android.server.cpu.CpuMonitorService.DEFAULT_MONITORING_INTERVAL_MILLISECONDS; + +import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.when; import android.content.Context; import android.os.Binder; import android.os.Handler; import android.os.HandlerExecutor; +import android.os.HandlerThread; import android.os.Looper; import android.os.ServiceManager; +import android.util.ArraySet; +import android.util.SparseArray; import com.android.server.ExtendedMockitoRule; import com.android.server.LocalServices; +import com.android.server.Watchdog; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.stubbing.OngoingStubbing; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; public final class CpuMonitorServiceTest { - private static final CpuAvailabilityMonitoringConfig TEST_CPU_AVAILABILITY_MONITORING_CONFIG = + private static final String TAG = CpuMonitorServiceTest.class.getSimpleName(); + private static final String USER_BUILD_TAG = TAG + "UserBuild"; + private static final long ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS = + TimeUnit.SECONDS.toMillis(1); + private static final long HANDLER_THREAD_SYNC_TIMEOUT_MILLISECONDS = + TimeUnit.SECONDS.toMillis(5); + private static final long TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS = 100; + private static final long TEST_DEBUG_MONITORING_INTERVAL_MILLISECONDS = 150; + private static final long TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS = 300; + private static final CpuAvailabilityMonitoringConfig TEST_MONITORING_CONFIG_ALL_CPUSET = new CpuAvailabilityMonitoringConfig.Builder(CPUSET_ALL) .addThreshold(30).addThreshold(70).build(); + private static final CpuAvailabilityMonitoringConfig TEST_MONITORING_CONFIG_BG_CPUSET = + new CpuAvailabilityMonitoringConfig.Builder(CPUSET_BACKGROUND) + .addThreshold(50).addThreshold(90).build(); + private static final List<StaticCpuInfo> STATIC_CPU_INFOS = List.of( + new StaticCpuInfo(/* cpuCore= */ 0, + /* cpusetCategories= */ FLAG_CPUSET_CATEGORY_TOP_APP, + /* maxCpuFreqKHz= */ 4000), + new StaticCpuInfo(/* cpuCore= */ 1, + /* cpusetCategories= */ FLAG_CPUSET_CATEGORY_TOP_APP, + /* maxCpuFreqKHz= */ 3000), + new StaticCpuInfo(/* cpuCore= */ 2, /* cpusetCategories= */ FLAG_CPUSET_CATEGORY_TOP_APP + | FLAG_CPUSET_CATEGORY_BACKGROUND, /* maxCpuFreqKHz= */ 3000), + new StaticCpuInfo(/* cpuCore= */ 3, /* cpusetCategories= */ FLAG_CPUSET_CATEGORY_TOP_APP + | FLAG_CPUSET_CATEGORY_BACKGROUND, /* maxCpuFreqKHz= */ 3000), + new StaticCpuInfo(/* cpuCore= */ 4, /* cpusetCategories= */ FLAG_CPUSET_CATEGORY_TOP_APP + | FLAG_CPUSET_CATEGORY_BACKGROUND, /* maxCpuFreqKHz= */ 2000)); + private static final ArraySet<Integer> NO_OFFLINE_CORES = new ArraySet<>(); - private static final CpuAvailabilityMonitoringConfig TEST_CPU_AVAILABILITY_MONITORING_CONFIG_2 = - new CpuAvailabilityMonitoringConfig.Builder(CPUSET_ALL) - .addThreshold(10).addThreshold(90).build(); - @Mock - private Context mContext; + private Context mMockContext; + @Mock + private CpuInfoReader mMockCpuInfoReader; + @Captor + private ArgumentCaptor<CpuAvailabilityInfo> mCpuAvailabilityInfoCaptor; + private HandlerThread mServiceHandlerThread; + private Handler mServiceHandler; private CpuMonitorService mService; - private HandlerExecutor mHandlerExecutor; private CpuMonitorInternal mLocalService; @Rule public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this) .mockStatic(ServiceManager.class) + .mockStatic(Watchdog.class) .build(); @Before - public void setUp() { - mService = new CpuMonitorService(mContext); - mHandlerExecutor = new HandlerExecutor(new Handler(Looper.getMainLooper())); + public void setUp() throws Exception { + mServiceHandlerThread = new HandlerThread(TAG); + mService = new CpuMonitorService(mMockContext, mMockCpuInfoReader, mServiceHandlerThread, + /* shouldDebugMonitor= */ true, TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS, + TEST_DEBUG_MONITORING_INTERVAL_MILLISECONDS, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS); + doNothing().when(() -> ServiceManager.addService(eq("cpu_monitor"), any(Binder.class), anyBoolean(), anyInt())); - mService.onStart(); - mLocalService = LocalServices.getService(CpuMonitorInternal.class); + doReturn(mock(Watchdog.class)).when(Watchdog::getInstance); + when(mMockCpuInfoReader.init()).thenReturn(true); + when(mMockCpuInfoReader.readCpuInfos()).thenReturn(new SparseArray<>()); + + startService(); } @After - public void tearDown() { - // The CpuMonitorInternal.class service is added by the mService.onStart call. - // Remove the service to ensure the setUp procedure can add this service again. - LocalServices.removeServiceForTest(CpuMonitorInternal.class); + public void tearDown() throws Exception { + terminateService(); } @Test - public void testAddRemoveCpuAvailabilityCallback() { + public void testAddRemoveCpuAvailabilityCallbackOnDebugBuild() throws Exception { CpuMonitorInternal.CpuAvailabilityCallback mockCallback = mock( CpuMonitorInternal.CpuAvailabilityCallback.class); - mLocalService.addCpuAvailabilityCallback(mHandlerExecutor, - TEST_CPU_AVAILABILITY_MONITORING_CONFIG, mockCallback); + mLocalService.addCpuAvailabilityCallback(/* executor= */ null, + TEST_MONITORING_CONFIG_ALL_CPUSET, mockCallback); + + assertWithMessage("Monitoring interval after adding a client callback") + .that(mService.getCurrentMonitoringIntervalMillis()) + .isEqualTo(TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS); + + // Monitoring interval changed notification is sent asynchronously from the handler thread. + // So, sync with this thread before verifying the client call. + syncWithHandler(mServiceHandler, /* delayMillis= */ 0); + + verify(mockCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS)) + .onMonitoringIntervalChanged(TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS); - // TODO(b/242722241): Verify that {@link mockCallback.onAvailabilityChanged} and - // {@link mockCallback.onMonitoringIntervalChanged} are called when the callback is added. + verify(mockCallback, never()).onAvailabilityChanged(any()); mLocalService.removeCpuAvailabilityCallback(mockCallback); - } + assertWithMessage("Monitoring interval after removing all client callbacks") + .that(mService.getCurrentMonitoringIntervalMillis()) + .isEqualTo(TEST_DEBUG_MONITORING_INTERVAL_MILLISECONDS); + } @Test - public void testDuplicateAddCpuAvailabilityCallback() { + public void testAddRemoveCpuAvailabilityCallbackOnUserBuild() throws Exception { + // The default service instantiated during test setUp has the debug monitoring enabled. + // But on a user build, debug monitoring is disabled. So, replace the default service with + // an equivalent user build service. + replaceServiceWithUserBuildService(); + CpuMonitorInternal.CpuAvailabilityCallback mockCallback = mock( CpuMonitorInternal.CpuAvailabilityCallback.class); - mLocalService.addCpuAvailabilityCallback(mHandlerExecutor, - TEST_CPU_AVAILABILITY_MONITORING_CONFIG, mockCallback); + mLocalService.addCpuAvailabilityCallback(/* executor= */ null, + TEST_MONITORING_CONFIG_ALL_CPUSET, mockCallback); + + assertWithMessage("Monitoring interval after adding a client callback") + .that(mService.getCurrentMonitoringIntervalMillis()) + .isEqualTo(TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS); - mLocalService.addCpuAvailabilityCallback(mHandlerExecutor, - TEST_CPU_AVAILABILITY_MONITORING_CONFIG_2, mockCallback); + // Monitoring interval changed notification is sent asynchronously from the handler thread. + // So, sync with this thread before verifying the client call. + syncWithHandler(mServiceHandler, /* delayMillis= */ 0); - // TODO(b/242722241): Verify that {@link mockCallback} is called only when CPU availability - // thresholds cross the bounds specified in the - // {@link TEST_CPU_AVAILABILITY_MONITORING_CONFIG_2} config. + verify(mockCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS)) + .onMonitoringIntervalChanged(TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS); + + verify(mockCallback, never()).onAvailabilityChanged(any()); mLocalService.removeCpuAvailabilityCallback(mockCallback); + + assertWithMessage("Monitoring interval after removing all client callbacks") + .that(mService.getCurrentMonitoringIntervalMillis()) + .isEqualTo(DEFAULT_MONITORING_INTERVAL_MILLISECONDS); } @Test - public void testRemoveInvalidCpuAvailabilityCallback() { + public void testRemoveInvalidCpuAvailabilityCallback() throws Exception { CpuMonitorInternal.CpuAvailabilityCallback mockCallback = mock( CpuMonitorInternal.CpuAvailabilityCallback.class); mLocalService.removeCpuAvailabilityCallback(mockCallback); } + + @Test + public void testReceiveCpuAvailabilityCallbackOnAddingFirstCallback() throws Exception { + // Debug monitoring is in progress but the default {@link CpuInfoReader.CpuInfo} returned by + // the {@link CpuInfoReader.readCpuInfos} is empty, so the client won't be notified when + // adding a callback. Inject {@link CpuInfoReader.CpuInfo}, so the client callback is + // notified on adding a callback. + injectCpuInfosAndWait(List.of( + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 10.0f, + NO_OFFLINE_CORES))); + + CpuMonitorInternal.CpuAvailabilityCallback mockCallback = + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_ALL_CPUSET); + + verify(mockCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS)) + .onAvailabilityChanged(mCpuAvailabilityInfoCaptor.capture()); + + List<CpuAvailabilityInfo> actual = mCpuAvailabilityInfoCaptor.getAllValues(); + + List<CpuAvailabilityInfo> expected = List.of( + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(0).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 10, MISSING_CPU_AVAILABILITY_PERCENT, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS)); + + assertWithMessage("CPU availability infos").that(actual).isEqualTo(expected); + } + + @Test + public void testReceiveCpuAvailabilityCallbackOnAddingMultipleCallbacks() throws Exception { + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_BG_CPUSET); + + injectCpuInfosAndWait(List.of( + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 10.0f, + NO_OFFLINE_CORES))); + + CpuMonitorInternal.CpuAvailabilityCallback mockCallback = + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_ALL_CPUSET); + + verify(mockCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS)) + .onAvailabilityChanged(mCpuAvailabilityInfoCaptor.capture()); + + List<CpuAvailabilityInfo> actual = mCpuAvailabilityInfoCaptor.getAllValues(); + + List<CpuAvailabilityInfo> expected = List.of( + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(0).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 10, MISSING_CPU_AVAILABILITY_PERCENT, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS)); + + assertWithMessage("CPU availability infos").that(actual).isEqualTo(expected); + } + + @Test + public void testCrossCpuAvailabilityThresholdsWithSingleCallback() throws Exception { + CpuMonitorInternal.CpuAvailabilityCallback mockCallback = + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_ALL_CPUSET); + + injectCpuInfosAndWait(List.of( + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 10.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 90.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 15.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 30.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 60.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 82.0f, + NO_OFFLINE_CORES))); + + verify(mockCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS).times(4)) + .onAvailabilityChanged(mCpuAvailabilityInfoCaptor.capture()); + + List<CpuAvailabilityInfo> actual = mCpuAvailabilityInfoCaptor.getAllValues(); + + List<CpuAvailabilityInfo> expected = List.of( + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(0).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 90, MISSING_CPU_AVAILABILITY_PERCENT, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(1).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 15, MISSING_CPU_AVAILABILITY_PERCENT, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(2).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 30, + /* pastNMillisAvgAvailabilityPercent= */ 45, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(3).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 82, + /* pastNMillisAvgAvailabilityPercent= */ 57, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS)); + + assertWithMessage("CPU availability infos").that(actual).isEqualTo(expected); + } + + @Test + public void testCrossCpuAvailabilityThresholdsWithMultipleCallbacks() throws Exception { + CpuMonitorInternal.CpuAvailabilityCallback mockAllCpusetCallback = + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_ALL_CPUSET); + + CpuMonitorInternal.CpuAvailabilityCallback mockBgCpusetCallback = + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_BG_CPUSET); + + injectCpuInfosAndWait(List.of( + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 5.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 20.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 30.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 60.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 75.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 90.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 15.0f, + NO_OFFLINE_CORES))); + + verify(mockAllCpusetCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS).times(3)) + .onAvailabilityChanged(mCpuAvailabilityInfoCaptor.capture()); + + List<CpuAvailabilityInfo> actual = mCpuAvailabilityInfoCaptor.getAllValues(); + List<CpuAvailabilityInfo> expected = List.of( + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(0).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 30, MISSING_CPU_AVAILABILITY_PERCENT, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(1).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 75, + /* pastNMillisAvgAvailabilityPercent= */ 55, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(2).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 15, + /* pastNMillisAvgAvailabilityPercent= */ 60, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS)); + + assertWithMessage("CPU availability infos for CPUSET_ALL callback").that(actual) + .isEqualTo(expected); + + ArgumentCaptor<CpuAvailabilityInfo> bgCpusetAvailabilityInfoCaptor = + ArgumentCaptor.forClass(CpuAvailabilityInfo.class); + + verify(mockBgCpusetCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS).times(3)) + .onAvailabilityChanged(bgCpusetAvailabilityInfoCaptor.capture()); + + actual = bgCpusetAvailabilityInfoCaptor.getAllValues(); + expected = List.of( + new CpuAvailabilityInfo(CPUSET_BACKGROUND, actual.get(0).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 60, + /* pastNMillisAvgAvailabilityPercent= */ 36, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_BACKGROUND, actual.get(1).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 90, + /* pastNMillisAvgAvailabilityPercent= */ 75, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_BACKGROUND, actual.get(2).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 15, + /* pastNMillisAvgAvailabilityPercent= */ 60, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS)); + + assertWithMessage("CPU availability infos for CPUSET_BACKGROUND callback").that(actual) + .isEqualTo(expected); + } + + @Test + public void testCrossCpuAvailabilityThresholdsWithOfflineCores() throws Exception { + CpuMonitorInternal.CpuAvailabilityCallback mockAllCpusetCallback = + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_ALL_CPUSET); + + CpuMonitorInternal.CpuAvailabilityCallback mockBgCpusetCallback = + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_BG_CPUSET); + + // Disable one top-app and one all cpuset core. + ArraySet<Integer> offlineCoresA = new ArraySet<>(); + offlineCoresA.add(1); + offlineCoresA.add(3); + + // Disable two all cpuset cores. + ArraySet<Integer> offlineCoresB = new ArraySet<>(); + offlineCoresB.add(2); + offlineCoresB.add(4); + + injectCpuInfosAndWait(List.of( + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 5.0f, offlineCoresA), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 20.0f, offlineCoresB), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 30.0f, offlineCoresA), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 60.0f, offlineCoresB), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 75.0f, offlineCoresA), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 90.0f, offlineCoresB), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 15.0f, + offlineCoresA))); + + verify(mockAllCpusetCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS).times(3)) + .onAvailabilityChanged(mCpuAvailabilityInfoCaptor.capture()); + + List<CpuAvailabilityInfo> actual = mCpuAvailabilityInfoCaptor.getAllValues(); + List<CpuAvailabilityInfo> expected = List.of( + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(0).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 30, MISSING_CPU_AVAILABILITY_PERCENT, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(1).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 75, + /* pastNMillisAvgAvailabilityPercent= */ 55, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(2).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 15, + /* pastNMillisAvgAvailabilityPercent= */ 61, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS)); + + assertWithMessage("CPU availability infos for CPUSET_ALL callback").that(actual) + .isEqualTo(expected); + + ArgumentCaptor<CpuAvailabilityInfo> bgCpusetAvailabilityInfoCaptor = + ArgumentCaptor.forClass(CpuAvailabilityInfo.class); + + verify(mockBgCpusetCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS).times(3)) + .onAvailabilityChanged(bgCpusetAvailabilityInfoCaptor.capture()); + + actual = bgCpusetAvailabilityInfoCaptor.getAllValues(); + expected = List.of( + new CpuAvailabilityInfo(CPUSET_BACKGROUND, actual.get(0).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 60, + /* pastNMillisAvgAvailabilityPercent= */ 35, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_BACKGROUND, actual.get(1).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 90, + /* pastNMillisAvgAvailabilityPercent= */ 75, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_BACKGROUND, actual.get(2).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 15, + /* pastNMillisAvgAvailabilityPercent= */ 55, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS)); + + assertWithMessage("CPU availability infos for CPUSET_BACKGROUND callback").that(actual) + .isEqualTo(expected); + } + + @Test + public void testReceiveCpuAvailabilityCallbacksOnExecutorThread() throws Exception { + Handler testHandler = new Handler(Looper.getMainLooper()); + + assertWithMessage("Test main handler").that(testHandler).isNotNull(); + + HandlerExecutor testExecutor = new HandlerExecutor(testHandler); + + assertWithMessage("Test main executor").that(testExecutor).isNotNull(); + + CpuMonitorInternal.CpuAvailabilityCallback mockCallback = + addCpuAvailabilityCallback(testHandler, testExecutor, + TEST_MONITORING_CONFIG_ALL_CPUSET); + + // CPU monitoring is started on the service handler thread. Sync with this thread before + // proceeding. Otherwise, debug monitoring may consume the injected CPU infos and cause + // the test to be flaky. Because the {@link addCpuAvailabilityCallback} syncs only with + // the passed handler, the test must explicitly sync with the service handler. + syncWithHandler(mServiceHandler, /* delayMillis= */ 0); + + injectCpuInfosAndWait(testHandler, List.of( + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 10.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 90.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 15.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 30.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 60.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 82.0f, + NO_OFFLINE_CORES))); + + verify(mockCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS).times(4)) + .onAvailabilityChanged(mCpuAvailabilityInfoCaptor.capture()); + + List<CpuAvailabilityInfo> actual = mCpuAvailabilityInfoCaptor.getAllValues(); + + List<CpuAvailabilityInfo> expected = List.of( + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(0).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 90, MISSING_CPU_AVAILABILITY_PERCENT, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(1).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 15, MISSING_CPU_AVAILABILITY_PERCENT, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(2).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 30, + /* pastNMillisAvgAvailabilityPercent= */ 45, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_ALL, actual.get(3).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 82, + /* pastNMillisAvgAvailabilityPercent= */ 57, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS)); + + assertWithMessage("CPU availability infos").that(actual).isEqualTo(expected); + } + + @Test + public void testDuplicateAddCpuAvailabilityCallback() throws Exception { + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_ALL_CPUSET); + + CpuMonitorInternal.CpuAvailabilityCallback mockCallback = + addCpuAvailabilityCallback(TEST_MONITORING_CONFIG_BG_CPUSET); + + injectCpuInfosAndWait(List.of( + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 10.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 40.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 60.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 80.0f, + NO_OFFLINE_CORES), + generateCpuInfosForAvailability(/* cpuAvailabilityPercent= */ 95.0f, + NO_OFFLINE_CORES))); + + verify(mockCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS).times(2)) + .onAvailabilityChanged(mCpuAvailabilityInfoCaptor.capture()); + + List<CpuAvailabilityInfo> actual = mCpuAvailabilityInfoCaptor.getAllValues(); + + // Verify that the callback is called for the last added monitoring config. + List<CpuAvailabilityInfo> expected = List.of( + new CpuAvailabilityInfo(CPUSET_BACKGROUND, actual.get(0).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 60, MISSING_CPU_AVAILABILITY_PERCENT, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS), + new CpuAvailabilityInfo(CPUSET_BACKGROUND, actual.get(1).dataTimestampUptimeMillis, + /* latestAvgAvailabilityPercent= */ 95, + /* pastNMillisAvgAvailabilityPercent= */ 78, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS)); + + assertWithMessage("CPU availability infos").that(actual).isEqualTo(expected); + } + + @Test + public void testHeavyCpuLoadMonitoring() throws Exception { + // TODO(b/267500110): Once heavy CPU load detection logic is added, add unittest. + } + + private void startService() { + mService.onStart(); + mServiceHandler = mServiceHandlerThread.getThreadHandler(); + + assertWithMessage("Service thread handler").that(mServiceHandler).isNotNull(); + + mLocalService = LocalServices.getService(CpuMonitorInternal.class); + + assertWithMessage("CpuMonitorInternal local service").that(mLocalService).isNotNull(); + } + + private void terminateService() { + // The CpuMonitorInternal.class service is added by the {@link CpuMonitorService#onStart} + // call. Remove the service to ensure this service can be added again during + // the {@link CpuMonitorService#onStart} call. + LocalServices.removeServiceForTest(CpuMonitorInternal.class); + if (mServiceHandlerThread != null && mServiceHandlerThread.isAlive()) { + mServiceHandlerThread.quitSafely(); + } + } + + private void replaceServiceWithUserBuildService() { + terminateService(); + mServiceHandlerThread = new HandlerThread(USER_BUILD_TAG); + mService = new CpuMonitorService(mMockContext, mMockCpuInfoReader, + mServiceHandlerThread, /* shouldDebugMonitor= */ false, + TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS, + TEST_DEBUG_MONITORING_INTERVAL_MILLISECONDS, + TEST_LATEST_AVAILABILITY_DURATION_MILLISECONDS); + + startService(); + } + + private CpuMonitorInternal.CpuAvailabilityCallback addCpuAvailabilityCallback( + CpuAvailabilityMonitoringConfig config) throws Exception { + return addCpuAvailabilityCallback(mServiceHandler, /* executor= */ null, config); + } + + private CpuMonitorInternal.CpuAvailabilityCallback addCpuAvailabilityCallback(Handler handler, + HandlerExecutor executor, CpuAvailabilityMonitoringConfig config) throws Exception { + CpuMonitorInternal.CpuAvailabilityCallback mockCallback = mock( + CpuMonitorInternal.CpuAvailabilityCallback.class); + + mLocalService.addCpuAvailabilityCallback(executor, config, mockCallback); + + // Monitoring interval changed notification is sent asynchronously from the given handler. + // So, sync with this thread before verifying the client call. + syncWithHandler(handler, /* delayMillis= */ 0); + + verify(mockCallback, timeout(ASYNC_CALLBACK_WAIT_TIMEOUT_MILLISECONDS)) + .onMonitoringIntervalChanged(TEST_NORMAL_MONITORING_INTERVAL_MILLISECONDS); + + return mockCallback; + } + + private void injectCpuInfosAndWait(List<SparseArray<CpuInfoReader.CpuInfo>> cpuInfos) + throws Exception { + injectCpuInfosAndWait(mServiceHandler, cpuInfos); + } + + private void injectCpuInfosAndWait(Handler handler, + List<SparseArray<CpuInfoReader.CpuInfo>> cpuInfos) throws Exception { + assertWithMessage("CPU info configs").that(cpuInfos).isNotEmpty(); + + OngoingStubbing<SparseArray<CpuInfoReader.CpuInfo>> ongoingStubbing = + when(mMockCpuInfoReader.readCpuInfos()); + for (SparseArray<CpuInfoReader.CpuInfo> cpuInfo : cpuInfos) { + ongoingStubbing = ongoingStubbing.thenReturn(cpuInfo); + } + + // CPU infos are read asynchronously on a separate handler thread. So, wait based on + // the current monitoring interval and the number of CPU infos were injected. + syncWithHandler(handler, + /* delayMillis= */ mService.getCurrentMonitoringIntervalMillis() * cpuInfos.size()); + } + + private void syncWithHandler(Handler handler, long delayMillis) throws Exception { + AtomicBoolean didRun = new AtomicBoolean(false); + handler.postDelayed(() -> { + synchronized (didRun) { + didRun.set(true); + didRun.notifyAll(); + } + }, delayMillis); + synchronized (didRun) { + while (!didRun.get()) { + didRun.wait(HANDLER_THREAD_SYNC_TIMEOUT_MILLISECONDS); + } + } + } + + private static SparseArray<CpuInfoReader.CpuInfo> generateCpuInfosForAvailability( + double cpuAvailabilityPercent, ArraySet<Integer> offlineCores) { + SparseArray<CpuInfoReader.CpuInfo> cpuInfos = new SparseArray<>(STATIC_CPU_INFOS.size()); + for (StaticCpuInfo staticCpuInfo : STATIC_CPU_INFOS) { + boolean isOnline = !offlineCores.contains(staticCpuInfo.cpuCore); + cpuInfos.append(staticCpuInfo.cpuCore, constructCpuInfo(staticCpuInfo.cpuCore, + staticCpuInfo.cpusetCategories, isOnline, staticCpuInfo.maxCpuFreqKHz, + cpuAvailabilityPercent)); + } + return cpuInfos; + } + + private static CpuInfoReader.CpuInfo constructCpuInfo(int cpuCore, + @CpuInfoReader.CpusetCategory int cpusetCategories, boolean isOnline, + long maxCpuFreqKHz, double cpuAvailabilityPercent) { + long availCpuFreqKHz = (long) (maxCpuFreqKHz * (cpuAvailabilityPercent / 100.0)); + long curCpuFreqKHz = maxCpuFreqKHz - availCpuFreqKHz; + return new CpuInfoReader.CpuInfo(cpuCore, cpusetCategories, isOnline, + isOnline ? curCpuFreqKHz : MISSING_FREQUENCY, maxCpuFreqKHz, + /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY, + isOnline ? availCpuFreqKHz : MISSING_FREQUENCY, + /* latestCpuUsageStats= */ null); + } + + private static final class StaticCpuInfo { + public final int cpuCore; + public final int cpusetCategories; + public final int maxCpuFreqKHz; + + StaticCpuInfo(int cpuCore, @CpuInfoReader.CpusetCategory int cpusetCategories, + int maxCpuFreqKHz) { + this.cpuCore = cpuCore; + this.cpusetCategories = cpusetCategories; + this.maxCpuFreqKHz = maxCpuFreqKHz; + } + + @Override + public String toString() { + return "StaticCpuInfo{cpuCore=" + cpuCore + ", cpusetCategories=" + cpusetCategories + + ", maxCpuFreqKHz=" + maxCpuFreqKHz + '}'; + } + } } 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 e056417811c7..ba70c584f2de 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java @@ -262,9 +262,10 @@ public class JobSchedulerServiceTest { ConnectivityController connectivityController = mService.getConnectivityController(); spyOn(connectivityController); mService.mConstants.RUNTIME_MIN_GUARANTEE_MS = 10 * MINUTE_IN_MILLIS; + mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS = 2 * HOUR_IN_MILLIS; mService.mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR = 1.5f; mService.mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS = HOUR_IN_MILLIS; - mService.mConstants.RUNTIME_UI_DATA_TRANSFER_LIMIT_MS = 6 * HOUR_IN_MILLIS; + mService.mConstants.RUNTIME_UI_LIMIT_MS = 6 * HOUR_IN_MILLIS; assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, mService.getMinJobExecutionGuaranteeMs(ejMax)); @@ -283,9 +284,9 @@ public class JobSchedulerServiceTest { // Permission isn't granted, so it should just be treated as a regular job. assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, mService.getMinJobExecutionGuaranteeMs(jobUIDT)); + grantRunUserInitiatedJobsPermission(true); // With permission - assertEquals(mService.mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS, - mService.getMinJobExecutionGuaranteeMs(jobUIDT)); + mService.mConstants.RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS = true; doReturn(ConnectivityController.UNKNOWN_TIME) .when(connectivityController).getEstimatedTransferTimeMs(any()); assertEquals(mService.mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS, @@ -294,16 +295,20 @@ public class JobSchedulerServiceTest { .when(connectivityController).getEstimatedTransferTimeMs(any()); assertEquals(mService.mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS, mService.getMinJobExecutionGuaranteeMs(jobUIDT)); - doReturn(mService.mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS * 2) + final long estimatedTransferTimeMs = + mService.mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS * 2; + doReturn(estimatedTransferTimeMs) .when(connectivityController).getEstimatedTransferTimeMs(any()); - assertEquals( - (long) (mService.mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS - * 2 * mService.mConstants - .RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR), + assertEquals((long) (estimatedTransferTimeMs + * mService.mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR), mService.getMinJobExecutionGuaranteeMs(jobUIDT)); - doReturn(mService.mConstants.RUNTIME_UI_DATA_TRANSFER_LIMIT_MS * 2) + doReturn(mService.mConstants.RUNTIME_UI_LIMIT_MS * 2) .when(connectivityController).getEstimatedTransferTimeMs(any()); - assertEquals(mService.mConstants.RUNTIME_UI_DATA_TRANSFER_LIMIT_MS, + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMinJobExecutionGuaranteeMs(jobUIDT)); + + mService.mConstants.RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS = false; + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, mService.getMinJobExecutionGuaranteeMs(jobUIDT)); } @@ -325,7 +330,7 @@ public class JobSchedulerServiceTest { .when(quotaController).getMaxJobExecutionTimeMsLocked(any()); grantRunUserInitiatedJobsPermission(true); - assertEquals(mService.mConstants.RUNTIME_UI_DATA_TRANSFER_LIMIT_MS, + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobUIDT)); grantRunUserInitiatedJobsPermission(false); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, 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 32e5c836ac3d..a3ae83428af5 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 @@ -840,8 +840,9 @@ public class ConnectivityControllerTest { final JobStatus blue = createJobStatus(createJob() .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY), UID_BLUE); + // Unmetered preference is disabled for now. assertFalse(red.getPreferUnmetered()); - assertTrue(blue.getPreferUnmetered()); + assertFalse(blue.getPreferUnmetered()); controller.maybeStartTrackingJobLocked(red, null); controller.maybeStartTrackingJobLocked(blue, null); @@ -895,7 +896,7 @@ public class ConnectivityControllerTest { generalCallback.onLost(meteredNet); assertTrue(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - assertFalse(red.getHasAccessToUnmetered()); + assertTrue(red.getHasAccessToUnmetered()); assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); assertTrue(blue.getHasAccessToUnmetered()); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java index 6bc552c7f32e..4b19bbb72805 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java @@ -189,7 +189,10 @@ public class FlexibilityControllerTest { } private static JobInfo.Builder createJob(int id) { - return new JobInfo.Builder(id, new ComponentName("foo", "bar")); + return new JobInfo.Builder(id, new ComponentName("foo", "bar")) + .setPrefersBatteryNotLow(true) + .setPrefersCharging(true) + .setPrefersDeviceIdle(true); } private JobStatus createJobStatus(String testTag, JobInfo.Builder job) { @@ -533,12 +536,15 @@ public class FlexibilityControllerTest { jb = createJob(i); if (i > 0) { jb.setRequiresDeviceIdle(true); + jb.setPrefersDeviceIdle(false); } if (i > 1) { jb.setRequiresBatteryNotLow(true); + jb.setPrefersBatteryNotLow(false); } if (i > 2) { jb.setRequiresCharging(true); + jb.setPrefersCharging(false); } jobs[i] = createJobStatus("", jb); flexTracker.add(jobs[i]); @@ -547,53 +553,55 @@ public class FlexibilityControllerTest { synchronized (mFlexibilityController.mLock) { ArrayList<ArraySet<JobStatus>> trackedJobs = flexTracker.getArrayList(); assertEquals(1, trackedJobs.get(0).size()); - assertEquals(0, trackedJobs.get(1).size()); - assertEquals(0, trackedJobs.get(2).size()); - assertEquals(3, trackedJobs.get(3).size()); + assertEquals(1, trackedJobs.get(1).size()); + assertEquals(1, trackedJobs.get(2).size()); + assertEquals(1, trackedJobs.get(3).size()); assertEquals(0, trackedJobs.get(4).size()); flexTracker.adjustJobsRequiredConstraints(jobs[0], -1, FROZEN_TIME); assertEquals(1, trackedJobs.get(0).size()); - assertEquals(0, trackedJobs.get(1).size()); - assertEquals(1, trackedJobs.get(2).size()); - assertEquals(2, trackedJobs.get(3).size()); + assertEquals(1, trackedJobs.get(1).size()); + assertEquals(2, trackedJobs.get(2).size()); + assertEquals(0, trackedJobs.get(3).size()); assertEquals(0, trackedJobs.get(4).size()); flexTracker.adjustJobsRequiredConstraints(jobs[0], -1, FROZEN_TIME); assertEquals(1, trackedJobs.get(0).size()); - assertEquals(1, trackedJobs.get(1).size()); - assertEquals(0, trackedJobs.get(2).size()); - assertEquals(2, trackedJobs.get(3).size()); + assertEquals(2, trackedJobs.get(1).size()); + assertEquals(1, trackedJobs.get(2).size()); + assertEquals(0, trackedJobs.get(3).size()); assertEquals(0, trackedJobs.get(4).size()); flexTracker.adjustJobsRequiredConstraints(jobs[0], -1, FROZEN_TIME); assertEquals(2, trackedJobs.get(0).size()); - assertEquals(0, trackedJobs.get(1).size()); - assertEquals(0, trackedJobs.get(2).size()); - assertEquals(2, trackedJobs.get(3).size()); + assertEquals(1, trackedJobs.get(1).size()); + assertEquals(1, trackedJobs.get(2).size()); + assertEquals(0, trackedJobs.get(3).size()); assertEquals(0, trackedJobs.get(4).size()); flexTracker.remove(jobs[1]); assertEquals(2, trackedJobs.get(0).size()); - assertEquals(0, trackedJobs.get(1).size()); + assertEquals(1, trackedJobs.get(1).size()); assertEquals(0, trackedJobs.get(2).size()); - assertEquals(1, trackedJobs.get(3).size()); + assertEquals(0, trackedJobs.get(3).size()); assertEquals(0, trackedJobs.get(4).size()); flexTracker.resetJobNumDroppedConstraints(jobs[0], FROZEN_TIME); assertEquals(1, trackedJobs.get(0).size()); - assertEquals(0, trackedJobs.get(1).size()); + assertEquals(1, trackedJobs.get(1).size()); assertEquals(0, trackedJobs.get(2).size()); - assertEquals(2, trackedJobs.get(3).size()); + assertEquals(1, trackedJobs.get(3).size()); assertEquals(0, trackedJobs.get(4).size()); flexTracker.adjustJobsRequiredConstraints(jobs[0], -2, FROZEN_TIME); assertEquals(1, trackedJobs.get(0).size()); - assertEquals(1, trackedJobs.get(1).size()); + assertEquals(2, trackedJobs.get(1).size()); assertEquals(0, trackedJobs.get(2).size()); - assertEquals(1, trackedJobs.get(3).size()); + assertEquals(0, trackedJobs.get(3).size()); assertEquals(0, trackedJobs.get(4).size()); + // Over halfway through the flex window. The job that prefers all flex constraints + // should have its first flex constraint dropped. final long nowElapsed = ((DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS / 2) + HOUR_IN_MILLIS); JobSchedulerService.sElapsedRealtimeClock = @@ -601,9 +609,9 @@ public class FlexibilityControllerTest { flexTracker.resetJobNumDroppedConstraints(jobs[0], nowElapsed); assertEquals(1, trackedJobs.get(0).size()); - assertEquals(0, trackedJobs.get(1).size()); + assertEquals(1, trackedJobs.get(1).size()); assertEquals(1, trackedJobs.get(2).size()); - assertEquals(1, trackedJobs.get(3).size()); + assertEquals(0, trackedJobs.get(3).size()); assertEquals(0, trackedJobs.get(4).size()); } } @@ -618,8 +626,13 @@ public class FlexibilityControllerTest { @Test public void testExceptions_UserInitiated() { - JobInfo.Builder jb = createJob(0); - jb.setUserInitiated(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); + JobInfo.Builder jb = createJob(0) + .setUserInitiated(true) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + // Attempt to add flex constraints to the job. For now, we will ignore them. + .setPrefersBatteryNotLow(true) + .setPrefersCharging(true) + .setPrefersDeviceIdle(false); JobStatus js = createJobStatus("testExceptions_UserInitiated", jb); assertFalse(js.hasFlexibilityConstraint()); } @@ -635,10 +648,10 @@ public class FlexibilityControllerTest { @Test public void testExceptions_NoFlexibleConstraints() { - JobInfo.Builder jb = createJob(0); - jb.setRequiresDeviceIdle(true); - jb.setRequiresCharging(true); - jb.setRequiresBatteryNotLow(true); + JobInfo.Builder jb = createJob(0) + .setPrefersBatteryNotLow(false) + .setPrefersCharging(false) + .setPrefersDeviceIdle(false); JobStatus js = createJobStatus("testExceptions_NoFlexibleConstraints", jb); assertFalse(js.hasFlexibilityConstraint()); } @@ -697,15 +710,50 @@ public class FlexibilityControllerTest { JobStatus js = createJobStatus("testTopAppBypass", jb); synchronized (mFlexibilityController.mLock) { js.setHasAccessToUnmetered(false); - assertEquals(0, mFlexibilityController.getNumSatisfiedRequiredConstraintsLocked(js)); + assertEquals(0, mFlexibilityController.getNumSatisfiedFlexibleConstraintsLocked(js)); js.setHasAccessToUnmetered(true); - assertEquals(1, mFlexibilityController.getNumSatisfiedRequiredConstraintsLocked(js)); + assertEquals(1, mFlexibilityController.getNumSatisfiedFlexibleConstraintsLocked(js)); js.setHasAccessToUnmetered(false); - assertEquals(0, mFlexibilityController.getNumSatisfiedRequiredConstraintsLocked(js)); + assertEquals(0, mFlexibilityController.getNumSatisfiedFlexibleConstraintsLocked(js)); } } @Test + public void testGetNumSatisfiedFlexibleConstraints() { + long nowElapsed = FROZEN_TIME; + mFlexibilityController.setConstraintSatisfied(CONSTRAINT_BATTERY_NOT_LOW, true, nowElapsed); + mFlexibilityController.setConstraintSatisfied(CONSTRAINT_CHARGING, true, nowElapsed); + mFlexibilityController.setConstraintSatisfied(CONSTRAINT_IDLE, true, nowElapsed); + JobInfo.Builder jb = createJob(0) + .setPrefersBatteryNotLow(false) + .setPrefersCharging(false) + .setPrefersDeviceIdle(false); + JobStatus js = createJobStatus("testGetNumSatisfiedFlexibleConstraints", jb); + assertEquals(0, mFlexibilityController.getNumSatisfiedFlexibleConstraintsLocked(js)); + + jb = createJob(0) + .setPrefersBatteryNotLow(true) + .setPrefersCharging(false) + .setPrefersDeviceIdle(false); + js = createJobStatus("testGetNumSatisfiedFlexibleConstraints", jb); + assertEquals(1, mFlexibilityController.getNumSatisfiedFlexibleConstraintsLocked(js)); + + jb = createJob(0) + .setPrefersBatteryNotLow(true) + .setPrefersCharging(false) + .setPrefersDeviceIdle(true); + js = createJobStatus("testGetNumSatisfiedFlexibleConstraints", jb); + assertEquals(2, mFlexibilityController.getNumSatisfiedFlexibleConstraintsLocked(js)); + + jb = createJob(0) + .setPrefersBatteryNotLow(true) + .setPrefersCharging(true) + .setPrefersDeviceIdle(true); + js = createJobStatus("testGetNumSatisfiedFlexibleConstraints", jb); + assertEquals(3, mFlexibilityController.getNumSatisfiedFlexibleConstraintsLocked(js)); + } + + @Test public void testSetConstraintSatisfied_Constraints() { mFlexibilityController.setConstraintSatisfied(CONSTRAINT_IDLE, false, FROZEN_TIME); assertFalse(mFlexibilityController.isConstraintSatisfied(CONSTRAINT_IDLE)); @@ -736,8 +784,11 @@ public class FlexibilityControllerTest { jb = createJob(i); constraints = constraintCombinations[i]; jb.setRequiresDeviceIdle((constraints & CONSTRAINT_IDLE) != 0); + jb.setPrefersDeviceIdle((constraints & CONSTRAINT_IDLE) == 0); jb.setRequiresBatteryNotLow((constraints & CONSTRAINT_BATTERY_NOT_LOW) != 0); + jb.setPrefersBatteryNotLow((constraints & CONSTRAINT_BATTERY_NOT_LOW) == 0); jb.setRequiresCharging((constraints & CONSTRAINT_CHARGING) != 0); + jb.setPrefersCharging((constraints & CONSTRAINT_CHARGING) == 0); synchronized (mFlexibilityController.mLock) { mFlexibilityController.maybeStartTrackingJobLocked( createJobStatus(String.valueOf(i), jb), null); 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 5dc8ed58994d..b076ab495d0c 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 @@ -1039,7 +1039,9 @@ public class JobStatusTest { @Test public void testReadinessStatusWithConstraint_FlexibilityConstraint() { final JobStatus job = createJobStatus( - new JobInfo.Builder(101, new ComponentName("foo", "bar")).build()); + new JobInfo.Builder(101, new ComponentName("foo", "bar")) + .setPrefersCharging(true) + .build()); job.setConstraintSatisfied(CONSTRAINT_FLEXIBLE, sElapsedRealtimeClock.millis(), false); markImplicitConstraintsSatisfied(job, true); assertTrue(job.readinessStatusWithConstraint(CONSTRAINT_FLEXIBLE, true)); diff --git a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java index 5560b41b164f..236c74fc29e6 100644 --- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java +++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java @@ -728,6 +728,66 @@ public class JobStoreTest { } @Test + public void testPersistedPreferredBatteryNotLowConstraint() throws Exception { + JobInfo.Builder b = new Builder(8, mComponent) + .setPrefersBatteryNotLow(true) + .setPersisted(true); + JobStatus taskStatus = + JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null, null); + + mTaskStoreUnderTest.add(taskStatus); + waitForPendingIo(); + + final JobSet jobStatusSet = new JobSet(); + mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true); + assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size()); + JobStatus loaded = jobStatusSet.getAllJobs().iterator().next(); + assertEquals("Battery-not-low constraint not persisted correctly.", + taskStatus.getJob().isPreferBatteryNotLow(), + loaded.getJob().isPreferBatteryNotLow()); + } + + @Test + public void testPersistedPreferredChargingConstraint() throws Exception { + JobInfo.Builder b = new Builder(8, mComponent) + .setPrefersCharging(true) + .setPersisted(true); + JobStatus taskStatus = + JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null, null); + + mTaskStoreUnderTest.add(taskStatus); + waitForPendingIo(); + + final JobSet jobStatusSet = new JobSet(); + mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true); + assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size()); + JobStatus loaded = jobStatusSet.getAllJobs().iterator().next(); + assertEquals("Charging constraint not persisted correctly.", + taskStatus.getJob().isPreferCharging(), + loaded.getJob().isPreferCharging()); + } + + @Test + public void testPersistedPreferredDeviceIdleConstraint() throws Exception { + JobInfo.Builder b = new Builder(8, mComponent) + .setPrefersDeviceIdle(true) + .setPersisted(true); + JobStatus taskStatus = + JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null, null); + + mTaskStoreUnderTest.add(taskStatus); + waitForPendingIo(); + + final JobSet jobStatusSet = new JobSet(); + mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true); + assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size()); + JobStatus loaded = jobStatusSet.getAllJobs().iterator().next(); + assertEquals("Idle constraint not persisted correctly.", + taskStatus.getJob().isPreferDeviceIdle(), + loaded.getJob().isPreferDeviceIdle()); + } + + @Test public void testJobWorkItems() throws Exception { JobWorkItem item1 = new JobWorkItem.Builder().build(); item1.bumpDeliveryCount(); diff --git a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java index 9570ff6323d2..86878c5384fd 100644 --- a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java +++ b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java @@ -160,6 +160,9 @@ public class AppStandbyControllerTests { private static final String ADMIN_PKG2 = "com.android.admin2"; private static final String ADMIN_PKG3 = "com.android.admin3"; + private static final String ADMIN_PROTECTED_PKG = "com.android.admin.protected"; + private static final String ADMIN_PROTECTED_PKG2 = "com.android.admin.protected2"; + private static final long MINUTE_MS = 60 * 1000; private static final long HOUR_MS = 60 * MINUTE_MS; private static final long DAY_MS = 24 * HOUR_MS; @@ -1758,6 +1761,19 @@ public class AppStandbyControllerTests { } @Test + public void testSetAdminProtectedPackages() { + assertAdminProtectedPackagesForTest(USER_ID, (String[]) null); + assertAdminProtectedPackagesForTest(USER_ID2, (String[]) null); + + setAdminProtectedPackages(USER_ID, ADMIN_PROTECTED_PKG, ADMIN_PROTECTED_PKG2); + assertAdminProtectedPackagesForTest(USER_ID, ADMIN_PROTECTED_PKG, ADMIN_PROTECTED_PKG2); + assertAdminProtectedPackagesForTest(USER_ID2, (String[]) null); + + setAdminProtectedPackages(USER_ID, (String[]) null); + assertAdminProtectedPackagesForTest(USER_ID, (String[]) null); + } + + @Test @FlakyTest(bugId = 185169504) public void testUserInteraction_CrossProfile() throws Exception { mInjector.mRunningUsers = new int[] {USER_ID, USER_ID2, USER_ID3}; @@ -2195,6 +2211,28 @@ public class AppStandbyControllerTests { mController.setActiveAdminApps(new ArraySet<>(Arrays.asList(admins)), userId); } + private void setAdminProtectedPackages(int userId, String... packageNames) { + Set<String> adminProtectedPackages = packageNames != null ? new ArraySet<>( + Arrays.asList(packageNames)) : null; + mController.setAdminProtectedPackages(adminProtectedPackages, userId); + } + + private void assertAdminProtectedPackagesForTest(int userId, String... packageNames) { + final Set<String> actualAdminProtectedPackages = + mController.getAdminProtectedPackagesForTest(userId); + if (packageNames == null) { + if (actualAdminProtectedPackages != null && !actualAdminProtectedPackages.isEmpty()) { + fail("Admin protected packages should be null; " + getAdminAppsStr(userId, + actualAdminProtectedPackages)); + } + return; + } + assertEquals(packageNames.length, actualAdminProtectedPackages.size()); + for (String adminProtectedPackage : packageNames) { + assertTrue(actualAdminProtectedPackages.contains(adminProtectedPackage)); + } + } + private void setAndAssertBucket(String pkg, int user, int bucket, int reason) throws Exception { rearmLatch(pkg); mController.setAppStandbyBucket(pkg, user, bucket, reason); diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index 7ff5b4a28f1b..8948e494d851 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -3129,6 +3129,11 @@ public class UsageStatsService extends SystemService implements } @Override + public void setAdminProtectedPackages(Set<String> packageNames, int userId) { + mAppStandby.setAdminProtectedPackages(packageNames, userId); + } + + @Override public void onAdminDataAvailable() { mAppStandby.onAdminDataAvailable(); } diff --git a/tools/locked_region_code_injection/Android.bp b/tools/locked_region_code_injection/Android.bp index a0cc446cd42d..954b816a52bf 100644 --- a/tools/locked_region_code_injection/Android.bp +++ b/tools/locked_region_code_injection/Android.bp @@ -19,3 +19,20 @@ java_binary_host { "ow2-asm-tree", ], } + +java_library_host { + name: "lockedregioncodeinjection_input", + manifest: "test/manifest.txt", + srcs: ["test/*/*.java"], + static_libs: [ + "guava", + "ow2-asm", + "ow2-asm-analysis", + "ow2-asm-commons", + "ow2-asm-tree", + "hamcrest-library", + "hamcrest", + "platform-test-annotations", + "junit", + ], +} diff --git a/tools/locked_region_code_injection/OWNERS b/tools/locked_region_code_injection/OWNERS new file mode 100644 index 000000000000..bd43f1736ca5 --- /dev/null +++ b/tools/locked_region_code_injection/OWNERS @@ -0,0 +1,4 @@ +# Everyone in frameworks/base is included by default +shayba@google.com +shombert@google.com +timmurray@google.com diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java index 81a077324e6c..2067bb4ef2fe 100644 --- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java +++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java @@ -13,37 +13,51 @@ */ package lockedregioncodeinjection; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; +import static com.google.common.base.Preconditions.checkElementIndex; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.commons.TryCatchBlockSorter; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LineNumberNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TypeInsnNode; import org.objectweb.asm.tree.TryCatchBlockNode; import org.objectweb.asm.tree.analysis.Analyzer; import org.objectweb.asm.tree.analysis.AnalyzerException; import org.objectweb.asm.tree.analysis.BasicValue; import org.objectweb.asm.tree.analysis.Frame; -import static com.google.common.base.Preconditions.checkElementIndex; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; /** - * This visitor does two things: + * This visitor operates on two kinds of targets. For a legacy target, it does the following: * - * 1. Finds all the MONITOR_ENTER / MONITOR_EXIT in the byte code and insert the corresponding pre + * 1. Finds all the MONITOR_ENTER / MONITOR_EXIT in the byte code and inserts the corresponding pre * and post methods calls should it matches one of the given target type in the Configuration. * * 2. Find all methods that are synchronized and insert pre method calls in the beginning and post * method calls just before all return instructions. + * + * For a scoped target, it does the following: + * + * 1. Finds all the MONITOR_ENTER instructions in the byte code. If the target of the opcode is + * named in a --scope switch, then the pre method is invoked ON THE TARGET immediately after + * MONITOR_ENTER opcode completes. + * + * 2. Finds all the MONITOR_EXIT instructions in the byte code. If the target of the opcode is + * named in a --scope switch, then the post method is invoked ON THE TARGET immediately before + * MONITOR_EXIT opcode completes. */ class LockFindingClassVisitor extends ClassVisitor { private String className = null; @@ -73,12 +87,16 @@ class LockFindingClassVisitor extends ClassVisitor { class LockFindingMethodVisitor extends MethodVisitor { private String owner; private MethodVisitor chain; + private final String className; + private final String methodName; public LockFindingMethodVisitor(String owner, MethodNode mn, MethodVisitor chain) { super(Utils.ASM_VERSION, mn); assert owner != null; this.owner = owner; this.chain = chain; + className = owner; + methodName = mn.name; } @SuppressWarnings("unchecked") @@ -93,6 +111,12 @@ class LockFindingClassVisitor extends ClassVisitor { for (LockTarget t : targets) { if (t.getTargetDesc().equals("L" + owner + ";")) { ownerMonitor = t; + if (ownerMonitor.getScoped()) { + final String emsg = String.format( + "scoped targets do not support synchronized methods in %s.%s()", + className, methodName); + throw new RuntimeException(emsg); + } } } } @@ -118,9 +142,11 @@ class LockFindingClassVisitor extends ClassVisitor { AbstractInsnNode s = instructions.getFirst(); MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC, ownerMonitor.getPreOwner(), ownerMonitor.getPreMethod(), "()V", false); - insertMethodCallBefore(mn, frameMap, handlersMap, s, 0, call); + insertMethodCallBeforeSync(mn, frameMap, handlersMap, s, 0, call); } + boolean anyDup = false; + for (int i = 0; i < instructions.size(); i++) { AbstractInsnNode s = instructions.get(i); @@ -131,9 +157,15 @@ class LockFindingClassVisitor extends ClassVisitor { LockTargetState state = (LockTargetState) operand; for (int j = 0; j < state.getTargets().size(); j++) { LockTarget target = state.getTargets().get(j); - MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC, - target.getPreOwner(), target.getPreMethod(), "()V", false); - insertMethodCallAfter(mn, frameMap, handlersMap, s, i, call); + MethodInsnNode call = methodCall(target, true); + if (target.getScoped()) { + TypeInsnNode cast = typeCast(target); + i += insertInvokeAcquire(mn, frameMap, handlersMap, s, i, + call, cast); + anyDup = true; + } else { + i += insertMethodCallBefore(mn, frameMap, handlersMap, s, i, call); + } } } } @@ -144,8 +176,9 @@ class LockFindingClassVisitor extends ClassVisitor { if (operand instanceof LockTargetState) { LockTargetState state = (LockTargetState) operand; for (int j = 0; j < state.getTargets().size(); j++) { - // The instruction after a monitor_exit should be a label for the end of the implicit - // catch block that surrounds the synchronized block to call monitor_exit when an exception + // The instruction after a monitor_exit should be a label for + // the end of the implicit catch block that surrounds the + // synchronized block to call monitor_exit when an exception // occurs. checkState(instructions.get(i + 1).getType() == AbstractInsnNode.LABEL, "Expected to find label after monitor exit"); @@ -161,9 +194,16 @@ class LockFindingClassVisitor extends ClassVisitor { "Expected label to be the end of monitor exit's try block"); LockTarget target = state.getTargets().get(j); - MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC, - target.getPostOwner(), target.getPostMethod(), "()V", false); - insertMethodCallAfter(mn, frameMap, handlersMap, label, labelIndex, call); + MethodInsnNode call = methodCall(target, false); + if (target.getScoped()) { + TypeInsnNode cast = typeCast(target); + i += insertInvokeRelease(mn, frameMap, handlersMap, s, i, + call, cast); + anyDup = true; + } else { + insertMethodCallAfter(mn, frameMap, handlersMap, label, + labelIndex, call); + } } } } @@ -174,16 +214,116 @@ class LockFindingClassVisitor extends ClassVisitor { MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC, ownerMonitor.getPostOwner(), ownerMonitor.getPostMethod(), "()V", false); - insertMethodCallBefore(mn, frameMap, handlersMap, s, i, call); + insertMethodCallBeforeSync(mn, frameMap, handlersMap, s, i, call); i++; // Skip ahead. Otherwise, we will revisit this instruction again. } } + + if (anyDup) { + mn.maxStack++; + } + super.visitEnd(); mn.accept(chain); } + + // Insert a call to a monitor pre handler. The node and the index identify the + // monitorenter call itself. Insert DUP immediately prior to the MONITORENTER. + // Insert the typecast and call (in that order) after the MONITORENTER. + public int insertInvokeAcquire(MethodNode mn, List<Frame> frameMap, + List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index, + MethodInsnNode call, TypeInsnNode cast) { + InsnList instructions = mn.instructions; + + // Insert a DUP right before MONITORENTER, to capture the object being locked. + // Note that the object will be typed as java.lang.Object. + instructions.insertBefore(node, new InsnNode(Opcodes.DUP)); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + // Insert the call right after the MONITORENTER. These entries are pushed after + // MONITORENTER so they are inserted in reverse order. MONITORENTER should be + // the target of a try/catch block, which means it must be immediately + // followed by a label (which is part of the try/catch block definition). + // Move forward past the label so the invocation in inside the proper block. + // Throw an error if the next instruction is not a label. + node = node.getNext(); + if (!(node instanceof LabelNode)) { + throw new RuntimeException(String.format("invalid bytecode sequence in %s.%s()", + className, methodName)); + } + node = node.getNext(); + index = instructions.indexOf(node); + + instructions.insertBefore(node, cast); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + instructions.insertBefore(node, call); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + return 3; + } + + // Insert instructions completely before the current opcode. This is slightly + // different from insertMethodCallBefore(), which inserts the call before MONITOREXIT + // but inserts the start and end labels after MONITOREXIT. + public int insertInvokeRelease(MethodNode mn, List<Frame> frameMap, + List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index, + MethodInsnNode call, TypeInsnNode cast) { + InsnList instructions = mn.instructions; + + instructions.insertBefore(node, new InsnNode(Opcodes.DUP)); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + instructions.insertBefore(node, cast); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + instructions.insertBefore(node, call); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + return 3; + } + } + + public static MethodInsnNode methodCall(LockTarget target, boolean pre) { + String spec = "()V"; + if (!target.getScoped()) { + if (pre) { + return new MethodInsnNode( + Opcodes.INVOKESTATIC, target.getPreOwner(), target.getPreMethod(), spec); + } else { + return new MethodInsnNode( + Opcodes.INVOKESTATIC, target.getPostOwner(), target.getPostMethod(), spec); + } + } else { + if (pre) { + return new MethodInsnNode( + Opcodes.INVOKEVIRTUAL, target.getPreOwner(), target.getPreMethod(), spec); + } else { + return new MethodInsnNode( + Opcodes.INVOKEVIRTUAL, target.getPostOwner(), target.getPostMethod(), spec); + } + } } - public static void insertMethodCallBefore(MethodNode mn, List<Frame> frameMap, + public static TypeInsnNode typeCast(LockTarget target) { + if (!target.getScoped()) { + return null; + } else { + // preOwner and postOwner return the same string for scoped targets. + return new TypeInsnNode(Opcodes.CHECKCAST, target.getPreOwner()); + } + } + + /** + * Insert a method call before the beginning or end of a synchronized method. + */ + public static void insertMethodCallBeforeSync(MethodNode mn, List<Frame> frameMap, List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index, MethodInsnNode call) { List<TryCatchBlockNode> handlers = handlersMap.get(index); @@ -226,6 +366,22 @@ class LockFindingClassVisitor extends ClassVisitor { updateCatchHandler(mn, handlers, start, end, handlersMap); } + // Insert instructions completely before the current opcode. This is slightly different from + // insertMethodCallBeforeSync(), which inserts the call before MONITOREXIT but inserts the + // start and end labels after MONITOREXIT. + public int insertMethodCallBefore(MethodNode mn, List<Frame> frameMap, + List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index, + MethodInsnNode call) { + InsnList instructions = mn.instructions; + + instructions.insertBefore(node, call); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + return 1; + } + + @SuppressWarnings("unchecked") public static void updateCatchHandler(MethodNode mn, List<TryCatchBlockNode> handlers, LabelNode start, LabelNode end, List<List<TryCatchBlockNode>> handlersMap) { diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java index c5e59e3dc64d..5f6240327a7f 100644 --- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java +++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java @@ -21,14 +21,28 @@ package lockedregioncodeinjection; public class LockTarget { public static final LockTarget NO_TARGET = new LockTarget("", null, null); + // The lock which must be instrumented, in Java internal form (L<path>;). private final String targetDesc; + // The methods to be called when the lock is taken (released). For non-scoped locks, + // these are fully qualified static methods. For scoped locks, these are the + // unqualified names of a member method of the target lock. private final String pre; private final String post; + // If true, the pre and post methods are virtual on the target class. The pre and post methods + // are both called while the lock is held. If this field is false then the pre and post methods + // take no parameters and the post method is called after the lock is released. This is legacy + // behavior. + private final boolean scoped; - public LockTarget(String targetDesc, String pre, String post) { + public LockTarget(String targetDesc, String pre, String post, boolean scoped) { this.targetDesc = targetDesc; this.pre = pre; this.post = post; + this.scoped = scoped; + } + + public LockTarget(String targetDesc, String pre, String post) { + this(targetDesc, pre, post, false); } public String getTargetDesc() { @@ -40,7 +54,11 @@ public class LockTarget { } public String getPreOwner() { - return pre.substring(0, pre.lastIndexOf('.')); + if (scoped) { + return targetDesc.substring(1, targetDesc.length() - 1); + } else { + return pre.substring(0, pre.lastIndexOf('.')); + } } public String getPreMethod() { @@ -52,10 +70,23 @@ public class LockTarget { } public String getPostOwner() { - return post.substring(0, post.lastIndexOf('.')); + if (scoped) { + return targetDesc.substring(1, targetDesc.length() - 1); + } else { + return post.substring(0, post.lastIndexOf('.')); + } } public String getPostMethod() { return post.substring(post.lastIndexOf('.') + 1); } + + public boolean getScoped() { + return scoped; + } + + @Override + public String toString() { + return targetDesc + ":" + pre + ":" + post; + } } diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java index 99d841829132..5df0160abd8c 100644 --- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java +++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java @@ -13,10 +13,11 @@ */ package lockedregioncodeinjection; -import java.util.List; import org.objectweb.asm.Type; import org.objectweb.asm.tree.analysis.BasicValue; +import java.util.List; + public class LockTargetState extends BasicValue { private final List<LockTarget> lockTargets; @@ -31,4 +32,10 @@ public class LockTargetState extends BasicValue { public List<LockTarget> getTargets() { return lockTargets; } + + @Override + public String toString() { + return "LockTargetState(" + getType().getDescriptor() + + ", " + lockTargets.size() + ")"; + } } diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java index 828cce72dda9..d22ea2338ff7 100644 --- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java +++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java @@ -21,7 +21,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.Collections; +import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.zip.ZipEntry; @@ -36,6 +36,7 @@ public class Main { String legacyTargets = null; String legacyPreMethods = null; String legacyPostMethods = null; + List<LockTarget> targets = new ArrayList<>(); for (int i = 0; i < args.length; i++) { if ("-i".equals(args[i].trim())) { i++; @@ -52,23 +53,25 @@ public class Main { } else if ("--post".equals(args[i].trim())) { i++; legacyPostMethods = args[i].trim(); + } else if ("--scoped".equals(args[i].trim())) { + i++; + targets.add(Utils.getScopedTarget(args[i].trim())); } - } - // TODO(acleung): Better help message than asserts. - assert inJar != null; - assert outJar != null; + if (inJar == null) { + throw new RuntimeException("missing input jar path"); + } + if (outJar == null) { + throw new RuntimeException("missing output jar path"); + } assert legacyTargets == null || (legacyPreMethods != null && legacyPostMethods != null); ZipFile zipSrc = new ZipFile(inJar); ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(outJar)); - List<LockTarget> targets = null; if (legacyTargets != null) { - targets = Utils.getTargetsFromLegacyJackConfig(legacyTargets, legacyPreMethods, - legacyPostMethods); - } else { - targets = Collections.emptyList(); + targets.addAll(Utils.getTargetsFromLegacyJackConfig(legacyTargets, legacyPreMethods, + legacyPostMethods)); } Enumeration<? extends ZipEntry> srcEntries = zipSrc.entries(); diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java index b44e8b42f052..bfef10611e6c 100644 --- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java +++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java @@ -44,4 +44,27 @@ public class Utils { return config; } + + /** + * Returns a single {@link LockTarget} from a string. The target is a comma-separated list of + * the target class, the request method, the release method, and a boolean which is true if this + * is a scoped target and false if this is a legacy target. The boolean is optional and + * defaults to true. + */ + public static LockTarget getScopedTarget(String arg) { + String[] c = arg.split(","); + if (c.length == 3) { + return new LockTarget(c[0], c[1], c[2], true); + } else if (c.length == 4) { + if (c[3].equals("true")) { + return new LockTarget(c[0], c[1], c[2], true); + } else if (c[3].equals("false")) { + return new LockTarget(c[0], c[1], c[2], false); + } else { + System.err.println("illegal target parameter \"" + c[3] + "\""); + } + } + // Fall through + throw new RuntimeException("invalid scoped target format"); + } } diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java index 31fa0bf63416..28f00b9c897a 100644 --- a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java +++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java @@ -17,7 +17,10 @@ import org.junit.Assert; import org.junit.Test; /** - * To run the unit tests: + * To run the unit tests, first build the two necessary artifacts. Do this explicitly as they are + * not generally retained by a normal "build all". After lunching a target: + * m lockedregioncodeinjection + * m lockedregioncodeinjection_input * * <pre> * <code> @@ -29,31 +32,25 @@ import org.junit.Test; * mkdir -p out * rm -fr out/* * - * # Make booster - * javac -cp lib/asm-7.0_BETA.jar:lib/asm-commons-7.0_BETA.jar:lib/asm-tree-7.0_BETA.jar:lib/asm-analysis-7.0_BETA.jar:lib/guava-21.0.jar src/*/*.java -d out/ - * pushd out - * jar cfe lockedregioncodeinjection.jar lockedregioncodeinjection.Main */*.class - * popd - * - * # Make unit tests. - * javac -cp lib/junit-4.12.jar test/*/*.java -d out/ - * - * pushd out - * jar cfe test_input.jar lockedregioncodeinjection.Test */*.class - * popd + * # Paths to the build artifacts. These assume linux-x86; YMMV. + * ROOT=$TOP/out/host/linux-x86 + * EXE=$ROOT/bin/lockedregioncodeinjection + * INPUT=$ROOT/frameworkd/lockedregioncodeinjection_input.jar * * # Run tool on unit tests. - * java -ea -cp lib/asm-7.0_BETA.jar:lib/asm-commons-7.0_BETA.jar:lib/asm-tree-7.0_BETA.jar:lib/asm-analysis-7.0_BETA.jar:lib/guava-21.0.jar:out/lockedregioncodeinjection.jar \ - * lockedregioncodeinjection.Main \ - * -i out/test_input.jar -o out/test_output.jar \ + * $EXE -i $INPUT -o out/test_output.jar \ * --targets 'Llockedregioncodeinjection/TestTarget;' \ * --pre 'lockedregioncodeinjection/TestTarget.boost' \ * --post 'lockedregioncodeinjection/TestTarget.unboost' * * # Run unit tests. - * java -ea -cp lib/hamcrest-core-1.3.jar:lib/junit-4.12.jar:out/test_output.jar \ + * java -ea -cp out/test_output.jar \ * org.junit.runner.JUnitCore lockedregioncodeinjection.TestMain * </code> + * OR + * <code> + * bash test/unit-test.sh + * </code> * </pre> */ public class TestMain { @@ -64,7 +61,9 @@ public class TestMain { Assert.assertEquals(TestTarget.boostCount, 0); Assert.assertEquals(TestTarget.unboostCount, 0); - Assert.assertEquals(TestTarget.unboostCount, 0); + Assert.assertEquals(TestTarget.invokeCount, 0); + Assert.assertEquals(TestTarget.boostCountLocked, 0); + Assert.assertEquals(TestTarget.unboostCountLocked, 0); synchronized (t) { Assert.assertEquals(TestTarget.boostCount, 1); @@ -75,6 +74,8 @@ public class TestMain { Assert.assertEquals(TestTarget.boostCount, 1); Assert.assertEquals(TestTarget.unboostCount, 1); Assert.assertEquals(TestTarget.invokeCount, 1); + Assert.assertEquals(TestTarget.boostCountLocked, 0); + Assert.assertEquals(TestTarget.unboostCountLocked, 0); } @Test @@ -84,12 +85,16 @@ public class TestMain { Assert.assertEquals(TestTarget.boostCount, 0); Assert.assertEquals(TestTarget.unboostCount, 0); + Assert.assertEquals(TestTarget.boostCountLocked, 0); + Assert.assertEquals(TestTarget.unboostCountLocked, 0); t.synchronizedCall(); Assert.assertEquals(TestTarget.boostCount, 1); Assert.assertEquals(TestTarget.unboostCount, 1); Assert.assertEquals(TestTarget.invokeCount, 1); + Assert.assertEquals(TestTarget.boostCountLocked, 0); + Assert.assertEquals(TestTarget.unboostCountLocked, 0); } @Test @@ -99,12 +104,16 @@ public class TestMain { Assert.assertEquals(TestTarget.boostCount, 0); Assert.assertEquals(TestTarget.unboostCount, 0); + Assert.assertEquals(TestTarget.boostCountLocked, 0); + Assert.assertEquals(TestTarget.unboostCountLocked, 0); t.synchronizedCallReturnInt(); Assert.assertEquals(TestTarget.boostCount, 1); Assert.assertEquals(TestTarget.unboostCount, 1); Assert.assertEquals(TestTarget.invokeCount, 1); + Assert.assertEquals(TestTarget.boostCountLocked, 0); + Assert.assertEquals(TestTarget.unboostCountLocked, 0); } @Test @@ -253,4 +262,125 @@ public class TestMain { Assert.assertEquals(TestTarget.invokeCount, 1); } + @Test + public void testScopedTarget() { + TestScopedTarget target = new TestScopedTarget(); + Assert.assertEquals(0, target.scopedLock().mLevel); + + synchronized (target.scopedLock()) { + Assert.assertEquals(1, target.scopedLock().mLevel); + } + Assert.assertEquals(0, target.scopedLock().mLevel); + + synchronized (target.scopedLock()) { + synchronized (target.scopedLock()) { + Assert.assertEquals(2, target.scopedLock().mLevel); + } + } + Assert.assertEquals(0, target.scopedLock().mLevel); + } + + @Test + public void testScopedExceptionHandling() { + TestScopedTarget target = new TestScopedTarget(); + Assert.assertEquals(0, target.scopedLock().mLevel); + + boolean handled; + + // 1: an exception inside the block properly releases the lock. + handled = false; + try { + synchronized (target.scopedLock()) { + Assert.assertEquals(true, Thread.holdsLock(target.scopedLock())); + Assert.assertEquals(1, target.scopedLock().mLevel); + throw new RuntimeException(); + } + } catch (RuntimeException e) { + Assert.assertEquals(0, target.scopedLock().mLevel); + handled = true; + } + Assert.assertEquals(0, target.scopedLock().mLevel); + Assert.assertEquals(true, handled); + // Just verify that the lock can still be taken + Assert.assertEquals(false, Thread.holdsLock(target.scopedLock())); + + // 2: An exception inside the monitor enter function + handled = false; + target.throwOnEnter(true); + try { + synchronized (target.scopedLock()) { + // The exception was thrown inside monitorEnter(), so the code should + // never reach this point. + Assert.assertEquals(0, 1); + } + } catch (RuntimeException e) { + Assert.assertEquals(0, target.scopedLock().mLevel); + handled = true; + } + Assert.assertEquals(0, target.scopedLock().mLevel); + Assert.assertEquals(true, handled); + // Just verify that the lock can still be taken + Assert.assertEquals(false, Thread.holdsLock(target.scopedLock())); + + // 3: An exception inside the monitor exit function + handled = false; + target.throwOnEnter(true); + try { + synchronized (target.scopedLock()) { + Assert.assertEquals(true, Thread.holdsLock(target.scopedLock())); + Assert.assertEquals(1, target.scopedLock().mLevel); + } + } catch (RuntimeException e) { + Assert.assertEquals(0, target.scopedLock().mLevel); + handled = true; + } + Assert.assertEquals(0, target.scopedLock().mLevel); + Assert.assertEquals(true, handled); + // Just verify that the lock can still be taken + Assert.assertEquals(false, Thread.holdsLock(target.scopedLock())); + } + + // Provide an in-class type conversion for the scoped target. + private Object untypedLock(TestScopedTarget target) { + return target.scopedLock(); + } + + @Test + public void testScopedLockTyping() { + TestScopedTarget target = new TestScopedTarget(); + Assert.assertEquals(target.scopedLock().mLevel, 0); + + // Scoped lock injection works on the static type of an object. In general, it is + // a very bad idea to do type conversion on scoped locks, but the general rule is + // that conversions within a single method are recognized by the lock injection + // tool and injection occurs. Conversions outside a single method are not + // recognized and injection does not occur. + + // 1. Conversion occurs outside the class. The visible type of the lock is Object + // in this block, so no injection takes place on 'untypedLock', even though the + // dynamic type is TestScopedLock. + synchronized (target.untypedLock()) { + Assert.assertEquals(0, target.scopedLock().mLevel); + Assert.assertEquals(true, target.untypedLock() instanceof TestScopedLock); + Assert.assertEquals(true, Thread.holdsLock(target.scopedLock())); + } + + // 2. Conversion occurs inside the class but in another method. The visible type + // of the lock is Object in this block, so no injection takes place on + // 'untypedLock', even though the dynamic type is TestScopedLock. + synchronized (untypedLock(target)) { + Assert.assertEquals(0, target.scopedLock().mLevel); + Assert.assertEquals(true, target.untypedLock() instanceof TestScopedLock); + Assert.assertEquals(true, Thread.holdsLock(target.scopedLock())); + } + + // 3. Conversion occurs inside the method. The compiler can determine the type of + // the lock within a single function, so injection does take place here. + Object untypedLock = target.scopedLock(); + synchronized (untypedLock) { + Assert.assertEquals(1, target.scopedLock().mLevel); + Assert.assertEquals(true, untypedLock instanceof TestScopedLock); + Assert.assertEquals(true, Thread.holdsLock(target.scopedLock())); + } + } } diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedLock.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedLock.java new file mode 100644 index 000000000000..7441d2b1da48 --- /dev/null +++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedLock.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 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 lockedregioncodeinjection; + +public class TestScopedLock { + public int mEntered = 0; + public int mExited = 0; + + public int mLevel = 0; + private final TestScopedTarget mTarget; + + TestScopedLock(TestScopedTarget target) { + mTarget = target; + } + + void monitorEnter() { + mLevel++; + mEntered++; + mTarget.enter(mLevel); + } + + void monitorExit() { + mLevel--; + mExited++; + mTarget.exit(mLevel); + } +} diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedTarget.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedTarget.java new file mode 100644 index 000000000000..c80975e9c6b3 --- /dev/null +++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedTarget.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2017 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 lockedregioncodeinjection; + +public class TestScopedTarget { + + public final TestScopedLock mLock;; + + private boolean mNextEnterThrows = false; + private boolean mNextExitThrows = false; + + TestScopedTarget() { + mLock = new TestScopedLock(this); + } + + TestScopedLock scopedLock() { + return mLock; + } + + Object untypedLock() { + return mLock; + } + + void enter(int level) { + if (mNextEnterThrows) { + mNextEnterThrows = false; + throw new RuntimeException(); + } + } + + void exit(int level) { + if (mNextExitThrows) { + mNextExitThrows = false; + throw new RuntimeException(); + } + } + + void throwOnEnter(boolean b) { + mNextEnterThrows = b; + } + + void throwOnExit(boolean b) { + mNextExitThrows = b; + } +} diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java index d1c8f340a598..e3ba6a77e3b7 100644 --- a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java +++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java @@ -19,8 +19,17 @@ public class TestTarget { public static int invokeCount = 0; public static boolean nextUnboostThrows = false; + // If this is not null, then this is the lock under test. The lock must not be held when boost() + // or unboost() are called. + public static Object mLock = null; + public static int boostCountLocked = 0; + public static int unboostCountLocked = 0; + public static void boost() { boostCount++; + if (mLock != null && Thread.currentThread().holdsLock(mLock)) { + boostCountLocked++; + } } public static void unboost() { @@ -29,6 +38,9 @@ public class TestTarget { throw new RuntimeException(); } unboostCount++; + if (mLock != null && Thread.currentThread().holdsLock(mLock)) { + unboostCountLocked++; + } } public static void invoke() { diff --git a/tools/locked_region_code_injection/test/manifest.txt b/tools/locked_region_code_injection/test/manifest.txt new file mode 100644 index 000000000000..2314c188721c --- /dev/null +++ b/tools/locked_region_code_injection/test/manifest.txt @@ -0,0 +1 @@ +Main-Class: org.junit.runner.JUnitCore diff --git a/tools/locked_region_code_injection/test/unit-test.sh b/tools/locked_region_code_injection/test/unit-test.sh new file mode 100755 index 000000000000..9fa6f39af14a --- /dev/null +++ b/tools/locked_region_code_injection/test/unit-test.sh @@ -0,0 +1,98 @@ +#! /bin/bash +# + +# Copyright (C) 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +# in compliance with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License +# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing permissions and limitations under +# the License. + +# This script runs the tests for the lockedregioninjectioncode. See +# TestMain.java for the invocation. The script expects that a full build has +# already been done and artifacts are in $TOP/out. + +# Compute the default top of the workspace. The following code copies the +# strategy of croot. (croot cannot be usd directly because it is a function and +# functions are not carried over into subshells.) This gives the correct answer +# if run from inside a workspace. If run from outside a workspace, supply TOP +# on the command line. +TOPFILE=build/make/core/envsetup.mk +TOP=$(dirname $(realpath $0)) +while [[ ! $TOP = / && ! -f $TOP/$TOPFILE ]]; do + TOP=$(dirname $TOP) +done +# TOP is "/" if this script is located outside a workspace. + +# If the user supplied a top directory, use it instead +if [[ -n $1 ]]; then + TOP=$1 + shift +fi +if [[ -z $TOP || $TOP = / ]]; then + echo "usage: $0 <workspace-root>" + exit 1 +elif [[ ! -d $TOP ]]; then + echo "$TOP is not a directory" + exit 1 +elif [[ ! -d $TOP/prebuilts/misc/common ]]; then + echo "$TOP does not look like w workspace" + exit 1 +fi +echo "Using workspace $TOP" + +# Pick up the current java compiler. The lunch target is not very important, +# since most, if not all, will use the same host binaries. +pushd $TOP > /dev/null +. build/envsetup.sh > /dev/null 2>&1 +lunch redfin-userdebug > /dev/null 2>&1 +popd > /dev/null + +# Bail on any error +set -o pipefail +trap 'exit 1' ERR + +# Create the two sources +pushd $TOP > /dev/null +m lockedregioncodeinjection +m lockedregioncodeinjection_input +popd > /dev/null + +# Create a temporary directory outside of the workspace. +OUT=$TOP/out/host/test/lockedregioncodeinjection +echo + +# Clean the directory +if [[ -d $OUT ]]; then rm -r $OUT; fi +mkdir -p $OUT + +ROOT=$TOP/out/host/linux-x86 +EXE=$ROOT/bin/lockedregioncodeinjection +INP=$ROOT/framework/lockedregioncodeinjection_input.jar + +# Run tool on unit tests. +$EXE \ + -i $INP -o $OUT/test_output.jar \ + --targets 'Llockedregioncodeinjection/TestTarget;' \ + --pre 'lockedregioncodeinjection/TestTarget.boost' \ + --post 'lockedregioncodeinjection/TestTarget.unboost' \ + --scoped 'Llockedregioncodeinjection/TestScopedLock;,monitorEnter,monitorExit' + +# Run unit tests. +java -ea -cp $OUT/test_output.jar \ + org.junit.runner.JUnitCore lockedregioncodeinjection.TestMain + +# Extract the class files and decompile them for possible post-analysis. +pushd $OUT > /dev/null +jar -x --file test_output.jar lockedregioncodeinjection +for class in lockedregioncodeinjection/*.class; do + javap -c -v $class > ${class%.class}.asm +done +popd > /dev/null + +echo "artifacts are in $OUT" |