diff options
52 files changed, 2261 insertions, 353 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 1c6df75a4f02..79b4fd6ee9ae 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -855,6 +855,13 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +java_aconfig_library { + name: "device_policy_aconfig_flags_lib_host", + aconfig_declarations: "device_policy_aconfig_flags", + host_supported: true, + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + cc_aconfig_library { name: "device_policy_aconfig_flags_c_lib", aconfig_declarations: "device_policy_aconfig_flags", diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index 7de67993bc62..52c0ac1d981d 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -124,6 +124,15 @@ public class JobInfo implements Parcelable { @Overridable // Aid in testing public static final long ENFORCE_MINIMUM_TIME_WINDOWS = 311402873L; + /** + * Require that minimum latencies and override deadlines are nonnegative. + * + * @hide + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + public static final long REJECT_NEGATIVE_DELAYS_AND_DEADLINES = 323349338L; + /** @hide */ @IntDef(prefix = { "NETWORK_TYPE_" }, value = { NETWORK_TYPE_NONE, @@ -692,14 +701,14 @@ public class JobInfo implements Parcelable { * @see JobInfo.Builder#setMinimumLatency(long) */ public long getMinLatencyMillis() { - return minLatencyMillis; + return Math.max(0, minLatencyMillis); } /** * @see JobInfo.Builder#setOverrideDeadline(long) */ public long getMaxExecutionDelayMillis() { - return maxExecutionDelayMillis; + return Math.max(0, maxExecutionDelayMillis); } /** @@ -818,7 +827,7 @@ public class JobInfo implements Parcelable { * @hide */ public boolean hasEarlyConstraint() { - return hasEarlyConstraint; + return hasEarlyConstraint && minLatencyMillis > 0; } /** @@ -827,7 +836,7 @@ public class JobInfo implements Parcelable { * @hide */ public boolean hasLateConstraint() { - return hasLateConstraint; + return hasLateConstraint && maxExecutionDelayMillis >= 0; } @Override @@ -1869,6 +1878,13 @@ public class JobInfo implements Parcelable { * Because it doesn't make sense setting this property on a periodic job, doing so will * throw an {@link java.lang.IllegalArgumentException} when * {@link android.app.job.JobInfo.Builder#build()} is called. + * + * Negative latencies also don't make sense for a job and are indicative of an error, + * so starting in Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, + * setting a negative deadline will result in + * {@link android.app.job.JobInfo.Builder#build()} throwing an + * {@link java.lang.IllegalArgumentException}. + * * @param minLatencyMillis Milliseconds before which this job will not be considered for * execution. * @see JobInfo#getMinLatencyMillis() @@ -1892,6 +1908,13 @@ public class JobInfo implements Parcelable { * throw an {@link java.lang.IllegalArgumentException} when * {@link android.app.job.JobInfo.Builder#build()} is called. * + * <p> + * Negative deadlines also don't make sense for a job and are indicative of an error, + * so starting in Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, + * setting a negative deadline will result in + * {@link android.app.job.JobInfo.Builder#build()} throwing an + * {@link java.lang.IllegalArgumentException}. + * * <p class="note"> * Since a job will run once the deadline has passed regardless of the status of other * constraints, setting a deadline of 0 (or a {@link #setMinimumLatency(long) delay} equal @@ -2189,13 +2212,15 @@ public class JobInfo implements Parcelable { public JobInfo build() { return build(Compatibility.isChangeEnabled(DISALLOW_DEADLINES_FOR_PREFETCH_JOBS), Compatibility.isChangeEnabled(REJECT_NEGATIVE_NETWORK_ESTIMATES), - Compatibility.isChangeEnabled(ENFORCE_MINIMUM_TIME_WINDOWS)); + Compatibility.isChangeEnabled(ENFORCE_MINIMUM_TIME_WINDOWS), + Compatibility.isChangeEnabled(REJECT_NEGATIVE_DELAYS_AND_DEADLINES)); } /** @hide */ public JobInfo build(boolean disallowPrefetchDeadlines, boolean rejectNegativeNetworkEstimates, - boolean enforceMinimumTimeWindows) { + boolean enforceMinimumTimeWindows, + boolean rejectNegativeDelaysAndDeadlines) { // This check doesn't need to be inside enforceValidity. It's an unnecessary legacy // check that would ideally be phased out instead. if (mBackoffPolicySet && (mConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) { @@ -2205,7 +2230,7 @@ public class JobInfo implements Parcelable { } JobInfo jobInfo = new JobInfo(this); jobInfo.enforceValidity(disallowPrefetchDeadlines, rejectNegativeNetworkEstimates, - enforceMinimumTimeWindows); + enforceMinimumTimeWindows, rejectNegativeDelaysAndDeadlines); return jobInfo; } @@ -2225,7 +2250,8 @@ public class JobInfo implements Parcelable { */ public final void enforceValidity(boolean disallowPrefetchDeadlines, boolean rejectNegativeNetworkEstimates, - boolean enforceMinimumTimeWindows) { + boolean enforceMinimumTimeWindows, + boolean rejectNegativeDelaysAndDeadlines) { // Check that network estimates require network type and are reasonable values. if ((networkDownloadBytes > 0 || networkUploadBytes > 0 || minimumNetworkChunkBytes > 0) && networkRequest == null) { @@ -2259,6 +2285,17 @@ public class JobInfo implements Parcelable { throw new IllegalArgumentException("Minimum chunk size must be positive"); } + if (rejectNegativeDelaysAndDeadlines) { + if (minLatencyMillis < 0) { + throw new IllegalArgumentException( + "Minimum latency is negative: " + minLatencyMillis); + } + if (maxExecutionDelayMillis < 0) { + throw new IllegalArgumentException( + "Override deadline is negative: " + maxExecutionDelayMillis); + } + } + final boolean hasDeadline = maxExecutionDelayMillis != 0L; // Check that a deadline was not set on a periodic job. if (isPeriodic) { 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 a83c099b764d..f819f15b430f 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -4850,7 +4850,7 @@ public class JobSchedulerService extends com.android.server.SystemService Slog.w(TAG, "Uid " + uid + " set bias on its job"); return new JobInfo.Builder(job) .setBias(JobInfo.BIAS_DEFAULT) - .build(false, false, false); + .build(false, false, false, false); } } @@ -4874,7 +4874,9 @@ public class JobSchedulerService extends com.android.server.SystemService JobInfo.DISALLOW_DEADLINES_FOR_PREFETCH_JOBS, callingUid), rejectNegativeNetworkEstimates, CompatChanges.isChangeEnabled( - JobInfo.ENFORCE_MINIMUM_TIME_WINDOWS, callingUid)); + JobInfo.ENFORCE_MINIMUM_TIME_WINDOWS, callingUid), + CompatChanges.isChangeEnabled( + JobInfo.REJECT_NEGATIVE_DELAYS_AND_DEADLINES, callingUid)); if ((job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0) { getContext().enforceCallingOrSelfPermission( android.Manifest.permission.CONNECTIVITY_INTERNAL, TAG); 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 53b14d616ecc..d8934d8f83b8 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java @@ -1495,7 +1495,7 @@ public final class JobStore { // return value), the deadline is dropped. Periodic jobs require all constraints // to be met, so there's no issue with their deadlines. // The same logic applies for other target SDK-based validation checks. - builtJob = jobBuilder.build(false, false, false); + builtJob = jobBuilder.build(false, false, false, false); } catch (Exception e) { Slog.w(TAG, "Unable to build job from XML, ignoring: " + jobBuilder.summarize(), e); return null; 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 6883d18cd937..aec464d0e820 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 @@ -241,6 +241,8 @@ public final class FlexibilityController extends StateController { private static final long MAX_TIME_WINDOW_MS = 24 * HOUR_IN_MILLIS; private final JobScoreBucket[] mScoreBuckets = new JobScoreBucket[NUM_SCORE_BUCKETS]; private int mScoreBucketIndex = 0; + private long mCachedScoreExpirationTimeElapsed; + private int mCachedScore; public void addScore(int add, long nowElapsed) { JobScoreBucket bucket = mScoreBuckets[mScoreBucketIndex]; @@ -248,10 +250,17 @@ public final class FlexibilityController extends StateController { bucket = new JobScoreBucket(); bucket.startTimeElapsed = nowElapsed; mScoreBuckets[mScoreBucketIndex] = bucket; + // Brand new bucket, there's nothing to remove from the score, + // so just update the expiration time if needed. + mCachedScoreExpirationTimeElapsed = Math.min(mCachedScoreExpirationTimeElapsed, + nowElapsed + MAX_TIME_WINDOW_MS); } else if (bucket.startTimeElapsed < nowElapsed - MAX_TIME_WINDOW_MS) { // The bucket is too old. bucket.reset(); bucket.startTimeElapsed = nowElapsed; + // Force a recalculation of the cached score instead of just updating the cached + // value and time in case there are multiple stale buckets. + mCachedScoreExpirationTimeElapsed = nowElapsed; } else if (bucket.startTimeElapsed < nowElapsed - MAX_TIME_WINDOW_MS / NUM_SCORE_BUCKETS) { // The current bucket's duration has completed. Move on to the next bucket. @@ -261,16 +270,26 @@ public final class FlexibilityController extends StateController { } bucket.score += add; + mCachedScore += add; } public int getScore(long nowElapsed) { + if (nowElapsed < mCachedScoreExpirationTimeElapsed) { + return mCachedScore; + } int score = 0; final long earliestElapsed = nowElapsed - MAX_TIME_WINDOW_MS; + long earliestValidBucketTimeElapsed = Long.MAX_VALUE; for (JobScoreBucket bucket : mScoreBuckets) { if (bucket != null && bucket.startTimeElapsed >= earliestElapsed) { score += bucket.score; + if (earliestValidBucketTimeElapsed > bucket.startTimeElapsed) { + earliestValidBucketTimeElapsed = bucket.startTimeElapsed; + } } } + mCachedScore = score; + mCachedScoreExpirationTimeElapsed = earliestValidBucketTimeElapsed + MAX_TIME_WINDOW_MS; return score; } @@ -378,10 +397,16 @@ public final class FlexibilityController extends StateController { @Override public void prepareForExecutionLocked(JobStatus jobStatus) { + if (jobStatus.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) { + // Don't include jobs for the TOP app in the score calculation. + return; + } // Use the job's requested priority to determine its score since that is what the developer // selected and it will be stable across job runs. - final int score = mFallbackFlexibilityDeadlineScores - .get(jobStatus.getJob().getPriority(), jobStatus.getJob().getPriority() / 100); + final int priority = jobStatus.getJob().getPriority(); + final int score = mFallbackFlexibilityDeadlineScores.get(priority, + FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES + .get(priority, priority / 100)); JobScoreTracker jobScoreTracker = mJobScoreTrackers.get(jobStatus.getSourceUid(), jobStatus.getSourcePackageName()); if (jobScoreTracker == null) { @@ -394,6 +419,10 @@ public final class FlexibilityController extends StateController { @Override public void unprepareFromExecutionLocked(JobStatus jobStatus) { + if (jobStatus.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) { + // Jobs for the TOP app are excluded from the score calculation. + return; + } // The job didn't actually start. Undo the score increase. JobScoreTracker jobScoreTracker = mJobScoreTrackers.get(jobStatus.getSourceUid(), jobStatus.getSourcePackageName()); @@ -401,8 +430,10 @@ public final class FlexibilityController extends StateController { Slog.e(TAG, "Unprepared a job that didn't result in a score change"); return; } - final int score = mFallbackFlexibilityDeadlineScores - .get(jobStatus.getJob().getPriority(), jobStatus.getJob().getPriority() / 100); + final int priority = jobStatus.getJob().getPriority(); + final int score = mFallbackFlexibilityDeadlineScores.get(priority, + FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES + .get(priority, priority / 100)); jobScoreTracker.addScore(-score, sElapsedRealtimeClock.millis()); } @@ -649,21 +680,24 @@ public final class FlexibilityController extends StateController { (long) Math.scalb(mRescheduledJobDeadline, js.getNumPreviousAttempts() - 2), mMaxRescheduledDeadline); } + + // Intentionally use the effective priority here. If a job's priority was effectively + // lowered, it will be less likely to run quickly given other policies in JobScheduler. + // Thus, there's no need to further delay the job based on flex policy. + final int jobPriority = js.getEffectivePriority(); + final int jobScore = + getScoreLocked(js.getSourceUid(), js.getSourcePackageName(), nowElapsed); + // Set an upper limit on the fallback deadline so that the delay doesn't become extreme. + final long fallbackDurationMs = Math.min(3 * mFallbackFlexibilityDeadlineMs, + mFallbackFlexibilityDeadlines.get(jobPriority, mFallbackFlexibilityDeadlineMs) + + mFallbackFlexibilityAdditionalScoreTimeFactors + .get(jobPriority, MINUTE_IN_MILLIS) * jobScore); + final long fallbackDeadlineMs = earliest + fallbackDurationMs; + if (js.getLatestRunTimeElapsed() == JobStatus.NO_LATEST_RUNTIME) { - // Intentionally use the effective priority here. If a job's priority was effectively - // lowered, it will be less likely to run quickly given other policies in JobScheduler. - // Thus, there's no need to further delay the job based on flex policy. - final int jobPriority = js.getEffectivePriority(); - final int jobScore = - getScoreLocked(js.getSourceUid(), js.getSourcePackageName(), nowElapsed); - // Set an upper limit on the fallback deadline so that the delay doesn't become extreme. - final long fallbackDeadlineMs = Math.min(3 * mFallbackFlexibilityDeadlineMs, - mFallbackFlexibilityDeadlines.get(jobPriority, mFallbackFlexibilityDeadlineMs) - + mFallbackFlexibilityAdditionalScoreTimeFactors - .get(jobPriority, MINUTE_IN_MILLIS) * jobScore); - return earliest + fallbackDeadlineMs; + return fallbackDeadlineMs; } - return js.getLatestRunTimeElapsed(); + return Math.max(fallbackDeadlineMs, js.getLatestRunTimeElapsed()); } @VisibleForTesting @@ -976,7 +1010,8 @@ public final class FlexibilityController extends StateController { // Something has gone horribly wrong. This has only occurred on incorrectly // configured tests, but add a check here for safety. Slog.wtf(TAG, "Got invalid latest when scheduling alarm." - + " Prefetch=" + js.getJob().isPrefetch()); + + " prefetch=" + js.getJob().isPrefetch() + + " periodic=" + js.getJob().isPeriodic()); // Since things have gone wrong, the safest and most reliable thing to do is // stop applying flex policy to the job. mFlexibilityTracker.setNumDroppedFlexibleConstraints(js, @@ -991,7 +1026,7 @@ public final class FlexibilityController extends StateController { if (DEBUG) { Slog.d(TAG, "scheduleDropNumConstraintsAlarm: " - + js.getSourcePackageName() + " " + js.getSourceUserId() + + js.toShortString() + " numApplied: " + js.getNumAppliedFlexibleConstraints() + " numRequired: " + js.getNumRequiredFlexibleConstraints() + " numSatisfied: " + Integer.bitCount( @@ -1199,11 +1234,11 @@ public final class FlexibilityController extends StateController { DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS .put(PRIORITY_MAX, 0); DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS - .put(PRIORITY_HIGH, 4 * MINUTE_IN_MILLIS); + .put(PRIORITY_HIGH, 3 * MINUTE_IN_MILLIS); DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS - .put(PRIORITY_DEFAULT, 3 * MINUTE_IN_MILLIS); + .put(PRIORITY_DEFAULT, 2 * MINUTE_IN_MILLIS); DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS - .put(PRIORITY_LOW, 2 * MINUTE_IN_MILLIS); + .put(PRIORITY_LOW, 1 * MINUTE_IN_MILLIS); DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS .put(PRIORITY_MIN, 1 * MINUTE_IN_MILLIS); DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS @@ -1220,7 +1255,7 @@ public final class FlexibilityController extends StateController { private static final long DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS = MINUTE_IN_MILLIS; private static final long DEFAULT_RESCHEDULED_JOB_DEADLINE_MS = HOUR_IN_MILLIS; - private static final long DEFAULT_MAX_RESCHEDULED_DEADLINE_MS = 5 * DAY_IN_MILLIS; + private static final long DEFAULT_MAX_RESCHEDULED_DEADLINE_MS = DAY_IN_MILLIS; @VisibleForTesting static final long DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS = 3 * DAY_IN_MILLIS; 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 a3a686fdc5c8..edd86e3454a5 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,8 +16,6 @@ package com.android.server.job.controllers; -import static android.text.format.DateUtils.HOUR_IN_MILLIS; - import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX; import static com.android.server.job.JobSchedulerService.NEVER_INDEX; @@ -430,9 +428,6 @@ public final class JobStatus { */ public static final int INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ = 1 << 2; - /** Minimum difference between start and end time to have flexible constraint */ - @VisibleForTesting - static final long MIN_WINDOW_FOR_FLEXIBILITY_MS = HOUR_IN_MILLIS; /** * Versatile, persistable flags for a job that's updated within the system server, * as opposed to {@link JobInfo#flags} that's set by callers. @@ -657,7 +652,7 @@ public final class JobStatus { .build()); // Don't perform validation checks at this point since we've already passed the // initial validation check. - job = builder.build(false, false, false); + job = builder.build(false, false, false, false); } this.job = job; @@ -708,14 +703,10 @@ public final class JobStatus { final boolean lacksSomeFlexibleConstraints = ((~requiredConstraints) & SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS) != 0 || mCanApplyTransportAffinities; - 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() - && satisfiesMinWindowException && (numFailures + numSystemStops) != 1 && lacksSomeFlexibleConstraints) { requiredConstraints |= CONSTRAINT_FLEXIBLE; diff --git a/cmds/uinput/README.md b/cmds/uinput/README.md index f1775864aca0..b6e4e0de8cf7 100644 --- a/cmds/uinput/README.md +++ b/cmds/uinput/README.md @@ -154,8 +154,7 @@ will be unregistered. There is no explicit command for unregistering a device. #### `delay` -Add a delay between the processing of commands. The delay will be timed from when the last delay -ended, rather than from the current time, to allow for more precise timings to be produced. +Add a delay to command processing | Field | Type | Description | |:-------------:|:-------------:|:-------------------------- | diff --git a/cmds/uinput/jni/com_android_commands_uinput_Device.cpp b/cmds/uinput/jni/com_android_commands_uinput_Device.cpp index bd61000186e5..a78a46504684 100644 --- a/cmds/uinput/jni/com_android_commands_uinput_Device.cpp +++ b/cmds/uinput/jni/com_android_commands_uinput_Device.cpp @@ -166,14 +166,14 @@ UinputDevice::~UinputDevice() { ::ioctl(mFd, UI_DEV_DESTROY); } -void UinputDevice::injectEvent(std::chrono::microseconds timestamp, uint16_t type, uint16_t code, - int32_t value) { +void UinputDevice::injectEvent(uint16_t type, uint16_t code, int32_t value) { struct input_event event = {}; event.type = type; event.code = code; event.value = value; - event.time.tv_sec = timestamp.count() / 1'000'000; - event.time.tv_usec = timestamp.count() % 1'000'000; + timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + TIMESPEC_TO_TIMEVAL(&event.time, &ts); if (::write(mFd, &event, sizeof(input_event)) < 0) { ALOGE("Could not write event %" PRIu16 " %" PRIu16 " with value %" PRId32 " : %s", type, @@ -268,12 +268,12 @@ static void closeUinputDevice(JNIEnv* /* env */, jclass /* clazz */, jlong ptr) } } -static void injectEvent(JNIEnv* /* env */, jclass /* clazz */, jlong ptr, jlong timestampMicros, - jint type, jint code, jint value) { +static void injectEvent(JNIEnv* /* env */, jclass /* clazz */, jlong ptr, jint type, jint code, + jint value) { uinput::UinputDevice* d = reinterpret_cast<uinput::UinputDevice*>(ptr); if (d != nullptr) { - d->injectEvent(std::chrono::microseconds(timestampMicros), static_cast<uint16_t>(type), - static_cast<uint16_t>(code), static_cast<int32_t>(value)); + d->injectEvent(static_cast<uint16_t>(type), static_cast<uint16_t>(code), + static_cast<int32_t>(value)); } else { ALOGE("Could not inject event, Device* is null!"); } @@ -330,7 +330,7 @@ static JNINativeMethod sMethods[] = { "(Ljava/lang/String;IIIIIILjava/lang/String;" "Lcom/android/commands/uinput/Device$DeviceCallback;)J", reinterpret_cast<void*>(openUinputDevice)}, - {"nativeInjectEvent", "(JJIII)V", reinterpret_cast<void*>(injectEvent)}, + {"nativeInjectEvent", "(JIII)V", reinterpret_cast<void*>(injectEvent)}, {"nativeConfigure", "(II[I)V", reinterpret_cast<void*>(configure)}, {"nativeSetAbsInfo", "(IILandroid/os/Parcel;)V", reinterpret_cast<void*>(setAbsInfo)}, {"nativeCloseUinputDevice", "(J)V", reinterpret_cast<void*>(closeUinputDevice)}, diff --git a/cmds/uinput/jni/com_android_commands_uinput_Device.h b/cmds/uinput/jni/com_android_commands_uinput_Device.h index 72c8647ae743..9769a75bd9ef 100644 --- a/cmds/uinput/jni/com_android_commands_uinput_Device.h +++ b/cmds/uinput/jni/com_android_commands_uinput_Device.h @@ -14,14 +14,13 @@ * limitations under the License. */ -#include <android-base/unique_fd.h> -#include <jni.h> -#include <linux/input.h> - -#include <chrono> #include <memory> #include <vector> +#include <jni.h> +#include <linux/input.h> + +#include <android-base/unique_fd.h> #include "src/com/android/commands/uinput/InputAbsInfo.h" namespace android { @@ -54,8 +53,7 @@ public: virtual ~UinputDevice(); - void injectEvent(std::chrono::microseconds timestamp, uint16_t type, uint16_t code, - int32_t value); + void injectEvent(uint16_t type, uint16_t code, int32_t value); int handleEvents(int events); private: diff --git a/cmds/uinput/src/com/android/commands/uinput/Device.java b/cmds/uinput/src/com/android/commands/uinput/Device.java index 76ab475adc0a..25d3a341bf6e 100644 --- a/cmds/uinput/src/com/android/commands/uinput/Device.java +++ b/cmds/uinput/src/com/android/commands/uinput/Device.java @@ -55,7 +55,7 @@ public class Device { private final SparseArray<InputAbsInfo> mAbsInfo; private final OutputStream mOutputStream; private final Object mCond = new Object(); - private long mTimeToSendNanos; + private long mTimeToSend; static { System.loadLibrary("uinputcommand_jni"); @@ -65,8 +65,7 @@ public class Device { int productId, int versionId, int bus, int ffEffectsMax, String port, DeviceCallback callback); private static native void nativeCloseUinputDevice(long ptr); - private static native void nativeInjectEvent(long ptr, long timestampMicros, int type, int code, - int value); + private static native void nativeInjectEvent(long ptr, int type, int code, int value); private static native void nativeConfigure(int handle, int code, int[] configs); private static native void nativeSetAbsInfo(int handle, int axisCode, Parcel axisParcel); private static native int nativeGetEvdevEventTypeByLabel(String label); @@ -102,54 +101,27 @@ public class Device { } mHandler.obtainMessage(MSG_OPEN_UINPUT_DEVICE, args).sendToTarget(); - mTimeToSendNanos = SystemClock.uptimeNanos(); - } - - private long getTimeToSendMillis() { - // Since we can only specify delays in milliseconds but evemu timestamps are in - // microseconds, we have to round up the delays to avoid setting event timestamps - // which are in the future (which the kernel would silently reject and replace with - // the current time). - // - // This should be the same as (long) Math.ceil(mTimeToSendNanos / 1_000_000.0), except - // without the precision loss that comes from converting from long to double and back. - return mTimeToSendNanos / 1_000_000 + ((mTimeToSendNanos % 1_000_000 > 0) ? 1 : 0); + mTimeToSend = SystemClock.uptimeMillis(); } /** * Inject uinput events to device * * @param events Array of raw uinput events. - * @param offsetMicros The difference in microseconds between the timestamps of the previous - * batch of events injected and this batch. If set to -1, the current - * timestamp will be used. */ - public void injectEvent(int[] events, long offsetMicros) { + public void injectEvent(int[] events) { // if two messages are sent at identical time, they will be processed in order received - SomeArgs args = SomeArgs.obtain(); - args.arg1 = events; - args.argl1 = offsetMicros; - args.argl2 = mTimeToSendNanos; - Message msg = mHandler.obtainMessage(MSG_INJECT_EVENT, args); - mHandler.sendMessageAtTime(msg, getTimeToSendMillis()); + Message msg = mHandler.obtainMessage(MSG_INJECT_EVENT, events); + mHandler.sendMessageAtTime(msg, mTimeToSend); } /** - * Delay subsequent device activity by the specified amount of time. + * Impose a delay to the device for execution. * - * <p>Note that although the delay is specified in nanoseconds, due to limitations of {@link - * Handler}'s API, scheduling only occurs with millisecond precision. When scheduling an - * injection or sync, the time at which it is scheduled will be rounded up to the nearest - * millisecond. While this means that a particular injection cannot be scheduled precisely, - * rounding errors will not accumulate over time. For example, if five injections are scheduled - * with a delay of 1,200,000ns before each one, the total delay will be 6ms, as opposed to the - * 10ms it would have been if each individual delay had been rounded up (as {@link EvemuParser} - * would otherwise have to do to avoid sending timestamps that are in the future). - * - * @param delayNanos Time to delay in unit of nanoseconds. + * @param delay Time to delay in unit of milliseconds. */ - public void addDelayNanos(long delayNanos) { - mTimeToSendNanos += delayNanos; + public void addDelay(int delay) { + mTimeToSend = Math.max(SystemClock.uptimeMillis(), mTimeToSend) + delay; } /** @@ -159,8 +131,7 @@ public class Device { * @param syncToken The token for this sync command. */ public void syncEvent(String syncToken) { - mHandler.sendMessageAtTime( - mHandler.obtainMessage(MSG_SYNC_EVENT, syncToken), getTimeToSendMillis()); + mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_SYNC_EVENT, syncToken), mTimeToSend); } /** @@ -169,8 +140,7 @@ public class Device { */ public void close() { Message msg = mHandler.obtainMessage(MSG_CLOSE_UINPUT_DEVICE); - mHandler.sendMessageAtTime( - msg, Math.max(SystemClock.uptimeMillis(), getTimeToSendMillis()) + 1); + mHandler.sendMessageAtTime(msg, Math.max(SystemClock.uptimeMillis(), mTimeToSend) + 1); try { synchronized (mCond) { mCond.wait(); @@ -181,7 +151,6 @@ public class Device { private class DeviceHandler extends Handler { private long mPtr; - private long mLastInjectTimestampMicros = -1; private int mBarrierToken; DeviceHandler(Looper looper) { @@ -191,7 +160,7 @@ public class Device { @Override public void handleMessage(Message msg) { switch (msg.what) { - case MSG_OPEN_UINPUT_DEVICE: { + case MSG_OPEN_UINPUT_DEVICE: SomeArgs args = (SomeArgs) msg.obj; String name = (String) args.arg1; mPtr = nativeOpenUinputDevice(name, args.argi1 /* id */, @@ -208,44 +177,15 @@ public class Device { } args.recycle(); break; - } - case MSG_INJECT_EVENT: { - SomeArgs args = (SomeArgs) msg.obj; - if (mPtr == 0) { - args.recycle(); - break; - } - long offsetMicros = args.argl1; - if (mLastInjectTimestampMicros == -1 || offsetMicros == -1) { - // There's often a delay of a few milliseconds between the time specified to - // Handler.sendMessageAtTime and the handler actually being called, due to - // the way threads are scheduled. We don't take this into account when - // calling addDelayNanos between the first batch of event injections (when - // we set the "base timestamp" from which all others will be offset) and the - // second batch, meaning that the actual time between the handler calls for - // those batches may be less than the offset between their timestamps. When - // that happens, we would pass a timestamp for the second batch that's - // actually in the future. The kernel's uinput API rejects timestamps that - // are in the future and uses the current time instead, making the reported - // timestamps inconsistent with the recording we're replaying. - // - // To prevent this, we need to use the time we scheduled this first batch - // for (in microseconds, to avoid potential rounding up from - // getTimeToSendMillis), rather than the actual current time. - mLastInjectTimestampMicros = args.argl2 / 1000; - } else { - mLastInjectTimestampMicros += offsetMicros; - } - - int[] events = (int[]) args.arg1; - for (int pos = 0; pos + 2 < events.length; pos += 3) { - nativeInjectEvent(mPtr, mLastInjectTimestampMicros, events[pos], - events[pos + 1], events[pos + 2]); + case MSG_INJECT_EVENT: + if (mPtr != 0) { + int[] events = (int[]) msg.obj; + for (int pos = 0; pos + 2 < events.length; pos += 3) { + nativeInjectEvent(mPtr, events[pos], events[pos + 1], events[pos + 2]); + } } - args.recycle(); break; - } - case MSG_CLOSE_UINPUT_DEVICE: { + case MSG_CLOSE_UINPUT_DEVICE: if (mPtr != 0) { nativeCloseUinputDevice(mPtr); getLooper().quitSafely(); @@ -258,14 +198,11 @@ public class Device { mCond.notify(); } break; - } - case MSG_SYNC_EVENT: { + case MSG_SYNC_EVENT: handleSyncEvent((String) msg.obj); break; - } - default: { + default: throw new IllegalArgumentException("Unknown device message"); - } } } diff --git a/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java index da991624e685..7652f2403f6e 100644 --- a/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java +++ b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java @@ -44,7 +44,7 @@ public class EvemuParser implements EventParser { * recordings, this will always be the same. */ private static final int DEVICE_ID = 1; - private static final int REGISTRATION_DELAY_NANOS = 500_000_000; + private static final int REGISTRATION_DELAY_MILLIS = 500; private static class CommentAwareReader { private final LineNumberReader mReader; @@ -152,7 +152,7 @@ public class EvemuParser implements EventParser { final Event.Builder delayEb = new Event.Builder(); delayEb.setId(DEVICE_ID); delayEb.setCommand(Event.Command.DELAY); - delayEb.setDurationNanos(REGISTRATION_DELAY_NANOS); + delayEb.setDurationMillis(REGISTRATION_DELAY_MILLIS); mQueuedEvents.add(delayEb.build()); } @@ -175,6 +175,7 @@ public class EvemuParser implements EventParser { throw new ParsingException( "Invalid timestamp '" + parts[0] + "' (should contain a single '.')", mReader); } + // TODO(b/310958309): use timeMicros to set the timestamp on the event being sent. final long timeMicros = parseLong(timeParts[0], 10) * 1_000_000 + parseInt(timeParts[1], 10); final Event.Builder eb = new Event.Builder(); @@ -191,18 +192,21 @@ public class EvemuParser implements EventParser { return eb.build(); } else { final long delayMicros = timeMicros - mLastEventTimeMicros; - eb.setTimestampOffsetMicros(delayMicros); - if (delayMicros == 0) { + // The shortest delay supported by Handler.sendMessageAtTime (used for timings by the + // Device class) is 1ms, so ignore time differences smaller than that. + if (delayMicros < 1000) { + mLastEventTimeMicros = timeMicros; return eb.build(); + } else { + // Send a delay now, and queue the actual event for the next call. + mQueuedEvents.add(eb.build()); + mLastEventTimeMicros = timeMicros; + final Event.Builder delayEb = new Event.Builder(); + delayEb.setId(DEVICE_ID); + delayEb.setCommand(Event.Command.DELAY); + delayEb.setDurationMillis((int) (delayMicros / 1000)); + return delayEb.build(); } - // Send a delay now, and queue the actual event for the next call. - mQueuedEvents.add(eb.build()); - mLastEventTimeMicros = timeMicros; - final Event.Builder delayEb = new Event.Builder(); - delayEb.setId(DEVICE_ID); - delayEb.setCommand(Event.Command.DELAY); - delayEb.setDurationNanos(delayMicros * 1000); - return delayEb.build(); } } diff --git a/cmds/uinput/src/com/android/commands/uinput/Event.java b/cmds/uinput/src/com/android/commands/uinput/Event.java index 9e7ee0937efe..0f16a27aac1d 100644 --- a/cmds/uinput/src/com/android/commands/uinput/Event.java +++ b/cmds/uinput/src/com/android/commands/uinput/Event.java @@ -99,9 +99,8 @@ public class Event { private int mVersionId; private int mBusId; private int[] mInjections; - private long mTimestampOffsetMicros = -1; private SparseArray<int[]> mConfiguration; - private long mDurationNanos; + private int mDurationMillis; private int mFfEffectsMax = 0; private String mInputPort; private SparseArray<InputAbsInfo> mAbsInfo; @@ -140,28 +139,19 @@ public class Event { } /** - * Returns the number of microseconds that should be added to the previous {@code INJECT} - * event's timestamp to produce the timestamp for this {@code INJECT} event. A value of -1 - * indicates that the current timestamp should be used instead. - */ - public long getTimestampOffsetMicros() { - return mTimestampOffsetMicros; - } - - /** * Returns a {@link SparseArray} describing the event codes that should be registered for the * device. The keys are uinput ioctl codes (such as those returned from {@link * UinputControlCode#getValue()}, while the values are arrays of event codes to be enabled with * those ioctls. For example, key 101 (corresponding to {@link UinputControlCode#UI_SET_KEYBIT}) - * could have values 0x110 ({@code BTN_LEFT}), 0x111 ({@code BTN_RIGHT}), and 0x112 + * could have values 0x110 ({@code BTN_LEFT}, 0x111 ({@code BTN_RIGHT}), and 0x112 * ({@code BTN_MIDDLE}). */ public SparseArray<int[]> getConfiguration() { return mConfiguration; } - public long getDurationNanos() { - return mDurationNanos; + public int getDurationMillis() { + return mDurationMillis; } public int getFfEffectsMax() { @@ -192,7 +182,7 @@ public class Event { + ", busId=" + mBusId + ", events=" + Arrays.toString(mInjections) + ", configuration=" + mConfiguration - + ", duration=" + mDurationNanos + "ns" + + ", duration=" + mDurationMillis + "ms" + ", ff_effects_max=" + mFfEffectsMax + ", port=" + mInputPort + "}"; @@ -221,10 +211,6 @@ public class Event { mEvent.mInjections = events; } - public void setTimestampOffsetMicros(long offsetMicros) { - mEvent.mTimestampOffsetMicros = offsetMicros; - } - /** * Sets the event codes that should be registered with a {@code register} command. * @@ -251,8 +237,8 @@ public class Event { mEvent.mBusId = busId; } - public void setDurationNanos(long durationNanos) { - mEvent.mDurationNanos = durationNanos; + public void setDurationMillis(int durationMillis) { + mEvent.mDurationMillis = durationMillis; } public void setFfEffectsMax(int ffEffectsMax) { @@ -285,7 +271,7 @@ public class Event { } } case DELAY -> { - if (mEvent.mDurationNanos <= 0) { + if (mEvent.mDurationMillis <= 0) { throw new IllegalStateException("Delay has missing or invalid duration"); } } diff --git a/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java b/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java index 6994f0cb0e4b..ed3ff33f7e52 100644 --- a/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java +++ b/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java @@ -71,8 +71,7 @@ public class JsonStyleParser implements EventParser { case "configuration" -> eb.setConfiguration(readConfiguration()); case "ff_effects_max" -> eb.setFfEffectsMax(readInt()); case "abs_info" -> eb.setAbsInfo(readAbsInfoArray()); - // Duration is specified in milliseconds in the JSON-style format. - case "duration" -> eb.setDurationNanos(readInt() * 1_000_000L); + case "duration" -> eb.setDurationMillis(readInt()); case "port" -> eb.setInputPort(mReader.nextString()); case "syncToken" -> eb.setSyncToken(mReader.nextString()); default -> mReader.skipValue(); diff --git a/cmds/uinput/src/com/android/commands/uinput/Uinput.java b/cmds/uinput/src/com/android/commands/uinput/Uinput.java index 760e981c8465..04df27987d58 100644 --- a/cmds/uinput/src/com/android/commands/uinput/Uinput.java +++ b/cmds/uinput/src/com/android/commands/uinput/Uinput.java @@ -134,8 +134,8 @@ public class Uinput { switch (Objects.requireNonNull(e.getCommand())) { case REGISTER -> error("Device id=" + e.getId() + " is already registered. Ignoring event."); - case INJECT -> d.injectEvent(e.getInjections(), e.getTimestampOffsetMicros()); - case DELAY -> d.addDelayNanos(e.getDurationNanos()); + case INJECT -> d.injectEvent(e.getInjections()); + case DELAY -> d.addDelay(e.getDurationMillis()); case SYNC -> d.syncEvent(e.getSyncToken()); } } diff --git a/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java b/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java index 4dc4b68eba60..a05cc676ddfa 100644 --- a/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java +++ b/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java @@ -183,22 +183,16 @@ public class EvemuParserTest { } private void assertInjectEvent(Event event, int eventType, int eventCode, int value) { - assertInjectEvent(event, eventType, eventCode, value, 0); - } - - private void assertInjectEvent(Event event, int eventType, int eventCode, int value, - long timestampOffsetMicros) { assertThat(event).isNotNull(); assertThat(event.getCommand()).isEqualTo(Event.Command.INJECT); assertThat(event.getInjections()).asList() .containsExactly(eventType, eventCode, value).inOrder(); - assertThat(event.getTimestampOffsetMicros()).isEqualTo(timestampOffsetMicros); } - private void assertDelayEvent(Event event, int durationNanos) { + private void assertDelayEvent(Event event, int durationMillis) { assertThat(event).isNotNull(); assertThat(event.getCommand()).isEqualTo(Event.Command.DELAY); - assertThat(event.getDurationNanos()).isEqualTo(durationNanos); + assertThat(event.getDurationMillis()).isEqualTo(durationMillis); } @Test @@ -213,7 +207,7 @@ public class EvemuParserTest { EvemuParser parser = new EvemuParser(reader); assertThat(parser.getNextEvent().getCommand()).isEqualTo(Event.Command.REGISTER); assertThat(parser.getNextEvent().getCommand()).isEqualTo(Event.Command.DELAY); - assertInjectEvent(parser.getNextEvent(), 0x2, 0x0, 1, -1); + assertInjectEvent(parser.getNextEvent(), 0x2, 0x0, 1); assertInjectEvent(parser.getNextEvent(), 0x2, 0x1, -2); assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0); } @@ -234,17 +228,17 @@ public class EvemuParserTest { assertThat(parser.getNextEvent().getCommand()).isEqualTo(Event.Command.REGISTER); assertThat(parser.getNextEvent().getCommand()).isEqualTo(Event.Command.DELAY); - assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1, -1); + assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1); assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0); - assertDelayEvent(parser.getNextEvent(), 10_000_000); + assertDelayEvent(parser.getNextEvent(), 10); - assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 0, 10_000); + assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 0); assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0); - assertDelayEvent(parser.getNextEvent(), 1_000_000_000); + assertDelayEvent(parser.getNextEvent(), 1000); - assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1, 1_000_000); + assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1); assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0); } @@ -455,7 +449,7 @@ public class EvemuParserTest { assertThat(regEvent.getBus()).isEqualTo(0x001d); assertThat(regEvent.getVendorId()).isEqualTo(0x6cb); assertThat(regEvent.getProductId()).isEqualTo(0x0000); - // TODO(b/302297266): check version ID once it's supported + assertThat(regEvent.getVersionId()).isEqualTo(0x0000); assertThat(regEvent.getConfiguration().get(UinputControlCode.UI_SET_PROPBIT.getValue())) .asList().containsExactly(0, 2); @@ -483,7 +477,7 @@ public class EvemuParserTest { assertThat(parser.getNextEvent().getCommand()).isEqualTo(Event.Command.DELAY); - assertInjectEvent(parser.getNextEvent(), 0x3, 0x39, 0, -1); + assertInjectEvent(parser.getNextEvent(), 0x3, 0x39, 0); assertInjectEvent(parser.getNextEvent(), 0x3, 0x35, 891); assertInjectEvent(parser.getNextEvent(), 0x3, 0x36, 333); assertInjectEvent(parser.getNextEvent(), 0x3, 0x3a, 56); @@ -496,8 +490,8 @@ public class EvemuParserTest { assertInjectEvent(parser.getNextEvent(), 0x3, 0x18, 56); assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0); - assertDelayEvent(parser.getNextEvent(), 6_080_000); + assertDelayEvent(parser.getNextEvent(), 6); - assertInjectEvent(parser.getNextEvent(), 0x3, 0x0035, 888, 6_080); + assertInjectEvent(parser.getNextEvent(), 0x3, 0x0035, 888); } } diff --git a/core/api/current.txt b/core/api/current.txt index 0e42f807c34b..18153a5d4d34 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -7840,6 +7840,7 @@ package android.app.admin { method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.app.admin.DeviceAdminInfo> CREATOR; field public static final int HEADLESS_DEVICE_OWNER_MODE_AFFILIATED = 1; // 0x1 + field @FlaggedApi("android.app.admin.flags.headless_device_owner_single_user_enabled") public static final int HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER = 2; // 0x2 field public static final int HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED = 0; // 0x0 field public static final int USES_ENCRYPTED_STORAGE = 7; // 0x7 field public static final int USES_POLICY_DISABLE_CAMERA = 8; // 0x8 @@ -25869,6 +25870,9 @@ package android.media.metrics { method public int describeContents(); method public int getErrorCode(); method public int getFinalState(); + method @NonNull public java.util.List<android.media.metrics.MediaItemInfo> getInputMediaItemInfos(); + method public long getOperationTypes(); + method @Nullable public android.media.metrics.MediaItemInfo getOutputMediaItemInfo(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.media.metrics.EditingEndedEvent> CREATOR; field public static final int ERROR_CODE_AUDIO_PROCESSING_FAILED = 18; // 0x12 @@ -25893,14 +25897,25 @@ package android.media.metrics { field public static final int FINAL_STATE_CANCELED = 2; // 0x2 field public static final int FINAL_STATE_ERROR = 3; // 0x3 field public static final int FINAL_STATE_SUCCEEDED = 1; // 0x1 + field public static final long OPERATION_TYPE_AUDIO_EDIT = 8L; // 0x8L + field public static final long OPERATION_TYPE_AUDIO_TRANSCODE = 2L; // 0x2L + field public static final long OPERATION_TYPE_AUDIO_TRANSMUX = 32L; // 0x20L + field public static final long OPERATION_TYPE_PAUSED = 64L; // 0x40L + field public static final long OPERATION_TYPE_RESUMED = 128L; // 0x80L + field public static final long OPERATION_TYPE_VIDEO_EDIT = 4L; // 0x4L + field public static final long OPERATION_TYPE_VIDEO_TRANSCODE = 1L; // 0x1L + field public static final long OPERATION_TYPE_VIDEO_TRANSMUX = 16L; // 0x10L field public static final int TIME_SINCE_CREATED_UNKNOWN = -1; // 0xffffffff } @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public static final class EditingEndedEvent.Builder { ctor public EditingEndedEvent.Builder(int); + method @NonNull public android.media.metrics.EditingEndedEvent.Builder addInputMediaItemInfo(@NonNull android.media.metrics.MediaItemInfo); + method @NonNull public android.media.metrics.EditingEndedEvent.Builder addOperationType(long); method @NonNull public android.media.metrics.EditingEndedEvent build(); method @NonNull public android.media.metrics.EditingEndedEvent.Builder setErrorCode(int); method @NonNull public android.media.metrics.EditingEndedEvent.Builder setMetricsBundle(@NonNull android.os.Bundle); + method @NonNull public android.media.metrics.EditingEndedEvent.Builder setOutputMediaItemInfo(@NonNull android.media.metrics.MediaItemInfo); method @NonNull public android.media.metrics.EditingEndedEvent.Builder setTimeSinceCreatedMillis(@IntRange(from=android.media.metrics.EditingEndedEvent.TIME_SINCE_CREATED_UNKNOWN) long); } @@ -25920,6 +25935,65 @@ package android.media.metrics { field @NonNull public static final android.media.metrics.LogSessionId LOG_SESSION_ID_NONE; } + @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public final class MediaItemInfo implements android.os.Parcelable { + method public int describeContents(); + method public int getAudioChannelCount(); + method public long getAudioSampleCount(); + method public int getAudioSampleRateHz(); + method public long getClipDurationMillis(); + method @NonNull public java.util.List<java.lang.String> getCodecNames(); + method @Nullable public String getContainerMimeType(); + method public long getDataTypes(); + method public long getDurationMillis(); + method @NonNull public java.util.List<java.lang.String> getSampleMimeTypes(); + method public int getSourceType(); + method public int getVideoDataSpace(); + method public float getVideoFrameRate(); + method public long getVideoSampleCount(); + method @NonNull public android.util.Size getVideoSize(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.media.metrics.MediaItemInfo> CREATOR; + field public static final long DATA_TYPE_AUDIO = 4L; // 0x4L + field public static final long DATA_TYPE_CUE_POINTS = 128L; // 0x80L + field public static final long DATA_TYPE_DEPTH = 16L; // 0x10L + field public static final long DATA_TYPE_GAIN_MAP = 32L; // 0x20L + field public static final long DATA_TYPE_GAPLESS = 256L; // 0x100L + field public static final long DATA_TYPE_HIGH_DYNAMIC_RANGE_VIDEO = 1024L; // 0x400L + field public static final long DATA_TYPE_HIGH_FRAME_RATE = 64L; // 0x40L + field public static final long DATA_TYPE_IMAGE = 1L; // 0x1L + field public static final long DATA_TYPE_METADATA = 8L; // 0x8L + field public static final long DATA_TYPE_SPATIAL_AUDIO = 512L; // 0x200L + field public static final long DATA_TYPE_VIDEO = 2L; // 0x2L + field public static final int SOURCE_TYPE_CAMERA = 2; // 0x2 + field public static final int SOURCE_TYPE_EDITING_SESSION = 3; // 0x3 + field public static final int SOURCE_TYPE_GALLERY = 1; // 0x1 + field public static final int SOURCE_TYPE_GENERATED = 7; // 0x7 + field public static final int SOURCE_TYPE_LOCAL_FILE = 4; // 0x4 + field public static final int SOURCE_TYPE_REMOTE_FILE = 5; // 0x5 + field public static final int SOURCE_TYPE_REMOTE_LIVE_STREAM = 6; // 0x6 + field public static final int SOURCE_TYPE_UNSPECIFIED = 0; // 0x0 + field public static final int VALUE_UNSPECIFIED = -1; // 0xffffffff + } + + @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public static final class MediaItemInfo.Builder { + ctor public MediaItemInfo.Builder(); + method @NonNull public android.media.metrics.MediaItemInfo.Builder addCodecName(@NonNull String); + method @NonNull public android.media.metrics.MediaItemInfo.Builder addDataType(long); + method @NonNull public android.media.metrics.MediaItemInfo.Builder addSampleMimeType(@NonNull String); + method @NonNull public android.media.metrics.MediaItemInfo build(); + method @NonNull public android.media.metrics.MediaItemInfo.Builder setAudioChannelCount(@IntRange(from=0) int); + method @NonNull public android.media.metrics.MediaItemInfo.Builder setAudioSampleCount(@IntRange(from=0) long); + method @NonNull public android.media.metrics.MediaItemInfo.Builder setAudioSampleRateHz(@IntRange(from=0) int); + method @NonNull public android.media.metrics.MediaItemInfo.Builder setClipDurationMillis(long); + method @NonNull public android.media.metrics.MediaItemInfo.Builder setContainerMimeType(@NonNull String); + method @NonNull public android.media.metrics.MediaItemInfo.Builder setDurationMillis(long); + method @NonNull public android.media.metrics.MediaItemInfo.Builder setSourceType(int); + method @NonNull public android.media.metrics.MediaItemInfo.Builder setVideoDataSpace(int); + method @NonNull public android.media.metrics.MediaItemInfo.Builder setVideoFrameRate(@FloatRange(from=0) float); + method @NonNull public android.media.metrics.MediaItemInfo.Builder setVideoSampleCount(@IntRange(from=0) long); + method @NonNull public android.media.metrics.MediaItemInfo.Builder setVideoSize(@NonNull android.util.Size); + } + public final class MediaMetricsManager { method @NonNull public android.media.metrics.BundleSession createBundleSession(); method @NonNull public android.media.metrics.EditingSession createEditingSession(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index ecbdeaa5fdb8..748845284cbc 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -1389,6 +1389,7 @@ package android.app.admin { field public static final int STATUS_DEVICE_ADMIN_NOT_SUPPORTED = 13; // 0xd field public static final int STATUS_HAS_DEVICE_OWNER = 1; // 0x1 field public static final int STATUS_HAS_PAIRED = 8; // 0x8 + field @FlaggedApi("android.app.admin.flags.headless_device_owner_single_user_enabled") public static final int STATUS_HEADLESS_ONLY_SYSTEM_USER = 17; // 0x11 field public static final int STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED = 16; // 0x10 field public static final int STATUS_MANAGED_USERS_NOT_SUPPORTED = 9; // 0x9 field public static final int STATUS_NONSYSTEM_USER_EXISTS = 5; // 0x5 diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java index 14462b853c02..7d5d5c162271 100644 --- a/core/java/android/app/admin/DeviceAdminInfo.java +++ b/core/java/android/app/admin/DeviceAdminInfo.java @@ -16,8 +16,10 @@ package android.app.admin; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; +import android.app.admin.flags.Flags; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; @@ -176,7 +178,18 @@ public final class DeviceAdminInfo implements Parcelable { */ public static final int HEADLESS_DEVICE_OWNER_MODE_AFFILIATED = 1; - @IntDef({HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED, HEADLESS_DEVICE_OWNER_MODE_AFFILIATED}) + /** + * Value for {@link #getHeadlessDeviceOwnerMode} which indicates that this DPC should be + * provisioned into the first secondary user when on a Headless System User Mode device. + * + * <p>This mode only allows a single secondary user on the device blocking the creation of + * additional secondary users. + */ + @FlaggedApi(Flags.FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED) + public static final int HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER = 2; + + @IntDef({HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED, HEADLESS_DEVICE_OWNER_MODE_AFFILIATED, + HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER}) @Retention(RetentionPolicy.SOURCE) private @interface HeadlessDeviceOwnerMode {} @@ -373,6 +386,8 @@ public final class DeviceAdminInfo implements Parcelable { mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED; } else if (deviceOwnerModeStringValue.equalsIgnoreCase("affiliated")) { mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_AFFILIATED; + } else if (deviceOwnerModeStringValue.equalsIgnoreCase("single_user")) { + mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER; } else { throw new XmlPullParserException("headless-system-user mode must be valid"); } diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index c8762c69eeca..c649e622dfc5 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -84,6 +84,7 @@ import android.app.Activity; import android.app.IServiceConnection; import android.app.KeyguardManager; import android.app.admin.SecurityLog.SecurityEvent; +import android.app.admin.flags.Flags; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; @@ -2863,6 +2864,19 @@ public class DevicePolicyManager { public static final int STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED = 16; /** + * Result code for {@link #checkProvisioningPrecondition}. + * + * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE} when provisioning a DPC into the + * {@link DeviceAdminInfo#HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER} mode but only the system + * user exists on the device. + * + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED) + public static final int STATUS_HEADLESS_ONLY_SYSTEM_USER = 17; + + /** * Result codes for {@link #checkProvisioningPrecondition} indicating all the provisioning pre * conditions. * @@ -2876,7 +2890,7 @@ public class DevicePolicyManager { STATUS_CANNOT_ADD_MANAGED_PROFILE, STATUS_DEVICE_ADMIN_NOT_SUPPORTED, STATUS_SPLIT_SYSTEM_USER_DEVICE_SYSTEM_USER, STATUS_PROVISIONING_NOT_ALLOWED_FOR_NON_DEVELOPER_USERS, - STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED + STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED, STATUS_HEADLESS_ONLY_SYSTEM_USER }) public @interface ProvisioningPrecondition {} @@ -9178,9 +9192,11 @@ public class DevicePolicyManager { * <p>Calling this after the setup phase of the device owner user has completed is allowed only * if the caller is the {@link Process#SHELL_UID Shell UID}, and there are no additional users * (except when the device runs on headless system user mode, in which case it could have exact - * one extra user, which is the current user - the device owner will be set in the - * {@link UserHandle#SYSTEM system} user and a profile owner will be set in the current user) - * and no accounts. + * one extra user, which is the current user. + * + * <p>On a headless devices, if it is in affiliated mode the device owner will be set in the + * {@link UserHandle#SYSTEM system} user. If the device is in single user mode, the device owner + * will be set in the first secondary user. * * @param who the component name to be registered as device owner. * @param userId ID of the user on which the device owner runs. @@ -11371,7 +11387,9 @@ public class DevicePolicyManager { * @see UserHandle * @return the {@link android.os.UserHandle} object for the created user, or {@code null} if the * user could not be created. - * @throws SecurityException if {@code admin} is not a device owner. + * @throws SecurityException if headless device is in + * {@link DeviceAdminInfo#HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER} mode. + * @throws SecurityException if {@code admin} is not a device owner * @throws UserOperationException if the user could not be created and the calling app is * targeting {@link android.os.Build.VERSION_CODES#P} and running on * {@link android.os.Build.VERSION_CODES#P}. @@ -16612,7 +16630,10 @@ public class DevicePolicyManager { * before calling this method. * * <p>Holders of {@link android.Manifest.permission#PROVISION_DEMO_DEVICE} can call this API - * only if {@link FullyManagedDeviceProvisioningParams#isDemoDevice()} is {@code true}.</p> + * only if {@link FullyManagedDeviceProvisioningParams#isDemoDevice()} is {@code true}. + * + * <p>If headless device is in {@link DeviceAdminInfo#HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER} + * mode then it sets the device owner on the first secondary user.</p> * * @param provisioningParams Params required to provision a fully managed device, * see {@link FullyManagedDeviceProvisioningParams}. diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 6cc8af8c4b51..3c98ef9cfb00 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -76,3 +76,10 @@ flag { description: "Enable APIs to provision and manage eSIMs" bug: "295301164" } + +flag { + name: "headless_device_owner_single_user_enabled" + namespace: "enterprise" + description: "Add Headless DO support." + bug: "289515470" +} diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index c14fe572aba1..dbe7196d8e6a 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -212,6 +212,11 @@ <integer name="config_emergency_call_wait_for_connection_timeout_millis">20000</integer> <java-symbol type="integer" name="config_emergency_call_wait_for_connection_timeout_millis" /> + <!-- Indicates the data limit in bytes that can be used for bootstrap sim until factory reset. + -1 means unlimited. --> + <integer name="config_esim_bootstrap_data_limit_bytes">-1</integer> + <java-symbol type="integer" name="config_esim_bootstrap_data_limit_bytes" /> + <!-- Telephony config for the PLMNs of all satellite providers. This is used by satellite modem to identify providers that should be ignored if the carrier config carrier_supported_satellite_services_per_provider_bundle does not support them. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index e5045aea189b..70b2f211d943 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -2959,13 +2959,17 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } public void goToFullscreenFromSplit() { - boolean leftOrTop; - if (mSideStage.isFocused()) { - leftOrTop = (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT); + // If main stage is focused, toEnd = true if + // mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT. Otherwise toEnd = false + // If side stage is focused, toEnd = true if + // mSideStagePosition = SPLIT_POSITION_TOP_OR_LEFT. Otherwise toEnd = false + final boolean toEnd; + if (mMainStage.isFocused()) { + toEnd = (mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT); } else { - leftOrTop = (mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT); + toEnd = (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT); } - mSplitLayout.flingDividerToDismiss(!leftOrTop, EXIT_REASON_FULLSCREEN_SHORTCUT); + mSplitLayout.flingDividerToDismiss(toEnd, EXIT_REASON_FULLSCREEN_SHORTCUT); } /** Move the specified task to fullscreen, regardless of focus state. */ diff --git a/media/java/android/media/metrics/EditingEndedEvent.java b/media/java/android/media/metrics/EditingEndedEvent.java index 5ed8d40af63e..f1c5c9d0c748 100644 --- a/media/java/android/media/metrics/EditingEndedEvent.java +++ b/media/java/android/media/metrics/EditingEndedEvent.java @@ -20,6 +20,7 @@ import static com.android.media.editing.flags.Flags.FLAG_ADD_MEDIA_METRICS_EDITI import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; +import android.annotation.LongDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Bundle; @@ -27,6 +28,8 @@ import android.os.Parcel; import android.os.Parcelable; import java.lang.annotation.Retention; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; /** Event for an editing operation having ended. */ @@ -156,14 +159,66 @@ public final class EditingEndedEvent extends Event implements Parcelable { @SuppressWarnings("HidingField") // Hiding field from superclass as for playback events. private final long mTimeSinceCreatedMillis; + private final ArrayList<MediaItemInfo> mInputMediaItemInfos; + @Nullable private final MediaItemInfo mOutputMediaItemInfo; + + /** @hide */ + @LongDef( + prefix = {"OPERATION_TYPE_"}, + flag = true, + value = { + OPERATION_TYPE_VIDEO_TRANSCODE, + OPERATION_TYPE_AUDIO_TRANSCODE, + OPERATION_TYPE_VIDEO_EDIT, + OPERATION_TYPE_AUDIO_EDIT, + OPERATION_TYPE_VIDEO_TRANSMUX, + OPERATION_TYPE_AUDIO_TRANSMUX, + OPERATION_TYPE_PAUSED, + OPERATION_TYPE_RESUMED, + }) + @Retention(java.lang.annotation.RetentionPolicy.SOURCE) + public @interface OperationType {} + + /** Input video was decoded and re-encoded. */ + public static final long OPERATION_TYPE_VIDEO_TRANSCODE = 1; + + /** Input audio was decoded and re-encoded. */ + public static final long OPERATION_TYPE_AUDIO_TRANSCODE = 1L << 1; + + /** Input video was edited. */ + public static final long OPERATION_TYPE_VIDEO_EDIT = 1L << 2; + + /** Input audio was edited. */ + public static final long OPERATION_TYPE_AUDIO_EDIT = 1L << 3; + + /** Input video samples were writted (muxed) directly to the output file without transcoding. */ + public static final long OPERATION_TYPE_VIDEO_TRANSMUX = 1L << 4; + + /** Input audio samples were written (muxed) directly to the output file without transcoding. */ + public static final long OPERATION_TYPE_AUDIO_TRANSMUX = 1L << 5; + + /** The editing operation was paused before it completed. */ + public static final long OPERATION_TYPE_PAUSED = 1L << 6; + + /** The editing operation resumed a previous (paused) operation. */ + public static final long OPERATION_TYPE_RESUMED = 1L << 7; + + private final @OperationType long mOperationTypes; + private EditingEndedEvent( @FinalState int finalState, @ErrorCode int errorCode, long timeSinceCreatedMillis, + ArrayList<MediaItemInfo> inputMediaItemInfos, + @Nullable MediaItemInfo outputMediaItemInfo, + @OperationType long operationTypes, @NonNull Bundle extras) { mFinalState = finalState; mErrorCode = errorCode; mTimeSinceCreatedMillis = timeSinceCreatedMillis; + mInputMediaItemInfos = inputMediaItemInfos; + mOutputMediaItemInfo = outputMediaItemInfo; + mOperationTypes = operationTypes; mMetricsBundle = extras.deepCopy(); } @@ -194,6 +249,23 @@ public final class EditingEndedEvent extends Event implements Parcelable { return mTimeSinceCreatedMillis; } + /** Gets information about the input media items, or an empty list if unspecified. */ + @NonNull + public List<MediaItemInfo> getInputMediaItemInfos() { + return new ArrayList<>(mInputMediaItemInfos); + } + + /** Gets information about the output media item, or {@code null} if unspecified. */ + @Nullable + public MediaItemInfo getOutputMediaItemInfo() { + return mOutputMediaItemInfo; + } + + /** Gets a set of flags describing the types of operations performed. */ + public @OperationType long getOperationTypes() { + return mOperationTypes; + } + /** * Gets metrics-related information that is not supported by dedicated methods. * @@ -208,7 +280,7 @@ public final class EditingEndedEvent extends Event implements Parcelable { @Override @NonNull public String toString() { - return "PlaybackErrorEvent { " + return "EditingEndedEvent { " + "finalState = " + mFinalState + ", " @@ -217,6 +289,15 @@ public final class EditingEndedEvent extends Event implements Parcelable { + ", " + "timeSinceCreatedMillis = " + mTimeSinceCreatedMillis + + ", " + + "inputMediaItemInfos = " + + mInputMediaItemInfos + + ", " + + "outputMediaItemInfo = " + + mOutputMediaItemInfo + + ", " + + "operationTypes = " + + mOperationTypes + " }"; } @@ -227,12 +308,21 @@ public final class EditingEndedEvent extends Event implements Parcelable { EditingEndedEvent that = (EditingEndedEvent) o; return mFinalState == that.mFinalState && mErrorCode == that.mErrorCode + && Objects.equals(mInputMediaItemInfos, that.mInputMediaItemInfos) + && Objects.equals(mOutputMediaItemInfo, that.mOutputMediaItemInfo) + && mOperationTypes == that.mOperationTypes && mTimeSinceCreatedMillis == that.mTimeSinceCreatedMillis; } @Override public int hashCode() { - return Objects.hash(mFinalState, mErrorCode, mTimeSinceCreatedMillis); + return Objects.hash( + mFinalState, + mErrorCode, + mInputMediaItemInfos, + mOutputMediaItemInfo, + mOperationTypes, + mTimeSinceCreatedMillis); } @Override @@ -240,6 +330,9 @@ public final class EditingEndedEvent extends Event implements Parcelable { dest.writeInt(mFinalState); dest.writeInt(mErrorCode); dest.writeLong(mTimeSinceCreatedMillis); + dest.writeTypedList(mInputMediaItemInfos); + dest.writeTypedObject(mOutputMediaItemInfo, /* parcelableFlags= */ 0); + dest.writeLong(mOperationTypes); dest.writeBundle(mMetricsBundle); } @@ -249,15 +342,14 @@ public final class EditingEndedEvent extends Event implements Parcelable { } private EditingEndedEvent(@NonNull Parcel in) { - int finalState = in.readInt(); - int errorCode = in.readInt(); - long timeSinceCreatedMillis = in.readLong(); - Bundle metricsBundle = in.readBundle(); - - mFinalState = finalState; - mErrorCode = errorCode; - mTimeSinceCreatedMillis = timeSinceCreatedMillis; - mMetricsBundle = metricsBundle; + mFinalState = in.readInt(); + mErrorCode = in.readInt(); + mTimeSinceCreatedMillis = in.readLong(); + mInputMediaItemInfos = new ArrayList<>(); + in.readTypedList(mInputMediaItemInfos, MediaItemInfo.CREATOR); + mOutputMediaItemInfo = in.readTypedObject(MediaItemInfo.CREATOR); + mOperationTypes = in.readLong(); + mMetricsBundle = in.readBundle(); } public static final @NonNull Creator<EditingEndedEvent> CREATOR = @@ -277,8 +369,11 @@ public final class EditingEndedEvent extends Event implements Parcelable { @FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING) public static final class Builder { private final @FinalState int mFinalState; + private final ArrayList<MediaItemInfo> mInputMediaItemInfos; private @ErrorCode int mErrorCode; private long mTimeSinceCreatedMillis; + @Nullable private MediaItemInfo mOutputMediaItemInfo; + private @OperationType long mOperationTypes; private Bundle mMetricsBundle; /** @@ -290,6 +385,7 @@ public final class EditingEndedEvent extends Event implements Parcelable { mFinalState = finalState; mErrorCode = ERROR_CODE_NONE; mTimeSinceCreatedMillis = TIME_SINCE_CREATED_UNKNOWN; + mInputMediaItemInfos = new ArrayList<>(); mMetricsBundle = new Bundle(); } @@ -312,20 +408,49 @@ public final class EditingEndedEvent extends Event implements Parcelable { return this; } + /** Adds information about a media item that was input to the editing operation. */ + public @NonNull Builder addInputMediaItemInfo(@NonNull MediaItemInfo mediaItemInfo) { + mInputMediaItemInfos.add(Objects.requireNonNull(mediaItemInfo)); + return this; + } + + /** Sets information about the output media item. */ + public @NonNull Builder setOutputMediaItemInfo(@NonNull MediaItemInfo mediaItemInfo) { + mOutputMediaItemInfo = Objects.requireNonNull(mediaItemInfo); + return this; + } + + /** + * Adds an operation type to the set of operations performed. + * + * @param operationType A type of operation performed as part of this editing operation. + */ + public @NonNull Builder addOperationType(@OperationType long operationType) { + mOperationTypes |= operationType; + return this; + } + /** * Sets metrics-related information that is not supported by dedicated methods. * * <p>Used for backwards compatibility by the metrics infrastructure. */ public @NonNull Builder setMetricsBundle(@NonNull Bundle metricsBundle) { - mMetricsBundle = metricsBundle; + mMetricsBundle = Objects.requireNonNull(metricsBundle); return this; } /** Builds an instance. */ public @NonNull EditingEndedEvent build() { return new EditingEndedEvent( - mFinalState, mErrorCode, mTimeSinceCreatedMillis, mMetricsBundle); + mFinalState, + mErrorCode, + mTimeSinceCreatedMillis, + mInputMediaItemInfos, + mOutputMediaItemInfo, + mOperationTypes, + mMetricsBundle); } } + } diff --git a/media/java/android/media/metrics/MediaItemInfo.java b/media/java/android/media/metrics/MediaItemInfo.java new file mode 100644 index 000000000000..63dd3ccd3b33 --- /dev/null +++ b/media/java/android/media/metrics/MediaItemInfo.java @@ -0,0 +1,565 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.media.metrics; + +import static com.android.media.editing.flags.Flags.FLAG_ADD_MEDIA_METRICS_EDITING; + +import android.annotation.FlaggedApi; +import android.annotation.FloatRange; +import android.annotation.IntDef; +import android.annotation.IntRange; +import android.annotation.LongDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.hardware.DataSpace; +import android.media.MediaCodec; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Size; + +import java.lang.annotation.Retention; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** Represents information about a piece of media (for example, an audio or video file). */ +@FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING) +public final class MediaItemInfo implements Parcelable { + + /** @hide */ + @IntDef( + prefix = {"SOURCE_TYPE_"}, + value = { + SOURCE_TYPE_UNSPECIFIED, + SOURCE_TYPE_GALLERY, + SOURCE_TYPE_CAMERA, + SOURCE_TYPE_EDITING_SESSION, + SOURCE_TYPE_LOCAL_FILE, + SOURCE_TYPE_REMOTE_FILE, + SOURCE_TYPE_REMOTE_LIVE_STREAM, + SOURCE_TYPE_GENERATED, + }) + @Retention(java.lang.annotation.RetentionPolicy.SOURCE) + public @interface SourceType {} + + /** The media item's source is not known. */ + public static final int SOURCE_TYPE_UNSPECIFIED = 0; + + /** The media item came from the device gallery. */ + public static final int SOURCE_TYPE_GALLERY = 1; + + /** The media item came directly from camera capture. */ + public static final int SOURCE_TYPE_CAMERA = 2; + + /** The media item was output by a previous editing session. */ + public static final int SOURCE_TYPE_EDITING_SESSION = 3; + + /** The media item is stored on the local device's file system. */ + public static final int SOURCE_TYPE_LOCAL_FILE = 4; + + /** The media item is a remote file (for example, it's loaded from an HTTP server). */ + public static final int SOURCE_TYPE_REMOTE_FILE = 5; + + /** The media item is a remotely-served live stream. */ + public static final int SOURCE_TYPE_REMOTE_LIVE_STREAM = 6; + + /** The media item was generated by another system. */ + public static final int SOURCE_TYPE_GENERATED = 7; + + /** @hide */ + @LongDef( + prefix = {"DATA_TYPE_"}, + flag = true, + value = { + DATA_TYPE_IMAGE, + DATA_TYPE_VIDEO, + DATA_TYPE_AUDIO, + DATA_TYPE_METADATA, + DATA_TYPE_DEPTH, + DATA_TYPE_GAIN_MAP, + DATA_TYPE_HIGH_FRAME_RATE, + DATA_TYPE_CUE_POINTS, + DATA_TYPE_GAPLESS, + DATA_TYPE_SPATIAL_AUDIO, + DATA_TYPE_HIGH_DYNAMIC_RANGE_VIDEO, + }) + @Retention(java.lang.annotation.RetentionPolicy.SOURCE) + public @interface DataType {} + + /** The media item includes image data. */ + public static final long DATA_TYPE_IMAGE = 1L; + + /** The media item includes video data. */ + public static final long DATA_TYPE_VIDEO = 1L << 1; + + /** The media item includes audio data. */ + public static final long DATA_TYPE_AUDIO = 1L << 2; + + /** The media item includes metadata. */ + public static final long DATA_TYPE_METADATA = 1L << 3; + + /** The media item includes depth (z-distance) information. */ + public static final long DATA_TYPE_DEPTH = 1L << 4; + + /** The media item includes gain map information (for example, an Ultra HDR gain map). */ + public static final long DATA_TYPE_GAIN_MAP = 1L << 5; + + /** The media item includes high frame rate video data. */ + public static final long DATA_TYPE_HIGH_FRAME_RATE = 1L << 6; + + /** The media item includes time-dependent speed setting metadata. */ + public static final long DATA_TYPE_CUE_POINTS = 1L << 7; + + /** The media item includes gapless audio metadata. */ + public static final long DATA_TYPE_GAPLESS = 1L << 8; + + /** The media item includes spatial audio data. */ + public static final long DATA_TYPE_SPATIAL_AUDIO = 1L << 9; + + /** The media item includes high dynamic range (HDR) video. */ + public static final long DATA_TYPE_HIGH_DYNAMIC_RANGE_VIDEO = 1L << 10; + + /** Special value for numerical fields where the value was not specified. */ + public static final int VALUE_UNSPECIFIED = -1; + + private final @SourceType int mSourceType; + private final @DataType long mDataTypes; + private final long mDurationMillis; + private final long mClipDurationMillis; + @Nullable private final String mContainerMimeType; + private final List<String> mSampleMimeTypes; + private final List<String> mCodecNames; + private final int mAudioSampleRateHz; + private final int mAudioChannelCount; + private final long mAudioSampleCount; + private final Size mVideoSize; + private final int mVideoDataSpace; + private final float mVideoFrameRate; + private final long mVideoSampleCount; + + private MediaItemInfo( + @SourceType int sourceType, + @DataType long dataTypes, + long durationMillis, + long clipDurationMillis, + @Nullable String containerMimeType, + List<String> sampleMimeTypes, + List<String> codecNames, + int audioSampleRateHz, + int audioChannelCount, + long audioSampleCount, + Size videoSize, + int videoDataSpace, + float videoFrameRate, + long videoSampleCount) { + mSourceType = sourceType; + mDataTypes = dataTypes; + mDurationMillis = durationMillis; + mClipDurationMillis = clipDurationMillis; + mContainerMimeType = containerMimeType; + mSampleMimeTypes = sampleMimeTypes; + mCodecNames = codecNames; + mAudioSampleRateHz = audioSampleRateHz; + mAudioChannelCount = audioChannelCount; + mAudioSampleCount = audioSampleCount; + mVideoSize = videoSize; + mVideoDataSpace = videoDataSpace; + mVideoFrameRate = videoFrameRate; + mVideoSampleCount = videoSampleCount; + } + + /** + * Returns where the media item came from, or {@link #SOURCE_TYPE_UNSPECIFIED} if not specified. + */ + public @SourceType int getSourceType() { + return mSourceType; + } + + /** Returns the data types that are present in the media item. */ + public @DataType long getDataTypes() { + return mDataTypes; + } + + /** + * Returns the duration of the media item, in milliseconds, or {@link #VALUE_UNSPECIFIED} if not + * specified. + */ + public long getDurationMillis() { + return mDurationMillis; + } + + /** + * Returns the duration of the clip taken from the media item, in milliseconds, or {@link + * #VALUE_UNSPECIFIED} if not specified. + */ + public long getClipDurationMillis() { + return mClipDurationMillis; + } + + /** Returns the MIME type of the media container, or {@code null} if unspecified. */ + @Nullable + public String getContainerMimeType() { + return mContainerMimeType; + } + + /** + * Returns the MIME types of samples stored in the media container, or an empty list if not + * known. + */ + @NonNull + public List<String> getSampleMimeTypes() { + return new ArrayList<>(mSampleMimeTypes); + } + + /** + * Returns the {@linkplain MediaCodec#getName() media codec names} for codecs that were used as + * part of encoding/decoding this media item, or an empty list if not known or not applicable. + */ + @NonNull + public List<String> getCodecNames() { + return new ArrayList<>(mCodecNames); + } + + /** + * Returns the sample rate of audio, in Hertz, or {@link #VALUE_UNSPECIFIED} if not specified. + */ + public int getAudioSampleRateHz() { + return mAudioSampleRateHz; + } + + /** Returns the number of audio channels, or {@link #VALUE_UNSPECIFIED} if not specified. */ + public int getAudioChannelCount() { + return mAudioChannelCount; + } + + /** + * Returns the number of audio frames in the item, after clipping (if applicable), or {@link + * #VALUE_UNSPECIFIED} if not specified. + */ + public long getAudioSampleCount() { + return mAudioSampleCount; + } + + /** + * Returns the video size, in pixels, or a {@link Size} with width and height set to {@link + * #VALUE_UNSPECIFIED} if not specified. + */ + @NonNull + public Size getVideoSize() { + return mVideoSize; + } + + /** Returns the {@linkplain DataSpace data space} for video, as a packed integer. */ + @SuppressLint("MethodNameUnits") // Packed integer for an android.hardware.DataSpace. + public int getVideoDataSpace() { + return mVideoDataSpace; + } + + /** + * Returns the average video frame rate, in frames per second, or {@link #VALUE_UNSPECIFIED} if + * not specified. + */ + public float getVideoFrameRate() { + return mVideoFrameRate; + } + + /** + * Returns the number of video frames, aftrer clipping (if applicable), or {@link + * #VALUE_UNSPECIFIED} if not specified. + */ + public long getVideoSampleCount() { + return mVideoSampleCount; + } + + /** Builder for {@link MediaItemInfo}. */ + @FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING) + public static final class Builder { + + private @SourceType int mSourceType; + private @DataType long mDataTypes; + private long mDurationMillis; + private long mClipDurationMillis; + @Nullable private String mContainerMimeType; + private final ArrayList<String> mSampleMimeTypes; + private final ArrayList<String> mCodecNames; + private int mAudioSampleRateHz; + private int mAudioChannelCount; + private long mAudioSampleCount; + @Nullable private Size mVideoSize; + private int mVideoDataSpace; + private float mVideoFrameRate; + private long mVideoSampleCount; + + /** Creates a new builder. */ + public Builder() { + mSourceType = SOURCE_TYPE_UNSPECIFIED; + mDurationMillis = VALUE_UNSPECIFIED; + mClipDurationMillis = VALUE_UNSPECIFIED; + mSampleMimeTypes = new ArrayList<>(); + mCodecNames = new ArrayList<>(); + mAudioSampleRateHz = VALUE_UNSPECIFIED; + mAudioChannelCount = VALUE_UNSPECIFIED; + mAudioSampleCount = VALUE_UNSPECIFIED; + mVideoSize = new Size(VALUE_UNSPECIFIED, VALUE_UNSPECIFIED); + mVideoFrameRate = VALUE_UNSPECIFIED; + mVideoSampleCount = VALUE_UNSPECIFIED; + } + + /** Sets where the media item came from. */ + public @NonNull Builder setSourceType(@SourceType int sourceType) { + mSourceType = sourceType; + return this; + } + + /** Adds an additional data type represented as part of the media item. */ + public @NonNull Builder addDataType(@DataType long dataType) { + mDataTypes |= dataType; + return this; + } + + /** Sets the duration of the media item, in milliseconds. */ + public @NonNull Builder setDurationMillis(long durationMillis) { + mDurationMillis = durationMillis; + return this; + } + + /** Sets the duration of the clip taken from the media item, in milliseconds. */ + public @NonNull Builder setClipDurationMillis(long clipDurationMillis) { + mClipDurationMillis = clipDurationMillis; + return this; + } + + /** Sets the MIME type of the media container. */ + public @NonNull Builder setContainerMimeType(@NonNull String containerMimeType) { + mContainerMimeType = Objects.requireNonNull(containerMimeType); + return this; + } + + /** Adds a sample MIME type stored in the media container. */ + public @NonNull Builder addSampleMimeType(@NonNull String mimeType) { + mSampleMimeTypes.add(Objects.requireNonNull(mimeType)); + return this; + } + + /** + * Adds an {@linkplain MediaCodec#getName() media codec name} that was used as part of + * decoding/encoding this media item. + */ + public @NonNull Builder addCodecName(@NonNull String codecName) { + mCodecNames.add(Objects.requireNonNull(codecName)); + return this; + } + + /** Sets the sample rate of audio, in Hertz. */ + public @NonNull Builder setAudioSampleRateHz(@IntRange(from = 0) int audioSampleRateHz) { + mAudioSampleRateHz = audioSampleRateHz; + return this; + } + + /** Sets the number of audio channels. */ + public @NonNull Builder setAudioChannelCount(@IntRange(from = 0) int audioChannelCount) { + mAudioChannelCount = audioChannelCount; + return this; + } + + /** Sets the number of audio frames in the item, after clipping (if applicable). */ + public @NonNull Builder setAudioSampleCount(@IntRange(from = 0) long audioSampleCount) { + mAudioSampleCount = audioSampleCount; + return this; + } + + /** Sets the video size, in pixels. */ + public @NonNull Builder setVideoSize(@NonNull Size videoSize) { + mVideoSize = Objects.requireNonNull(videoSize); + return this; + } + + /** + * Sets the {@link DataSpace} of video frames. + * + * @param videoDataSpace The data space, returned by {@link DataSpace#pack(int, int, int)}. + */ + public @NonNull Builder setVideoDataSpace(int videoDataSpace) { + mVideoDataSpace = videoDataSpace; + return this; + } + + /** Sets the average video frame rate, in frames per second. */ + public @NonNull Builder setVideoFrameRate(@FloatRange(from = 0) float videoFrameRate) { + mVideoFrameRate = videoFrameRate; + return this; + } + + /** Sets the number of video frames, after clipping (if applicable). */ + public @NonNull Builder setVideoSampleCount(@IntRange(from = 0) long videoSampleCount) { + mVideoSampleCount = videoSampleCount; + return this; + } + + /** Builds an instance. */ + @NonNull + public MediaItemInfo build() { + return new MediaItemInfo( + mSourceType, + mDataTypes, + mDurationMillis, + mClipDurationMillis, + mContainerMimeType, + mSampleMimeTypes, + mCodecNames, + mAudioSampleRateHz, + mAudioChannelCount, + mAudioSampleCount, + mVideoSize, + mVideoDataSpace, + mVideoFrameRate, + mVideoSampleCount); + } + } + + @Override + @NonNull + public String toString() { + return "MediaItemInfo { " + + "sourceType = " + + mSourceType + + ", " + + "dataTypes = " + + mDataTypes + + ", " + + "durationMillis = " + + mDurationMillis + + ", " + + "clipDurationMillis = " + + mClipDurationMillis + + ", " + + "containerMimeType = " + + mContainerMimeType + + ", " + + "sampleMimeTypes = " + + mSampleMimeTypes + + ", " + + "codecNames = " + + mCodecNames + + ", " + + "audioSampleRateHz = " + + mAudioSampleRateHz + + ", " + + "audioChannelCount = " + + mAudioChannelCount + + ", " + + "audioSampleCount = " + + mAudioSampleCount + + ", " + + "videoSize = " + + mVideoSize + + ", " + + "videoDataSpace = " + + mVideoDataSpace + + ", " + + "videoFrameRate = " + + mVideoFrameRate + + ", " + + "videoSampleCount = " + + mVideoSampleCount + + " }"; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MediaItemInfo that = (MediaItemInfo) o; + return mSourceType == that.mSourceType + && mDataTypes == that.mDataTypes + && mDurationMillis == that.mDurationMillis + && mClipDurationMillis == that.mClipDurationMillis + && Objects.equals(mContainerMimeType, that.mContainerMimeType) + && mSampleMimeTypes.equals(that.mSampleMimeTypes) + && mCodecNames.equals(that.mCodecNames) + && mAudioSampleRateHz == that.mAudioSampleRateHz + && mAudioChannelCount == that.mAudioChannelCount + && mAudioSampleCount == that.mAudioSampleCount + && Objects.equals(mVideoSize, that.mVideoSize) + && Objects.equals(mVideoDataSpace, that.mVideoDataSpace) + && mVideoFrameRate == that.mVideoFrameRate + && mVideoSampleCount == that.mVideoSampleCount; + } + + @Override + public int hashCode() { + return Objects.hash(mSourceType, mDataTypes); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mSourceType); + dest.writeLong(mDataTypes); + dest.writeLong(mDurationMillis); + dest.writeLong(mClipDurationMillis); + dest.writeString(mContainerMimeType); + dest.writeStringList(mSampleMimeTypes); + dest.writeStringList(mCodecNames); + dest.writeInt(mAudioSampleRateHz); + dest.writeInt(mAudioChannelCount); + dest.writeLong(mAudioSampleCount); + dest.writeInt(mVideoSize.getWidth()); + dest.writeInt(mVideoSize.getHeight()); + dest.writeInt(mVideoDataSpace); + dest.writeFloat(mVideoFrameRate); + dest.writeLong(mVideoSampleCount); + } + + @Override + public int describeContents() { + return 0; + } + + private MediaItemInfo(@NonNull Parcel in) { + mSourceType = in.readInt(); + mDataTypes = in.readLong(); + mDurationMillis = in.readLong(); + mClipDurationMillis = in.readLong(); + mContainerMimeType = in.readString(); + mSampleMimeTypes = new ArrayList<>(); + in.readStringList(mSampleMimeTypes); + mCodecNames = new ArrayList<>(); + in.readStringList(mCodecNames); + mAudioSampleRateHz = in.readInt(); + mAudioChannelCount = in.readInt(); + mAudioSampleCount = in.readLong(); + int videoSizeWidth = in.readInt(); + int videoSizeHeight = in.readInt(); + mVideoSize = new Size(videoSizeWidth, videoSizeHeight); + mVideoDataSpace = in.readInt(); + mVideoFrameRate = in.readFloat(); + mVideoSampleCount = in.readLong(); + } + + public static final @NonNull Creator<MediaItemInfo> CREATOR = + new Creator<>() { + @Override + public MediaItemInfo[] newArray(int size) { + return new MediaItemInfo[size]; + } + + @Override + public MediaItemInfo createFromParcel(@NonNull Parcel in) { + return new MediaItemInfo(in); + } + }; +} diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java index e3012cd3e6fd..249fa7f0df77 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java @@ -1621,6 +1621,7 @@ public class ApplicationsState { } public static class AppEntry extends SizeInfo { + @VisibleForTesting String mProfileType; @Nullable public final File apkFile; public final long id; public String label; @@ -1647,11 +1648,6 @@ public class ApplicationsState { */ public boolean isHomeApp; - /** - * Whether or not it's a cloned app . - */ - public boolean isCloned; - public String getNormalizedLabel() { if (normalizedLabel != null) { return normalizedLabel; @@ -1692,11 +1688,21 @@ public class ApplicationsState { () -> this.ensureLabelDescriptionLocked(context)); } UserManager um = UserManager.get(context); - this.showInPersonalTab = shouldShowInPersonalTab(um, info.uid); UserInfo userInfo = um.getUserInfo(UserHandle.getUserId(info.uid)); - if (userInfo != null) { - this.isCloned = userInfo.isCloneProfile(); - } + mProfileType = userInfo.userType; + this.showInPersonalTab = shouldShowInPersonalTab(um, info.uid); + } + + public boolean isClonedProfile() { + return UserManager.USER_TYPE_PROFILE_CLONE.equals(mProfileType); + } + + public boolean isManagedProfile() { + return UserManager.USER_TYPE_PROFILE_MANAGED.equals(mProfileType); + } + + public boolean isPrivateProfile() { + return UserManager.USER_TYPE_PROFILE_PRIVATE.equals(mProfileType); } /** @@ -1890,16 +1896,24 @@ public class ApplicationsState { }; public static final AppFilter FILTER_WORK = new AppFilter() { - private int mCurrentUser; @Override - public void init() { - mCurrentUser = ActivityManager.getCurrentUser(); + public void init() {} + + @Override + public boolean filterApp(AppEntry entry) { + return !entry.showInPersonalTab && entry.isManagedProfile(); } + }; + + public static final AppFilter FILTER_PRIVATE_PROFILE = new AppFilter() { + + @Override + public void init() {} @Override public boolean filterApp(AppEntry entry) { - return !entry.showInPersonalTab; + return !entry.showInPersonalTab && entry.isPrivateProfile(); } }; diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java index c5598bfa9438..213a66e546ab 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.pm.ApplicationInfo; +import android.os.UserManager; import org.junit.Before; import org.junit.Test; @@ -297,11 +298,26 @@ public class ApplicationsStateTest { @Test public void testPersonalAndWorkFiltersDisplaysCorrectApps() { mEntry.showInPersonalTab = true; + mEntry.mProfileType = UserManager.USER_TYPE_FULL_SYSTEM; assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isTrue(); assertThat(ApplicationsState.FILTER_WORK.filterApp(mEntry)).isFalse(); mEntry.showInPersonalTab = false; + mEntry.mProfileType = UserManager.USER_TYPE_PROFILE_MANAGED; assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isFalse(); assertThat(ApplicationsState.FILTER_WORK.filterApp(mEntry)).isTrue(); } + + @Test + public void testPrivateProfileFilterDisplaysCorrectApps() { + mEntry.showInPersonalTab = true; + mEntry.mProfileType = UserManager.USER_TYPE_FULL_SYSTEM; + assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isTrue(); + assertThat(ApplicationsState.FILTER_PRIVATE_PROFILE.filterApp(mEntry)).isFalse(); + + mEntry.showInPersonalTab = false; + mEntry.mProfileType = UserManager.USER_TYPE_PROFILE_PRIVATE; + assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isFalse(); + assertThat(ApplicationsState.FILTER_PRIVATE_PROFILE.filterApp(mEntry)).isTrue(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt index 40a9b9c699bc..13d743f8a4e4 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt @@ -141,6 +141,7 @@ constructor( if (field != value) { field = value checkIfPollingNeeded() + _data = _data.copy(listening = value) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java index 90abec17771c..80c3551b7de0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar; +import static android.app.Flags.lifetimeExtensionRefactor; + import android.annotation.NonNull; import android.app.Notification; import android.app.RemoteInputHistoryItem; @@ -29,6 +31,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import java.util.ArrayList; import java.util.Arrays; import java.util.stream.Stream; @@ -68,7 +71,7 @@ public class RemoteInputNotificationRebuilder { @NonNull public StatusBarNotification rebuildForCanceledSmartReplies( NotificationEntry entry) { - return rebuildWithRemoteInputInserted(entry, null /* remoteInputTest */, + return rebuildWithRemoteInputInserted(entry, null /* remoteInputText */, false /* showSpinner */, null /* mimeType */, null /* uri */); } @@ -97,22 +100,50 @@ public class RemoteInputNotificationRebuilder { StatusBarNotification rebuildWithRemoteInputInserted(NotificationEntry entry, CharSequence remoteInputText, boolean showSpinner, String mimeType, Uri uri) { StatusBarNotification sbn = entry.getSbn(); - Notification.Builder b = Notification.Builder .recoverBuilder(mContext, sbn.getNotification().clone()); - if (remoteInputText != null || uri != null) { - RemoteInputHistoryItem newItem = uri != null - ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText) - : new RemoteInputHistoryItem(remoteInputText); + + if (lifetimeExtensionRefactor()) { + if (entry.remoteInputs == null) { + entry.remoteInputs = new ArrayList<RemoteInputHistoryItem>(); + } + + // Append new remote input information to remoteInputs list + if (remoteInputText != null || uri != null) { + RemoteInputHistoryItem newItem = uri != null + ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText) + : new RemoteInputHistoryItem(remoteInputText); + // The list is latest-first, so new elements should be added as the first element. + entry.remoteInputs.add(0, newItem); + } + + // Read the whole remoteInputs list from the entry, then append all of those to the sbn. Parcelable[] oldHistoryItems = sbn.getNotification().extras .getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS); + RemoteInputHistoryItem[] newHistoryItems = oldHistoryItems != null ? Stream.concat( - Stream.of(newItem), - Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p)) + entry.remoteInputs.stream(), + Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p)) .toArray(RemoteInputHistoryItem[]::new) - : new RemoteInputHistoryItem[] { newItem }; + : entry.remoteInputs.toArray(RemoteInputHistoryItem[]::new); b.setRemoteInputHistory(newHistoryItems); + + } else { + if (remoteInputText != null || uri != null) { + RemoteInputHistoryItem newItem = uri != null + ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText) + : new RemoteInputHistoryItem(remoteInputText); + Parcelable[] oldHistoryItems = sbn.getNotification().extras + .getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS); + RemoteInputHistoryItem[] newHistoryItems = oldHistoryItems != null + ? Stream.concat( + Stream.of(newItem), + Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p)) + .toArray(RemoteInputHistoryItem[]::new) + : new RemoteInputHistoryItem[]{newItem}; + b.setRemoteInputHistory(newHistoryItems); + } } b.setShowRemoteInputSpinner(showSpinner); b.setHideSmartReplies(true); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index cdacb10e1676..8678f0aad181 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -40,6 +40,7 @@ import android.app.NotificationChannel; import android.app.NotificationManager.Policy; import android.app.Person; import android.app.RemoteInput; +import android.app.RemoteInputHistoryItem; import android.content.Context; import android.content.pm.ShortcutInfo; import android.net.Uri; @@ -127,6 +128,7 @@ public final class NotificationEntry extends ListEntry { public int targetSdk; private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET; public CharSequence remoteInputText; + public List<RemoteInputHistoryItem> remoteInputs = null; public String remoteInputMimeType; public Uri remoteInputUri; public ContentInfo remoteInputAttachment; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt index 918bf083f9fe..28fff1519032 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.collection.coordinator +import android.app.Flags.lifetimeExtensionRefactor +import android.app.Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY import android.os.Handler import android.service.notification.NotificationListenerService.REASON_CANCEL import android.service.notification.NotificationListenerService.REASON_CLICK @@ -88,11 +90,21 @@ class RemoteInputCoordinator @Inject constructor( override fun attach(pipeline: NotifPipeline) { mNotificationRemoteInputManager.setRemoteInputListener(this) - mRemoteInputLifetimeExtenders.forEach { pipeline.addNotificationLifetimeExtender(it) } + if (lifetimeExtensionRefactor()) { + pipeline.addNotificationLifetimeExtender(mRemoteInputActiveExtender) + } else { + mRemoteInputLifetimeExtenders.forEach { + pipeline.addNotificationLifetimeExtender(it) + } + } mNotifUpdater = pipeline.getInternalNotifUpdater(TAG) pipeline.addCollectionListener(mCollectionListener) } + /* + * Listener that updates the appearance of the notification if it has been lifetime extended + * by a a direct reply or a smart reply, and cancelled. + */ val mCollectionListener = object : NotifCollectionListener { override fun onEntryUpdated(entry: NotificationEntry, fromSystem: Boolean) { if (DEBUG) { @@ -100,9 +112,32 @@ class RemoteInputCoordinator @Inject constructor( " fromSystem=$fromSystem)") } if (fromSystem) { - // Mark smart replies as sent whenever a notification is updated by the app, - // otherwise the smart replies are never marked as sent. - mSmartReplyController.stopSending(entry) + if (lifetimeExtensionRefactor()) { + if ((entry.getSbn().getNotification().flags + and FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) { + if (mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory( + entry)) { + val newSbn = mRebuilder.rebuildForRemoteInputReply(entry) + entry.onRemoteInputInserted() + mNotifUpdater.onInternalNotificationUpdate(newSbn, + "Extending lifetime of notification with remote input") + } else if (mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory( + entry)) { + val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry) + mSmartReplyController.stopSending(entry) + mNotifUpdater.onInternalNotificationUpdate(newSbn, + "Extending lifetime of notification with smart reply") + } + } else { + // Notifications updated without FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY + // should have their remote inputs list cleared. + entry.remoteInputs = null + } + } else { + // Mark smart replies as sent whenever a notification is updated by the app, + // otherwise the smart replies are never marked as sent. + mSmartReplyController.stopSending(entry) + } } } @@ -130,8 +165,10 @@ class RemoteInputCoordinator @Inject constructor( // NOTE: This is some trickery! By removing the lifetime extensions when we know they should // be immediately re-upped, we ensure that the side-effects of the lifetime extenders get to // fire again, thus ensuring that we add subsequent replies to the notification. - mRemoteInputHistoryExtender.endLifetimeExtension(entry.key) - mSmartReplyHistoryExtender.endLifetimeExtension(entry.key) + if (!lifetimeExtensionRefactor()) { + mRemoteInputHistoryExtender.endLifetimeExtension(entry.key) + mSmartReplyHistoryExtender.endLifetimeExtension(entry.key) + } // If we're extending for remote input being active, then from the apps point of // view it is already canceled, so we'll need to cancel it on the apps behalf @@ -160,15 +197,19 @@ class RemoteInputCoordinator @Inject constructor( } override fun isNotificationKeptForRemoteInputHistory(key: String) = + if (!lifetimeExtensionRefactor()) { mRemoteInputHistoryExtender.isExtending(key) || mSmartReplyHistoryExtender.isExtending(key) + } else false override fun releaseNotificationIfKeptForRemoteInputHistory(entry: NotificationEntry) { if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entry.key})") - mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(entry.key, - REMOTE_INPUT_EXTENDER_RELEASE_DELAY) - mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(entry.key, - REMOTE_INPUT_EXTENDER_RELEASE_DELAY) + if (!lifetimeExtensionRefactor()) { + mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(entry.key, + REMOTE_INPUT_EXTENDER_RELEASE_DELAY) + mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(entry.key, + REMOTE_INPUT_EXTENDER_RELEASE_DELAY) + } mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key, REMOTE_INPUT_EXTENDER_RELEASE_DELAY) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java index 45bdae83bbd9..8ac3b4a75141 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java @@ -132,9 +132,9 @@ public class PhoneStatusBarView extends FrameLayout { if (updateDisplayParameters()) { updateLayoutForCutout(); requestLayout(); - if (truncatedStatusBarIconsFix()) { - updateWindowHeight(); - } + } + if (truncatedStatusBarIconsFix()) { + updateWindowHeight(); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt index 100e579f1d93..4ec29ceb56d3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt @@ -164,6 +164,18 @@ class SeekBarObserverTest : SysuiTestCase() { } @Test + fun seekbarNotListeningNotScrubbingPlaying() { + // WHEN playing + val isPlaying = true + val isScrubbing = false + val data = + SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000, false) + observer.onChanged(data) + // THEN progress drawable is not animating + verify(mockSquigglyProgress).animate = false + } + + @Test fun seekBarPlayingScrubbing() { // WHEN playing & scrubbing val isPlaying = true diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt index 7073cc7c5707..85b8b03a1b46 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt @@ -15,7 +15,13 @@ */ package com.android.systemui.statusbar.notification.collection.coordinator +import android.app.Flags.lifetimeExtensionRefactor +import android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR +import android.app.Notification +import android.app.RemoteInputHistoryItem import android.os.Handler +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.service.notification.StatusBarNotification import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper @@ -34,6 +40,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.No import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.captureMany import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -42,6 +49,7 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.Mockito.never +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations.initMocks @@ -57,6 +65,7 @@ class RemoteInputCoordinatorTest : SysuiTestCase() { private lateinit var entry2: NotificationEntry @Mock private lateinit var lifetimeExtensionCallback: OnEndLifetimeExtensionCallback + @Mock private lateinit var rebuilder: RemoteInputNotificationRebuilder @Mock private lateinit var remoteInputManager: NotificationRemoteInputManager @Mock private lateinit var mainHandler: Handler @@ -84,9 +93,6 @@ class RemoteInputCoordinatorTest : SysuiTestCase() { listener = withArgCaptor { verify(remoteInputManager).setRemoteInputListener(capture()) } - collectionListener = withArgCaptor { - verify(pipeline).addCollectionListener(capture()) - } entry1 = NotificationEntryBuilder().setId(1).build() entry2 = NotificationEntryBuilder().setId(2).build() `when`(rebuilder.rebuildForCanceledSmartReplies(any())).thenReturn(sbn) @@ -98,16 +104,23 @@ class RemoteInputCoordinatorTest : SysuiTestCase() { val remoteInputHistoryExtender get() = coordinator.mRemoteInputHistoryExtender val smartReplyHistoryExtender get() = coordinator.mSmartReplyHistoryExtender + val collectionListeners get() = captureMany { + verify(pipeline, times(1)).addCollectionListener(capture()) + } + @Test fun testRemoteInputActive() { `when`(remoteInputManager.isRemoteInputActive(entry1)).thenReturn(true) assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isTrue() - assertThat(remoteInputHistoryExtender.maybeExtendLifetime(entry1, 0)).isFalse() - assertThat(smartReplyHistoryExtender.maybeExtendLifetime(entry1, 0)).isFalse() + if (!lifetimeExtensionRefactor()) { + assertThat(remoteInputHistoryExtender.maybeExtendLifetime(entry1, 0)).isFalse() + assertThat(smartReplyHistoryExtender.maybeExtendLifetime(entry1, 0)).isFalse() + } assertThat(listener.isNotificationKeptForRemoteInputHistory(entry1.key)).isFalse() } @Test + @DisableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR) fun testRemoteInputHistory() { `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry1)).thenReturn(true) assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isFalse() @@ -117,6 +130,7 @@ class RemoteInputCoordinatorTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR) fun testSmartReplyHistory() { `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry1)).thenReturn(true) assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isFalse() @@ -142,4 +156,81 @@ class RemoteInputCoordinatorTest : SysuiTestCase() { verify(lifetimeExtensionCallback).onEndLifetimeExtension(remoteInputActiveExtender, entry1) assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isFalse() } + + @Test + @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR) + fun testOnlyRemoteInputActiveLifetimeExtenderExtends() { + `when`(remoteInputManager.isRemoteInputActive(entry1)).thenReturn(true) + assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isTrue() + assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isTrue() + + listener.onPanelCollapsed() + assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isFalse() + + // Checks that lifetimeExtensionCallback is only called the expected number of times, + // by the remoteInputActiveExtender. + // Checks that the remote input history extender and smart reply history extenders + // aren't attached to the pipeline. + verify(lifetimeExtensionCallback, times(1)).onEndLifetimeExtension(any(), any()) + } + + @Test + @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR) + fun testRemoteInputLifetimeExtensionListenerTrigger() { + // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag. + val entry = NotificationEntryBuilder() + .setId(3) + .setTag("entry") + .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true) + .build() + `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(true) + `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false) + + collectionListeners.forEach { + it.onEntryUpdated(entry, true) + } + + verify(rebuilder, times(1)).rebuildForRemoteInputReply(entry) + } + + @Test + @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR) + fun testSmartReplyLifetimeExtensionListenerTrigger() { + // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag. + val entry = NotificationEntryBuilder() + .setId(3) + .setTag("entry") + .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true) + .build() + `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false) + `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(true) + collectionListeners.forEach { + it.onEntryUpdated(entry, true) + } + + + verify(rebuilder, times(1)).rebuildForCanceledSmartReplies(entry) + verify(smartReplyController, times(1)).stopSending(entry) + } + + @Test + @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR) + fun testLifetimeExtensionListenerClearsRemoteInputs() { + // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag. + val entry = NotificationEntryBuilder() + .setId(3) + .setTag("entry") + .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, false) + .build() + entry.remoteInputs = ArrayList<RemoteInputHistoryItem>() + entry.remoteInputs.add(RemoteInputHistoryItem("Test Text")) + `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false) + `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false) + + collectionListeners.forEach { + it.onEntryUpdated(entry, true) + } + + assertThat(entry.remoteInputs).isNull() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt index 6eb1c1a9f12c..4293a27095c0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt @@ -42,6 +42,7 @@ import org.junit.Before import org.junit.Test import org.mockito.Mockito.never import org.mockito.Mockito.spy +import org.mockito.Mockito.times import org.mockito.Mockito.verify @SmallTest @@ -145,6 +146,18 @@ class PhoneStatusBarViewTest : SysuiTestCase() { } @Test + fun onConfigurationChanged_multipleCalls_flagEnabled_updatesWindowHeightMultipleTimes() { + mSetFlagsRule.enableFlags(Flags.FLAG_TRUNCATED_STATUS_BAR_ICONS_FIX) + + view.onConfigurationChanged(Configuration()) + view.onConfigurationChanged(Configuration()) + view.onConfigurationChanged(Configuration()) + view.onConfigurationChanged(Configuration()) + + verify(windowController, times(4)).refreshStatusBarHeight() + } + + @Test fun onConfigurationChanged_flagDisabled_doesNotUpdateWindowHeight() { mSetFlagsRule.disableFlags(Flags.FLAG_TRUNCATED_STATUS_BAR_ICONS_FIX) @@ -154,6 +167,18 @@ class PhoneStatusBarViewTest : SysuiTestCase() { } @Test + fun onConfigurationChanged_multipleCalls_flagDisabled_doesNotUpdateWindowHeight() { + mSetFlagsRule.disableFlags(Flags.FLAG_TRUNCATED_STATUS_BAR_ICONS_FIX) + + view.onConfigurationChanged(Configuration()) + view.onConfigurationChanged(Configuration()) + view.onConfigurationChanged(Configuration()) + view.onConfigurationChanged(Configuration()) + + verify(windowController, never()).refreshStatusBarHeight() + } + + @Test fun onAttachedToWindow_updatesLeftTopRightPaddingsBasedOnInsets() { val insets = Insets.of(/* left = */ 10, /* top = */ 20, /* right = */ 30, /* bottom = */ 40) whenever(contentInsetsProvider.getStatusBarContentInsetsForCurrentRotation()) diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java index cec7a79cdada..5d415c2f636f 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java @@ -200,7 +200,9 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider Slog.d(TAG, this + ": Starting"); } mRunning = true; - updateBinding(); + if (!Flags.enablePreventionOfKeepAliveRouteProviders()) { + updateBinding(); + } } if (rebindIfDisconnected && mActiveConnection == null && shouldBind()) { unbind(); diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java index 233a3ab4a4ea..fcca94b0611a 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java @@ -150,7 +150,9 @@ final class MediaRoute2ProviderWatcher { mCallback.onAddProviderService(proxy); } else if (sourceIndex >= targetIndex) { MediaRoute2ProviderServiceProxy proxy = mProxies.get(sourceIndex); - proxy.start(/* rebindIfDisconnected= */ true); // restart the proxy if needed + proxy.start( + /* rebindIfDisconnected= */ + !Flags.enablePreventionOfKeepAliveRouteProviders()); Collections.swap(mProxies, sourceIndex, targetIndex++); } } diff --git a/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java b/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java index bbe6d3a0c8fa..2cd8fe0ac43e 100644 --- a/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java +++ b/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java @@ -18,10 +18,13 @@ package com.android.server.media.metrics; import android.content.Context; import android.content.pm.PackageManager; +import android.hardware.DataSpace; import android.media.MediaMetrics; +import android.media.codec.Enums; import android.media.metrics.BundleSession; import android.media.metrics.EditingEndedEvent; import android.media.metrics.IMediaMetricsManager; +import android.media.metrics.MediaItemInfo; import android.media.metrics.NetworkEvent; import android.media.metrics.PlaybackErrorEvent; import android.media.metrics.PlaybackMetrics; @@ -31,7 +34,9 @@ import android.os.Binder; import android.os.PersistableBundle; import android.provider.DeviceConfig; import android.provider.DeviceConfig.Properties; +import android.text.TextUtils; import android.util.Base64; +import android.util.Size; import android.util.Slog; import android.util.StatsEvent; import android.util.StatsLog; @@ -72,7 +77,14 @@ public final class MediaMetricsManagerService extends SystemService { private static final String mMetricsId = MediaMetrics.Name.METRICS_MANAGER; private static final String FAILED_TO_GET = "failed_to_get"; + + private static final MediaItemInfo EMPTY_MEDIA_ITEM_INFO = new MediaItemInfo.Builder().build(); + private static final int DURATION_BUCKETS_BELOW_ONE_MINUTE = 8; + private static final int DURATION_BUCKETS_COUNT = 13; + private static final String AUDIO_MIME_TYPE_PREFIX = "audio/"; + private static final String VIDEO_MIME_TYPE_PREFIX = "video/"; private final SecureRandom mSecureRandom; + @GuardedBy("mLock") private Integer mMode = null; @GuardedBy("mLock") @@ -353,6 +365,51 @@ public final class MediaMetricsManagerService extends SystemService { if (level == LOGGING_LEVEL_BLOCKED) { return; } + MediaItemInfo inputMediaItemInfo = + event.getInputMediaItemInfos().isEmpty() + ? EMPTY_MEDIA_ITEM_INFO + : event.getInputMediaItemInfos().get(0); + String inputAudioSampleMimeType = + getFilteredFirstMimeType( + inputMediaItemInfo.getSampleMimeTypes(), AUDIO_MIME_TYPE_PREFIX); + String inputVideoSampleMimeType = + getFilteredFirstMimeType( + inputMediaItemInfo.getSampleMimeTypes(), VIDEO_MIME_TYPE_PREFIX); + Size inputVideoSize = inputMediaItemInfo.getVideoSize(); + int inputVideoResolution = getVideoResolutionEnum(inputVideoSize); + if (inputVideoResolution == Enums.RESOLUTION_UNKNOWN) { + // Try swapping width/height in case it's a portrait video. + inputVideoResolution = + getVideoResolutionEnum( + new Size(inputVideoSize.getHeight(), inputVideoSize.getWidth())); + } + List<String> inputCodecNames = inputMediaItemInfo.getCodecNames(); + String inputFirstCodecName = !inputCodecNames.isEmpty() ? inputCodecNames.get(0) : ""; + String inputSecondCodecName = inputCodecNames.size() > 1 ? inputCodecNames.get(1) : ""; + + MediaItemInfo outputMediaItemInfo = + event.getOutputMediaItemInfo() == null + ? EMPTY_MEDIA_ITEM_INFO + : event.getOutputMediaItemInfo(); + String outputAudioSampleMimeType = + getFilteredFirstMimeType( + outputMediaItemInfo.getSampleMimeTypes(), AUDIO_MIME_TYPE_PREFIX); + String outputVideoSampleMimeType = + getFilteredFirstMimeType( + outputMediaItemInfo.getSampleMimeTypes(), VIDEO_MIME_TYPE_PREFIX); + Size outputVideoSize = outputMediaItemInfo.getVideoSize(); + int outputVideoResolution = getVideoResolutionEnum(outputVideoSize); + if (outputVideoResolution == Enums.RESOLUTION_UNKNOWN) { + // Try swapping width/height in case it's a portrait video. + outputVideoResolution = + getVideoResolutionEnum( + new Size(outputVideoSize.getHeight(), outputVideoSize.getWidth())); + } + List<String> outputCodecNames = outputMediaItemInfo.getCodecNames(); + String outputFirstCodecName = + !outputCodecNames.isEmpty() ? outputCodecNames.get(0) : ""; + String outputSecondCodecName = + outputCodecNames.size() > 1 ? outputCodecNames.get(1) : ""; StatsEvent statsEvent = StatsEvent.newBuilder() .setAtomId(798) @@ -360,6 +417,66 @@ public final class MediaMetricsManagerService extends SystemService { .writeInt(event.getFinalState()) .writeInt(event.getErrorCode()) .writeLong(event.getTimeSinceCreatedMillis()) + .writeInt(getThroughputFps(event)) + .writeInt(event.getInputMediaItemInfos().size()) + .writeInt(inputMediaItemInfo.getSourceType()) + .writeLong( + getBucketedDurationMillis( + inputMediaItemInfo.getDurationMillis())) + .writeLong( + getBucketedDurationMillis( + inputMediaItemInfo.getClipDurationMillis())) + .writeString( + getFilteredMimeType(inputMediaItemInfo.getContainerMimeType())) + .writeString(inputAudioSampleMimeType) + .writeString(inputVideoSampleMimeType) + .writeInt(getCodecEnum(inputVideoSampleMimeType)) + .writeInt( + getFilteredAudioSampleRateHz( + inputMediaItemInfo.getAudioSampleRateHz())) + .writeInt(inputMediaItemInfo.getAudioChannelCount()) + .writeInt(inputVideoSize.getWidth()) + .writeInt(inputVideoSize.getHeight()) + .writeInt(inputVideoResolution) + .writeInt(getVideoResolutionAspectRatioEnum(inputVideoSize)) + .writeInt(inputMediaItemInfo.getVideoDataSpace()) + .writeInt( + getVideoHdrFormatEnum( + inputMediaItemInfo.getVideoDataSpace(), + inputVideoSampleMimeType)) + .writeInt(Math.round(inputMediaItemInfo.getVideoFrameRate())) + .writeInt(getVideoFrameRateEnum(inputMediaItemInfo.getVideoFrameRate())) + .writeString(inputFirstCodecName) + .writeString(inputSecondCodecName) + .writeLong( + getBucketedDurationMillis( + outputMediaItemInfo.getDurationMillis())) + .writeLong( + getBucketedDurationMillis( + outputMediaItemInfo.getClipDurationMillis())) + .writeString( + getFilteredMimeType(outputMediaItemInfo.getContainerMimeType())) + .writeString(outputAudioSampleMimeType) + .writeString(outputVideoSampleMimeType) + .writeInt(getCodecEnum(outputVideoSampleMimeType)) + .writeInt( + getFilteredAudioSampleRateHz( + outputMediaItemInfo.getAudioSampleRateHz())) + .writeInt(outputMediaItemInfo.getAudioChannelCount()) + .writeInt(outputVideoSize.getWidth()) + .writeInt(outputVideoSize.getHeight()) + .writeInt(outputVideoResolution) + .writeInt(getVideoResolutionAspectRatioEnum(outputVideoSize)) + .writeInt(outputMediaItemInfo.getVideoDataSpace()) + .writeInt( + getVideoHdrFormatEnum( + outputMediaItemInfo.getVideoDataSpace(), + outputVideoSampleMimeType)) + .writeInt(Math.round(outputMediaItemInfo.getVideoFrameRate())) + .writeInt( + getVideoFrameRateEnum(outputMediaItemInfo.getVideoFrameRate())) + .writeString(outputFirstCodecName) + .writeString(outputSecondCodecName) .usePooledBuffer() .build(); StatsLog.write(statsEvent); @@ -511,4 +628,215 @@ public final class MediaMetricsManagerService extends SystemService { } } } + + private static int getThroughputFps(EditingEndedEvent event) { + MediaItemInfo outputMediaItemInfo = event.getOutputMediaItemInfo(); + if (outputMediaItemInfo == null) { + return -1; + } + long videoSampleCount = outputMediaItemInfo.getVideoSampleCount(); + if (videoSampleCount == MediaItemInfo.VALUE_UNSPECIFIED) { + return -1; + } + long elapsedTimeMs = event.getTimeSinceCreatedMillis(); + if (elapsedTimeMs == EditingEndedEvent.TIME_SINCE_CREATED_UNKNOWN) { + return -1; + } + return (int) + Math.min(Integer.MAX_VALUE, Math.round(1000.0 * videoSampleCount / elapsedTimeMs)); + } + + private static long getBucketedDurationMillis(long durationMillis) { + if (durationMillis == MediaItemInfo.VALUE_UNSPECIFIED || durationMillis <= 0) { + return -1; + } + // Bucket values in an exponential distribution to reduce the precision that's stored: + // bucket index -> range -> bucketed duration + // 1 -> [0, 469 ms) -> 235 ms + // 2 -> [469 ms, 938 ms) -> 469 ms + // 3 -> [938 ms, 1875 ms) -> 938 ms + // 4 -> [1875 ms, 3750 ms) -> 1875 ms + // 5 -> [3750 ms, 7500 ms) -> 3750 ms + // [...] + // 13 -> [960000 ms, max) -> 960000 ms + int bucketIndex = + (int) + Math.floor( + DURATION_BUCKETS_BELOW_ONE_MINUTE + + Math.log((durationMillis + 1) / 60_000.0) / Math.log(2)); + // Clamp to range [0, DURATION_BUCKETS_COUNT]. + bucketIndex = Math.min(DURATION_BUCKETS_COUNT, Math.max(0, bucketIndex)); + // Map back onto the representative value for the bucket. + return (long) + Math.ceil(Math.pow(2, bucketIndex - DURATION_BUCKETS_BELOW_ONE_MINUTE) * 60_000.0); + } + + /** + * Returns the first entry in {@code mimeTypes} with the given prefix, if it matches the + * filtering allowlist. If no entries match the prefix or if the first matching entry is not on + * the allowlist, returns an empty string. + */ + private static String getFilteredFirstMimeType(List<String> mimeTypes, String prefix) { + int size = mimeTypes.size(); + for (int i = 0; i < size; i++) { + String mimeType = mimeTypes.get(i); + if (mimeType.startsWith(prefix)) { + return getFilteredMimeType(mimeType); + } + } + return ""; + } + + private static String getFilteredMimeType(String mimeType) { + if (TextUtils.isEmpty(mimeType)) { + return ""; + } + // Discard all inputs that aren't allowlisted MIME types. + return switch (mimeType) { + case "video/mp4", + "video/x-matroska", + "video/webm", + "video/3gpp", + "video/avc", + "video/hevc", + "video/x-vnd.on2.vp8", + "video/x-vnd.on2.vp9", + "video/av01", + "video/mp2t", + "video/mp4v-es", + "video/mpeg", + "video/x-flv", + "video/dolby-vision", + "video/raw", + "audio/mp4", + "audio/mp4a-latm", + "audio/x-matroska", + "audio/webm", + "audio/mpeg", + "audio/mpeg-L1", + "audio/mpeg-L2", + "audio/ac3", + "audio/eac3", + "audio/eac3-joc", + "audio/av4", + "audio/true-hd", + "audio/vnd.dts", + "audio/vnd.dts.hd", + "audio/vorbis", + "audio/opus", + "audio/flac", + "audio/ogg", + "audio/wav", + "audio/midi", + "audio/raw", + "application/mp4", + "application/webm", + "application/x-matroska", + "application/dash+xml", + "application/x-mpegURL", + "application/vnd.ms-sstr+xml" -> + mimeType; + default -> ""; + }; + } + + private static int getCodecEnum(String mimeType) { + if (TextUtils.isEmpty(mimeType)) { + return Enums.CODEC_UNKNOWN; + } + return switch (mimeType) { + case "video/avc" -> Enums.CODEC_AVC; + case "video/hevc" -> Enums.CODEC_HEVC; + case "video/x-vnd.on2.vp8" -> Enums.CODEC_VP8; + case "video/x-vnd.on2.vp9" -> Enums.CODEC_VP9; + case "video/av01" -> Enums.CODEC_AV1; + default -> Enums.CODEC_UNKNOWN; + }; + } + + private static int getFilteredAudioSampleRateHz(int sampleRateHz) { + return switch (sampleRateHz) { + case 8000, 11025, 16000, 22050, 44100, 48000, 96000, 192000 -> sampleRateHz; + default -> -1; + }; + } + + private static int getVideoResolutionEnum(Size size) { + int width = size.getWidth(); + int height = size.getHeight(); + if (width == 352 && height == 640) { + return Enums.RESOLUTION_352X640; + } else if (width == 360 && height == 640) { + return Enums.RESOLUTION_360X640; + } else if (width == 480 && height == 640) { + return Enums.RESOLUTION_480X640; + } else if (width == 480 && height == 854) { + return Enums.RESOLUTION_480X854; + } else if (width == 540 && height == 960) { + return Enums.RESOLUTION_540X960; + } else if (width == 576 && height == 1024) { + return Enums.RESOLUTION_576X1024; + } else if (width == 1280 && height == 720) { + return Enums.RESOLUTION_720P_HD; + } else if (width == 1920 && height == 1080) { + return Enums.RESOLUTION_1080P_FHD; + } else if (width == 1440 && height == 2560) { + return Enums.RESOLUTION_1440X2560; + } else if (width == 3840 && height == 2160) { + return Enums.RESOLUTION_4K_UHD; + } else if (width == 7680 && height == 4320) { + return Enums.RESOLUTION_8K_UHD; + } else { + return Enums.RESOLUTION_UNKNOWN; + } + } + + private static int getVideoResolutionAspectRatioEnum(Size size) { + int width = size.getWidth(); + int height = size.getHeight(); + if (width <= 0 || height <= 0) { + return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_UNSPECIFIED; + } else if (width < height) { + return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_PORTRAIT; + } else if (height < width) { + return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_LANDSCAPE; + } else { + return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_SQUARE; + } + } + + private static int getVideoHdrFormatEnum(int dataSpace, String mimeType) { + if (dataSpace == DataSpace.DATASPACE_UNKNOWN) { + return Enums.HDR_FORMAT_UNKNOWN; + } + if (mimeType.equals("video/dolby-vision")) { + return Enums.HDR_FORMAT_DOLBY_VISION; + } + int standard = DataSpace.getStandard(dataSpace); + int transfer = DataSpace.getTransfer(dataSpace); + if (standard == DataSpace.STANDARD_BT2020 && transfer == DataSpace.TRANSFER_HLG) { + return Enums.HDR_FORMAT_HLG; + } + if (standard == DataSpace.STANDARD_BT2020 && transfer == DataSpace.TRANSFER_ST2084) { + // We don't currently distinguish HDR10+ from HDR10. + return Enums.HDR_FORMAT_HDR10; + } + return Enums.HDR_FORMAT_NONE; + } + + private static int getVideoFrameRateEnum(float frameRate) { + int frameRateInt = Math.round(frameRate); + return switch (frameRateInt) { + case 24 -> Enums.FRAMERATE_24; + case 25 -> Enums.FRAMERATE_25; + case 30 -> Enums.FRAMERATE_30; + case 50 -> Enums.FRAMERATE_50; + case 60 -> Enums.FRAMERATE_60; + case 120 -> Enums.FRAMERATE_120; + case 240 -> Enums.FRAMERATE_240; + case 480 -> Enums.FRAMERATE_480; + case 960 -> Enums.FRAMERATE_960; + default -> Enums.FRAMERATE_UNKNOWN; + }; + } } diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java index d0c054307d0c..f645eaa28632 100644 --- a/services/core/java/com/android/server/notification/ManagedServices.java +++ b/services/core/java/com/android/server/notification/ManagedServices.java @@ -16,6 +16,7 @@ package com.android.server.notification; +import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR; import static android.content.Context.BIND_ALLOW_WHITELIST_MANAGEMENT; import static android.content.Context.BIND_AUTO_CREATE; import static android.content.Context.BIND_FOREGROUND_SERVICE; @@ -24,6 +25,7 @@ import static android.os.UserHandle.USER_ALL; import static android.os.UserHandle.USER_SYSTEM; import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.app.ActivityManager; import android.app.ActivityOptions; @@ -1802,6 +1804,8 @@ abstract public class ManagedServices { public ComponentName component; public int userid; public boolean isSystem; + @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR) + public boolean isSystemUi; public ServiceConnection connection; public int targetSdkVersion; public Pair<ComponentName, Integer> mKey; @@ -1836,6 +1840,11 @@ abstract public class ManagedServices { return isSystem; } + @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR) + public boolean isSystemUi() { + return isSystemUi; + } + @Override public String toString() { return new StringBuilder("ManagedServiceInfo[") diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index e7ad99a8cf20..3507d2d56cbd 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -71,6 +71,7 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_ON; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; +import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR; import static android.app.Flags.lifetimeExtensionRefactor; import static android.app.NotificationManager.zenModeFromInterruptionFilter; import static android.app.StatusBarManager.ACTION_KEYGUARD_PRIVATE_NOTIFICATIONS_CHANGED; @@ -159,6 +160,7 @@ import android.Manifest; import android.Manifest.permission; import android.annotation.DurationMillisLong; import android.annotation.ElapsedRealtimeLong; +import android.annotation.FlaggedApi; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; @@ -1852,6 +1854,7 @@ public class NotificationManagerService extends SystemService { } if (ACTION_NOTIFICATION_TIMEOUT.equals(action)) { final NotificationRecord record; + // TODO: b/323013410 - Record should be cloned instead of used directly. synchronized (mNotificationLock) { record = findNotificationByKeyLocked(intent.getStringExtra(EXTRA_KEY)); } @@ -1864,6 +1867,14 @@ public class NotificationManagerService extends SystemService { FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB | FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true, record.getUserId(), REASON_TIMEOUT, null); + // If cancellation will be prevented due to lifetime extension, we send an + // update to system UI. + synchronized (mNotificationLock) { + maybeNotifySystemUiListenerLifetimeExtendedLocked(record, + record.getSbn().getPackageName(), + mActivityManager.getPackageImportance( + record.getSbn().getPackageName())); + } } else { cancelNotification(record.getSbn().getUid(), record.getSbn().getInitialPid(), @@ -3825,7 +3836,17 @@ public class NotificationManagerService extends SystemService { int mustNotHaveFlags = isCallingUidSystem() ? 0 : (FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB | FLAG_AUTOGROUP_SUMMARY); if (lifetimeExtensionRefactor()) { + // Also don't allow client apps to cancel lifetime extended notifs. mustNotHaveFlags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY; + // If cancellation will be prevented due to lifetime extension, we send an update to + // system UI. + NotificationRecord record = null; + final int packageImportance = mActivityManager.getPackageImportance(pkg); + synchronized (mNotificationLock) { + record = findNotificationLocked(pkg, tag, id, userId); + maybeNotifySystemUiListenerLifetimeExtendedLocked(record, pkg, + packageImportance); + } } cancelNotificationInternal(pkg, opPkg, Binder.getCallingUid(), Binder.getCallingPid(), @@ -3845,6 +3866,16 @@ public class NotificationManagerService extends SystemService { pkg, null, 0, FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB | FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, userId, REASON_APP_CANCEL_ALL); + // If cancellation will be prevented due to lifetime extension, we send updates + // to system UI. + // In this case, we need to hold the lock to access these lists. + final int packageImportance = mActivityManager.getPackageImportance(pkg); + synchronized (mNotificationLock) { + notifySystemUiListenerLifetimeExtendedListLocked(mNotificationList, + packageImportance); + notifySystemUiListenerLifetimeExtendedListLocked(mEnqueuedNotifications, + packageImportance); + } } else { cancelAllNotificationsInt(Binder.getCallingUid(), Binder.getCallingPid(), pkg, null, 0, FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB, @@ -4891,11 +4922,19 @@ public class NotificationManagerService extends SystemService { final long identity = Binder.clearCallingIdentity(); boolean notificationsRapidlyCleared = false; final String pkg; + final int packageImportance; + final ManagedServiceInfo info; try { synchronized (mNotificationLock) { - final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); + info = mListeners.checkServiceTokenLocked(token); pkg = info.component.getPackageName(); - + } + if (lifetimeExtensionRefactor()) { + packageImportance = mActivityManager.getPackageImportance(pkg); + } else { + packageImportance = IMPORTANCE_NONE; + } + synchronized (mNotificationLock) { // Cancellation reason. If the token comes from assistant, label the // cancellation as coming from the assistant; default to LISTENER_CANCEL. int reason = REASON_LISTENER_CANCEL; @@ -4917,7 +4956,7 @@ public class NotificationManagerService extends SystemService { || isNotificationRecent(r.getUpdateTimeMs()); cancelNotificationFromListenerLocked(info, callingUid, callingPid, r.getSbn().getPackageName(), r.getSbn().getTag(), - r.getSbn().getId(), userId, reason); + r.getSbn().getId(), userId, reason, packageImportance); } } else { for (NotificationRecord notificationRecord : mNotificationList) { @@ -4931,6 +4970,12 @@ public class NotificationManagerService extends SystemService { REASON_LISTENER_CANCEL_ALL, info, info.supportsProfiles(), FLAG_ONGOING_EVENT | FLAG_NO_CLEAR | FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY); + // If cancellation will be prevented due to lifetime extension, we send + // an update to system UI. + notifySystemUiListenerLifetimeExtendedListLocked(mNotificationList, + packageImportance); + notifySystemUiListenerLifetimeExtendedListLocked(mEnqueuedNotifications, + packageImportance); } else { cancelAllLocked(callingUid, callingPid, info.userid, REASON_LISTENER_CANCEL_ALL, info, info.supportsProfiles(), @@ -5051,10 +5096,14 @@ public class NotificationManagerService extends SystemService { @GuardedBy("mNotificationLock") private void cancelNotificationFromListenerLocked(ManagedServiceInfo info, int callingUid, int callingPid, String pkg, String tag, int id, int userId, - int reason) { + int reason, int packageImportance) { int mustNotHaveFlags = FLAG_ONGOING_EVENT; if (lifetimeExtensionRefactor()) { mustNotHaveFlags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY; + // If cancellation will be prevented due to lifetime extension, we send an update + // to system UI. + NotificationRecord record = findNotificationLocked(pkg, tag, id, userId); + maybeNotifySystemUiListenerLifetimeExtendedLocked(record, pkg, packageImportance); } cancelNotification(callingUid, callingPid, pkg, tag, id, 0 /* mustHaveFlags */, mustNotHaveFlags, @@ -5197,7 +5246,13 @@ public class NotificationManagerService extends SystemService { final int callingUid = Binder.getCallingUid(); final int callingPid = Binder.getCallingPid(); final long identity = Binder.clearCallingIdentity(); + final int packageImportance; try { + if (lifetimeExtensionRefactor()) { + packageImportance = mActivityManager.getPackageImportance(pkg); + } else { + packageImportance = IMPORTANCE_NONE; + } synchronized (mNotificationLock) { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); int cancelReason = REASON_LISTENER_CANCEL; @@ -5210,7 +5265,7 @@ public class NotificationManagerService extends SystemService { + " use cancelNotification(key) instead."); } else { cancelNotificationFromListenerLocked(info, callingUid, callingPid, - pkg, tag, id, info.userid, cancelReason); + pkg, tag, id, info.userid, cancelReason, packageImportance); } } } finally { @@ -11654,6 +11709,30 @@ public class NotificationManagerService extends SystemService { }); } + @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR) + @GuardedBy("mNotificationLock") + private void notifySystemUiListenerLifetimeExtendedListLocked( + List<NotificationRecord> notificationList, int packageImportance) { + for (int i = notificationList.size() - 1; i >= 0; --i) { + NotificationRecord record = notificationList.get(i); + maybeNotifySystemUiListenerLifetimeExtendedLocked(record, + record.getSbn().getPackageName(), packageImportance); + } + } + + @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR) + @GuardedBy("mNotificationLock") + private void maybeNotifySystemUiListenerLifetimeExtendedLocked(NotificationRecord record, + String pkg, int packageImportance) { + if (record != null && (record.getSbn().getNotification().flags + & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) { + boolean isAppForeground = pkg != null && packageImportance == IMPORTANCE_FOREGROUND; + mHandler.post(new EnqueueNotificationRunnable(record.getUser().getIdentifier(), + record, isAppForeground, + mPostNotificationTrackerFactory.newTracker(null))); + } + } + public class NotificationListeners extends ManagedServices { static final String TAG_ENABLED_NOTIFICATION_LISTENERS = "enabled_listeners"; static final String TAG_REQUESTED_LISTENERS = "request_listeners"; @@ -11777,6 +11856,11 @@ public class NotificationManagerService extends SystemService { @Override public void onServiceAdded(ManagedServiceInfo info) { + if (lifetimeExtensionRefactor()) { + // Only System or System UI can call registerSystemService, so if the caller is not + // system, we know it's system UI. + info.isSystemUi = !isCallerSystemOrPhone(); + } final INotificationListener listener = (INotificationListener) info.service; final NotificationRankingUpdate update; synchronized (mNotificationLock) { @@ -12141,6 +12225,23 @@ public class NotificationManagerService extends SystemService { continue; } + if (lifetimeExtensionRefactor()) { + // Checks if this is a request to notify system UI about a notification that + // has been lifetime extended. + // (We only need to check old for the flag, because in both cancellation and + // update cases, old should have the flag.) + // If it is such a request, and this is system UI, we send the post request + // only to System UI, and break as we don't need to continue checking other + // Managed Services. + if (info.isSystemUi() && old != null && old.getNotification() != null + && (old.getNotification().flags + & Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) { + final NotificationRankingUpdate update = makeRankingUpdateLocked(info); + listenerCalls.add(() -> notifyPosted(info, oldSbn, update)); + break; + } + } + // If we shouldn't notify all listeners, this means the hidden state of // a notification was changed. Don't notifyPosted listeners targeting >= P. // Instead, those listeners will receive notifyRankingUpdate. diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 05d1c49b3508..85eac29599f5 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -98,6 +98,7 @@ import static android.app.AppOpsManager.OPSTR_SYSTEM_EXEMPT_FROM_HIBERNATION; import static android.app.AppOpsManager.OPSTR_SYSTEM_EXEMPT_FROM_POWER_RESTRICTIONS; import static android.app.AppOpsManager.OPSTR_SYSTEM_EXEMPT_FROM_SUSPENSION; import static android.app.admin.DeviceAdminInfo.HEADLESS_DEVICE_OWNER_MODE_AFFILIATED; +import static android.app.admin.DeviceAdminInfo.HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER; import static android.app.admin.DeviceAdminInfo.USES_POLICY_FORCE_LOCK; import static android.app.admin.DeviceAdminInfo.USES_POLICY_WIPE_DATA; import static android.app.admin.DeviceAdminReceiver.ACTION_COMPLIANCE_ACKNOWLEDGEMENT_REQUIRED; @@ -189,6 +190,7 @@ import static android.app.admin.DevicePolicyManager.STATUS_HEADLESS_SYSTEM_USER_ import static android.app.admin.DevicePolicyManager.STATUS_MANAGED_USERS_NOT_SUPPORTED; import static android.app.admin.DevicePolicyManager.STATUS_NONSYSTEM_USER_EXISTS; import static android.app.admin.DevicePolicyManager.STATUS_NOT_SYSTEM_USER; +import static android.app.admin.DevicePolicyManager.STATUS_HEADLESS_ONLY_SYSTEM_USER; import static android.app.admin.DevicePolicyManager.STATUS_OK; import static android.app.admin.DevicePolicyManager.STATUS_PROVISIONING_NOT_ALLOWED_FOR_NON_DEVELOPER_USERS; import static android.app.admin.DevicePolicyManager.STATUS_SYSTEM_USER; @@ -225,6 +227,7 @@ import static android.app.admin.ProvisioningException.ERROR_SET_DEVICE_OWNER_FAI import static android.app.admin.ProvisioningException.ERROR_STARTING_PROFILE_FAILED; import static android.app.admin.flags.Flags.backupServiceSecurityLogEventEnabled; import static android.app.admin.flags.Flags.dumpsysPolicyEngineMigrationEnabled; +import static android.app.admin.flags.Flags.headlessDeviceOwnerSingleUserEnabled; import static android.app.admin.flags.Flags.policyEngineMigrationV2Enabled; import static android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE; import static android.content.Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE; @@ -9460,7 +9463,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } if (setProfileOwnerOnCurrentUserIfNecessary - && mInjector.userManagerIsHeadlessSystemUserMode()) { + && mInjector.userManagerIsHeadlessSystemUserMode() + && getHeadlessDeviceOwnerMode() == HEADLESS_DEVICE_OWNER_MODE_AFFILIATED) { int currentForegroundUser; synchronized (getLockObject()) { currentForegroundUser = getCurrentForegroundUserId(); @@ -9476,6 +9480,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return true; } + private int getHeadlessDeviceOwnerMode() { + synchronized (getLockObject()) { + return getDeviceOwnerAdminLocked().info.getHeadlessDeviceOwnerMode(); + } + } + /** * This API is cached: invalidate with invalidateBinderCaches(). */ @@ -12226,6 +12236,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { + admin + " are not in the same package"); } final CallerIdentity caller = getCallerIdentity(admin); + + if (headlessDeviceOwnerSingleUserEnabled()) { + // Block this method if the device is in headless main user mode + Preconditions.checkCallAuthorization( + getHeadlessDeviceOwnerMode() != HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER, + "createAndManageUser was called while in headless single user mode"); + } // Only allow the system user to use this method Preconditions.checkCallAuthorization(caller.getUserHandle().isSystem(), "createAndManageUser was called from non-system user"); @@ -16636,29 +16653,51 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return STATUS_USER_NOT_RUNNING; } + DeviceAdminInfo adminInfo = null; + + boolean isHeadlessModeAffiliated = false; + + boolean isHeadlessModeSingleUser = false; + boolean isHeadlessSystemUserMode = mInjector.userManagerIsHeadlessSystemUserMode(); + int ensureSetUpUser = UserHandle.USER_SYSTEM; if (isHeadlessSystemUserMode) { - if (deviceOwnerUserId != UserHandle.USER_SYSTEM) { - Slogf.e(LOG_TAG, "In headless system user mode, " - + "device owner can only be set on headless system user."); - return STATUS_NOT_SYSTEM_USER; - } - if (owner != null) { - DeviceAdminInfo adminInfo = findAdmin( - owner, deviceOwnerUserId, /* throwForMissingPermission= */ false); + adminInfo = findAdmin(owner, + deviceOwnerUserId, /* throwForMissingPermission= */ false); - if (adminInfo.getHeadlessDeviceOwnerMode() - != HEADLESS_DEVICE_OWNER_MODE_AFFILIATED) { + isHeadlessModeAffiliated = + adminInfo.getHeadlessDeviceOwnerMode() + == HEADLESS_DEVICE_OWNER_MODE_AFFILIATED; + + isHeadlessModeSingleUser = + adminInfo.getHeadlessDeviceOwnerMode() + == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER; + + if (!isHeadlessModeAffiliated && !isHeadlessModeSingleUser) { return STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED; } + + if (headlessDeviceOwnerSingleUserEnabled() && isHeadlessModeSingleUser) { + ensureSetUpUser = mUserManagerInternal.getMainUserId(); + if (ensureSetUpUser == UserHandle.USER_NULL) { + return STATUS_HEADLESS_ONLY_SYSTEM_USER; + } + } + } + + if (isHeadlessModeAffiliated && deviceOwnerUserId != UserHandle.USER_SYSTEM) { + Slogf.e(LOG_TAG, "In headless system user mode, " + + "device owner can only be set on headless system user."); + return STATUS_NOT_SYSTEM_USER; } + } if (isAdb) { // If shell command runs after user setup completed check device status. Otherwise, OK. - if (hasUserSetupCompleted(UserHandle.USER_SYSTEM)) { + if (hasUserSetupCompleted(ensureSetUpUser)) { // DO can be setup only if there are no users which are neither created by default // nor marked as FOR_TESTING @@ -16681,11 +16720,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return STATUS_OK; } else { // DO has to be user 0 - if (deviceOwnerUserId != UserHandle.USER_SYSTEM) { + if ((!isHeadlessSystemUserMode || isHeadlessModeAffiliated) + && deviceOwnerUserId != UserHandle.USER_SYSTEM) { return STATUS_NOT_SYSTEM_USER; } // Only provision DO before setup wizard completes - if (hasUserSetupCompleted(UserHandle.USER_SYSTEM)) { + if (hasUserSetupCompleted(ensureSetUpUser)) { return STATUS_USER_SETUP_COMPLETED; } return STATUS_OK; @@ -21260,7 +21300,11 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { setTimeAndTimezone(provisioningParams.getTimeZone(), provisioningParams.getLocalTime()); setLocale(provisioningParams.getLocale()); - int deviceOwnerUserId = UserHandle.USER_SYSTEM; + int deviceOwnerUserId = headlessDeviceOwnerSingleUserEnabled() + && getHeadlessDeviceOwnerMode() == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER + ? mUserManagerInternal.getMainUserId() + : UserHandle.USER_SYSTEM; + if (!removeNonRequiredAppsForManagedDevice( deviceOwnerUserId, provisioningParams.isLeaveAllSystemAppsEnabled(), 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 28471b37d2a0..6bcd778c234b 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 @@ -49,7 +49,6 @@ import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CONNECTIVI import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CONTENT_TRIGGER; import static com.android.server.job.controllers.JobStatus.CONSTRAINT_FLEXIBLE; import static com.android.server.job.controllers.JobStatus.CONSTRAINT_IDLE; -import static com.android.server.job.controllers.JobStatus.MIN_WINDOW_FOR_FLEXIBILITY_MS; import static com.android.server.job.controllers.JobStatus.NO_LATEST_RUNTIME; import static org.junit.Assert.assertArrayEquals; @@ -410,10 +409,12 @@ public class FlexibilityControllerTest { @Test public void testOnConstantsUpdated_PercentsToDropConstraints() { + final long fallbackDuration = 12 * HOUR_IN_MILLIS; JobInfo.Builder jb = createJob(0) - .setOverrideDeadline(MIN_WINDOW_FOR_FLEXIBILITY_MS); + .setOverrideDeadline(HOUR_IN_MILLIS); JobStatus js = createJobStatus("testPercentsToDropConstraintsConfig", jb); - assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 5, + // Even though the override deadline is 1 hour, the fallback duration is still used. + assertEquals(FROZEN_TIME + fallbackDuration / 10 * 5, mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js)); setDeviceConfigString(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS, "500=1|2|3|4" @@ -441,13 +442,13 @@ public class FlexibilityControllerTest { mFlexibilityController.mFcConfig.PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS .get(JobInfo.PRIORITY_MIN), new int[]{54, 55, 56, 57}); - assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10, + assertEquals(FROZEN_TIME + fallbackDuration / 10, mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js)); js.setNumDroppedFlexibleConstraints(1); - assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 2, + assertEquals(FROZEN_TIME + fallbackDuration / 10 * 2, mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js)); js.setNumDroppedFlexibleConstraints(2); - assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 3, + assertEquals(FROZEN_TIME + fallbackDuration / 10 * 3, mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js)); } @@ -504,37 +505,38 @@ public class FlexibilityControllerTest { @Test public void testGetNextConstraintDropTimeElapsedLocked() { + final long fallbackDuration = 50 * HOUR_IN_MILLIS; setDeviceConfigLong(KEY_FALLBACK_FLEXIBILITY_DEADLINE, 200 * HOUR_IN_MILLIS); setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES, "500=" + HOUR_IN_MILLIS + ",400=" + 25 * HOUR_IN_MILLIS - + ",300=" + 50 * HOUR_IN_MILLIS + + ",300=" + fallbackDuration + ",200=" + 100 * HOUR_IN_MILLIS + ",100=" + 200 * HOUR_IN_MILLIS); long nextTimeToDropNumConstraints; // no delay, deadline - JobInfo.Builder jb = createJob(0).setOverrideDeadline(MIN_WINDOW_FOR_FLEXIBILITY_MS); + JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS); JobStatus js = createJobStatus("time", jb); assertEquals(JobStatus.NO_EARLIEST_RUNTIME, js.getEarliestRunTime()); - assertEquals(MIN_WINDOW_FOR_FLEXIBILITY_MS + FROZEN_TIME, js.getLatestRunTimeElapsed()); + assertEquals(HOUR_IN_MILLIS + FROZEN_TIME, js.getLatestRunTimeElapsed()); assertEquals(FROZEN_TIME, js.enqueueTime); nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 5, + assertEquals(FROZEN_TIME + fallbackDuration / 10 * 5, nextTimeToDropNumConstraints); js.setNumDroppedFlexibleConstraints(1); nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 6, + assertEquals(FROZEN_TIME + fallbackDuration / 10 * 6, nextTimeToDropNumConstraints); js.setNumDroppedFlexibleConstraints(2); nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 7, + assertEquals(FROZEN_TIME + fallbackDuration / 10 * 7, nextTimeToDropNumConstraints); // delay, no deadline @@ -574,81 +576,83 @@ public class FlexibilityControllerTest { // delay, deadline jb = createJob(0) - .setOverrideDeadline(2 * MIN_WINDOW_FOR_FLEXIBILITY_MS) - .setMinimumLatency(MIN_WINDOW_FOR_FLEXIBILITY_MS); + .setOverrideDeadline(2 * HOUR_IN_MILLIS) + .setMinimumLatency(HOUR_IN_MILLIS); js = createJobStatus("time", jb); - final long windowStart = FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS; + final long windowStart = FROZEN_TIME + HOUR_IN_MILLIS; nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(windowStart + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 5, + assertEquals(windowStart + fallbackDuration / 10 * 5, nextTimeToDropNumConstraints); js.setNumDroppedFlexibleConstraints(1); nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(windowStart + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 6, + assertEquals(windowStart + fallbackDuration / 10 * 6, nextTimeToDropNumConstraints); js.setNumDroppedFlexibleConstraints(2); nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(windowStart + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 7, + assertEquals(windowStart + fallbackDuration / 10 * 7, nextTimeToDropNumConstraints); } @Test public void testCurPercent() { + final long fallbackDuration = 10 * HOUR_IN_MILLIS; + setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES, "300=" + fallbackDuration); long deadline = 100 * MINUTE_IN_MILLIS; long nowElapsed = FROZEN_TIME; JobInfo.Builder jb = createJob(0).setOverrideDeadline(deadline); JobStatus js = createJobStatus("time", jb); assertEquals(FROZEN_TIME, mFlexibilityController.getLifeCycleBeginningElapsedLocked(js)); - assertEquals(deadline + FROZEN_TIME, + assertEquals(FROZEN_TIME + fallbackDuration, mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, FROZEN_TIME)); - nowElapsed = FROZEN_TIME + 60 * MINUTE_IN_MILLIS; + nowElapsed = FROZEN_TIME + 6 * HOUR_IN_MILLIS; JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC); assertEquals(60, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed)); - nowElapsed = FROZEN_TIME + 130 * MINUTE_IN_MILLIS; + nowElapsed = FROZEN_TIME + 13 * HOUR_IN_MILLIS; JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC); assertEquals(100, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed)); - nowElapsed = FROZEN_TIME + 95 * MINUTE_IN_MILLIS; + nowElapsed = FROZEN_TIME + 9 * HOUR_IN_MILLIS; JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC); - assertEquals(95, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed)); + assertEquals(90, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed)); nowElapsed = FROZEN_TIME; JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC); - long delay = MINUTE_IN_MILLIS; - deadline = 101 * MINUTE_IN_MILLIS; + long delay = HOUR_IN_MILLIS; + deadline = HOUR_IN_MILLIS + 100 * MINUTE_IN_MILLIS; jb = createJob(0).setOverrideDeadline(deadline).setMinimumLatency(delay); js = createJobStatus("time", jb); assertEquals(FROZEN_TIME + delay, mFlexibilityController.getLifeCycleBeginningElapsedLocked(js)); - assertEquals(deadline + FROZEN_TIME, + assertEquals(FROZEN_TIME + delay + fallbackDuration, mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, FROZEN_TIME + delay)); - nowElapsed = FROZEN_TIME + delay + 60 * MINUTE_IN_MILLIS; + nowElapsed = FROZEN_TIME + delay + 6 * HOUR_IN_MILLIS; JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC); assertEquals(60, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed)); - nowElapsed = FROZEN_TIME + 130 * MINUTE_IN_MILLIS; + nowElapsed = FROZEN_TIME + 13 * HOUR_IN_MILLIS; JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC); assertEquals(100, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed)); - nowElapsed = FROZEN_TIME + delay + 95 * MINUTE_IN_MILLIS; + nowElapsed = FROZEN_TIME + delay + 9 * HOUR_IN_MILLIS; JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC); - assertEquals(95, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed)); + assertEquals(90, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed)); } @Test @@ -786,26 +790,27 @@ public class FlexibilityControllerTest { // deadline JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS); JobStatus js = createJobStatus("time", jb); - assertEquals(HOUR_IN_MILLIS + FROZEN_TIME, - mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0)); + assertEquals(3 * HOUR_IN_MILLIS + js.enqueueTime, + mFlexibilityController + .getLifeCycleEndElapsedLocked(js, nowElapsed, js.enqueueTime)); // no deadline - assertEquals(FROZEN_TIME + 2 * HOUR_IN_MILLIS, + assertEquals(js.enqueueTime + 2 * HOUR_IN_MILLIS, mFlexibilityController.getLifeCycleEndElapsedLocked( createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_HIGH)), - nowElapsed, 100L)); - assertEquals(FROZEN_TIME + 3 * HOUR_IN_MILLIS, + nowElapsed, js.enqueueTime)); + assertEquals(js.enqueueTime + 3 * HOUR_IN_MILLIS, mFlexibilityController.getLifeCycleEndElapsedLocked( createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT)), - nowElapsed, 100L)); - assertEquals(FROZEN_TIME + 4 * HOUR_IN_MILLIS, + nowElapsed, js.enqueueTime)); + assertEquals(js.enqueueTime + 4 * HOUR_IN_MILLIS, mFlexibilityController.getLifeCycleEndElapsedLocked( createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_LOW)), - nowElapsed, 100L)); - assertEquals(FROZEN_TIME + 5 * HOUR_IN_MILLIS, + nowElapsed, js.enqueueTime)); + assertEquals(js.enqueueTime + 5 * HOUR_IN_MILLIS, mFlexibilityController.getLifeCycleEndElapsedLocked( createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_MIN)), - nowElapsed, 100L)); + nowElapsed, js.enqueueTime)); } @Test @@ -871,14 +876,16 @@ public class FlexibilityControllerTest { mFlexibilityController.prepareForExecutionLocked(jsLow); mFlexibilityController.prepareForExecutionLocked(jsMin); - // deadline - JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS); - JobStatus js = createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition", jb); - assertEquals(HOUR_IN_MILLIS + FROZEN_TIME, - mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0)); + final long longDeadlineMs = 14 * 24 * HOUR_IN_MILLIS; + JobInfo.Builder jbWithLongDeadline = createJob(0).setOverrideDeadline(longDeadlineMs); + JobStatus jsWithLongDeadline = createJobStatus( + "testGetLifeCycleEndElapsedLocked_ScoreAddition", jbWithLongDeadline); + JobInfo.Builder jbWithShortDeadline = + createJob(0).setOverrideDeadline(15 * MINUTE_IN_MILLIS); + JobStatus jsWithShortDeadline = createJobStatus( + "testGetLifeCycleEndElapsedLocked_ScoreAddition", jbWithShortDeadline); final long earliestMs = 123L; - // no deadline assertEquals(earliestMs + HOUR_IN_MILLIS + 5 * 15 * MINUTE_IN_MILLIS, mFlexibilityController.getLifeCycleEndElapsedLocked( createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition", @@ -894,6 +901,9 @@ public class FlexibilityControllerTest { createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition", createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT)), nowElapsed, earliestMs)); + assertEquals(earliestMs + HOUR_IN_MILLIS + 3 * 15 * MINUTE_IN_MILLIS, + mFlexibilityController.getLifeCycleEndElapsedLocked( + jsWithShortDeadline, nowElapsed, earliestMs)); assertEquals(earliestMs + HOUR_IN_MILLIS + 2 * 15 * MINUTE_IN_MILLIS, mFlexibilityController.getLifeCycleEndElapsedLocked( createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition", @@ -904,6 +914,9 @@ public class FlexibilityControllerTest { createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition", createJob(0).setPriority(JobInfo.PRIORITY_MIN)), nowElapsed, earliestMs)); + assertEquals(jsWithLongDeadline.enqueueTime + longDeadlineMs, + mFlexibilityController.getLifeCycleEndElapsedLocked( + jsWithLongDeadline, nowElapsed, earliestMs)); } @Test @@ -1033,8 +1046,8 @@ public class FlexibilityControllerTest { JobInfo.Builder jb = createJob(0); jb.setMinimumLatency(1); jb.setOverrideDeadline(2); - JobStatus js = createJobStatus("Disable Flexible When Job Has Short Window", jb); - assertFalse(js.hasFlexibilityConstraint()); + JobStatus js = createJobStatus("testExceptions_ShortWindow", jb); + assertTrue(js.hasFlexibilityConstraint()); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java index 344a4b0ce324..4dded1d0342d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java @@ -16,6 +16,7 @@ package com.android.server.notification; import static android.content.Context.DEVICE_POLICY_SERVICE; +import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR; import static android.os.UserManager.USER_TYPE_FULL_SECONDARY; import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; @@ -62,6 +63,7 @@ import android.os.IInterface; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.annotations.EnableFlags; import android.provider.Settings; import android.text.TextUtils; import android.util.ArrayMap; @@ -1983,6 +1985,22 @@ public class ManagedServicesTest extends UiServiceTestCase { new ComponentName("pkg1", "cmp1"))).isFalse(); } + @Test + @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR) + public void testManagedServiceInfoIsSystemUi() { + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, + APPROVAL_BY_COMPONENT); + + ManagedServices.ManagedServiceInfo service0 = service.new ManagedServiceInfo( + mock(IInterface.class), ComponentName.unflattenFromString("a/a"), 0, false, + mock(ServiceConnection.class), 26, 34); + + service0.isSystemUi = true; + assertThat(service0.isSystemUi()).isTrue(); + service0.isSystemUi = false; + assertThat(service0.isSystemUi()).isFalse(); + } + private void mockServiceInfoWithMetaData(List<ComponentName> componentNames, ManagedServices service, ArrayMap<ComponentName, Bundle> metaDatas) throws RemoteException { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 96ffec1509da..046e0570d439 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -2545,6 +2545,17 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertThat(mBinderService.getActiveNotifications(sbn.getPackageName()).length).isEqualTo(1); assertThat(mService.getNotificationRecordCount()).isEqualTo(1); + // Checks that a post update is sent. + verify(mWorkerHandler, times(1)) + .post(any(NotificationManagerService.PostNotificationRunnable.class)); + ArgumentCaptor<NotificationRecord> captor = + ArgumentCaptor.forClass(NotificationRecord.class); + verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(), + anyBoolean()); + assertThat(captor.getValue().getNotification().flags + & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo( + FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY); + mSetFlagsRule.disableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR); mBinderService.cancelNotificationWithTag(PKG, PKG, sbn.getTag(), sbn.getId(), sbn.getUserId()); @@ -2577,6 +2588,17 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { StatusBarNotification[] notifs = mBinderService.getActiveNotifications(PKG); assertThat(notifs.length).isEqualTo(1); assertThat(notifs[0].getId()).isEqualTo(1); + + // Checks that a post update is sent. + verify(mWorkerHandler, times(1)) + .post(any(NotificationManagerService.PostNotificationRunnable.class)); + ArgumentCaptor<NotificationRecord> captor = + ArgumentCaptor.forClass(NotificationRecord.class); + verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(), + anyBoolean()); + assertThat(captor.getValue().getNotification().flags + & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo( + FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY); } @Test @@ -2985,18 +3007,29 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testCancelNotificationsFromListener_clearAll_NoClearLifetimeExt() throws Exception { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR); - final NotificationRecord notif = generateNotificationRecord( mTestNotificationChannel, 1, null, false); - notif.getNotification().flags = FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY; + notif.getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY; mService.addNotification(notif); - + verify(mWorkerHandler, times(0)) + .post(any(NotificationManagerService.PostNotificationRunnable.class)); mService.getBinderService().cancelNotificationsFromListener(null, null); waitForIdle(); - + // Notification not cancelled. StatusBarNotification[] notifs = mBinderService.getActiveNotifications(notif.getSbn().getPackageName()); assertThat(notifs.length).isEqualTo(1); + + // Checks that a post update is sent. + verify(mWorkerHandler, times(1)) + .post(any(NotificationManagerService.PostNotificationRunnable.class)); + ArgumentCaptor<NotificationRecord> captor = + ArgumentCaptor.forClass(NotificationRecord.class); + verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(), + anyBoolean()); + assertThat(captor.getValue().getNotification().flags + & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo( + FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY); } @Test @@ -3217,6 +3250,17 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { StatusBarNotification[] notifs = mBinderService.getActiveNotifications(notif.getSbn().getPackageName()); assertEquals(1, notifs.length); + + // Checks that a post update is sent. + verify(mWorkerHandler, times(1)) + .post(any(NotificationManagerService.PostNotificationRunnable.class)); + ArgumentCaptor<NotificationRecord> captor = + ArgumentCaptor.forClass(NotificationRecord.class); + verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(), + anyBoolean()); + assertThat(captor.getValue().getNotification().flags + & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo( + FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY); } @Test @@ -5659,6 +5703,17 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG); assertThat(notifsAfter.length).isEqualTo(1); assertThat(mService.getNotificationRecord(notif.getKey())).isEqualTo(notif); + + // Checks that a post update is sent. + verify(mWorkerHandler, times(1)) + .post(any(NotificationManagerService.PostNotificationRunnable.class)); + ArgumentCaptor<NotificationRecord> captor = + ArgumentCaptor.forClass(NotificationRecord.class); + verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(), + anyBoolean()); + assertThat(captor.getValue().getNotification().flags + & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo( + FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY); } @Test diff --git a/services/usb/Android.bp b/services/usb/Android.bp index 3a0a6abb2307..e8ffe541d641 100644 --- a/services/usb/Android.bp +++ b/services/usb/Android.bp @@ -34,8 +34,20 @@ java_library_static { "android.hardware.usb-V1.2-java", "android.hardware.usb-V1.3-java", "android.hardware.usb-V3-java", + "usb_flags_lib", ], lint: { baseline_filename: "lint-baseline.xml", }, } + +aconfig_declarations { + name: "usb_flags", + package: "com.android.server.usb.flags", + srcs: ["**/usb_flags.aconfig"], +} + +java_aconfig_library { + name: "usb_flags_lib", + aconfig_declarations: "usb_flags", +} diff --git a/services/usb/java/com/android/server/usb/UsbHandlerManager.java b/services/usb/java/com/android/server/usb/UsbHandlerManager.java index f3112743bcf2..d83ff1fbc381 100644 --- a/services/usb/java/com/android/server/usb/UsbHandlerManager.java +++ b/services/usb/java/com/android/server/usb/UsbHandlerManager.java @@ -37,7 +37,7 @@ import java.util.ArrayList; * * @hide */ -class UsbHandlerManager { +public class UsbHandlerManager { private static final String LOG_TAG = UsbHandlerManager.class.getSimpleName(); private final Context mContext; diff --git a/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java b/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java index f91666081e82..2ff21ad40558 100644 --- a/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java +++ b/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java @@ -18,6 +18,7 @@ package com.android.server.usb; import static com.android.internal.app.IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE; +import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; @@ -62,6 +63,7 @@ import com.android.internal.util.XmlUtils; import com.android.internal.util.dump.DualDumpOutputStream; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import com.android.server.usb.flags.Flags; import com.android.server.utils.EventLogger; import libcore.io.IoUtils; @@ -80,8 +82,20 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; -class UsbProfileGroupSettingsManager { +public class UsbProfileGroupSettingsManager { + /** + * <application> level property that an app can specify to restrict any overlaying of + * activities when usb device is attached. + * + * + * <p>This should only be set by privileged apps having {@link Manifest.permission#MANAGE_USB} + * permission. + * @hide + */ + public static final String PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES = + "android.app.PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES"; private static final String TAG = UsbProfileGroupSettingsManager.class.getSimpleName(); private static final boolean DEBUG = false; @@ -101,6 +115,8 @@ class UsbProfileGroupSettingsManager { private final PackageManager mPackageManager; + private final ActivityManager mActivityManager; + private final UserManager mUserManager; private final @NonNull UsbSettingsManager mSettingsManager; @@ -224,7 +240,7 @@ class UsbProfileGroupSettingsManager { * @param settingsManager The settings manager of the service * @param usbResolveActivityManager The resovle activity manager of the service */ - UsbProfileGroupSettingsManager(@NonNull Context context, @NonNull UserHandle user, + public UsbProfileGroupSettingsManager(@NonNull Context context, @NonNull UserHandle user, @NonNull UsbSettingsManager settingsManager, @NonNull UsbHandlerManager usbResolveActivityManager) { if (DEBUG) Slog.v(TAG, "Creating settings for " + user); @@ -238,6 +254,7 @@ class UsbProfileGroupSettingsManager { mContext = context; mPackageManager = context.getPackageManager(); + mActivityManager = context.getSystemService(ActivityManager.class); mSettingsManager = settingsManager; mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); @@ -895,7 +912,10 @@ class UsbProfileGroupSettingsManager { // Send broadcast to running activities with registered intent mContext.sendBroadcastAsUser(intent, UserHandle.ALL); - resolveActivity(intent, device, true /* showMtpNotification */); + //resolving activities only if there is no foreground activity restricting it. + if (!shouldRestrictOverlayActivities()) { + resolveActivity(intent, device, true /* showMtpNotification */); + } } private void resolveActivity(Intent intent, UsbDevice device, boolean showMtpNotification) { @@ -918,6 +938,63 @@ class UsbProfileGroupSettingsManager { resolveActivity(intent, matches, defaultActivity, device, null); } + /** + * @return true if any application in foreground have set restrict_usb_overlay_activities as + * true in manifest file. The application needs to have MANAGE_USB permission. + */ + private boolean shouldRestrictOverlayActivities() { + + if (!Flags.allowRestrictionOfOverlayActivities()) return false; + + List<ActivityManager.RunningAppProcessInfo> appProcessInfos = mActivityManager + .getRunningAppProcesses(); + + List<String> filteredAppProcessInfos = new ArrayList<>(); + boolean shouldRestrictOverlayActivities; + + //filtering out applications in foreground. + for (ActivityManager.RunningAppProcessInfo processInfo : appProcessInfos) { + if (processInfo.importance <= ActivityManager + .RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { + filteredAppProcessInfos.addAll(List.of(processInfo.pkgList)); + } + } + + if (DEBUG) Slog.d(TAG, "packages in foreground : " + filteredAppProcessInfos); + + List<String> packagesHoldingManageUsbPermission = + mPackageManager.getPackagesHoldingPermissions( + new String[]{android.Manifest.permission.MANAGE_USB}, + PackageManager.MATCH_SYSTEM_ONLY).stream() + .map(packageInfo -> packageInfo.packageName).collect(Collectors.toList()); + + //retaining only packages that hold the required permission + filteredAppProcessInfos.retainAll(packagesHoldingManageUsbPermission); + + if (DEBUG) { + Slog.d(TAG, "packages in foreground with required permission : " + + filteredAppProcessInfos); + } + + shouldRestrictOverlayActivities = filteredAppProcessInfos.stream().anyMatch(pkg -> { + try { + return mPackageManager.getProperty(PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES, pkg) + .getBoolean(); + } catch (NameNotFoundException e) { + if (DEBUG) { + Slog.d(TAG, "property PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES " + + "not present for " + pkg); + } + return false; + } + }); + + if (shouldRestrictOverlayActivities) { + Slog.d(TAG, "restricting starting of usb overlay activities"); + } + return shouldRestrictOverlayActivities; + } + public void deviceAttachedForFixedHandler(UsbDevice device, ComponentName component) { final Intent intent = createDeviceAttachedIntent(device); diff --git a/services/usb/java/com/android/server/usb/UsbSettingsManager.java b/services/usb/java/com/android/server/usb/UsbSettingsManager.java index 8e53ff412f0a..0b854a8440a5 100644 --- a/services/usb/java/com/android/server/usb/UsbSettingsManager.java +++ b/services/usb/java/com/android/server/usb/UsbSettingsManager.java @@ -33,7 +33,7 @@ import java.util.List; /** * Maintains all {@link UsbUserSettingsManager} for all users. */ -class UsbSettingsManager { +public class UsbSettingsManager { private static final String LOG_TAG = UsbSettingsManager.class.getSimpleName(); private static final boolean DEBUG = false; @@ -70,7 +70,7 @@ class UsbSettingsManager { * * @return The settings for the user */ - @NonNull UsbUserSettingsManager getSettingsForUser(@UserIdInt int userId) { + public @NonNull UsbUserSettingsManager getSettingsForUser(@UserIdInt int userId) { synchronized (mSettingsByUser) { UsbUserSettingsManager settings = mSettingsByUser.get(userId); if (settings == null) { diff --git a/services/usb/java/com/android/server/usb/UsbUserSettingsManager.java b/services/usb/java/com/android/server/usb/UsbUserSettingsManager.java index c2b8d0109e68..be729c562f64 100644 --- a/services/usb/java/com/android/server/usb/UsbUserSettingsManager.java +++ b/services/usb/java/com/android/server/usb/UsbUserSettingsManager.java @@ -49,7 +49,7 @@ import org.xmlpull.v1.XmlPullParser; import java.util.ArrayList; import java.util.List; -class UsbUserSettingsManager { +public class UsbUserSettingsManager { private static final String TAG = UsbUserSettingsManager.class.getSimpleName(); private static final boolean DEBUG = false; @@ -81,7 +81,7 @@ class UsbUserSettingsManager { * * @return The resolve infos of the activities that can handle the intent */ - List<ResolveInfo> queryIntentActivities(@NonNull Intent intent) { + public List<ResolveInfo> queryIntentActivities(@NonNull Intent intent) { return mPackageManager.queryIntentActivitiesAsUser(intent, PackageManager.GET_META_DATA, mUser.getIdentifier()); } diff --git a/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig new file mode 100644 index 000000000000..ea6e50221cd0 --- /dev/null +++ b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig @@ -0,0 +1,8 @@ +package: "com.android.server.usb.flags" + +flag { + name: "allow_restriction_of_overlay_activities" + namespace: "usb" + description: "This flag controls the restriction of usb overlay activities" + bug: "307231174" +} diff --git a/telephony/java/android/telephony/DomainSelectionService.java b/telephony/java/android/telephony/DomainSelectionService.java index 3c11da5f2daa..4ff9712f0907 100644 --- a/telephony/java/android/telephony/DomainSelectionService.java +++ b/telephony/java/android/telephony/DomainSelectionService.java @@ -831,7 +831,7 @@ public abstract class DomainSelectionService extends Service { @NonNull String tag, @NonNull String errorLogName) { try { CompletableFuture.runAsync( - () -> TelephonyUtils.runWithCleanCallingIdentity(r), executor).join(); + () -> TelephonyUtils.runWithCleanCallingIdentity(r), executor); } catch (CancellationException | CompletionException e) { Rlog.w(tag, "Binder - " + errorLogName + " exception: " + e.getMessage()); } diff --git a/tests/UsbManagerTests/Android.bp b/tests/UsbManagerTests/Android.bp index c02d8e96abb0..70c7dad2fd55 100644 --- a/tests/UsbManagerTests/Android.bp +++ b/tests/UsbManagerTests/Android.bp @@ -29,12 +29,17 @@ android_test { static_libs: [ "frameworks-base-testutils", "androidx.test.rules", - "mockito-target-inline-minus-junit4", + "mockito-target-extended-minus-junit4", "platform-test-annotations", "truth", "UsbManagerTestLib", ], - jni_libs: ["libdexmakerjvmtiagent"], + jni_libs: [ + // Required for ExtendedMockito + "libdexmakerjvmtiagent", + "libmultiplejvmtiagentsinterferenceagent", + "libstaticjvmtiagent", + ], certificate: "platform", platform_apis: true, test_suites: ["device-tests"], diff --git a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java new file mode 100644 index 000000000000..4780d8a610e8 --- /dev/null +++ b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.usbtest; + +import static com.android.server.usb.UsbProfileGroupSettingsManager.PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.Property; +import android.content.pm.UserInfo; +import android.content.res.Resources; +import android.hardware.usb.UsbDevice; +import android.os.UserHandle; +import android.os.UserManager; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.server.usb.UsbHandlerManager; +import com.android.server.usb.UsbProfileGroupSettingsManager; +import com.android.server.usb.UsbSettingsManager; +import com.android.server.usb.UsbUserSettingsManager; +import com.android.server.usb.flags.Flags; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +import java.util.ArrayList; +import java.util.List; + +/** + * Unit tests for {@link com.android.server.usb.UsbProfileGroupSettingsManager}. + * Note: MUST claim MANAGE_USB permission in Manifest + */ +@RunWith(AndroidJUnit4.class) +public class UsbProfileGroupSettingsManagerTest { + + private static final String TEST_PACKAGE_NAME = "testPkg"; + @Mock + private Context mContext; + @Mock + private PackageManager mPackageManager; + @Mock + private ActivityManager mActivityManager; + @Mock + private UserHandle mUserHandle; + @Mock + private UsbSettingsManager mUsbSettingsManager; + @Mock + private UsbHandlerManager mUsbHandlerManager; + @Mock + private UserManager mUserManager; + @Mock + private UsbUserSettingsManager mUsbUserSettingsManager; + @Mock private Property mProperty; + private ActivityManager.RunningAppProcessInfo mRunningAppProcessInfo; + private PackageInfo mPackageInfo; + private UsbProfileGroupSettingsManager mUsbProfileGroupSettingsManager; + private MockitoSession mStaticMockSession; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mRunningAppProcessInfo = new ActivityManager.RunningAppProcessInfo(); + mRunningAppProcessInfo.pkgList = new String[]{TEST_PACKAGE_NAME}; + mPackageInfo = new PackageInfo(); + mPackageInfo.packageName = TEST_PACKAGE_NAME; + mPackageInfo.applicationInfo = Mockito.mock(ApplicationInfo.class); + + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mContext.getSystemService(ActivityManager.class)).thenReturn(mActivityManager); + when(mContext.getResources()).thenReturn(Mockito.mock(Resources.class)); + when(mContext.createPackageContextAsUser(anyString(), anyInt(), any(UserHandle.class))) + .thenReturn(mContext); + when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager); + + mUsbProfileGroupSettingsManager = new UsbProfileGroupSettingsManager(mContext, mUserHandle, + mUsbSettingsManager, mUsbHandlerManager); + + mStaticMockSession = ExtendedMockito.mockitoSession() + .mockStatic(Flags.class) + .strictness(Strictness.WARN) + .startMocking(); + + when(mPackageManager.getPackageInfo(TEST_PACKAGE_NAME, 0)).thenReturn(mPackageInfo); + when(mPackageManager.getProperty(eq(PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES), + eq(TEST_PACKAGE_NAME))).thenReturn(mProperty); + when(mUserManager.getEnabledProfiles(anyInt())) + .thenReturn(List.of(Mockito.mock(UserInfo.class))); + when(mUsbSettingsManager.getSettingsForUser(anyInt())).thenReturn(mUsbUserSettingsManager); + } + + @After + public void tearDown() throws Exception { + mStaticMockSession.finishMocking(); + } + + @Test + public void testDeviceAttached_flagTrueWithoutForegroundActivity_resolveActivityCalled() { + when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true); + when(mActivityManager.getRunningAppProcesses()).thenReturn(new ArrayList<>()); + when(mPackageManager.getPackagesHoldingPermissions( + new String[]{android.Manifest.permission.MANAGE_USB}, + PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo)); + UsbDevice device = Mockito.mock(UsbDevice.class); + mUsbProfileGroupSettingsManager.deviceAttached(device); + verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class)); + } + + @Test + public void testDeviceAttached_noForegroundActivityWithUsbPermission_resolveActivityCalled() { + when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true); + when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo)); + when(mPackageManager.getPackagesHoldingPermissions( + new String[]{android.Manifest.permission.MANAGE_USB}, + PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(new ArrayList<>()); + UsbDevice device = Mockito.mock(UsbDevice.class); + mUsbProfileGroupSettingsManager.deviceAttached(device); + verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class)); + } + + @Test + public void testDeviceAttached_foregroundActivityWithManifestField_resolveActivityNotCalled() { + when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true); + when(mProperty.getBoolean()).thenReturn(true); + when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo)); + when(mPackageManager.getPackagesHoldingPermissions( + new String[]{android.Manifest.permission.MANAGE_USB}, + PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo)); + UsbDevice device = Mockito.mock(UsbDevice.class); + mUsbProfileGroupSettingsManager.deviceAttached(device); + verify(mUsbUserSettingsManager, times(0)) + .queryIntentActivities(any(Intent.class)); + } + + @Test + public void testDeviceAttached_foregroundActivityWithoutManifestField_resolveActivityCalled() { + when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true); + when(mProperty.getBoolean()).thenReturn(false); + when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo)); + when(mPackageManager.getPackagesHoldingPermissions( + new String[]{android.Manifest.permission.MANAGE_USB}, + PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo)); + UsbDevice device = Mockito.mock(UsbDevice.class); + mUsbProfileGroupSettingsManager.deviceAttached(device); + verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class)); + } + + @Test + public void testDeviceAttached_flagFalseForegroundActivity_resolveActivityCalled() { + when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(false); + when(mProperty.getBoolean()).thenReturn(true); + when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo)); + when(mPackageManager.getPackagesHoldingPermissions( + new String[]{android.Manifest.permission.MANAGE_USB}, + PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo)); + UsbDevice device = Mockito.mock(UsbDevice.class); + mUsbProfileGroupSettingsManager.deviceAttached(device); + verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class)); + } +} |