diff options
272 files changed, 10283 insertions, 1499 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/Ravenwood.bp b/Ravenwood.bp index d13c4d78190c..93febca49421 100644 --- a/Ravenwood.bp +++ b/Ravenwood.bp @@ -33,6 +33,7 @@ java_genrule { "@$(location ravenwood/ravenwood-standard-options.txt) " + "--debug-log $(location hoststubgen_framework-minus-apex.log) " + + "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " + "--out-impl-jar $(location ravenwood.jar) " + @@ -56,6 +57,7 @@ java_genrule { "hoststubgen_dump.txt", "hoststubgen_framework-minus-apex.log", + "hoststubgen_framework-minus-apex_stats.csv", ], visibility: ["//visibility:private"], } diff --git a/apct-tests/perftests/core/src/android/view/HandwritingInitiatorPerfTest.java b/apct-tests/perftests/core/src/android/view/HandwritingInitiatorPerfTest.java index 123b2eeba5dd..0fd24493f2d5 100644 --- a/apct-tests/perftests/core/src/android/view/HandwritingInitiatorPerfTest.java +++ b/apct-tests/perftests/core/src/android/view/HandwritingInitiatorPerfTest.java @@ -21,8 +21,11 @@ import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_UP; import static android.view.MotionEvent.TOOL_TYPE_FINGER; import static android.view.MotionEvent.TOOL_TYPE_STYLUS; +import static android.view.inputmethod.Flags.initiationWithoutInputConnection; +import static org.junit.Assume.assumeFalse; + import android.app.Instrumentation; import android.content.Context; import android.perftests.utils.BenchmarkState; @@ -186,6 +189,7 @@ public class HandwritingInitiatorPerfTest { @Test public void onInputConnectionCreated() { + assumeFalse(initiationWithoutInputConnection()); final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); final View view = new View(mContext); final EditorInfo editorInfo = new EditorInfo(); @@ -199,6 +203,7 @@ public class HandwritingInitiatorPerfTest { @Test public void onInputConnectionClosed() { + assumeFalse(initiationWithoutInputConnection()); final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); final View view = new View(mContext); while (state.keepRunning()) { diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index 7284f479df35..7de67993bc62 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -2354,9 +2354,9 @@ public class JobInfo implements Parcelable { if (maxExecutionDelayMillis - windowStart < MIN_ALLOWED_TIME_WINDOW_MILLIS) { if (enforceMinimumTimeWindows && Flags.enforceMinimumTimeWindows()) { - throw new IllegalArgumentException("Jobs with a deadline and" - + " functional constraints cannot have a time window less than " - + MIN_ALLOWED_TIME_WINDOW_MILLIS + " ms." + throw new IllegalArgumentException("Time window too short. Constraints" + + " unlikely to be satisfied. Increase deadline to a reasonable" + + " duration." + " Job '" + service.flattenToShortString() + "#" + jobId + "'" + " has delay=" + windowStart + ", deadline=" + maxExecutionDelayMillis); 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..a0b9c5fb7a60 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. @@ -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 3fde9a69c5fb..0c504b643ea2 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -1804,6 +1804,7 @@ package android { field public static final int useEmbeddedDex = 16844190; // 0x101059e field public static final int useIntrinsicSizeAsMinimum = 16843536; // 0x1010310 field public static final int useLevel = 16843167; // 0x101019f + field @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public static final int useLocalePreferredLineHeightForMinimum; field public static final int userVisible = 16843409; // 0x1010291 field public static final int usesCleartextTraffic = 16844012; // 0x10104ec field public static final int usesPermissionFlags = 16844356; // 0x1010644 @@ -3322,11 +3323,13 @@ package android.accessibilityservice { method @FlaggedApi("android.view.accessibility.a11y_overlay_callbacks") public void attachAccessibilityOverlayToWindow(int, @NonNull android.view.SurfaceControl, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer); method public boolean clearCache(); method public boolean clearCachedSubtree(@NonNull android.view.accessibility.AccessibilityNodeInfo); + method @FlaggedApi("android.view.accessibility.braille_display_hid") public void clearTestBrailleDisplayController(); method public final void disableSelf(); method public final boolean dispatchGesture(@NonNull android.accessibilityservice.GestureDescription, @Nullable android.accessibilityservice.AccessibilityService.GestureResultCallback, @Nullable android.os.Handler); method public android.view.accessibility.AccessibilityNodeInfo findFocus(int); method @NonNull public final android.accessibilityservice.AccessibilityButtonController getAccessibilityButtonController(); method @NonNull public final android.accessibilityservice.AccessibilityButtonController getAccessibilityButtonController(int); + method @FlaggedApi("android.view.accessibility.braille_display_hid") @NonNull public android.accessibilityservice.BrailleDisplayController getBrailleDisplayController(); method @NonNull @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public final android.accessibilityservice.FingerprintGestureController getFingerprintGestureController(); method @Nullable public final android.accessibilityservice.InputMethod getInputMethod(); method @NonNull public final android.accessibilityservice.AccessibilityService.MagnificationController getMagnificationController(); @@ -3356,6 +3359,7 @@ package android.accessibilityservice { method public boolean setCacheEnabled(boolean); method public void setGestureDetectionPassthroughRegion(int, @NonNull android.graphics.Region); method public final void setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo); + method @FlaggedApi("android.view.accessibility.braille_display_hid") public void setTestBrailleDisplayController(@NonNull android.accessibilityservice.BrailleDisplayController); method public void setTouchExplorationPassthroughRegion(int, @NonNull android.graphics.Region); method public void takeScreenshot(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback); method public void takeScreenshotOfWindow(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback); @@ -3560,6 +3564,25 @@ package android.accessibilityservice { field public String[] packageNames; } + @FlaggedApi("android.view.accessibility.braille_display_hid") public interface BrailleDisplayController { + method @FlaggedApi("android.view.accessibility.braille_display_hid") @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public void connect(@NonNull android.bluetooth.BluetoothDevice, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback); + method @FlaggedApi("android.view.accessibility.braille_display_hid") @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public void connect(@NonNull android.bluetooth.BluetoothDevice, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback); + method @FlaggedApi("android.view.accessibility.braille_display_hid") public void connect(@NonNull android.hardware.usb.UsbDevice, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback); + method @FlaggedApi("android.view.accessibility.braille_display_hid") public void connect(@NonNull android.hardware.usb.UsbDevice, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback); + method @FlaggedApi("android.view.accessibility.braille_display_hid") public void disconnect(); + method @FlaggedApi("android.view.accessibility.braille_display_hid") public boolean isConnected(); + method @FlaggedApi("android.view.accessibility.braille_display_hid") public void write(@NonNull byte[]) throws java.io.IOException; + } + + @FlaggedApi("android.view.accessibility.braille_display_hid") public static interface BrailleDisplayController.BrailleDisplayCallback { + method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onConnected(@NonNull byte[]); + method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onConnectionFailed(int); + method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onDisconnected(); + method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onInput(@NonNull byte[]); + field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final int FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND = 2; // 0x2 + field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final int FLAG_ERROR_CANNOT_ACCESS = 1; // 0x1 + } + public final class FingerprintGestureController { method public boolean isGestureDetectionAvailable(); method public void registerFingerprintGestureCallback(@NonNull android.accessibilityservice.FingerprintGestureController.FingerprintGestureCallback, @Nullable android.os.Handler); @@ -7817,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 @@ -18823,6 +18847,7 @@ package android.hardware.biometrics { method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable public android.hardware.biometrics.PromptContentView getContentView(); method @Nullable public CharSequence getDescription(); method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public android.graphics.Bitmap getLogoBitmap(); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public String getLogoDescription(); method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @DrawableRes @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public int getLogoRes(); method @Nullable public CharSequence getNegativeButtonText(); method @Nullable public CharSequence getSubtitle(); @@ -18874,6 +18899,7 @@ package android.hardware.biometrics { method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDescription(@NonNull CharSequence); method @Deprecated @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDeviceCredentialAllowed(boolean); method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public android.hardware.biometrics.BiometricPrompt.Builder setLogoBitmap(@NonNull android.graphics.Bitmap); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public android.hardware.biometrics.BiometricPrompt.Builder setLogoDescription(@NonNull String); method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public android.hardware.biometrics.BiometricPrompt.Builder setLogoRes(@DrawableRes int); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setNegativeButton(@NonNull CharSequence, @NonNull java.util.concurrent.Executor, @NonNull android.content.DialogInterface.OnClickListener); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setSubtitle(@NonNull CharSequence); @@ -25846,6 +25872,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 @@ -25870,14 +25899,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); } @@ -25897,6 +25937,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(); @@ -35463,7 +35562,7 @@ package android.provider { field public static final String LONGITUDE = "longitude"; } - @FlaggedApi("android.provider.user_keys") public class ContactKeysManager { + @FlaggedApi("android.provider.user_keys") public final class ContactKeysManager { method @NonNull @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public java.util.List<android.provider.ContactKeysManager.ContactKey> getAllContactKeys(@NonNull String); method @NonNull @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public java.util.List<android.provider.ContactKeysManager.SelfKey> getAllSelfKeys(); method @Nullable @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public android.provider.ContactKeysManager.ContactKey getContactKey(@NonNull String, @NonNull String, @NonNull String); @@ -35478,9 +35577,9 @@ package android.provider { method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public void updateOrInsertContactKey(@NonNull String, @NonNull String, @NonNull String, @NonNull byte[]); method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public boolean updateOrInsertSelfKey(@NonNull String, @NonNull String, @NonNull byte[]); method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public boolean updateSelfKeyRemoteVerificationState(@NonNull String, @NonNull String, int); - field public static final int UNVERIFIED = 0; // 0x0 - field public static final int VERIFICATION_FAILED = 1; // 0x1 - field public static final int VERIFIED = 2; // 0x2 + field public static final int VERIFICATION_STATE_UNVERIFIED = 0; // 0x0 + field public static final int VERIFICATION_STATE_VERIFICATION_FAILED = 1; // 0x1 + field public static final int VERIFICATION_STATE_VERIFIED = 2; // 0x2 } public static final class ContactKeysManager.ContactKey implements android.os.Parcelable { @@ -36977,6 +37076,7 @@ package android.provider { field public static final String ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = "android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"; field public static final String ACTION_REQUEST_MANAGE_MEDIA = "android.settings.REQUEST_MANAGE_MEDIA"; field @FlaggedApi("com.android.media.flags.enable_privileged_routing_for_media_routing_control") public static final String ACTION_REQUEST_MEDIA_ROUTING_CONTROL = "android.settings.REQUEST_MEDIA_ROUTING_CONTROL"; + field @FlaggedApi("android.provider.backup_tasks_settings_screen") public static final String ACTION_REQUEST_RUN_BACKUP_JOBS = "android.settings.REQUEST_RUN_BACKUP_JOBS"; field public static final String ACTION_REQUEST_SCHEDULE_EXACT_ALARM = "android.settings.REQUEST_SCHEDULE_EXACT_ALARM"; field public static final String ACTION_REQUEST_SET_AUTOFILL_SERVICE = "android.settings.REQUEST_SET_AUTOFILL_SERVICE"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String ACTION_SATELLITE_SETTING = "android.settings.SATELLITE_SETTING"; @@ -52219,6 +52319,7 @@ package android.view { method public final boolean getClipToOutline(); method @Nullable public final android.view.contentcapture.ContentCaptureSession getContentCaptureSession(); method public CharSequence getContentDescription(); + method @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public final int getContentSensitivity(); method @UiContext public final android.content.Context getContext(); method protected android.view.ContextMenu.ContextMenuInfo getContextMenuInfo(); method public final boolean getDefaultFocusHighlightEnabled(); @@ -52398,6 +52499,7 @@ package android.view { method public boolean isAttachedToWindow(); method public boolean isAutoHandwritingEnabled(); method public boolean isClickable(); + method @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public final boolean isContentSensitive(); method public boolean isContextClickable(); method public boolean isCredential(); method public boolean isDirty(); @@ -52602,6 +52704,7 @@ package android.view { method public void setClipToOutline(boolean); method public void setContentCaptureSession(@Nullable android.view.contentcapture.ContentCaptureSession); method public void setContentDescription(CharSequence); + method @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public final void setContentSensitivity(int); method public void setContextClickable(boolean); method public void setDefaultFocusHighlightEnabled(boolean); method @Deprecated public void setDrawingCacheBackgroundColor(@ColorInt int); @@ -52786,6 +52889,9 @@ package android.view { field public static final int AUTOFILL_TYPE_NONE = 0; // 0x0 field public static final int AUTOFILL_TYPE_TEXT = 1; // 0x1 field public static final int AUTOFILL_TYPE_TOGGLE = 2; // 0x2 + field @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public static final int CONTENT_SENSITIVITY_AUTO = 0; // 0x0 + field @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public static final int CONTENT_SENSITIVITY_NOT_SENSITIVE = 2; // 0x2 + field @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public static final int CONTENT_SENSITIVITY_SENSITIVE = 1; // 0x1 field public static final int DRAG_FLAG_ACCESSIBILITY_ACTION = 1024; // 0x400 field public static final int DRAG_FLAG_GLOBAL = 256; // 0x100 field public static final int DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION = 64; // 0x40 @@ -60506,6 +60612,7 @@ package android.widget { method public boolean isFallbackLineSpacing(); method public final boolean isHorizontallyScrollable(); method public boolean isInputMethodTarget(); + method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public boolean isLocalePreferredLineHeightForMinimumUsed(); method public boolean isSingleLine(); method public boolean isSuggestionsEnabled(); method public boolean isTextSelectable(); @@ -60588,6 +60695,7 @@ package android.widget { method public final void setLinkTextColor(@ColorInt int); method public final void setLinkTextColor(android.content.res.ColorStateList); method public final void setLinksClickable(boolean); + method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public void setLocalePreferredLineHeightForMinimumUsed(boolean); method public void setMarqueeRepeatLimit(int); method public void setMaxEms(int); method public void setMaxHeight(int); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index c09653ded9f0..18f955f17fb6 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 @@ -4359,10 +4360,12 @@ package android.content.pm.verify.domain { public final class DomainVerificationManager { method @Nullable @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public android.content.pm.verify.domain.DomainVerificationInfo getDomainVerificationInfo(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException; method @NonNull @RequiresPermission(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION) public java.util.SortedSet<android.content.pm.verify.domain.DomainOwner> getOwnersForDomain(@NonNull String); + method @FlaggedApi("android.content.pm.relative_reference_intent_filters") @NonNull public java.util.Map<java.lang.String,java.util.List<android.content.UriRelativeFilterGroup>> getUriRelativeFilterGroups(@NonNull String, @NonNull java.util.List<java.lang.String>); method @NonNull @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public java.util.List<java.lang.String> queryValidVerificationPackageNames(); method @RequiresPermission(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION) public void setDomainVerificationLinkHandlingAllowed(@NonNull String, boolean) throws android.content.pm.PackageManager.NameNotFoundException; method @CheckResult @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public int setDomainVerificationStatus(@NonNull java.util.UUID, @NonNull java.util.Set<java.lang.String>, int) throws android.content.pm.PackageManager.NameNotFoundException; method @CheckResult @RequiresPermission(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION) public int setDomainVerificationUserSelection(@NonNull java.util.UUID, @NonNull java.util.Set<java.lang.String>, boolean) throws android.content.pm.PackageManager.NameNotFoundException; + method @FlaggedApi("android.content.pm.relative_reference_intent_filters") @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public void setUriRelativeFilterGroups(@NonNull String, @NonNull java.util.Map<java.lang.String,java.util.List<android.content.UriRelativeFilterGroup>>); field public static final int ERROR_DOMAIN_SET_ID_INVALID = 1; // 0x1 field public static final int ERROR_UNABLE_TO_APPROVE = 3; // 0x3 field public static final int ERROR_UNKNOWN_DOMAIN = 2; // 0x2 @@ -11062,6 +11065,7 @@ package android.os { field public static final String USER_TYPE_FULL_SYSTEM = "android.os.usertype.full.SYSTEM"; field public static final String USER_TYPE_PROFILE_CLONE = "android.os.usertype.profile.CLONE"; field public static final String USER_TYPE_PROFILE_MANAGED = "android.os.usertype.profile.MANAGED"; + field @FlaggedApi("android.os.allow_private_profile") public static final String USER_TYPE_PROFILE_PRIVATE = "android.os.usertype.profile.PRIVATE"; field public static final String USER_TYPE_SYSTEM_HEADLESS = "android.os.usertype.system.HEADLESS"; } @@ -11465,7 +11469,7 @@ package android.provider { field public static final int ERROR_UNKNOWN = 0; // 0x0 } - @FlaggedApi("android.provider.user_keys") public class ContactKeysManager { + @FlaggedApi("android.provider.user_keys") public final class ContactKeysManager { method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateContactKeyLocalVerificationState(@NonNull String, @NonNull String, @NonNull String, @NonNull String, int); method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateContactKeyRemoteVerificationState(@NonNull String, @NonNull String, @NonNull String, @NonNull String, int); method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateSelfKeyRemoteVerificationState(@NonNull String, @NonNull String, @NonNull String, int); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 288374d4cf49..a7f80ddc1598 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -104,6 +104,14 @@ package android.accessibilityservice { method @FlaggedApi("android.view.accessibility.motion_event_observing") public void setObservedMotionEventSources(int); } + @FlaggedApi("android.view.accessibility.braille_display_hid") public interface BrailleDisplayController { + method @FlaggedApi("android.view.accessibility.braille_display_hid") @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public static void setTestBrailleDisplayData(@NonNull android.accessibilityservice.AccessibilityService, @NonNull java.util.List<android.os.Bundle>); + field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH = "BUS_BLUETOOTH"; + field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_DESCRIPTOR = "DESCRIPTOR"; + field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_HIDRAW_PATH = "HIDRAW_PATH"; + field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_UNIQUE_ID = "UNIQUE_ID"; + } + } package android.animation { @@ -2454,7 +2462,6 @@ package android.os { method public boolean isVisibleBackgroundUsersOnDefaultDisplaySupported(); method public boolean isVisibleBackgroundUsersSupported(); method @Deprecated @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo preCreateUser(@NonNull String) throws android.os.UserManager.UserOperationException; - field @FlaggedApi("android.os.allow_private_profile") public static final String USER_TYPE_PROFILE_PRIVATE = "android.os.usertype.profile.PRIVATE"; } public final class VibrationAttributes implements android.os.Parcelable { diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 2a7dbab2bfd1..f7d75222a6f7 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -80,6 +80,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -364,12 +365,12 @@ public abstract class AccessibilityService extends Service { public static final int GESTURE_SWIPE_UP_AND_RIGHT = 14; /** - * The user has performed an down and left gesture on the touch screen. + * The user has performed a down and left gesture on the touch screen. */ public static final int GESTURE_SWIPE_DOWN_AND_LEFT = 15; /** - * The user has performed an down and right gesture on the touch screen. + * The user has performed a down and right gesture on the touch screen. */ public static final int GESTURE_SWIPE_DOWN_AND_RIGHT = 16; @@ -850,6 +851,8 @@ public abstract class AccessibilityService extends Service { private boolean mInputMethodInitialized = false; private final SparseArray<AccessibilityButtonController> mAccessibilityButtonControllers = new SparseArray<>(0); + private BrailleDisplayController mBrailleDisplayController; + private BrailleDisplayController mTestBrailleDisplayController; private int mGestureStatusCallbackSequence; @@ -3634,4 +3637,56 @@ public abstract class AccessibilityService extends Service { .attachAccessibilityOverlayToWindow( mConnectionId, accessibilityWindowId, sc, executor, callback); } + + /** + * Returns the {@link BrailleDisplayController} which may be used to communicate with + * refreshable Braille displays that provide USB or Bluetooth Braille display HID support. + */ + @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID) + @NonNull + public BrailleDisplayController getBrailleDisplayController() { + BrailleDisplayController.checkApiFlagIsEnabled(); + synchronized (mLock) { + if (mTestBrailleDisplayController != null) { + return mTestBrailleDisplayController; + } + + if (mBrailleDisplayController == null) { + mBrailleDisplayController = new BrailleDisplayControllerImpl(this, mLock); + } + return mBrailleDisplayController; + } + } + + /** + * Set the {@link BrailleDisplayController} implementation that will be returned by + * {@link #getBrailleDisplayController}, to allow this accessibility service to test its + * interaction with BrailleDisplayController without requiring a real Braille display. + * + * <p>For full test fidelity, ensure that this test-only implementation follows the same + * behavior specified in the documentation for {@link BrailleDisplayController}, including + * thrown exceptions. + * + * @param controller A test-only implementation of {@link BrailleDisplayController}. + */ + @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID) + public void setTestBrailleDisplayController(@NonNull BrailleDisplayController controller) { + BrailleDisplayController.checkApiFlagIsEnabled(); + Objects.requireNonNull(controller); + synchronized (mLock) { + mTestBrailleDisplayController = controller; + } + } + + /** + * Clears the {@link BrailleDisplayController} previously set by + * {@link #setTestBrailleDisplayController}. + */ + @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID) + public void clearTestBrailleDisplayController() { + BrailleDisplayController.checkApiFlagIsEnabled(); + synchronized (mLock) { + mTestBrailleDisplayController = null; + } + } } diff --git a/core/java/android/accessibilityservice/BrailleDisplayController.java b/core/java/android/accessibilityservice/BrailleDisplayController.java new file mode 100644 index 000000000000..5282aa3fccf7 --- /dev/null +++ b/core/java/android/accessibilityservice/BrailleDisplayController.java @@ -0,0 +1,308 @@ +/* + * 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.accessibilityservice; + +import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SuppressLint; +import android.annotation.TestApi; +import android.bluetooth.BluetoothDevice; +import android.hardware.usb.UsbDevice; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.view.accessibility.AccessibilityInteractionClient; +import android.view.accessibility.Flags; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Used to communicate with a Braille display that supports the Braille display HID standard + * (usage page 0x41). + * + * <p>Only one Braille display may be connected at a time. + */ +// This interface doesn't actually own resources. Its I/O connections are owned, monitored, +// and automatically closed by the system after the accessibility service is disconnected. +@SuppressLint("NotCloseable") +@FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) +public interface BrailleDisplayController { + + /** + * Throw {@link IllegalStateException} if this feature's aconfig flag is disabled. + * + * @hide + */ + static void checkApiFlagIsEnabled() { + if (!Flags.brailleDisplayHid()) { + throw new IllegalStateException("Flag BRAILLE_DISPLAY_HID not enabled"); + } + } + + /** + * Interface provided to {@link BrailleDisplayController} connection methods to + * receive callbacks from the system. + */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + interface BrailleDisplayCallback { + /** + * The system cannot access connected HID devices. + */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + int FLAG_ERROR_CANNOT_ACCESS = 1 << 0; + /** + * A unique Braille display matching the requested properties could not be identified. + */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + int FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND = 1 << 1; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, prefix = "FLAG_ERROR_", value = { + FLAG_ERROR_CANNOT_ACCESS, + FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND, + }) + @interface ErrorCode { + } + + /** + * Callback to observe a successful Braille display connection. + * + * <p>The provided HID report descriptor should be used to understand the input bytes + * received from the Braille display via {@link #onInput} and to prepare + * the output sent to the Braille display via {@link #write}. + * + * @param hidDescriptor The HID report descriptor for this Braille display. + * @see #connect(BluetoothDevice, BrailleDisplayCallback) + * @see #connect(UsbDevice, BrailleDisplayCallback) + */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + void onConnected(@NonNull byte[] hidDescriptor); + + /** + * Callback to observe a failed Braille display connection. + * + * @param errorFlags A bitmask of error codes for the connection failure. + * @see #connect(BluetoothDevice, BrailleDisplayCallback) + * @see #connect(UsbDevice, BrailleDisplayCallback) + */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + void onConnectionFailed(@ErrorCode int errorFlags); + + /** + * Callback to observe input bytes from the currently connected Braille display. + * + * @param input The input bytes from the Braille display, formatted according to the HID + * report descriptor and the HIDRAW kernel driver. + */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + void onInput(@NonNull byte[] input); + + /** + * Callback to observe when the currently connected Braille display is disconnected by the + * system. + */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + void onDisconnected(); + } + + /** + * Connects to the requested bluetooth Braille display using the Braille + * display HID standard (usage page 0x41). + * + * <p>If successful then the HID report descriptor will be provided to + * {@link BrailleDisplayCallback#onConnected} + * and the Braille display will start sending incoming input bytes to + * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input + * then the system will disconnect the Braille display. + * + * <p>Note that the callbacks will be executed on the main thread using + * {@link AccessibilityService#getMainExecutor()}. To specify the execution thread, use + * {@link #connect(BluetoothDevice, Executor, BrailleDisplayCallback)}. + * + * @param bluetoothDevice The Braille display device. + * @param callback Callbacks used to provide connection results. + * @see BrailleDisplayCallback#onConnected + * @see BrailleDisplayCallback#onConnectionFailed + * @throws IllegalStateException if a Braille display is already connected to this controller. + */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + void connect(@NonNull BluetoothDevice bluetoothDevice, + @NonNull BrailleDisplayCallback callback); + + /** + * Connects to the requested bluetooth Braille display using the Braille + * display HID standard (usage page 0x41). + * + * <p>If successful then the HID report descriptor will be provided to + * {@link BrailleDisplayCallback#onConnected} + * and the Braille display will start sending incoming input bytes to + * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input + * then the system will disconnect the Braille display. + * + * @param bluetoothDevice The Braille display device. + * @param callbackExecutor Executor for executing the provided callbacks. + * @param callback Callbacks used to provide connection results. + * @see BrailleDisplayCallback#onConnected + * @see BrailleDisplayCallback#onConnectionFailed + * @throws IllegalStateException if a Braille display is already connected to this controller. + */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + void connect(@NonNull BluetoothDevice bluetoothDevice, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull BrailleDisplayCallback callback); + + /** + * Connects to the requested USB Braille display using the Braille + * display HID standard (usage page 0x41). + * + * <p>If successful then the HID report descriptor will be provided to + * {@link BrailleDisplayCallback#onConnected} + * and the Braille display will start sending incoming input bytes to + * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input + * then the system will disconnect the Braille display. + * + * <p>The accessibility service app must already have approval to access the USB device + * from the standard {@link android.hardware.usb.UsbManager} access approval process. + * + * <p>Note that the callbacks will be executed on the main thread using + * {@link AccessibilityService#getMainExecutor()}. To specify the execution thread, use + * {@link #connect(UsbDevice, Executor, BrailleDisplayCallback)}. + * + * @param usbDevice The Braille display device. + * @param callback Callbacks used to provide connection results. + * @see BrailleDisplayCallback#onConnected + * @see BrailleDisplayCallback#onConnectionFailed + * @throws SecurityException if the caller does not have USB device approval. + * @throws IllegalStateException if a Braille display is already connected to this controller. + */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + void connect(@NonNull UsbDevice usbDevice, + @NonNull BrailleDisplayCallback callback); + + /** + * Connects to the requested USB Braille display using the Braille + * display HID standard (usage page 0x41). + * + * <p>If successful then the HID report descriptor will be provided to + * {@link BrailleDisplayCallback#onConnected} + * and the Braille display will start sending incoming input bytes to + * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input + * then the system will disconnect the Braille display. + * + * <p>The accessibility service app must already have approval to access the USB device + * from the standard {@link android.hardware.usb.UsbManager} access approval process. + * + * @param usbDevice The Braille display device. + * @param callbackExecutor Executor for executing the provided callbacks. + * @param callback Callbacks used to provide connection results. + * @see BrailleDisplayCallback#onConnected + * @see BrailleDisplayCallback#onConnectionFailed + * @throws SecurityException if the caller does not have USB device approval. + * @throws IllegalStateException if a Braille display is already connected to this controller. + */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + void connect(@NonNull UsbDevice usbDevice, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull BrailleDisplayCallback callback); + + /** + * Returns true if a Braille display is currently connected, otherwise false. + * + * @see #connect + */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + boolean isConnected(); + + /** + * Writes a HID report to the currently connected Braille display. + * + * <p>This method returns immediately after dispatching the write request to the system. + * If the system experiences an error in writing output (e.g. the Braille display is unplugged + * after the system receives the write request but before writing the bytes to the Braille + * display) then the system will disconnect the Braille display, which calls + * {@link BrailleDisplayCallback#onDisconnected()}. + * + * @param buffer The bytes to write to the Braille display. These bytes should be formatted + * according to the HID report descriptor and the HIDRAW kernel driver. + * @throws IOException if there is no currently connected Braille display. + * @throws IllegalArgumentException if the buffer exceeds the maximum safe payload size for + * binder transactions of + * {@link IBinder#getSuggestedMaxIpcSizeBytes()} + */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + void write(@NonNull byte[] buffer) throws IOException; + + /** + * Disconnects from the currently connected Braille display. + * + * @see #isConnected() + */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + void disconnect(); + + /** + * Provides test Braille display data to be used for automated CTS tests. + * + * <p>See {@code TEST_BRAILLE_DISPLAY_*} bundle keys. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) + @TestApi + static void setTestBrailleDisplayData( + @NonNull AccessibilityService service, + @NonNull List<Bundle> brailleDisplays) { + checkApiFlagIsEnabled(); + final IAccessibilityServiceConnection serviceConnection = + AccessibilityInteractionClient.getConnection(service.getConnectionId()); + if (serviceConnection != null) { + try { + serviceConnection.setTestBrailleDisplayData(brailleDisplays); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** @hide */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + @TestApi + String TEST_BRAILLE_DISPLAY_HIDRAW_PATH = "HIDRAW_PATH"; + /** @hide */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + @TestApi + String TEST_BRAILLE_DISPLAY_DESCRIPTOR = "DESCRIPTOR"; + /** @hide */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + @TestApi + String TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH = "BUS_BLUETOOTH"; + /** @hide */ + @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) + @TestApi + String TEST_BRAILLE_DISPLAY_UNIQUE_ID = "UNIQUE_ID"; +} diff --git a/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java b/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java new file mode 100644 index 000000000000..cac1dc4e04a9 --- /dev/null +++ b/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java @@ -0,0 +1,267 @@ +/* + * 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.accessibilityservice; + +import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.bluetooth.BluetoothDevice; +import android.hardware.usb.UsbDevice; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteException; +import android.view.accessibility.AccessibilityInteractionClient; +import android.view.accessibility.Flags; + +import com.android.internal.util.FunctionalUtils; + +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * Default implementation of {@link BrailleDisplayController}. + */ +// BrailleDisplayControllerImpl is not an API, but it implements BrailleDisplayController APIs. +// This @FlaggedApi annotation tells the linter that this method delegates API checks to its +// callers. +@FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID) +final class BrailleDisplayControllerImpl implements BrailleDisplayController { + + private final AccessibilityService mAccessibilityService; + private final Object mLock; + + private IBrailleDisplayConnection mBrailleDisplayConnection; + private Executor mCallbackExecutor; + private BrailleDisplayCallback mCallback; + + BrailleDisplayControllerImpl(AccessibilityService accessibilityService, + Object lock) { + mAccessibilityService = accessibilityService; + mLock = lock; + } + + @Override + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public void connect(@NonNull BluetoothDevice bluetoothDevice, + @NonNull BrailleDisplayCallback callback) { + connect(bluetoothDevice, mAccessibilityService.getMainExecutor(), callback); + } + + @Override + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public void connect(@NonNull BluetoothDevice bluetoothDevice, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull BrailleDisplayCallback callback) { + Objects.requireNonNull(bluetoothDevice); + Objects.requireNonNull(callbackExecutor); + Objects.requireNonNull(callback); + connect(serviceConnection -> serviceConnection.connectBluetoothBrailleDisplay( + bluetoothDevice.getAddress(), new IBrailleDisplayControllerWrapper()), + callbackExecutor, callback); + } + + @Override + public void connect(@NonNull UsbDevice usbDevice, + @NonNull BrailleDisplayCallback callback) { + connect(usbDevice, mAccessibilityService.getMainExecutor(), callback); + } + + @Override + public void connect(@NonNull UsbDevice usbDevice, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull BrailleDisplayCallback callback) { + Objects.requireNonNull(usbDevice); + Objects.requireNonNull(callbackExecutor); + Objects.requireNonNull(callback); + connect(serviceConnection -> serviceConnection.connectUsbBrailleDisplay( + usbDevice, new IBrailleDisplayControllerWrapper()), + callbackExecutor, callback); + } + + /** + * Shared implementation for the {@code connect()} API methods. + * + * <p>Performs a blocking call to system_server to create the connection. Success is + * returned through {@link BrailleDisplayCallback#onConnected} while normal connection + * errors are returned through {@link BrailleDisplayCallback#onConnectionFailed}. This + * connection is implemented using cached data from the HIDRAW driver so it returns + * quickly without needing to perform any I/O with the Braille display. + * + * <p>The AIDL call to system_server is blocking (not posted to a handler thread) so + * that runtime exceptions signaling abnormal connection errors from API misuse + * (e.g. lacking permissions, providing an invalid BluetoothDevice, calling connect + * while already connected) are propagated to the API caller. + */ + private void connect( + FunctionalUtils.RemoteExceptionIgnoringConsumer<IAccessibilityServiceConnection> + createConnection, + @NonNull Executor callbackExecutor, @NonNull BrailleDisplayCallback callback) { + BrailleDisplayController.checkApiFlagIsEnabled(); + if (isConnected()) { + throw new IllegalStateException( + "This service already has a connected Braille display"); + } + final IAccessibilityServiceConnection serviceConnection = + AccessibilityInteractionClient.getConnection( + mAccessibilityService.getConnectionId()); + if (serviceConnection == null) { + throw new IllegalStateException("Accessibility service is not connected"); + } + synchronized (mLock) { + mCallbackExecutor = callbackExecutor; + mCallback = callback; + } + try { + createConnection.acceptOrThrow(serviceConnection); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @Override + public boolean isConnected() { + BrailleDisplayController.checkApiFlagIsEnabled(); + return mBrailleDisplayConnection != null; + } + + @Override + public void write(@NonNull byte[] buffer) throws IOException { + BrailleDisplayController.checkApiFlagIsEnabled(); + Objects.requireNonNull(buffer); + if (buffer.length > IBinder.getSuggestedMaxIpcSizeBytes()) { + // This same check must be performed in the system to prevent reflection misuse, + // but perform it here too to prevent unnecessary IPCs from non-reflection callers. + throw new IllegalArgumentException("Invalid write buffer size " + buffer.length); + } + synchronized (mLock) { + if (mBrailleDisplayConnection == null) { + throw new IOException("Braille display is not connected"); + } + try { + mBrailleDisplayConnection.write(buffer); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + @Override + public void disconnect() { + BrailleDisplayController.checkApiFlagIsEnabled(); + synchronized (mLock) { + try { + if (mBrailleDisplayConnection != null) { + mBrailleDisplayConnection.disconnect(); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } finally { + clearConnectionLocked(); + } + } + } + + /** + * Implementation of the {@code IBrailleDisplayController} AIDL interface provided to + * system_server, which system_server uses to pass messages back to this + * {@code BrailleDisplayController}. + * + * <p>Messages from system_server are routed to the {@link BrailleDisplayCallback} callbacks + * implemented by the accessibility service. + * + * <p>Note: Per API Guidelines 7.5 the Binder identity must be cleared before invoking the + * callback executor so that Binder identity checks in the callbacks are performed using the + * app's identity. + */ + private final class IBrailleDisplayControllerWrapper extends IBrailleDisplayController.Stub { + /** + * Called when the system successfully connects to a Braille display. + */ + @Override + public void onConnected(IBrailleDisplayConnection connection, byte[] hidDescriptor) { + BrailleDisplayController.checkApiFlagIsEnabled(); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + mBrailleDisplayConnection = connection; + mCallbackExecutor.execute(() -> mCallback.onConnected(hidDescriptor)); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + /** + * Called when the system is unable to connect to a Braille display. + */ + @Override + public void onConnectionFailed(@BrailleDisplayCallback.ErrorCode int errorCode) { + BrailleDisplayController.checkApiFlagIsEnabled(); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + mCallbackExecutor.execute(() -> mCallback.onConnectionFailed(errorCode)); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + /** + * Called when input is received from the currently connected Braille display. + */ + @Override + public void onInput(byte[] input) { + BrailleDisplayController.checkApiFlagIsEnabled(); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + // Ignore input that arrives after disconnection. + if (mBrailleDisplayConnection != null) { + mCallbackExecutor.execute(() -> mCallback.onInput(input)); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + /** + * Called when the currently connected Braille display is disconnected. + */ + @Override + public void onDisconnected() { + BrailleDisplayController.checkApiFlagIsEnabled(); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + mCallbackExecutor.execute(mCallback::onDisconnected); + clearConnectionLocked(); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + + private void clearConnectionLocked() { + mBrailleDisplayConnection = null; + } + +} diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index 96716dbbaca1..dc5c7f694c58 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -17,10 +17,12 @@ package android.accessibilityservice; import android.accessibilityservice.AccessibilityServiceInfo; +import android.accessibilityservice.IBrailleDisplayController; import android.accessibilityservice.MagnificationConfig; import android.content.pm.ParceledListSlice; import android.graphics.Bitmap; import android.graphics.Region; +import android.hardware.usb.UsbDevice; import android.os.Bundle; import android.os.RemoteCallback; import android.view.MagnificationSpec; @@ -160,4 +162,12 @@ interface IAccessibilityServiceConnection { void attachAccessibilityOverlayToDisplay(int interactionId, int displayId, in SurfaceControl sc, IAccessibilityInteractionConnectionCallback callback); void attachAccessibilityOverlayToWindow(int interactionId, int accessibilityWindowId, in SurfaceControl sc, IAccessibilityInteractionConnectionCallback callback); + + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)") + void connectBluetoothBrailleDisplay(in String bluetoothAddress, in IBrailleDisplayController controller); + + void connectUsbBrailleDisplay(in UsbDevice usbDevice, in IBrailleDisplayController controller); + + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)") + void setTestBrailleDisplayData(in List<Bundle> brailleDisplays); }
\ No newline at end of file diff --git a/core/java/android/accessibilityservice/IBrailleDisplayConnection.aidl b/core/java/android/accessibilityservice/IBrailleDisplayConnection.aidl new file mode 100644 index 000000000000..ec4d7b17ed01 --- /dev/null +++ b/core/java/android/accessibilityservice/IBrailleDisplayConnection.aidl @@ -0,0 +1,12 @@ +package android.accessibilityservice; + +/** + * Interface given to a BrailleDisplayController to talk to a BrailleDisplayConnection + * in system_server. + * + * @hide + */ +interface IBrailleDisplayConnection { + oneway void disconnect(); + oneway void write(in byte[] output); +}
\ No newline at end of file diff --git a/core/java/android/accessibilityservice/IBrailleDisplayController.aidl b/core/java/android/accessibilityservice/IBrailleDisplayController.aidl new file mode 100644 index 000000000000..7a5d83eeccb5 --- /dev/null +++ b/core/java/android/accessibilityservice/IBrailleDisplayController.aidl @@ -0,0 +1,17 @@ +package android.accessibilityservice; + +import android.accessibilityservice.IBrailleDisplayConnection; + +/** + * Interface given to a BrailleDisplayConnection to talk to a BrailleDisplayController + * in an accessibility service. + * + * IPCs from system_server to apps must be oneway, so designate this entire interface as oneway. + * @hide + */ +oneway interface IBrailleDisplayController { + void onConnected(in IBrailleDisplayConnection connection, in byte[] hidDescriptor); + void onConnectionFailed(int error); + void onInput(in byte[] input); + void onDisconnected(); +}
\ No newline at end of file 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/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java index 79af65a3a3e5..d91358105b0b 100644 --- a/core/java/android/content/IntentFilter.java +++ b/core/java/android/content/IntentFilter.java @@ -559,6 +559,10 @@ public class IntentFilter implements Parcelable { sb.append(" sch="); sb.append(mDataSchemes.toString()); } + if (Flags.relativeReferenceIntentFilters() && countUriRelativeFilterGroups() > 0) { + sb.append(" grp="); + sb.append(mUriRelativeFilterGroups.toString()); + } sb.append(" }"); return sb.toString(); } @@ -1807,13 +1811,7 @@ public class IntentFilter implements Parcelable { if (mUriRelativeFilterGroups == null) { return false; } - for (int i = 0; i < mUriRelativeFilterGroups.size(); i++) { - UriRelativeFilterGroup group = mUriRelativeFilterGroups.get(i); - if (group.matchData(data)) { - return group.getAction() == UriRelativeFilterGroup.ACTION_ALLOW; - } - } - return false; + return UriRelativeFilterGroup.matchGroupsToUri(mUriRelativeFilterGroups, data); } /** diff --git a/core/java/android/content/UriRelativeFilter.java b/core/java/android/content/UriRelativeFilter.java index 9866cd0e992a..6d33246ae60d 100644 --- a/core/java/android/content/UriRelativeFilter.java +++ b/core/java/android/content/UriRelativeFilter.java @@ -217,6 +217,15 @@ public final class UriRelativeFilter { + " }"; } + /** @hide */ + public UriRelativeFilterParcel toParcel() { + UriRelativeFilterParcel parcel = new UriRelativeFilterParcel(); + parcel.uriPart = mUriPart; + parcel.patternType = mPatternType; + parcel.filter = mFilter; + return parcel; + } + @Override public boolean equals(@Nullable Object o) { if (this == o) return true; @@ -257,4 +266,11 @@ public final class UriRelativeFilter { mPatternType = Integer.parseInt(parser.getAttributeValue(null, PATTERN_STR)); mFilter = parser.getAttributeValue(null, FILTER_STR); } + + /** @hide */ + public UriRelativeFilter(UriRelativeFilterParcel parcel) { + mUriPart = parcel.uriPart; + mPatternType = parcel.patternType; + mFilter = parcel.filter; + } } diff --git a/core/java/android/content/UriRelativeFilterGroup.aidl b/core/java/android/content/UriRelativeFilterGroup.aidl new file mode 100644 index 000000000000..b251054c967f --- /dev/null +++ b/core/java/android/content/UriRelativeFilterGroup.aidl @@ -0,0 +1,19 @@ +/** + * 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.content; + +parcelable UriRelativeFilterGroup; diff --git a/core/java/android/content/UriRelativeFilterGroup.java b/core/java/android/content/UriRelativeFilterGroup.java index 72c396a73ec8..0e49b4fe427a 100644 --- a/core/java/android/content/UriRelativeFilterGroup.java +++ b/core/java/android/content/UriRelativeFilterGroup.java @@ -19,6 +19,7 @@ package android.content; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.pm.Flags; import android.net.Uri; import android.os.Parcel; @@ -36,9 +37,11 @@ import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; +import java.util.List; import java.util.Objects; /** @@ -83,6 +86,40 @@ public final class UriRelativeFilterGroup { private final @Action int mAction; private final ArraySet<UriRelativeFilter> mUriRelativeFilters = new ArraySet<>(); + /** @hide */ + public static boolean matchGroupsToUri(List<UriRelativeFilterGroup> groups, Uri uri) { + for (int i = 0; i < groups.size(); i++) { + if (groups.get(i).matchData(uri)) { + return groups.get(i).getAction() == UriRelativeFilterGroup.ACTION_ALLOW; + } + } + return false; + } + + /** @hide */ + public static List<UriRelativeFilterGroup> parcelsToGroups( + @Nullable List<UriRelativeFilterGroupParcel> parcels) { + List<UriRelativeFilterGroup> groups = new ArrayList<>(); + if (parcels != null) { + for (int i = 0; i < parcels.size(); i++) { + groups.add(new UriRelativeFilterGroup(parcels.get(i))); + } + } + return groups; + } + + /** @hide */ + public static List<UriRelativeFilterGroupParcel> groupsToParcels( + @Nullable List<UriRelativeFilterGroup> groups) { + List<UriRelativeFilterGroupParcel> parcels = new ArrayList<>(); + if (groups != null) { + for (int i = 0; i < groups.size(); i++) { + parcels.add(groups.get(i).toParcel()); + } + } + return parcels; + } + /** * New UriRelativeFilterGroup that matches a Intent data. * @@ -205,6 +242,35 @@ public final class UriRelativeFilterGroup { } } + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + UriRelativeFilterGroup that = (UriRelativeFilterGroup) o; + if (mAction != that.mAction) return false; + return mUriRelativeFilters.equals(that.mUriRelativeFilters); + } + + @Override + public int hashCode() { + int _hash = 0; + _hash = 31 * _hash + mAction; + _hash = 31 * _hash + java.util.Objects.hashCode(mUriRelativeFilters); + return _hash; + } + + /** @hide */ + public UriRelativeFilterGroupParcel toParcel() { + UriRelativeFilterGroupParcel parcel = new UriRelativeFilterGroupParcel(); + parcel.action = mAction; + parcel.filters = new ArrayList<>(); + for (UriRelativeFilter filter : mUriRelativeFilters) { + parcel.filters.add(filter.toParcel()); + } + return parcel; + } + /** @hide */ UriRelativeFilterGroup(@NonNull Parcel src) { mAction = src.readInt(); @@ -213,4 +279,12 @@ public final class UriRelativeFilterGroup { mUriRelativeFilters.add(new UriRelativeFilter(src)); } } + + /** @hide */ + public UriRelativeFilterGroup(UriRelativeFilterGroupParcel parcel) { + mAction = parcel.action; + for (int i = 0; i < parcel.filters.size(); i++) { + mUriRelativeFilters.add(new UriRelativeFilter(parcel.filters.get(i))); + } + } } diff --git a/core/java/android/content/UriRelativeFilterGroupParcel.aidl b/core/java/android/content/UriRelativeFilterGroupParcel.aidl new file mode 100644 index 000000000000..3679e7f13aa3 --- /dev/null +++ b/core/java/android/content/UriRelativeFilterGroupParcel.aidl @@ -0,0 +1,28 @@ +/** + * 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.content; + +import android.content.UriRelativeFilterParcel; + +/** + * Class for holding UriRelativeFilterGroup data. + * @hide + */ +parcelable UriRelativeFilterGroupParcel { + int action; + List<UriRelativeFilterParcel> filters; +} diff --git a/core/java/android/content/UriRelativeFilterParcel.aidl b/core/java/android/content/UriRelativeFilterParcel.aidl new file mode 100644 index 000000000000..4fb196d38714 --- /dev/null +++ b/core/java/android/content/UriRelativeFilterParcel.aidl @@ -0,0 +1,27 @@ +/* + * 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.content; + +/** + * Class for holding UriRelativeFilter data. + * @hide + */ +parcelable UriRelativeFilterParcel { + int uriPart; + int patternType; + String filter; +} diff --git a/core/java/android/content/pm/verify/domain/DomainVerificationManager.java b/core/java/android/content/pm/verify/domain/DomainVerificationManager.java index 77bd14756637..4dcc51729a61 100644 --- a/core/java/android/content/pm/verify/domain/DomainVerificationManager.java +++ b/core/java/android/content/pm/verify/domain/DomainVerificationManager.java @@ -17,6 +17,7 @@ package android.content.pm.verify.domain; import android.annotation.CheckResult; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -25,15 +26,21 @@ import android.annotation.SystemApi; import android.annotation.SystemService; import android.content.Context; import android.content.Intent; +import android.content.UriRelativeFilterGroup; +import android.content.UriRelativeFilterGroupParcel; import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; import android.os.RemoteException; import android.os.ServiceSpecificException; import android.os.UserHandle; +import android.util.ArrayMap; import com.android.internal.util.CollectionUtils; +import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.SortedSet; @@ -156,6 +163,74 @@ public final class DomainVerificationManager { } /** + * Update the URI relative filter groups for a package. All previously existing groups + * will be cleared before the new groups will be applied. + * + * @param packageName The name of the package. + * @param domainToGroupsMap A map of domains to a list of {@link UriRelativeFilterGroup}s that + * should apply to them. Groups for each domain will replace any groups + * provided for that domain in a prior call to this method. Groups will + * be evaluated in the order they are provided. + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) + @FlaggedApi(android.content.pm.Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS) + public void setUriRelativeFilterGroups(@NonNull String packageName, + @NonNull Map<String, List<UriRelativeFilterGroup>> domainToGroupsMap) { + Objects.requireNonNull(packageName); + Objects.requireNonNull(domainToGroupsMap); + Bundle bundle = new Bundle(); + for (String domain : domainToGroupsMap.keySet()) { + List<UriRelativeFilterGroup> groups = domainToGroupsMap.get(domain); + bundle.putParcelableList(domain, UriRelativeFilterGroup.groupsToParcels(groups)); + } + try { + mDomainVerificationManager.setUriRelativeFilterGroups(packageName, bundle); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Retrieves a map of a package's verified domains to a list of {@link UriRelativeFilterGroup}s + * that applies to them. + * + * @param packageName The name of the package. + * @param domains List of domains for which to retrieve group matches. + * @return A map of domains to the lists of {@link UriRelativeFilterGroup}s that apply to them. + * @hide + */ + @NonNull + @SystemApi + @FlaggedApi(android.content.pm.Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS) + public Map<String, List<UriRelativeFilterGroup>> getUriRelativeFilterGroups( + @NonNull String packageName, + @NonNull List<String> domains) { + Objects.requireNonNull(packageName); + Objects.requireNonNull(domains); + if (domains.isEmpty()) { + return Collections.emptyMap(); + } + try { + Bundle bundle = mDomainVerificationManager.getUriRelativeFilterGroups(packageName, + domains); + ArrayMap<String, List<UriRelativeFilterGroup>> map = new ArrayMap<>(); + if (!bundle.isEmpty()) { + for (String domain : bundle.keySet()) { + List<UriRelativeFilterGroupParcel> parcels = + bundle.getParcelableArrayList(domain, + UriRelativeFilterGroupParcel.class); + map.put(domain, UriRelativeFilterGroup.parcelsToGroups(parcels)); + } + } + return map; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Used to iterate all {@link DomainVerificationInfo} values to do cleanup or retries. This is * usually a heavy workload and should be done infrequently. * diff --git a/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl b/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl index 53205f3ea470..f5af82d36d71 100644 --- a/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl +++ b/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl @@ -20,6 +20,8 @@ import android.content.pm.verify.domain.DomainOwner; import android.content.pm.verify.domain.DomainSet; import android.content.pm.verify.domain.DomainVerificationInfo; import android.content.pm.verify.domain.DomainVerificationUserState; +import android.content.UriRelativeFilterGroup; +import android.os.Bundle; import java.util.List; /** @@ -46,4 +48,8 @@ interface IDomainVerificationManager { int setDomainVerificationUserSelection(String domainSetId, in DomainSet domains, boolean enabled, int userId); + + void setUriRelativeFilterGroups(String packageName, in Bundle domainToGroupsBundle); + + Bundle getUriRelativeFilterGroups(String packageName, in List<String> domains); } diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index bdaf9d789960..d4c58b239c84 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -200,6 +200,25 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan return this; } + /** + * Optional: Sets logo description text that will be shown on the prompt. + * + * <p> Note that using this method is not recommended in most scenarios because the calling + * application's name will be used by default. Setting the logo description is intended for + * large bundled applications that perform a wide range of functions and need to show + * distinct description for each function. + * + * @param logoDescription The logo description text that will be shown on the prompt. + * @return This builder. + */ + @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) + @RequiresPermission(SET_BIOMETRIC_DIALOG_LOGO) + @NonNull + public BiometricPrompt.Builder setLogoDescription(@NonNull String logoDescription) { + mPromptInfo.setLogoDescription(logoDescription); + return this; + } + /** * Required: Sets the title that will be shown on the prompt. @@ -743,7 +762,20 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan return mPromptInfo.getLogoBitmap(); } - + /** + * Gets the logo description for the prompt, as set by + * {@link Builder#setDescription(CharSequence)}. + * Currently for system applications use only. + * + * @return The logo description of the prompt, or null if the prompt has no logo description + * set. + */ + @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) + @RequiresPermission(SET_BIOMETRIC_DIALOG_LOGO) + @Nullable + public String getLogoDescription() { + return mPromptInfo.getLogoDescription(); + } /** * Gets the title for the prompt, as set by {@link Builder#setTitle(CharSequence)}. diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java index 0f9cadc52608..2236660ee388 100644 --- a/core/java/android/hardware/biometrics/PromptInfo.java +++ b/core/java/android/hardware/biometrics/PromptInfo.java @@ -34,6 +34,7 @@ public class PromptInfo implements Parcelable { @DrawableRes private int mLogoRes = -1; @Nullable private Bitmap mLogoBitmap; + @Nullable private String mLogoDescription; @NonNull private CharSequence mTitle; private boolean mUseDefaultTitle; @Nullable private CharSequence mSubtitle; @@ -62,6 +63,7 @@ public class PromptInfo implements Parcelable { PromptInfo(Parcel in) { mLogoRes = in.readInt(); mLogoBitmap = in.readTypedObject(Bitmap.CREATOR); + mLogoDescription = in.readString(); mTitle = in.readCharSequence(); mUseDefaultTitle = in.readBoolean(); mSubtitle = in.readCharSequence(); @@ -106,6 +108,7 @@ public class PromptInfo implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mLogoRes); dest.writeTypedObject(mLogoBitmap, 0); + dest.writeString(mLogoDescription); dest.writeCharSequence(mTitle); dest.writeBoolean(mUseDefaultTitle); dest.writeCharSequence(mSubtitle); @@ -173,6 +176,8 @@ public class PromptInfo implements Parcelable { return true; } else if (mLogoBitmap != null) { return true; + } else if (mLogoDescription != null) { + return true; } return false; } @@ -189,6 +194,10 @@ public class PromptInfo implements Parcelable { checkOnlyOneLogoSet(); } + public void setLogoDescription(@NonNull String logoDescription) { + mLogoDescription = logoDescription; + } + public void setTitle(CharSequence title) { mTitle = title; } @@ -282,6 +291,10 @@ public class PromptInfo implements Parcelable { return mLogoBitmap; } + public String getLogoDescription() { + return mLogoDescription; + } + public CharSequence getTitle() { return mTitle; } diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java index 7abe821b7013..3b10e0dd516a 100644 --- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java @@ -19,7 +19,9 @@ import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; @@ -40,6 +42,7 @@ import android.os.ConditionVariable; import android.os.IBinder; import android.os.RemoteException; import android.os.SystemProperties; +import android.provider.Settings; import android.util.IntArray; import android.util.Log; import android.util.Pair; @@ -53,6 +56,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -243,16 +247,19 @@ public final class CameraExtensionCharacteristics { private static final String PROXY_SERVICE_NAME = "com.android.cameraextensions.CameraExtensionsProxyService"; + @FlaggedApi(Flags.FLAG_CONCERT_MODE) + private static final int FALLBACK_PACKAGE_NAME = + com.android.internal.R.string.config_extensionFallbackPackageName; + @FlaggedApi(Flags.FLAG_CONCERT_MODE) + private static final int FALLBACK_SERVICE_NAME = + com.android.internal.R.string.config_extensionFallbackServiceName; + // Singleton instance private static final CameraExtensionManagerGlobal GLOBAL_CAMERA_MANAGER = new CameraExtensionManagerGlobal(); private final Object mLock = new Object(); private final int PROXY_SERVICE_DELAY_MS = 2000; - private InitializerFuture mInitFuture = null; - private ServiceConnection mConnection = null; - private int mConnectionCount = 0; - private ICameraExtensionsProxyService mProxy = null; - private boolean mSupportsAdvancedExtensions = false; + private ExtensionConnectionManager mConnectionManager = new ExtensionConnectionManager(); // Singleton, don't allow construction private CameraExtensionManagerGlobal() {} @@ -261,17 +268,17 @@ public final class CameraExtensionCharacteristics { return GLOBAL_CAMERA_MANAGER; } - private void releaseProxyConnectionLocked(Context ctx) { - if (mConnection != null ) { - ctx.unbindService(mConnection); - mConnection = null; - mProxy = null; - mConnectionCount = 0; + private void releaseProxyConnectionLocked(Context ctx, int extension) { + if (mConnectionManager.getConnection(extension) != null) { + ctx.unbindService(mConnectionManager.getConnection(extension)); + mConnectionManager.setConnection(extension, null); + mConnectionManager.setProxy(extension, null); + mConnectionManager.resetConnectionCount(extension); } } - private void connectToProxyLocked(Context ctx) { - if (mConnection == null) { + private void connectToProxyLocked(Context ctx, int extension, boolean useFallback) { + if (mConnectionManager.getConnection(extension) == null) { Intent intent = new Intent(); intent.setClassName(PROXY_PACKAGE_NAME, PROXY_SERVICE_NAME); String vendorProxyPackage = SystemProperties.get( @@ -287,34 +294,55 @@ public final class CameraExtensionCharacteristics { + vendorProxyService); intent.setClassName(vendorProxyPackage, vendorProxyService); } - mInitFuture = new InitializerFuture(); - mConnection = new ServiceConnection() { + + if (Flags.concertMode() && useFallback) { + String packageName = ctx.getResources().getString(FALLBACK_PACKAGE_NAME); + String serviceName = ctx.getResources().getString(FALLBACK_SERVICE_NAME); + + if (!packageName.isEmpty() && !serviceName.isEmpty()) { + Log.v(TAG, + "Choosing the fallback software implementation package: " + + packageName); + Log.v(TAG, + "Choosing the fallback software implementation service: " + + serviceName); + intent.setClassName(packageName, serviceName); + } + } + + InitializerFuture initFuture = new InitializerFuture(); + ServiceConnection connection = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName component) { - mConnection = null; - mProxy = null; + mConnectionManager.setConnection(extension, null); + mConnectionManager.setProxy(extension, null); } @Override public void onServiceConnected(ComponentName component, IBinder binder) { - mProxy = ICameraExtensionsProxyService.Stub.asInterface(binder); - if (mProxy == null) { + ICameraExtensionsProxyService proxy = + ICameraExtensionsProxyService.Stub.asInterface(binder); + mConnectionManager.setProxy(extension, proxy); + if (mConnectionManager.getProxy(extension) == null) { throw new IllegalStateException("Camera Proxy service is null"); } try { - mSupportsAdvancedExtensions = mProxy.advancedExtensionsSupported(); + mConnectionManager.setAdvancedExtensionsSupported(extension, + mConnectionManager.getProxy(extension) + .advancedExtensionsSupported()); } catch (RemoteException e) { Log.e(TAG, "Remote IPC failed!"); } - mInitFuture.setStatus(true); + initFuture.setStatus(true); } }; ctx.bindService(intent, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT | Context.BIND_ABOVE_CLIENT | Context.BIND_NOT_VISIBLE, - android.os.AsyncTask.THREAD_POOL_EXECUTOR, mConnection); + android.os.AsyncTask.THREAD_POOL_EXECUTOR, connection); + mConnectionManager.setConnection(extension, connection); try { - mInitFuture.get(PROXY_SERVICE_DELAY_MS, TimeUnit.MILLISECONDS); + initFuture.get(PROXY_SERVICE_DELAY_MS, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { Log.e(TAG, "Timed out while initializing proxy service!"); } @@ -366,64 +394,102 @@ public final class CameraExtensionCharacteristics { } } - public boolean registerClient(Context ctx, IBinder token) { + public boolean registerClientHelper(Context ctx, IBinder token, int extension, + boolean useFallback) { synchronized (mLock) { boolean ret = false; - connectToProxyLocked(ctx); - if (mProxy == null) { + connectToProxyLocked(ctx, extension, useFallback); + if (mConnectionManager.getProxy(extension) == null) { return false; } - mConnectionCount++; + mConnectionManager.incrementConnectionCount(extension); try { - ret = mProxy.registerClient(token); + ret = mConnectionManager.getProxy(extension).registerClient(token); } catch (RemoteException e) { Log.e(TAG, "Failed to initialize extension! Extension service does " + " not respond!"); } if (!ret) { - mConnectionCount--; + mConnectionManager.decrementConnectionCount(extension); } - if (mConnectionCount <= 0) { - releaseProxyConnectionLocked(ctx); + if (mConnectionManager.getConnectionCount(extension) <= 0) { + releaseProxyConnectionLocked(ctx, extension); } return ret; } } - public void unregisterClient(Context ctx, IBinder token) { + @SuppressLint("NonUserGetterCalled") + public boolean registerClient(Context ctx, IBinder token, int extension, + String cameraId, Map<String, CameraMetadataNative> characteristicsMapNative) { + boolean ret = registerClientHelper(ctx, token, extension, false /*useFallback*/); + + if (Flags.concertMode()) { + // Check if user enabled fallback impl + ContentResolver resolver = ctx.getContentResolver(); + int userEnabled = Settings.Secure.getInt(resolver, + Settings.Secure.CAMERA_EXTENSIONS_FALLBACK, 1); + + boolean vendorImpl = true; + if (ret && (mConnectionManager.getProxy(extension) != null) && (userEnabled == 1)) { + // At this point, we are connected to either CameraExtensionsProxyService or + // the vendor extension proxy service. If the vendor does not support the + // extension, unregisterClient and re-register client with the proxy service + // containing the fallback impl + vendorImpl = isExtensionSupported(cameraId, extension, + characteristicsMapNative); + } + + if (!vendorImpl) { + unregisterClient(ctx, token, extension); + ret = registerClientHelper(ctx, token, extension, true /*useFallback*/); + + } + } + + return ret; + } + + public void unregisterClient(Context ctx, IBinder token, int extension) { synchronized (mLock) { - if (mProxy != null) { + if (mConnectionManager.getProxy(extension) != null) { try { - mProxy.unregisterClient(token); + mConnectionManager.getProxy(extension).unregisterClient(token); } catch (RemoteException e) { Log.e(TAG, "Failed to de-initialize extension! Extension service does" + " not respond!"); } finally { - mConnectionCount--; - if (mConnectionCount <= 0) { - releaseProxyConnectionLocked(ctx); + mConnectionManager.decrementConnectionCount(extension); + if (mConnectionManager.getConnectionCount(extension) <= 0) { + releaseProxyConnectionLocked(ctx, extension); } } } } } - public void initializeSession(IInitializeSessionCallback cb) throws RemoteException { + public void initializeSession(IInitializeSessionCallback cb, int extension) + throws RemoteException { synchronized (mLock) { - if (mProxy != null) { - mProxy.initializeSession(cb); + if (mConnectionManager.getProxy(extension) != null + && !mConnectionManager.isSessionInitialized()) { + mConnectionManager.getProxy(extension).initializeSession(cb); + mConnectionManager.setSessionInitialized(true); + } else { + cb.onFailure(); } } } - public void releaseSession() { + public void releaseSession(int extension) { synchronized (mLock) { - if (mProxy != null) { + if (mConnectionManager.getProxy(extension) != null) { try { - mProxy.releaseSession(); + mConnectionManager.getProxy(extension).releaseSession(); + mConnectionManager.setSessionInitialized(false); } catch (RemoteException e) { Log.e(TAG, "Failed to release session! Extension service does" + " not respond!"); @@ -432,77 +498,157 @@ public final class CameraExtensionCharacteristics { } } - public boolean areAdvancedExtensionsSupported() { - return mSupportsAdvancedExtensions; + public boolean areAdvancedExtensionsSupported(int extension) { + return mConnectionManager.areAdvancedExtensionsSupported(extension); } - public IPreviewExtenderImpl initializePreviewExtension(int extensionType) + public IPreviewExtenderImpl initializePreviewExtension(int extension) throws RemoteException { synchronized (mLock) { - if (mProxy != null) { - return mProxy.initializePreviewExtension(extensionType); + if (mConnectionManager.getProxy(extension) != null) { + return mConnectionManager.getProxy(extension) + .initializePreviewExtension(extension); } else { return null; } } } - public IImageCaptureExtenderImpl initializeImageExtension(int extensionType) + public IImageCaptureExtenderImpl initializeImageExtension(int extension) throws RemoteException { synchronized (mLock) { - if (mProxy != null) { - return mProxy.initializeImageExtension(extensionType); + if (mConnectionManager.getProxy(extension) != null) { + return mConnectionManager.getProxy(extension) + .initializeImageExtension(extension); } else { return null; } } } - public IAdvancedExtenderImpl initializeAdvancedExtension(int extensionType) + public IAdvancedExtenderImpl initializeAdvancedExtension(int extension) throws RemoteException { synchronized (mLock) { - if (mProxy != null) { - return mProxy.initializeAdvancedExtension(extensionType); + if (mConnectionManager.getProxy(extension) != null) { + return mConnectionManager.getProxy(extension) + .initializeAdvancedExtension(extension); } else { return null; } } } + + private class ExtensionConnectionManager { + // Maps extension to ExtensionConnection + private Map<Integer, ExtensionConnection> mConnections = new HashMap<>(); + private boolean mSessionInitialized = false; + + public ExtensionConnectionManager() { + IntArray extensionList = new IntArray(EXTENSION_LIST.length); + extensionList.addAll(EXTENSION_LIST); + if (Flags.concertMode()) { + extensionList.add(EXTENSION_EYES_FREE_VIDEOGRAPHY); + } + + for (int extensionType : extensionList.toArray()) { + mConnections.put(extensionType, new ExtensionConnection()); + } + } + + public ICameraExtensionsProxyService getProxy(@Extension int extension) { + return mConnections.get(extension).mProxy; + } + + public ServiceConnection getConnection(@Extension int extension) { + return mConnections.get(extension).mConnection; + } + + public int getConnectionCount(@Extension int extension) { + return mConnections.get(extension).mConnectionCount; + } + + public boolean areAdvancedExtensionsSupported(@Extension int extension) { + return mConnections.get(extension).mSupportsAdvancedExtensions; + } + + public boolean isSessionInitialized() { + return mSessionInitialized; + } + + public void setProxy(@Extension int extension, ICameraExtensionsProxyService proxy) { + mConnections.get(extension).mProxy = proxy; + } + + public void setConnection(@Extension int extension, ServiceConnection connection) { + mConnections.get(extension).mConnection = connection; + } + + public void incrementConnectionCount(@Extension int extension) { + mConnections.get(extension).mConnectionCount++; + } + + public void decrementConnectionCount(@Extension int extension) { + mConnections.get(extension).mConnectionCount--; + } + + public void resetConnectionCount(@Extension int extension) { + mConnections.get(extension).mConnectionCount = 0; + } + + public void setAdvancedExtensionsSupported(@Extension int extension, + boolean advancedExtSupported) { + mConnections.get(extension).mSupportsAdvancedExtensions = advancedExtSupported; + } + + public void setSessionInitialized(boolean initialized) { + mSessionInitialized = initialized; + } + + private class ExtensionConnection { + public ICameraExtensionsProxyService mProxy = null; + public ServiceConnection mConnection = null; + public int mConnectionCount = 0; + public boolean mSupportsAdvancedExtensions = false; + } + } } /** * @hide */ - public static boolean registerClient(Context ctx, IBinder token) { - return CameraExtensionManagerGlobal.get().registerClient(ctx, token); + public static boolean registerClient(Context ctx, IBinder token, int extension, + String cameraId, Map<String, CameraMetadataNative> characteristicsMapNative) { + return CameraExtensionManagerGlobal.get().registerClient(ctx, token, extension, cameraId, + characteristicsMapNative); } /** * @hide */ - public static void unregisterClient(Context ctx, IBinder token) { - CameraExtensionManagerGlobal.get().unregisterClient(ctx, token); + public static void unregisterClient(Context ctx, IBinder token, int extension) { + CameraExtensionManagerGlobal.get().unregisterClient(ctx, token, extension); } /** * @hide */ - public static void initializeSession(IInitializeSessionCallback cb) throws RemoteException { - CameraExtensionManagerGlobal.get().initializeSession(cb); + public static void initializeSession(IInitializeSessionCallback cb, int extension) + throws RemoteException { + CameraExtensionManagerGlobal.get().initializeSession(cb, extension); } /** * @hide */ - public static void releaseSession() { - CameraExtensionManagerGlobal.get().releaseSession(); + public static void releaseSession(int extension) { + CameraExtensionManagerGlobal.get().releaseSession(extension); } /** * @hide */ - public static boolean areAdvancedExtensionsSupported() { - return CameraExtensionManagerGlobal.get().areAdvancedExtensionsSupported(); + public static boolean areAdvancedExtensionsSupported(int extension) { + return CameraExtensionManagerGlobal.get().areAdvancedExtensionsSupported(extension); } /** @@ -510,7 +656,7 @@ public final class CameraExtensionCharacteristics { */ public static boolean isExtensionSupported(String cameraId, int extensionType, Map<String, CameraMetadataNative> characteristicsMap) { - if (areAdvancedExtensionsSupported()) { + if (areAdvancedExtensionsSupported(extensionType)) { try { IAdvancedExtenderImpl extender = initializeAdvancedExtension(extensionType); return extender.isExtensionAvailable(cameraId, characteristicsMap); @@ -600,24 +746,24 @@ public final class CameraExtensionCharacteristics { public @NonNull List<Integer> getSupportedExtensions() { ArrayList<Integer> ret = new ArrayList<>(); final IBinder token = new Binder(TAG + "#getSupportedExtensions:" + mCameraId); - boolean success = registerClient(mContext, token); - if (!success) { - return Collections.unmodifiableList(ret); - } IntArray extensionList = new IntArray(EXTENSION_LIST.length); extensionList.addAll(EXTENSION_LIST); if (Flags.concertMode()) { extensionList.add(EXTENSION_EYES_FREE_VIDEOGRAPHY); } - try { - for (int extensionType : extensionList.toArray()) { - if (isExtensionSupported(mCameraId, extensionType, mCharacteristicsMapNative)) { + + for (int extensionType : extensionList.toArray()) { + try { + boolean success = registerClient(mContext, token, extensionType, mCameraId, + mCharacteristicsMapNative); + if (success && isExtensionSupported(mCameraId, extensionType, + mCharacteristicsMapNative)) { ret.add(extensionType); } + } finally { + unregisterClient(mContext, token, extensionType); } - } finally { - unregisterClient(mContext, token); } return Collections.unmodifiableList(ret); @@ -643,7 +789,8 @@ public final class CameraExtensionCharacteristics { public <T> @Nullable T get(@Extension int extension, @NonNull CameraCharacteristics.Key<T> key) { final IBinder token = new Binder(TAG + "#get:" + mCameraId); - boolean success = registerClient(mContext, token); + boolean success = registerClient(mContext, token, extension, mCameraId, + mCharacteristicsMapNative); if (!success) { throw new IllegalArgumentException("Unsupported extensions"); } @@ -653,7 +800,7 @@ public final class CameraExtensionCharacteristics { throw new IllegalArgumentException("Unsupported extension"); } - if (areAdvancedExtensionsSupported() && getKeys(extension).contains(key)) { + if (areAdvancedExtensionsSupported(extension) && getKeys(extension).contains(key)) { IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension); extender.init(mCameraId, mCharacteristicsMapNative); CameraMetadataNative metadata = @@ -670,7 +817,7 @@ public final class CameraExtensionCharacteristics { Log.e(TAG, "Failed to query the extension for the specified key! Extension " + "service does not respond!"); } finally { - unregisterClient(mContext, token); + unregisterClient(mContext, token, extension); } return null; } @@ -691,7 +838,8 @@ public final class CameraExtensionCharacteristics { public @NonNull Set<CameraCharacteristics.Key> getKeys(@Extension int extension) { final IBinder token = new Binder(TAG + "#getKeys:" + mCameraId); - boolean success = registerClient(mContext, token); + boolean success = registerClient(mContext, token, extension, mCameraId, + mCharacteristicsMapNative); if (!success) { throw new IllegalArgumentException("Unsupported extensions"); } @@ -703,7 +851,7 @@ public final class CameraExtensionCharacteristics { throw new IllegalArgumentException("Unsupported extension"); } - if (areAdvancedExtensionsSupported()) { + if (areAdvancedExtensionsSupported(extension)) { IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension); extender.init(mCameraId, mCharacteristicsMapNative); CameraMetadataNative metadata = @@ -732,7 +880,7 @@ public final class CameraExtensionCharacteristics { Log.e(TAG, "Failed to query the extension for all available keys! Extension " + "service does not respond!"); } finally { - unregisterClient(mContext, token); + unregisterClient(mContext, token, extension); } return Collections.unmodifiableSet(ret); } @@ -755,7 +903,8 @@ public final class CameraExtensionCharacteristics { */ public boolean isPostviewAvailable(@Extension int extension) { final IBinder token = new Binder(TAG + "#isPostviewAvailable:" + mCameraId); - boolean success = registerClient(mContext, token); + boolean success = registerClient(mContext, token, extension, mCameraId, + mCharacteristicsMapNative); if (!success) { throw new IllegalArgumentException("Unsupported extensions"); } @@ -765,7 +914,7 @@ public final class CameraExtensionCharacteristics { throw new IllegalArgumentException("Unsupported extension"); } - if (areAdvancedExtensionsSupported()) { + if (areAdvancedExtensionsSupported(extension)) { IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension); extender.init(mCameraId, mCharacteristicsMapNative); return extender.isPostviewAvailable(); @@ -779,7 +928,7 @@ public final class CameraExtensionCharacteristics { Log.e(TAG, "Failed to query the extension for postview availability! Extension " + "service does not respond!"); } finally { - unregisterClient(mContext, token); + unregisterClient(mContext, token, extension); } return false; @@ -813,7 +962,8 @@ public final class CameraExtensionCharacteristics { public List<Size> getPostviewSupportedSizes(@Extension int extension, @NonNull Size captureSize, int format) { final IBinder token = new Binder(TAG + "#getPostviewSupportedSizes:" + mCameraId); - boolean success = registerClient(mContext, token); + boolean success = registerClient(mContext, token, extension, mCameraId, + mCharacteristicsMapNative); if (!success) { throw new IllegalArgumentException("Unsupported extensions"); } @@ -831,7 +981,7 @@ public final class CameraExtensionCharacteristics { StreamConfigurationMap streamMap = mCharacteristicsMap.get(mCameraId).get( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); - if (areAdvancedExtensionsSupported()) { + if (areAdvancedExtensionsSupported(extension)) { switch(format) { case ImageFormat.YUV_420_888: case ImageFormat.JPEG: @@ -879,7 +1029,7 @@ public final class CameraExtensionCharacteristics { + "service does not respond!"); return Collections.emptyList(); } finally { - unregisterClient(mContext, token); + unregisterClient(mContext, token, extension); } } @@ -917,7 +1067,8 @@ public final class CameraExtensionCharacteristics { // ambiguity is resolved in b/169799538. final IBinder token = new Binder(TAG + "#getExtensionSupportedSizes:" + mCameraId); - boolean success = registerClient(mContext, token); + boolean success = registerClient(mContext, token, extension, mCameraId, + mCharacteristicsMapNative); if (!success) { throw new IllegalArgumentException("Unsupported extensions"); } @@ -929,7 +1080,7 @@ public final class CameraExtensionCharacteristics { StreamConfigurationMap streamMap = mCharacteristicsMap.get(mCameraId).get( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); - if (areAdvancedExtensionsSupported()) { + if (areAdvancedExtensionsSupported(extension)) { IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension); extender.init(mCameraId, mCharacteristicsMapNative); return generateSupportedSizes( @@ -948,7 +1099,7 @@ public final class CameraExtensionCharacteristics { + " not respond!"); return new ArrayList<>(); } finally { - unregisterClient(mContext, token); + unregisterClient(mContext, token, extension); } } @@ -978,7 +1129,8 @@ public final class CameraExtensionCharacteristics { List<Size> getExtensionSupportedSizes(@Extension int extension, int format) { try { final IBinder token = new Binder(TAG + "#getExtensionSupportedSizes:" + mCameraId); - boolean success = registerClient(mContext, token); + boolean success = registerClient(mContext, token, extension, mCameraId, + mCharacteristicsMapNative); if (!success) { throw new IllegalArgumentException("Unsupported extensions"); } @@ -990,7 +1142,7 @@ public final class CameraExtensionCharacteristics { StreamConfigurationMap streamMap = mCharacteristicsMap.get(mCameraId).get( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); - if (areAdvancedExtensionsSupported()) { + if (areAdvancedExtensionsSupported(extension)) { switch(format) { case ImageFormat.YUV_420_888: case ImageFormat.JPEG: @@ -1035,7 +1187,7 @@ public final class CameraExtensionCharacteristics { } } } finally { - unregisterClient(mContext, token); + unregisterClient(mContext, token, extension); } } catch (RemoteException e) { Log.e(TAG, "Failed to query the extension supported sizes! Extension service does" @@ -1073,7 +1225,8 @@ public final class CameraExtensionCharacteristics { } final IBinder token = new Binder(TAG + "#getEstimatedCaptureLatencyRangeMillis:" + mCameraId); - boolean success = registerClient(mContext, token); + boolean success = registerClient(mContext, token, extension, mCameraId, + mCharacteristicsMapNative); if (!success) { throw new IllegalArgumentException("Unsupported extensions"); } @@ -1087,7 +1240,7 @@ public final class CameraExtensionCharacteristics { new android.hardware.camera2.extension.Size(); sz.width = captureOutputSize.getWidth(); sz.height = captureOutputSize.getHeight(); - if (areAdvancedExtensionsSupported()) { + if (areAdvancedExtensionsSupported(extension)) { IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension); extender.init(mCameraId, mCharacteristicsMapNative); LatencyRange latencyRange = extender.getEstimatedCaptureLatencyRange(mCameraId, @@ -1126,7 +1279,7 @@ public final class CameraExtensionCharacteristics { Log.e(TAG, "Failed to query the extension capture latency! Extension service does" + " not respond!"); } finally { - unregisterClient(mContext, token); + unregisterClient(mContext, token, extension); } return null; @@ -1143,7 +1296,8 @@ public final class CameraExtensionCharacteristics { */ public boolean isCaptureProcessProgressAvailable(@Extension int extension) { final IBinder token = new Binder(TAG + "#isCaptureProcessProgressAvailable:" + mCameraId); - boolean success = registerClient(mContext, token); + boolean success = registerClient(mContext, token, extension, mCameraId, + mCharacteristicsMapNative); if (!success) { throw new IllegalArgumentException("Unsupported extensions"); } @@ -1153,7 +1307,7 @@ public final class CameraExtensionCharacteristics { throw new IllegalArgumentException("Unsupported extension"); } - if (areAdvancedExtensionsSupported()) { + if (areAdvancedExtensionsSupported(extension)) { IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension); extender.init(mCameraId, mCharacteristicsMapNative); return extender.isCaptureProcessProgressAvailable(); @@ -1167,7 +1321,7 @@ public final class CameraExtensionCharacteristics { Log.e(TAG, "Failed to query the extension progress callbacks! Extension service does" + " not respond!"); } finally { - unregisterClient(mContext, token); + unregisterClient(mContext, token, extension); } return false; @@ -1195,7 +1349,8 @@ public final class CameraExtensionCharacteristics { @NonNull public Set<CaptureRequest.Key> getAvailableCaptureRequestKeys(@Extension int extension) { final IBinder token = new Binder(TAG + "#getAvailableCaptureRequestKeys:" + mCameraId); - boolean success = registerClient(mContext, token); + boolean success = registerClient(mContext, token, extension, mCameraId, + mCharacteristicsMapNative); if (!success) { throw new IllegalArgumentException("Unsupported extensions"); } @@ -1208,7 +1363,7 @@ public final class CameraExtensionCharacteristics { } CameraMetadataNative captureRequestMeta = null; - if (areAdvancedExtensionsSupported()) { + if (areAdvancedExtensionsSupported(extension)) { IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension); extender.init(mCameraId, mCharacteristicsMapNative); captureRequestMeta = extender.getAvailableCaptureRequestKeys(mCameraId); @@ -1250,7 +1405,7 @@ public final class CameraExtensionCharacteristics { } catch (RemoteException e) { throw new IllegalStateException("Failed to query the available capture request keys!"); } finally { - unregisterClient(mContext, token); + unregisterClient(mContext, token, extension); } return Collections.unmodifiableSet(ret); @@ -1282,7 +1437,8 @@ public final class CameraExtensionCharacteristics { @NonNull public Set<CaptureResult.Key> getAvailableCaptureResultKeys(@Extension int extension) { final IBinder token = new Binder(TAG + "#getAvailableCaptureResultKeys:" + mCameraId); - boolean success = registerClient(mContext, token); + boolean success = registerClient(mContext, token, extension, mCameraId, + mCharacteristicsMapNative); if (!success) { throw new IllegalArgumentException("Unsupported extensions"); } @@ -1294,7 +1450,7 @@ public final class CameraExtensionCharacteristics { } CameraMetadataNative captureResultMeta = null; - if (areAdvancedExtensionsSupported()) { + if (areAdvancedExtensionsSupported(extension)) { IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension); extender.init(mCameraId, mCharacteristicsMapNative); captureResultMeta = extender.getAvailableCaptureResultKeys(mCameraId); @@ -1336,7 +1492,7 @@ public final class CameraExtensionCharacteristics { } catch (RemoteException e) { throw new IllegalStateException("Failed to query the available capture result keys!"); } finally { - unregisterClient(mContext, token); + unregisterClient(mContext, token, extension); } return Collections.unmodifiableSet(ret); diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java index f6c8f36a1b01..b2032fa3db81 100644 --- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java @@ -102,6 +102,8 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes private boolean mInitialized; private boolean mSessionClosed; + private int mExtensionType; + private final Context mContext; @@ -205,7 +207,7 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes CameraAdvancedExtensionSessionImpl ret = new CameraAdvancedExtensionSessionImpl(ctx, extender, cameraDevice, characteristicsMapNative, repeatingRequestSurface, burstCaptureSurface, postviewSurface, config.getStateCallback(), - config.getExecutor(), sessionId, token); + config.getExecutor(), sessionId, token, config.getExtension()); ret.mStatsAggregator.setClientName(ctx.getOpPackageName()); ret.mStatsAggregator.setExtensionType(config.getExtension()); @@ -223,7 +225,8 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes @Nullable Surface postviewSurface, @NonNull StateCallback callback, @NonNull Executor executor, int sessionId, - @NonNull IBinder token) { + @NonNull IBinder token, + int extension) { mContext = ctx; mAdvancedExtender = extender; mCameraDevice = cameraDevice; @@ -242,6 +245,7 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes mSessionId = sessionId; mToken = token; mInterfaceLock = cameraDevice.mInterfaceLock; + mExtensionType = extension; mStatsAggregator = new ExtensionSessionStatsAggregator(mCameraDevice.getId(), /*isAdvanced=*/true); @@ -583,9 +587,9 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes if (mToken != null) { if (mInitialized || (mCaptureSession != null)) { notifyClose = true; - CameraExtensionCharacteristics.releaseSession(); + CameraExtensionCharacteristics.releaseSession(mExtensionType); } - CameraExtensionCharacteristics.unregisterClient(mContext, mToken); + CameraExtensionCharacteristics.unregisterClient(mContext, mToken, mExtensionType); } mInitialized = false; mToken = null; @@ -654,7 +658,8 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes } try { - CameraExtensionCharacteristics.initializeSession(mInitializeHandler); + CameraExtensionCharacteristics.initializeSession( + mInitializeHandler, mExtensionType); } catch (RemoteException e) { Log.e(TAG, "Failed to initialize session! Extension service does" + " not respond!"); diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index ccb24e7d2457..f03876b985ec 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -2559,13 +2559,16 @@ public class CameraDeviceImpl extends CameraDevice boolean initializationFailed = true; IBinder token = new Binder(TAG + " : " + mNextSessionId++); try { - boolean ret = CameraExtensionCharacteristics.registerClient(mContext, token); + boolean ret = CameraExtensionCharacteristics.registerClient(mContext, token, + extensionConfiguration.getExtension(), mCameraId, + CameraExtensionUtils.getCharacteristicsMapNative(characteristicsMap)); if (!ret) { token = null; throw new UnsupportedOperationException("Unsupported extension!"); } - if (CameraExtensionCharacteristics.areAdvancedExtensionsSupported()) { + if (CameraExtensionCharacteristics.areAdvancedExtensionsSupported( + extensionConfiguration.getExtension())) { mCurrentAdvancedExtensionSession = CameraAdvancedExtensionSessionImpl.createCameraAdvancedExtensionSession( this, characteristicsMap, mContext, extensionConfiguration, @@ -2580,7 +2583,8 @@ public class CameraDeviceImpl extends CameraDevice throw new CameraAccessException(CameraAccessException.CAMERA_ERROR); } finally { if (initializationFailed && (token != null)) { - CameraExtensionCharacteristics.unregisterClient(mContext, token); + CameraExtensionCharacteristics.unregisterClient(mContext, token, + extensionConfiguration.getExtension()); } } } diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java index db7055b1756d..725b4139bb95 100644 --- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java @@ -118,6 +118,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { // In case the client doesn't explicitly enable repeating requests, the framework // will do so internally. private boolean mInternalRepeatingRequestEnabled = true; + private int mExtensionType; private final Context mContext; @@ -244,7 +245,8 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { sessionId, token, extensionChars.getAvailableCaptureRequestKeys(config.getExtension()), - extensionChars.getAvailableCaptureResultKeys(config.getExtension())); + extensionChars.getAvailableCaptureResultKeys(config.getExtension()), + config.getExtension()); session.mStatsAggregator.setClientName(ctx.getOpPackageName()); session.mStatsAggregator.setExtensionType(config.getExtension()); @@ -266,7 +268,8 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { int sessionId, @NonNull IBinder token, @NonNull Set<CaptureRequest.Key> requestKeys, - @Nullable Set<CaptureResult.Key> resultKeys) { + @Nullable Set<CaptureResult.Key> resultKeys, + int extension) { mContext = ctx; mImageExtender = imageExtender; mPreviewExtender = previewExtender; @@ -289,6 +292,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { mSupportedResultKeys = resultKeys; mCaptureResultsSupported = !resultKeys.isEmpty(); mInterfaceLock = cameraDevice.mInterfaceLock; + mExtensionType = extension; mStatsAggregator = new ExtensionSessionStatsAggregator(mCameraDevice.getId(), /*isAdvanced=*/false); @@ -881,9 +885,9 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { if (mToken != null) { if (mInitialized || (mCaptureSession != null)) { notifyClose = true; - CameraExtensionCharacteristics.releaseSession(); + CameraExtensionCharacteristics.releaseSession(mExtensionType); } - CameraExtensionCharacteristics.unregisterClient(mContext, mToken); + CameraExtensionCharacteristics.unregisterClient(mContext, mToken, mExtensionType); } mInitialized = false; mToken = null; @@ -1000,7 +1004,8 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { mStatsAggregator.commit(/*isFinal*/false); try { finishPipelineInitialization(); - CameraExtensionCharacteristics.initializeSession(mInitializeHandler); + CameraExtensionCharacteristics.initializeSession( + mInitializeHandler, mExtensionType); } catch (RemoteException e) { Log.e(TAG, "Failed to initialize session! Extension service does" + " not respond!"); diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index b68b94d5bf2d..a6b2ed09a64d 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -31,6 +31,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.location.GnssSignalQuality; +import android.net.NetworkCapabilities; import android.os.BatteryStatsManager.WifiState; import android.os.BatteryStatsManager.WifiSupplState; import android.server.ServerProtoEnums; @@ -59,6 +60,7 @@ import com.android.internal.os.BatteryStatsHistoryIterator; import com.android.internal.os.CpuScalingPolicies; import com.android.internal.os.MonotonicClock; import com.android.internal.os.PowerStats; +import com.android.net.module.util.NetworkCapabilitiesUtils; import com.google.android.collect.Lists; @@ -2734,26 +2736,28 @@ public abstract class BatteryStats { "emngcy", "other" }; + public static final int NUM_ALL_NETWORK_TYPES = getAllNetworkTypesCount(); public static final int DATA_CONNECTION_OUT_OF_SERVICE = 0; - public static final int DATA_CONNECTION_EMERGENCY_SERVICE = getEmergencyNetworkConnectionType(); - public static final int DATA_CONNECTION_OTHER = DATA_CONNECTION_EMERGENCY_SERVICE + 1; + public static final int DATA_CONNECTION_EMERGENCY_SERVICE = NUM_ALL_NETWORK_TYPES + 1; + public static final int DATA_CONNECTION_OTHER = NUM_ALL_NETWORK_TYPES + 2; @UnsupportedAppUsage - public static final int NUM_DATA_CONNECTION_TYPES = DATA_CONNECTION_OTHER + 1; + public static final int NUM_DATA_CONNECTION_TYPES = NUM_ALL_NETWORK_TYPES + 3; + @android.ravenwood.annotation.RavenwoodReplace - private static int getEmergencyNetworkConnectionType() { + public static int getAllNetworkTypesCount() { int count = TelephonyManager.getAllNetworkTypes().length; if (DATA_CONNECTION_NAMES.length != count + 3) { // oos, emngcy, other throw new IllegalStateException( "DATA_CONNECTION_NAMES length does not match network type count. " + "Expected: " + (count + 3) + ", actual:" + DATA_CONNECTION_NAMES.length); } - return count + 1; + return count; } - private static int getEmergencyNetworkConnectionType$ravenwood() { - return DATA_CONNECTION_NAMES.length - 2; + public static int getAllNetworkTypesCount$ravenwood() { + return DATA_CONNECTION_NAMES.length - 3; // oos, emngcy, other } /** @@ -9071,4 +9075,31 @@ public abstract class BatteryStats { protected static boolean isKernelStatsAvailable$ravenwood() { return false; } + + @android.ravenwood.annotation.RavenwoodReplace + protected static int getDisplayTransport(int[] transports) { + return NetworkCapabilitiesUtils.getDisplayTransport(transports); + } + + // See NetworkCapabilitiesUtils + private static final int[] DISPLAY_TRANSPORT_PRIORITIES = new int[] { + NetworkCapabilities.TRANSPORT_VPN, + NetworkCapabilities.TRANSPORT_CELLULAR, + NetworkCapabilities.TRANSPORT_WIFI_AWARE, + NetworkCapabilities.TRANSPORT_BLUETOOTH, + NetworkCapabilities.TRANSPORT_WIFI, + NetworkCapabilities.TRANSPORT_ETHERNET, + NetworkCapabilities.TRANSPORT_USB + }; + + protected static int getDisplayTransport$ravenwood(int[] transports) { + for (int transport : DISPLAY_TRANSPORT_PRIORITIES) { + for (int t : transports) { + if (t == transport) { + return transport; + } + } + } + return transports[0]; + } } diff --git a/core/java/android/os/BluetoothBatteryStats.java b/core/java/android/os/BluetoothBatteryStats.java index 3d99a08a59c5..fa8f39d1642b 100644 --- a/core/java/android/os/BluetoothBatteryStats.java +++ b/core/java/android/os/BluetoothBatteryStats.java @@ -26,6 +26,7 @@ import java.util.List; * * @hide */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class BluetoothBatteryStats implements Parcelable { /** @hide */ diff --git a/core/java/android/os/UserBatteryConsumer.java b/core/java/android/os/UserBatteryConsumer.java index a2ff078263ca..23ba0c635eca 100644 --- a/core/java/android/os/UserBatteryConsumer.java +++ b/core/java/android/os/UserBatteryConsumer.java @@ -34,6 +34,7 @@ import java.util.List; * * {@hide} */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class UserBatteryConsumer extends BatteryConsumer { static final int CONSUMER_TYPE_USER = 2; diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 0fbdbc48fd99..ce0379887d36 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -180,11 +180,14 @@ public class UserManager { /** - * User type representing a private profile. + * User type representing a private profile. Private profile is a user profile that can be used + * as an alternative user-space to install and use sensitive apps. + * UI surfaces can adopt an alternative strategy to show apps belonging to this profile, in line + * with their sensitive nature. * @hide */ @FlaggedApi(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE) - @TestApi + @SystemApi public static final String USER_TYPE_PROFILE_PRIVATE = "android.os.usertype.profile.PRIVATE"; /** diff --git a/core/java/android/os/WakeLockStats.java b/core/java/android/os/WakeLockStats.java index 05a7313d1600..69e70a0a8b07 100644 --- a/core/java/android/os/WakeLockStats.java +++ b/core/java/android/os/WakeLockStats.java @@ -25,6 +25,7 @@ import java.util.List; * Snapshot of wake lock stats. * @hide */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public final class WakeLockStats implements Parcelable { /** @hide */ diff --git a/core/java/android/provider/ContactKeysManager.java b/core/java/android/provider/ContactKeysManager.java index bef645698b4a..01aaa3d9647e 100644 --- a/core/java/android/provider/ContactKeysManager.java +++ b/core/java/android/provider/ContactKeysManager.java @@ -39,18 +39,19 @@ import java.util.List; import java.util.Objects; /** - * ContactKeysManager provides the access to the E2EE contact keys provider. - * It manages two types of keys - {@link ContactKey} of other users' and the owner's keys - - * {@link SelfKey}. + * ContactKeysManager provides access to the provider of end-to-end encryption contact keys. + * It manages two types of keys - {@link ContactKey} and {@link SelfKey}. * <ul> * <li> - * For {@link ContactKey} this API allows the insert/update, removal, changing of the - * verification state, retrieving the keys (either created by or visible to the caller app) - * operations. + * A {@link ContactKey} is a public key associated with a contact. It's used to end-to-end + * encrypt the communications between a user and the contact. This API allows operations on + * {@link ContactKey}s to insert/update, remove, change the verification state, and retrieving + * keys (either created by or visible to the caller app). * </li> * <li> - * For {@link SelfKey} this API allows the insert/update, removal, retrieving the self keys - * (either created by or visible to the caller app) operations. + * A {@link SelfKey} is a key for this device, so the key represents the owner of the device. + * This API allows operations on {@link SelfKey}s to insert/update, remove, and retrieving + * self keys (either created by or visible to the caller app). * </li> * </ul> * Keys are uniquely identified by: @@ -71,7 +72,7 @@ import java.util.Objects; * ContactsProvider. */ @FlaggedApi(Flags.FLAG_USER_KEYS) -public class ContactKeysManager { +public final class ContactKeysManager { /** * The authority for the contact keys provider. * @hide @@ -354,9 +355,9 @@ public class ContactKeysManager { private static void validateVerificationState(int verificationState) { - if (verificationState != UNVERIFIED - && verificationState != VERIFICATION_FAILED - && verificationState != VERIFIED) { + if (verificationState != VERIFICATION_STATE_UNVERIFIED + && verificationState != VERIFICATION_STATE_VERIFICATION_FAILED + && verificationState != VERIFICATION_STATE_VERIFIED) { throw new IllegalArgumentException("Verification state value " + verificationState + " is not supported"); } @@ -600,25 +601,25 @@ public class ContactKeysManager { * @hide */ @IntDef(prefix = {"VERIFICATION_STATE_"}, value = { - UNVERIFIED, - VERIFICATION_FAILED, - VERIFIED + VERIFICATION_STATE_UNVERIFIED, + VERIFICATION_STATE_VERIFICATION_FAILED, + VERIFICATION_STATE_VERIFIED }) @Retention(RetentionPolicy.SOURCE) public @interface VerificationState {} /** - * Unverified state of a contact E2EE key. + * Unverified state of a contact end to end encrypted key. */ - public static final int UNVERIFIED = 0; + public static final int VERIFICATION_STATE_UNVERIFIED = 0; /** - * Failed verification state of a contact E2EE key. + * Failed verification state of a contact end to end encrypted key. */ - public static final int VERIFICATION_FAILED = 1; + public static final int VERIFICATION_STATE_VERIFICATION_FAILED = 1; /** - * Verified state of a contact E2EE key. + * Verified state of a contact end to end encrypted key. */ - public static final int VERIFIED = 2; + public static final int VERIFICATION_STATE_VERIFIED = 2; /** @hide */ public static final class ContactKeys { @@ -791,7 +792,7 @@ public class ContactKeysManager { } /** - * A parcelable class encapsulating other users' E2EE contact key. + * A parcelable class encapsulating other users' end to end encrypted contact key. */ public static final class ContactKey implements Parcelable { /** @@ -1056,7 +1057,7 @@ public class ContactKeysManager { } /** - * A parcelable class encapsulating self E2EE contact key. + * A parcelable class encapsulating self end to end encrypted contact key. */ public static final class SelfKey implements Parcelable { /** diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 26f46cfe85c7..b026ce94b3fd 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -668,6 +668,23 @@ public final class Settings { "android.settings.MANAGE_APP_LONG_RUNNING_JOBS"; /** + * Activity Action: Show settings to allow configuration of + * {@link Manifest.permission#RUN_BACKUP_JOBS} permission. + * + * Input: Optionally, the Intent's data URI can specify the application package name to + * directly invoke the management GUI specific to the package name. For example + * "package:com.my.app". + * <p> + * Output: When a package data uri is passed as input, the activity result is set to + * {@link android.app.Activity#RESULT_OK} if the permission was granted to the app. Otherwise, + * the result is set to {@link android.app.Activity#RESULT_CANCELED}. + */ + @FlaggedApi(Flags.FLAG_BACKUP_TASKS_SETTINGS_SCREEN) + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_REQUEST_RUN_BACKUP_JOBS = + "android.settings.REQUEST_RUN_BACKUP_JOBS"; + + /** * Activity Action: Show settings to allow configuration of cross-profile access for apps * * Input: Optionally, the Intent's data URI can specify the application package name to @@ -11833,6 +11850,7 @@ public final class Settings { * Whether to enable camera extensions software fallback. * @hide */ + @Readable public static final String CAMERA_EXTENSIONS_FALLBACK = "camera_extensions_fallback"; /** diff --git a/core/java/android/provider/flags.aconfig b/core/java/android/provider/flags.aconfig index 0f12b1397c5b..ea1ac2793a11 100644 --- a/core/java/android/provider/flags.aconfig +++ b/core/java/android/provider/flags.aconfig @@ -13,3 +13,10 @@ flag { description: "This flag controls new E2EE contact keys API" bug: "290696572" } + +flag { + name: "backup_tasks_settings_screen" + namespace: "backstage_power" + description: "Add a new settings page for the RUN_BACKUP_JOBS permission." + bug: "320563660" +} diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig index 3cc7f5a6ee04..add575b23792 100644 --- a/core/java/android/service/chooser/flags.aconfig +++ b/core/java/android/service/chooser/flags.aconfig @@ -8,6 +8,13 @@ flag { } flag { + name: "enable_sharesheet_metadata_extra" + namespace: "intentresolver" + description: "This flag enables sharesheet metadata to be displayed to users." + bug: "318942069" +} + +flag { name: "support_nfc_resolver" namespace: "systemui" description: "This flag controls the new NFC 'resolver' activity" @@ -20,3 +27,10 @@ flag { description: "This flag controls content toggling in Chooser" bug: "302691505" } + +flag { + name: "enable_chooser_result" + namespace: "intentresolver" + description: "Provides additional callbacks with information about user actions in ChooserResult" + bug: "263474465" +} diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java index a6d3bb47d9c8..6410609a589d 100644 --- a/core/java/android/text/BoringLayout.java +++ b/core/java/android/text/BoringLayout.java @@ -585,9 +585,7 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback } if (ClientFlags.fixLineHeightForLocale()) { - if (minimumFontMetrics == null) { - paint.getFontMetricsIntForLocale(fm); - } else { + if (minimumFontMetrics != null) { fm.set(minimumFontMetrics); // Because the font metrics is provided by public APIs, adjust the top/bottom with // ascent/descent: top must be smaller than ascent, bottom must be larger than diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index 99bd2ff7134c..5986238d3035 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -767,22 +767,14 @@ public class StaticLayout extends Layout { } int defaultTop; - int defaultAscent; - int defaultDescent; + final int defaultAscent; + final int defaultDescent; int defaultBottom; - if (ClientFlags.fixLineHeightForLocale()) { - if (b.mMinimumFontMetrics != null) { - defaultTop = (int) Math.floor(b.mMinimumFontMetrics.top); - defaultAscent = Math.round(b.mMinimumFontMetrics.ascent); - defaultDescent = Math.round(b.mMinimumFontMetrics.descent); - defaultBottom = (int) Math.ceil(b.mMinimumFontMetrics.bottom); - } else { - paint.getFontMetricsIntForLocale(fm); - defaultTop = fm.top; - defaultAscent = fm.ascent; - defaultDescent = fm.descent; - defaultBottom = fm.bottom; - } + if (ClientFlags.fixLineHeightForLocale() && b.mMinimumFontMetrics != null) { + defaultTop = (int) Math.floor(b.mMinimumFontMetrics.top); + defaultAscent = Math.round(b.mMinimumFontMetrics.ascent); + defaultDescent = Math.round(b.mMinimumFontMetrics.descent); + defaultBottom = (int) Math.ceil(b.mMinimumFontMetrics.bottom); // Because the font metrics is provided by public APIs, adjust the top/bottom with // ascent/descent: top must be smaller than ascent, bottom must be larger than descent. @@ -1043,10 +1035,10 @@ public class StaticLayout extends Layout { if (endPos < spanEnd) { // preserve metrics for current span - fmTop = fm.top; - fmBottom = fm.bottom; - fmAscent = fm.ascent; - fmDescent = fm.descent; + fmTop = Math.min(defaultTop, fm.top); + fmBottom = Math.max(defaultBottom, fm.bottom); + fmAscent = Math.min(defaultAscent, fm.ascent); + fmDescent = Math.max(defaultDescent, fm.descent); } else { fmTop = fmBottom = fmAscent = fmDescent = 0; } @@ -1069,7 +1061,7 @@ public class StaticLayout extends Layout { && mLineCount < mMaximumVisibleLineCount) { final MeasuredParagraph measuredPara = MeasuredParagraph.buildForBidi(source, bufEnd, bufEnd, textDir, null); - if (ClientFlags.fixLineHeightForLocale()) { + if (defaultAscent != 0 && defaultDescent != 0) { fm.top = defaultTop; fm.ascent = defaultAscent; fm.descent = defaultDescent; diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index fbadef3d19ef..0006139d2cb0 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -86,6 +86,7 @@ import java.util.function.Consumer; * that are currently attached and whether mirroring has been enabled. * </p> */ +@android.ravenwood.annotation.RavenwoodKeepPartialClass public final class Display { private static final String TAG = "Display"; private static final boolean DEBUG = false; @@ -1998,6 +1999,7 @@ public final class Display { * display power state. In SUSPEND states, updates are absolutely forbidden. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isSuspendedState(int state) { return state == STATE_OFF || state == STATE_DOZE_SUSPEND || state == STATE_ON_SUSPEND; } @@ -2007,6 +2009,7 @@ public final class Display { * specified display power state. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isDozeState(int state) { return state == STATE_DOZE || state == STATE_DOZE_SUSPEND; } @@ -2016,6 +2019,7 @@ public final class Display { * or {@link #STATE_VR}. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isActiveState(int state) { return state == STATE_ON || state == STATE_VR; } @@ -2024,6 +2028,7 @@ public final class Display { * Returns true if the display is in an off state such as {@link #STATE_OFF}. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isOffState(int state) { return state == STATE_OFF; } @@ -2033,6 +2038,7 @@ public final class Display { * or {@link #STATE_VR} or {@link #STATE_ON_SUSPEND}. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isOnState(int state) { return state == STATE_ON || state == STATE_VR || state == STATE_ON_SUSPEND; } diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java index 0ce1d4711c6d..eb289204e481 100644 --- a/core/java/android/view/HandwritingInitiator.java +++ b/core/java/android/view/HandwritingInitiator.java @@ -23,8 +23,10 @@ import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; +import android.view.inputmethod.Flags; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; +import android.widget.Editor; import android.widget.TextView; import com.android.internal.annotations.VisibleForTesting; @@ -80,6 +82,16 @@ public class HandwritingInitiator { * connections and only set mConnectedView to null when mConnectionCount is zero. */ private int mConnectionCount = 0; + + /** + * The reference to the View that currently has focus. + * This replaces mConnecteView when {@code Flags#intitiationWithoutInputConnection()} is + * enabled. + */ + @Nullable + @VisibleForTesting + public WeakReference<View> mFocusedView = null; + private final InputMethodManager mImm; private final int[] mTempLocation = new int[2]; @@ -112,9 +124,15 @@ public class HandwritingInitiator { * * If the stylus is hovering on an unconnected editor that supports handwriting, we always show * the hover icon. + * TODO(b/308827131): Rename to FocusedView after Flag is flipped. */ private boolean mShowHoverIconForConnectedView = true; + /** When flag is enabled, touched editors don't wait for InputConnection for initiation. + * However, delegation still waits for InputConnection. + */ + private final boolean mInitiateWithoutConnection = Flags.initiationWithoutInputConnection(); + @VisibleForTesting public HandwritingInitiator(@NonNull ViewConfiguration viewConfiguration, @NonNull InputMethodManager inputMethodManager) { @@ -201,8 +219,8 @@ public class HandwritingInitiator { View candidateView = findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY, /* isHover */ false); if (candidateView != null) { - if (candidateView == getConnectedView()) { - if (!candidateView.hasFocus()) { + if (candidateView == getConnectedOrFocusedView()) { + if (!mInitiateWithoutConnection && !candidateView.hasFocus()) { requestFocusWithoutReveal(candidateView); } startHandwriting(candidateView); @@ -217,8 +235,17 @@ public class HandwritingInitiator { candidateView.getHandwritingDelegatorCallback().run(); mState.mHasPreparedHandwritingDelegation = true; } else { - mState.mPendingConnectedView = new WeakReference<>(candidateView); - requestFocusWithoutReveal(candidateView); + if (!mInitiateWithoutConnection) { + mState.mPendingConnectedView = new WeakReference<>(candidateView); + } + if (!candidateView.hasFocus()) { + requestFocusWithoutReveal(candidateView); + } + if (mInitiateWithoutConnection + && updateFocusedView(candidateView, + /* fromTouchEvent */ true)) { + startHandwriting(candidateView); + } } } } @@ -244,11 +271,7 @@ public class HandwritingInitiator { */ public void onDelegateViewFocused(@NonNull View view) { if (view == getConnectedView()) { - if (tryAcceptStylusHandwritingDelegation(view)) { - // A handwriting delegate view is accepted and handwriting starts; hide the - // hover icon. - mShowHoverIconForConnectedView = false; - } + tryAcceptStylusHandwritingDelegation(view); } } @@ -260,6 +283,10 @@ public class HandwritingInitiator { * @see #onInputConnectionClosed(View) */ public void onInputConnectionCreated(@NonNull View view) { + if (mInitiateWithoutConnection && !view.isHandwritingDelegate()) { + // When flag is enabled, only delegation continues to wait for InputConnection. + return; + } if (!view.isAutoHandwritingEnabled()) { clearConnectedView(); return; @@ -274,12 +301,15 @@ public class HandwritingInitiator { // A new view just gain focus. By default, we should show hover icon for it. mShowHoverIconForConnectedView = true; if (view.isHandwritingDelegate() && tryAcceptStylusHandwritingDelegation(view)) { - // A handwriting delegate view is accepted and handwriting starts; hide the - // hover icon. + // tryAcceptStylusHandwritingDelegation should set boolean below, however, we + // cannot mock IMM to return true for acceptStylusDelegation(). + // TODO(b/324670412): we should move any dependent tests to integration and remove + // the assignment below. mShowHoverIconForConnectedView = false; return; } - if (mState != null && mState.mPendingConnectedView != null + if (!mInitiateWithoutConnection && mState != null + && mState.mPendingConnectedView != null && mState.mPendingConnectedView.get() == view) { startHandwriting(view); } @@ -293,6 +323,9 @@ public class HandwritingInitiator { * @param view the view that closed the InputConnection. */ public void onInputConnectionClosed(@NonNull View view) { + if (mInitiateWithoutConnection && !view.isHandwritingDelegate()) { + return; + } final View connectedView = getConnectedView(); if (connectedView == null) return; if (connectedView == view) { @@ -306,6 +339,48 @@ public class HandwritingInitiator { } } + @Nullable + private View getFocusedView() { + if (mFocusedView == null) return null; + return mFocusedView.get(); + } + + /** + * Clear the tracked focused view tracked for handwriting initiation. + * @param view the focused view. + */ + public void clearFocusedView(View view) { + if (view == null || mFocusedView == null) { + return; + } + if (mFocusedView.get() == view) { + mFocusedView = null; + } + } + + /** + * Called when new {@link Editor} is focused. + * @return {@code true} if handwriting can initiate for given view. + */ + @VisibleForTesting + public boolean updateFocusedView(@NonNull View view, boolean fromTouchEvent) { + if (!view.shouldInitiateHandwriting()) { + mFocusedView = null; + return false; + } + + final View focusedView = getFocusedView(); + if (focusedView != view) { + mFocusedView = new WeakReference<>(view); + // A new view just gain focus. By default, we should show hover icon for it. + mShowHoverIconForConnectedView = true; + } + if (!fromTouchEvent) { + tryAcceptStylusHandwritingDelegation(view); + } + return true; + } + /** Starts a stylus handwriting session for the view. */ @VisibleForTesting public void startHandwriting(@NonNull View view) { @@ -324,6 +399,9 @@ public class HandwritingInitiator { */ @VisibleForTesting public boolean tryAcceptStylusHandwritingDelegation(@NonNull View view) { + if (!view.isHandwritingDelegate() || (mState != null && mState.mHasInitiatedHandwriting)) { + return false; + } String delegatorPackageName = view.getAllowedHandwritingDelegatorPackageName(); if (delegatorPackageName == null) { @@ -337,6 +415,9 @@ public class HandwritingInitiator { if (view instanceof TextView) { ((TextView) view).hideHint(); } + // A handwriting delegate view is accepted and handwriting starts; hide the + // hover icon. + mShowHoverIconForConnectedView = false; return true; } return false; @@ -377,16 +458,25 @@ public class HandwritingInitiator { return PointerIcon.getSystemIcon(context, PointerIcon.TYPE_HANDWRITING); } - if (hoverView != getConnectedView()) { + if (hoverView != getConnectedOrFocusedView()) { // The stylus is hovering on another view that supports handwriting. We should show - // hover icon. Also reset the mShowHoverIconForConnectedView so that hover - // icon is displayed again next time when the stylus hovers on connected view. + // hover icon. Also reset the mShowHoverIconForFocusedView so that hover + // icon is displayed again next time when the stylus hovers on focused view. mShowHoverIconForConnectedView = true; return PointerIcon.getSystemIcon(context, PointerIcon.TYPE_HANDWRITING); } return null; } + // TODO(b/308827131): Remove once Flag is flipped. + private View getConnectedOrFocusedView() { + if (mInitiateWithoutConnection) { + return mFocusedView == null ? null : mFocusedView.get(); + } else { + return mConnectedView == null ? null : mConnectedView.get(); + } + } + private View getCachedHoverTarget() { if (mCachedHoverTarget == null) { return null; @@ -458,20 +548,21 @@ public class HandwritingInitiator { */ @Nullable private View findBestCandidateView(float x, float y, boolean isHover) { + // TODO(b/308827131): Rename to FocusedView after Flag is flipped. // If the connectedView is not null and do not set any handwriting area, it will check // whether the connectedView's boundary contains the initial stylus position. If true, // directly return the connectedView. - final View connectedView = getConnectedView(); - if (connectedView != null) { + final View connectedOrFocusedView = getConnectedOrFocusedView(); + if (connectedOrFocusedView != null) { Rect handwritingArea = mTempRect; - if (getViewHandwritingArea(connectedView, handwritingArea) - && isInHandwritingArea(handwritingArea, x, y, connectedView, isHover) - && shouldTriggerStylusHandwritingForView(connectedView)) { + if (getViewHandwritingArea(connectedOrFocusedView, handwritingArea) + && isInHandwritingArea(handwritingArea, x, y, connectedOrFocusedView, isHover) + && shouldTriggerStylusHandwritingForView(connectedOrFocusedView)) { if (!isHover && mState != null) { mState.mStylusDownWithinEditorBounds = contains(handwritingArea, x, y, 0f, 0f, 0f, 0f); } - return connectedView; + return connectedOrFocusedView; } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index a9f189700789..c22986b3473b 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -30,6 +30,7 @@ import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ER import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_UNKNOWN; import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH; import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH_ERROR_CODE; +import static android.view.flags.Flags.FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API; import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY; import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API; import static android.view.flags.Flags.enableUseMeasureCacheDuringForceLayout; @@ -1946,6 +1947,41 @@ public class View implements Drawable.Callback, KeyEvent.Callback, static final int TOOLTIP = 0x40000000; /** @hide */ + @IntDef(prefix = { "CONTENT_SENSITIVITY_" }, value = { + CONTENT_SENSITIVITY_AUTO, + CONTENT_SENSITIVITY_SENSITIVE, + CONTENT_SENSITIVITY_NOT_SENSITIVE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ContentSensitivity {} + + /** + * Automatically determine whether a view displays sensitive content. For example, available + * autofill hints (or some other signal) can be used to determine if this view + * displays sensitive content. + * + * @see #getContentSensitivity() + */ + @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API) + public static final int CONTENT_SENSITIVITY_AUTO = 0x0; + + /** + * The view displays sensitive content. + * + * @see #getContentSensitivity() + */ + @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API) + public static final int CONTENT_SENSITIVITY_SENSITIVE = 0x1; + + /** + * The view doesn't display sensitive content. + * + * @see #getContentSensitivity() + */ + @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API) + public static final int CONTENT_SENSITIVITY_NOT_SENSITIVE = 0x2; + + /** @hide */ @IntDef(flag = true, prefix = { "FOCUSABLES_" }, value = { FOCUSABLES_ALL, FOCUSABLES_TOUCH_MODE @@ -3646,6 +3682,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * 1 PFLAG4_ROTARY_HAPTICS_ENABLED * 1 PFLAG4_ROTARY_HAPTICS_SCROLL_SINCE_LAST_ROTARY_INPUT * 1 PFLAG4_ROTARY_HAPTICS_WAITING_FOR_SCROLL_EVENT + * 11 PFLAG4_CONTENT_SENSITIVITY_MASK * |-------|-------|-------|-------| */ @@ -3762,6 +3799,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private static final int PFLAG4_ROTARY_HAPTICS_WAITING_FOR_SCROLL_EVENT = 0x800000; + private static final int PFLAG4_CONTENT_SENSITIVITY_SHIFT = 24; + + /** + * Mask for obtaining the bits which specify how to determine whether a view + * displays sensitive content or not. + */ + private static final int PFLAG4_CONTENT_SENSITIVITY_MASK = + (CONTENT_SENSITIVITY_AUTO | CONTENT_SENSITIVITY_SENSITIVE + | CONTENT_SENSITIVITY_NOT_SENSITIVE) << PFLAG4_CONTENT_SENSITIVITY_SHIFT; /* End of masks for mPrivateFlags4 */ /** @hide */ @@ -10150,6 +10196,54 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Sets content sensitivity mode to determine whether this view displays sensitive content. + * + * @param mode {@link #CONTENT_SENSITIVITY_AUTO}, {@link #CONTENT_SENSITIVITY_NOT_SENSITIVE} + * or {@link #CONTENT_SENSITIVITY_SENSITIVE} + */ + @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API) + public final void setContentSensitivity(@ContentSensitivity int mode) { + mPrivateFlags4 &= ~PFLAG4_CONTENT_SENSITIVITY_MASK; + mPrivateFlags4 |= ((mode << PFLAG4_CONTENT_SENSITIVITY_SHIFT) + & PFLAG4_CONTENT_SENSITIVITY_MASK); + } + + /** + * Gets content sensitivity mode to determine whether this view displays sensitive content. + * + * <p>See {@link #setContentSensitivity(int)} and + * {@link #isContentSensitive()} for more info about this mode. + * + * @return {@link #CONTENT_SENSITIVITY_AUTO} by default, or value passed to + * {@link #setContentSensitivity(int)}. + */ + @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API) + public @ContentSensitivity + final int getContentSensitivity() { + return (mPrivateFlags4 & PFLAG4_CONTENT_SENSITIVITY_MASK) + >> PFLAG4_CONTENT_SENSITIVITY_SHIFT; + } + + /** + * Returns whether this view displays sensitive content, based + * on the value explicitly set by {@link #setContentSensitivity(int)}. + * + * @return whether the view displays sensitive content. + * + * @see #setContentSensitivity(int) + * @see #CONTENT_SENSITIVITY_AUTO + * @see #CONTENT_SENSITIVITY_SENSITIVE + * @see #CONTENT_SENSITIVITY_NOT_SENSITIVE + */ + @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API) + public final boolean isContentSensitive() { + if (getContentSensitivity() == CONTENT_SENSITIVITY_SENSITIVE) { + return true; + } + return false; + } + + /** * Gets the mode for determining whether this view is important for content capture. * * <p>See {@link #setImportantForContentCapture(int)} and diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 3d70c5ba0715..3b07f27dc95a 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -18,6 +18,7 @@ package android.view.inputmethod; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.inputmethod.Flags.FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR; +import static android.view.inputmethod.Flags.initiationWithoutInputConnection; import static android.view.inputmethod.InputConnection.CURSOR_UPDATE_IMMEDIATE; import static android.view.inputmethod.InputConnection.CURSOR_UPDATE_MONITOR; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.DISPLAY_ID; @@ -1950,6 +1951,10 @@ public final class InputMethodManager { if (mServedView != null) { clearedView = mServedView; mServedView = null; + if (initiationWithoutInputConnection() && clearedView.getViewRootImpl() != null) { + clearedView.getViewRootImpl().getHandwritingInitiator() + .clearFocusedView(clearedView); + } } if (clearedView != null) { if (DEBUG) { @@ -2932,6 +2937,10 @@ public final class InputMethodManager { switch (res.result) { case InputBindResult.ResultCode.ERROR_NOT_IME_TARGET_WINDOW: mRestartOnNextWindowFocus = true; + if (initiationWithoutInputConnection()) { + mServedView.getViewRootImpl().getHandwritingInitiator().clearFocusedView( + mServedView); + } mServedView = null; break; } @@ -3094,6 +3103,11 @@ public final class InputMethodManager { return false; } mServedView = mNextServedView; + if (initiationWithoutInputConnection() && mServedView.onCheckIsTextEditor() + && mServedView.isHandwritingDelegate()) { + mServedView.getViewRootImpl().getHandwritingInitiator().onDelegateViewFocused( + mServedView); + } if (mServedInputConnection != null) { mServedInputConnection.finishComposingTextFromImm(); } diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig index 7f1cc8e46d3c..8b91bcb5095e 100644 --- a/core/java/android/view/inputmethod/flags.aconfig +++ b/core/java/android/view/inputmethod/flags.aconfig @@ -62,3 +62,11 @@ flag { bug: "311791923" is_fixed_read_only: true } + +flag { + name: "initiation_without_input_connection" + namespace: "input_method" + description: "Feature flag for initiating handwriting without InputConnection" + bug: "308827131" + is_fixed_read_only: true +} diff --git a/core/java/android/widget/EditText.java b/core/java/android/widget/EditText.java index aa2474d7903e..3e0161a9b791 100644 --- a/core/java/android/widget/EditText.java +++ b/core/java/android/widget/EditText.java @@ -16,9 +16,13 @@ package android.widget; +import android.app.compat.CompatChanges; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; +import android.os.Build; import android.text.Editable; import android.text.Selection; import android.text.Spannable; @@ -29,6 +33,8 @@ import android.text.style.SpanUtils; import android.util.AttributeSet; import android.view.KeyEvent; +import com.android.internal.R; + /* * This is supposed to be a *very* thin veneer over TextView. * Do not make any changes here that do anything that a TextView @@ -85,6 +91,11 @@ public class EditText extends TextView { private static final int ID_ITALIC = android.R.id.italic; private static final int ID_UNDERLINE = android.R.id.underline; + /** @hide */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) + public static final long LINE_HEIGHT_FOR_LOCALE = 303326708L; + public EditText(Context context) { this(context, null); } @@ -104,15 +115,39 @@ public class EditText extends TextView { final TypedArray a = theme.obtainStyledAttributes(attrs, com.android.internal.R.styleable.EditText, defStyleAttr, defStyleRes); - final int n = a.getIndexCount(); - for (int i = 0; i < n; ++i) { - int attr = a.getIndex(i); - switch (attr) { - case com.android.internal.R.styleable.EditText_enableTextStylingShortcuts: - mStyleShortcutsEnabled = a.getBoolean(attr, false); - break; + try { + final int n = a.getIndexCount(); + for (int i = 0; i < n; ++i) { + int attr = a.getIndex(i); + switch (attr) { + case com.android.internal.R.styleable.EditText_enableTextStylingShortcuts: + mStyleShortcutsEnabled = a.getBoolean(attr, false); + break; + } } + } finally { + a.recycle(); + } + + boolean hasUseLocalePreferredLineHeightForMinimumInt = false; + boolean useLocalePreferredLineHeightForMinimumInt = false; + TypedArray tvArray = theme.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes); + try { + hasUseLocalePreferredLineHeightForMinimumInt = + tvArray.hasValue(R.styleable.TextView_useLocalePreferredLineHeightForMinimum); + if (hasUseLocalePreferredLineHeightForMinimumInt) { + useLocalePreferredLineHeightForMinimumInt = tvArray.getBoolean( + R.styleable.TextView_useLocalePreferredLineHeightForMinimum, false); + } + } finally { + tvArray.recycle(); + } + if (!hasUseLocalePreferredLineHeightForMinimumInt) { + useLocalePreferredLineHeightForMinimumInt = + CompatChanges.isChangeEnabled(LINE_HEIGHT_FOR_LOCALE); } + setLocalePreferredLineHeightForMinimumUsed(useLocalePreferredLineHeightForMinimumInt); } @Override diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 9a4106d930b4..57e4e6a2fa5b 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -867,6 +867,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private boolean mUseBoundsForWidth; @Nullable private Paint.FontMetrics mMinimumFontMetrics; + @Nullable private Paint.FontMetrics mLocalePreferredFontMetrics; + private boolean mUseLocalePreferredLineHeightForMinimum; @ViewDebug.ExportedProperty(category = "text") @UnsupportedAppUsage @@ -1617,6 +1619,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case com.android.internal.R.styleable.TextView_useBoundsForWidth: mUseBoundsForWidth = a.getBoolean(attr, false); hasUseBoundForWidthValue = true; + break; + case com.android.internal.R.styleable + .TextView_useLocalePreferredLineHeightForMinimum: + mUseLocalePreferredLineHeightForMinimum = a.getBoolean(attr, false); + break; } } @@ -4992,6 +4999,41 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Returns true if the locale preferred line height is used for the minimum line height. + * + * @return true if using locale preferred line height for the minimum line height. Otherwise + * false. + * + * @see #setLocalePreferredLineHeightForMinimumUsed(boolean) + * @see #setMinimumFontMetrics(Paint.FontMetrics) + * @see #getMinimumFontMetrics() + */ + @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE) + public boolean isLocalePreferredLineHeightForMinimumUsed() { + return mUseLocalePreferredLineHeightForMinimum; + } + + /** + * Set true if the locale preferred line height is used for the minimum line height. + * + * By setting this flag to true is equivalenet to call + * {@link #setMinimumFontMetrics(Paint.FontMetrics)} with the one obtained by + * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)}. + * + * If custom minimum line height was specified by + * {@link #setMinimumFontMetrics(Paint.FontMetrics)}, this flag will be ignored. + * + * @param flag true for using locale preferred line height for the minimum line height. + * @see #isLocalePreferredLineHeightForMinimumUsed() + * @see #setMinimumFontMetrics(Paint.FontMetrics) + * @see #getMinimumFontMetrics() + */ + @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE) + public void setLocalePreferredLineHeightForMinimumUsed(boolean flag) { + mUseLocalePreferredLineHeightForMinimum = flag; + } + + /** * @return whether fallback line spacing is enabled, {@code true} by default * * @see #setFallbackLineSpacing(boolean) @@ -10728,6 +10770,21 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return alignment; } + private Paint.FontMetrics getResolvedMinimumFontMetrics() { + if (mMinimumFontMetrics != null) { + return mMinimumFontMetrics; + } + if (!mUseLocalePreferredLineHeightForMinimum) { + return null; + } + + if (mLocalePreferredFontMetrics == null) { + mLocalePreferredFontMetrics = new Paint.FontMetrics(); + } + mTextPaint.getFontMetricsForLocale(mLocalePreferredFontMetrics); + return mLocalePreferredFontMetrics; + } + /** * The width passed in is now the desired layout width, * not the full view width with padding. @@ -10792,7 +10849,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (hintBoring == UNKNOWN_BORING) { hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, isFallbackLineSpacingForBoringLayout(), - mMinimumFontMetrics, mHintBoring); + getResolvedMinimumFontMetrics(), mHintBoring); + if (hintBoring != null) { mHintBoring = hintBoring; } @@ -10842,7 +10900,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener .setLineBreakConfig(LineBreakConfig.getLineBreakConfig( mLineBreakStyle, mLineBreakWordStyle)) .setUseBoundsForWidth(mUseBoundsForWidth) - .setMinimumFontMetrics(mMinimumFontMetrics); + .setMinimumFontMetrics(getResolvedMinimumFontMetrics()); + if (shouldEllipsize) { builder.setEllipsize(mEllipsize) .setEllipsizedWidth(ellipsisWidth); @@ -10907,12 +10966,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener .setUseBoundsForWidth(mUseBoundsForWidth) .setEllipsize(getKeyListener() == null ? effectiveEllipsize : null) .setEllipsizedWidth(ellipsisWidth) - .setMinimumFontMetrics(mMinimumFontMetrics); + .setMinimumFontMetrics(getResolvedMinimumFontMetrics()); result = builder.build(); } else { if (boring == UNKNOWN_BORING) { boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, - isFallbackLineSpacingForBoringLayout(), mMinimumFontMetrics, mBoring); + isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(), + mBoring); if (boring != null) { mBoring = boring; } @@ -10926,7 +10986,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener wantWidth, alignment, mSpacingMult, mSpacingAdd, boring, mIncludePad, null, wantWidth, isFallbackLineSpacingForBoringLayout(), - mUseBoundsForWidth, mMinimumFontMetrics); + mUseBoundsForWidth, getResolvedMinimumFontMetrics()); } else { result = new BoringLayout( mTransformed, @@ -10941,7 +11001,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener null, boring, mUseBoundsForWidth, - mMinimumFontMetrics); + getResolvedMinimumFontMetrics()); } if (useSaved) { @@ -10953,7 +11013,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener wantWidth, alignment, mSpacingMult, mSpacingAdd, boring, mIncludePad, effectiveEllipsize, ellipsisWidth, isFallbackLineSpacingForBoringLayout(), - mUseBoundsForWidth, mMinimumFontMetrics); + mUseBoundsForWidth, getResolvedMinimumFontMetrics()); } else { result = new BoringLayout( mTransformed, @@ -10968,7 +11028,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener effectiveEllipsize, boring, mUseBoundsForWidth, - mMinimumFontMetrics); + getResolvedMinimumFontMetrics()); } } } @@ -10988,7 +11048,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener .setLineBreakConfig(LineBreakConfig.getLineBreakConfig( mLineBreakStyle, mLineBreakWordStyle)) .setUseBoundsForWidth(mUseBoundsForWidth) - .setMinimumFontMetrics(mMinimumFontMetrics); + .setMinimumFontMetrics(getResolvedMinimumFontMetrics()); if (shouldEllipsize) { builder.setEllipsize(effectiveEllipsize) .setEllipsizedWidth(ellipsisWidth); @@ -11116,7 +11176,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (des < 0) { boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, - isFallbackLineSpacingForBoringLayout(), mMinimumFontMetrics, mBoring); + isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(), + mBoring); if (boring != null) { mBoring = boring; } @@ -11156,7 +11217,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (hintDes < 0) { hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, - isFallbackLineSpacingForBoringLayout(), mMinimumFontMetrics, + isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(), mHintBoring); if (hintBoring != null) { mHintBoring = hintBoring; @@ -11370,7 +11431,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener .setLineBreakConfig(LineBreakConfig.getLineBreakConfig( mLineBreakStyle, mLineBreakWordStyle)) .setUseBoundsForWidth(mUseBoundsForWidth) - .setMinimumFontMetrics(mMinimumFontMetrics); + .setMinimumFontMetrics(getResolvedMinimumFontMetrics()); final StaticLayout layout = layoutBuilder.build(); diff --git a/core/java/android/widget/flags/notification_widget_flags.aconfig b/core/java/android/widget/flags/notification_widget_flags.aconfig index 9f0b7c3dfb02..bfe3d052479b 100644 --- a/core/java/android/widget/flags/notification_widget_flags.aconfig +++ b/core/java/android/widget/flags/notification_widget_flags.aconfig @@ -5,4 +5,14 @@ flag { namespace: "systemui" description: "Enables notification specific LinearLayout optimization" bug: "316110233" +} + +flag { + name: "call_style_set_data_async" + namespace: "systemui" + description: "Offloads caller icon drawable loading to the background thread" + bug: "293961072" + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index feae173f3e61..15b9b788bca9 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -159,8 +159,11 @@ public final class TransitionInfo implements Parcelable { */ public static final int FLAG_SYNC = 1 << 21; + /** This change represents its start configuration for the duration of the animation. */ + public static final int FLAG_CONFIG_AT_END = 1 << 22; + /** The first unused bit. This can be used by remotes to attach custom flags to this change. */ - public static final int FLAG_FIRST_CUSTOM = 1 << 22; + public static final int FLAG_FIRST_CUSTOM = 1 << 23; /** The change belongs to a window that won't contain activities. */ public static final int FLAGS_IS_NON_APP_WINDOW = @@ -193,6 +196,7 @@ public final class TransitionInfo implements Parcelable { FLAG_TASK_LAUNCHING_BEHIND, FLAG_MOVED_TO_TOP, FLAG_SYNC, + FLAG_CONFIG_AT_END, FLAG_FIRST_CUSTOM }) public @interface ChangeFlags {} diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index d9b5b2d725e2..efc71d7b43de 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -914,6 +914,23 @@ public final class WindowContainerTransaction implements Parcelable { } /** + * Defers client-facing configuration changes for activities in `container` until the end of + * the transition animation. The configuration will still be applied to the WMCore hierarchy + * at the normal time (beginning); so, special consideration must be made for this in the + * animation. + * + * @param container WindowContainerToken who's children should defer config notification. + * @hide + */ + @NonNull + public WindowContainerTransaction deferConfigToTransitionEnd( + @NonNull WindowContainerToken container) { + final Change change = getOrCreateChange(container.asBinder()); + change.mConfigAtTransitionEnd = true; + return this; + } + + /** * Merges another WCT into this one. * @param transfer When true, this will transfer everything from other potentially leaving * other in an unusable state. When false, other is left alone, but @@ -1050,6 +1067,7 @@ public final class WindowContainerTransaction implements Parcelable { private Rect mBoundsChangeSurfaceBounds = null; @Nullable private Rect mRelativeBounds = null; + private boolean mConfigAtTransitionEnd = false; private int mActivityWindowingMode = -1; private int mWindowingMode = -1; @@ -1082,6 +1100,7 @@ public final class WindowContainerTransaction implements Parcelable { mRelativeBounds = new Rect(); mRelativeBounds.readFromParcel(in); } + mConfigAtTransitionEnd = in.readBoolean(); mWindowingMode = in.readInt(); mActivityWindowingMode = in.readInt(); @@ -1134,6 +1153,8 @@ public final class WindowContainerTransaction implements Parcelable { ? other.mRelativeBounds : new Rect(other.mRelativeBounds); } + mConfigAtTransitionEnd = mConfigAtTransitionEnd + || other.mConfigAtTransitionEnd; } public int getWindowingMode() { @@ -1191,6 +1212,11 @@ public final class WindowContainerTransaction implements Parcelable { return mDragResizing; } + /** Gets whether the config should be sent to the client at the end of the transition. */ + public boolean getConfigAtTransitionEnd() { + return mConfigAtTransitionEnd; + } + public int getChangeMask() { return mChangeMask; } @@ -1269,6 +1295,9 @@ public final class WindowContainerTransaction implements Parcelable { if ((mChangeMask & CHANGE_RELATIVE_BOUNDS) != 0) { sb.append("relativeBounds:").append(mRelativeBounds).append(","); } + if (mConfigAtTransitionEnd) { + sb.append("configAtTransitionEnd").append(","); + } sb.append("}"); return sb.toString(); } @@ -1297,6 +1326,7 @@ public final class WindowContainerTransaction implements Parcelable { if (mRelativeBounds != null) { mRelativeBounds.writeToParcel(dest, flags); } + dest.writeBoolean(mConfigAtTransitionEnd); dest.writeInt(mWindowingMode); dest.writeInt(mActivityWindowingMode); diff --git a/core/java/com/android/internal/os/BackgroundThread.java b/core/java/com/android/internal/os/BackgroundThread.java index 72da819bb736..b75daeda480c 100644 --- a/core/java/com/android/internal/os/BackgroundThread.java +++ b/core/java/com/android/internal/os/BackgroundThread.java @@ -27,6 +27,7 @@ import java.util.concurrent.Executor; /** * Shared singleton background thread for each process. */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public final class BackgroundThread extends HandlerThread { private static final long SLOW_DISPATCH_THRESHOLD_MS = 10_000; private static final long SLOW_DELIVERY_THRESHOLD_MS = 30_000; diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java index aa60cc9e672c..0b7593a1a5a2 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistory.java +++ b/core/java/com/android/internal/os/BatteryStatsHistory.java @@ -297,9 +297,20 @@ public class BatteryStatsHistory { } } + public static class EventLogger { + /** + * Records a statsd event when the batterystats config file is written to disk. + */ + public void writeCommitSysConfigFile(long startTimeMs) { + com.android.internal.logging.EventLogTags.writeCommitSysConfigFile( + "batterystats", SystemClock.uptimeMillis() - startTimeMs); + } + } + private TraceDelegate mTracer; private int mTraceLastState = 0; private int mTraceLastState2 = 0; + private final EventLogger mEventLogger; /** * Constructor @@ -311,8 +322,16 @@ public class BatteryStatsHistory { public BatteryStatsHistory(File systemDir, int maxHistoryFiles, int maxHistoryBufferSize, HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock, MonotonicClock monotonicClock) { + this(systemDir, maxHistoryFiles, maxHistoryBufferSize, + stepDetailsCalculator, clock, monotonicClock, new TraceDelegate(), + new EventLogger()); + } + + public BatteryStatsHistory(File systemDir, int maxHistoryFiles, int maxHistoryBufferSize, + HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock, + MonotonicClock monotonicClock, TraceDelegate tracer, EventLogger eventLogger) { this(Parcel.obtain(), systemDir, maxHistoryFiles, maxHistoryBufferSize, - stepDetailsCalculator, clock, monotonicClock, new TraceDelegate()); + stepDetailsCalculator, clock, monotonicClock, tracer, eventLogger); initHistoryBuffer(); } @@ -320,15 +339,15 @@ public class BatteryStatsHistory { public BatteryStatsHistory(Parcel historyBuffer, File systemDir, int maxHistoryFiles, int maxHistoryBufferSize, HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock, - MonotonicClock monotonicClock, TraceDelegate tracer) { + MonotonicClock monotonicClock, TraceDelegate tracer, EventLogger eventLogger) { this(historyBuffer, systemDir, maxHistoryFiles, maxHistoryBufferSize, stepDetailsCalculator, - clock, monotonicClock, tracer, null); + clock, monotonicClock, tracer, eventLogger, null); } private BatteryStatsHistory(Parcel historyBuffer, File systemDir, int maxHistoryFiles, int maxHistoryBufferSize, HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock, - MonotonicClock monotonicClock, TraceDelegate tracer, + MonotonicClock monotonicClock, TraceDelegate tracer, EventLogger eventLogger, BatteryStatsHistory writableHistory) { mHistoryBuffer = historyBuffer; mSystemDir = systemDir; @@ -338,6 +357,7 @@ public class BatteryStatsHistory { mTracer = tracer; mClock = clock; mMonotonicClock = monotonicClock; + mEventLogger = eventLogger; mWritableHistory = writableHistory; if (mWritableHistory != null) { mMutable = false; @@ -394,19 +414,21 @@ public class BatteryStatsHistory { HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock, MonotonicClock monotonicClock) { this(maxHistoryFiles, maxHistoryBufferSize, stepDetailsCalculator, clock, monotonicClock, - new TraceDelegate()); + new TraceDelegate(), new EventLogger()); } @VisibleForTesting public BatteryStatsHistory(int maxHistoryFiles, int maxHistoryBufferSize, HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock, - MonotonicClock monotonicClock, TraceDelegate traceDelegate) { + MonotonicClock monotonicClock, TraceDelegate traceDelegate, + EventLogger eventLogger) { mMaxHistoryFiles = maxHistoryFiles; mMaxHistoryBufferSize = maxHistoryBufferSize; mStepDetailsCalculator = stepDetailsCalculator; mTracer = traceDelegate; mClock = clock; mMonotonicClock = monotonicClock; + mEventLogger = eventLogger; mHistoryBuffer = Parcel.obtain(); mSystemDir = null; @@ -425,6 +447,7 @@ public class BatteryStatsHistory { mSystemDir = null; mHistoryDir = null; mStepDetailsCalculator = null; + mEventLogger = new EventLogger(); mWritableHistory = null; mMutable = false; @@ -482,7 +505,7 @@ public class BatteryStatsHistory { historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize()); return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null, null, - null, this); + null, mEventLogger, this); } } @@ -2154,8 +2177,7 @@ public class BatteryStatsHistory { + " duration ms:" + (SystemClock.uptimeMillis() - startTimeMs) + " bytes:" + p.dataSize()); } - com.android.internal.logging.EventLogTags.writeCommitSysConfigFile( - "batterystats", SystemClock.uptimeMillis() - startTimeMs); + mEventLogger.writeCommitSysConfigFile(startTimeMs); } catch (IOException e) { Slog.w(TAG, "Error writing battery statistics", e); file.failWrite(fos); @@ -2164,6 +2186,7 @@ public class BatteryStatsHistory { } } + /** * Returns the total number of history tags in the tag pool. */ diff --git a/core/java/com/android/internal/os/LongMultiStateCounter.java b/core/java/com/android/internal/os/LongMultiStateCounter.java index 33a9d547be78..064609f9dfe4 100644 --- a/core/java/com/android/internal/os/LongMultiStateCounter.java +++ b/core/java/com/android/internal/os/LongMultiStateCounter.java @@ -55,11 +55,12 @@ import libcore.util.NativeAllocationRegistry; * * @hide */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass +@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass( + "com.android.hoststubgen.nativesubstitution.LongMultiStateCounter_host") public final class LongMultiStateCounter implements Parcelable { - private static final NativeAllocationRegistry sRegistry = - NativeAllocationRegistry.createMalloced( - LongMultiStateCounter.class.getClassLoader(), native_getReleaseFunc()); + private static NativeAllocationRegistry sRegistry; private final int mStateCount; @@ -71,16 +72,33 @@ public final class LongMultiStateCounter implements Parcelable { Preconditions.checkArgumentPositive(stateCount, "stateCount must be greater than 0"); mStateCount = stateCount; mNativeObject = native_init(stateCount); - sRegistry.registerNativeAllocation(this, mNativeObject); + registerNativeAllocation(); } private LongMultiStateCounter(Parcel in) { mNativeObject = native_initFromParcel(in); - sRegistry.registerNativeAllocation(this, mNativeObject); + registerNativeAllocation(); mStateCount = native_getStateCount(mNativeObject); } + @android.ravenwood.annotation.RavenwoodReplace + private void registerNativeAllocation() { + if (sRegistry == null) { + synchronized (LongMultiStateCounter.class) { + if (sRegistry == null) { + sRegistry = NativeAllocationRegistry.createMalloced( + LongMultiStateCounter.class.getClassLoader(), native_getReleaseFunc()); + } + } + } + sRegistry.registerNativeAllocation(this, mNativeObject); + } + + private void registerNativeAllocation$ravenwood() { + // No-op under ravenwood + } + public int getStateCount() { return mStateCount; } @@ -221,10 +239,10 @@ public final class LongMultiStateCounter implements Parcelable { private static native long native_getCount(long nativeObject, int state); @FastNative - private native String native_toString(long nativeObject); + private static native String native_toString(long nativeObject); @FastNative - private native void native_writeToParcel(long nativeObject, Parcel dest, int flags); + private static native void native_writeToParcel(long nativeObject, Parcel dest, int flags); @FastNative private static native long native_initFromParcel(Parcel parcel); diff --git a/core/java/com/android/internal/power/EnergyConsumerStats.java b/core/java/com/android/internal/power/EnergyConsumerStats.java index e2098dd11531..764908d65b17 100644 --- a/core/java/com/android/internal/power/EnergyConsumerStats.java +++ b/core/java/com/android/internal/power/EnergyConsumerStats.java @@ -44,6 +44,7 @@ import java.util.Arrays; * This class doesn't use a TimeBase, and instead requires manual decisions about when to * accumulate since it is trivial. However, in the future, a TimeBase could be used instead. */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class EnergyConsumerStats { private static final String TAG = "MeasuredEnergyStats"; diff --git a/core/jni/com_android_internal_os_LongMultiStateCounter.cpp b/core/jni/com_android_internal_os_LongMultiStateCounter.cpp index ddf7a67e00ce..56d3fbba9458 100644 --- a/core/jni/com_android_internal_os_LongMultiStateCounter.cpp +++ b/core/jni/com_android_internal_os_LongMultiStateCounter.cpp @@ -100,7 +100,7 @@ static jlong native_getCount(jlong nativePtr, jint state) { return asLongMultiStateCounter(nativePtr)->getCount(state); } -static jobject native_toString(JNIEnv *env, jobject self, jlong nativePtr) { +static jobject native_toString(JNIEnv *env, jclass, jlong nativePtr) { return env->NewStringUTF(asLongMultiStateCounter(nativePtr)->toString().c_str()); } @@ -118,7 +118,7 @@ static void throwWriteRE(JNIEnv *env, binder_status_t status) { } \ } -static void native_writeToParcel(JNIEnv *env, jobject self, jlong nativePtr, jobject jParcel, +static void native_writeToParcel(JNIEnv *env, jclass, jlong nativePtr, jobject jParcel, jint flags) { battery::LongMultiStateCounter *counter = asLongMultiStateCounter(nativePtr); ndk::ScopedAParcel parcel(AParcel_fromJavaParcel(env, jParcel)); diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 45861a3e8893..41bc8251694f 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -5857,6 +5857,23 @@ use glyph bound's as a source of text width. --> <!-- @FlaggedApi("com.android.text.flags.use_bounds_for_width") --> <attr name="useBoundsForWidth" format="boolean" /> + <!-- Whether to use the locale preferred line height for the minimum line height. + + This flag is useful for preventing jitter of entering letters into empty EditText. + The line height of the text is determined by the font files used for drawing text in a + line. However, in case of the empty text case, the line height cannot be determined and + the default line height: usually it is came from a font of Latin script. By making this + attribute to true, the TextView/EditText uses a line height that is likely used for the + locale associated with the widget. For example, if the system locale is Japanese, the + height of the EditText will be adjusted to meet the height of the Japanese font even if + the text is empty. + + The default value for EditText is true if targetSdkVersion is + {@link android.os.Build.VERSION_CODE#VANILLA_ICE_CREAM} or later, otherwise false. + For other TextViews, the default value is false. + --> + <!-- @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") --> + <attr name="useLocalePreferredLineHeightForMinimum" format="boolean" /> </declare-styleable> <declare-styleable name="TextViewAppearance"> <!-- Base text color, typeface, size, and style. --> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index d4e727ea0afb..c6bc589cffcb 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2141,6 +2141,12 @@ <item>com.android.location.fused</item> </string-array> + <!-- Package name of the extension software fallback. --> + <string name="config_extensionFallbackPackageName" translatable="false"></string> + + <!-- Service name of the extension software fallback. --> + <string name="config_extensionFallbackServiceName" translatable="false"></string> + <!-- Package name(s) of Advanced Driver Assistance applications. These packages have additional management of access to location, specific to driving assistance use-cases. They must be system packages. This configuration is only applicable to devices that declare diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index d0216b308a4c..78ce2d9ca28f 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -82,6 +82,13 @@ <bool name="config_wlan_data_service_conn_persistence_on_restart">true</bool> <java-symbol type="bool" name="config_wlan_data_service_conn_persistence_on_restart" /> + <!-- Indicating whether the retry timer from setup data call response for data throttling should + be honored for emergency network request. By default this is off, meaning for emergency + network requests, the data frameworks will ignore the previous retry timer passed in from + setup data call response. --> + <bool name="config_honor_data_retry_timer_for_emergency_network">false</bool> + <java-symbol type="bool" name="config_honor_data_retry_timer_for_emergency_network" /> + <!-- Cellular data service package name to bind to by default. If none is specified in an overlay, an empty string is passed in --> <string name="config_wwan_data_service_package" translatable="false">com.android.phone</string> @@ -172,6 +179,35 @@ <integer name="config_satellite_nb_iot_inactivity_timeout_millis">180000</integer> <java-symbol type="integer" name="config_satellite_nb_iot_inactivity_timeout_millis" /> + <!-- The time duration in millis needed to switch the modem image from TN to NTN. --> + <integer name="config_satellite_modem_image_switching_duration_millis">20000</integer> + <java-symbol type="integer" name="config_satellite_modem_image_switching_duration_millis" /> + + <!-- The time duration in millis after which Telephony will abort the datagram sending requests. + Telephony starts a timer when receiving a datagram sending request in either OFF, IDLE, or + NOT_CONNECTED state. In NOT_CONNECTED, the duration of the timer is given by this config. + In OFF or IDLE state, the duration of the timer is the sum of this config and the + config_satellite_modem_image_switching_duration_millis. + --> + <integer name="config_datagram_wait_for_connected_state_timeout_millis">60000</integer> + <java-symbol type="integer" name="config_datagram_wait_for_connected_state_timeout_millis" /> + + <!-- The time duration in millis after which Telephony will stop waiting for the response of the + satellite enable request from modem, and send failure response to the client that has + requested Telephony to enable satellite. + --> + <integer name="config_wait_for_satellite_enabling_response_timeout_millis">180000</integer> + <java-symbol type="integer" name="config_wait_for_satellite_enabling_response_timeout_millis" /> + + <!-- The time duration in millis after which Telephony will abort the datagram sending requests + and send failure response to the client that has requested sending the datagrams. Telephony + starts a timer after pushing down the datagram sending request to modem. Before expiry, the + timer will be stopped when Telephony receives the response for the sending request from + modem. + --> + <integer name="config_wait_for_datagram_sending_response_timeout_millis">180000</integer> + <java-symbol type="integer" name="config_wait_for_datagram_sending_response_timeout_millis" /> + <!-- The timeout duration in milliseconds to determine whether to recommend Dialer to show the emergency messaging option to users. @@ -183,6 +219,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/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index 0a6779a9bd8b..5ee555543387 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -153,6 +153,8 @@ <public name="requireContentUriPermissionFromCaller" /> <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") --> <public name="languageSettingsActivity"/> + <!-- @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") --> + <public name="useLocalePreferredLineHeightForMinimum"/> </staging-public-group> <staging-public-group type="id" first-id="0x01bc0000"> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 3df7570c818e..3d19c8587f71 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2154,6 +2154,8 @@ <java-symbol type="string" name="config_systemImageEditor" /> <java-symbol type="string" name="config_datause_iface" /> <java-symbol type="string" name="config_activityRecognitionHardwarePackageName" /> + <java-symbol type="string" name="config_extensionFallbackPackageName" /> + <java-symbol type="string" name="config_extensionFallbackServiceName" /> <java-symbol type="string" name="config_fusedLocationProviderPackageName" /> <java-symbol type="string" name="config_gnssLocationProviderPackageName" /> <java-symbol type="string" name="config_geocoderProviderPackageName" /> diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index ee1a4acb3839..861f71992f54 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -217,6 +217,7 @@ android_ravenwood_test { "src/android/os/**/*.java", "src/android/telephony/PinResultTest.java", "src/android/util/**/*.java", + "src/android/view/DisplayTest.java", "src/android/view/DisplayInfoTest.java", "src/com/android/internal/logging/**/*.java", "src/com/android/internal/os/**/*.java", diff --git a/core/tests/coretests/src/android/os/BluetoothBatteryStatsTest.java b/core/tests/coretests/src/android/os/BluetoothBatteryStatsTest.java new file mode 100644 index 000000000000..e12ca24acb85 --- /dev/null +++ b/core/tests/coretests/src/android/os/BluetoothBatteryStatsTest.java @@ -0,0 +1,64 @@ +/* + * 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.os; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +@RunWith(AndroidJUnit4.class) +public class BluetoothBatteryStatsTest { + + @Test + public void parcelability() { + BluetoothBatteryStats stats = new BluetoothBatteryStats(List.of( + new BluetoothBatteryStats.UidStats(42, 100, 200, 300, 400, 500), + new BluetoothBatteryStats.UidStats(99, 600, 700, 800, 900, 999) + )); + + Parcel parcel = Parcel.obtain(); + stats.writeToParcel(parcel, 0); + byte[] bytes = parcel.marshall(); + parcel.recycle(); + + parcel = Parcel.obtain(); + parcel.unmarshall(bytes, 0, bytes.length); + parcel.setDataPosition(0); + + BluetoothBatteryStats actual = new BluetoothBatteryStats(parcel); + + assertThat(actual.getUidStats()).hasSize(2); + + BluetoothBatteryStats.UidStats uid1 = actual.getUidStats().stream() + .filter(s->s.uid == 42).findFirst().get(); + + assertThat(uid1.scanTimeMs).isEqualTo(100); + assertThat(uid1.unoptimizedScanTimeMs).isEqualTo(200); + assertThat(uid1.scanResultCount).isEqualTo(300); + assertThat(uid1.rxTimeMs).isEqualTo(400); + assertThat(uid1.txTimeMs).isEqualTo(500); + + BluetoothBatteryStats.UidStats uid2 = actual.getUidStats().stream() + .filter(s->s.uid == 99).findFirst().get(); + assertThat(uid2.scanTimeMs).isEqualTo(600); + } +} diff --git a/core/tests/coretests/src/android/os/WakeLockStatsTest.java b/core/tests/coretests/src/android/os/WakeLockStatsTest.java new file mode 100644 index 000000000000..2675ba07d2f7 --- /dev/null +++ b/core/tests/coretests/src/android/os/WakeLockStatsTest.java @@ -0,0 +1,58 @@ +/* + * 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.os; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +@RunWith(AndroidJUnit4.class) +public class WakeLockStatsTest { + + @Test + public void parcelablity() { + WakeLockStats wakeLockStats = new WakeLockStats( + List.of(new WakeLockStats.WakeLock(1, "foo", 200, 3000, 40000), + new WakeLockStats.WakeLock(2, "bar", 500, 6000, 70000))); + + Parcel parcel = Parcel.obtain(); + wakeLockStats.writeToParcel(parcel, 0); + byte[] bytes = parcel.marshall(); + parcel.recycle(); + + parcel = Parcel.obtain(); + parcel.unmarshall(bytes, 0, bytes.length); + parcel.setDataPosition(0); + + WakeLockStats actual = WakeLockStats.CREATOR.createFromParcel(parcel); + assertThat(actual.getWakeLocks()).hasSize(2); + WakeLockStats.WakeLock wl1 = actual.getWakeLocks().get(0); + assertThat(wl1.uid).isEqualTo(1); + assertThat(wl1.name).isEqualTo("foo"); + assertThat(wl1.timesAcquired).isEqualTo(200); + assertThat(wl1.totalTimeHeldMs).isEqualTo(3000); + assertThat(wl1.timeHeldMs).isEqualTo(40000); + + WakeLockStats.WakeLock wl2 = actual.getWakeLocks().get(1); + assertThat(wl2.uid).isEqualTo(2); + } +} diff --git a/core/tests/coretests/src/android/view/DisplayTest.java b/core/tests/coretests/src/android/view/DisplayTest.java new file mode 100644 index 000000000000..4d2a1c45e6da --- /dev/null +++ b/core/tests/coretests/src/android/view/DisplayTest.java @@ -0,0 +1,98 @@ +/* + * 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.view; + +import static com.google.common.truth.Truth.assertWithMessage; + +import android.util.DebugUtils; + +import androidx.test.filters.SmallTest; + +import com.android.internal.util.ArrayUtils; + +import org.junit.Test; + +import java.util.function.IntFunction; + +@SmallTest +public class DisplayTest { + private static final int[] DISPLAY_STATES = { + Display.STATE_UNKNOWN, + Display.STATE_OFF, + Display.STATE_ON, + Display.STATE_DOZE, + Display.STATE_DOZE_SUSPEND, + Display.STATE_VR, + Display.STATE_ON_SUSPEND + }; + + @Test + public void isSuspendedState() { + assertOnlyTrueForStates( + Display::isSuspendedState, + Display.STATE_OFF, + Display.STATE_DOZE_SUSPEND, + Display.STATE_ON_SUSPEND + ); + } + + @Test + public void isDozeState() { + assertOnlyTrueForStates( + Display::isDozeState, + Display.STATE_DOZE, + Display.STATE_DOZE_SUSPEND + ); + } + + @Test + public void isActiveState() { + assertOnlyTrueForStates( + Display::isActiveState, + Display.STATE_ON, + Display.STATE_VR + ); + } + + @Test + public void isOffState() { + assertOnlyTrueForStates( + Display::isOffState, + Display.STATE_OFF + ); + } + + @Test + public void isOnState() { + assertOnlyTrueForStates( + Display::isOnState, + Display.STATE_ON, + Display.STATE_VR, + Display.STATE_ON_SUSPEND + ); + } + + private void assertOnlyTrueForStates(IntFunction<Boolean> function, int... trueStates) { + for (int state : DISPLAY_STATES) { + boolean actual = function.apply(state); + boolean expected = ArrayUtils.contains(trueStates, state); + assertWithMessage("Unexpected return for Display.STATE_" + + DebugUtils.constantToString(Display.class, "STATE_", state)) + .that(actual).isEqualTo(expected); + } + } +} diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java index 610b8aedc983..0b0fd66f0744 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java @@ -19,10 +19,12 @@ package android.view.accessibility; import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.IAccessibilityServiceConnection; +import android.accessibilityservice.IBrailleDisplayController; import android.accessibilityservice.MagnificationConfig; import android.annotation.NonNull; import android.content.pm.ParceledListSlice; import android.graphics.Region; +import android.hardware.usb.UsbDevice; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteCallback; @@ -237,4 +239,15 @@ public class AccessibilityServiceConnectionImpl extends IAccessibilityServiceCon int accessibilityWindowId, SurfaceControl sc, IAccessibilityInteractionConnectionCallback callback) {} + + @Override + public void connectBluetoothBrailleDisplay(String bluetoothAddress, + IBrailleDisplayController controller) {} + + @Override + public void connectUsbBrailleDisplay(UsbDevice usbDevice, + IBrailleDisplayController controller) {} + + @Override + public void setTestBrailleDisplayData(List<Bundle> brailleDisplays) {} } diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java index f39bddd7f032..51eb41c5a271 100644 --- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java +++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java @@ -20,10 +20,12 @@ import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_HOVER_MOVE; import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_UP; +import static android.view.inputmethod.Flags.initiationWithoutInputConnection; import static android.view.stylus.HandwritingTestUtil.createView; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; @@ -91,6 +93,7 @@ public class HandwritingInitiatorTest { private EditText mTestView1; private EditText mTestView2; private Context mContext; + private boolean mInitiateWithoutConnection; @Before public void setup() throws Exception { @@ -119,6 +122,7 @@ public class HandwritingInitiatorTest { mHandwritingInitiator.updateHandwritingAreasForView(mTestView1); mHandwritingInitiator.updateHandwritingAreasForView(mTestView2); doReturn(true).when(mHandwritingInitiator).tryAcceptStylusHandwritingDelegation(any()); + mInitiateWithoutConnection = initiationWithoutInputConnection(); } @Test @@ -194,7 +198,9 @@ public class HandwritingInitiatorTest { mTestView1.setText("hello"); when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4); - mHandwritingInitiator.onInputConnectionCreated(mTestView1); + if (!mInitiateWithoutConnection) { + mHandwritingInitiator.onInputConnectionCreated(mTestView1); + } final int x1 = sHwArea1.left - HW_BOUNDS_OFFSETS_LEFT_PX / 2; final int y1 = sHwArea1.top - HW_BOUNDS_OFFSETS_TOP_PX / 2; MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); @@ -214,7 +220,7 @@ public class HandwritingInitiatorTest { } @Test - public void onTouchEvent_startHandwriting_inputConnectionBuiltAfterStylusMove() { + public void onTouchEvent_startHandwriting_servedViewUpdateAfterStylusMove() { final int x1 = (sHwArea1.left + sHwArea1.right) / 2; final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2; MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); @@ -225,14 +231,19 @@ public class HandwritingInitiatorTest { MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0); mHandwritingInitiator.onTouchEvent(stylusEvent2); - // InputConnection is created after stylus movement. - mHandwritingInitiator.onInputConnectionCreated(mTestView1); + if (mInitiateWithoutConnection) { + // Focus is changed after stylus movement. + mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true); + } else { + // InputConnection is created after stylus movement. + mHandwritingInitiator.onInputConnectionCreated(mTestView1); + } verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1); } @Test - public void onTouchEvent_startHandwriting_inputConnectionBuilt_stylusMoveInExtendedHWArea() { + public void onTouchEvent_startHandwriting_servedViewUpdate_stylusMoveInExtendedHWArea() { mTestView1.setText("hello"); when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4); // The stylus down point is between mTestView1 and mTestView2, but it is within the @@ -247,21 +258,35 @@ public class HandwritingInitiatorTest { MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0); mHandwritingInitiator.onTouchEvent(stylusEvent2); - // First create InputConnection for mTestView2 and verify that handwriting is not started. - mHandwritingInitiator.onInputConnectionCreated(mTestView2); - verify(mHandwritingInitiator, never()).startHandwriting(mTestView2); + if (!mInitiateWithoutConnection) { + // First create InputConnection for mTestView2 and verify that handwriting is not + // started. + mHandwritingInitiator.onInputConnectionCreated(mTestView2); + } - // Next create InputConnection for mTextView1. Handwriting is started for this view since - // the stylus down point is closest to this view. - mHandwritingInitiator.onInputConnectionCreated(mTestView1); + // Note: mTestView2 receives focus when initiationWithoutInputConnection() is enabled. + // verify that handwriting is not started. + verify(mHandwritingInitiator, never()).startHandwriting(mTestView2); + if (mInitiateWithoutConnection) { + // Focus is changed after stylus movement. + mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true); + } else { + // Next create InputConnection for mTextView1. Handwriting is started for this view + // since the stylus down point is closest to this view. + mHandwritingInitiator.onInputConnectionCreated(mTestView1); + } + // Handwriting is started for this view since the stylus down point is closest to this + // view. verify(mHandwritingInitiator).startHandwriting(mTestView1); // Since the stylus down point was outside the TextView's bounds, the handwriting initiator // sets the cursor position. verify(mTestView1).setSelection(4); } + @Test public void onTouchEvent_tryAcceptDelegation_delegatorCallbackCreatesInputConnection() { + assumeFalse(mInitiateWithoutConnection); View delegateView = new EditText(mContext); delegateView.setIsHandwritingDelegate(true); @@ -281,6 +306,7 @@ public class HandwritingInitiatorTest { verify(mHandwritingInitiator, times(1)).tryAcceptStylusHandwritingDelegation(delegateView); } + @Test public void onTouchEvent_tryAcceptDelegation_delegatorCallbackFocusesDelegate() { View delegateView = new EditText(mContext); @@ -288,8 +314,14 @@ public class HandwritingInitiatorTest { mHandwritingInitiator.onInputConnectionCreated(delegateView); reset(mHandwritingInitiator); - mTestView1.setHandwritingDelegatorCallback( - () -> mHandwritingInitiator.onDelegateViewFocused(delegateView)); + if (mInitiateWithoutConnection) { + mTestView1.setHandwritingDelegatorCallback( + () -> mHandwritingInitiator.updateFocusedView( + delegateView, /*fromTouchEvent*/ false)); + } else { + mTestView1.setHandwritingDelegatorCallback( + () -> mHandwritingInitiator.onDelegateViewFocused(delegateView)); + } final int x1 = (sHwArea1.left + sHwArea1.right) / 2; final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2; @@ -339,6 +371,14 @@ public class HandwritingInitiatorTest { assertThat(onTouchEventResult4).isTrue(); } + private void callOnInputConnectionOrUpdateViewFocus(View view) { + if (mInitiateWithoutConnection) { + mHandwritingInitiator.updateFocusedView(view, /*fromTouchEvent*/ true); + } else { + mHandwritingInitiator.onInputConnectionCreated(view); + } + } + @Test public void onTouchEvent_notStartHandwriting_whenHandwritingNotAvailable() { final Rect rect = new Rect(600, 600, 900, 900); @@ -346,7 +386,7 @@ public class HandwritingInitiatorTest { false /* isStylusHandwritingAvailable */); mHandwritingInitiator.updateHandwritingAreasForView(testView); - mHandwritingInitiator.onInputConnectionCreated(testView); + callOnInputConnectionOrUpdateViewFocus(testView); final int x1 = (rect.left + rect.right) / 2; final int y1 = (rect.top + rect.bottom) / 2; MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); @@ -365,7 +405,7 @@ public class HandwritingInitiatorTest { @Test public void onTouchEvent_notStartHandwriting_when_stylusTap_withinHWArea() { - mHandwritingInitiator.onInputConnectionCreated(mTestView1); + callOnInputConnectionOrUpdateViewFocus(mTestView1); final int x1 = 200; final int y1 = 200; MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); @@ -381,7 +421,7 @@ public class HandwritingInitiatorTest { @Test public void onTouchEvent_notStartHandwriting_when_stylusMove_outOfHWArea() { - mHandwritingInitiator.onInputConnectionCreated(mTestView1); + callOnInputConnectionOrUpdateViewFocus(mTestView1); final int x1 = 10; final int y1 = 10; MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); @@ -397,7 +437,7 @@ public class HandwritingInitiatorTest { @Test public void onTouchEvent_notStartHandwriting_when_stylusMove_afterTimeOut() { - mHandwritingInitiator.onInputConnectionCreated(mTestView1); + callOnInputConnectionOrUpdateViewFocus(mTestView1); final int x1 = 10; final int y1 = 10; final long time1 = 10L; @@ -433,8 +473,9 @@ public class HandwritingInitiatorTest { @Test public void onTouchEvent_focusView_inputConnectionAlreadyBuilt_stylusMoveOnce_withinHWArea() { - mHandwritingInitiator.onInputConnectionCreated(mTestView1); - + if (!mInitiateWithoutConnection) { + mHandwritingInitiator.onInputConnectionCreated(mTestView1); + } final int x1 = (sHwArea1.left + sHwArea1.right) / 2; final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2; MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); @@ -487,14 +528,14 @@ public class HandwritingInitiatorTest { verify(mTestView2, times(1)).requestFocus(); - mHandwritingInitiator.onInputConnectionCreated(mTestView2); + callOnInputConnectionOrUpdateViewFocus(mTestView2); verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView2); } @Test public void onTouchEvent_handwritingAreaOverlapped_focusedViewHasPriority() { // Simulate the case where mTestView1 is focused. - mHandwritingInitiator.onInputConnectionCreated(mTestView1); + callOnInputConnectionOrUpdateViewFocus(mTestView1); // The ACTION_DOWN location is within the handwriting bounds of both mTestView1 and // mTestView2. Although it's closer to mTestView2's handwriting bounds, handwriting is // initiated for mTestView1 because it's focused. @@ -559,9 +600,14 @@ public class HandwritingInitiatorTest { // Set mTextView2 to be the delegate of mTestView1. mTestView2.setIsHandwritingDelegate(true); - mTestView1.setHandwritingDelegatorCallback( - () -> mHandwritingInitiator.onInputConnectionCreated(mTestView2)); - + if (mInitiateWithoutConnection) { + mTestView1.setHandwritingDelegatorCallback( + () -> mHandwritingInitiator.updateFocusedView( + mTestView2, /*fromTouchEvent*/ false)); + } else { + mTestView1.setHandwritingDelegatorCallback( + () -> mHandwritingInitiator.onInputConnectionCreated(mTestView2)); + } injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(), /* exceedsHWSlop */ true); // Prerequisite check, verify that handwriting started for delegateView. @@ -610,8 +656,13 @@ public class HandwritingInitiatorTest { assertThat(icon1).isNull(); // Simulate that focus is switched to mTestView2 first and then switched back. - mHandwritingInitiator.onInputConnectionCreated(mTestView2); - mHandwritingInitiator.onInputConnectionCreated(mTestView1); + if (mInitiateWithoutConnection) { + mHandwritingInitiator.updateFocusedView(mTestView2, /*fromTouchEvent*/ true); + mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true); + } else { + mHandwritingInitiator.onInputConnectionCreated(mTestView2); + mHandwritingInitiator.onInputConnectionCreated(mTestView1); + } PointerIcon icon2 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent1); // After the change of focus, hover icon shows again. @@ -620,9 +671,15 @@ public class HandwritingInitiatorTest { @Test public void autoHandwriting_whenDisabled_wontStartHW() { - View mockView = createView(sHwArea1, false /* autoHandwritingEnabled */, - true /* isStylusHandwritingAvailable */); - mHandwritingInitiator.onInputConnectionCreated(mockView); + if (mInitiateWithoutConnection) { + mTestView1.setAutoHandwritingEnabled(false); + mTestView1.setHandwritingDelegatorCallback(null); + mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true); + } else { + View mockView = createView(sHwArea1, false /* autoHandwritingEnabled */, + true /* isStylusHandwritingAvailable */); + mHandwritingInitiator.onInputConnectionCreated(mockView); + } final int x1 = (sHwArea1.left + sHwArea1.right) / 2; final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2; MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); @@ -639,6 +696,7 @@ public class HandwritingInitiatorTest { @Test public void onInputConnectionCreated() { + assumeFalse(mInitiateWithoutConnection); mHandwritingInitiator.onInputConnectionCreated(mTestView1); assertThat(mHandwritingInitiator.mConnectedView).isNotNull(); assertThat(mHandwritingInitiator.mConnectedView.get()).isEqualTo(mTestView1); @@ -646,6 +704,7 @@ public class HandwritingInitiatorTest { @Test public void onInputConnectionCreated_whenAutoHandwritingIsDisabled() { + assumeFalse(mInitiateWithoutConnection); View view = new View(mContext); view.setAutoHandwritingEnabled(false); assertThat(view.isAutoHandwritingEnabled()).isFalse(); @@ -656,6 +715,7 @@ public class HandwritingInitiatorTest { @Test public void onInputConnectionClosed() { + assumeFalse(mInitiateWithoutConnection); mHandwritingInitiator.onInputConnectionCreated(mTestView1); mHandwritingInitiator.onInputConnectionClosed(mTestView1); @@ -664,6 +724,7 @@ public class HandwritingInitiatorTest { @Test public void onInputConnectionClosed_whenAutoHandwritingIsDisabled() { + assumeFalse(mInitiateWithoutConnection); View view = new View(mContext); view.setAutoHandwritingEnabled(false); mHandwritingInitiator.onInputConnectionCreated(view); @@ -674,6 +735,7 @@ public class HandwritingInitiatorTest { @Test public void onInputConnectionCreated_inputConnectionRestarted() { + assumeFalse(mInitiateWithoutConnection); // When IMM restarts input connection, View#onInputConnectionCreatedInternal might be // called before View#onInputConnectionClosedInternal. As a result, we need to handle the // case where "one view "2 InputConnections". diff --git a/core/tests/coretests/src/com/android/internal/os/BackgroundThreadTest.java b/core/tests/coretests/src/com/android/internal/os/BackgroundThreadTest.java new file mode 100644 index 000000000000..8bdf4c6192ba --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/os/BackgroundThreadTest.java @@ -0,0 +1,60 @@ +/* + * 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.internal.os; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.Looper; +import android.platform.test.ravenwood.RavenwoodRule; + +import org.junit.Rule; +import org.junit.Test; + +import java.util.concurrent.Executor; + +public class BackgroundThreadTest { + + @Rule + public final RavenwoodRule mRavenwood = + new RavenwoodRule.Builder().setProvideMainThread(true).build(); + + @Test + public void test_get() { + BackgroundThread thread = BackgroundThread.get(); + assertThat(thread.getLooper()).isNotEqualTo(Looper.getMainLooper()); + } + + @Test + public void test_getHandler() { + Handler handler = BackgroundThread.getHandler(); + ConditionVariable done = new ConditionVariable(); + handler.post(done::open); + boolean success = done.block(5000); + assertThat(success).isTrue(); + } + + @Test + public void test_getExecutor() { + Executor executor = BackgroundThread.getExecutor(); + ConditionVariable done = new ConditionVariable(); + executor.execute(done::open); + boolean success = done.block(5000); + assertThat(success).isTrue(); + } +} diff --git a/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java b/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java index e064e7483bfa..78ef92baa441 100644 --- a/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java +++ b/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java @@ -22,7 +22,6 @@ import static org.junit.Assert.assertThrows; import android.os.BadParcelableException; import android.os.Parcel; -import android.platform.test.annotations.IgnoreUnderRavenwood; import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; @@ -34,7 +33,6 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SmallTest -@IgnoreUnderRavenwood(blockedBy = LongMultiStateCounterTest.class) public class LongMultiStateCounterTest { @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule(); diff --git a/core/tests/coretests/src/com/android/internal/power/EnergyConsumerStatsTest.java b/core/tests/coretests/src/com/android/internal/power/EnergyConsumerStatsTest.java index ae2ef0cb4e51..9c337d788c5c 100644 --- a/core/tests/coretests/src/com/android/internal/power/EnergyConsumerStatsTest.java +++ b/core/tests/coretests/src/com/android/internal/power/EnergyConsumerStatsTest.java @@ -34,7 +34,6 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import android.os.Parcel; -import android.platform.test.annotations.IgnoreUnderRavenwood; import android.platform.test.ravenwood.RavenwoodRule; import android.view.Display; @@ -49,7 +48,6 @@ import java.util.Arrays; * Test class for {@link EnergyConsumerStats}. */ @SmallTest -@IgnoreUnderRavenwood(reason = "Needs kernel support") public class EnergyConsumerStatsTest { @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule(); diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 0baaff0bb2fc..d8713f7a13f8 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -575,6 +575,8 @@ applications that come with the platform <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> <!-- Permissions required for CTS test - CtsContactKeysProviderPrivilegedApp --> <permission name="android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS"/> + <!-- Permission required for CTS test - PackageManagerTest --> + <permission name="android.permission.DOMAIN_VERIFICATION_AGENT"/> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> 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/libs/input/SpriteIcon.h b/libs/input/SpriteIcon.h index 9e6cc81f0bb4..0939af46c258 100644 --- a/libs/input/SpriteIcon.h +++ b/libs/input/SpriteIcon.h @@ -27,33 +27,20 @@ namespace android { * Icon that a sprite displays, including its hotspot. */ struct SpriteIcon { - inline SpriteIcon() : style(PointerIconStyle::TYPE_NULL), hotSpotX(0), hotSpotY(0) {} - inline SpriteIcon(const graphics::Bitmap& bitmap, PointerIconStyle style, float hotSpotX, - float hotSpotY, bool drawNativeDropShadow) + explicit SpriteIcon() = default; + explicit SpriteIcon(const graphics::Bitmap& bitmap, PointerIconStyle style, float hotSpotX, + float hotSpotY, bool drawNativeDropShadow) : bitmap(bitmap), style(style), hotSpotX(hotSpotX), hotSpotY(hotSpotY), drawNativeDropShadow(drawNativeDropShadow) {} - graphics::Bitmap bitmap; - PointerIconStyle style; - float hotSpotX; - float hotSpotY; - bool drawNativeDropShadow; - - inline SpriteIcon copy() const { - return SpriteIcon(bitmap.copy(ANDROID_BITMAP_FORMAT_RGBA_8888), style, hotSpotX, hotSpotY, - drawNativeDropShadow); - } - - inline void reset() { - bitmap.reset(); - style = PointerIconStyle::TYPE_NULL; - hotSpotX = 0; - hotSpotY = 0; - drawNativeDropShadow = false; - } + graphics::Bitmap bitmap{}; + PointerIconStyle style{PointerIconStyle::TYPE_NULL}; + float hotSpotX{}; + float hotSpotY{}; + bool drawNativeDropShadow{false}; inline bool isValid() const { return bitmap.isValid() && !bitmap.isEmpty(); } diff --git a/media/java/android/media/flags/projection.aconfig b/media/java/android/media/flags/projection.aconfig new file mode 100644 index 000000000000..c4b38c725ea4 --- /dev/null +++ b/media/java/android/media/flags/projection.aconfig @@ -0,0 +1,11 @@ +package: "com.android.media.flags" + +# Project link: https://gantry.corp.google.com/projects/android_platform_window_surfaces/changes + +flag { + name: "limit_manage_media_projection" + namespace: "lse_desktop_experience" + description: "Limit signature permission manage_media_projection to the SystemUI role" + bug: "323008518" + is_fixed_read_only: true +} 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/opengl/java/android/opengl/OWNERS b/opengl/java/android/opengl/OWNERS index 9c6c610de52a..e340bc62567a 100644 --- a/opengl/java/android/opengl/OWNERS +++ b/opengl/java/android/opengl/OWNERS @@ -2,3 +2,5 @@ sumir@google.com prahladk@google.com +ianelliott@google.com +lpy@google.com diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt index 7cc95c5d7299..0fc184506eaf 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt @@ -36,6 +36,7 @@ import android.content.pm.VersionedPackage import android.graphics.drawable.Icon import android.os.Build import android.os.Bundle +import android.os.Flags import android.os.Process import android.os.UserHandle import android.os.UserManager @@ -97,16 +98,17 @@ class UninstallRepository(private val context: Context) { } } - if (getMaxTargetSdkVersionForUid(context, callingUid) >= Build.VERSION_CODES.P - && !isPermissionGranted( + if (getMaxTargetSdkVersionForUid(context, callingUid) >= Build.VERSION_CODES.P && + !isPermissionGranted( context, Manifest.permission.REQUEST_DELETE_PACKAGES, callingUid - ) - && !isPermissionGranted(context, Manifest.permission.DELETE_PACKAGES, callingUid) + ) && + !isPermissionGranted(context, Manifest.permission.DELETE_PACKAGES, callingUid) ) { Log.e( - LOG_TAG, "Uid " + callingUid + " does not have " - + Manifest.permission.REQUEST_DELETE_PACKAGES + " or " - + Manifest.permission.DELETE_PACKAGES + LOG_TAG, + "Uid " + callingUid + " does not have " + + Manifest.permission.REQUEST_DELETE_PACKAGES + " or " + + Manifest.permission.DELETE_PACKAGES ) return UninstallAborted(UninstallAborted.ABORT_REASON_GENERIC_ERROR) } @@ -138,8 +140,9 @@ class UninstallRepository(private val context: Context) { val profiles = userManager!!.userProfiles if (!profiles.contains(uninstalledUser)) { Log.e( - LOG_TAG, "User " + Process.myUserHandle() + " can't request uninstall " - + "for user " + uninstalledUser + LOG_TAG, + "User " + Process.myUserHandle() + " can't request uninstall " + + "for user " + uninstalledUser ) return UninstallAborted(UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED) } @@ -202,9 +205,13 @@ class UninstallRepository(private val context: Context) { val isSingleUser = isSingleUser() if (isUpdate) { - messageBuilder.append(context.getString( - if (isSingleUser) R.string.uninstall_update_text - else R.string.uninstall_update_text_multiuser + messageBuilder.append( + context.getString( + if (isSingleUser) { + R.string.uninstall_update_text + } else { + R.string.uninstall_update_text_multiuser + } ) ) } else if (uninstallFromAllUsers && !isSingleUser) { @@ -214,42 +221,42 @@ class UninstallRepository(private val context: Context) { val customUserManager = context.createContextAsUser(uninstalledUser!!, 0) .getSystemService(UserManager::class.java) val userName = customUserManager!!.userName - - val uninstalledUserType = getUninstalledUserType(myUserHandle, uninstalledUser!!) - val messageString: String - when (uninstalledUserType) { - UserManager.USER_TYPE_PROFILE_MANAGED -> { + var messageString = context.getString( + R.string.uninstall_application_text_user, + userName + ) + if (userManager!!.isSameProfileGroup(myUserHandle, uninstalledUser!!)) { + if (customUserManager!!.isManagedProfile()) { messageString = context.getString( - R.string.uninstall_application_text_current_user_work_profile, userName + R.string.uninstall_application_text_current_user_work_profile, userName ) - } - - UserManager.USER_TYPE_PROFILE_CLONE -> { + } else if (customUserManager!!.isCloneProfile()){ isClonedApp = true messageString = context.getString( - R.string.uninstall_application_text_current_user_clone_profile + R.string.uninstall_application_text_current_user_clone_profile ) - } - - else -> { + } else if (Flags.allowPrivateProfile() && customUserManager!!.isPrivateProfile()) { + // TODO(b/324244123): Get these Strings from a User Property API. messageString = context.getString( - R.string.uninstall_application_text_user, userName + R.string.uninstall_application_text_current_user_private_profile ) } - } messageBuilder.append(messageString) } else if (isCloneProfile(uninstalledUser!!)) { isClonedApp = true - messageBuilder.append(context.getString( + messageBuilder.append( + context.getString( R.string.uninstall_application_text_current_user_clone_profile ) ) - } else if (myUserHandle == UserHandle.SYSTEM - && hasClonedInstance(targetAppInfo!!.packageName) + } else if (myUserHandle == UserHandle.SYSTEM && + hasClonedInstance(targetAppInfo!!.packageName) ) { - messageBuilder.append(context.getString( - R.string.uninstall_application_text_with_clone_instance, targetAppLabel + messageBuilder.append( + context.getString( + R.string.uninstall_application_text_with_clone_instance, + targetAppLabel ) ) } else { @@ -296,31 +303,6 @@ class UninstallRepository(private val context: Context) { return userCount == 1 || (UserManager.isHeadlessSystemUserMode() && userCount == 2) } - /** - * Returns the type of the user from where an app is being uninstalled. We are concerned with - * only USER_TYPE_PROFILE_MANAGED and USER_TYPE_PROFILE_CLONE and whether the user and profile - * belong to the same profile group. - */ - private fun getUninstalledUserType( - myUserHandle: UserHandle, - uninstalledUserHandle: UserHandle - ): String? { - if (!userManager!!.isSameProfileGroup(myUserHandle, uninstalledUserHandle)) { - return null - } - val customUserManager = context.createContextAsUser(uninstalledUserHandle, 0) - .getSystemService(UserManager::class.java) - val userTypes = - arrayOf(UserManager.USER_TYPE_PROFILE_MANAGED, UserManager.USER_TYPE_PROFILE_CLONE) - - for (userType in userTypes) { - if (customUserManager!!.isUserOfType(userType)) { - return userType - } - } - return null - } - private fun hasClonedInstance(packageName: String): Boolean { // Check if clone user is present on the device. var cloneUser: UserHandle? = null @@ -334,8 +316,8 @@ class UninstallRepository(private val context: Context) { } // Check if another instance of given package exists in clone user profile. return try { - cloneUser != null - && packageManager.getPackageUidAsUser( + cloneUser != null && + packageManager.getPackageUidAsUser( packageName, PackageManager.PackageInfoFlags.of(0), cloneUser.identifier ) > 0 } catch (e: PackageManager.NameNotFoundException) { @@ -382,7 +364,9 @@ class UninstallRepository(private val context: Context) { val storageStatsManager = context.getSystemService(StorageStatsManager::class.java) try { val stats = storageStatsManager!!.queryStatsForPackage( - packageManager.getApplicationInfo(pkg, 0).storageUuid, pkg, user + packageManager.getApplicationInfo(pkg, 0).storageUuid, + pkg, + user ) return stats.getDataBytes() } catch (e: Exception) { @@ -423,17 +407,24 @@ class UninstallRepository(private val context: Context) { broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, uninstallId) broadcastIntent.setPackage(context.packageName) val pendingIntent = PendingIntent.getBroadcast( - context, uninstallId, broadcastIntent, + context, + uninstallId, + broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE ) if (!startUninstall( - targetPackageName!!, uninstalledUser!!, pendingIntent, uninstallFromAllUsers, + targetPackageName!!, + uninstalledUser!!, + pendingIntent, + uninstallFromAllUsers, keepData ) ) { handleUninstallResult( PackageInstaller.STATUS_FAILURE, - PackageManager.DELETE_FAILED_INTERNAL_ERROR, null, 0 + PackageManager.DELETE_FAILED_INTERNAL_ERROR, + null, + 0 ) } } @@ -474,9 +465,14 @@ class UninstallRepository(private val context: Context) { // Caller did not want the result back. So, we either show a Toast, or a Notification. if (status == PackageInstaller.STATUS_SUCCESS) { - val statusMessage = if (isClonedApp) context.getString( - R.string.uninstall_done_clone_app, targetAppLabel - ) else context.getString(R.string.uninstall_done_app, targetAppLabel) + val statusMessage = if (isClonedApp) { + context.getString( + R.string.uninstall_done_clone_app, + targetAppLabel + ) + } else { + context.getString(R.string.uninstall_done_app, targetAppLabel) + } uninstallResult.setValue( UninstallSuccess(activityResultCode = legacyStatus, message = statusMessage) ) @@ -499,27 +495,32 @@ class UninstallRepository(private val context: Context) { findUserOfDeviceAdmin(myUserHandle, targetPackageName!!) if (otherBlockingUserHandle == null) { Log.d( - LOG_TAG, "Uninstall failed because $targetPackageName" - + " is a device admin" + LOG_TAG, + "Uninstall failed because $targetPackageName" + + " is a device admin" ) addDeviceManagerButton(context, uninstallFailedNotification) setBigText( - uninstallFailedNotification, context.getString( + uninstallFailedNotification, + context.getString( R.string.uninstall_failed_device_policy_manager ) ) } else { Log.d( - LOG_TAG, "Uninstall failed because $targetPackageName" - + " is a device admin of user $otherBlockingUserHandle" + LOG_TAG, + "Uninstall failed because $targetPackageName" + + " is a device admin of user $otherBlockingUserHandle" ) val userName = context.createContextAsUser(otherBlockingUserHandle, 0) .getSystemService(UserManager::class.java)!!.userName setBigText( - uninstallFailedNotification, String.format( + uninstallFailedNotification, + String.format( context.getString( R.string.uninstall_failed_device_policy_manager_of_user - ), userName + ), + userName ) ) } @@ -528,7 +529,9 @@ class UninstallRepository(private val context: Context) { PackageManager.DELETE_FAILED_OWNER_BLOCKED -> { val otherBlockingUserHandle = findBlockingUser(targetPackageName!!) val isProfileOfOrSame = isProfileOfOrSame( - userManager!!, myUserHandle, otherBlockingUserHandle + userManager!!, + myUserHandle, + otherBlockingUserHandle ) if (isProfileOfOrSame) { addDeviceManagerButton(context, uninstallFailedNotification) @@ -538,15 +541,19 @@ class UninstallRepository(private val context: Context) { var bigText: String? = null if (otherBlockingUserHandle == null) { Log.d( - LOG_TAG, "Uninstall failed for $targetPackageName " + + LOG_TAG, + "Uninstall failed for $targetPackageName " + "with code $status no blocking user" ) } else if (otherBlockingUserHandle === UserHandle.SYSTEM) { bigText = context.getString(R.string.uninstall_blocked_device_owner) } else { bigText = context.getString( - if (uninstallFromAllUsers) R.string.uninstall_all_blocked_profile_owner - else R.string.uninstall_blocked_profile_owner + if (uninstallFromAllUsers) { + R.string.uninstall_all_blocked_profile_owner + } else { + R.string.uninstall_blocked_profile_owner + } ) } bigText?.let { setBigText(uninstallFailedNotification, it) } @@ -554,8 +561,9 @@ class UninstallRepository(private val context: Context) { else -> { Log.d( - LOG_TAG, "Uninstall blocked for $targetPackageName" - + " with legacy code $legacyStatus" + LOG_TAG, + "Uninstall blocked for $targetPackageName" + + " with legacy code $legacyStatus" ) } } @@ -639,7 +647,9 @@ class UninstallRepository(private val context: Context) { Icon.createWithResource(context, R.drawable.ic_settings_multiuser), context.getString(R.string.manage_users), PendingIntent.getActivity( - context, 0, getUserSettingsIntent(), + context, + 0, + getUserSettingsIntent(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) ) @@ -668,7 +678,9 @@ class UninstallRepository(private val context: Context) { Icon.createWithResource(context, R.drawable.ic_lock), context.getString(R.string.manage_device_administrators), PendingIntent.getActivity( - context, 0, getDeviceManagerIntent(), + context, + 0, + getDeviceManagerIntent(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) ) @@ -706,7 +718,8 @@ class UninstallRepository(private val context: Context) { context.createContextAsUser(targetUser, 0) .packageManager.packageInstaller.uninstall( VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), - flags, pendingIntent.intentSender + flags, + pendingIntent.intentSender ) true } catch (e: IllegalArgumentException) { @@ -719,7 +732,8 @@ class UninstallRepository(private val context: Context) { if (callback != null) { callback!!.onUninstallComplete( targetPackageName!!, - PackageManager.DELETE_FAILED_ABORTED, "Cancelled by user" + PackageManager.DELETE_FAILED_ABORTED, + "Cancelled by user" ) } } diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig index bab678178ebf..d622eb859cd1 100644 --- a/packages/SettingsLib/aconfig/settingslib.aconfig +++ b/packages/SettingsLib/aconfig/settingslib.aconfig @@ -17,3 +17,9 @@ flag { } } +flag { + name: "bluetooth_qs_tile_dialog_auto_on_toggle" + namespace: "bluetooth" + description: "Displays the auto on toggle in the bluetooth QS tile dialog" + bug: "316985153" +} 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/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java index e55bbecb67d7..9ecbd50fc566 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java @@ -31,6 +31,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.PrintStream; import java.util.HashMap; +import java.util.List; import java.util.Map; public class SettingsStateTest extends AndroidTestCase { @@ -626,4 +627,121 @@ public class SettingsStateTest extends AndroidTestCase { assertEquals(VALUE2, settingsState.getSettingLocked(INVALID_STAGED_FLAG_1).getValue()); } } + + public void testsetSettingsLockedKeepTrunkDefault() throws Exception { + final PrintStream os = new PrintStream(new FileOutputStream(mSettingsFile)); + os.print( + "<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>" + + "<settings version=\"120\">" + + " <setting id=\"0\" name=\"test_namespace/flag0\" " + + "value=\"false\" package=\"com.android.flags\" />" + + " <setting id=\"1\" name=\"test_namespace/flag1\" " + + "value=\"false\" package=\"com.android.flags\" />" + + " <setting id=\"2\" name=\"test_namespace/com.android.flags.flag3\" " + + "value=\"false\" package=\"com.android.flags\" />" + + " <setting id=\"3\" " + + "name=\"test_another_namespace/com.android.another.flags.flag0\" " + + "value=\"false\" package=\"com.android.another.flags\" />" + + "</settings>"); + os.close(); + + int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0); + + SettingsState settingsState = new SettingsState( + getContext(), mLock, mSettingsFile, configKey, + SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); + + String prefix = "test_namespace"; + Map<String, String> keyValues = + Map.of("test_namespace/flag0", "true", "test_namespace/flag2", "false"); + String packageName = "com.android.flags"; + + parsed_flags flags = parsed_flags + .newBuilder() + .addParsedFlag(parsed_flag + .newBuilder() + .setPackage(packageName) + .setName("flag3") + .setNamespace(prefix) + .setDescription("test flag") + .addBug("12345678") + .setState(Aconfig.flag_state.DISABLED) + .setPermission(Aconfig.flag_permission.READ_WRITE)) + .addParsedFlag(parsed_flag + .newBuilder() + .setPackage("com.android.another.flags") + .setName("flag0") + .setNamespace("test_another_namespace") + .setDescription("test flag") + .addBug("12345678") + .setState(Aconfig.flag_state.DISABLED) + .setPermission(Aconfig.flag_permission.READ_WRITE)) + .build(); + + synchronized (mLock) { + settingsState.loadAconfigDefaultValues( + flags.toByteArray(), settingsState.getAconfigDefaultValues()); + List<String> updates = + settingsState.setSettingsLocked("test_namespace/", keyValues, packageName); + assertEquals(3, updates.size()); + + SettingsState.Setting s; + + s = settingsState.getSettingLocked("test_namespace/flag0"); + assertEquals("true", s.getValue()); + + s = settingsState.getSettingLocked("test_namespace/flag1"); + assertNull(s.getValue()); + + s = settingsState.getSettingLocked("test_namespace/flag2"); + assertEquals("false", s.getValue()); + + s = settingsState.getSettingLocked("test_namespace/com.android.flags.flag3"); + assertEquals("false", s.getValue()); + + s = settingsState.getSettingLocked( + "test_another_namespace/com.android.another.flags.flag0"); + assertEquals("false", s.getValue()); + } + } + + public void testsetSettingsLockedNoTrunkDefault() throws Exception { + final PrintStream os = new PrintStream(new FileOutputStream(mSettingsFile)); + os.print( + "<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>" + + "<settings version=\"120\">" + + " <setting id=\"0\" name=\"test_namespace/flag0\" " + + "value=\"false\" package=\"com.android.flags\" />" + + " <setting id=\"1\" name=\"test_namespace/flag1\" " + + "value=\"false\" package=\"com.android.flags\" />" + + "</settings>"); + os.close(); + + int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0); + + SettingsState settingsState = new SettingsState( + getContext(), mLock, mSettingsFile, configKey, + SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); + + Map<String, String> keyValues = + Map.of("test_namespace/flag0", "true", "test_namespace/flag2", "false"); + String packageName = "com.android.flags"; + + synchronized (mLock) { + List<String> updates = + settingsState.setSettingsLocked("test_namespace/", keyValues, packageName); + assertEquals(3, updates.size()); + + SettingsState.Setting s; + + s = settingsState.getSettingLocked("test_namespace/flag0"); + assertEquals("true", s.getValue()); + + s = settingsState.getSettingLocked("test_namespace/flag1"); + assertNull(s.getValue()); + + s = settingsState.getSettingLocked("test_namespace/flag2"); + assertEquals("false", s.getValue()); + } + } } diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 84ef6e51a00b..926e181943e4 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -917,6 +917,9 @@ <!-- Permissions required for CTS test - GrammaticalInflectionManagerTest --> <uses-permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" /> + <!-- Permission required for CTS test - CtsPackageManagerTestCases--> + <uses-permission android:name="android.permission.DOMAIN_VERIFICATION_AGENT" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 7a4e60a45137..56576f1f0c03 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -354,13 +354,6 @@ flag { } flag { - name: "bluetooth_qs_tile_dialog_auto_on_toggle" - namespace: "systemui" - description: "Displays the auto on toggle in the bluetooth QS tile dialog" - bug: "316985153" -} - -flag { name: "smartspace_relocate_to_bottom" namespace: "systemui" description: "Relocate Smartspace to bottom of the Lock Screen" diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt new file mode 100644 index 000000000000..abe1e3de8eea --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt @@ -0,0 +1,377 @@ +/* + * 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.systemui.surfaceeffects.loadingeffect + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.graphics.Paint +import android.graphics.RenderEffect +import android.view.View +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader + +/** + * Plays loading effect with the given configuration. + * + * @param baseType immutable base shader type. This is used for constructing the shader. Reconstruct + * the [LoadingEffect] if the base type needs to be changed. + * @param config immutable parameters that are used for drawing the effect. + * @param paintCallback triggered every frame when animation is playing. Use this to draw the effect + * with [Canvas.drawPaint]. + * @param renderEffectCallback triggered every frame when animation is playing. Use this to draw the + * effect with [RenderEffect]. + * @param animationStateChangedCallback triggered when the [AnimationState] changes. Optional. + * + * The client is responsible to actually draw the [Paint] or [RenderEffect] returned in the + * callback. Note that [View.invalidate] must be called on each callback. There are a few ways to + * render the effect: + * 1) Use [Canvas.drawPaint]. (Preferred. Significantly cheaper!) + * 2) Set [RenderEffect] to the [View]. (Good for chaining effects.) + * 3) Use [RenderNode.setRenderEffect]. (This may be least preferred, as 2 should do what you want.) + * + * <p>First approach is more performant than other ones because [RenderEffect] forces an + * intermediate render pass of the View to a texture to feed into it. + * + * <p>If going with the first approach, your custom [View] would look like as follow: + * <pre>{@code + * private var paint: Paint? = null + * // Override [View.onDraw]. + * override fun onDraw(canvas: Canvas) { + * // RuntimeShader requires hardwareAcceleration. + * if (!canvas.isHardwareAccelerated) return + * + * paint?.let { canvas.drawPaint(it) } + * } + * + * // This is called [Callback.onDraw] + * fun draw(paint: Paint) { + * this.paint = paint + * + * // Must call invalidate to trigger View#onDraw + * invalidate() + * } + * }</pre> + * + * <p>If going with the second approach, it doesn't require an extra custom [View], and it is as + * simple as calling [View.setRenderEffect] followed by [View.invalidate]. You can also chain the + * effect with other [RenderEffect]. + * + * <p>Third approach is an option, but it's more of a boilerplate so you would like to stick with + * the second option. If you want to go with this option for some reason, below is the example: + * <pre>{@code + * // Initialize the shader and paint to use to pass into the [Canvas]. + * private val renderNode = RenderNode("LoadingEffect") + * + * // Override [View.onDraw]. + * override fun onDraw(canvas: Canvas) { + * // RuntimeShader requires hardwareAcceleration. + * if (!canvas.isHardwareAccelerated) return + * + * if (renderNode.hasDisplayList()) { + * canvas.drawRenderNode(renderNode) + * } + * } + * + * // This is called [Callback.onDraw] + * fun draw(renderEffect: RenderEffect) { + * renderNode.setPosition(0, 0, width, height) + * renderNode.setRenderEffect(renderEffect) + * + * val recordingCanvas = renderNode.beginRecording() + * // We need at least 1 drawing instruction. + * recordingCanvas.drawColor(Color.TRANSPARENT) + * renderNode.endRecording() + * + * // Must call invalidate to trigger View#onDraw + * invalidate() + * } + * }</pre> + */ +class LoadingEffect +private constructor( + baseType: TurbulenceNoiseShader.Companion.Type, + private val config: TurbulenceNoiseAnimationConfig, + private val paintCallback: PaintDrawCallback?, + private val renderEffectCallback: RenderEffectDrawCallback?, + private val animationStateChangedCallback: AnimationStateChangedCallback? = null +) { + constructor( + baseType: TurbulenceNoiseShader.Companion.Type, + config: TurbulenceNoiseAnimationConfig, + paintCallback: PaintDrawCallback, + animationStateChangedCallback: AnimationStateChangedCallback? = null + ) : this( + baseType, + config, + paintCallback, + renderEffectCallback = null, + animationStateChangedCallback + ) + constructor( + baseType: TurbulenceNoiseShader.Companion.Type, + config: TurbulenceNoiseAnimationConfig, + renderEffectCallback: RenderEffectDrawCallback, + animationStateChangedCallback: AnimationStateChangedCallback? = null + ) : this( + baseType, + config, + paintCallback = null, + renderEffectCallback, + animationStateChangedCallback + ) + + private val turbulenceNoiseShader: TurbulenceNoiseShader = + TurbulenceNoiseShader(baseType).apply { applyConfig(config) } + private var currentAnimator: ValueAnimator? = null + private var state: AnimationState = AnimationState.NOT_PLAYING + set(value) { + if (field != value) { + animationStateChangedCallback?.onStateChanged(field, value) + field = value + } + } + + // We create a paint instance only if the client renders it with Paint. + private val paint = + if (paintCallback != null) { + Paint().apply { this.shader = turbulenceNoiseShader } + } else { + null + } + + /** Plays LoadingEffect. */ + fun play() { + if (state != AnimationState.NOT_PLAYING) { + return // Ignore if any of the animation is playing. + } + + playEaseIn() + } + + // TODO(b/237282226): Support force finish. + /** Finishes the main animation, which triggers the ease-out animation. */ + fun finish() { + if (state == AnimationState.MAIN) { + // Calling Animator#end sets the animation state back to the initial state. Using pause + // to avoid visual artifacts. + currentAnimator?.pause() + currentAnimator = null + + playEaseOut() + } + } + + /** Updates the noise color dynamically. */ + fun updateColor(newColor: Int) { + turbulenceNoiseShader.setColor(newColor) + } + + /** + * Retrieves the noise offset x, y, z values. This is useful for replaying the animation + * smoothly from the last animation, by passing in the last values to the next animation. + */ + fun getNoiseOffset(): Array<Float> { + return arrayOf( + turbulenceNoiseShader.noiseOffsetX, + turbulenceNoiseShader.noiseOffsetY, + turbulenceNoiseShader.noiseOffsetZ + ) + } + + private fun playEaseIn() { + if (state != AnimationState.NOT_PLAYING) { + return + } + state = AnimationState.EASE_IN + + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.duration = config.easeInDuration.toLong() + + // Animation should start from the initial position to avoid abrupt transition. + val initialX = turbulenceNoiseShader.noiseOffsetX + val initialY = turbulenceNoiseShader.noiseOffsetY + val initialZ = turbulenceNoiseShader.noiseOffsetZ + + animator.addUpdateListener { updateListener -> + val timeInSec = updateListener.currentPlayTime * MS_TO_SEC + val progress = updateListener.animatedValue as Float + + turbulenceNoiseShader.setNoiseMove( + initialX + timeInSec * config.noiseMoveSpeedX, + initialY + timeInSec * config.noiseMoveSpeedY, + initialZ + timeInSec * config.noiseMoveSpeedZ + ) + + // TODO: Replace it with a better curve. + turbulenceNoiseShader.setOpacity(progress * config.luminosityMultiplier) + + draw() + } + + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + currentAnimator = null + playMain() + } + } + ) + + animator.start() + this.currentAnimator = animator + } + + private fun playMain() { + if (state != AnimationState.EASE_IN) { + return + } + state = AnimationState.MAIN + + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.duration = config.maxDuration.toLong() + + // Animation should start from the initial position to avoid abrupt transition. + val initialX = turbulenceNoiseShader.noiseOffsetX + val initialY = turbulenceNoiseShader.noiseOffsetY + val initialZ = turbulenceNoiseShader.noiseOffsetZ + + turbulenceNoiseShader.setOpacity(config.luminosityMultiplier) + + animator.addUpdateListener { updateListener -> + val timeInSec = updateListener.currentPlayTime * MS_TO_SEC + turbulenceNoiseShader.setNoiseMove( + initialX + timeInSec * config.noiseMoveSpeedX, + initialY + timeInSec * config.noiseMoveSpeedY, + initialZ + timeInSec * config.noiseMoveSpeedZ + ) + + draw() + } + + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + currentAnimator = null + playEaseOut() + } + } + ) + + animator.start() + this.currentAnimator = animator + } + + private fun playEaseOut() { + if (state != AnimationState.MAIN) { + return + } + state = AnimationState.EASE_OUT + + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.duration = config.easeOutDuration.toLong() + + // Animation should start from the initial position to avoid abrupt transition. + val initialX = turbulenceNoiseShader.noiseOffsetX + val initialY = turbulenceNoiseShader.noiseOffsetY + val initialZ = turbulenceNoiseShader.noiseOffsetZ + + animator.addUpdateListener { updateListener -> + val timeInSec = updateListener.currentPlayTime * MS_TO_SEC + val progress = updateListener.animatedValue as Float + + turbulenceNoiseShader.setNoiseMove( + initialX + timeInSec * config.noiseMoveSpeedX, + initialY + timeInSec * config.noiseMoveSpeedY, + initialZ + timeInSec * config.noiseMoveSpeedZ + ) + + // TODO: Replace it with a better curve. + turbulenceNoiseShader.setOpacity((1f - progress) * config.luminosityMultiplier) + + draw() + } + + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + currentAnimator = null + state = AnimationState.NOT_PLAYING + } + } + ) + + animator.start() + this.currentAnimator = animator + } + + private fun draw() { + paintCallback?.onDraw(paint!!) + renderEffectCallback?.onDraw( + RenderEffect.createRuntimeShaderEffect(turbulenceNoiseShader, "in_src") + ) + } + + companion object { + /** + * States of the loading effect animation. + * + * <p>The state is designed to be follow the order below: [AnimationState.EASE_IN], + * [AnimationState.MAIN], [AnimationState.EASE_OUT]. Note that ease in and out don't + * necessarily mean the acceleration and deceleration in the animation curve. They simply + * mean each stage of the animation. (i.e. Intro, core, and rest) + */ + enum class AnimationState { + EASE_IN, + MAIN, + EASE_OUT, + NOT_PLAYING + } + + /** Client must implement one of the draw callbacks. */ + interface PaintDrawCallback { + /** + * A callback with a [Paint] object that contains shader info, which is triggered every + * frame while animation is playing. Note that the [Paint] object here is always the + * same instance. + */ + fun onDraw(loadingPaint: Paint) + } + + interface RenderEffectDrawCallback { + /** + * A callback with a [RenderEffect] object that contains shader info, which is triggered + * every frame while animation is playing. Note that the [RenderEffect] instance is + * different each time to update shader uniforms. + */ + fun onDraw(loadingRenderEffect: RenderEffect) + } + + /** Optional callback that is triggered when the animation state changes. */ + interface AnimationStateChangedCallback { + /** + * A callback that's triggered when the [AnimationState] changes. Example usage is + * performing a cleanup when [AnimationState] becomes [NOT_PLAYING]. + */ + fun onStateChanged(oldState: AnimationState, newState: AnimationState) {} + } + + private const val MS_TO_SEC = 0.001f + + private val TAG = LoadingEffect::class.java.simpleName + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt index 30108ac779f2..8dd90a8ffe9f 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt @@ -30,6 +30,7 @@ class TurbulenceNoiseShader(val baseType: Type = Type.SIMPLEX_NOISE) : companion object { private const val UNIFORMS = """ + uniform shader in_src; // Needed to support RenderEffect. uniform float in_gridNum; uniform vec3 in_noiseMove; uniform vec2 in_size; @@ -114,6 +115,7 @@ class TurbulenceNoiseShader(val baseType: Type = Type.SIMPLEX_NOISE) : setSize(config.width, config.height) setLumaMatteFactors(config.lumaMatteBlendFactor, config.lumaMatteOverallBrightness) setInverseNoiseLuminosity(config.shouldInverseNoiseLuminosity) + setNoiseMove(config.noiseOffsetX, config.noiseOffsetY, config.noiseOffsetZ) } /** Sets the number of grid for generating noise. */ diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt index 9a34d6f25551..36ab46b4e616 100644 --- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.LifecycleOwner import com.android.systemui.bouncer.ui.BouncerDialogFactory import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel +import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint @@ -104,7 +105,7 @@ object ComposeFacade : BaseComposeFacade { throwComposeUnavailableError() } - override fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View { + override fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View { throwComposeUnavailableError() } diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt index 51d2a03342b7..5b6aa09e620e 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -40,6 +40,7 @@ import com.android.systemui.common.ui.compose.windowinsets.ScreenDecorProvider import com.android.systemui.communal.ui.compose.CommunalContainer import com.android.systemui.communal.ui.compose.CommunalHub import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel +import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.keyboard.stickykeys.ui.view.createStickyKeyIndicatorView import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel @@ -161,7 +162,7 @@ object ComposeFacade : BaseComposeFacade { } } - override fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View { + override fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View { return ComposeView(context).apply { setContent { PlatformTheme { CommunalContainer(viewModel = viewModel) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index 92bc1f18a6c5..bc85513ae296 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -23,7 +23,9 @@ import com.android.compose.animation.scene.transitions import com.android.compose.animation.scene.updateSceneTransitionLayoutState import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState +import com.android.systemui.communal.ui.compose.extensions.allowGestures import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel +import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.transform @@ -51,7 +53,7 @@ val sceneTransitions = transitions { @Composable fun CommunalContainer( modifier: Modifier = Modifier, - viewModel: BaseCommunalViewModel, + viewModel: CommunalViewModel, ) { val currentScene: SceneKey by viewModel.currentScene @@ -63,6 +65,7 @@ fun CommunalContainer( onChangeScene = { viewModel.onSceneChanged(it.toCommunalSceneKey()) }, transitions = sceneTransitions, ) + val touchesAllowed by viewModel.touchesAllowed.collectAsState(initial = false) // This effect exposes the SceneTransitionLayout's observable transition state to the rest of // the system, and unsets it when the view is disposed to avoid a memory leak. @@ -75,7 +78,7 @@ fun CommunalContainer( SceneTransitionLayout( state = sceneTransitionLayoutState, - modifier = modifier.fillMaxSize(), + modifier = modifier.fillMaxSize().allowGestures(allowed = touchesAllowed), swipeSourceDetector = FixedSizeEdgeDetector(ContainerDimensions.EdgeSwipeSize), ) { scene( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt index 72e884e9e5d6..9c2791f5a257 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt @@ -120,6 +120,7 @@ internal fun Collection<SensorPropertiesInternal?>.extractAuthenticatorTypes(): internal fun promptInfo( logoRes: Int = -1, logoBitmap: Bitmap? = null, + logoDescription: String? = null, title: String = "title", subtitle: String = "sub", description: String = "desc", @@ -132,6 +133,7 @@ internal fun promptInfo( val info = PromptInfo() info.logoRes = logoRes info.logoBitmap = logoBitmap + info.logoDescription = logoDescription info.title = title info.subtitle = subtitle info.description = description diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index f70b6a5a170f..b299ca7ee804 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -45,6 +45,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.media.controls.ui.MediaHost +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository import com.android.systemui.testKosmos @@ -102,6 +103,7 @@ class CommunalViewModelTest : SysuiTestCase() { testScope, kosmos.communalInteractor, kosmos.communalTutorialInteractor, + kosmos.shadeInteractor, mediaHost, logcatLogBuffer("CommunalViewModelTest"), ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt index c23ec2290d6a..bf1d76f87f5a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt @@ -74,6 +74,8 @@ class KeyguardRootViewModelTest : SysuiTestCase() { private val dozeParameters = kosmos.dozeParameters private val underTest by lazy { kosmos.keyguardRootViewModel } + private val viewState = ViewStateAccessor() + @Before fun setUp() { mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR) @@ -251,7 +253,7 @@ class KeyguardRootViewModelTest : SysuiTestCase() { @Test fun alpha_idleOnHub_isZero() = testScope.runTest { - val alpha by collectLastValue(underTest.alpha) + val alpha by collectLastValue(underTest.alpha(viewState)) // Hub transition state is idle with hub open. communalRepository.setTransitionState( @@ -269,7 +271,7 @@ class KeyguardRootViewModelTest : SysuiTestCase() { @Test fun alpha_transitionToHub_isZero() = testScope.runTest { - val alpha by collectLastValue(underTest.alpha) + val alpha by collectLastValue(underTest.alpha(viewState)) keyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.LOCKSCREEN, @@ -283,7 +285,7 @@ class KeyguardRootViewModelTest : SysuiTestCase() { @Test fun alpha_transitionFromHubToLockscreen_isOne() = testScope.runTest { - val alpha by collectLastValue(underTest.alpha) + val alpha by collectLastValue(underTest.alpha(viewState)) // Transition to the glanceable hub and back. keyguardTransitionRepository.sendTransitionSteps( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt index 0c7ce970cf3a..288c0832170f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt @@ -16,10 +16,12 @@ package com.android.systemui.statusbar.notification.collection.render +import android.os.Build import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager +import com.android.systemui.log.assertLogsWtf import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.NotifPipeline @@ -30,7 +32,7 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat -import org.junit.Assert.assertThrows +import org.junit.Assume import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -116,9 +118,9 @@ class GroupExpansionManagerTest : SysuiTestCase() { underTest.setGroupExpanded(summary1, false) // Expanding again should throw. - assertThrows(IllegalArgumentException::class.java) { - underTest.setGroupExpanded(summary1, true) - } + // TODO(b/320238410): Remove this check when robolectric supports wtf assertions. + Assume.assumeFalse(Build.FINGERPRINT.contains("robolectric")) + assertLogsWtf { underTest.setGroupExpanded(summary1, true) } } @Test diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index e853f02ef510..4e540de245dd 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -169,9 +169,6 @@ <dimen name="weather_clock_smartspace_translateX">0dp</dimen> <dimen name="weather_clock_smartspace_translateY">0dp</dimen> - <!-- Additional length to add to the SFPS sensor length we get from framework so that the length - of the progress bar matches the length of the power button --> - <dimen name="sfps_progress_bar_length_extra_padding">12dp</dimen> <!-- Thickness of the progress bar we show for the SFPS based authentication. --> <dimen name="sfps_progress_bar_thickness">6dp</dimen> <!-- Padding from the edge of the screen for the progress bar --> diff --git a/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml index a877853eaec8..0ecdcfcbdd83 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml @@ -13,6 +13,16 @@ android:layout_height="match_parent"> android:scaleType="fitXY" android:visibility="gone" /> + <TextView + android:id="@+id/logo_description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="@integer/biometric_dialog_text_gravity" + android:singleLine="true" + android:marqueeRepeatLimit="1" + android:ellipsize="marquee" + android:visibility="gone"/> + <ImageView android:id="@+id/background" android:layout_width="0dp" diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml index 10f71134c4cc..e7590746207d 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_layout.xml @@ -28,6 +28,15 @@ android:scaleType="fitXY"/> <TextView + android:id="@+id/logo_description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="@integer/biometric_dialog_text_gravity" + android:singleLine="true" + android:marqueeRepeatLimit="1" + android:ellipsize="marquee"/> + + <TextView android:id="@+id/title" android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml index a0f916c6827a..ac781ec2be68 100644 --- a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml +++ b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml @@ -81,7 +81,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="21dp" - android:minHeight="145dp" + android:minHeight="@dimen/bluetooth_dialog_scroll_view_min_height" android:fillViewport="true" app:layout_constrainedHeight="true" app:layout_constraintStart_toStartOf="parent" @@ -97,11 +97,11 @@ <TextView android:id="@+id/bluetooth_toggle_title" android:layout_width="0dp" - android:layout_height="64dp" - android:maxLines="1" + android:layout_height="68dp" + android:maxLines="2" android:ellipsize="end" android:gravity="start|center_vertical" - android:paddingEnd="0dp" + android:paddingEnd="15dp" android:paddingStart="36dp" android:text="@string/turn_on_bluetooth" android:clickable="false" @@ -114,7 +114,7 @@ <Switch android:id="@+id/bluetooth_toggle" android:layout_width="wrap_content" - android:layout_height="64dp" + android:layout_height="68dp" android:gravity="start|center_vertical" android:paddingEnd="40dp" android:contentDescription="@string/turn_on_bluetooth" @@ -126,14 +126,79 @@ app:layout_constraintStart_toEndOf="@+id/bluetooth_toggle_title" app:layout_constraintTop_toTopOf="parent" /> + <androidx.constraintlayout.widget.Group + android:id="@+id/bluetooth_auto_on_toggle_layout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + app:constraint_referenced_ids="bluetooth_auto_on_toggle_title,bluetooth_auto_on_toggle,bluetooth_auto_on_toggle_info_icon,bluetooth_auto_on_toggle_info_text" /> + + <TextView + android:id="@+id/bluetooth_auto_on_toggle_title" + android:layout_width="0dp" + android:layout_height="68dp" + android:layout_marginBottom="20dp" + android:maxLines="2" + android:ellipsize="end" + android:text="@string/turn_on_bluetooth_auto_tomorrow" + android:gravity="start|center_vertical" + android:paddingEnd="15dp" + android:paddingStart="36dp" + android:clickable="false" + android:textAppearance="@style/TextAppearance.Dialog.Title" + android:textSize="16sp" + app:layout_constraintEnd_toStartOf="@+id/bluetooth_auto_on_toggle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/bluetooth_toggle_title" /> + + <Switch + android:id="@+id/bluetooth_auto_on_toggle" + android:layout_width="wrap_content" + android:layout_height="68dp" + android:layout_marginBottom="20dp" + android:gravity="start|center_vertical" + android:paddingEnd="40dp" + android:contentDescription="@string/turn_on_bluetooth_auto_tomorrow" + android:switchMinWidth="@dimen/settingslib_switch_track_width" + android:theme="@style/MainSwitch.Settingslib" + android:thumb="@drawable/settingslib_thumb_selector" + android:track="@drawable/settingslib_track_selector" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/bluetooth_auto_on_toggle_title" + app:layout_constraintTop_toBottomOf="@+id/bluetooth_toggle" /> + + <ImageView + android:id="@+id/bluetooth_auto_on_toggle_info_icon" + android:src="@drawable/ic_info_outline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:tint="?android:attr/textColorTertiary" + android:paddingStart="36dp" + android:layout_marginTop="20dp" + android:layout_marginBottom="@dimen/bluetooth_dialog_layout_margin" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/bluetooth_auto_on_toggle" /> + + <TextView + android:id="@+id/bluetooth_auto_on_toggle_info_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="20dp" + android:paddingStart="36dp" + android:paddingEnd="40dp" + android:text="@string/turn_on_bluetooth_auto_info" + android:textAppearance="@style/TextAppearance.Dialog.Body.Message" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/bluetooth_auto_on_toggle_info_icon" /> + <androidx.recyclerview.widget.RecyclerView android:id="@+id/device_list" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@+id/bluetooth_toggle" - app:layout_constraintBottom_toTopOf="@+id/see_all_button" /> + app:layout_constraintTop_toBottomOf="@+id/bluetooth_toggle" /> <Button android:id="@+id/see_all_button" @@ -168,12 +233,10 @@ android:background="@drawable/bluetooth_tile_dialog_bg_off" android:layout_width="0dp" android:layout_height="64dp" - android:layout_marginBottom="9dp" android:contentDescription="@string/accessibility_bluetooth_device_settings_pair_new_device" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/see_all_button" - app:layout_constraintBottom_toTopOf="@+id/done_button" android:drawableStart="@drawable/ic_add" android:drawablePadding="20dp" android:drawableTint="?android:attr/textColorPrimary" @@ -186,11 +249,19 @@ android:ellipsize="end" android:visibility="gone" /> + <androidx.constraintlayout.widget.Barrier + android:id="@+id/barrier" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="bottom" + app:constraint_referenced_ids="pair_new_device_button,bluetooth_auto_on_toggle_info_text" /> + <Button android:id="@+id/done_button" style="@style/Widget.Dialog.Button" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginTop="9dp" android:layout_marginBottom="@dimen/dialog_bottom_padding" android:layout_marginEnd="@dimen/dialog_side_padding" android:layout_marginStart="@dimen/dialog_side_padding" @@ -200,7 +271,9 @@ android:maxLines="1" android:text="@string/inline_done_button" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintBottom_toBottomOf="parent" /> + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toBottomOf="@+id/barrier" + app:layout_constraintVertical_bias="1" /> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.core.widget.NestedScrollView> </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index cc317544de9b..7537a003275d 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1717,6 +1717,10 @@ <dimen name="bluetooth_dialog_layout_margin">16dp</dimen> <!-- The height of the bluetooth device in bluetooth dialog. --> <dimen name="bluetooth_dialog_device_height">72dp</dimen> + <!-- The height of the main scroll view in bluetooth dialog. --> + <dimen name="bluetooth_dialog_scroll_view_min_height">145dp</dimen> + <!-- The height of the main scroll view in bluetooth dialog with auto on toggle. --> + <dimen name="bluetooth_dialog_scroll_view_min_height_with_auto_on">350dp</dimen> <!-- Height percentage of the parent container occupied by the communal view --> <item name="communal_source_height_percentage" format="float" type="dimen">0.80</item> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 64c6cfa08259..e401c71b3716 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -669,6 +669,10 @@ <string name="accessibility_quick_settings_bluetooth_device_tap_to_disconnect">disconnect</string> <!-- QuickSettings: Accessibility label to activate a device [CHAR LIMIT=NONE]--> <string name="accessibility_quick_settings_bluetooth_device_tap_to_activate">activate</string> + <!-- QuickSettings: Bluetooth auto on tomorrow [CHAR LIMIT=NONE]--> + <string name="turn_on_bluetooth_auto_tomorrow">Automatically turn on again tomorrow</string> + <!-- QuickSettings: Bluetooth auto on info text [CHAR LIMIT=NONE]--> + <string name="turn_on_bluetooth_auto_info">Features like Quick Share, Find My Device, and device location use Bluetooth</string> <!-- QuickSettings: Bluetooth secondary label for the battery level of a connected device [CHAR LIMIT=20]--> <string name="quick_settings_bluetooth_secondary_label_battery_level"><xliff:g id="battery_level_as_percentage">%s</xliff:g> battery</string> diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt index c17c8dced668..6133a51c6497 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt @@ -40,6 +40,7 @@ sealed class BiometricPromptRequest( val contentView: PromptContentView? = info.contentView val logoRes: Int = info.logoRes val logoBitmap: Bitmap? = info.logoBitmap + val logoDescription: String? = info.logoDescription val negativeButtonText: String = info.negativeButtonText?.toString() ?: "" } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index efad21bda380..31aadf51c4f2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -95,6 +95,7 @@ object BiometricViewBinder { view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme) val logoView = view.requireViewById<ImageView>(R.id.logo) + val logoDescriptionView = view.requireViewById<TextView>(R.id.logo_description) val titleView = view.requireViewById<TextView>(R.id.title) val subtitleView = view.requireViewById<TextView>(R.id.subtitle) val descriptionView = view.requireViewById<TextView>(R.id.description) @@ -104,6 +105,8 @@ object BiometricViewBinder { // set selected to enable marquee unless a screen reader is enabled logoView.isSelected = !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled + logoDescriptionView.isSelected = + !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled titleView.isSelected = !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled subtitleView.isSelected = @@ -165,6 +168,7 @@ object BiometricViewBinder { } logoView.setImageDrawable(viewModel.logo.first()) + logoDescriptionView.text = viewModel.logoDescription.first() titleView.text = viewModel.title.first() subtitleView.text = viewModel.subtitle.first() descriptionView.text = viewModel.description.first() @@ -197,6 +201,7 @@ object BiometricViewBinder { viewsToHideWhenSmall = listOf( logoView, + logoDescriptionView, titleView, subtitleView, descriptionView, @@ -205,6 +210,7 @@ object BiometricViewBinder { viewsToFadeInOnSizeChange = listOf( logoView, + logoDescriptionView, titleView, subtitleView, descriptionView, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index ef5c37eaa953..788991d2e45b 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.biometrics.ui.viewmodel import android.content.Context +import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.graphics.Rect import android.graphics.drawable.BitmapDrawable @@ -280,8 +281,9 @@ constructor( it.logoBitmap != null -> BitmapDrawable(context.resources, it.logoBitmap) else -> try { - context.packageManager.getApplicationIcon(it.opPackageName) - } catch (e: PackageManager.NameNotFoundException) { + val info = context.getApplicationInfo(it.opPackageName) + context.packageManager.getApplicationIcon(info) + } catch (e: Exception) { Log.w(TAG, "Cannot find icon for package " + it.opPackageName, e) null } @@ -289,6 +291,25 @@ constructor( } .distinctUntilChanged() + /** Logo description for the prompt. */ + val logoDescription: Flow<String> = + promptSelectorInteractor.prompt + .map { + when { + !customBiometricPrompt() || it == null -> "" + it.logoDescription != null -> it.logoDescription + else -> + try { + val info = context.getApplicationInfo(it.opPackageName) + context.packageManager.getApplicationLabel(info).toString() + } catch (e: Exception) { + Log.w(TAG, "Cannot find name for package " + it.opPackageName, e) + "" + } + } + } + .distinctUntilChanged() + /** Title for the prompt. */ val title: Flow<String> = promptSelectorInteractor.prompt.map { it?.title ?: "" }.distinctUntilChanged() @@ -682,6 +703,12 @@ constructor( } } +private fun Context.getApplicationInfo(packageName: String): ApplicationInfo = + packageManager.getApplicationInfo( + packageName, + PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_ANY_USER + ) + /** How the fingerprint sensor was started for the prompt. */ enum class FingerprintStartMode { /** Fingerprint sensor has not started. */ diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 40d2d1656fbc..febfd4c95db1 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -28,6 +28,8 @@ import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.media.controls.ui.MediaHostState import com.android.systemui.media.dagger.MediaModule +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.util.kotlin.BooleanFlowOperators.not import javax.inject.Inject import javax.inject.Named import kotlinx.coroutines.CoroutineScope @@ -51,6 +53,7 @@ constructor( @Application private val scope: CoroutineScope, private val communalInteractor: CommunalInteractor, tutorialInteractor: CommunalTutorialInteractor, + shadeInteractor: ShadeInteractor, @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, @CommunalLog logBuffer: LogBuffer, ) : BaseCommunalViewModel(communalInteractor, mediaHost) { @@ -81,6 +84,9 @@ constructor( override val isPopupOnDismissCtaShowing: Flow<Boolean> = _isPopupOnDismissCtaShowing.asStateFlow() + /** Whether touches should be disabled in communal */ + val touchesAllowed: Flow<Boolean> = not(shadeInteractor.isAnyFullyExpanded) + init { // Initialize our media host for the UMO. This only needs to happen once and must be done // before the MediaHierarchyManager attempts to move the UMO to the hub. @@ -114,6 +120,7 @@ constructor( } private var delayedHidePopupJob: Job? = null + private fun schedulePopupHiding() { cancelDelayedPopupHiding() delayedHidePopupJob = diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt index 9a4dfdd5d1f6..4e23ecd9c937 100644 --- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt +++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.LifecycleOwner import com.android.systemui.bouncer.ui.BouncerDialogFactory import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel +import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint @@ -116,7 +117,7 @@ interface BaseComposeFacade { ): View /** Creates a container that hosts the communal UI and handles gesture transitions. */ - fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View + fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View /** Creates a [View] that represents the Lockscreen. */ fun createLockscreen( diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index 41ce3fd11e8a..83b2ee28b664 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -22,8 +22,10 @@ import com.android.server.notification.Flags.FLAG_VIBRATE_WHILE_UNLOCKED import com.android.server.notification.Flags.crossAppPoliteNotifications import com.android.server.notification.Flags.politeNotifications import com.android.server.notification.Flags.vibrateWhileUnlocked +import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT +import com.android.systemui.Flags.communalHub import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.SysUISingleton @@ -63,6 +65,9 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha ComposeLockscreen.token dependsOn KeyguardShadeMigrationNssl.token ComposeLockscreen.token dependsOn keyguardBottomAreaRefactor ComposeLockscreen.token dependsOn migrateClocksToBlueprint + + // CommunalHub dependencies + communalHub dependsOn KeyguardShadeMigrationNssl.token } private inline val politeNotifications @@ -75,4 +80,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha get() = FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor()) private inline val migrateClocksToBlueprint get() = FlagToken(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, migrateClocksToBlueprint()) + private inline val communalHub + get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub()) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 9e7c70d67156..1b7a50790561 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -48,6 +48,7 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel +import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.clocks.ClockController @@ -112,6 +113,10 @@ object KeyguardRootViewBinder { } val burnInParams = MutableStateFlow(BurnInParameters()) + val viewState = + ViewStateAccessor( + alpha = { view.alpha }, + ) val disposableHandle = view.repeatWhenAttached { @@ -134,7 +139,7 @@ object KeyguardRootViewBinder { if (keyguardBottomAreaRefactor()) { launch { - viewModel.alpha.collect { alpha -> + viewModel.alpha(viewState).collect { alpha -> view.alpha = alpha childViews[statusViewId]?.alpha = alpha } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 1144efe90c84..f95efaab75f4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -43,6 +43,10 @@ import android.widget.TextView import android.window.InputTransferToken import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet +import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID +import androidx.constraintlayout.widget.ConstraintSet.START +import androidx.constraintlayout.widget.ConstraintSet.TOP +import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import androidx.core.view.isInvisible import com.android.keyguard.ClockEventController import com.android.keyguard.KeyguardClockSwitch @@ -393,7 +397,7 @@ constructor( ), ) - setUpUdfps(previewContext, rootView) + setUpUdfps(previewContext, if (migrateClocksToBlueprint()) keyguardRootView else rootView) if (keyguardBottomAreaRefactor()) { setupShortcuts(keyguardRootView) @@ -468,15 +472,6 @@ constructor( return } - // Place the UDFPS view in the proper sensor location - val fingerprintLayoutParams = - FrameLayout.LayoutParams(sensorBounds.width(), sensorBounds.height()) - fingerprintLayoutParams.setMarginsRelative( - sensorBounds.left, - sensorBounds.top, - sensorBounds.right, - sensorBounds.bottom - ) val finger = LayoutInflater.from(previewContext) .inflate( @@ -484,7 +479,31 @@ constructor( parentView, false, ) as View - parentView.addView(finger, fingerprintLayoutParams) + + // Place the UDFPS view in the proper sensor location + if (migrateClocksToBlueprint()) { + finger.id = R.id.lock_icon_view + parentView.addView(finger) + val cs = ConstraintSet() + cs.clone(parentView as ConstraintLayout) + cs.apply { + constrainWidth(R.id.lock_icon_view, sensorBounds.width()) + constrainHeight(R.id.lock_icon_view, sensorBounds.height()) + connect(R.id.lock_icon_view, TOP, PARENT_ID, TOP, sensorBounds.top) + connect(R.id.lock_icon_view, START, PARENT_ID, START, sensorBounds.left) + } + cs.applyTo(parentView) + } else { + val fingerprintLayoutParams = + FrameLayout.LayoutParams(sensorBounds.width(), sensorBounds.height()) + fingerprintLayoutParams.setMarginsRelative( + sensorBounds.left, + sensorBounds.top, + sensorBounds.right, + sensorBounds.bottom + ) + parentView.addView(finger, fingerprintLayoutParams) + } } private fun setUpClock(previewContext: Context, parentView: ViewGroup) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt index d75a72f91061..75132a59eb88 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt @@ -24,11 +24,13 @@ import androidx.constraintlayout.widget.ConstraintSet.END import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP +import com.android.systemui.Flags.centralizedStatusBarDimensRefactor import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlags +import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.shade.NotificationPanelView import com.android.systemui.statusbar.notification.stack.AmbientState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController @@ -36,6 +38,7 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCa import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel +import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -52,6 +55,7 @@ constructor( ambientState: AmbientState, controller: NotificationStackScrollLayoutController, notificationStackSizeCalculator: NotificationStackSizeCalculator, + private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>, @Main mainDispatcher: CoroutineDispatcher, ) : NotificationStackScrollLayoutSection( @@ -74,12 +78,27 @@ constructor( val bottomMargin = context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin) if (migrateClocksToBlueprint()) { + val useLargeScreenHeader = + context.resources.getBoolean(R.bool.config_use_large_screen_shade_header) + val marginTopLargeScreen = + if (centralizedStatusBarDimensRefactor()) { + largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight() + } else { + context.resources.getDimensionPixelSize( + R.dimen.large_screen_shade_header_height + ) + } connect( R.id.nssl_placeholder, TOP, R.id.smart_space_barrier_bottom, BOTTOM, - bottomMargin + bottomMargin + + if (useLargeScreenHeader) { + marginTopLargeScreen + } else { + 0 + } ) } else { connect(R.id.nssl_placeholder, TOP, R.id.keyguard_status_view, BOTTOM, bottomMargin) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt index 756a4cca69d0..3e35ae4b2dc3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt @@ -23,13 +23,11 @@ import androidx.constraintlayout.widget.ConstraintSet.END import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP -import com.android.systemui.Flags.centralizedStatusBarDimensRefactor import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlags -import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.shade.NotificationPanelView import com.android.systemui.statusbar.notification.stack.AmbientState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController @@ -37,7 +35,6 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCa import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel -import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -56,7 +53,6 @@ constructor( notificationStackSizeCalculator: NotificationStackSizeCalculator, private val smartspaceViewModel: KeyguardSmartspaceViewModel, @Main mainDispatcher: CoroutineDispatcher, - private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>, ) : NotificationStackScrollLayoutSection( context, @@ -75,16 +71,13 @@ constructor( return } constraintSet.apply { - val splitShadeTopMargin = - if (centralizedStatusBarDimensRefactor()) { - largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight() - } else { - context.resources.getDimensionPixelSize( - R.dimen.large_screen_shade_header_height - ) - } - connect(R.id.nssl_placeholder, TOP, PARENT_ID, TOP, splitShadeTopMargin) - + connect( + R.id.nssl_placeholder, + TOP, + PARENT_ID, + TOP, + context.resources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin) + ) connect(R.id.nssl_placeholder, START, PARENT_ID, START) connect(R.id.nssl_placeholder, END, PARENT_ID, END) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index ec13228c6216..83be65181bea 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -60,18 +60,21 @@ constructor( private val deviceEntryInteractor: DeviceEntryInteractor, private val dozeParameters: DozeParameters, private val keyguardInteractor: KeyguardInteractor, - communalInteractor: CommunalInteractor, + private val communalInteractor: CommunalInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor, - aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, - lockscreenToGoneTransitionViewModel: LockscreenToGoneTransitionViewModel, - alternateBouncerToGoneTransitionViewModel: AlternateBouncerToGoneTransitionViewModel, - primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel, - lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel, - glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel, - screenOffAnimationController: ScreenOffAnimationController, + private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, + private val lockscreenToGoneTransitionViewModel: LockscreenToGoneTransitionViewModel, + private val alternateBouncerToGoneTransitionViewModel: + AlternateBouncerToGoneTransitionViewModel, + private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel, + private val lockscreenToGlanceableHubTransitionViewModel: + LockscreenToGlanceableHubTransitionViewModel, + private val glanceableHubToLockscreenTransitionViewModel: + GlanceableHubToLockscreenTransitionViewModel, + private val screenOffAnimationController: ScreenOffAnimationController, private val aodBurnInViewModel: AodBurnInViewModel, - aodAlphaViewModel: AodAlphaViewModel, + private val aodAlphaViewModel: AodAlphaViewModel, ) { val burnInLayerVisibility: Flow<Int> = @@ -101,8 +104,8 @@ constructor( val topClippingBounds: Flow<Int?> = keyguardInteractor.topClippingBounds /** An observable for the alpha level for the entire keyguard root view. */ - val alpha: Flow<Float> = - combine( + fun alpha(viewState: ViewStateAccessor): Flow<Float> { + return combine( communalInteractor.isIdleOnCommunal, // The transitions are mutually exclusive, so they are safe to merge to get the last // value emitted by any of them. Do not add flows that cannot make this guarantee. @@ -110,7 +113,7 @@ constructor( aodAlphaViewModel.alpha, lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha, glanceableHubToLockscreenTransitionViewModel.keyguardAlpha, - lockscreenToGoneTransitionViewModel.lockscreenAlpha, + lockscreenToGoneTransitionViewModel.lockscreenAlpha(viewState), primaryBouncerToGoneTransitionViewModel.lockscreenAlpha, alternateBouncerToGoneTransitionViewModel.lockscreenAlpha, ) @@ -125,6 +128,7 @@ constructor( } } .distinctUntilChanged() + } /** Specific alpha value for elements visible during [KeyguardState.LOCKSCREEN] */ val lockscreenStateAlpha: Flow<Float> = aodToLockscreenTransitionViewModel.lockscreenAlpha diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt index d981650adf60..15459f4af9a4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt @@ -16,10 +16,12 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.util.MathUtils import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow.FlowBuilder import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds @@ -37,7 +39,7 @@ constructor( animationFlow: KeyguardTransitionAnimationFlow, ) : DeviceEntryIconTransition { - private val transitionAnimation = + private val transitionAnimation: FlowBuilder = animationFlow.setup( duration = FromLockscreenTransitionInteractor.TO_GONE_DURATION, from = KeyguardState.LOCKSCREEN, @@ -52,7 +54,26 @@ constructor( onCancel = { 1f }, ) - val lockscreenAlpha: Flow<Float> = shortcutsAlpha + fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> { + var startAlpha: Float? = null + return transitionAnimation.sharedFlow( + duration = 200.milliseconds, + onStep = { + if (startAlpha == null) { + startAlpha = viewState.alpha() + } + MathUtils.lerp(startAlpha!!, 0f, it) + }, + onFinish = { + startAlpha = null + 0f + }, + onCancel = { + startAlpha = null + 1f + }, + ) + } override val deviceEntryParentViewAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt index 67c42f0fe343..00e5d35354b1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt @@ -67,8 +67,8 @@ class SideFpsProgressBarViewModel @Inject constructor( private val context: Context, - private val biometricStatusInteractor: BiometricStatusInteractor, - private val deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, + biometricStatusInteractor: BiometricStatusInteractor, + deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, private val sfpsSensorInteractor: SideFpsSensorInteractor, // todo (b/317432075) Injecting DozeServiceHost directly instead of using it through // DozeInteractor as DozeServiceHost already depends on DozeInteractor. @@ -89,9 +89,6 @@ constructor( _progress.value = 0.0f } - private val additionalSensorLengthPadding = - context.resources.getDimension(R.dimen.sfps_progress_bar_length_extra_padding).toInt() - // Merged [FingerprintAuthenticationStatus] from BiometricPrompt acquired messages and // device entry authentication messages private val mergedFingerprintAuthenticationStatus = @@ -114,9 +111,7 @@ constructor( val progress: Flow<Float> = _progress.asStateFlow() val progressBarLength: Flow<Int> = - sfpsSensorInteractor.sensorLocation - .map { it.length + additionalSensorLengthPadding } - .distinctUntilChanged() + sfpsSensorInteractor.sensorLocation.map { it.length }.distinctUntilChanged() val progressBarThickness = context.resources.getDimension(R.dimen.sfps_progress_bar_thickness).toInt() @@ -128,7 +123,6 @@ constructor( context.resources .getDimension(R.dimen.sfps_progress_bar_padding_from_edge) .toInt() - val lengthOfTheProgressBar = sensorLocation.length + additionalSensorLengthPadding val viewLeftTop = Point(sensorLocation.left, sensorLocation.top) val totalDistanceFromTheEdge = paddingFromEdge + progressBarThickness @@ -139,7 +133,7 @@ constructor( // Sensor is vertical to the current orientation, we rotate it 270 deg // around the (left,top) point as the pivot. We need to push it down the // length of the progress bar so that it is still aligned to the sensor - viewLeftTop.y += lengthOfTheProgressBar + viewLeftTop.y += sensorLocation.length val isSensorOnTheNearEdge = rotation == DisplayRotation.ROTATION_180 || rotation == DisplayRotation.ROTATION_90 @@ -164,7 +158,6 @@ constructor( // We want to push it up from the bottom edge by the padding and // the thickness of the progressbar. viewLeftTop.y -= totalDistanceFromTheEdge - viewLeftTop.x -= additionalSensorLengthPadding } } viewLeftTop diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ViewStateAccessor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ViewStateAccessor.kt new file mode 100644 index 000000000000..cb5db8632a3e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ViewStateAccessor.kt @@ -0,0 +1,24 @@ +/* + * 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.systemui.keyguard.ui.viewmodel + +/** View-level state information to be shared between ui and viewmodel. */ +data class ViewStateAccessor( + val alpha: () -> Float = { 0f }, + val translationY: () -> Int = { 0 }, + val translationX: () -> Int = { 0 }, +) 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/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt new file mode 100644 index 000000000000..dcae088fecf7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt @@ -0,0 +1,61 @@ +/* + * 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.systemui.qs.tiles.dialog.bluetooth + +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +/** Interactor class responsible for interacting with the Bluetooth Auto-On feature. */ +@SysUISingleton +class BluetoothAutoOnInteractor +@Inject +constructor( + private val bluetoothAutoOnRepository: BluetoothAutoOnRepository, +) { + + val isEnabled = bluetoothAutoOnRepository.getValue.map { it == ENABLED }.distinctUntilChanged() + + /** + * Checks if the auto on value is present in the repository. + * + * @return `true` if a value is present (i.e, the feature is enabled by the Bluetooth server). + */ + suspend fun isValuePresent(): Boolean = bluetoothAutoOnRepository.isValuePresent() + + /** + * Sets enabled or disabled based on the provided value. + * + * @param value `true` to enable the feature, `false` to disable it. + */ + suspend fun setEnabled(value: Boolean) { + if (!isValuePresent()) { + Log.e(TAG, "Trying to set toggle value while feature not available.") + } else { + val newValue = if (value) ENABLED else DISABLED + bluetoothAutoOnRepository.setValue(newValue) + } + } + + companion object { + private const val TAG = "BluetoothAutoOnInteractor" + const val DISABLED = 0 + const val ENABLED = 1 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt new file mode 100644 index 000000000000..e17b4d3f376b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.dialog.bluetooth + +import android.os.UserHandle +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.withContext + +/** Repository class responsible for managing the Bluetooth Auto-On feature settings. */ +// TODO(b/316822488): Handle multi-user +@SysUISingleton +class BluetoothAutoOnRepository +@Inject +constructor( + private val secureSettings: SecureSettings, + private val userRepository: UserRepository, + @Application private val coroutineScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) { + // Flow representing the auto on setting value + internal val getValue: Flow<Int> = + secureSettings + .observerFlow(UserHandle.USER_SYSTEM, SETTING_NAME) + .onStart { emit(Unit) } + .map { + if (userRepository.getSelectedUserInfo().id != UserHandle.USER_SYSTEM) { + Log.i(TAG, "Current user is not USER_SYSTEM. Multi-user is not supported") + return@map UNSET + } + secureSettings.getIntForUser(SETTING_NAME, UNSET, UserHandle.USER_SYSTEM) + } + .distinctUntilChanged() + .flowOn(backgroundDispatcher) + .shareIn(coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0)) + + /** + * Checks if the auto on setting value is ever set for the current user. + * + * @return `true` if the setting value is not UNSET, `false` otherwise. + */ + suspend fun isValuePresent(): Boolean = + withContext(backgroundDispatcher) { + if (userRepository.getSelectedUserInfo().id != UserHandle.USER_SYSTEM) { + Log.i(TAG, "Current user is not USER_SYSTEM. Multi-user is not supported") + false + } else { + secureSettings.getIntForUser(SETTING_NAME, UNSET, UserHandle.USER_SYSTEM) != UNSET + } + } + + /** + * Sets the Bluetooth Auto-On setting value for the current user. + * + * @param value The new setting value to be applied. + */ + suspend fun setValue(value: Int) { + withContext(backgroundDispatcher) { + if (userRepository.getSelectedUserInfo().id != UserHandle.USER_SYSTEM) { + Log.i(TAG, "Current user is not USER_SYSTEM. Multi-user is not supported") + } else { + secureSettings.putIntForUser(SETTING_NAME, value, UserHandle.USER_SYSTEM) + } + } + } + + companion object { + private const val TAG = "BluetoothAutoOnRepository" + const val SETTING_NAME = "bluetooth_automatic_turn_on" + const val UNSET = -1 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt index 1a06c38803af..6b53c7ac0a14 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt @@ -56,7 +56,7 @@ import kotlinx.coroutines.withContext internal class BluetoothTileDialog constructor( private val bluetoothToggleInitialValue: Boolean, - private val subtitleResIdInitialValue: Int, + private val initialUiProperties: BluetoothTileDialogViewModel.UiProperties, private val cachedContentHeight: Int, private val bluetoothTileDialogCallback: BluetoothTileDialogCallback, @Main private val mainDispatcher: CoroutineDispatcher, @@ -71,6 +71,10 @@ constructor( internal val bluetoothStateToggle get() = mutableBluetoothStateToggle.asStateFlow() + private val mutableBluetoothAutoOnToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null) + internal val bluetoothAutoOnToggle + get() = mutableBluetoothAutoOnToggle.asStateFlow() + private val mutableDeviceItemClick: MutableSharedFlow<DeviceItem> = MutableSharedFlow(extraBufferCapacity = 1) internal val deviceItemClick @@ -89,6 +93,8 @@ constructor( private lateinit var toggleView: Switch private lateinit var subtitleTextView: TextView + private lateinit var autoOnToggle: Switch + private lateinit var autoOnToggleView: View private lateinit var doneButton: View private lateinit var seeAllButton: View private lateinit var pairNewDeviceButton: View @@ -108,6 +114,8 @@ constructor( toggleView = requireViewById(R.id.bluetooth_toggle) subtitleTextView = requireViewById(R.id.bluetooth_tile_dialog_subtitle) as TextView + autoOnToggle = requireViewById(R.id.bluetooth_auto_on_toggle) + autoOnToggleView = requireViewById(R.id.bluetooth_auto_on_toggle_layout) doneButton = requireViewById(R.id.done_button) seeAllButton = requireViewById(R.id.see_all_button) pairNewDeviceButton = requireViewById(R.id.pair_new_device_button) @@ -116,7 +124,7 @@ constructor( setupToggle() setupRecyclerView() - subtitleTextView.text = context.getString(subtitleResIdInitialValue) + subtitleTextView.text = context.getString(initialUiProperties.subTitleResId) doneButton.setOnClickListener { dismiss() } seeAllButton.setOnClickListener { bluetoothTileDialogCallback.onSeeAllClicked(it) } pairNewDeviceButton.setOnClickListener { @@ -124,7 +132,9 @@ constructor( } requireViewById<View>(R.id.scroll_view).apply { scrollViewContent = this - layoutParams.height = cachedContentHeight + minimumHeight = + resources.getDimensionPixelSize(initialUiProperties.scrollViewMinHeightResId) + layoutParams.height = maxOf(cachedContentHeight, minimumHeight) } progressBarAnimation = requireViewById(R.id.bluetooth_tile_dialog_progress_animation) progressBarBackground = requireViewById(R.id.bluetooth_tile_dialog_progress_background) @@ -178,13 +188,27 @@ constructor( } } - internal fun onBluetoothStateUpdated(isEnabled: Boolean, subtitleResId: Int) { + internal fun onBluetoothStateUpdated( + isEnabled: Boolean, + uiProperties: BluetoothTileDialogViewModel.UiProperties + ) { toggleView.apply { isChecked = isEnabled setEnabled(true) alpha = ENABLED_ALPHA } - subtitleTextView.text = context.getString(subtitleResId) + subtitleTextView.text = context.getString(uiProperties.subTitleResId) + autoOnToggleView.visibility = uiProperties.autoOnToggleVisibility + } + + internal fun onBluetoothAutoOnUpdated(isEnabled: Boolean) { + if (::autoOnToggle.isInitialized) { + autoOnToggle.apply { + isChecked = isEnabled + setEnabled(true) + alpha = ENABLED_ALPHA + } + } } private fun setupToggle() { @@ -198,6 +222,16 @@ constructor( logger.logBluetoothState(BluetoothStateStage.USER_TOGGLED, isChecked.toString()) uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TOGGLE_CLICKED) } + + autoOnToggleView.visibility = initialUiProperties.autoOnToggleVisibility + autoOnToggle.setOnCheckedChangeListener { view, isChecked -> + mutableBluetoothAutoOnToggle.value = isChecked + view.apply { + isEnabled = false + alpha = DISABLED_ALPHA + } + uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_AUTO_ON_TOGGLE_CLICKED) + } } private fun setupRecyclerView() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt index 86e5ddef87e7..cd52e0dcca4a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt @@ -31,7 +31,8 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent @UiEvent(doc = "Saved clicked to connect") SAVED_DEVICE_CONNECT(1500), @UiEvent(doc = "Active device clicked to disconnect") ACTIVE_DEVICE_DISCONNECT(1507), @UiEvent(doc = "Connected other device clicked to disconnect") - CONNECTED_OTHER_DEVICE_DISCONNECT(1508); + CONNECTED_OTHER_DEVICE_DISCONNECT(1508), + @UiEvent(doc = "The auto on toggle is clicked") BLUETOOTH_AUTO_ON_TOGGLE_CLICKED(1617); override fun getId() = metricId } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt index 54bb95cafca6..5a14e5f11d38 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt @@ -21,9 +21,15 @@ import android.content.Intent import android.content.SharedPreferences import android.os.Bundle import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE import android.view.ViewGroup +import androidx.annotation.DimenRes +import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.UiEventLogger +import com.android.settingslib.flags.Flags.bluetoothQsTileDialogAutoOnToggle import com.android.systemui.Prefs import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator @@ -58,6 +64,7 @@ internal class BluetoothTileDialogViewModel constructor( private val deviceItemInteractor: DeviceItemInteractor, private val bluetoothStateInteractor: BluetoothStateInteractor, + private val bluetoothAutoOnInteractor: BluetoothAutoOnInteractor, private val dialogTransitionAnimator: DialogTransitionAnimator, private val activityStarter: ActivityStarter, private val systemClock: SystemClock, @@ -143,7 +150,10 @@ constructor( bluetoothStateInteractor.bluetoothStateUpdate .filterNotNull() .onEach { - dialog.onBluetoothStateUpdated(it, getSubtitleResId(it)) + dialog.onBluetoothStateUpdated( + it, + UiProperties.build(it, isAutoOnToggleFeatureAvailable()) + ) updateDeviceItemJob?.cancel() updateDeviceItemJob = launch { deviceItemInteractor.updateDeviceItems( @@ -177,6 +187,21 @@ constructor( } .launchIn(this) + if (isAutoOnToggleFeatureAvailable()) { + // bluetoothAutoOnUpdate is emitted when bluetooth auto on on/off state is + // changed. + bluetoothAutoOnInteractor.isEnabled + .onEach { dialog.onBluetoothAutoOnUpdated(it) } + .launchIn(this) + + // bluetoothAutoOnToggle is emitted when user toggles the bluetooth auto on + // switch, send the new value to the bluetoothAutoOnInteractor. + dialog.bluetoothAutoOnToggle + .filterNotNull() + .onEach { bluetoothAutoOnInteractor.setEnabled(it) } + .launchIn(this) + } + produce<Unit> { awaitClose { dialog.cancel() } } } } @@ -192,7 +217,10 @@ constructor( return BluetoothTileDialog( bluetoothStateInteractor.isBluetoothEnabled, - getSubtitleResId(bluetoothStateInteractor.isBluetoothEnabled), + UiProperties.build( + bluetoothStateInteractor.isBluetoothEnabled, + isAutoOnToggleFeatureAvailable() + ), cachedContentHeight, this@BluetoothTileDialogViewModel, mainDispatcher, @@ -244,6 +272,10 @@ constructor( } } + @VisibleForTesting + internal suspend fun isAutoOnToggleFeatureAvailable() = + bluetoothQsTileDialogAutoOnToggle() && bluetoothAutoOnInteractor.isValuePresent() + companion object { private const val INTERACTION_JANK_TAG = "bluetooth_tile_dialog" private const val CONTENT_HEIGHT_PREF_KEY = Prefs.Key.BLUETOOTH_TILE_DIALOG_CONTENT_HEIGHT @@ -251,6 +283,29 @@ constructor( if (isBluetoothEnabled) R.string.quick_settings_bluetooth_tile_subtitle else R.string.bt_is_off } + + internal data class UiProperties( + @StringRes val subTitleResId: Int, + val autoOnToggleVisibility: Int, + @DimenRes val scrollViewMinHeightResId: Int, + ) { + companion object { + internal fun build( + isBluetoothEnabled: Boolean, + isAutoOnToggleFeatureAvailable: Boolean + ) = + UiProperties( + subTitleResId = getSubtitleResId(isBluetoothEnabled), + autoOnToggleVisibility = + if (isAutoOnToggleFeatureAvailable && !isBluetoothEnabled) VISIBLE + else GONE, + scrollViewMinHeightResId = + if (isAutoOnToggleFeatureAvailable) + R.dimen.bluetooth_dialog_scroll_view_min_height_with_auto_on + else R.dimen.bluetooth_dialog_scroll_view_min_height + ) + } + } } internal interface BluetoothTileDialogCallback { 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/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java index 3cdb2cd9b5c6..d1aff80b4e7c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.collection.render; +import android.util.Log; + import androidx.annotation.NonNull; import com.android.systemui.Dumpable; @@ -40,6 +42,8 @@ import javax.inject.Inject; */ @SysUISingleton public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpable { + private static final String TAG = "GroupExpansionaManagerImpl"; + private final DumpManager mDumpManager; private final GroupMembershipManager mGroupMembershipManager; private final Set<OnGroupExpansionChangeListener> mOnGroupChangeListeners = new HashSet<>(); @@ -100,7 +104,7 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(entry); if (entry.getParent() == null) { if (expanded) { - throw new IllegalArgumentException("Cannot expand group that is not attached"); + Log.wtf(TAG, "Cannot expand group that is not attached"); } else { // The entry is no longer attached, but we still want to make sure we don't have // a stale expansion state. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationThrottleHun.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationThrottleHun.kt new file mode 100644 index 000000000000..dd81d42b58ee --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationThrottleHun.kt @@ -0,0 +1,53 @@ +/* + * 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.systemui.statusbar.notification.shared + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the notification throttle hun flag state. */ +@Suppress("NOTHING_TO_INLINE") +object NotificationThrottleHun { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_NOTIFICATION_THROTTLE_HUN + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.notificationThrottleHun() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 47daf495b87d..830b8c1ae63e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -1335,6 +1335,10 @@ public class NotificationStackScrollLayoutController implements Dumpable { } } + public float getAlpha() { + return mView.getAlpha(); + } + public void setSuppressChildrenMeasureAndLayout(boolean suppressLayout) { mView.suppressChildrenMeasureAndLayout(suppressLayout); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt index b4f578fec910..ffab9ea00b35 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt @@ -76,14 +76,10 @@ class SharedNotificationContainer( } val nsslId = R.id.notification_stack_scroller constraintSet.apply { - connect(nsslId, START, startConstraintId, START) - connect(nsslId, END, PARENT_ID, END) - connect(nsslId, BOTTOM, PARENT_ID, BOTTOM) - connect(nsslId, TOP, PARENT_ID, TOP) - setMargin(nsslId, START, marginStart) - setMargin(nsslId, END, marginEnd) - setMargin(nsslId, TOP, marginTop) - setMargin(nsslId, BOTTOM, marginBottom) + connect(nsslId, START, startConstraintId, START, marginStart) + connect(nsslId, END, PARENT_ID, END, marginEnd) + connect(nsslId, BOTTOM, PARENT_ID, BOTTOM, marginBottom) + connect(nsslId, TOP, PARENT_ID, TOP, marginTop) } constraintSet.applyTo(this) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index 97db9b6c4191..daea8af8f334 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -24,6 +24,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters +import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor @@ -75,6 +76,10 @@ object SharedNotificationContainerBinder { } val burnInParams = MutableStateFlow(BurnInParameters()) + val viewState = + ViewStateAccessor( + alpha = { controller.getAlpha() }, + ) /* * For animation sensitive coroutines, immediately run just like applicationScope does @@ -141,7 +146,7 @@ object SharedNotificationContainerBinder { if (!sceneContainerFlags.isEnabled()) { launch { - viewModel.expansionAlpha.collect { + viewModel.expansionAlpha(viewState).collect { controller.setMaxAlphaForExpansion(it) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index 811da51b55ce..ff00cb31783d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -50,6 +50,7 @@ import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGoneTransitionView import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine @@ -67,7 +68,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart @@ -100,21 +100,35 @@ constructor( setOf(AOD, LOCKSCREEN, DOZING, ALTERNATE_BOUNCER, PRIMARY_BOUNCER) private val edgeToAlphaViewModel = - mapOf<Edge?, Flow<Float>>( + mapOf<Edge?, (ViewStateAccessor) -> Flow<Float>>( Edge(from = LOCKSCREEN, to = DREAMING) to - lockscreenToDreamingTransitionViewModel.lockscreenAlpha, + { _: ViewStateAccessor -> + lockscreenToDreamingTransitionViewModel.lockscreenAlpha + }, Edge(from = LOCKSCREEN, to = GONE) to - lockscreenToGoneTransitionViewModel.lockscreenAlpha, + { viewState: ViewStateAccessor -> + lockscreenToGoneTransitionViewModel.lockscreenAlpha(viewState) + }, Edge(from = ALTERNATE_BOUNCER, to = GONE) to - alternateBouncerToGoneTransitionViewModel.lockscreenAlpha, + { _: ViewStateAccessor -> + alternateBouncerToGoneTransitionViewModel.lockscreenAlpha + }, Edge(from = PRIMARY_BOUNCER, to = GONE) to - primaryBouncerToGoneTransitionViewModel.lockscreenAlpha, + { _: ViewStateAccessor -> + primaryBouncerToGoneTransitionViewModel.lockscreenAlpha + }, Edge(from = DREAMING, to = LOCKSCREEN) to - dreamingToLockscreenTransitionViewModel.lockscreenAlpha, + { _: ViewStateAccessor -> + dreamingToLockscreenTransitionViewModel.lockscreenAlpha + }, Edge(from = LOCKSCREEN, to = OCCLUDED) to - lockscreenToOccludedTransitionViewModel.lockscreenAlpha, + { _: ViewStateAccessor -> + lockscreenToOccludedTransitionViewModel.lockscreenAlpha + }, Edge(from = OCCLUDED, to = LOCKSCREEN) to - occludedToLockscreenTransitionViewModel.lockscreenAlpha, + { _: ViewStateAccessor -> + occludedToLockscreenTransitionViewModel.lockscreenAlpha + }, ) private val lockscreenTransitionInProgress: Flow<Edge?> = @@ -151,21 +165,20 @@ constructor( val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> = interactor.configurationBasedDimensions .map { + val marginTop = + if (it.useLargeScreenHeader) it.marginTopLargeScreen else it.marginTop ConfigurationBasedDimensions( marginStart = if (it.useSplitShade) 0 else it.marginHorizontal, marginEnd = it.marginHorizontal, marginBottom = it.marginBottom, - marginTop = - if (it.useLargeScreenHeader) it.marginTopLargeScreen else it.marginTop, + marginTop = marginTop, useSplitShade = it.useSplitShade, paddingTop = if (it.useSplitShade) { - // When in split shade, the margin is applied twice as the legacy shade - // code uses it to calculate padding. - it.keyguardSplitShadeTopMargin - 2 * it.marginTopLargeScreen + marginTop } else { 0 - } + }, ) } .distinctUntilChanged() @@ -255,13 +268,15 @@ constructor( isOnLockscreenWithoutShade, keyguardInteractor.notificationContainerBounds, configurationBasedDimensions, - interactor.topPosition.sampleCombine( - keyguardTransitionInteractor.isInTransitionToAnyState, - shadeInteractor.qsExpansion, - ), + interactor.topPosition + .sampleCombine( + keyguardTransitionInteractor.isInTransitionToAnyState, + shadeInteractor.qsExpansion, + ) + .onStart { emit(Triple(0f, false, 0f)) } ) { onLockscreen, bounds, config, (top, isInTransitionToAnyState, qsExpansion) -> if (onLockscreen) { - bounds.copy(top = bounds.top + config.paddingTop) + bounds.copy(top = bounds.top - config.paddingTop) } else { // When QS expansion > 0, it should directly set the top padding so do not // animate it @@ -278,46 +293,63 @@ constructor( initialValue = NotificationContainerBounds(), ) - /** As QS is expanding, fade out notifications unless in splitshade */ - private val alphaForQsExpansion: Flow<Float> = - interactor.configurationBasedDimensions.flatMapLatest { - if (it.useSplitShade) { - flowOf(1f) - } else { - shadeInteractor.qsExpansion.map { 1f - it } + /** + * Ensure view is visible when the shade/qs are expanded. Also, as QS is expanding, fade out + * notifications unless in splitshade. + */ + private val alphaForShadeAndQsExpansion: Flow<Float> = + interactor.configurationBasedDimensions + .flatMapLatest { configurationBasedDimensions -> + combine( + shadeInteractor.shadeExpansion, + shadeInteractor.qsExpansion, + ) { shadeExpansion, qsExpansion -> + if (shadeExpansion > 0f || qsExpansion > 0f) { + if (configurationBasedDimensions.useSplitShade) { + 1f + } else { + // Fade as QS shade expands + 1f - qsExpansion + } + } else { + // Not visible unless the shade/qs is visible + 0f + } + } } - } + .distinctUntilChanged() - val expansionAlpha: Flow<Float> = + fun expansionAlpha(viewState: ViewStateAccessor): Flow<Float> { // Due to issues with the legacy shade, some shade expansion events are sent incorrectly, // such as when the shade resets. This can happen while the transition to/from LOCKSCREEN // is running. Therefore use a series of flatmaps to prevent unwanted interruptions while // those transitions are in progress. Without this, the alpha value will produce a visible // flicker. - lockscreenTransitionInProgress + return lockscreenTransitionInProgress .flatMapLatest { edge -> - edgeToAlphaViewModel.getOrElse( + edgeToAlphaViewModel.getOrDefault( edge, - { + { _: ViewStateAccessor -> isOnLockscreenWithoutShade.flatMapLatest { isOnLockscreenWithoutShade -> combineTransform( keyguardInteractor.keyguardAlpha, shadeCollpaseFadeIn, - alphaForQsExpansion, - ) { alpha, shadeCollpaseFadeIn, alphaForQsExpansion -> + alphaForShadeAndQsExpansion, + ) { alpha, shadeCollpaseFadeIn, alphaForShadeAndQsExpansion -> if (isOnLockscreenWithoutShade) { if (!shadeCollpaseFadeIn) { emit(alpha) } } else { - emit(alphaForQsExpansion) + emit(alphaForShadeAndQsExpansion) } } } } - ) + )(viewState) } .distinctUntilChanged() + } /** * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB transition diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java index ca3e3c629619..db55da7b8acb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java @@ -43,6 +43,7 @@ import javax.inject.Inject; */ public class KeyguardClockPositionAlgorithm { private static final String TAG = "KeyguardClockPositionAlgorithm"; + private static final boolean DEBUG = false; /** * Margin between the bottom of the status view and the notification shade. @@ -318,24 +319,26 @@ public class KeyguardClockPositionAlgorithm { } float fullyDarkBurnInOffset = burnInPreventionOffsetY(burnInPreventionOffsetY); - float clockYDark = clockY - + fullyDarkBurnInOffset - + shift; + float clockYDark = clockY + fullyDarkBurnInOffset + shift; mCurrentBurnInOffsetY = MathUtils.lerp(0, fullyDarkBurnInOffset, darkAmount); - final String inputs = "panelExpansion: " + panelExpansion + " darkAmount: " + darkAmount; - final String outputs = "clockY: " + clockY - + " burnInPreventionOffsetY: " + burnInPreventionOffsetY - + " fullyDarkBurnInOffset: " + fullyDarkBurnInOffset - + " shift: " + shift - + " mOverStretchAmount: " + mOverStretchAmount - + " mCurrentBurnInOffsetY: " + mCurrentBurnInOffsetY; - mLogger.i(msg -> { - return msg.getStr1() + " -> " + msg.getStr2(); - }, msg -> { - msg.setStr1(inputs); - msg.setStr2(outputs); - return kotlin.Unit.INSTANCE; - }); + + if (DEBUG) { + final float finalShift = shift; + final float finalBurnInPreventionOffsetY = burnInPreventionOffsetY; + mLogger.i(msg -> { + final String inputs = "panelExpansion: " + panelExpansion + + " darkAmount: " + darkAmount; + final String outputs = "clockY: " + clockY + + " burnInPreventionOffsetY: " + finalBurnInPreventionOffsetY + + " fullyDarkBurnInOffset: " + fullyDarkBurnInOffset + + " shift: " + finalShift + + " mOverStretchAmount: " + mOverStretchAmount + + " mCurrentBurnInOffsetY: " + mCurrentBurnInOffsetY; + return inputs + " -> " + outputs; + }, msg -> { + return kotlin.Unit.INSTANCE; + }); + } return (int) (MathUtils.lerp(clockY, clockYDark, darkAmount) + mOverStretchAmount); } 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/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java index d4c180df7442..2b0a92c6ecd7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java @@ -16,11 +16,14 @@ package com.android.systemui.statusbar.policy; +import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS; + import static com.android.server.notification.Flags.screenshareNotificationHiding; import android.annotation.MainThread; import android.app.IActivityManager; import android.content.Context; +import android.database.ExecutorContentObserver; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionManager; import android.os.Handler; @@ -37,6 +40,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.util.Assert; import com.android.systemui.util.ListenerSet; +import com.android.systemui.util.settings.GlobalSettings; import java.util.concurrent.Executor; @@ -50,6 +54,7 @@ public class SensitiveNotificationProtectionControllerImpl private final ArraySet<String> mExemptPackages = new ArraySet<>(); private final ListenerSet<Runnable> mListeners = new ListenerSet<>(); private volatile MediaProjectionInfo mProjection; + boolean mDisableScreenShareProtections = false; @VisibleForTesting final MediaProjectionManager.Callback mMediaProjectionCallback = @@ -58,6 +63,12 @@ public class SensitiveNotificationProtectionControllerImpl public void onStart(MediaProjectionInfo info) { Trace.beginSection("SNPC.onProjectionStart"); try { + if (mDisableScreenShareProtections) { + Log.w(LOG_TAG, + "Screen share protections disabled, ignoring projectionstart"); + return; + } + // Only enable sensitive content protection if sharing full screen // Launch cookie only set (non-null) if sharing single app/task updateProjectionStateAndNotifyListeners( @@ -81,6 +92,7 @@ public class SensitiveNotificationProtectionControllerImpl @Inject public SensitiveNotificationProtectionControllerImpl( Context context, + GlobalSettings settings, MediaProjectionManager mediaProjectionManager, IActivityManager activityManager, @Main Handler mainHandler, @@ -89,6 +101,25 @@ public class SensitiveNotificationProtectionControllerImpl return; } + ExecutorContentObserver developerOptionsObserver = new ExecutorContentObserver(bgExecutor) { + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + boolean disableScreenShareProtections = settings.getInt( + DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, + 0) != 0; + mainHandler.post(() -> { + mDisableScreenShareProtections = disableScreenShareProtections; + }); + } + }; + settings.registerContentObserver( + DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, + developerOptionsObserver); + + // Get current setting value + bgExecutor.execute(() -> developerOptionsObserver.onChange(true)); + bgExecutor.execute(() -> { ArraySet<String> exemptPackages = new ArraySet<>(); // Exempt SystemUI diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index e6637e60b146..cd19259091ab 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -22,6 +22,7 @@ import android.view.ViewTreeObserver import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.Flags as AConfigFlags import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository @@ -259,6 +260,7 @@ class ClockEventControllerTest : SysuiTestCase() { @Test fun keyguardCallback_visibilityChanged_clockDozeCalled() = runBlocking(IMMEDIATE) { + mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL) val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt index a46167a423f1..8fab2332c00e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt @@ -26,6 +26,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { @Test fun biometricRequestFromPromptInfo() { val logoRes = R.drawable.ic_cake + val logoDescription = "test cake" val title = "what" val subtitle = "a" val description = "request" @@ -41,6 +42,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { BiometricPromptRequest.Biometric( promptInfo( logoRes = logoRes, + logoDescription = logoDescription, title = title, subtitle = subtitle, description = description, @@ -53,6 +55,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { ) assertThat(request.logoRes).isEqualTo(logoRes) + assertThat(request.logoDescription).isEqualTo(logoDescription) assertThat(request.title).isEqualTo(title) assertThat(request.subtitle).isEqualTo(subtitle) assertThat(request.description).isEqualTo(description) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 2e94d381b8dc..ff68fe3df9e9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.biometrics.ui.viewmodel +import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.res.Configuration import android.graphics.Bitmap @@ -74,6 +75,8 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.junit.MockitoJUnit @@ -95,6 +98,8 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor @Mock private lateinit var udfpsUtils: UdfpsUtils @Mock private lateinit var packageManager: PackageManager + @Mock private lateinit var applicationInfoWithIcon: ApplicationInfo + @Mock private lateinit var applicationInfoNoIcon: ApplicationInfo private val fakeExecutor = FakeExecutor(FakeSystemClock()) private val testScope = TestScope() @@ -102,6 +107,8 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa private val logoResFromApp = R.drawable.ic_cake private val logoFromApp = context.getDrawable(logoResFromApp) private val logoBitmapFromApp = Bitmap.createBitmap(400, 400, Bitmap.Config.RGB_565) + private val defaultLogoDescription = "Test Android App" + private val logoDescriptionFromApp = "Test Cake App" private lateinit var fingerprintRepository: FakeFingerprintPropertyRepository private lateinit var promptRepository: FakePromptRepository @@ -166,7 +173,14 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa iconViewModel = viewModel.iconViewModel // Set up default logo icon and app customized icon - whenever(packageManager.getApplicationIcon(OP_PACKAGE_NAME)).thenReturn(defaultLogoIcon) + whenever(packageManager.getApplicationInfo(eq(OP_PACKAGE_NAME_NO_ICON), anyInt())) + .thenReturn(applicationInfoNoIcon) + whenever(packageManager.getApplicationInfo(eq(OP_PACKAGE_NAME), anyInt())) + .thenReturn(applicationInfoWithIcon) + whenever(packageManager.getApplicationIcon(applicationInfoWithIcon)) + .thenReturn(defaultLogoIcon) + whenever(packageManager.getApplicationLabel(applicationInfoWithIcon)) + .thenReturn(defaultLogoDescription) context.setMockPackageManager(packageManager) val resources = context.getOrCreateTestableResources() resources.addOverride(logoResFromApp, logoFromApp) @@ -1277,6 +1291,29 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat((logo as BitmapDrawable).bitmap).isEqualTo(logoBitmapFromApp) } + @Test + fun logoDescriptionIsEmptyIfPackageNameNotFound() = + runGenericTest(packageName = OP_PACKAGE_NAME_NO_ICON) { + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) + val logoDescription by collectLastValue(viewModel.logoDescription) + assertThat(logoDescription).isEqualTo("") + } + + @Test + fun defaultLogoDescriptionIfNoLogoDescriptionSet() = runGenericTest { + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) + val logoDescription by collectLastValue(viewModel.logoDescription) + assertThat(logoDescription).isEqualTo(defaultLogoDescription) + } + + @Test + fun logoDescriptionSetByApp() = + runGenericTest(logoDescription = logoDescriptionFromApp) { + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) + val logoDescription by collectLastValue(viewModel.logoDescription) + assertThat(logoDescription).isEqualTo(logoDescriptionFromApp) + } + /** Asserts that the selected buttons are visible now. */ private suspend fun TestScope.assertButtonsVisible( tryAgain: Boolean = false, @@ -1300,6 +1337,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa contentView: PromptContentView? = null, logoRes: Int = -1, logoBitmap: Bitmap? = null, + logoDescription: String? = null, packageName: String = OP_PACKAGE_NAME, block: suspend TestScope.() -> Unit, ) { @@ -1312,6 +1350,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa contentViewFromApp = contentView, logoResFromApp = logoRes, logoBitmapFromApp = logoBitmap, + logoDescriptionFromApp = logoDescription, packageName = packageName, ) @@ -1492,12 +1531,14 @@ private fun PromptSelectorInteractor.initializePrompt( contentViewFromApp: PromptContentView? = null, logoResFromApp: Int = -1, logoBitmapFromApp: Bitmap? = null, + logoDescriptionFromApp: String? = null, packageName: String = OP_PACKAGE_NAME, ) { val info = PromptInfo().apply { logoRes = logoResFromApp logoBitmap = logoBitmapFromApp + logoDescription = logoDescriptionFromApp title = "t" subtitle = "s" description = descriptionFromApp diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt index 8b05a54fe7cf..e7aaddd94695 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState @@ -56,6 +57,41 @@ class LockscreenToGoneTransitionViewModelTest : SysuiTestCase() { deviceEntryParentViewAlpha.forEach { assertThat(it).isEqualTo(0f) } } + @Test + fun lockscreenAlphaStartsFromViewStateAccessorAlpha() = + testScope.runTest { + val viewState = ViewStateAccessor(alpha = { 0.5f }) + val alpha by collectLastValue(underTest.lockscreenAlpha(viewState)) + + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) + + repository.sendTransitionStep(step(0f)) + assertThat(alpha).isEqualTo(0.5f) + + repository.sendTransitionStep(step(0.25f)) + assertThat(alpha).isEqualTo(0.25f) + + repository.sendTransitionStep(step(.5f)) + assertThat(alpha).isEqualTo(0f) + } + + @Test + fun lockscreenAlphaWithNoViewStateAccessorValue() = + testScope.runTest { + val alpha by collectLastValue(underTest.lockscreenAlpha(ViewStateAccessor())) + + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) + + repository.sendTransitionStep(step(0f)) + assertThat(alpha).isEqualTo(0f) + + repository.sendTransitionStep(step(0.25f)) + assertThat(alpha).isEqualTo(0f) + + repository.sendTransitionStep(step(0.5f)) + assertThat(alpha).isEqualTo(0f) + } + private fun step( value: Float, state: TransitionState = TransitionState.RUNNING 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/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt new file mode 100644 index 000000000000..37107135c6d8 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt @@ -0,0 +1,98 @@ +/* + * 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.systemui.qs.tiles.dialog.bluetooth + +import android.content.pm.UserInfo +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth +import kotlin.test.Test +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class BluetoothAutoOnInteractorTest : SysuiTestCase() { + @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + private var secureSettings: FakeSettings = FakeSettings() + private val userRepository: FakeUserRepository = FakeUserRepository() + private lateinit var bluetoothAutoOnInteractor: BluetoothAutoOnInteractor + + @Before + fun setUp() { + bluetoothAutoOnInteractor = + BluetoothAutoOnInteractor( + BluetoothAutoOnRepository( + secureSettings, + userRepository, + testScope.backgroundScope, + testDispatcher + ) + ) + } + + @Test + fun testSet_bluetoothAutoOnUnset_doNothing() { + testScope.runTest { + bluetoothAutoOnInteractor.setEnabled(true) + + val actualValue by collectLastValue(bluetoothAutoOnInteractor.isEnabled) + + runCurrent() + + Truth.assertThat(actualValue).isEqualTo(false) + } + } + + @Test + fun testSet_bluetoothAutoOnSet_setNewValue() { + testScope.runTest { + userRepository.setUserInfos(listOf(SYSTEM_USER)) + secureSettings.putIntForUser( + BluetoothAutoOnRepository.SETTING_NAME, + BluetoothAutoOnInteractor.DISABLED, + SYSTEM_USER_ID + ) + bluetoothAutoOnInteractor.setEnabled(true) + + val actualValue by collectLastValue(bluetoothAutoOnInteractor.isEnabled) + + runCurrent() + + Truth.assertThat(actualValue).isEqualTo(true) + } + } + + companion object { + private const val SYSTEM_USER_ID = 0 + private val SYSTEM_USER = + UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt new file mode 100644 index 000000000000..8986d99712a0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt @@ -0,0 +1,127 @@ +/* + * 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.systemui.qs.tiles.dialog.bluetooth + +import android.content.pm.UserInfo +import android.os.UserHandle +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnInteractor.Companion.DISABLED +import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnInteractor.Companion.ENABLED +import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnRepository.Companion.SETTING_NAME +import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnRepository.Companion.UNSET +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class BluetoothAutoOnRepositoryTest : SysuiTestCase() { + @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + private var secureSettings: FakeSettings = FakeSettings() + private val userRepository: FakeUserRepository = FakeUserRepository() + + private lateinit var bluetoothAutoOnRepository: BluetoothAutoOnRepository + + @Before + fun setUp() { + bluetoothAutoOnRepository = + BluetoothAutoOnRepository( + secureSettings, + userRepository, + testScope.backgroundScope, + testDispatcher + ) + + userRepository.setUserInfos(listOf(SECONDARY_USER, SYSTEM_USER)) + } + + @Test + fun testGetValue_valueUnset() { + testScope.runTest { + userRepository.setSelectedUserInfo(SYSTEM_USER) + val actualValue by collectLastValue(bluetoothAutoOnRepository.getValue) + + runCurrent() + + assertThat(actualValue).isEqualTo(UNSET) + assertThat(bluetoothAutoOnRepository.isValuePresent()).isFalse() + } + } + + @Test + fun testGetValue_valueFalse() { + testScope.runTest { + userRepository.setSelectedUserInfo(SYSTEM_USER) + val actualValue by collectLastValue(bluetoothAutoOnRepository.getValue) + + secureSettings.putIntForUser(SETTING_NAME, DISABLED, UserHandle.USER_SYSTEM) + runCurrent() + + assertThat(actualValue).isEqualTo(DISABLED) + } + } + + @Test + fun testGetValue_valueTrue() { + testScope.runTest { + userRepository.setSelectedUserInfo(SYSTEM_USER) + val actualValue by collectLastValue(bluetoothAutoOnRepository.getValue) + + secureSettings.putIntForUser(SETTING_NAME, ENABLED, UserHandle.USER_SYSTEM) + runCurrent() + + assertThat(actualValue).isEqualTo(ENABLED) + } + } + + @Test + fun testGetValue_valueTrue_secondaryUser_returnUnset() { + testScope.runTest { + userRepository.setSelectedUserInfo(SECONDARY_USER) + val actualValue by collectLastValue(bluetoothAutoOnRepository.getValue) + + secureSettings.putIntForUser(SETTING_NAME, ENABLED, SECONDARY_USER_ID) + runCurrent() + + assertThat(actualValue).isEqualTo(UNSET) + } + } + + companion object { + private const val SYSTEM_USER_ID = 0 + private const val SECONDARY_USER_ID = 1 + private val SYSTEM_USER = + UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0) + private val SECONDARY_USER = + UserInfo(/* id= */ SECONDARY_USER_ID, /* name= */ "secondary user", /* flags= */ 0) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt index 154aa1cabb0c..70b04175da91 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt @@ -71,7 +71,11 @@ class BluetoothTileDialogTest : SysuiTestCase() { @Mock private lateinit var logger: BluetoothTileDialogLogger - private val subtitleResId = R.string.quick_settings_bluetooth_tile_subtitle + private val uiProperties = + BluetoothTileDialogViewModel.UiProperties.build( + isBluetoothEnabled = ENABLED, + isAutoOnToggleFeatureAvailable = ENABLED + ) private val fakeSystemClock = FakeSystemClock() @@ -90,7 +94,7 @@ class BluetoothTileDialogTest : SysuiTestCase() { bluetoothTileDialog = BluetoothTileDialog( ENABLED, - subtitleResId, + uiProperties, CONTENT_HEIGHT, bluetoothTileDialogCallback, dispatcher, @@ -131,7 +135,7 @@ class BluetoothTileDialogTest : SysuiTestCase() { bluetoothTileDialog = BluetoothTileDialog( ENABLED, - subtitleResId, + uiProperties, CONTENT_HEIGHT, bluetoothTileDialogCallback, dispatcher, @@ -166,7 +170,7 @@ class BluetoothTileDialogTest : SysuiTestCase() { val viewHolder = BluetoothTileDialog( ENABLED, - subtitleResId, + uiProperties, CONTENT_HEIGHT, bluetoothTileDialogCallback, dispatcher, @@ -194,7 +198,7 @@ class BluetoothTileDialogTest : SysuiTestCase() { val viewHolder = BluetoothTileDialog( ENABLED, - subtitleResId, + uiProperties, CONTENT_HEIGHT, bluetoothTileDialogCallback, dispatcher, @@ -219,7 +223,7 @@ class BluetoothTileDialogTest : SysuiTestCase() { bluetoothTileDialog = BluetoothTileDialog( ENABLED, - subtitleResId, + uiProperties, CONTENT_HEIGHT, bluetoothTileDialogCallback, dispatcher, @@ -253,12 +257,36 @@ class BluetoothTileDialogTest : SysuiTestCase() { } @Test - fun testShowDialog_displayFromCachedHeight() { + fun testShowDialog_cachedHeightLargerThanMinHeight_displayFromCachedHeight() { testScope.runTest { + val cachedHeight = Int.MAX_VALUE bluetoothTileDialog = BluetoothTileDialog( ENABLED, - subtitleResId, + uiProperties, + cachedHeight, + bluetoothTileDialogCallback, + dispatcher, + fakeSystemClock, + uiEventLogger, + logger, + mContext + ) + bluetoothTileDialog.show() + assertThat( + bluetoothTileDialog.requireViewById<View>(R.id.scroll_view).layoutParams.height + ) + .isEqualTo(cachedHeight) + } + } + + @Test + fun testShowDialog_cachedHeightLessThanMinHeight_displayFromUiProperties() { + testScope.runTest { + bluetoothTileDialog = + BluetoothTileDialog( + ENABLED, + uiProperties, MATCH_PARENT, bluetoothTileDialogCallback, dispatcher, @@ -271,7 +299,32 @@ class BluetoothTileDialogTest : SysuiTestCase() { assertThat( bluetoothTileDialog.requireViewById<View>(R.id.scroll_view).layoutParams.height ) - .isEqualTo(MATCH_PARENT) + .isGreaterThan(MATCH_PARENT) + } + } + + @Test + fun testShowDialog_bluetoothEnabled_autoOnToggleGone() { + testScope.runTest { + bluetoothTileDialog = + BluetoothTileDialog( + ENABLED, + BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), + MATCH_PARENT, + bluetoothTileDialogCallback, + dispatcher, + fakeSystemClock, + uiEventLogger, + logger, + mContext + ) + bluetoothTileDialog.show() + assertThat( + bluetoothTileDialog + .requireViewById<View>(R.id.bluetooth_auto_on_toggle_layout) + .visibility + ) + .isEqualTo(GONE) } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt index 98ac17b96c16..cb9f4b4e4810 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt @@ -17,20 +17,27 @@ package com.android.systemui.qs.tiles.dialog.bluetooth import android.content.SharedPreferences +import android.content.pm.UserInfo import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE import android.widget.LinearLayout import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.flags.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.nullable +import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -84,16 +91,36 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { private lateinit var scheduler: TestCoroutineScheduler private lateinit var dispatcher: CoroutineDispatcher private lateinit var testScope: TestScope + private lateinit var secureSettings: FakeSettings + private lateinit var userRepository: FakeUserRepository @Before fun setUp() { + mSetFlagsRule.enableFlags(Flags.FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE) scheduler = TestCoroutineScheduler() dispatcher = UnconfinedTestDispatcher(scheduler) testScope = TestScope(dispatcher) + secureSettings = FakeSettings() + userRepository = FakeUserRepository() + userRepository.setUserInfos(listOf(SYSTEM_USER)) + secureSettings.putIntForUser( + BluetoothAutoOnRepository.SETTING_NAME, + BluetoothAutoOnInteractor.ENABLED, + SYSTEM_USER_ID + ) bluetoothTileDialogViewModel = BluetoothTileDialogViewModel( deviceItemInteractor, bluetoothStateInteractor, + // TODO(b/316822488): Create FakeBluetoothAutoOnInteractor. + BluetoothAutoOnInteractor( + BluetoothAutoOnRepository( + secureSettings, + userRepository, + testScope.backgroundScope, + dispatcher + ) + ), mDialogTransitionAnimator, activityStarter, fakeSystemClock, @@ -174,4 +201,64 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { verify(activityStarter).postStartActivityDismissingKeyguard(any(), anyInt(), nullable()) } } + + @Test + fun testBuildUiProperties_bluetoothOn_shouldHideAutoOn() { + testScope.runTest { + val actual = + BluetoothTileDialogViewModel.UiProperties.build( + isBluetoothEnabled = true, + isAutoOnToggleFeatureAvailable = true + ) + assertThat(actual.autoOnToggleVisibility).isEqualTo(GONE) + } + } + + @Test + fun testBuildUiProperties_bluetoothOff_shouldShowAutoOn() { + testScope.runTest { + val actual = + BluetoothTileDialogViewModel.UiProperties.build( + isBluetoothEnabled = false, + isAutoOnToggleFeatureAvailable = true + ) + assertThat(actual.autoOnToggleVisibility).isEqualTo(VISIBLE) + } + } + + @Test + fun testBuildUiProperties_bluetoothOff_autoOnFeatureUnavailable_shouldHideAutoOn() { + testScope.runTest { + val actual = + BluetoothTileDialogViewModel.UiProperties.build( + isBluetoothEnabled = false, + isAutoOnToggleFeatureAvailable = false + ) + assertThat(actual.autoOnToggleVisibility).isEqualTo(GONE) + } + } + + @Test + fun testIsAutoOnToggleFeatureAvailable_flagOn_settingValueSet_returnTrue() { + testScope.runTest { + val actual = bluetoothTileDialogViewModel.isAutoOnToggleFeatureAvailable() + assertThat(actual).isTrue() + } + } + + @Test + fun testIsAutoOnToggleFeatureAvailable_flagOff_settingValueSet_returnFalse() { + testScope.runTest { + mSetFlagsRule.disableFlags(Flags.FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE) + + val actual = bluetoothTileDialogViewModel.isAutoOnToggleFeatureAvailable() + assertThat(actual).isFalse() + } + } + + companion object { + private const val SYSTEM_USER_ID = 0 + private val SYSTEM_USER = + UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt index 0c4bf8120d25..ab5e51c1b1a1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -207,6 +207,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { @Test fun testDragDownHelperCalledWhenDraggingDown() = testScope.runTest { + mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL) whenever(dragDownHelper.isDraggingDown).thenReturn(true) val now = SystemClock.elapsedRealtime() val ev = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, 0f, 0f, 0 /* meta */) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt index c2261211b339..4cc123407d83 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt @@ -26,6 +26,7 @@ import androidx.annotation.IdRes import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest +import com.android.systemui.Flags as AConfigFlags import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR import com.android.systemui.SysuiTestCase import com.android.systemui.fragments.FragmentHostManager @@ -400,6 +401,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { @Test fun testSplitShadeLayout_isAlignedToGuideline() { + mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL) enableSplitShade() underTest.updateResources() assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd).isEqualTo(R.id.qs_edge_guideline) @@ -409,6 +411,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { @Test fun testSinglePaneLayout_childrenHaveEqualMargins() { + mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL) disableSplitShade() underTest.updateResources() val qsStartMargin = getConstraintSetLayout(R.id.qs_frame).startMargin @@ -425,6 +428,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { @Test fun testSplitShadeLayout_childrenHaveInsideMarginsOfZero() { + mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL) enableSplitShade() underTest.updateResources() assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0) @@ -443,6 +447,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { @Test fun testLargeScreenLayout_refactorFlagOff_qsAndNotifsTopMarginIsOfHeaderHeightResource() { mSetFlagsRule.disableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR) + mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL) setLargeScreen() val largeScreenHeaderResourceHeight = 100 val largeScreenHeaderHelperHeight = 200 @@ -465,6 +470,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { @Test fun testLargeScreenLayout_refactorFlagOn_qsAndNotifsTopMarginIsOfHeaderHeightHelper() { mSetFlagsRule.enableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR) + mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL) setLargeScreen() val largeScreenHeaderResourceHeight = 100 val largeScreenHeaderHelperHeight = 200 @@ -486,6 +492,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { @Test fun testSmallScreenLayout_qsAndNotifsTopMarginIsZero() { + mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL) setSmallScreen() underTest.updateResources() assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin).isEqualTo(0) @@ -506,6 +513,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { @Test fun testSinglePaneShadeLayout_isAlignedToParent() { + mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL) disableSplitShade() underTest.updateResources() assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd) 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/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index ff882b19ab13..9055ba4e1c4e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -138,7 +138,8 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { configurationRepository.onAnyConfigurationChange() - assertThat(dimens!!.paddingTop).isEqualTo(30) + // Should directly use the header height (flagged off value) + assertThat(dimens!!.paddingTop).isEqualTo(10) } @Test @@ -154,7 +155,8 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { configurationRepository.onAnyConfigurationChange() - assertThat(dimens!!.paddingTop).isEqualTo(40) + // Should directly use the header height (flagged on value) + assertThat(dimens!!.paddingTop).isEqualTo(5) } @Test @@ -456,8 +458,8 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { ) runCurrent() - // Top should be equal to bounds (1) + padding adjustment (30) - assertThat(bounds).isEqualTo(NotificationContainerBounds(top = 31f, bottom = 2f)) + // Top should be equal to bounds (1) - padding adjustment (10) + assertThat(bounds).isEqualTo(NotificationContainerBounds(top = -9f, bottom = 2f)) } @Test @@ -483,8 +485,8 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { ) runCurrent() - // Top should be equal to bounds (1) + padding adjustment (40) - assertThat(bounds).isEqualTo(NotificationContainerBounds(top = 41f, bottom = 2f)) + // Top should be equal to bounds (1) - padding adjustment (5) + assertThat(bounds).isEqualTo(NotificationContainerBounds(top = -4f, bottom = 2f)) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java index bbf9a6b93201..38698f86c659 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java @@ -45,6 +45,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; @SmallTest @RunWith(AndroidTestingRunner.class) @@ -80,6 +81,7 @@ public class KeyguardClockPositionAlgorithmTest extends SysuiTestCase { public void setUp() { MockitoAnnotations.initMocks(this); mStaticMockSession = mockitoSession() + .strictness(Strictness.WARN) .mockStatic(BurnInHelperKt.class) .mockStatic(LargeScreenHeaderHelper.class) .startMocking(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java index b3708bad6917..41b959e98221 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java @@ -26,6 +26,7 @@ import android.testing.TestableLooper; import androidx.test.filters.SmallTest; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.flags.FeatureFlags; @@ -118,6 +119,7 @@ public class LegacyNotificationIconAreaControllerImplTest extends SysuiTestCase @Test public void testAppearResetsTranslation() { + mSetFlagsRule.disableFlags(Flags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL); mController.setupAodIcons(mAodIcons); when(mDozeParameters.shouldControlScreenOff()).thenReturn(false); mController.appearAodIcons(); 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/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt index 98be163bdf34..759235655eca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt @@ -25,6 +25,7 @@ import androidx.test.filters.SmallTest import com.android.server.notification.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.settings.FakeGlobalSettings import com.android.systemui.util.time.FakeSystemClock import org.junit.Before import org.junit.Test @@ -49,6 +50,7 @@ class SensitiveNotificationProtectionControllerFlagDisabledTest : SysuiTestCase( controller = SensitiveNotificationProtectionControllerImpl( mContext, + FakeGlobalSettings(), mediaProjectionManager, activityManager, handler, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt index a1aff48ae1b7..1dac642836c6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt @@ -22,6 +22,7 @@ import android.app.Notification import android.media.projection.MediaProjectionInfo import android.media.projection.MediaProjectionManager import android.platform.test.annotations.EnableFlags +import android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS import android.service.notification.StatusBarNotification import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper @@ -33,6 +34,7 @@ import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.mockExecutorHandler import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.withArgCaptor +import com.android.systemui.util.settings.FakeGlobalSettings import com.android.systemui.util.time.FakeSystemClock import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull @@ -61,6 +63,8 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { @Mock private lateinit var listener2: Runnable @Mock private lateinit var listener3: Runnable + private lateinit var executor: FakeExecutor + private lateinit var globalSettings: FakeGlobalSettings private lateinit var mediaProjectionCallback: MediaProjectionManager.Callback private lateinit var controller: SensitiveNotificationProtectionControllerImpl @@ -73,18 +77,19 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { whenever(activityManager.bugreportWhitelistedPackages) .thenReturn(listOf(BUGREPORT_PACKAGE_NAME)) - val executor = FakeExecutor(FakeSystemClock()) - + executor = FakeExecutor(FakeSystemClock()) + globalSettings = FakeGlobalSettings() controller = SensitiveNotificationProtectionControllerImpl( mContext, + globalSettings, mediaProjectionManager, activityManager, mockExecutorHandler(executor), executor ) - // Process exemption processing + // Process pending work (getting global setting and list of exemptions) executor.runAllReady() // Obtain useful MediaProjectionCallback @@ -229,6 +234,14 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { } @Test + fun isSensitiveStateActive_projectionActive_disabledViaDevOption_false() { + setDisabledViaDeveloperOption() + mediaProjectionCallback.onStart(mediaProjectionInfo) + + assertFalse(controller.isSensitiveStateActive) + } + + @Test fun shouldProtectNotification_projectionInactive_false() { val notificationEntry = mock(NotificationEntry::class.java) @@ -294,6 +307,23 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { assertFalse(controller.shouldProtectNotification(notificationEntry)) } + @Test + fun shouldProtectNotification_projectionActive_disabledViaDevOption_false() { + setDisabledViaDeveloperOption() + mediaProjectionCallback.onStart(mediaProjectionInfo) + + val notificationEntry = setupNotificationEntry(TEST_PROJECTION_PACKAGE_NAME) + + assertFalse(controller.shouldProtectNotification(notificationEntry)) + } + + private fun setDisabledViaDeveloperOption() { + globalSettings.putInt(DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, 1) + + // Process pending work that gets current developer option global setting + executor.runAllReady() + } + private fun setShareFullScreen() { whenever(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME) whenever(mediaProjectionInfo.launchCookie).thenReturn(null) diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt new file mode 100644 index 000000000000..7c36a85243a2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt @@ -0,0 +1,267 @@ +/* + * 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.systemui.surfaceeffects.loadingeffect + +import android.graphics.Paint +import android.graphics.RenderEffect +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.model.SysUiStateTest +import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState +import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState.EASE_IN +import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState.EASE_OUT +import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState.MAIN +import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState.NOT_PLAYING +import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationStateChangedCallback +import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.PaintDrawCallback +import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.RenderEffectDrawCallback +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class LoadingEffectTest : SysUiStateTest() { + + private val fakeSystemClock = FakeSystemClock() + private val fakeExecutor = FakeExecutor(fakeSystemClock) + + @Test + fun play_paintCallback_triggersDrawCallback() { + var paintFromCallback: Paint? = null + val drawCallback = + object : PaintDrawCallback { + override fun onDraw(loadingPaint: Paint) { + paintFromCallback = loadingPaint + } + } + val loadingEffect = + LoadingEffect( + baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE, + TurbulenceNoiseAnimationConfig(), + paintCallback = drawCallback, + animationStateChangedCallback = null + ) + + fakeExecutor.execute { + assertThat(paintFromCallback).isNull() + + loadingEffect.play() + fakeSystemClock.advanceTime(500L) + + assertThat(paintFromCallback).isNotNull() + } + } + + @Test + fun play_renderEffectCallback_triggersDrawCallback() { + var renderEffectFromCallback: RenderEffect? = null + val drawCallback = + object : RenderEffectDrawCallback { + override fun onDraw(loadingRenderEffect: RenderEffect) { + renderEffectFromCallback = loadingRenderEffect + } + } + val loadingEffect = + LoadingEffect( + baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE, + TurbulenceNoiseAnimationConfig(), + renderEffectCallback = drawCallback, + animationStateChangedCallback = null + ) + + fakeExecutor.execute { + assertThat(renderEffectFromCallback).isNull() + + loadingEffect.play() + fakeSystemClock.advanceTime(500L) + + assertThat(renderEffectFromCallback).isNotNull() + } + } + + @Test + fun play_animationStateChangesInOrder() { + val config = TurbulenceNoiseAnimationConfig() + val expectedStates = arrayOf(NOT_PLAYING, EASE_IN, MAIN, EASE_OUT, NOT_PLAYING) + val actualStates = mutableListOf(NOT_PLAYING) + val stateChangedCallback = + object : AnimationStateChangedCallback { + override fun onStateChanged(oldState: AnimationState, newState: AnimationState) { + actualStates.add(newState) + } + } + val drawCallback = + object : PaintDrawCallback { + override fun onDraw(loadingPaint: Paint) {} + } + val loadingEffect = + LoadingEffect( + baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE, + config, + paintCallback = drawCallback, + stateChangedCallback + ) + + val timeToAdvance = + config.easeInDuration + config.maxDuration + config.easeOutDuration + 100 + + fakeExecutor.execute { + loadingEffect.play() + + fakeSystemClock.advanceTime(timeToAdvance.toLong()) + + assertThat(actualStates).isEqualTo(expectedStates) + } + } + + @Test + fun play_alreadyPlaying_playsOnlyOnce() { + val config = TurbulenceNoiseAnimationConfig() + var numPlay = 0 + val stateChangedCallback = + object : AnimationStateChangedCallback { + override fun onStateChanged(oldState: AnimationState, newState: AnimationState) { + if (oldState == NOT_PLAYING && newState == EASE_IN) { + numPlay++ + } + } + } + val drawCallback = + object : PaintDrawCallback { + override fun onDraw(loadingPaint: Paint) {} + } + val loadingEffect = + LoadingEffect( + baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE, + config, + paintCallback = drawCallback, + stateChangedCallback + ) + + fakeExecutor.execute { + assertThat(numPlay).isEqualTo(0) + + loadingEffect.play() + loadingEffect.play() + loadingEffect.play() + loadingEffect.play() + loadingEffect.play() + + assertThat(numPlay).isEqualTo(1) + } + } + + @Test + fun finish_finishesLoadingEffect() { + val config = TurbulenceNoiseAnimationConfig(maxDuration = 1000f) + val drawCallback = + object : PaintDrawCallback { + override fun onDraw(loadingPaint: Paint) {} + } + var isFinished = false + val stateChangedCallback = + object : AnimationStateChangedCallback { + override fun onStateChanged(oldState: AnimationState, newState: AnimationState) { + if (oldState == MAIN && newState == NOT_PLAYING) { + isFinished = true + } + } + } + val loadingEffect = + LoadingEffect( + baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE, + config, + paintCallback = drawCallback, + stateChangedCallback + ) + + fakeExecutor.execute { + assertThat(isFinished).isFalse() + + loadingEffect.play() + fakeSystemClock.advanceTime(config.easeInDuration.toLong() + 500L) + + assertThat(isFinished).isFalse() + + loadingEffect.finish() + + assertThat(isFinished).isTrue() + } + } + + @Test + fun finish_notMainState_hasNoEffect() { + val config = TurbulenceNoiseAnimationConfig(maxDuration = 1000f) + val drawCallback = + object : PaintDrawCallback { + override fun onDraw(loadingPaint: Paint) {} + } + var isFinished = false + val stateChangedCallback = + object : AnimationStateChangedCallback { + override fun onStateChanged(oldState: AnimationState, newState: AnimationState) { + if (oldState == MAIN && newState == NOT_PLAYING) { + isFinished = true + } + } + } + val loadingEffect = + LoadingEffect( + baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE, + config, + paintCallback = drawCallback, + stateChangedCallback + ) + + fakeExecutor.execute { + assertThat(isFinished).isFalse() + + loadingEffect.finish() + + assertThat(isFinished).isFalse() + } + } + + @Test + fun getNoiseOffset_returnsNoiseOffset() { + val expectedNoiseOffset = arrayOf(0.1f, 0.2f, 0.3f) + val config = + TurbulenceNoiseAnimationConfig( + noiseOffsetX = expectedNoiseOffset[0], + noiseOffsetY = expectedNoiseOffset[1], + noiseOffsetZ = expectedNoiseOffset[2] + ) + val drawCallback = + object : PaintDrawCallback { + override fun onDraw(loadingPaint: Paint) {} + } + val loadingEffect = + LoadingEffect( + baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE, + config, + paintCallback = drawCallback, + animationStateChangedCallback = null + ) + + assertThat(loadingEffect.getNoiseOffset()).isEqualTo(expectedNoiseOffset) + } +} diff --git a/ravenwood/mockito/Android.bp b/ravenwood/mockito/Android.bp index a74bca47f692..95c7394b19f3 100644 --- a/ravenwood/mockito/Android.bp +++ b/ravenwood/mockito/Android.bp @@ -13,6 +13,9 @@ android_ravenwood_test { srcs: [ "test/**/*.java", ], + exclude_srcs: [ + "test/**/*DeviceOnly*.java", + ], static_libs: [ "junit", "truth", @@ -31,6 +34,9 @@ android_test { srcs: [ "test/**/*.java", ], + exclude_srcs: [ + "test/**/*RavenwoodOnly*.java", + ], static_libs: [ "junit", "truth", diff --git a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoDeviceOnlyTest.java b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoDeviceOnlyTest.java new file mode 100644 index 000000000000..d02fe69d3168 --- /dev/null +++ b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoDeviceOnlyTest.java @@ -0,0 +1,46 @@ +/* + * 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.ravenwood.mockito; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.ActivityManager; +import android.platform.test.ravenwood.RavenwoodRule; + +import com.android.dx.mockito.inline.extended.ExtendedMockito; + +import org.junit.Rule; +import org.junit.Test; +import org.mockito.quality.Strictness; + +public class RavenwoodMockitoDeviceOnlyTest { + @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule(); + + @Test + public void testStaticMockOnDevice() { + var mockingSession = ExtendedMockito.mockitoSession() + .strictness(Strictness.LENIENT) + .mockStatic(ActivityManager.class) + .startMocking(); + try { + ExtendedMockito.doReturn(true).when(ActivityManager::isUserAMonkey); + + assertThat(ActivityManager.isUserAMonkey()).isEqualTo(true); + } finally { + mockingSession.finishMocking(); + } + } +} diff --git a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoRavenwoodOnlyTest.java b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoRavenwoodOnlyTest.java new file mode 100644 index 000000000000..0c137d5eaacc --- /dev/null +++ b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoRavenwoodOnlyTest.java @@ -0,0 +1,38 @@ +/* + * 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.ravenwood.mockito; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.ActivityManager; +import android.platform.test.ravenwood.RavenwoodRule; + +import org.junit.Rule; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +public class RavenwoodMockitoRavenwoodOnlyTest { + @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule(); + + @Test + public void testStaticMockOnRavenwood() { + try (MockedStatic<ActivityManager> am = Mockito.mockStatic(ActivityManager.class)) { + am.when(ActivityManager::isUserAMonkey).thenReturn(true); + assertThat(ActivityManager.isUserAMonkey()).isEqualTo(true); + } + } +} diff --git a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java index 1284d64b9a90..95667103bd21 100644 --- a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java +++ b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java @@ -31,28 +31,6 @@ import org.junit.Test; public class RavenwoodMockitoTest { @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule(); - -// Use this to mock static methods, which isn't supported by mockito 2. -// Mockito supports static mocking since 3.4.0: -// See: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#48 - -// private MockitoSession mMockingSession; -// -// @Before -// public void setUp() { -// mMockingSession = mockitoSession() -// .strictness(Strictness.LENIENT) -// .mockStatic(RavenwoodMockitoTest.class) -// .startMocking(); -// } -// -// @After -// public void tearDown() { -// if (mMockingSession != null) { -// mMockingSession.finishMocking(); -// } -// } - @Test public void testMockJdkClass() { Process object = mock(Process.class); diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt index 01c0074e463f..927ddd71dd15 100644 --- a/ravenwood/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/ravenwood-annotation-allowed-classes.txt @@ -13,11 +13,13 @@ com.android.internal.os.BatteryStatsHistoryIterator com.android.internal.os.Clock com.android.internal.os.LongArrayMultiStateCounter com.android.internal.os.LongArrayMultiStateCounter$LongArrayContainer +com.android.internal.os.LongMultiStateCounter com.android.internal.os.MonotonicClock com.android.internal.os.PowerProfile com.android.internal.os.PowerStats com.android.internal.os.PowerStats$Descriptor com.android.internal.os.RuntimeInit +com.android.internal.power.EnergyConsumerStats com.android.internal.power.ModemPowerProfile android.util.AtomicFile @@ -54,6 +56,7 @@ android.os.BatteryUsageStats android.os.BatteryUsageStatsQuery android.os.Binder android.os.Binder$IdentitySupplier +android.os.BluetoothBatteryStats android.os.Broadcaster android.os.Build android.os.BundleMerger @@ -83,12 +86,15 @@ android.os.TestLooperManager android.os.ThreadLocalWorkSource android.os.TimestampedValue android.os.Trace +android.os.UserBatteryConsumer +android.os.UserBatteryConsumer$Builder android.os.UidBatteryConsumer android.os.UidBatteryConsumer$Builder android.os.UserHandle android.os.UserManager android.os.VibrationAttributes android.os.VibrationAttributes$Builder +android.os.WakeLockStats android.os.WorkSource android.content.ClipData @@ -159,6 +165,7 @@ android.app.Instrumentation android.metrics.LogMaker +android.view.Display android.view.Display$HdrCapabilities android.view.Display$Mode android.view.DisplayInfo @@ -169,7 +176,6 @@ android.telephony.ModemActivityInfo android.telephony.ServiceState com.android.server.LocalServices -com.android.server.power.stats.BatteryStatsImpl com.android.internal.util.BitUtils com.android.internal.util.BitwiseInputStream @@ -192,6 +198,7 @@ com.android.internal.util.QuickSelect com.android.internal.util.RingBuffer com.android.internal.util.StringPool +com.android.internal.os.BackgroundThread com.android.internal.os.BinderCallHeavyHitterWatcher com.android.internal.os.BinderDeathDispatcher com.android.internal.os.BinderfsStatsReader diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index b64c74e69817..af47ed28e3b0 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -42,10 +42,12 @@ import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.AccessibilityTrace; import android.accessibilityservice.IAccessibilityServiceClient; import android.accessibilityservice.IAccessibilityServiceConnection; +import android.accessibilityservice.IBrailleDisplayController; import android.accessibilityservice.MagnificationConfig; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; @@ -58,6 +60,7 @@ import android.graphics.Region; import android.hardware.HardwareBuffer; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerInternal; +import android.hardware.usb.UsbDevice; import android.os.Binder; import android.os.Build; import android.os.Bundle; @@ -2776,4 +2779,23 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ t.close(); mOverlays.clear(); } + + @Override + @SuppressLint("AndroidFrameworkRequiresPermission") // Unsupported in Abstract class + public void connectBluetoothBrailleDisplay(String bluetoothAddress, + IBrailleDisplayController controller) { + throw new UnsupportedOperationException(); + } + + @Override + public void connectUsbBrailleDisplay(UsbDevice usbDevice, + IBrailleDisplayController controller) { + throw new UnsupportedOperationException(); + } + + @Override + @SuppressLint("AndroidFrameworkRequiresPermission") // Unsupported in Abstract class + public void setTestBrailleDisplayData(List<Bundle> brailleDisplays) { + throw new UnsupportedOperationException(); + } } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java index 5ebe16115a4b..b90a66a24442 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java @@ -26,16 +26,25 @@ import android.Manifest; import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.AccessibilityTrace; +import android.accessibilityservice.BrailleDisplayController; import android.accessibilityservice.IAccessibilityServiceClient; +import android.accessibilityservice.IBrailleDisplayController; import android.accessibilityservice.TouchInteractionController; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.PendingIntent; +import android.bluetooth.BluetoothAdapter; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ParceledListSlice; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; import android.os.Binder; +import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; @@ -44,6 +53,7 @@ import android.os.RemoteException; import android.os.Trace; import android.os.UserHandle; import android.provider.Settings; +import android.text.TextUtils; import android.util.Slog; import android.view.Display; import android.view.MotionEvent; @@ -55,6 +65,8 @@ import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.WindowManagerInternal; import java.lang.ref.WeakReference; +import java.util.List; +import java.util.Objects; import java.util.Set; /** @@ -82,6 +94,9 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect final Intent mIntent; final ActivityTaskManagerInternal mActivityTaskManagerService; + private BrailleDisplayConnection mBrailleDisplayConnection; + private List<Bundle> mTestBrailleDisplays = null; + private final Handler mMainHandler; private static final class AccessibilityInputMethodSessionCallback @@ -448,6 +463,16 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect } } + @Override + public void resetLocked() { + super.resetLocked(); + if (android.view.accessibility.Flags.brailleDisplayHid()) { + if (mBrailleDisplayConnection != null) { + mBrailleDisplayConnection.disconnect(); + } + } + } + public boolean isAccessibilityButtonAvailableLocked(AccessibilityUserState userState) { // If the service does not request the accessibility button, it isn't available if (!mRequestAccessibilityButton) { @@ -640,4 +665,123 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect } } } + + private void checkAccessibilityAccessLocked() { + if (!hasRightsToCurrentUserLocked() + || !mSecurityPolicy.checkAccessibilityAccess(this)) { + throw new SecurityException("Caller does not have accessibility access"); + } + } + + /** + * Sets up a BrailleDisplayConnection interface for the requested Bluetooth-connected + * Braille display. + * + * @param bluetoothAddress The address from + * {@link android.bluetooth.BluetoothDevice#getAddress()}. + */ + @Override + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + public void connectBluetoothBrailleDisplay( + @NonNull String bluetoothAddress, @NonNull IBrailleDisplayController controller) { + if (!android.view.accessibility.Flags.brailleDisplayHid()) { + throw new IllegalStateException("Flag BRAILLE_DISPLAY_HID not enabled"); + } + Objects.requireNonNull(bluetoothAddress); + Objects.requireNonNull(controller); + mContext.enforceCallingPermission(Manifest.permission.BLUETOOTH_CONNECT, + "Missing BLUETOOTH_CONNECT permission"); + if (!BluetoothAdapter.checkBluetoothAddress(bluetoothAddress)) { + throw new IllegalArgumentException( + bluetoothAddress + " is not a valid Bluetooth address"); + } + synchronized (mLock) { + checkAccessibilityAccessLocked(); + if (mBrailleDisplayConnection != null) { + throw new IllegalStateException( + "This service already has a connected Braille display"); + } + BrailleDisplayConnection connection = new BrailleDisplayConnection(mLock, this); + if (mTestBrailleDisplays != null) { + connection.setTestData(mTestBrailleDisplays); + } + connection.connectLocked( + bluetoothAddress, BrailleDisplayConnection.BUS_BLUETOOTH, controller); + } + } + + /** + * Sets up a BrailleDisplayConnection interface for the requested USB-connected + * Braille display. + * + * <p>The caller package must already have USB permission for this {@link UsbDevice}. + */ + @SuppressLint("MissingPermission") // system_server has the required MANAGE_USB permission + @Override + @NonNull + public void connectUsbBrailleDisplay(@NonNull UsbDevice usbDevice, + @NonNull IBrailleDisplayController controller) { + if (!android.view.accessibility.Flags.brailleDisplayHid()) { + throw new IllegalStateException("Flag BRAILLE_DISPLAY_HID not enabled"); + } + Objects.requireNonNull(usbDevice); + Objects.requireNonNull(controller); + final UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE); + final String usbSerialNumber; + final int uid = Binder.getCallingUid(); + final int pid = Binder.getCallingPid(); + final long identity = Binder.clearCallingIdentity(); + try { + if (usbManager == null || !usbManager.hasPermission( + usbDevice, mComponentName.getPackageName(), /*pid=*/ pid, /*uid=*/ uid)) { + throw new SecurityException( + "Caller does not have permission to access this UsbDevice"); + } + usbSerialNumber = usbDevice.getSerialNumber(); + if (TextUtils.isEmpty(usbSerialNumber)) { + // If the UsbDevice does not report a serial number for locating the HIDRAW + // node then notify connection error ERROR_BRAILLE_DISPLAY_NOT_FOUND. + try { + controller.onConnectionFailed(BrailleDisplayController.BrailleDisplayCallback + .FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND); + } catch (RemoteException e) { + Slog.e(LOG_TAG, "Error calling onConnectionFailed", e); + } + return; + } + } finally { + Binder.restoreCallingIdentity(identity); + } + synchronized (mLock) { + checkAccessibilityAccessLocked(); + if (mBrailleDisplayConnection != null) { + throw new IllegalStateException( + "This service already has a connected Braille display"); + } + BrailleDisplayConnection connection = new BrailleDisplayConnection(mLock, this); + if (mTestBrailleDisplays != null) { + connection.setTestData(mTestBrailleDisplays); + } + connection.connectLocked( + usbSerialNumber, BrailleDisplayConnection.BUS_USB, controller); + } + } + + @Override + @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY) + public void setTestBrailleDisplayData(List<Bundle> brailleDisplays) { + // Enforce that this TestApi is only called by trusted (test) callers. + mContext.enforceCallingPermission(Manifest.permission.MANAGE_ACCESSIBILITY, + "Missing MANAGE_ACCESSIBILITY permission"); + mTestBrailleDisplays = brailleDisplays; + } + + void onBrailleDisplayConnectedLocked(BrailleDisplayConnection connection) { + mBrailleDisplayConnection = connection; + } + + // Reset state when the BrailleDisplayConnection object disconnects itself. + void onBrailleDisplayDisconnectedLocked() { + mBrailleDisplayConnection = null; + } } diff --git a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java new file mode 100644 index 000000000000..1f18e15bb646 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java @@ -0,0 +1,534 @@ +/* + * 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.accessibility; + +import static android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback.FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND; +import static android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback.FLAG_ERROR_CANNOT_ACCESS; + +import android.accessibilityservice.BrailleDisplayController; +import android.accessibilityservice.IBrailleDisplayConnection; +import android.accessibilityservice.IBrailleDisplayController; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.PermissionManuallyEnforced; +import android.annotation.RequiresNoPermission; +import android.bluetooth.BluetoothDevice; +import android.hardware.usb.UsbDevice; +import android.os.Bundle; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Process; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Pair; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; + +/** + * This class represents the connection between {@code system_server} and a connected + * Braille Display using the Braille Display HID standard (usage page 0x41). + */ +class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub { + private static final String LOG_TAG = "BrailleDisplayConnection"; + + /** + * Represents the connection type of a Braille display. + * + * <p>The integer values must match the kernel's bus type values because this bus type is + * used to locate the correct HIDRAW node using data from the kernel. These values come + * from the UAPI header file bionic/libc/kernel/uapi/linux/input.h, which is guaranteed + * to stay constant. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, prefix = {"BUS_"}, value = { + BUS_UNKNOWN, + BUS_USB, + BUS_BLUETOOTH + }) + @interface BusType { + } + static final int BUS_UNKNOWN = -1; + static final int BUS_USB = 0x03; + static final int BUS_BLUETOOTH = 0x05; + + // Access to this static object must be guarded by a lock that is shared for all instances + // of this class: the singular Accessibility system_server lock (mLock). + private static final Set<File> sConnectedNodes = new ArraySet<>(); + + // Used to guard to AIDL methods from concurrent calls. + // Lock must match the one used by AccessibilityServiceConnection, which itself + // comes from AccessibilityManagerService. + private final Object mLock; + private final AccessibilityServiceConnection mServiceConnection; + + + private File mHidrawNode; + private IBrailleDisplayController mController; + + private Thread mInputThread; + private OutputStream mOutputStream; + private HandlerThread mOutputThread; + + // mScanner is not final because tests may modify this to use a test-only scanner. + private BrailleDisplayScanner mScanner; + + BrailleDisplayConnection(@NonNull Object lock, + @NonNull AccessibilityServiceConnection serviceConnection) { + this.mLock = Objects.requireNonNull(lock); + this.mScanner = getDefaultNativeScanner(getDefaultNativeInterface()); + this.mServiceConnection = Objects.requireNonNull(serviceConnection); + } + + /** + * Interface to scan for properties of connected Braille displays. + * + * <p>Helps simplify testing Braille Display APIs using test data without requiring + * a real Braille display to be connected to the device, by using a test implementation + * of this interface. + * + * @see #getDefaultNativeScanner + * @see #setTestData + */ + @VisibleForTesting + interface BrailleDisplayScanner { + Collection<Path> getHidrawNodePaths(); + + byte[] getDeviceReportDescriptor(@NonNull Path path); + + String getUniqueId(@NonNull Path path); + + @BusType + int getDeviceBusType(@NonNull Path path); + } + + /** + * Finds the Braille display HIDRAW node associated with the provided unique ID. + * + * <p>If found, saves instance state for this connection and starts a thread to + * read from the Braille display. + * + * @param expectedUniqueId The expected unique ID of the device to connect, from + * {@link UsbDevice#getSerialNumber()} + * or {@link BluetoothDevice#getAddress()} + * @param expectedBusType The expected bus type from {@link BusType}. + * @param controller Interface containing oneway callbacks used to communicate with the + * {@link android.accessibilityservice.BrailleDisplayController}. + */ + void connectLocked( + @NonNull String expectedUniqueId, + @BusType int expectedBusType, + @NonNull IBrailleDisplayController controller) { + Objects.requireNonNull(expectedUniqueId); + this.mController = Objects.requireNonNull(controller); + + final List<Pair<File, byte[]>> result = new ArrayList<>(); + final Collection<Path> hidrawNodePaths = mScanner.getHidrawNodePaths(); + if (hidrawNodePaths == null) { + Slog.w(LOG_TAG, "Unable to access the HIDRAW node directory"); + sendConnectionErrorLocked(FLAG_ERROR_CANNOT_ACCESS); + return; + } + boolean unableToGetDescriptor = false; + // For every present HIDRAW device node: + for (Path path : hidrawNodePaths) { + final byte[] descriptor = mScanner.getDeviceReportDescriptor(path); + if (descriptor == null) { + unableToGetDescriptor = true; + continue; + } + final String uniqueId = mScanner.getUniqueId(path); + if (isBrailleDisplay(descriptor) + && mScanner.getDeviceBusType(path) == expectedBusType + && expectedUniqueId.equalsIgnoreCase(uniqueId)) { + result.add(Pair.create(path.toFile(), descriptor)); + } + } + + // Return success only when exactly one matching device node is found. + if (result.size() != 1) { + @BrailleDisplayController.BrailleDisplayCallback.ErrorCode int errorCode = + FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND; + // If we were unable to get some /dev/hidraw* descriptor then tell the accessibility + // service that the device may not have proper access to these device nodes. + if (unableToGetDescriptor) { + Slog.w(LOG_TAG, "Unable to access some HIDRAW node's descriptor"); + errorCode |= FLAG_ERROR_CANNOT_ACCESS; + } else { + Slog.w(LOG_TAG, + "Unable to find a unique Braille display matching the provided device"); + } + sendConnectionErrorLocked(errorCode); + return; + } + + this.mHidrawNode = result.get(0).first; + final byte[] reportDescriptor = result.get(0).second; + + // Only one connection instance should exist for this hidraw node, across + // all currently running accessibility services. + if (sConnectedNodes.contains(this.mHidrawNode)) { + Slog.w(LOG_TAG, + "Unable to find an unused Braille display matching the provided device"); + sendConnectionErrorLocked(FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND); + return; + } + sConnectedNodes.add(this.mHidrawNode); + + startReadingLocked(); + + try { + mServiceConnection.onBrailleDisplayConnectedLocked(this); + mController.onConnected(this, reportDescriptor); + } catch (RemoteException e) { + Slog.e(LOG_TAG, "Error calling onConnected", e); + disconnect(); + } + } + + private void sendConnectionErrorLocked( + @BrailleDisplayController.BrailleDisplayCallback.ErrorCode int errorCode) { + try { + mController.onConnectionFailed(errorCode); + } catch (RemoteException e) { + Slog.e(LOG_TAG, "Error calling onConnectionFailed", e); + } + } + + /** Returns true if this descriptor includes usages for the Braille display usage page 0x41. */ + private static boolean isBrailleDisplay(byte[] descriptor) { + // TODO: b/316036493 - Check that descriptor includes 0x41 reports. + return true; + } + + /** + * Checks that the AccessibilityService that owns this BrailleDisplayConnection + * is still connected to the system. + * + * @throws IllegalStateException if not connected + */ + private void assertServiceIsConnectedLocked() { + if (!mServiceConnection.isConnectedLocked()) { + throw new IllegalStateException("Accessibility service is not connected"); + } + } + + /** + * Disconnects from this Braille display. This object is no longer valid after + * this call returns. + */ + @Override + // This is a cleanup method, so allow the call even if the calling service was disabled. + @RequiresNoPermission + public void disconnect() { + synchronized (mLock) { + closeInputLocked(); + closeOutputLocked(); + mServiceConnection.onBrailleDisplayDisconnectedLocked(); + try { + mController.onDisconnected(); + } catch (RemoteException e) { + Slog.e(LOG_TAG, "Error calling onDisconnected"); + } + sConnectedNodes.remove(this.mHidrawNode); + } + } + + /** + * Writes the provided HID bytes to this Braille display. + * + * <p>Writes are posted to a background thread handler. + * + * @param buffer The bytes to write to the Braille display. These bytes should be formatted + * according to the report descriptor. + */ + @Override + @PermissionManuallyEnforced // by assertServiceIsConnectedLocked() + public void write(@NonNull byte[] buffer) { + Objects.requireNonNull(buffer); + if (buffer.length > IBinder.getSuggestedMaxIpcSizeBytes()) { + Slog.e(LOG_TAG, "Requested write of size " + buffer.length + + " which is larger than maximum " + IBinder.getSuggestedMaxIpcSizeBytes()); + return; + } + synchronized (mLock) { + assertServiceIsConnectedLocked(); + if (mOutputThread == null) { + try { + mOutputStream = new FileOutputStream(mHidrawNode); + } catch (FileNotFoundException e) { + Slog.e(LOG_TAG, "Unable to create write stream", e); + disconnect(); + return; + } + mOutputThread = new HandlerThread("BrailleDisplayConnection output thread", + Process.THREAD_PRIORITY_BACKGROUND); + mOutputThread.setDaemon(true); + mOutputThread.start(); + } + // TODO: b/316035785 - Proactively disconnect a misbehaving Braille display by calling + // disconnect() if the mOutputThread handler queue grows too large. + mOutputThread.getThreadHandler().post(() -> { + try { + mOutputStream.write(buffer); + } catch (IOException e) { + Slog.d(LOG_TAG, "Error writing to connected Braille display", e); + disconnect(); + } + }); + } + } + + /** + * Starts reading HID bytes from this Braille display. + * + * <p>Reads are performed on a background thread. + */ + private void startReadingLocked() { + mInputThread = new Thread(() -> { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + try (InputStream inputStream = new FileInputStream(mHidrawNode)) { + final byte[] buffer = new byte[IBinder.getSuggestedMaxIpcSizeBytes()]; + int readSize; + while (!Thread.interrupted()) { + if (!mHidrawNode.exists()) { + disconnect(); + break; + } + // Reading from the HIDRAW character device node will block + // until bytes are available. + readSize = inputStream.read(buffer); + if (readSize > 0) { + try { + // Send the input to the AccessibilityService. + mController.onInput(Arrays.copyOfRange(buffer, 0, readSize)); + } catch (RemoteException e) { + // Error communicating with the AccessibilityService. + Slog.e(LOG_TAG, "Error calling onInput", e); + disconnect(); + break; + } + } + } + } catch (IOException e) { + Slog.d(LOG_TAG, "Error reading from connected Braille display", e); + disconnect(); + } + }, "BrailleDisplayConnection input thread"); + mInputThread.setDaemon(true); + mInputThread.start(); + } + + /** Stop the Input thread. */ + private void closeInputLocked() { + if (mInputThread != null) { + mInputThread.interrupt(); + } + mInputThread = null; + } + + /** Stop the Output thread and close the Output stream. */ + private void closeOutputLocked() { + if (mOutputThread != null) { + mOutputThread.quit(); + } + mOutputThread = null; + if (mOutputStream != null) { + try { + mOutputStream.close(); + } catch (IOException e) { + Slog.e(LOG_TAG, "Unable to close output stream", e); + } + } + mOutputStream = null; + } + + /** + * Returns a {@link BrailleDisplayScanner} that opens {@link FileInputStream}s to read + * from HIDRAW nodes and perform ioctls using the provided {@link NativeInterface}. + */ + @VisibleForTesting + BrailleDisplayScanner getDefaultNativeScanner(@NonNull NativeInterface nativeInterface) { + Objects.requireNonNull(nativeInterface); + return new BrailleDisplayScanner() { + private static final Path DEVICE_DIR = Path.of("/dev"); + private static final String HIDRAW_DEVICE_GLOB = "hidraw*"; + + @Override + public Collection<Path> getHidrawNodePaths() { + final List<Path> result = new ArrayList<>(); + try (DirectoryStream<Path> hidrawNodePaths = Files.newDirectoryStream( + DEVICE_DIR, HIDRAW_DEVICE_GLOB)) { + for (Path path : hidrawNodePaths) { + result.add(path); + } + return result; + } catch (IOException e) { + return null; + } + } + + private <T> T readFromFileDescriptor(Path path, Function<Integer, T> readFn) { + try (FileInputStream stream = new FileInputStream(path.toFile())) { + return readFn.apply(stream.getFD().getInt$()); + } catch (IOException e) { + return null; + } + } + + @Override + public byte[] getDeviceReportDescriptor(@NonNull Path path) { + Objects.requireNonNull(path); + return readFromFileDescriptor(path, fd -> { + final int descSize = nativeInterface.getHidrawDescSize(fd); + if (descSize > 0) { + return nativeInterface.getHidrawDesc(fd, descSize); + } + return null; + }); + } + + @Override + public String getUniqueId(@NonNull Path path) { + Objects.requireNonNull(path); + return readFromFileDescriptor(path, nativeInterface::getHidrawUniq); + } + + @Override + public int getDeviceBusType(@NonNull Path path) { + Objects.requireNonNull(path); + Integer busType = readFromFileDescriptor(path, nativeInterface::getHidrawBusType); + return busType != null ? busType : BUS_UNKNOWN; + } + }; + } + + /** + * Sets test data to be used by CTS tests. + * + * <p>Replaces the default {@link BrailleDisplayScanner} object for this connection, + * and also returns it to allow unit testing this test-only implementation. + * + * @see BrailleDisplayController#setTestBrailleDisplayData + */ + BrailleDisplayScanner setTestData(@NonNull List<Bundle> brailleDisplays) { + Objects.requireNonNull(brailleDisplays); + final Map<Path, Bundle> brailleDisplayMap = new ArrayMap<>(); + for (Bundle brailleDisplay : brailleDisplays) { + Path hidrawNodePath = Path.of(brailleDisplay.getString( + BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH)); + brailleDisplayMap.put(hidrawNodePath, brailleDisplay); + } + synchronized (mLock) { + mScanner = new BrailleDisplayScanner() { + @Override + public Collection<Path> getHidrawNodePaths() { + return brailleDisplayMap.keySet(); + } + + @Override + public byte[] getDeviceReportDescriptor(@NonNull Path path) { + return brailleDisplayMap.get(path).getByteArray( + BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR); + } + + @Override + public String getUniqueId(@NonNull Path path) { + return brailleDisplayMap.get(path).getString( + BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID); + } + + @Override + public int getDeviceBusType(@NonNull Path path) { + return brailleDisplayMap.get(path).getBoolean( + BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH) + ? BUS_BLUETOOTH : BUS_USB; + } + }; + return mScanner; + } + } + + /** + * This interface exists to support unit testing {@link #getDefaultNativeScanner}. + */ + @VisibleForTesting + interface NativeInterface { + int getHidrawDescSize(int fd); + + byte[] getHidrawDesc(int fd, int descSize); + + String getHidrawUniq(int fd); + + int getHidrawBusType(int fd); + } + + /** Native interface that actually calls native HIDRAW ioctls. */ + private NativeInterface getDefaultNativeInterface() { + return new NativeInterface() { + @Override + public int getHidrawDescSize(int fd) { + return nativeGetHidrawDescSize(fd); + } + + @Override + public byte[] getHidrawDesc(int fd, int descSize) { + return nativeGetHidrawDesc(fd, descSize); + } + + @Override + public String getHidrawUniq(int fd) { + return nativeGetHidrawUniq(fd); + } + + @Override + public int getHidrawBusType(int fd) { + return nativeGetHidrawBusType(fd); + } + }; + } + + private native int nativeGetHidrawDescSize(int fd); + + private native byte[] nativeGetHidrawDesc(int fd, int descSize); + + private native String nativeGetHidrawUniq(int fd); + + private native int nativeGetHidrawBusType(int fd); +} diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java index f619ca3f66a2..44d0132ef9a8 100644 --- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java +++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java @@ -16,6 +16,8 @@ package com.android.server; +import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS; + import static com.android.internal.util.Preconditions.checkNotNull; import android.annotation.NonNull; @@ -24,11 +26,10 @@ import android.content.ComponentName; import android.content.Context; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionManager; -import android.os.Handler; -import android.os.Looper; import android.os.RemoteException; import android.os.Trace; import android.os.UserHandle; +import android.provider.Settings; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.RankingMap; import android.service.notification.StatusBarNotification; @@ -117,7 +118,7 @@ public final class SensitiveContentProtectionManagerService extends SystemServic // TODO(b/317250444): use MediaProjectionManagerService directly, reduces unnecessary // handler, delegate, and binder death recipient - mProjectionManager.addCallback(mProjectionCallback, new Handler(Looper.getMainLooper())); + mProjectionManager.addCallback(mProjectionCallback, getContext().getMainThreadHandler()); try { mNotificationListener.registerAsSystemService( @@ -148,6 +149,15 @@ public final class SensitiveContentProtectionManagerService extends SystemServic } private void onProjectionStart() { + // TODO(b/324447419): move GlobalSettings lookup to background thread + boolean disableScreenShareProtections = + Settings.Global.getInt(getContext().getContentResolver(), + DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, 0) != 0; + if (disableScreenShareProtections) { + Log.w(TAG, "Screen share protections disabled, ignoring projection start"); + return; + } + synchronized (mSensitiveContentProtectionLock) { mProjectionActive = true; updateAppsThatShouldBlockScreenCapture(); diff --git a/services/core/java/com/android/server/audio/AudioServiceEvents.java b/services/core/java/com/android/server/audio/AudioServiceEvents.java index 3417f6501459..3b1c011f09c0 100644 --- a/services/core/java/com/android/server/audio/AudioServiceEvents.java +++ b/services/core/java/com/android/server/audio/AudioServiceEvents.java @@ -259,6 +259,7 @@ public class AudioServiceEvents { /** used for VOL_ADJUST_VOL_UID, * VOL_ADJUST_SUGG_VOL, * VOL_ADJUST_STREAM_VOL, + * VOL_SET_LE_AUDIO_VOL */ VolumeEvent(int op, int stream, int val1, int val2, String caller) { mOp = op; @@ -434,6 +435,8 @@ public class AudioServiceEvents { .set(MediaMetrics.Property.EVENT, "setLeAudioVolume") .set(MediaMetrics.Property.INDEX, mVal1) .set(MediaMetrics.Property.MAX_INDEX, mVal2) + .set(MediaMetrics.Property.STREAM_TYPE, + AudioSystem.streamToString(mStream)) .record(); return; case VOL_SET_AVRCP_VOL: @@ -519,7 +522,8 @@ public class AudioServiceEvents { .append(" gain dB:").append(mVal2) .toString(); case VOL_SET_LE_AUDIO_VOL: - return new StringBuilder("setLeAudioVolume:") + return new StringBuilder("setLeAudioVolume(stream:") + .append(AudioSystem.streamToString(mStream)) .append(" index:").append(mVal1) .append(" maxIndex:").append(mVal2) .toString(); diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index f51043dc1cdc..0f3f8073edcc 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -470,7 +470,8 @@ public class BtHelper { + index + " volume=" + volume); } AudioService.sVolumeLogger.enqueue(new AudioServiceEvents.VolumeEvent( - AudioServiceEvents.VolumeEvent.VOL_SET_LE_AUDIO_VOL, index, maxIndex)); + AudioServiceEvents.VolumeEvent.VOL_SET_LE_AUDIO_VOL, streamType, index, + maxIndex, /*caller=*/null)); try { mLeAudio.setVolume(volume); } catch (Exception e) { diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMap.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMap.java new file mode 100644 index 000000000000..f2185163824f --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMap.java @@ -0,0 +1,155 @@ +/* + * 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.inputmethod; + +import android.annotation.AnyThread; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.ArrayMap; +import android.view.inputmethod.InputMethodSubtype; + +import java.util.Collection; +import java.util.List; + +/** + * An on-memory immutable data representation of subtype.xml, which contains so-called additional + * {@link InputMethodSubtype}. + * + * <p>While the data structure could be also used for general purpose map from IME ID to + * a list of {@link InputMethodSubtype}, unlike {@link InputMethodMap} this particular data + * structure is currently used only around additional {@link InputMethodSubtype}, which is why this + * class is (still) called {@code AdditionalSubtypeMap} rather than {@code InputMethodSubtypeMap}. + * </p> + */ +final class AdditionalSubtypeMap { + /** + * An empty {@link AdditionalSubtypeMap}. + */ + static final AdditionalSubtypeMap EMPTY_MAP = new AdditionalSubtypeMap(new ArrayMap<>()); + + @NonNull + private final ArrayMap<String, List<InputMethodSubtype>> mMap; + + @AnyThread + @NonNull + private static AdditionalSubtypeMap createOrEmpty( + @NonNull ArrayMap<String, List<InputMethodSubtype>> map) { + return map.isEmpty() ? EMPTY_MAP : new AdditionalSubtypeMap(map); + } + + /** + * Create a new instance from the given {@link ArrayMap}. + * + * <p>This method effectively creates a new copy of map.</p> + * + * @param map An {@link ArrayMap} from which {@link AdditionalSubtypeMap} is to be created. + * @return A {@link AdditionalSubtypeMap} that contains a new copy of {@code map}. + */ + @AnyThread + @NonNull + static AdditionalSubtypeMap of(@NonNull ArrayMap<String, List<InputMethodSubtype>> map) { + return createOrEmpty(map); + } + + /** + * Create a new instance of {@link AdditionalSubtypeMap} from an existing + * {@link AdditionalSubtypeMap} by removing {@code key}, or return {@code map} itself if it does + * not contain an entry of {@code key}. + * + * @param key The key to be removed from {@code map}. + * @return A new instance of {@link AdditionalSubtypeMap}, which is guaranteed to not contain + * {@code key}, or {@code map} itself if it does not contain an entry of {@code key}. + */ + @AnyThread + @NonNull + AdditionalSubtypeMap cloneWithRemoveOrSelf(@NonNull String key) { + if (isEmpty() || !containsKey(key)) { + return this; + } + final ArrayMap<String, List<InputMethodSubtype>> newMap = new ArrayMap<>(mMap); + newMap.remove(key); + return createOrEmpty(newMap); + } + + /** + * Create a new instance of {@link AdditionalSubtypeMap} from an existing + * {@link AdditionalSubtypeMap} by removing {@code keys} or return {@code map} itself if it does + * not contain any entry for {@code keys}. + * + * @param keys Keys to be removed from {@code map}. + * @return A new instance of {@link AdditionalSubtypeMap}, which is guaranteed to not contain + * {@code keys}, or {@code map} itself if it does not contain any entry of {@code keys}. + */ + @AnyThread + @NonNull + AdditionalSubtypeMap cloneWithRemoveOrSelf(@NonNull Collection<String> keys) { + if (isEmpty()) { + return this; + } + final ArrayMap<String, List<InputMethodSubtype>> newMap = new ArrayMap<>(mMap); + return newMap.removeAll(keys) ? createOrEmpty(newMap) : this; + } + + /** + * Create a new instance of {@link AdditionalSubtypeMap} from an existing + * {@link AdditionalSubtypeMap} by putting {@code key} and {@code value}. + * + * @param key Key to be put into {@code map}. + * @param value Value to be put into {@code map}. + * @return A new instance of {@link AdditionalSubtypeMap}, which is guaranteed to contain the + * pair of {@code key} and {@code value}. + */ + @AnyThread + @NonNull + AdditionalSubtypeMap cloneWithPut( + @Nullable String key, @NonNull List<InputMethodSubtype> value) { + final ArrayMap<String, List<InputMethodSubtype>> newMap = new ArrayMap<>(mMap); + newMap.put(key, value); + return new AdditionalSubtypeMap(newMap); + } + + private AdditionalSubtypeMap(@NonNull ArrayMap<String, List<InputMethodSubtype>> map) { + mMap = map; + } + + @AnyThread + @Nullable + List<InputMethodSubtype> get(@Nullable String key) { + return mMap.get(key); + } + + @AnyThread + boolean containsKey(@Nullable String key) { + return mMap.containsKey(key); + } + + @AnyThread + boolean isEmpty() { + return mMap.isEmpty(); + } + + @AnyThread + @NonNull + Collection<String> keySet() { + return mMap.keySet(); + } + + @AnyThread + int size() { + return mMap.size(); + } +} diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java index fba71fd0ff9e..146ce1732070 100644 --- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java +++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java @@ -108,12 +108,12 @@ final class AdditionalSubtypeUtils { * multiple threads are not calling this method at the same time for the same {@code userId}. * </p> * - * @param allSubtypes {@link ArrayMap} from IME ID to additional subtype list. Passing an empty - * map deletes the file. + * @param allSubtypes {@link AdditionalSubtypeMap} from IME ID to additional subtype list. + * Passing an empty map deletes the file. * @param methodMap {@link ArrayMap} from IME ID to {@link InputMethodInfo}. * @param userId The user ID to be associated with. */ - static void save(ArrayMap<String, List<InputMethodSubtype>> allSubtypes, + static void save(AdditionalSubtypeMap allSubtypes, InputMethodMap methodMap, @UserIdInt int userId) { final File inputMethodDir = getInputMethodDir(userId); @@ -142,7 +142,7 @@ final class AdditionalSubtypeUtils { } @VisibleForTesting - static void saveToFile(ArrayMap<String, List<InputMethodSubtype>> allSubtypes, + static void saveToFile(AdditionalSubtypeMap allSubtypes, InputMethodMap methodMap, AtomicFile subtypesFile) { // Safety net for the case that this function is called before methodMap is set. final boolean isSetMethodMap = methodMap != null && methodMap.size() > 0; @@ -212,24 +212,21 @@ final class AdditionalSubtypeUtils { * multiple threads are not calling this method at the same time for the same {@code userId}. * </p> * - * @param allSubtypes {@link ArrayMap} from IME ID to additional subtype list. This parameter - * will be used to return the result. - * @param userId The user ID to be associated with. + * @param userId The user ID to be associated with. + * @return {@link AdditionalSubtypeMap} that contains the additional {@link InputMethodSubtype}. */ - static void load(@NonNull ArrayMap<String, List<InputMethodSubtype>> allSubtypes, - @UserIdInt int userId) { - allSubtypes.clear(); - + static AdditionalSubtypeMap load(@UserIdInt int userId) { final AtomicFile subtypesFile = getAdditionalSubtypeFile(getInputMethodDir(userId)); // Not having the file means there is no additional subtype. if (subtypesFile.exists()) { - loadFromFile(allSubtypes, subtypesFile); + return loadFromFile(subtypesFile); } + return AdditionalSubtypeMap.EMPTY_MAP; } @VisibleForTesting - static void loadFromFile(@NonNull ArrayMap<String, List<InputMethodSubtype>> allSubtypes, - AtomicFile subtypesFile) { + static AdditionalSubtypeMap loadFromFile(AtomicFile subtypesFile) { + final ArrayMap<String, List<InputMethodSubtype>> allSubtypes = new ArrayMap<>(); try (FileInputStream fis = subtypesFile.openRead()) { final TypedXmlPullParser parser = Xml.resolvePullParser(fis); int type = parser.next(); @@ -310,5 +307,6 @@ final class AdditionalSubtypeUtils { } catch (XmlPullParserException | IOException | NumberFormatException e) { Slog.w(TAG, "Error reading subtypes", e); } + return AdditionalSubtypeMap.of(allSubtypes); } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index ffdc9347369e..b8a63cd7941c 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -286,8 +286,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final InputManagerInternal mInputManagerInternal; final ImePlatformCompatUtils mImePlatformCompatUtils; final InputMethodDeviceConfigs mInputMethodDeviceConfigs; - private final ArrayMap<String, List<InputMethodSubtype>> mAdditionalSubtypeMap = - new ArrayMap<>(); + + @GuardedBy("ImfLock.class") + @NonNull + private AdditionalSubtypeMap mAdditionalSubtypeMap = AdditionalSubtypeMap.EMPTY_MAP; private final UserManagerInternal mUserManagerInternal; private final InputMethodMenuController mMenuController; @NonNull private final InputMethodBindingController mBindingController; @@ -1332,16 +1334,21 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Override public void onPackageDataCleared(String packageName, int uid) { synchronized (ImfLock.class) { - boolean changed = false; + // Note that one package may implement multiple IMEs. + final ArrayList<String> changedImes = new ArrayList<>(); for (InputMethodInfo imi : mSettings.getMethodList()) { if (imi.getPackageName().equals(packageName)) { - mAdditionalSubtypeMap.remove(imi.getId()); - changed = true; + changedImes.add(imi.getId()); } } - if (changed) { + final AdditionalSubtypeMap newMap = + mAdditionalSubtypeMap.cloneWithRemoveOrSelf(changedImes); + if (newMap != mAdditionalSubtypeMap) { + mAdditionalSubtypeMap = newMap; AdditionalSubtypeUtils.save( mAdditionalSubtypeMap, mSettings.getMethodMap(), mSettings.getUserId()); + } + if (!changedImes.isEmpty()) { mChangedPackages.add(packageName); } } @@ -1413,7 +1420,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub Slog.i(TAG, "Input method reinstalling, clearing additional subtypes: " + imi.getComponent()); - mAdditionalSubtypeMap.remove(imi.getId()); + mAdditionalSubtypeMap = + mAdditionalSubtypeMap.cloneWithRemoveOrSelf(imi.getId()); AdditionalSubtypeUtils.save(mAdditionalSubtypeMap, mSettings.getMethodMap(), mSettings.getUserId()); } @@ -1648,7 +1656,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // mSettings should be created before buildInputMethodListLocked mSettings = InputMethodSettings.createEmptyMap(userId); - AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId); + mAdditionalSubtypeMap = AdditionalSubtypeUtils.load(userId); mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(context, mSettings.getMethodMap(), userId); @@ -1783,7 +1791,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mSettings = InputMethodSettings.createEmptyMap(newUserId); // Additional subtypes should be reset when the user is changed - AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, newUserId); + mAdditionalSubtypeMap = AdditionalSubtypeUtils.load(newUserId); final String defaultImiId = mSettings.getSelectedInputMethod(); if (DEBUG) { @@ -2016,9 +2024,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub && directBootAwareness == DirectBootAwareness.AUTO) { settings = mSettings; } else { - final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = - new ArrayMap<>(); - AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); + final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeUtils.load(userId); settings = queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, directBootAwareness); } @@ -4218,10 +4224,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } if (mSettings.getUserId() == userId) { - if (!mSettings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, - mAdditionalSubtypeMap, mPackageManagerInternal, callingUid)) { + final var newAdditionalSubtypeMap = mSettings.getNewAdditionalSubtypeMap( + imiId, toBeAdded, mAdditionalSubtypeMap, mPackageManagerInternal, + callingUid); + if (mAdditionalSubtypeMap == newAdditionalSubtypeMap) { return; } + AdditionalSubtypeUtils.save(newAdditionalSubtypeMap, mSettings.getMethodMap(), + mSettings.getUserId()); + mAdditionalSubtypeMap = newAdditionalSubtypeMap; final long ident = Binder.clearCallingIdentity(); try { buildInputMethodListLocked(false /* resetDefaultEnabledIme */); @@ -4231,13 +4242,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return; } - final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = - new ArrayMap<>(); - AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); + final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeUtils.load(userId); final InputMethodSettings settings = queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO); - settings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, additionalSubtypeMap, - mPackageManagerInternal, callingUid); + final var newAdditionalSubtypeMap = settings.getNewAdditionalSubtypeMap( + imiId, toBeAdded, additionalSubtypeMap, mPackageManagerInternal, callingUid); + if (additionalSubtypeMap != newAdditionalSubtypeMap) { + AdditionalSubtypeUtils.save(newAdditionalSubtypeMap, settings.getMethodMap(), + settings.getUserId()); + } } } @@ -5072,7 +5085,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @NonNull static InputMethodSettings queryInputMethodServicesInternal(Context context, - @UserIdInt int userId, ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, + @UserIdInt int userId, @NonNull AdditionalSubtypeMap additionalSubtypeMap, @DirectBootAwareness int directBootAwareness) { final Context userAwareContext = context.getUserId() == userId ? context @@ -5112,7 +5125,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @NonNull static InputMethodMap filterInputMethodServices( - ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, + @NonNull AdditionalSubtypeMap additionalSubtypeMap, List<String> enabledInputMethodList, Context userAwareContext, List<ResolveInfo> services) { final ArrayMap<String, Integer> imiPackageCount = new ArrayMap<>(); @@ -5512,9 +5525,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (userId == mSettings.getUserId()) { settings = mSettings; } else { - final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = - new ArrayMap<>(); - AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); + final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeUtils.load(userId); settings = queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO); } @@ -5522,9 +5533,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } private InputMethodSettings queryMethodMapForUser(@UserIdInt int userId) { - final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = - new ArrayMap<>(); - AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); + final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeUtils.load(userId); return queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO); } @@ -6572,9 +6581,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub nextIme = mSettings.getSelectedInputMethod(); nextEnabledImes = mSettings.getEnabledInputMethodList(); } else { - final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = - new ArrayMap<>(); - AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); + final AdditionalSubtypeMap additionalSubtypeMap = + AdditionalSubtypeUtils.load(userId); final InputMethodSettings settings = queryInputMethodServicesInternal( mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java index e444db1b79e8..a558838172f8 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java @@ -24,7 +24,6 @@ import android.content.pm.PackageManagerInternal; import android.os.LocaleList; import android.provider.Settings; import android.text.TextUtils; -import android.util.ArrayMap; import android.util.IntArray; import android.util.Pair; import android.util.Printer; @@ -614,26 +613,27 @@ final class InputMethodSettings { explicitlyOrImplicitlyEnabledSubtypes, null, locale, true); } - boolean setAdditionalInputMethodSubtypes(@NonNull String imeId, + @NonNull + AdditionalSubtypeMap getNewAdditionalSubtypeMap(@NonNull String imeId, @NonNull ArrayList<InputMethodSubtype> subtypes, - @NonNull ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, + @NonNull AdditionalSubtypeMap additionalSubtypeMap, @NonNull PackageManagerInternal packageManagerInternal, int callingUid) { final InputMethodInfo imi = mMethodMap.get(imeId); if (imi == null) { - return false; + return additionalSubtypeMap; } if (!InputMethodUtils.checkIfPackageBelongsToUid(packageManagerInternal, callingUid, imi.getPackageName())) { - return false; + return additionalSubtypeMap; } + final AdditionalSubtypeMap newMap; if (subtypes.isEmpty()) { - additionalSubtypeMap.remove(imi.getId()); + newMap = additionalSubtypeMap.cloneWithRemoveOrSelf(imi.getId()); } else { - additionalSubtypeMap.put(imi.getId(), subtypes); + newMap = additionalSubtypeMap.cloneWithPut(imi.getId(), subtypes); } - AdditionalSubtypeUtils.save(additionalSubtypeMap, mMethodMap, getUserId()); - return true; + return newMap; } boolean setEnabledInputMethodSubtypes(@NonNull String imeId, 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/core/java/com/android/server/notification/ZenLog.java b/services/core/java/com/android/server/notification/ZenLog.java index 88d23ce3f9a1..82c5733655b0 100644 --- a/services/core/java/com/android/server/notification/ZenLog.java +++ b/services/core/java/com/android/server/notification/ZenLog.java @@ -28,6 +28,7 @@ import android.service.notification.IConditionProvider; import android.service.notification.NotificationListenerService; import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeDiff; +import android.util.LocalLog; import android.util.Log; import android.util.Slog; @@ -37,26 +38,16 @@ import java.util.Date; import java.util.List; public class ZenLog { - private static final String TAG = "ZenLog"; - // the ZenLog is *very* verbose, so be careful about setting this to true - private static final boolean DEBUG = false; private static final int SIZE = Build.IS_DEBUGGABLE ? 200 : 100; - private static final long[] TIMES = new long[SIZE]; - private static final int[] TYPES = new int[SIZE]; - private static final String[] MSGS = new String[SIZE]; - - private static final SimpleDateFormat FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); + private static final LocalLog STATE_CHANGES = new LocalLog(SIZE); + private static final LocalLog INTERCEPTION_EVENTS = new LocalLog(SIZE); private static final int TYPE_INTERCEPTED = 1; - private static final int TYPE_ALLOW_DISABLE = 2; private static final int TYPE_SET_RINGER_MODE_EXTERNAL = 3; private static final int TYPE_SET_RINGER_MODE_INTERNAL = 4; - private static final int TYPE_DOWNTIME = 5; private static final int TYPE_SET_ZEN_MODE = 6; - private static final int TYPE_UPDATE_ZEN_MODE = 7; - private static final int TYPE_EXIT_CONDITION = 8; private static final int TYPE_SUBSCRIBE = 9; private static final int TYPE_UNSUBSCRIBE = 10; private static final int TYPE_CONFIG = 11; @@ -71,9 +62,6 @@ public class ZenLog { private static final int TYPE_CHECK_REPEAT_CALLER = 20; private static final int TYPE_ALERT_ON_UPDATED_INTERCEPT = 21; - private static int sNext; - private static int sSize; - public static void traceIntercepted(NotificationRecord record, String reason) { append(TYPE_INTERCEPTED, record.getKey() + "," + reason); } @@ -104,10 +92,6 @@ public class ZenLog { ringerModeToString(ringerModeExternalOut)); } - public static void traceDowntimeAutotrigger(String result) { - append(TYPE_DOWNTIME, result); - } - public static void traceSetZenMode(int zenMode, String reason) { append(TYPE_SET_ZEN_MODE, zenModeToString(zenMode) + "," + reason); } @@ -120,21 +104,12 @@ public class ZenLog { append(TYPE_SET_CONSOLIDATED_ZEN_POLICY, policy.toString() + "," + reason); } - public static void traceUpdateZenMode(int fromMode, int toMode) { - append(TYPE_UPDATE_ZEN_MODE, zenModeToString(fromMode) + " -> " + zenModeToString(toMode)); - } - - public static void traceExitCondition(Condition c, ComponentName component, String reason) { - append(TYPE_EXIT_CONDITION, c + "," + componentToString(component) + "," + reason); - } public static void traceSetNotificationPolicy(String pkg, int targetSdk, NotificationManager.Policy policy) { String policyLog = "pkg=" + pkg + " targetSdk=" + targetSdk + " NotificationPolicy=" + policy.toString(); append(TYPE_SET_NOTIFICATION_POLICY, policyLog); - // TODO(b/180205791): remove when we can better surface apps that are changing policy - Log.d(TAG, "Zen Policy Changed: " + policyLog); } public static void traceSubscribe(Uri uri, IConditionProvider provider, RemoteException e) { @@ -145,13 +120,14 @@ public class ZenLog { append(TYPE_UNSUBSCRIBE, uri + "," + subscribeResult(provider, e)); } - public static void traceConfig(String reason, ZenModeConfig oldConfig, - ZenModeConfig newConfig) { + public static void traceConfig(String reason, ComponentName triggeringComponent, + ZenModeConfig oldConfig, ZenModeConfig newConfig, int callingUid) { ZenModeDiff.ConfigDiff diff = new ZenModeDiff.ConfigDiff(oldConfig, newConfig); if (diff == null || !diff.hasDiff()) { append(TYPE_CONFIG, reason + " no changes"); } else { append(TYPE_CONFIG, reason + + " - " + triggeringComponent + " : " + callingUid + ",\n" + (newConfig != null ? newConfig.toString() : null) + ",\n" + diff); } @@ -204,13 +180,9 @@ public class ZenLog { private static String typeToString(int type) { switch (type) { case TYPE_INTERCEPTED: return "intercepted"; - case TYPE_ALLOW_DISABLE: return "allow_disable"; case TYPE_SET_RINGER_MODE_EXTERNAL: return "set_ringer_mode_external"; case TYPE_SET_RINGER_MODE_INTERNAL: return "set_ringer_mode_internal"; - case TYPE_DOWNTIME: return "downtime"; case TYPE_SET_ZEN_MODE: return "set_zen_mode"; - case TYPE_UPDATE_ZEN_MODE: return "update_zen_mode"; - case TYPE_EXIT_CONDITION: return "exit_condition"; case TYPE_SUBSCRIBE: return "subscribe"; case TYPE_UNSUBSCRIBE: return "unsubscribe"; case TYPE_CONFIG: return "config"; @@ -278,30 +250,27 @@ public class ZenLog { } private static void append(int type, String msg) { - synchronized(MSGS) { - TIMES[sNext] = System.currentTimeMillis(); - TYPES[sNext] = type; - MSGS[sNext] = msg; - sNext = (sNext + 1) % SIZE; - if (sSize < SIZE) { - sSize++; + if (type == TYPE_INTERCEPTED || type == TYPE_NOT_INTERCEPTED + || type == TYPE_CHECK_REPEAT_CALLER || type == TYPE_RECORD_CALLER + || type == TYPE_MATCHES_CALL_FILTER || type == TYPE_ALERT_ON_UPDATED_INTERCEPT) { + synchronized (INTERCEPTION_EVENTS) { + INTERCEPTION_EVENTS.log(typeToString(type) + ": " +msg); + } + } else { + synchronized (STATE_CHANGES) { + STATE_CHANGES.log(typeToString(type) + ": " +msg); } } - if (DEBUG) Slog.d(TAG, typeToString(type) + ": " + msg); } public static void dump(PrintWriter pw, String prefix) { - synchronized(MSGS) { - final int start = (sNext - sSize + SIZE) % SIZE; - for (int i = 0; i < sSize; i++) { - final int j = (start + i) % SIZE; - pw.print(prefix); - pw.print(FORMAT.format(new Date(TIMES[j]))); - pw.print(' '); - pw.print(typeToString(TYPES[j])); - pw.print(": "); - pw.println(MSGS[j]); - } + synchronized (INTERCEPTION_EVENTS) { + pw.printf(prefix + "Interception Events:\n"); + INTERCEPTION_EVENTS.dump(prefix, pw); + } + synchronized (STATE_CHANGES) { + pw.printf(prefix + "State Changes:\n"); + STATE_CHANGES.dump(prefix, pw); } } } diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index aebd28a549ca..1c20b2df4283 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -1713,7 +1713,7 @@ public class ZenModeHelper { mConfigs.put(config.user, config); } if (DEBUG) Log.d(TAG, "setConfigLocked reason=" + reason, new Throwable()); - ZenLog.traceConfig(reason, mConfig, config); + ZenLog.traceConfig(reason, triggeringComponent, mConfig, config, callingUid); // send some broadcasts Policy newPolicy = getNotificationPolicy(config); diff --git a/services/core/java/com/android/server/pm/AppDataHelper.java b/services/core/java/com/android/server/pm/AppDataHelper.java index 79d17534ab26..1dd790502486 100644 --- a/services/core/java/com/android/server/pm/AppDataHelper.java +++ b/services/core/java/com/android/server/pm/AppDataHelper.java @@ -50,6 +50,7 @@ import com.android.server.pm.dex.ArtManagerService; import com.android.server.pm.parsing.pkg.AndroidPackageUtils; import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.PackageStateInternal; +import com.android.server.pm.pkg.PackageUserStateInternal; import com.android.server.pm.pkg.SELinuxUtil; import dalvik.system.VMRuntime; @@ -502,6 +503,7 @@ public class AppDataHelper { private void assertPackageStorageValid(@NonNull Computer snapshot, String volumeUuid, String packageName, int userId) throws PackageManagerException { final PackageStateInternal packageState = snapshot.getPackageStateInternal(packageName); + final PackageUserStateInternal userState = packageState.getUserStateOrDefault(userId); if (packageState == null) { throw PackageManagerException.ofInternalError("Package " + packageName + " is unknown", PackageManagerException.INTERNAL_ERROR_STORAGE_INVALID_PACKAGE_UNKNOWN); @@ -510,9 +512,10 @@ public class AppDataHelper { "Package " + packageName + " found on unknown volume " + volumeUuid + "; expected volume " + packageState.getVolumeUuid(), PackageManagerException.INTERNAL_ERROR_STORAGE_INVALID_VOLUME_UNKNOWN); - } else if (!packageState.getUserStateOrDefault(userId).isInstalled()) { + } else if (!userState.isInstalled() && !userState.dataExists()) { throw PackageManagerException.ofInternalError( - "Package " + packageName + " not installed for user " + userId, + "Package " + packageName + " not installed for user " + userId + + " or was deleted without DELETE_KEEP_DATA", PackageManagerException.INTERNAL_ERROR_STORAGE_INVALID_NOT_INSTALLED_FOR_USER); } else if (packageState.getPkg() != null && !shouldHaveAppStorage(packageState.getPkg())) { diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java index 3f00a9d999aa..7d902401a537 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java @@ -26,6 +26,7 @@ import android.content.pm.verify.domain.DomainVerificationInfo; import android.content.pm.verify.domain.DomainVerificationManager; import android.content.pm.verify.domain.DomainVerificationUserState; import android.content.pm.verify.domain.IDomainVerificationManager; +import android.os.Bundle; import android.os.ServiceSpecificException; import java.util.List; @@ -41,6 +42,27 @@ public class DomainVerificationManagerStub extends IDomainVerificationManager.St mService = service; } + @Override + public void setUriRelativeFilterGroups(@NonNull String packageName, + @NonNull Bundle domainToGroupsBundle) { + try { + mService.setUriRelativeFilterGroups(packageName, domainToGroupsBundle); + } catch (Exception e) { + throw rethrow(e); + } + } + + @NonNull + @Override + public Bundle getUriRelativeFilterGroups( + @NonNull String packageName, @NonNull List<String> domains) { + try { + return mService.getUriRelativeFilterGroups(packageName, domains); + } catch (Exception e) { + throw rethrow(e); + } + } + @NonNull @Override public List<String> queryValidVerificationPackageNames() { diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java index ac6d79541bc1..de464a4a42bb 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java @@ -19,6 +19,8 @@ package com.android.server.pm.verify.domain; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.content.UriRelativeFilter; +import android.content.UriRelativeFilterGroup; import android.content.pm.Signature; import android.content.pm.verify.domain.DomainVerificationState; import android.os.UserHandle; @@ -38,7 +40,10 @@ import com.android.server.pm.verify.domain.models.DomainVerificationStateMap; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; +import java.util.Iterator; +import java.util.List; import java.util.UUID; import java.util.function.Function; @@ -67,6 +72,13 @@ public class DomainVerificationPersistence { public static final String TAG_DOMAIN = "domain"; public static final String ATTR_NAME = "name"; public static final String ATTR_STATE = "state"; + public static final String TAG_URI_RELATIVE_FILTER_GROUPS = "uri-relative-filter-groups"; + public static final String TAG_URI_RELATIVE_FILTER_GROUP = "uri-relative-filter-group"; + public static final String ATTR_ACTION = "action"; + public static final String TAG_URI_RELATIVE_FILTER = "uri-relative-filter"; + public static final String ATTR_URI_PART = "uri-part"; + public static final String ATTR_PATTERN_TYPE = "pattern-type"; + public static final String ATTR_FILTER = "filter"; /** * @param pkgNameToSignature Converts package name to a string representation of its signature. @@ -176,6 +188,7 @@ public class DomainVerificationPersistence { final ArrayMap<String, Integer> stateMap = new ArrayMap<>(); final SparseArray<DomainVerificationInternalUserState> userStates = new SparseArray<>(); + final ArrayMap<String, List<UriRelativeFilterGroup>> groupMap = new ArrayMap<>(); SettingsXml.ChildSection child = section.children(); while (child.moveToNext()) { @@ -186,11 +199,47 @@ public class DomainVerificationPersistence { case TAG_USER_STATES: readUserStates(child, userStates); break; + case TAG_URI_RELATIVE_FILTER_GROUPS: + readUriRelativeFilterGroups(child, groupMap); + break; } } return new DomainVerificationPkgState(packageName, id, hasAutoVerifyDomains, stateMap, - userStates, signature); + userStates, signature, groupMap); + } + + private static void readUriRelativeFilterGroups(@NonNull SettingsXml.ReadSection section, + @NonNull ArrayMap<String, List<UriRelativeFilterGroup>> groupMap) { + SettingsXml.ChildSection child = section.children(); + while (child.moveToNext(TAG_DOMAIN)) { + String domain = child.getString(ATTR_NAME); + groupMap.put(domain, createUriRelativeFilterGroupsFromXml(child)); + } + } + + private static ArrayList<UriRelativeFilterGroup> createUriRelativeFilterGroupsFromXml( + @NonNull SettingsXml.ReadSection section) { + SettingsXml.ChildSection child = section.children(); + ArrayList<UriRelativeFilterGroup> groups = new ArrayList<>(); + while (child.moveToNext(TAG_URI_RELATIVE_FILTER_GROUP)) { + UriRelativeFilterGroup group = new UriRelativeFilterGroup(section.getInt(ATTR_ACTION)); + readUriRelativeFiltersFromXml(child, group); + groups.add(group); + } + return groups; + } + + private static void readUriRelativeFiltersFromXml( + @NonNull SettingsXml.ReadSection section, UriRelativeFilterGroup group) { + SettingsXml.ChildSection child = section.children(); + while (child.moveToNext(TAG_URI_RELATIVE_FILTER)) { + String filter = child.getString(ATTR_FILTER); + if (filter != null) { + group.addUriRelativeFilter(new UriRelativeFilter(child.getInt(ATTR_URI_PART), + child.getInt(ATTR_PATTERN_TYPE), filter)); + } + } } private static void readUserStates(@NonNull SettingsXml.ReadSection section, @@ -236,6 +285,7 @@ public class DomainVerificationPersistence { .attribute(ATTR_SIGNATURE, signature)) { writeStateMap(parentSection, pkgState.getStateMap()); writeUserStates(parentSection, userId, pkgState.getUserStates()); + writeUriRelativeFilterGroupMap(parentSection, pkgState.getUriRelativeFilterGroupMap()); } } @@ -334,6 +384,52 @@ public class DomainVerificationPersistence { } } + private static void writeUriRelativeFilterGroupMap( + @NonNull SettingsXml.WriteSection parentSection, + @NonNull ArrayMap<String, List<UriRelativeFilterGroup>> groupMap) throws IOException { + if (groupMap.isEmpty()) { + return; + } + try (SettingsXml.WriteSection section = + parentSection.startSection(TAG_URI_RELATIVE_FILTER_GROUPS)) { + for (int i = 0; i < groupMap.size(); i++) { + writeUriRelativeFilterGroups(section, groupMap.keyAt(i), groupMap.valueAt(i)); + } + } + } + + private static void writeUriRelativeFilterGroups( + @NonNull SettingsXml.WriteSection parentSection, @NonNull String domain, + @NonNull List<UriRelativeFilterGroup> groups) throws IOException { + if (groups.isEmpty()) { + return; + } + try (SettingsXml.WriteSection section = + parentSection.startSection(TAG_DOMAIN) + .attribute(ATTR_NAME, domain)) { + for (int i = 0; i < groups.size(); i++) { + writeUriRelativeFilterGroup(section, groups.get(i)); + } + } + } + + private static void writeUriRelativeFilterGroup( + @NonNull SettingsXml.WriteSection parentSection, + @NonNull UriRelativeFilterGroup group) throws IOException { + try (SettingsXml.WriteSection section = + parentSection.startSection(TAG_URI_RELATIVE_FILTER_GROUP) + .attribute(ATTR_ACTION, group.getAction())) { + Iterator<UriRelativeFilter> it = group.getUriRelativeFilters().iterator(); + while (it.hasNext()) { + UriRelativeFilter filter = it.next(); + section.startSection(TAG_URI_RELATIVE_FILTER) + .attribute(ATTR_URI_PART, filter.getUriPart()) + .attribute(ATTR_PATTERN_TYPE, filter.getPatternType()) + .attribute(ATTR_FILTER, filter.getFilter()).finish(); + } + } + } + public static class ReadResult { @NonNull diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java index c796b40f11bf..305b087190d6 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java @@ -19,14 +19,19 @@ package com.android.server.pm.verify.domain; import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; +import android.Manifest; import android.annotation.CheckResult; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.compat.annotation.ChangeId; import android.content.Context; import android.content.Intent; +import android.content.UriRelativeFilterGroup; +import android.content.UriRelativeFilterGroupParcel; +import android.content.pm.Flags; import android.content.pm.IntentFilterVerificationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; @@ -38,6 +43,8 @@ import android.content.pm.verify.domain.DomainVerificationManager; import android.content.pm.verify.domain.DomainVerificationState; import android.content.pm.verify.domain.DomainVerificationUserState; import android.content.pm.verify.domain.IDomainVerificationManager; +import android.net.Uri; +import android.os.Bundle; import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; @@ -223,6 +230,72 @@ public class DomainVerificationService extends SystemService mProxy = proxy; } + /** + * Update the URI relative filter groups for a package's verified domains. All previously + * existing groups will be cleared before the new groups will be applied. + */ + @RequiresPermission(Manifest.permission.DOMAIN_VERIFICATION_AGENT) + public void setUriRelativeFilterGroups(@NonNull String packageName, + @NonNull Bundle bundle) + throws NameNotFoundException { + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.DOMAIN_VERIFICATION_AGENT, + "Caller " + mConnection.getCallingUid() + " does not hold " + + android.Manifest.permission.DOMAIN_VERIFICATION_AGENT); + if (bundle.isEmpty()) { + return; + } + synchronized (mLock) { + DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName); + if (pkgState == null) { + throw DomainVerificationUtils.throwPackageUnavailable(packageName); + } + Map<String, List<UriRelativeFilterGroup>> domainToGroupsMap = + pkgState.getUriRelativeFilterGroupMap(); + for (String domain : bundle.keySet()) { + ArrayList<UriRelativeFilterGroupParcel> parcels = + bundle.getParcelableArrayList(domain, UriRelativeFilterGroupParcel.class); + domainToGroupsMap.put(domain, UriRelativeFilterGroup.parcelsToGroups(parcels)); + } + } + } + + /** + * Retrieve the current URI relative filter groups for a package's verified domain. + */ + @NonNull + public Bundle getUriRelativeFilterGroups(@NonNull String packageName, + @NonNull List<String> domains) { + Bundle bundle = new Bundle(); + synchronized (mLock) { + DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName); + if (pkgState != null) { + Map<String, List<UriRelativeFilterGroup>> map = + pkgState.getUriRelativeFilterGroupMap(); + for (int i = 0; i < domains.size(); i++) { + List<UriRelativeFilterGroup> groups = map.get(domains.get(i)); + bundle.putParcelableList(domains.get(i), + UriRelativeFilterGroup.groupsToParcels(groups)); + } + } + } + return bundle; + } + + @NonNull + private List<UriRelativeFilterGroup> getUriRelativeFilterGroups(@NonNull String packageName, + @NonNull String domain) { + List<UriRelativeFilterGroup> groups = Collections.emptyList(); + synchronized (mLock) { + DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName); + if (pkgState != null) { + groups = pkgState.getUriRelativeFilterGroupMap().getOrDefault(domain, + Collections.emptyList()); + } + } + return groups; + } + @NonNull public List<String> queryValidVerificationPackageNames() { mEnforcer.assertApprovedVerifier(mConnection.getCallingUid(), mProxy); @@ -891,6 +964,8 @@ public class DomainVerificationService extends SystemService } ArrayMap<String, Integer> oldStateMap = oldPkgState.getStateMap(); + ArrayMap<String, List<UriRelativeFilterGroup>> oldGroups = + oldPkgState.getUriRelativeFilterGroupMap(); ArraySet<String> newAutoVerifyDomains = mCollector.collectValidAutoVerifyDomains(newPkg); int newDomainsSize = newAutoVerifyDomains.size(); @@ -941,7 +1016,7 @@ public class DomainVerificationService extends SystemService mAttachedPkgStates.put(pkgName, newDomainSetId, new DomainVerificationPkgState( pkgName, newDomainSetId, hasAutoVerifyDomains, newStateMap, newUserStates, - null /* signature */)); + null /* signature */, oldGroups)); } if (sendBroadcast) { @@ -1572,8 +1647,6 @@ public class DomainVerificationService extends SystemService public Pair<List<ResolveInfo>, Integer> filterToApprovedApp(@NonNull Intent intent, @NonNull List<ResolveInfo> infos, @UserIdInt int userId, @NonNull Function<String, PackageStateInternal> pkgSettingFunction) { - String domain = intent.getData().getHost(); - // Collect valid infos ArrayMap<ResolveInfo, Integer> infoApprovals = new ArrayMap<>(); int infosSize = infos.size(); @@ -1586,7 +1659,7 @@ public class DomainVerificationService extends SystemService } // Find all approval levels - int highestApproval = fillMapWithApprovalLevels(infoApprovals, domain, userId, + int highestApproval = fillMapWithApprovalLevels(infoApprovals, intent.getData(), userId, pkgSettingFunction); if (highestApproval <= APPROVAL_LEVEL_NONE) { return Pair.create(emptyList(), highestApproval); @@ -1623,12 +1696,23 @@ public class DomainVerificationService extends SystemService return Pair.create(finalList, highestApproval); } + private boolean matchUriRelativeFilterGroups(Uri uri, String pkgName) { + if (uri.getHost() == null) { + return false; + } + List<UriRelativeFilterGroup> groups = getUriRelativeFilterGroups(pkgName, uri.getHost()); + if (groups.isEmpty()) { + return true; + } + return UriRelativeFilterGroup.matchGroupsToUri(groups, uri); + } + /** * @return highest approval level found */ @ApprovalLevel private int fillMapWithApprovalLevels(@NonNull ArrayMap<ResolveInfo, Integer> inputMap, - @NonNull String domain, @UserIdInt int userId, + @NonNull Uri uri, @UserIdInt int userId, @NonNull Function<String, PackageStateInternal> pkgSettingFunction) { int highestApproval = APPROVAL_LEVEL_NONE; int size = inputMap.size(); @@ -1641,12 +1725,13 @@ public class DomainVerificationService extends SystemService ResolveInfo info = inputMap.keyAt(index); final String packageName = info.getComponentInfo().packageName; PackageStateInternal pkgSetting = pkgSettingFunction.apply(packageName); - if (pkgSetting == null) { + if (pkgSetting == null || (Flags.relativeReferenceIntentFilters() + && !matchUriRelativeFilterGroups(uri, packageName))) { fillInfoMapForSamePackage(inputMap, packageName, APPROVAL_LEVEL_NONE); continue; } - int approval = approvalLevelForDomain(pkgSetting, domain, false, userId, DEBUG_APPROVAL, - domain); + int approval = approvalLevelForDomain(pkgSetting, uri.getHost(), false, userId, + DEBUG_APPROVAL, uri.getHost()); highestApproval = Math.max(highestApproval, approval); fillInfoMapForSamePackage(inputMap, packageName, approval); } diff --git a/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java b/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java index d71dbbb5d53b..46051fe3d782 100644 --- a/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java +++ b/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java @@ -19,6 +19,7 @@ package com.android.server.pm.verify.domain.models; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.content.UriRelativeFilterGroup; import android.content.pm.Signature; import android.content.pm.verify.domain.DomainVerificationState; import android.util.ArrayMap; @@ -26,6 +27,7 @@ import android.util.SparseArray; import com.android.internal.util.DataClass; +import java.util.List; import java.util.Objects; import java.util.UUID; @@ -77,15 +79,30 @@ public class DomainVerificationPkgState { @Nullable private final String mBackupSignatureHash; + /** + * List of {@link UriRelativeFilterGroup} for filtering domains. + */ + @NonNull + private final ArrayMap<String, List<UriRelativeFilterGroup>> mUriRelativeFilterGroupMap; + public DomainVerificationPkgState(@NonNull String packageName, @NonNull UUID id, boolean hasAutoVerifyDomains) { - this(packageName, id, hasAutoVerifyDomains, new ArrayMap<>(0), new SparseArray<>(0), null); + this(packageName, id, hasAutoVerifyDomains, new ArrayMap<>(0), new SparseArray<>(0), null, + new ArrayMap<>()); } public DomainVerificationPkgState(@NonNull DomainVerificationPkgState pkgState, @NonNull UUID id, boolean hasAutoVerifyDomains) { this(pkgState.getPackageName(), id, hasAutoVerifyDomains, pkgState.getStateMap(), - pkgState.getUserStates(), null); + pkgState.getUserStates(), null, new ArrayMap<>()); + } + + public DomainVerificationPkgState(@NonNull String packageName, @NonNull UUID id, + boolean hasAutoVerifyDomains, @NonNull ArrayMap<String, Integer> stateMap, + @NonNull SparseArray<DomainVerificationInternalUserState> userStates, + @Nullable String backupSignatureHash) { + this(packageName, id, hasAutoVerifyDomains, stateMap, userStates, backupSignatureHash, + new ArrayMap<>()); } @Nullable @@ -158,6 +175,8 @@ public class DomainVerificationPkgState { * * It's assumed the domain verification agent will eventually re-verify this domain * and revoke if necessary. + * @param uriRelativeFilterGroupMap + * List of {@link UriRelativeFilterGroup} for filtering domains. */ @DataClass.Generated.Member public DomainVerificationPkgState( @@ -166,7 +185,8 @@ public class DomainVerificationPkgState { boolean hasAutoVerifyDomains, @NonNull ArrayMap<String,Integer> stateMap, @NonNull SparseArray<DomainVerificationInternalUserState> userStates, - @Nullable String backupSignatureHash) { + @Nullable String backupSignatureHash, + @NonNull ArrayMap<String,List<UriRelativeFilterGroup>> uriRelativeFilterGroupMap) { this.mPackageName = packageName; com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mPackageName); @@ -181,6 +201,9 @@ public class DomainVerificationPkgState { com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mUserStates); this.mBackupSignatureHash = backupSignatureHash; + this.mUriRelativeFilterGroupMap = uriRelativeFilterGroupMap; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mUriRelativeFilterGroupMap); // onConstructed(); // You can define this method to get a callback } @@ -239,6 +262,14 @@ public class DomainVerificationPkgState { return mBackupSignatureHash; } + /** + * List of {@link UriRelativeFilterGroup} for filtering domains. + */ + @DataClass.Generated.Member + public @NonNull ArrayMap<String,List<UriRelativeFilterGroup>> getUriRelativeFilterGroupMap() { + return mUriRelativeFilterGroupMap; + } + @Override @DataClass.Generated.Member public String toString() { @@ -251,7 +282,8 @@ public class DomainVerificationPkgState { "hasAutoVerifyDomains = " + mHasAutoVerifyDomains + ", " + "stateMap = " + mStateMap + ", " + "userStates = " + mUserStates + ", " + - "backupSignatureHash = " + mBackupSignatureHash + + "backupSignatureHash = " + mBackupSignatureHash + ", " + + "uriRelativeFilterGroupMap = " + mUriRelativeFilterGroupMap + " }"; } @@ -273,7 +305,8 @@ public class DomainVerificationPkgState { && mHasAutoVerifyDomains == that.mHasAutoVerifyDomains && Objects.equals(mStateMap, that.mStateMap) && userStatesEquals(that.mUserStates) - && Objects.equals(mBackupSignatureHash, that.mBackupSignatureHash); + && Objects.equals(mBackupSignatureHash, that.mBackupSignatureHash) + && Objects.equals(mUriRelativeFilterGroupMap, that.mUriRelativeFilterGroupMap); } @Override @@ -289,14 +322,15 @@ public class DomainVerificationPkgState { _hash = 31 * _hash + Objects.hashCode(mStateMap); _hash = 31 * _hash + userStatesHashCode(); _hash = 31 * _hash + Objects.hashCode(mBackupSignatureHash); + _hash = 31 * _hash + Objects.hashCode(mUriRelativeFilterGroupMap); return _hash; } @DataClass.Generated( - time = 1617315369614L, + time = 1707351734724L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java", - inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate @android.annotation.NonNull java.util.UUID mId\nprivate final boolean mHasAutoVerifyDomains\nprivate final @android.annotation.NonNull android.util.ArrayMap<java.lang.String,java.lang.Integer> mStateMap\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState> mUserStates\nprivate final @android.annotation.Nullable java.lang.String mBackupSignatureHash\npublic @android.annotation.Nullable com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState getUserState(int)\npublic @android.annotation.Nullable com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState getOrCreateUserState(int)\npublic void removeUser(int)\npublic void removeAllUsers()\nprivate int userStatesHashCode()\nprivate boolean userStatesEquals(android.util.SparseArray<com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState>)\nclass DomainVerificationPkgState extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genToString=true, genEqualsHashCode=true)") + inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.util.UUID mId\nprivate final boolean mHasAutoVerifyDomains\nprivate final @android.annotation.NonNull android.util.ArrayMap<java.lang.String,java.lang.Integer> mStateMap\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState> mUserStates\nprivate final @android.annotation.Nullable java.lang.String mBackupSignatureHash\nprivate final @android.annotation.NonNull android.util.ArrayMap<java.lang.String,java.util.List<android.content.UriRelativeFilterGroup>> mUriRelativeFilterGroupMap\npublic @android.annotation.Nullable com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState getUserState(int)\npublic @android.annotation.Nullable com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState getOrCreateUserState(int)\npublic void removeUser(int)\npublic void removeAllUsers()\nprivate int userStatesHashCode()\nprivate boolean userStatesEquals(android.util.SparseArray<com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState>)\nclass DomainVerificationPkgState extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genToString=true, genEqualsHashCode=true)") @Deprecated private void __metadata() {} diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index 25e749f08782..00036e48891f 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -137,7 +137,6 @@ import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; -import com.android.net.module.util.NetworkCapabilitiesUtils; import com.android.server.power.optimization.Flags; import com.android.server.power.stats.SystemServerCpuThreadReader.SystemServiceCpuThreadTimes; @@ -1716,14 +1715,101 @@ public class BatteryStatsImpl extends BatteryStats { return mMaxLearnedBatteryCapacityUah; } + public class FrameworkStatsLogger { + public void uidProcessStateChanged(int uid, int state) { + // TODO(b/155216561): It is possible for isolated uids to be in a higher + // state than its parent uid. We should track the highest state within the union of host + // and isolated uids rather than only the parent uid. + FrameworkStatsLog.write(FrameworkStatsLog.UID_PROCESS_STATE_CHANGED, uid, + ActivityManager.processStateAmToProto(state)); + } + + public void wakelockStateChanged(int uid, WorkChain wc, String name, int type, + int procState, boolean acquired) { + int event = acquired + ? FrameworkStatsLog.WAKELOCK_STATE_CHANGED__STATE__ACQUIRE + : FrameworkStatsLog.WAKELOCK_STATE_CHANGED__STATE__RELEASE; + if (wc != null) { + FrameworkStatsLog.write(FrameworkStatsLog.WAKELOCK_STATE_CHANGED, wc.getUids(), + wc.getTags(), getPowerManagerWakeLockLevel(type), name, + event, procState); + } else { + FrameworkStatsLog.write_non_chained(FrameworkStatsLog.WAKELOCK_STATE_CHANGED, + mapIsolatedUid(uid), null, getPowerManagerWakeLockLevel(type), name, + event, procState); + } + } + + public void kernelWakeupReported(long deltaUptimeUs) { + FrameworkStatsLog.write(FrameworkStatsLog.KERNEL_WAKEUP_REPORTED, mLastWakeupReason, + /* duration_usec */ deltaUptimeUs, mLastWakeupElapsedTimeMs); + } + + public void gpsScanStateChanged(int uid, WorkChain workChain, boolean stateOn) { + int event = stateOn + ? FrameworkStatsLog.GPS_SCAN_STATE_CHANGED__STATE__ON + : FrameworkStatsLog.GPS_SCAN_STATE_CHANGED__STATE__OFF; + if (workChain != null) { + FrameworkStatsLog.write(FrameworkStatsLog.GPS_SCAN_STATE_CHANGED, + workChain.getUids(), workChain.getTags(), event); + } else { + FrameworkStatsLog.write_non_chained(FrameworkStatsLog.GPS_SCAN_STATE_CHANGED, + uid, null, event); + } + } + + public void batterySaverModeChanged(boolean enabled) { + FrameworkStatsLog.write(FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED, + enabled + ? FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__ON + : FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__OFF); + } + + public void deviceIdlingModeStateChanged(int mode) { + FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_IDLING_MODE_STATE_CHANGED, mode); + } + + public void deviceIdleModeStateChanged(int mode) { + FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_IDLE_MODE_STATE_CHANGED, mode); + } + + public void chargingStateChanged(int status) { + FrameworkStatsLog.write(FrameworkStatsLog.CHARGING_STATE_CHANGED, status); + } + + public void pluggedStateChanged(int plugType) { + FrameworkStatsLog.write(FrameworkStatsLog.PLUGGED_STATE_CHANGED, plugType); + } + + public void batteryLevelChanged(int level) { + FrameworkStatsLog.write(FrameworkStatsLog.BATTERY_LEVEL_CHANGED, level); + } + + public void phoneServiceStateChanged(int state, int simState, int strengthBin) { + FrameworkStatsLog.write(FrameworkStatsLog.PHONE_SERVICE_STATE_CHANGED, state, + simState, strengthBin); + } + + public void phoneSignalStrengthChanged(int strengthBin) { + FrameworkStatsLog.write( + FrameworkStatsLog.PHONE_SIGNAL_STRENGTH_CHANGED, strengthBin); + } + } + + private final FrameworkStatsLogger mFrameworkStatsLogger; + @VisibleForTesting public BatteryStatsImpl(Clock clock, File historyDirectory, @NonNull Handler handler, - @NonNull PowerStatsUidResolver powerStatsUidResolver) { + @NonNull PowerStatsUidResolver powerStatsUidResolver, + @NonNull FrameworkStatsLogger frameworkStatsLogger, + @NonNull BatteryStatsHistory.TraceDelegate traceDelegate, + @NonNull BatteryStatsHistory.EventLogger eventLogger) { mClock = clock; initKernelStatsReaders(); mBatteryStatsConfig = new BatteryStatsConfig.Builder().build(); mHandler = handler; mPowerStatsUidResolver = powerStatsUidResolver; + mFrameworkStatsLogger = frameworkStatsLogger; mConstants = new Constants(mHandler); mStartClockTimeMs = clock.currentTimeMillis(); mDailyFile = null; @@ -1732,12 +1818,14 @@ public class BatteryStatsImpl extends BatteryStats { mCheckinFile = null; mStatsFile = null; mHistory = new BatteryStatsHistory(mConstants.MAX_HISTORY_FILES, - mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock, mMonotonicClock); + mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock, mMonotonicClock, + traceDelegate, eventLogger); } else { mCheckinFile = new AtomicFile(new File(historyDirectory, "batterystats-checkin.bin")); mStatsFile = new AtomicFile(new File(historyDirectory, "batterystats.bin")); mHistory = new BatteryStatsHistory(historyDirectory, mConstants.MAX_HISTORY_FILES, - mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock, mMonotonicClock); + mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock, mMonotonicClock, + traceDelegate, eventLogger); } mPlatformIdleStateCallback = null; mEnergyConsumerRetriever = null; @@ -4269,7 +4357,7 @@ public class BatteryStatsImpl extends BatteryStats { } @GuardedBy("this") - private void updateBatteryPropertiesLocked() { + protected void updateBatteryPropertiesLocked() { try { IBatteryPropertiesRegistrar registrar = IBatteryPropertiesRegistrar.Stub.asInterface( ServiceManager.getService("batteryproperties")); @@ -4403,11 +4491,7 @@ public class BatteryStatsImpl extends BatteryStats { return; } } - // TODO(b/155216561): It is possible for isolated uids to be in a higher - // state than its parent uid. We should track the highest state within the union of host - // and isolated uids rather than only the parent uid. - FrameworkStatsLog.write(FrameworkStatsLog.UID_PROCESS_STATE_CHANGED, uid, - ActivityManager.processStateAmToProto(state)); + mFrameworkStatsLogger.uidProcessStateChanged(uid, state); getUidStatsLocked(parentUid, elapsedRealtimeMs, uptimeMs) .updateUidProcessStateLocked(state, elapsedRealtimeMs, uptimeMs); } @@ -4721,17 +4805,8 @@ public class BatteryStatsImpl extends BatteryStats { Uid uidStats = getUidStatsLocked(mappedUid, elapsedRealtimeMs, uptimeMs); uidStats.noteStartWakeLocked(pid, name, type, elapsedRealtimeMs); - int procState = uidStats.mProcessState; - - if (wc != null) { - FrameworkStatsLog.write(FrameworkStatsLog.WAKELOCK_STATE_CHANGED, wc.getUids(), - wc.getTags(), getPowerManagerWakeLockLevel(type), name, - FrameworkStatsLog.WAKELOCK_STATE_CHANGED__STATE__ACQUIRE, procState); - } else { - FrameworkStatsLog.write_non_chained(FrameworkStatsLog.WAKELOCK_STATE_CHANGED, - mapIsolatedUid(uid), null, getPowerManagerWakeLockLevel(type), name, - FrameworkStatsLog.WAKELOCK_STATE_CHANGED__STATE__ACQUIRE, procState); - } + mFrameworkStatsLogger.wakelockStateChanged(mapIsolatedUid(uid), wc, name, type, + uidStats.mProcessState, true /* acquired */); } } @@ -4774,16 +4849,8 @@ public class BatteryStatsImpl extends BatteryStats { Uid uidStats = getUidStatsLocked(mappedUid, elapsedRealtimeMs, uptimeMs); uidStats.noteStopWakeLocked(pid, name, type, elapsedRealtimeMs); - int procState = uidStats.mProcessState; - if (wc != null) { - FrameworkStatsLog.write(FrameworkStatsLog.WAKELOCK_STATE_CHANGED, wc.getUids(), - wc.getTags(), getPowerManagerWakeLockLevel(type), name, - FrameworkStatsLog.WAKELOCK_STATE_CHANGED__STATE__RELEASE, procState); - } else { - FrameworkStatsLog.write_non_chained(FrameworkStatsLog.WAKELOCK_STATE_CHANGED, - mapIsolatedUid(uid), null, getPowerManagerWakeLockLevel(type), name, - FrameworkStatsLog.WAKELOCK_STATE_CHANGED__STATE__RELEASE, procState); - } + mFrameworkStatsLogger.wakelockStateChanged(mapIsolatedUid(uid), wc, name, type, + uidStats.mProcessState, false /* acquired */); if (mappedUid != uid) { // Decrement the ref count for the isolated uid and delete the mapping if uneeded. @@ -5020,8 +5087,7 @@ public class BatteryStatsImpl extends BatteryStats { long deltaUptimeMs = uptimeMs - mLastWakeupUptimeMs; SamplingTimer timer = getWakeupReasonTimerLocked(mLastWakeupReason); timer.add(deltaUptimeMs * 1000, 1, elapsedRealtimeMs); // time in in microseconds - FrameworkStatsLog.write(FrameworkStatsLog.KERNEL_WAKEUP_REPORTED, mLastWakeupReason, - /* duration_usec */ deltaUptimeMs * 1000, mLastWakeupElapsedTimeMs); + mFrameworkStatsLogger.kernelWakeupReported(deltaUptimeMs * 1000); mLastWakeupReason = null; } } @@ -5159,14 +5225,7 @@ public class BatteryStatsImpl extends BatteryStats { } mGpsNesting++; - if (workChain == null) { - FrameworkStatsLog.write_non_chained(FrameworkStatsLog.GPS_SCAN_STATE_CHANGED, - mapIsolatedUid(uid), null, FrameworkStatsLog.GPS_SCAN_STATE_CHANGED__STATE__ON); - } else { - FrameworkStatsLog.write(FrameworkStatsLog.GPS_SCAN_STATE_CHANGED, - workChain.getUids(), workChain.getTags(), - FrameworkStatsLog.GPS_SCAN_STATE_CHANGED__STATE__ON); - } + mFrameworkStatsLogger.gpsScanStateChanged(mapIsolatedUid(uid), workChain, /* on */true); getUidStatsLocked(mappedUid, elapsedRealtimeMs, uptimeMs).noteStartGps(elapsedRealtimeMs); } @@ -5188,14 +5247,7 @@ public class BatteryStatsImpl extends BatteryStats { mGpsSignalQualityBin = -1; } - if (workChain == null) { - FrameworkStatsLog.write_non_chained(FrameworkStatsLog.GPS_SCAN_STATE_CHANGED, - mapIsolatedUid(uid), null, - FrameworkStatsLog.GPS_SCAN_STATE_CHANGED__STATE__OFF); - } else { - FrameworkStatsLog.write(FrameworkStatsLog.GPS_SCAN_STATE_CHANGED, workChain.getUids(), - workChain.getTags(), FrameworkStatsLog.GPS_SCAN_STATE_CHANGED__STATE__OFF); - } + mFrameworkStatsLogger.gpsScanStateChanged(mapIsolatedUid(uid), workChain, /* on */ false); getUidStatsLocked(mappedUid, elapsedRealtimeMs, uptimeMs).noteStopGps(elapsedRealtimeMs); } @@ -5673,10 +5725,7 @@ public class BatteryStatsImpl extends BatteryStats { } else { // Log an initial value for BATTERY_SAVER_MODE_STATE_CHANGED in order to // allow the atom to read all future state changes. - FrameworkStatsLog.write(FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED, - enabled - ? FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__ON - : FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__OFF); + mFrameworkStatsLogger.batterySaverModeChanged(enabled); } } @@ -5696,10 +5745,7 @@ public class BatteryStatsImpl extends BatteryStats { HistoryItem.STATE2_POWER_SAVE_FLAG); mPowerSaveModeEnabledTimer.stopRunningLocked(elapsedRealtimeMs); } - FrameworkStatsLog.write(FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED, - enabled - ? FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__ON - : FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__OFF); + mFrameworkStatsLogger.batterySaverModeChanged(enabled); } } @@ -5727,7 +5773,7 @@ public class BatteryStatsImpl extends BatteryStats { if (nowIdling) statsmode = DEVICE_IDLE_MODE_DEEP; else if (nowLightIdling) statsmode = DEVICE_IDLE_MODE_LIGHT; else statsmode = DEVICE_IDLE_MODE_OFF; - FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_IDLING_MODE_STATE_CHANGED, statsmode); + mFrameworkStatsLogger.deviceIdlingModeStateChanged(statsmode); } if (mDeviceIdling != nowIdling) { mDeviceIdling = nowIdling; @@ -5769,7 +5815,7 @@ public class BatteryStatsImpl extends BatteryStats { mDeviceIdleModeFullTimer.startRunningLocked(elapsedRealtimeMs); } mDeviceIdleMode = mode; - FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_IDLE_MODE_STATE_CHANGED, mode); + mFrameworkStatsLogger.deviceIdleModeStateChanged(mode); } } @@ -5933,8 +5979,7 @@ public class BatteryStatsImpl extends BatteryStats { addStateFlag = HistoryItem.STATE_PHONE_SCANNING_FLAG; newHistory = true; mPhoneSignalScanningTimer.startRunningLocked(elapsedRealtimeMs); - FrameworkStatsLog.write(FrameworkStatsLog.PHONE_SERVICE_STATE_CHANGED, state, - simState, strengthBin); + mFrameworkStatsLogger.phoneServiceStateChanged(state, simState, strengthBin); } } @@ -5944,8 +5989,7 @@ public class BatteryStatsImpl extends BatteryStats { removeStateFlag = HistoryItem.STATE_PHONE_SCANNING_FLAG; newHistory = true; mPhoneSignalScanningTimer.stopRunningLocked(elapsedRealtimeMs); - FrameworkStatsLog.write(FrameworkStatsLog.PHONE_SERVICE_STATE_CHANGED, state, - simState, strengthBin); + mFrameworkStatsLogger.phoneServiceStateChanged(state, simState, strengthBin); } } @@ -5966,8 +6010,7 @@ public class BatteryStatsImpl extends BatteryStats { } newSignalStrength = strengthBin; newHistory = true; - FrameworkStatsLog.write( - FrameworkStatsLog.PHONE_SIGNAL_STRENGTH_CHANGED, strengthBin); + mFrameworkStatsLogger.phoneSignalStrengthChanged(strengthBin); } else { stopAllPhoneSignalStrengthTimersLocked(-1, elapsedRealtimeMs); } @@ -6076,7 +6119,7 @@ public class BatteryStatsImpl extends BatteryStats { // Unknown is included in DATA_CONNECTION_OTHER. int bin = DATA_CONNECTION_OUT_OF_SERVICE; if (hasData) { - if (dataType > 0 && dataType <= TelephonyManager.getAllNetworkTypes().length) { + if (dataType > 0 && dataType <= NUM_ALL_NETWORK_TYPES) { bin = dataType; } else { switch (serviceType) { @@ -6995,7 +7038,7 @@ public class BatteryStatsImpl extends BatteryStats { /** @hide */ public void noteNetworkInterfaceForTransports(String iface, int[] transportTypes) { if (TextUtils.isEmpty(iface)) return; - final int displayTransport = NetworkCapabilitiesUtils.getDisplayTransport(transportTypes); + final int displayTransport = getDisplayTransport(transportTypes); synchronized (mModemNetworkLock) { if (displayTransport == TRANSPORT_CELLULAR) { @@ -10507,8 +10550,7 @@ public class BatteryStatsImpl extends BatteryStats { long elapsedRealtimeMs, long uptimeMs) { int uidRunningState; // Make special note of Foreground Services - final boolean userAwareService = - (ActivityManager.isForegroundService(procState)); + final boolean userAwareService = ActivityManager.isForegroundService(procState); uidRunningState = BatteryStats.mapToInternalProcessState(procState); if (mProcessState == uidRunningState && userAwareService == mInForegroundService) { @@ -10912,6 +10954,7 @@ public class BatteryStatsImpl extends BatteryStats { mPowerProfile = powerProfile; mCpuScalingPolicies = cpuScalingPolicies; mPowerStatsUidResolver = powerStatsUidResolver; + mFrameworkStatsLogger = new FrameworkStatsLogger(); initPowerProfile(); @@ -10966,7 +11009,7 @@ public class BatteryStatsImpl extends BatteryStats { // Notify statsd that the system is initially not in doze. mDeviceIdleMode = DEVICE_IDLE_MODE_OFF; - FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_IDLE_MODE_STATE_CHANGED, mDeviceIdleMode); + mFrameworkStatsLogger.deviceIdleModeStateChanged(mDeviceIdleMode); } private void recordPowerStats(PowerStats stats) { @@ -11702,7 +11745,9 @@ public class BatteryStatsImpl extends BatteryStats { mWakeupReasonStats.clear(); } - mTmpRailStats.reset(); + if (mTmpRailStats != null) { + mTmpRailStats.reset(); + } EnergyConsumerStats.resetIfNotNull(mGlobalEnergyConsumerStats); @@ -11864,6 +11909,78 @@ public class BatteryStatsImpl extends BatteryStats { return networkStatsManager.getWifiUidStats(); } + private static class NetworkStatsDelta { + int mUid; + int mSet; + long mRxBytes; + long mRxPackets; + long mTxBytes; + long mTxPackets; + + public int getUid() { + return mUid; + } + + + public int getSet() { + return mSet; + } + + public long getRxBytes() { + return mRxBytes; + } + + public long getRxPackets() { + return mRxPackets; + } + + public long getTxBytes() { + return mTxBytes; + } + + public long getTxPackets() { + return mTxPackets; + } + } + + private List<NetworkStatsDelta> computeDelta(NetworkStats currentStats, + NetworkStats lastStats) { + List<NetworkStatsDelta> deltaList = new ArrayList<>(); + for (NetworkStats.Entry entry : currentStats) { + NetworkStatsDelta delta = new NetworkStatsDelta(); + delta.mUid = entry.getUid(); + delta.mSet = entry.getSet(); + NetworkStats.Entry lastEntry = null; + if (lastStats != null) { + for (NetworkStats.Entry e : lastStats) { + if (e.getUid() == entry.getUid() && e.getSet() == entry.getSet() + && e.getTag() == entry.getTag() + && e.getMetered() == entry.getMetered() + && e.getRoaming() == entry.getRoaming() + && e.getDefaultNetwork() == entry.getDefaultNetwork() + /*&& Objects.equals(e.getIface(), entry.getIface())*/) { + lastEntry = e; + break; + } + } + } + if (lastEntry != null) { + delta.mRxBytes = entry.getRxBytes() - lastEntry.getRxBytes(); + delta.mRxPackets = entry.getRxPackets() - lastEntry.getRxPackets(); + delta.mTxBytes = entry.getTxBytes() - lastEntry.getTxBytes(); + delta.mTxPackets = entry.getTxPackets() - lastEntry.getTxPackets(); + } else { + delta.mRxBytes = entry.getRxBytes(); + delta.mRxPackets = entry.getRxPackets(); + delta.mTxBytes = entry.getTxBytes(); + delta.mTxPackets = entry.getTxPackets(); + } + deltaList.add(delta); + } + + return deltaList; + } + /** * Distribute WiFi energy info and network traffic to apps. * @param info The energy information from the WiFi controller. @@ -11879,14 +11996,14 @@ public class BatteryStatsImpl extends BatteryStats { } // Grab a separate lock to acquire the network stats, which may do I/O. - NetworkStats delta = null; + List<NetworkStatsDelta> delta; synchronized (mWifiNetworkLock) { final NetworkStats latestStats = readWifiNetworkStatsLocked(networkStatsManager); if (latestStats != null) { - delta = mLastWifiNetworkStats != null - ? latestStats.subtract(mLastWifiNetworkStats) - : latestStats.subtract(new NetworkStats(0, -1)); + delta = computeDelta(latestStats, mLastWifiNetworkStats); mLastWifiNetworkStats = latestStats; + } else { + delta = null; } } @@ -11914,7 +12031,7 @@ public class BatteryStatsImpl extends BatteryStats { long totalTxPackets = 0; long totalRxPackets = 0; if (delta != null) { - for (NetworkStats.Entry entry : delta) { + for (NetworkStatsDelta entry : delta) { if (DEBUG_ENERGY) { Slog.d(TAG, "Wifi uid " + entry.getUid() + ": delta rx=" + entry.getRxBytes() @@ -12199,13 +12316,16 @@ public class BatteryStatsImpl extends BatteryStats { } // Converting uWs to mAms. // Conversion: (uWs * (1000ms / 1s) * (1mW / 1000uW)) / mV = mAms - long monitoredRailChargeConsumedMaMs = - (long) (mTmpRailStats.getWifiTotalEnergyUseduWs() / opVolt); + long monitoredRailChargeConsumedMaMs = mTmpRailStats != null + ? (long) (mTmpRailStats.getWifiTotalEnergyUseduWs() / opVolt) + : 0L; mWifiActivity.getMonitoredRailChargeConsumedMaMs().addCountLocked( monitoredRailChargeConsumedMaMs); mHistory.recordWifiConsumedCharge(elapsedRealtimeMs, uptimeMs, (monitoredRailChargeConsumedMaMs / MILLISECONDS_IN_HOUR)); - mTmpRailStats.resetWifiTotalEnergyUsed(); + if (mTmpRailStats != null) { + mTmpRailStats.resetWifiTotalEnergyUsed(); + } if (uidEstimatedConsumptionMah != null) { totalEstimatedConsumptionMah = Math.max(controllerMaMs / MILLISECONDS_IN_HOUR, @@ -13519,14 +13639,16 @@ public class BatteryStatsImpl extends BatteryStats { } } - // Record whether we've seen a non-zero time (for debugging b/22716723). - if (wakelockStats.isEmpty()) { - Slog.wtf(TAG, "All kernel wakelocks had time of zero"); - } + if (DEBUG) { + // Record whether we've seen a non-zero time (for debugging b/22716723). + if (wakelockStats.isEmpty()) { + Slog.wtf(TAG, "All kernel wakelocks had time of zero"); + } - if (numWakelocksSetStale == mKernelWakelockStats.size()) { - Slog.wtf(TAG, "All kernel wakelocks were set stale. new version=" + - wakelockStats.kernelWakelockVersion); + if (numWakelocksSetStale == mKernelWakelockStats.size()) { + Slog.wtf(TAG, "All kernel wakelocks were set stale. new version=" + + wakelockStats.kernelWakelockVersion); + } } } @@ -14711,13 +14833,13 @@ public class BatteryStatsImpl extends BatteryStats { // Inform StatsLog of setBatteryState changes. private void reportChangesToStatsLog(final int status, final int plugType, final int level) { if (!mHaveBatteryLevel || mBatteryStatus != status) { - FrameworkStatsLog.write(FrameworkStatsLog.CHARGING_STATE_CHANGED, status); + mFrameworkStatsLogger.chargingStateChanged(status); } if (!mHaveBatteryLevel || mBatteryPlugType != plugType) { - FrameworkStatsLog.write(FrameworkStatsLog.PLUGGED_STATE_CHANGED, plugType); + mFrameworkStatsLogger.pluggedStateChanged(plugType); } if (!mHaveBatteryLevel || mBatteryLevel != level) { - FrameworkStatsLog.write(FrameworkStatsLog.BATTERY_LEVEL_CHANGED, level); + mFrameworkStatsLogger.batteryLevelChanged(level); } } diff --git a/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java b/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java index aba8e5fa9507..1050e8a371e8 100644 --- a/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java +++ b/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java @@ -32,6 +32,7 @@ import com.android.internal.power.ModemPowerProfile; import java.util.ArrayList; +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class MobileRadioPowerCalculator extends PowerCalculator { private static final String TAG = "MobRadioPowerCalculator"; private static final boolean DEBUG = PowerCalculator.DEBUG; @@ -320,7 +321,7 @@ public class MobileRadioPowerCalculator extends PowerCalculator { private double calculateActiveModemPowerMah(BatteryStats bs, long elapsedRealtimeUs) { final long elapsedRealtimeMs = elapsedRealtimeUs / 1000; - final int txLvlCount = CellSignalStrength.getNumSignalStrengthLevels(); + final int txLvlCount = NUM_SIGNAL_STRENGTH_LEVELS; double consumptionMah = 0.0; if (DEBUG) { diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 2accf9a2a43a..5d9c42d4c3a8 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -44,6 +44,7 @@ import static android.view.WindowManager.TransitionType; import static android.view.WindowManager.transitTypeToString; import static android.window.TaskFragmentAnimationParams.DEFAULT_ANIMATION_BACKGROUND_COLOR; import static android.window.TransitionInfo.FLAGS_IS_OCCLUDED_NO_ANIMATION; +import static android.window.TransitionInfo.FLAG_CONFIG_AT_END; import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; import static android.window.TransitionInfo.FLAG_FILLS_TASK; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; @@ -65,6 +66,7 @@ import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_RECENTS_ANIM; import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_SPLASH_SCREEN; import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_WINDOWS_DRAWN; +import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION; import android.annotation.IntDef; import android.annotation.NonNull; @@ -307,6 +309,12 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { */ int mAnimationTrack = 0; + /** + * List of activities whose configurations are sent to the client at the end of the transition + * instead of immediately when the configuration changes. + */ + ArrayList<ActivityRecord> mConfigAtEndActivities = null; + Transition(@TransitionType int type, @TransitionFlags int flags, TransitionController controller, BLASTSyncEngine syncEngine) { mType = type; @@ -484,6 +492,22 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return mTargetDisplays.contains(dc); } + void setConfigAtEnd(@NonNull WindowContainer<?> wc) { + wc.forAllActivities(ar -> { + if (!ar.isVisible() || !ar.isVisibleRequested()) return; + if (mConfigAtEndActivities == null) { + mConfigAtEndActivities = new ArrayList<>(); + } + if (mConfigAtEndActivities.contains(ar)) { + return; + } + mConfigAtEndActivities.add(ar); + ar.pauseConfigurationDispatch(); + }); + snapshotStartState(wc); + mChanges.get(wc).mFlags |= ChangeInfo.FLAG_CHANGE_CONFIG_AT_END; + } + /** Set a transition to be a seamless-rotation. */ void setSeamlessRotation(@NonNull WindowContainer wc) { final ChangeInfo info = mChanges.get(wc); @@ -644,20 +668,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Collecting in transition %d: %s", mSyncId, wc); - // "snapshot" all parents (as potential promotion targets). Do this before checking - // if this is already a participant in case it has since been re-parented. - for (WindowContainer<?> curr = getAnimatableParent(wc); - curr != null && !mChanges.containsKey(curr); - curr = getAnimatableParent(curr)) { - final ChangeInfo info = new ChangeInfo(curr); - updateTransientFlags(info); - mChanges.put(curr, info); - if (isReadyGroup(curr)) { - mReadyTrackerOld.addGroup(curr); - ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Creating Ready-group for" - + " Transition %d with root=%s", mSyncId, curr); - } - } + // Snapshot before checking if this is a participant in case it has been re-parented. + snapshotStartState(getAnimatableParent(wc)); if (mParticipants.contains(wc)) return; // Transient-hide may be hidden later, so no need to request redraw. if (!isInTransientHide(wc)) { @@ -688,6 +700,22 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } } + /** "snapshot" `wc` and all its parents (as potential promotion targets). */ + private void snapshotStartState(@NonNull WindowContainer<?> wc) { + for (WindowContainer<?> curr = wc; + curr != null && !mChanges.containsKey(curr); + curr = getAnimatableParent(curr)) { + final ChangeInfo info = new ChangeInfo(curr); + updateTransientFlags(info); + mChanges.put(curr, info); + if (isReadyGroup(curr)) { + mReadyTrackerOld.addGroup(curr); + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Creating Ready-group for" + + " Transition %d with root=%s", mSyncId, curr); + } + } + } + private void updateTransientFlags(@NonNull ChangeInfo info) { final WindowContainer<?> wc = info.mContainer; // Only look at tasks, taskfragments, or activities @@ -934,47 +962,60 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } /** + * Populates `t` with instructions to reset surface transform of `change` so it matches + * the WM hierarchy. This "undoes" lingering state left by the animation. + */ + private void resetSurfaceTransform(SurfaceControl.Transaction t, WindowContainer target, + SurfaceControl targetLeash) { + final Point tmpPos = new Point(); + target.getRelativePosition(tmpPos); + t.setPosition(targetLeash, tmpPos.x, tmpPos.y); + // No need to clip the display in case seeing the clipped content when during the + // display rotation. No need to clip activities because they rely on clipping on + // task layers. + if (target.asTaskFragment() == null) { + t.setCrop(targetLeash, null /* crop */); + } else { + // Crop to the resolved override bounds. + final Rect clipRect = target.getResolvedOverrideBounds(); + t.setWindowCrop(targetLeash, clipRect.width(), clipRect.height()); + } + t.setMatrix(targetLeash, 1, 0, 0, 1); + // The bounds sent to the transition is always a real bounds. This means we lose + // information about "null" bounds (inheriting from parent). Core will fix-up + // non-organized window surface bounds; however, since Core can't touch organized + // surfaces, add the "inherit from parent" restoration here. + if (target.isOrganized() && target.matchParentBounds()) { + t.setWindowCrop(targetLeash, -1, -1); + } + } + + /** * Build a transaction that "resets" all the re-parenting and layer changes. This is * intended to be applied at the end of the transition but before the finish callback. This * needs to be passed/applied in shell because until finish is called, shell owns the surfaces. * Additionally, this gives shell the ability to better deal with merged transitions. */ private void buildFinishTransaction(SurfaceControl.Transaction t, TransitionInfo info) { - final Point tmpPos = new Point(); // usually only size 1 final ArraySet<DisplayContent> displays = new ArraySet<>(); for (int i = mTargets.size() - 1; i >= 0; --i) { - final WindowContainer target = mTargets.get(i).mContainer; - if (target.getParent() != null) { - final SurfaceControl targetLeash = getLeashSurface(target, null /* t */); - final SurfaceControl origParent = getOrigParentSurface(target); - // Ensure surfaceControls are re-parented back into the hierarchy. - t.reparent(targetLeash, origParent); - t.setLayer(targetLeash, target.getLastLayer()); - target.getRelativePosition(tmpPos); - t.setPosition(targetLeash, tmpPos.x, tmpPos.y); - // No need to clip the display in case seeing the clipped content when during the - // display rotation. No need to clip activities because they rely on clipping on - // task layers. - if (target.asTaskFragment() == null) { - t.setCrop(targetLeash, null /* crop */); - } else { - // Crop to the resolved override bounds. - final Rect clipRect = target.getResolvedOverrideBounds(); - t.setWindowCrop(targetLeash, clipRect.width(), clipRect.height()); - } - t.setCornerRadius(targetLeash, 0); - t.setShadowRadius(targetLeash, 0); - t.setMatrix(targetLeash, 1, 0, 0, 1); - t.setAlpha(targetLeash, 1); - // The bounds sent to the transition is always a real bounds. This means we lose - // information about "null" bounds (inheriting from parent). Core will fix-up - // non-organized window surface bounds; however, since Core can't touch organized - // surfaces, add the "inherit from parent" restoration here. - if (target.isOrganized() && target.matchParentBounds()) { - t.setWindowCrop(targetLeash, -1, -1); - } - displays.add(target.getDisplayContent()); + final WindowContainer<?> target = mTargets.get(i).mContainer; + if (target.getParent() == null) continue; + final SurfaceControl targetLeash = getLeashSurface(target, null /* t */); + final SurfaceControl origParent = getOrigParentSurface(target); + // Ensure surfaceControls are re-parented back into the hierarchy. + t.reparent(targetLeash, origParent); + t.setLayer(targetLeash, target.getLastLayer()); + t.setCornerRadius(targetLeash, 0); + t.setShadowRadius(targetLeash, 0); + t.setAlpha(targetLeash, 1); + displays.add(target.getDisplayContent()); + // For config-at-end, the end-transform will be reset after the config is actually + // applied in the client (since the transform depends on config). The other properties + // remain here because shell might want to persistently override them. + if ((mTargets.get(i).mFlags & ChangeInfo.FLAG_CHANGE_CONFIG_AT_END) == 0) { + resetSurfaceTransform(t, target, targetLeash); } } // Remove screenshot layers if necessary @@ -1304,6 +1345,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mController.mAtm.mRootWindowContainer.rankTaskLayers(); } + commitConfigAtEndActivities(); + // dispatch legacy callback in a different loop. This is because multiple legacy handlers // (fixed-rotation/displaycontent) make global changes, so we want to ensure that we've // processed all the participants first (in particular, we want to trigger pip-enter first) @@ -1421,6 +1464,52 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mController.updateAnimatingState(); } + private void commitConfigAtEndActivities() { + if (mConfigAtEndActivities == null || mConfigAtEndActivities.isEmpty()) { + return; + } + final SurfaceControl.Transaction t = + mController.mAtm.mWindowManager.mTransactionFactory.get(); + for (int i = 0; i < mTargets.size(); ++i) { + final WindowContainer target = mTargets.get(i).mContainer; + if (target.getParent() == null || (mTargets.get(i).mFlags + & ChangeInfo.FLAG_CHANGE_CONFIG_AT_END) == 0) { + continue; + } + final SurfaceControl targetLeash = getLeashSurface(target, null /* t */); + // Reset surface state here (since it was skipped in buildFinishTransaction). Since + // we are resuming config to the "current" state, we have to calculate the matching + // surface state now (rather than snapshotting it at animation start). + resetSurfaceTransform(t, target, targetLeash); + } + + // Now we resume the configuration dispatch, wait until the now resumed configs have been + // drawn, and then apply everything together. + final BLASTSyncEngine.SyncGroup sg = mSyncEngine.prepareSyncSet( + new BLASTSyncEngine.TransactionReadyListener() { + @Override + public void onTransactionReady(int mSyncId, + SurfaceControl.Transaction transaction) { + t.merge(transaction); + t.apply(); + } + + @Override + public void onTransactionCommitTimeout() { + t.apply(); + } + }, "ConfigAtTransitEnd"); + final int syncId = sg.mSyncId; + mSyncEngine.startSyncSet(sg, BLAST_TIMEOUT_DURATION, true /* parallel */); + mSyncEngine.setSyncMethod(syncId, BLASTSyncEngine.METHOD_BLAST); + for (int i = 0; i < mConfigAtEndActivities.size(); ++i) { + final ActivityRecord ar = mConfigAtEndActivities.get(i); + mSyncEngine.addToSyncSet(syncId, ar); + ar.resumeConfigurationDispatch(); + } + mSyncEngine.setReady(syncId); + } + @Nullable private ActivityRecord getVisibleTransientLaunch(TaskDisplayArea taskDisplayArea) { if (mTransientLaunches == null) return null; @@ -1546,6 +1635,12 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if (mState == STATE_ABORT) { mController.onAbort(this); + if (mConfigAtEndActivities != null) { + for (int i = 0; i < mConfigAtEndActivities.size(); ++i) { + mConfigAtEndActivities.get(i).resumeConfigurationDispatch(); + } + mConfigAtEndActivities = null; + } primaryDisplay.getPendingTransaction().merge(transaction); mSyncId = -1; mOverrideOptions = null; @@ -2291,6 +2386,11 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } else { parentChange.mFlags |= ChangeInfo.FLAG_CHANGE_YES_ANIMATION; } + final ActivityRecord ar = targetChange.mContainer.asActivityRecord(); + if ((ar != null && ar.isConfigurationDispatchPaused()) + || ((targetChange.mFlags & ChangeInfo.FLAG_CHANGE_CONFIG_AT_END) != 0)) { + parentChange.mFlags |= ChangeInfo.FLAG_CHANGE_CONFIG_AT_END; + } } } @@ -2940,6 +3040,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { /** Whether this change's container moved to the top. */ private static final int FLAG_CHANGE_MOVED_TO_TOP = 0x20; + /** Whether this change contains config-at-end members. */ + private static final int FLAG_CHANGE_CONFIG_AT_END = 0x40; + @IntDef(prefix = { "FLAG_" }, value = { FLAG_NONE, FLAG_SEAMLESS_ROTATION, @@ -2947,7 +3050,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { FLAG_ABOVE_TRANSIENT_LAUNCH, FLAG_CHANGE_NO_ANIMATION, FLAG_CHANGE_YES_ANIMATION, - FLAG_CHANGE_MOVED_TO_TOP + FLAG_CHANGE_MOVED_TO_TOP, + FLAG_CHANGE_CONFIG_AT_END }) @Retention(RetentionPolicy.SOURCE) @interface Flag {} @@ -3095,6 +3199,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { flags |= FLAG_IS_VOICE_INTERACTION; } flags |= record.mTransitionChangeFlags; + if (record.isConfigurationDispatchPaused()) { + flags |= FLAG_CONFIG_AT_END; + } } final TaskFragment taskFragment = wc.asTaskFragment(); if (taskFragment != null && task == null) { @@ -3140,6 +3247,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if ((mFlags & FLAG_CHANGE_MOVED_TO_TOP) != 0) { flags |= FLAG_MOVED_TO_TOP; } + if ((mFlags & FLAG_CHANGE_CONFIG_AT_END) != 0) { + flags |= FLAG_CONFIG_AT_END; + } return flags; } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 8cd399f6a12e..a8de9198943c 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -583,8 +583,21 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } final List<WindowContainerTransaction.HierarchyOp> hops = t.getHierarchyOps(); final int hopSize = hops.size(); - Iterator<Map.Entry<IBinder, WindowContainerTransaction.Change>> entries = - t.getChanges().entrySet().iterator(); + Iterator<Map.Entry<IBinder, WindowContainerTransaction.Change>> entries; + if (transition != null) { + // Mark any config-at-end containers before applying config changes so that + // the config changes don't dispatch to client. + entries = t.getChanges().entrySet().iterator(); + while (entries.hasNext()) { + final Map.Entry<IBinder, WindowContainerTransaction.Change> entry = + entries.next(); + if (!entry.getValue().getConfigAtTransitionEnd()) continue; + final WindowContainer wc = WindowContainer.fromBinder(entry.getKey()); + if (wc == null || !wc.isAttached()) continue; + transition.setConfigAtEnd(wc); + } + } + entries = t.getChanges().entrySet().iterator(); while (entries.hasNext()) { final Map.Entry<IBinder, WindowContainerTransaction.Change> entry = entries.next(); final WindowContainer wc = WindowContainer.fromBinder(entry.getKey()); diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index dfa9dcecfbb5..3607dddc66d5 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -34,6 +34,7 @@ cc_library_static { "tvinput/BufferProducerThread.cpp", "tvinput/JTvInputHal.cpp", "tvinput/TvInputHal_hidl.cpp", + "com_android_server_accessibility_BrailleDisplayConnection.cpp", "com_android_server_adb_AdbDebuggingManager.cpp", "com_android_server_am_BatteryStatsService.cpp", "com_android_server_biometrics_SurfaceToNativeHandleConverter.cpp", diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS index df7fb991e39e..b999305fbee0 100644 --- a/services/core/jni/OWNERS +++ b/services/core/jni/OWNERS @@ -15,6 +15,7 @@ per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION per-file com_android_server_SystemClock* = file:/services/core/java/com/android/server/timedetector/OWNERS per-file com_android_server_Usb* = file:/services/usb/OWNERS per-file com_android_server_Vibrator* = file:/services/core/java/com/android/server/vibrator/OWNERS +per-file com_android_server_accessibility_* = file:/services/accessibility/OWNERS per-file com_android_server_hdmi_* = file:/core/java/android/hardware/hdmi/OWNERS per-file com_android_server_lights_* = file:/services/core/java/com/android/server/lights/OWNERS per-file com_android_server_location_* = file:/location/java/android/location/OWNERS diff --git a/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp b/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp new file mode 100644 index 000000000000..9a509a71cf92 --- /dev/null +++ b/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp @@ -0,0 +1,103 @@ +/* + * 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. + */ + +#include <core_jni_helpers.h> +#include <jni.h> +#include <linux/hidraw.h> +#include <linux/input.h> +#include <nativehelper/JNIHelp.h> +#include <sys/ioctl.h> + +/* + * This file defines simple wrappers around the kernel UAPI HIDRAW driver's ioctl() commands. + * See kernel example samples/hidraw/hid-example.c + * + * All methods expect an open file descriptor int from Java. + */ + +namespace android { + +namespace { + +// Max size we allow for the result from HIDIOCGRAWUNIQ (Bluetooth address or USB serial number). +// Copied from linux/hid.h struct hid_device->uniq char array size; the ioctl implementation +// writes at most this many bytes to the provided buffer. +constexpr int UNIQ_SIZE_MAX = 64; + +} // anonymous namespace + +static jint com_android_server_accessibility_BrailleDisplayConnection_getHidrawDescSize( + JNIEnv* env, jobject thiz, int fd) { + int size = 0; + if (ioctl(fd, HIDIOCGRDESCSIZE, &size) < 0) { + return -1; + } + return size; +} + +static jbyteArray com_android_server_accessibility_BrailleDisplayConnection_getHidrawDesc( + JNIEnv* env, jobject thiz, int fd, int descSize) { + struct hidraw_report_descriptor desc; + desc.size = descSize; + if (ioctl(fd, HIDIOCGRDESC, &desc) < 0) { + return nullptr; + } + jbyteArray result = env->NewByteArray(descSize); + if (result != nullptr) { + env->SetByteArrayRegion(result, 0, descSize, (jbyte*)desc.value); + } + // Local ref is not deleted because it is returned to Java + return result; +} + +static jstring com_android_server_accessibility_BrailleDisplayConnection_getHidrawUniq(JNIEnv* env, + jobject thiz, + int fd) { + char buf[UNIQ_SIZE_MAX]; + if (ioctl(fd, HIDIOCGRAWUNIQ(UNIQ_SIZE_MAX), buf) < 0) { + return nullptr; + } + // Local ref is not deleted because it is returned to Java + return env->NewStringUTF(buf); +} + +static jint com_android_server_accessibility_BrailleDisplayConnection_getHidrawBusType(JNIEnv* env, + jobject thiz, + int fd) { + struct hidraw_devinfo info; + if (ioctl(fd, HIDIOCGRAWINFO, &info) < 0) { + return -1; + } + return info.bustype; +} + +static const JNINativeMethod gMethods[] = { + {"nativeGetHidrawDescSize", "(I)I", + (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawDescSize}, + {"nativeGetHidrawDesc", "(II)[B", + (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawDesc}, + {"nativeGetHidrawUniq", "(I)Ljava/lang/String;", + (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawUniq}, + {"nativeGetHidrawBusType", "(I)I", + (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawBusType}, +}; + +int register_com_android_server_accessibility_BrailleDisplayConnection(JNIEnv* env) { + return RegisterMethodsOrDie(env, "com/android/server/accessibility/BrailleDisplayConnection", + gMethods, NELEM(gMethods)); +} + +}; // namespace android diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index 5d1eb496903b..0936888b24a0 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -70,6 +70,7 @@ int register_com_android_server_wm_TaskFpsCallbackController(JNIEnv* env); int register_com_android_server_display_DisplayControl(JNIEnv* env); int register_com_android_server_SystemClockTime(JNIEnv* env); int register_android_server_display_smallAreaDetectionController(JNIEnv* env); +int register_com_android_server_accessibility_BrailleDisplayConnection(JNIEnv* env); }; using namespace android; @@ -132,5 +133,6 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_com_android_server_display_DisplayControl(env); register_com_android_server_SystemClockTime(env); register_android_server_display_smallAreaDetectionController(env); + register_com_android_server_accessibility_BrailleDisplayConnection(env); return JNI_VERSION_1_4; } 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/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeMapTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeMapTest.java new file mode 100644 index 000000000000..3bb6712a34f1 --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeMapTest.java @@ -0,0 +1,125 @@ +/* + * 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.inputmethod; + +import static com.google.common.truth.Truth.assertThat; + +import android.util.ArrayMap; +import android.view.inputmethod.InputMethodSubtype; + +import androidx.annotation.NonNull; + +import org.junit.Test; + +import java.util.List; +public final class AdditionalSubtypeMapTest { + + private static final String TEST_IME1_ID = "com.android.test.inputmethod/.TestIme1"; + private static final String TEST_IME2_ID = "com.android.test.inputmethod/.TestIme2"; + + private static InputMethodSubtype createTestSubtype(String locale) { + return new InputMethodSubtype + .InputMethodSubtypeBuilder() + .setSubtypeNameResId(0) + .setSubtypeIconResId(0) + .setSubtypeLocale(locale) + .setIsAsciiCapable(true) + .build(); + } + + private static final InputMethodSubtype TEST_SUBTYPE_EN_US = createTestSubtype("en_US"); + private static final InputMethodSubtype TEST_SUBTYPE_JA_JP = createTestSubtype("ja_JP"); + + private static final List<InputMethodSubtype> TEST_SUBTYPE_LIST1 = List.of(TEST_SUBTYPE_EN_US); + private static final List<InputMethodSubtype> TEST_SUBTYPE_LIST2 = List.of(TEST_SUBTYPE_JA_JP); + + private static ArrayMap<String, List<InputMethodSubtype>> mapOf( + @NonNull String key1, @NonNull List<InputMethodSubtype> value1) { + final ArrayMap<String, List<InputMethodSubtype>> map = new ArrayMap<>(); + map.put(key1, value1); + return map; + } + + private static ArrayMap<String, List<InputMethodSubtype>> mapOf( + @NonNull String key1, @NonNull List<InputMethodSubtype> value1, + @NonNull String key2, @NonNull List<InputMethodSubtype> value2) { + final ArrayMap<String, List<InputMethodSubtype>> map = new ArrayMap<>(); + map.put(key1, value1); + map.put(key2, value2); + return map; + } + + @Test + public void testOfReturnsEmptyInstance() { + assertThat(AdditionalSubtypeMap.of(new ArrayMap<>())) + .isSameInstanceAs(AdditionalSubtypeMap.EMPTY_MAP); + } + + @Test + public void testOfReturnsNewInstance() { + final AdditionalSubtypeMap instance = AdditionalSubtypeMap.of( + mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1)); + assertThat(instance.keySet()).containsExactly(TEST_IME1_ID); + assertThat(instance.get(TEST_IME1_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST1); + } + + @Test + public void testCloneWithRemoveOrSelfReturnsEmptyInstance() { + final AdditionalSubtypeMap original = AdditionalSubtypeMap.of( + mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1)); + final AdditionalSubtypeMap result = original.cloneWithRemoveOrSelf(TEST_IME1_ID); + assertThat(result).isSameInstanceAs(AdditionalSubtypeMap.EMPTY_MAP); + } + + @Test + public void testCloneWithRemoveOrSelfWithMultipleKeysReturnsEmptyInstance() { + final AdditionalSubtypeMap original = AdditionalSubtypeMap.of( + mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1, TEST_IME2_ID, TEST_SUBTYPE_LIST2)); + final AdditionalSubtypeMap result = original.cloneWithRemoveOrSelf( + List.of(TEST_IME1_ID, TEST_IME2_ID)); + assertThat(result).isSameInstanceAs(AdditionalSubtypeMap.EMPTY_MAP); + } + + @Test + public void testCloneWithRemoveOrSelfReturnsNewInstance() { + final AdditionalSubtypeMap original = AdditionalSubtypeMap.of( + mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1, TEST_IME2_ID, TEST_SUBTYPE_LIST2)); + final AdditionalSubtypeMap result = original.cloneWithRemoveOrSelf(TEST_IME1_ID); + assertThat(result.keySet()).containsExactly(TEST_IME2_ID); + assertThat(result.get(TEST_IME2_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST2); + } + + @Test + public void testCloneWithPutWithNewKey() { + final AdditionalSubtypeMap original = AdditionalSubtypeMap.of( + mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1)); + final AdditionalSubtypeMap result = original.cloneWithPut(TEST_IME2_ID, TEST_SUBTYPE_LIST2); + assertThat(result.keySet()).containsExactly(TEST_IME1_ID, TEST_IME2_ID); + assertThat(result.get(TEST_IME1_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST1); + assertThat(result.get(TEST_IME2_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST2); + } + + @Test + public void testCloneWithPutWithExistingKey() { + final AdditionalSubtypeMap original = AdditionalSubtypeMap.of( + mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1, TEST_IME2_ID, TEST_SUBTYPE_LIST2)); + final AdditionalSubtypeMap result = original.cloneWithPut(TEST_IME2_ID, TEST_SUBTYPE_LIST1); + assertThat(result.keySet()).containsExactly(TEST_IME1_ID, TEST_IME2_ID); + assertThat(result.get(TEST_IME1_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST1); + assertThat(result.get(TEST_IME2_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST1); + } +} diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java index 0edb3dfc0bc0..63224bb2aa3f 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java @@ -55,9 +55,9 @@ public final class AdditionalSubtypeUtilsTest { // Save & load. AtomicFile atomicFile = new AtomicFile( new File(InstrumentationRegistry.getContext().getCacheDir(), "subtypes.xml")); - AdditionalSubtypeUtils.saveToFile(allSubtypes, InputMethodMap.of(methodMap), atomicFile); - ArrayMap<String, List<InputMethodSubtype>> loadedSubtypes = new ArrayMap<>(); - AdditionalSubtypeUtils.loadFromFile(loadedSubtypes, atomicFile); + AdditionalSubtypeUtils.saveToFile(AdditionalSubtypeMap.of(allSubtypes), + InputMethodMap.of(methodMap), atomicFile); + AdditionalSubtypeMap loadedSubtypes = AdditionalSubtypeUtils.loadFromFile(atomicFile); // Verifies the loaded data. assertEquals(1, loadedSubtypes.size()); diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java index 71752ba3b393..2ea2e2264bec 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java @@ -25,10 +25,8 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import android.util.ArrayMap; import android.view.inputmethod.InputMethod; import android.view.inputmethod.InputMethodInfo; -import android.view.inputmethod.InputMethodSubtype; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -124,10 +122,8 @@ public class InputMethodManagerServiceRestrictImeAmountTest extends private List<InputMethodInfo> filterInputMethodServices(List<ResolveInfo> resolveInfoList, List<String> enabledComponents) { - final ArrayMap<String, List<InputMethodSubtype>> emptyAdditionalSubtypeMap = - new ArrayMap<>(); final InputMethodMap methodMap = InputMethodManagerService.filterInputMethodServices( - emptyAdditionalSubtypeMap, enabledComponents, mContext, resolveInfoList); + AdditionalSubtypeMap.EMPTY_MAP, enabledComponents, mContext, resolveInfoList); return methodMap.values(); } diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt index a8100afc4ac4..66e07175e7f5 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt @@ -18,6 +18,10 @@ package com.android.server.pm.test.verify.domain import android.content.Context import android.content.Intent +import android.content.UriRelativeFilter +import android.content.UriRelativeFilterGroup +import android.content.UriRelativeFilterGroupParcel +import android.content.pm.Flags import android.content.pm.PackageManager import android.content.pm.verify.domain.DomainOwner import android.content.pm.verify.domain.DomainVerificationInfo @@ -25,8 +29,10 @@ import android.content.pm.verify.domain.DomainVerificationManager import android.content.pm.verify.domain.DomainVerificationUserState import android.content.pm.verify.domain.IDomainVerificationManager import android.os.Build -import android.os.PatternMatcher +import android.os.Bundle +import android.os.PatternMatcher.PATTERN_LITERAL import android.os.Process +import android.platform.test.annotations.RequiresFlagsEnabled import android.util.ArraySet import android.util.SparseArray import com.android.internal.pm.parsing.pkg.AndroidPackageInternal @@ -68,6 +74,63 @@ class DomainVerificationManagerApiTest { private val DOMAIN_4 = "four.$DOMAIN_BASE" } + @RequiresFlagsEnabled(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS) + @Test + fun updateUriRelativeFilterGroups() { + val pkgWithDomains = mockPkgState(PKG_ONE, UUID_ONE, listOf(DOMAIN_1, DOMAIN_2)) + val service = makeService(pkgWithDomains).apply { + addPackages(pkgWithDomains) + } + + val bundle = service.getUriRelativeFilterGroups(PKG_ONE, listOf(DOMAIN_1, DOMAIN_2)) + assertThat(bundle.keySet()).containsExactlyElementsIn(listOf(DOMAIN_1, DOMAIN_2)) + assertThat(bundle.getParcelableArrayList(DOMAIN_1, UriRelativeFilterGroup::class.java)) + .isEmpty() + assertThat(bundle.getParcelableArrayList(DOMAIN_2, UriRelativeFilterGroup::class.java)) + .isEmpty() + + val pathGroup = UriRelativeFilterGroup(UriRelativeFilterGroup.ACTION_ALLOW) + pathGroup.addUriRelativeFilter( + UriRelativeFilter(UriRelativeFilter.PATH, PATTERN_LITERAL, "path") + ) + val queryGroup = UriRelativeFilterGroup(UriRelativeFilterGroup.ACTION_BLOCK) + queryGroup.addUriRelativeFilter( + UriRelativeFilter(UriRelativeFilter.QUERY, PATTERN_LITERAL, "query") + ) + val fragmentGroup = UriRelativeFilterGroup(UriRelativeFilterGroup.ACTION_ALLOW) + fragmentGroup.addUriRelativeFilter( + UriRelativeFilter(UriRelativeFilter.FRAGMENT, PATTERN_LITERAL, "fragment") + ) + + assertGroups(service, arrayListOf(pathGroup)) + assertGroups(service, arrayListOf(queryGroup, pathGroup)) + assertGroups(service, arrayListOf(queryGroup, fragmentGroup, pathGroup)) + } + + private fun assertGroups( + service: DomainVerificationService, + groups: List<UriRelativeFilterGroup> + ) { + val bundle = Bundle() + bundle.putParcelableList(DOMAIN_1, UriRelativeFilterGroup.groupsToParcels(groups)) + service.setUriRelativeFilterGroups(PKG_ONE, bundle) + val fetchedBundle = service.getUriRelativeFilterGroups(PKG_ONE, listOf(DOMAIN_1)) + assertThat(fetchedBundle.keySet()).containsExactlyElementsIn(bundle.keySet()) + assertThat( + UriRelativeFilterGroup.parcelsToGroups( + fetchedBundle.getParcelableArrayList( + DOMAIN_1, + UriRelativeFilterGroupParcel::class.java) + ) + ).containsExactlyElementsIn( + UriRelativeFilterGroup.parcelsToGroups( + bundle.getParcelableArrayList( + DOMAIN_1, + UriRelativeFilterGroupParcel::class.java) + ) + ).inOrder() + } + @Test fun queryValidVerificationPackageNames() { val pkgWithDomains = mockPkgState(PKG_ONE, UUID_ONE, listOf(DOMAIN_1, DOMAIN_2)) @@ -484,6 +547,7 @@ class DomainVerificationManagerApiTest { DomainVerificationService(mockThrowOnUnmocked { // Assume the test has every permission necessary whenever(enforcePermission(anyString(), anyInt(), anyInt(), anyString())) + whenever(enforceCallingOrSelfPermission(anyString(), anyString())) whenever(checkPermission(anyString(), anyInt(), anyInt())) { PackageManager.PERMISSION_GRANTED } @@ -539,7 +603,7 @@ class DomainVerificationManagerApiTest { addCategory(Intent.CATEGORY_DEFAULT) addDataScheme("http") addDataScheme("https") - addDataPath("/sub", PatternMatcher.PATTERN_LITERAL) + addDataPath("/sub", PATTERN_LITERAL) addDataAuthority(it, null) } } diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt index 65b99c524e0e..4fa4190c847b 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt @@ -16,7 +16,12 @@ package com.android.server.pm.test.verify.domain +import android.content.UriRelativeFilter +import android.content.UriRelativeFilter.PATH +import android.content.UriRelativeFilterGroup +import android.content.UriRelativeFilterGroup.ACTION_ALLOW import android.content.pm.verify.domain.DomainVerificationState +import android.os.PatternMatcher.PATTERN_LITERAL import android.os.UserHandle import android.util.ArrayMap import android.util.SparseArray @@ -157,7 +162,7 @@ class DomainVerificationPersistenceTest { @Test fun writeStateSignatureIfFunctionReturnsNull() { - val (attached, pending, restored) = mockWriteValues { "SIGNATURE_$it" } + val (attached, pending, restored) = mockWriteValues { "SIGNATURE_$it" } val file = tempFolder.newFile().writeXml { DomainVerificationPersistence.writeToXml(it, attached, pending, restored, UserHandle.USER_ALL) { null } @@ -313,6 +318,9 @@ class DomainVerificationPersistenceTest { addHosts(setOf("$packageName-user.com")) isLinkHandlingAllowed = true } + val group = UriRelativeFilterGroup(ACTION_ALLOW) + group.addUriRelativeFilter(UriRelativeFilter(PATH, PATTERN_LITERAL, "test")) + uriRelativeFilterGroupMap.put("example.com", listOf(group)) } private fun pkgName(id: Int) = "$PKG_PREFIX.pkg$id" diff --git a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java index d78143381ae5..9e98105e5495 100644 --- a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java @@ -31,6 +31,7 @@ import static org.mockito.Mockito.when; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionManager; +import android.provider.Settings; import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.NotificationListenerService.RankingMap; import android.service.notification.StatusBarNotification; @@ -391,6 +392,16 @@ public class SensitiveContentProtectionManagerServiceTest { } @Test + public void mediaProjectionOnStart_disabledViaDevOption_noBlockedPackages() { + mockDisabledViaDevelopOption(); + setupSensitiveNotification(); + + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + + verifyZeroInteractions(mWindowManager); + } + + @Test public void nlsOnListenerConnected_projectionNotStarted_noop() { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive @@ -484,6 +495,18 @@ public class SensitiveContentProtectionManagerServiceTest { } @Test + public void nlsOnListenerConnected_disabledViaDevOption_noBlockedPackages() { + mockDisabledViaDevelopOption(); + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); + + verifyZeroInteractions(mWindowManager); + } + + @Test public void nlsOnNotificationRankingUpdate_projectionNotStarted_noop() { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive @@ -599,6 +622,19 @@ public class SensitiveContentProtectionManagerServiceTest { } @Test + public void nlsOnNotificationRankingUpdate_disabledViaDevOption_noBlockedPackages() { + mockDisabledViaDevelopOption(); + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationRankingUpdate(mRankingMap); + + verifyZeroInteractions(mWindowManager); + } + + @Test public void nlsOnNotificationPosted_projectionNotStarted_noop() { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive @@ -697,4 +733,26 @@ public class SensitiveContentProtectionManagerServiceTest { verifyZeroInteractions(mWindowManager); } + + @Test + public void nlsOnNotificationPosted_disabledViaDevOption_noBlockedPackages() { + mockDisabledViaDevelopOption(); + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationPosted(mNotification1, mRankingMap); + + verifyZeroInteractions(mWindowManager); + } + + private void mockDisabledViaDevelopOption() { + // mContext (TestableContext) uses [TestableSettingsProvider] and it will be cleared after + // the test + Settings.Global.putInt( + mContext.getContentResolver(), + Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, + 1); + } } 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/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp index 64fef68e387a..1de049eaf263 100644 --- a/services/tests/powerstatstests/Android.bp +++ b/services/tests/powerstatstests/Android.bp @@ -8,6 +8,37 @@ filegroup { srcs: [ "src/com/android/server/power/stats/AggregatedPowerStatsProcessorTest.java", "src/com/android/server/power/stats/AggregatedPowerStatsTest.java", + "src/com/android/server/power/stats/AmbientDisplayPowerCalculatorTest.java", + "src/com/android/server/power/stats/AudioPowerCalculatorTest.java", + "src/com/android/server/power/stats/BatteryChargeCalculatorTest.java", + "src/com/android/server/power/stats/BatteryStatsCounterTest.java", + "src/com/android/server/power/stats/BatteryStatsCpuTimesTest.java", + "src/com/android/server/power/stats/BatteryStatsDualTimerTest.java", + "src/com/android/server/power/stats/BatteryStatsDurationTimerTest.java", + "src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java", + "src/com/android/server/power/stats/BatteryStatsHistoryTest.java", + "src/com/android/server/power/stats/BatteryStatsImplTest.java", + "src/com/android/server/power/stats/BatteryStatsNoteTest.java", + "src/com/android/server/power/stats/BatteryStatsSamplingTimerTest.java", + "src/com/android/server/power/stats/BatteryStatsSensorTest.java", + "src/com/android/server/power/stats/BatteryStatsServTest.java", + "src/com/android/server/power/stats/BatteryStatsStopwatchTimerTest.java", + "src/com/android/server/power/stats/BatteryStatsTimeBaseTest.java", + "src/com/android/server/power/stats/BatteryStatsTimerTest.java", + "src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java", + "src/com/android/server/power/stats/BatteryUsageStatsTest.java", + "src/com/android/server/power/stats/BluetoothPowerCalculatorTest.java", + "src/com/android/server/power/stats/CameraPowerCalculatorTest.java", + "src/com/android/server/power/stats/CpuAggregatedPowerStatsProcessorTest.java", + "src/com/android/server/power/stats/CpuPowerCalculatorTest.java", + "src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java", + "src/com/android/server/power/stats/EnergyConsumerSnapshotTest.java", + "src/com/android/server/power/stats/FlashlightPowerCalculatorTest.java", + "src/com/android/server/power/stats/GnssPowerCalculatorTest.java", + "src/com/android/server/power/stats/IdlePowerCalculatorTest.java", + "src/com/android/server/power/stats/LongSamplingCounterArrayTest.java", + "src/com/android/server/power/stats/LongSamplingCounterTest.java", + "src/com/android/server/power/stats/MemoryPowerCalculatorTest.java", "src/com/android/server/power/stats/MultiStateStatsTest.java", "src/com/android/server/power/stats/PowerStatsAggregatorTest.java", "src/com/android/server/power/stats/PowerStatsCollectorTest.java", @@ -15,6 +46,12 @@ filegroup { "src/com/android/server/power/stats/PowerStatsSchedulerTest.java", "src/com/android/server/power/stats/PowerStatsStoreTest.java", "src/com/android/server/power/stats/PowerStatsUidResolverTest.java", + "src/com/android/server/power/stats/ScreenPowerCalculatorTest.java", + "src/com/android/server/power/stats/SensorPowerCalculatorTest.java", + "src/com/android/server/power/stats/UserPowerCalculatorTest.java", + "src/com/android/server/power/stats/VideoPowerCalculatorTest.java", + "src/com/android/server/power/stats/WakelockPowerCalculatorTest.java", + "src/com/android/server/power/stats/WifiPowerCalculatorTest.java", ], } @@ -26,10 +63,6 @@ android_test { "src/**/*.java", ], - exclude_srcs: [ - ":power_stats_ravenwood_tests", - ], - static_libs: [ "services.core", "coretests-aidl", @@ -41,6 +74,7 @@ android_test { "androidx.test.ext.truth", "androidx.test.uiautomator_uiautomator", "mockito-target-minus-junit4", + "ravenwood-junit", "servicestests-utils", "platform-test-annotations", "flag-junit", diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/AmbientDisplayPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/AmbientDisplayPowerCalculatorTest.java index 319a280d10cc..f74cfae6a81b 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/AmbientDisplayPowerCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/AmbientDisplayPowerCalculatorTest.java @@ -21,6 +21,7 @@ import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_AMBIENT; import static com.google.common.truth.Truth.assertThat; import android.os.BatteryConsumer; +import android.platform.test.ravenwood.RavenwoodRule; import android.view.Display; import androidx.test.filters.SmallTest; @@ -34,10 +35,15 @@ import org.junit.runner.RunWith; @SmallTest @SuppressWarnings("GuardedBy") public class AmbientDisplayPowerCalculatorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final double PRECISION = 0.00001; private static final long MINUTE_IN_MS = 60 * 1000; - @Rule + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() .setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_AMBIENT, 0, 10.0) .setNumDisplays(1); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/AudioPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/AudioPowerCalculatorTest.java index fb367b24168e..ce451c2a842a 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/AudioPowerCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/AudioPowerCalculatorTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import android.os.BatteryConsumer; import android.os.Process; import android.os.UidBatteryConsumer; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -34,11 +35,16 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SmallTest public class AudioPowerCalculatorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final double PRECISION = 0.00001; private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42; - @Rule + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() .setAveragePower(PowerProfile.POWER_AUDIO, 360.0); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java index 3f058a2348c3..3ab1c2eab6ca 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import android.os.BatteryManager; import android.os.BatteryUsageStats; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -34,9 +35,14 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SmallTest public class BatteryChargeCalculatorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final double PRECISION = 0.00001; - @Rule + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() .setAveragePower(PowerProfile.POWER_BATTERY_CAPACITY, 4000.0); @@ -46,15 +52,17 @@ public class BatteryChargeCalculatorTest { final BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats(); - batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, - /* plugType */ 0, 90, 72, 3700, 3_600_000, 4_000_000, 0, - 1_000_000, 1_000_000, 1_000_000); - batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, - /* plugType */ 0, 85, 72, 3700, 3_000_000, 4_000_000, 0, - 1_500_000, 1_500_000, 1_500_000); - batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, - /* plugType */ 0, 80, 72, 3700, 2_400_000, 4_000_000, 0, - 2_000_000, 2_000_000, 2_000_000); + synchronized (batteryStats) { + batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, + /* plugType */ 0, 90, 72, 3700, 3_600_000, 4_000_000, 0, + 1_000_000, 1_000_000, 1_000_000); + batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, + /* plugType */ 0, 85, 72, 3700, 3_000_000, 4_000_000, 0, + 1_500_000, 1_500_000, 1_500_000); + batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, + /* plugType */ 0, 80, 72, 3700, 2_400_000, 4_000_000, 0, + 2_000_000, 2_000_000, 2_000_000); + } mStatsRule.setTime(5_000_000, 5_000_000); BatteryChargeCalculator calculator = new BatteryChargeCalculator(); @@ -73,10 +81,11 @@ public class BatteryChargeCalculatorTest { assertThat(batteryUsageStats.getChargeTimeRemainingMs()).isEqualTo(-1); // Plug in - batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_CHARGING, 100, - BatteryManager.BATTERY_PLUGGED_USB, 80, 72, 3700, 2_400_000, 4_000_000, 100, - 4_000_000, 4_000_000, 4_000_000); - + synchronized (batteryStats) { + batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_CHARGING, 100, + BatteryManager.BATTERY_PLUGGED_USB, 80, 72, 3700, 2_400_000, 4_000_000, 100, + 4_000_000, 4_000_000, 4_000_000); + } batteryUsageStats = mStatsRule.apply(calculator); assertThat(batteryUsageStats.getChargeTimeRemainingMs()).isEqualTo(100_000); @@ -86,15 +95,17 @@ public class BatteryChargeCalculatorTest { public void testDischargeTotals_chargeUahUnavailable() { final BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats(); - batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, - /* plugType */ 0, 90, 72, 3700, 0, 0, 0, - 1_000_000, 1_000_000, 1_000_000); - batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, - /* plugType */ 0, 85, 72, 3700, 0, 0, 0, - 1_500_000, 1_500_000, 1_500_000); - batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, - /* plugType */ 0, 80, 72, 3700, 0, 0, 0, - 2_000_000, 2_000_000, 2_000_000); + synchronized (batteryStats) { + batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, + /* plugType */ 0, 90, 72, 3700, 0, 0, 0, + 1_000_000, 1_000_000, 1_000_000); + batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, + /* plugType */ 0, 85, 72, 3700, 0, 0, 0, + 1_500_000, 1_500_000, 1_500_000); + batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, + /* plugType */ 0, 80, 72, 3700, 0, 0, 0, + 2_000_000, 2_000_000, 2_000_000); + } BatteryChargeCalculator calculator = new BatteryChargeCalculator(); BatteryUsageStats batteryUsageStats = mStatsRule.apply(calculator); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java index 9c2834d31609..997b7712a9dd 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java @@ -215,7 +215,7 @@ public class BatteryExternalStatsWorkerTest { public class TestBatteryStatsImpl extends BatteryStatsImpl { public TestBatteryStatsImpl(Context context) { - super(Clock.SYSTEM_CLOCK, null, null, null); + super(Clock.SYSTEM_CLOCK, null, null, null, null, null, null); mPowerProfile = new PowerProfile(context, true /* forTest */); SparseArray<int[]> cpusByPolicy = new SparseArray<>(); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsCpuTimesTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsCpuTimesTest.java index f9f32b2e7091..6e62147ac6c1 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsCpuTimesTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsCpuTimesTest.java @@ -40,6 +40,7 @@ import android.os.BatteryStats; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; +import android.platform.test.ravenwood.RavenwoodRule; import android.util.SparseArray; import android.util.SparseLongArray; import android.view.Display; @@ -56,6 +57,7 @@ import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidUserSysTimeRea import com.android.internal.util.ArrayUtils; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -84,7 +86,13 @@ import java.util.Arrays; */ @SmallTest @RunWith(AndroidJUnit4.class) +@SuppressWarnings("SynchronizeOnNonFinalField") public class BatteryStatsCpuTimesTest { + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + @Mock KernelCpuUidUserSysTimeReader mCpuUidUserSysTimeReader; @Mock @@ -128,7 +136,9 @@ public class BatteryStatsCpuTimesTest { initKernelCpuSpeedReaders(numClusters); // RUN - mBatteryStatsImpl.updateCpuTimeLocked(false, false, null); + synchronized (mBatteryStatsImpl) { + mBatteryStatsImpl.updateCpuTimeLocked(false, false, null); + } // VERIFY verify(mCpuUidUserSysTimeReader).readDelta(anyBoolean(), isNull()); @@ -147,7 +157,9 @@ public class BatteryStatsCpuTimesTest { mBatteryStatsImpl.setOnBatteryInternal(true); // RUN - mBatteryStatsImpl.updateCpuTimeLocked(true, false, null); + synchronized (mBatteryStatsImpl) { + mBatteryStatsImpl.updateCpuTimeLocked(true, false, null); + } // VERIFY verify(mUserInfoProvider).refreshUserIds(); @@ -239,7 +251,7 @@ public class BatteryStatsCpuTimesTest { mBatteryStatsImpl.updateClusterSpeedTimes(updatedUids, true, null); // VERIFY - int totalClustersTimeMs = 0; + long totalClustersTimeMs = 0; for (int i = 0; i < clusterSpeedTimesMs.length; ++i) { for (int j = 0; j < clusterSpeedTimesMs[i].length; ++j) { totalClustersTimeMs += clusterSpeedTimesMs[i][j]; diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java index bf5bf365965d..395b3aac8fd5 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import android.os.BatteryManager; import android.os.BatteryStats; import android.os.Process; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -29,6 +30,7 @@ import com.android.internal.os.BatteryStatsHistoryIterator; import com.android.internal.os.MonotonicClock; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,6 +44,11 @@ import java.util.concurrent.Future; @SmallTest @SuppressWarnings("GuardedBy") public class BatteryStatsHistoryIteratorTest { + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42; private final MockClock mMockClock = new MockClock(); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java index 92513760fa4a..c58c92b47dd3 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java @@ -26,7 +26,6 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; -import android.content.Context; import android.os.BatteryConsumer; import android.os.BatteryManager; import android.os.BatteryStats; @@ -39,7 +38,6 @@ import android.telephony.NetworkRegistrationInfo; import android.util.AtomicFile; import android.util.Log; -import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import com.android.internal.os.BatteryStatsHistory; @@ -59,6 +57,7 @@ import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -80,13 +79,14 @@ public class BatteryStatsHistoryTest { private BatteryStatsHistory.TraceDelegate mTracer; @Mock private BatteryStatsHistory.HistoryStepDetailsCalculator mStepDetailsCalculator; + @Mock + private BatteryStatsHistory.EventLogger mEventLogger; private List<String> mReadFiles = new ArrayList<>(); @Before - public void setUp() { + public void setUp() throws IOException { MockitoAnnotations.initMocks(this); - Context context = InstrumentationRegistry.getContext(); - mSystemDir = context.getDataDir(); + mSystemDir = Files.createTempDirectory("BatteryStatsHistoryTest").toFile(); mHistoryDir = new File(mSystemDir, "battery-history"); String[] files = mHistoryDir.list(); if (files != null) { @@ -99,7 +99,7 @@ public class BatteryStatsHistoryTest { mClock.realtime = 123; mHistory = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024, - mStepDetailsCalculator, mClock, mMonotonicClock, mTracer); + mStepDetailsCalculator, mClock, mMonotonicClock, mTracer, mEventLogger); when(mStepDetailsCalculator.getHistoryStepDetails()) .thenReturn(new BatteryStats.HistoryStepDetails()); @@ -238,7 +238,7 @@ public class BatteryStatsHistoryTest { // create a new BatteryStatsHistory object, it will pick up existing history files. BatteryStatsHistory history2 = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024, - null, mClock, mMonotonicClock, mTracer); + null, mClock, mMonotonicClock, mTracer, mEventLogger); // verify constructor can pick up all files from file system. verifyFileNames(history2, fileList); verifyActiveFile(history2, "33000.bh"); @@ -534,7 +534,7 @@ public class BatteryStatsHistoryTest { // Keep the preserved part of history short - we only need to capture the very tail of // history. mHistory = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 1, 6000, - mStepDetailsCalculator, mClock, mMonotonicClock, mTracer); + mStepDetailsCalculator, mClock, mMonotonicClock, mTracer, mEventLogger); mHistory.forceRecordAllHistory(); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java index 8d51592667c8..548fae7a0b01 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java @@ -31,15 +31,18 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.bluetooth.BluetoothActivityEnergyInfo; import android.bluetooth.UidTraffic; import android.content.Context; +import android.hardware.SensorManager; import android.os.BatteryConsumer; import android.os.BatteryManager; import android.os.BatteryStats; @@ -51,11 +54,11 @@ import android.os.HandlerThread; import android.os.Parcel; import android.os.WakeLockStats; import android.os.WorkSource; +import android.platform.test.ravenwood.RavenwoodRule; import android.util.SparseArray; import android.view.Display; import androidx.test.InstrumentationRegistry; -import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; import com.android.internal.os.CpuScalingPolicies; @@ -68,19 +71,26 @@ import com.google.common.collect.ImmutableList; import com.google.common.truth.LongSubject; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import java.time.Instant; import java.util.List; -@LargeTest @RunWith(AndroidJUnit4.class) @SuppressWarnings("GuardedBy") public class BatteryStatsImplTest { + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + @Mock private KernelCpuUidFreqTimeReader mKernelUidCpuFreqTimeReader; @Mock @@ -110,7 +120,7 @@ public class BatteryStatsImplTest { private PowerStatsExporter mPowerStatsExporter; @Before - public void setUp() { + public void setUp() throws IOException { MockitoAnnotations.initMocks(this); when(mKernelUidCpuFreqTimeReader.isFastCpuTimesReader()).thenReturn(true); @@ -128,8 +138,17 @@ public class BatteryStatsImplTest { .setKernelSingleUidTimeReader(mKernelSingleUidTimeReader) .setKernelWakelockReader(mKernelWakelockReader); - final Context context = InstrumentationRegistry.getContext(); - File systemDir = context.getCacheDir(); + File systemDir = Files.createTempDirectory("BatteryStatsHistoryTest").toFile(); + + Context context; + if (RavenwoodRule.isUnderRavenwood()) { + context = mock(Context.class); + SensorManager sensorManager = mock(SensorManager.class); + when(sensorManager.getSensorList(anyInt())).thenReturn(List.of()); + when(context.getSystemService(SensorManager.class)).thenReturn(sensorManager); + } else { + context = InstrumentationRegistry.getContext(); + } mPowerStatsStore = new PowerStatsStore(systemDir, mHandler, new AggregatedPowerStatsConfig()); mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context, mPowerStatsExporter, @@ -747,14 +766,22 @@ public class BatteryStatsImplTest { } private UidTraffic createUidTraffic(int appUid, long rxBytes, long txBytes) { - final Parcel parcel = Parcel.obtain(); - parcel.writeInt(appUid); // mAppUid - parcel.writeLong(rxBytes); // mRxBytes - parcel.writeLong(txBytes); // mTxBytes - parcel.setDataPosition(0); - UidTraffic uidTraffic = UidTraffic.CREATOR.createFromParcel(parcel); - parcel.recycle(); - return uidTraffic; + if (RavenwoodRule.isUnderRavenwood()) { + UidTraffic uidTraffic = mock(UidTraffic.class); + when(uidTraffic.getUid()).thenReturn(appUid); + when(uidTraffic.getRxBytes()).thenReturn(rxBytes); + when(uidTraffic.getTxBytes()).thenReturn(txBytes); + return uidTraffic; + } else { + final Parcel parcel = Parcel.obtain(); + parcel.writeInt(appUid); // mAppUid + parcel.writeLong(rxBytes); // mRxBytes + parcel.writeLong(txBytes); // mTxBytes + parcel.setDataPosition(0); + UidTraffic uidTraffic = UidTraffic.CREATOR.createFromParcel(parcel); + parcel.recycle(); + return uidTraffic; + } } private BluetoothActivityEnergyInfo createBluetoothActivityEnergyInfo( @@ -764,21 +791,31 @@ public class BatteryStatsImplTest { long controllerIdleTimeMs, long controllerEnergyUsed, UidTraffic... uidTraffic) { - Parcel parcel = Parcel.obtain(); - parcel.writeLong(timestamp); // mTimestamp - parcel.writeInt( - BluetoothActivityEnergyInfo.BT_STACK_STATE_STATE_ACTIVE); // mBluetoothStackState - parcel.writeLong(controllerTxTimeMs); // mControllerTxTimeMs; - parcel.writeLong(controllerRxTimeMs); // mControllerRxTimeMs; - parcel.writeLong(controllerIdleTimeMs); // mControllerIdleTimeMs; - parcel.writeLong(controllerEnergyUsed); // mControllerEnergyUsed; - parcel.writeTypedList(ImmutableList.copyOf(uidTraffic)); // mUidTraffic - parcel.setDataPosition(0); - - BluetoothActivityEnergyInfo info = - BluetoothActivityEnergyInfo.CREATOR.createFromParcel(parcel); - parcel.recycle(); - return info; + if (RavenwoodRule.isUnderRavenwood()) { + BluetoothActivityEnergyInfo info = mock(BluetoothActivityEnergyInfo.class); + when(info.getTimestampMillis()).thenReturn(timestamp); + when(info.getControllerTxTimeMillis()).thenReturn(controllerTxTimeMs); + when(info.getControllerRxTimeMillis()).thenReturn(controllerRxTimeMs); + when(info.getControllerIdleTimeMillis()).thenReturn(controllerIdleTimeMs); + when(info.getControllerEnergyUsed()).thenReturn(controllerEnergyUsed); + when(info.getUidTraffic()).thenReturn(ImmutableList.copyOf(uidTraffic)); + return info; + } else { + Parcel parcel = Parcel.obtain(); + parcel.writeLong(timestamp); // mTimestamp + parcel.writeInt(BluetoothActivityEnergyInfo.BT_STACK_STATE_STATE_ACTIVE); + parcel.writeLong(controllerTxTimeMs); // mControllerTxTimeMs; + parcel.writeLong(controllerRxTimeMs); // mControllerRxTimeMs; + parcel.writeLong(controllerIdleTimeMs); // mControllerIdleTimeMs; + parcel.writeLong(controllerEnergyUsed); // mControllerEnergyUsed; + parcel.writeTypedList(ImmutableList.copyOf(uidTraffic)); // mUidTraffic + parcel.setDataPosition(0); + + BluetoothActivityEnergyInfo info = + BluetoothActivityEnergyInfo.CREATOR.createFromParcel(parcel); + parcel.recycle(); + return info; + } } @Test diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsNoteTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsNoteTest.java index eea287568e13..07cefa9ae878 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsNoteTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsNoteTest.java @@ -28,6 +28,10 @@ import static com.android.server.power.stats.BatteryStatsImpl.ExternalStatsSync. import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import android.app.ActivityManager; @@ -40,6 +44,7 @@ import android.os.Looper; import android.os.Process; import android.os.UserHandle; import android.os.WorkSource; +import android.platform.test.ravenwood.RavenwoodRule; import android.telephony.AccessNetworkConstants; import android.telephony.ActivityStatsTechSpecificInfo; import android.telephony.Annotation; @@ -50,7 +55,6 @@ import android.telephony.NetworkRegistrationInfo; import android.telephony.ServiceState; import android.telephony.TelephonyManager; import android.util.Log; -import android.util.MutableInt; import android.util.SparseIntArray; import android.util.SparseLongArray; import android.view.Display; @@ -63,8 +67,8 @@ import com.android.internal.os.PowerProfile; import com.android.internal.power.EnergyConsumerStats; import com.android.server.power.stats.BatteryStatsImpl.DualTimer; -import junit.framework.TestCase; - +import org.junit.Rule; +import org.junit.Test; import org.mockito.Mock; import java.util.ArrayList; @@ -78,7 +82,14 @@ import java.util.function.IntConsumer; * Test various BatteryStatsImpl noteStart methods. */ @SuppressWarnings("GuardedBy") -public class BatteryStatsNoteTest extends TestCase { +@SmallTest +public class BatteryStatsNoteTest { + + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final String TAG = BatteryStatsNoteTest.class.getSimpleName(); private static final int UID = 10500; @@ -96,7 +107,7 @@ public class BatteryStatsNoteTest extends TestCase { /** * Test BatteryStatsImpl.Uid.noteBluetoothScanResultLocked. */ - @SmallTest + @Test public void testNoteBluetoothScanResultLocked() throws Exception { MockBatteryStatsImpl bi = new MockBatteryStatsImpl(new MockClock()); bi.updateTimeBasesLocked(true, Display.STATE_OFF, 0, 0); @@ -125,7 +136,7 @@ public class BatteryStatsNoteTest extends TestCase { /** * Test BatteryStatsImpl.Uid.noteStartWakeLocked. */ - @SmallTest + @Test public void testNoteStartWakeLocked() throws Exception { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -155,7 +166,7 @@ public class BatteryStatsNoteTest extends TestCase { /** * Test BatteryStatsImpl.Uid.noteStartWakeLocked for an isolated uid. */ - @SmallTest + @Test public void testNoteStartWakeLocked_isolatedUid() throws Exception { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms PowerStatsUidResolver uidResolver = new PowerStatsUidResolver(); @@ -197,7 +208,7 @@ public class BatteryStatsNoteTest extends TestCase { * Test BatteryStatsImpl.Uid.noteStartWakeLocked for an isolated uid, with a race where the * isolated uid is removed from batterystats before the wakelock has been stopped. */ - @SmallTest + @Test public void testNoteStartWakeLocked_isolatedUidRace() throws Exception { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms PowerStatsUidResolver uidResolver = new PowerStatsUidResolver(); @@ -241,7 +252,7 @@ public class BatteryStatsNoteTest extends TestCase { /** * Test BatteryStatsImpl.Uid.noteLongPartialWakelockStart for an isolated uid. */ - @SmallTest + @Test public void testNoteLongPartialWakelockStart_isolatedUid() throws Exception { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms PowerStatsUidResolver uidResolver = new PowerStatsUidResolver(); @@ -296,7 +307,7 @@ public class BatteryStatsNoteTest extends TestCase { /** * Test BatteryStatsImpl.Uid.noteLongPartialWakelockStart for an isolated uid. */ - @SmallTest + @Test public void testNoteLongPartialWakelockStart_isolatedUidRace() throws Exception { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms PowerStatsUidResolver uidResolver = new PowerStatsUidResolver(); @@ -354,7 +365,7 @@ public class BatteryStatsNoteTest extends TestCase { /** * Test BatteryStatsImpl.noteUidProcessStateLocked. */ - @SmallTest + @Test public void testNoteUidProcessStateLocked() throws Exception { final MockClock clocks = new MockClock(); MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -441,7 +452,7 @@ public class BatteryStatsNoteTest extends TestCase { /** * Test BatteryStatsImpl.updateTimeBasesLocked. */ - @SmallTest + @Test public void testUpdateTimeBasesLocked() throws Exception { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -465,7 +476,7 @@ public class BatteryStatsNoteTest extends TestCase { /** * Test BatteryStatsImpl.noteScreenStateLocked sets timebases and screen states correctly. */ - @SmallTest + @Test public void testNoteScreenStateLocked() throws Exception { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -512,7 +523,7 @@ public class BatteryStatsNoteTest extends TestCase { * Test BatteryStatsImpl.noteScreenStateLocked sets timebases and screen states correctly for * multi display devices */ - @SmallTest + @Test public void testNoteScreenStateLocked_multiDisplay() throws Exception { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -606,7 +617,7 @@ public class BatteryStatsNoteTest extends TestCase { * Off ------- ---------------------- * Doze ---------------- */ - @SmallTest + @Test public void testNoteScreenStateTimersLocked() throws Exception { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -656,7 +667,7 @@ public class BatteryStatsNoteTest extends TestCase { * Test BatteryStatsImpl.noteScreenStateLocked updates timers correctly for multi display * devices. */ - @SmallTest + @Test public void testNoteScreenStateTimersLocked_multiDisplay() throws Exception { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -807,7 +818,7 @@ public class BatteryStatsNoteTest extends TestCase { /** * Test BatteryStatsImpl.noteScreenBrightnessLocked updates timers correctly. */ - @SmallTest + @Test public void testScreenBrightnessLocked_multiDisplay() throws Exception { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -929,7 +940,7 @@ public class BatteryStatsNoteTest extends TestCase { checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs); } - @SmallTest + @Test public void testAlarmStartAndFinishLocked() throws Exception { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -967,7 +978,7 @@ public class BatteryStatsNoteTest extends TestCase { assertThat(iterator.next()).isNull(); } - @SmallTest + @Test public void testAlarmStartAndFinishLocked_workSource() throws Exception { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -1013,7 +1024,7 @@ public class BatteryStatsNoteTest extends TestCase { assertEquals(500, item.eventTag.uid); } - @SmallTest + @Test public void testNoteWakupAlarmLocked() { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -1031,7 +1042,7 @@ public class BatteryStatsNoteTest extends TestCase { assertEquals(1, pkg.getWakeupAlarmStats().size()); } - @SmallTest + @Test public void testNoteWakupAlarmLocked_workSource_uid() { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -1064,7 +1075,7 @@ public class BatteryStatsNoteTest extends TestCase { assertEquals(1, pkg.getWakeupAlarmStats().size()); } - @SmallTest + @Test public void testNoteWakupAlarmLocked_workSource_workChain() { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -1090,7 +1101,7 @@ public class BatteryStatsNoteTest extends TestCase { assertEquals(0, pkg.getWakeupAlarmStats().size()); } - @SmallTest + @Test public void testNoteGpsChanged() { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -1114,7 +1125,7 @@ public class BatteryStatsNoteTest extends TestCase { assertFalse(t.isRunningLocked()); } - @SmallTest + @Test public void testNoteGpsChanged_workSource() { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -1138,7 +1149,7 @@ public class BatteryStatsNoteTest extends TestCase { assertFalse(t.isRunningLocked()); } - @SmallTest + @Test public void testUpdateDisplayMeasuredEnergyStatsLocked() { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -1223,7 +1234,7 @@ public class BatteryStatsNoteTest extends TestCase { checkMeasuredCharge("H", uid1, blame1, uid2, blame2, globalDoze, bi); } - @SmallTest + @Test public void testUpdateCustomMeasuredEnergyStatsLocked_neverCalled() { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -1237,7 +1248,7 @@ public class BatteryStatsNoteTest extends TestCase { checkCustomBatteryConsumption("0", 0, 0, uid1, 0, 0, uid2, 0, 0, bi); } - @SmallTest + @Test public void testUpdateCustomMeasuredEnergyStatsLocked() { final MockClock clocks = new MockClock(); // holds realtime and uptime in ms final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -1314,7 +1325,7 @@ public class BatteryStatsNoteTest extends TestCase { "D", totalBlameA, totalBlameB, uid1, blame1A, blame1B, uid2, blame2A, blame2B, bi); } - @SmallTest + @Test public void testGetPerStateActiveRadioDurationMs_noModemActivity() { final MockClock clock = new MockClock(); // holds realtime and uptime in ms final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clock); @@ -1470,7 +1481,7 @@ public class BatteryStatsNoteTest extends TestCase { expectedTxDurationsMs, bi, state.currentTimeMs); } - @SmallTest + @Test public void testGetPerStateActiveRadioDurationMs_initialModemActivity() { final MockClock clock = new MockClock(); // holds realtime and uptime in ms final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clock); @@ -1612,7 +1623,7 @@ public class BatteryStatsNoteTest extends TestCase { expectedTxDurationsMs, bi, state.currentTimeMs); } - @SmallTest + @Test public void testGetPerStateActiveRadioDurationMs_withModemActivity() { final MockClock clock = new MockClock(); // holds realtime and uptime in ms final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clock); @@ -1852,7 +1863,7 @@ public class BatteryStatsNoteTest extends TestCase { expectedTxDurationsMs, bi, state.currentTimeMs); } - @SmallTest + @Test public void testGetPerStateActiveRadioDurationMs_withSpecificInfoModemActivity() { final MockClock clock = new MockClock(); // holds realtime and uptime in ms final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clock); @@ -2145,19 +2156,19 @@ public class BatteryStatsNoteTest extends TestCase { expectedTxDurationsMs, bi, state.currentTimeMs); } - @SmallTest + @Test @SuppressWarnings("GuardedBy") public void testProcStateSyncScheduling_mobileRadioActiveState() { final MockClock clock = new MockClock(); // holds realtime and uptime in ms final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clock); - final MutableInt lastProcStateChangeFlags = new MutableInt(0); + final int[] lastProcStateChangeFlags = new int[1]; MockBatteryStatsImpl.DummyExternalStatsSync externalStatsSync = new MockBatteryStatsImpl.DummyExternalStatsSync() { @Override public void scheduleSyncDueToProcessStateChange(int flags, long delayMillis) { - lastProcStateChangeFlags.value = flags; + lastProcStateChangeFlags[0] = flags; } }; @@ -2170,19 +2181,19 @@ public class BatteryStatsNoteTest extends TestCase { bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH, curr, UID); - lastProcStateChangeFlags.value = 0; + lastProcStateChangeFlags[0] = 0; clock.realtime = clock.uptime = 2002; bi.noteUidProcessStateLocked(UID, ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND); final int allProcFlags = BatteryStatsImpl.ExternalStatsSync.UPDATE_ON_PROC_STATE_CHANGE; - assertEquals(allProcFlags, lastProcStateChangeFlags.value); + assertEquals(allProcFlags, lastProcStateChangeFlags[0]); // Note mobile radio is off. curr = 1000L * (clock.realtime = clock.uptime = 3003); bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_LOW, curr, UID); - lastProcStateChangeFlags.value = 0; + lastProcStateChangeFlags[0] = 0; clock.realtime = clock.uptime = 4004; bi.noteUidProcessStateLocked(UID, ActivityManager.PROCESS_STATE_CACHED_EMPTY); @@ -2190,10 +2201,10 @@ public class BatteryStatsNoteTest extends TestCase { & ~BatteryStatsImpl.ExternalStatsSync.UPDATE_RADIO; assertEquals( "An inactive radio should not be queried on proc state change", - noRadioProcFlags, lastProcStateChangeFlags.value); + noRadioProcFlags, lastProcStateChangeFlags[0]); } - @SmallTest + @Test public void testNoteMobileRadioPowerStateLocked() { long curr; boolean update; @@ -2243,7 +2254,7 @@ public class BatteryStatsNoteTest extends TestCase { update); } - @SmallTest + @Test public void testNoteMobileRadioPowerStateLocked_rateLimited() { long curr; boolean update; diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsSensorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsSensorTest.java index 9c70f376ca14..96780c322445 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsSensorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsSensorTest.java @@ -16,24 +16,36 @@ package com.android.server.power.stats; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + import android.app.ActivityManager; import android.os.BatteryStats; +import android.platform.test.ravenwood.RavenwoodRule; import android.view.Display; import androidx.test.filters.SmallTest; -import junit.framework.TestCase; +import org.junit.Rule; +import org.junit.Test; /** * Test BatteryStatsImpl Sensor Timers. */ -public class BatteryStatsSensorTest extends TestCase { +@SmallTest +public class BatteryStatsSensorTest { + + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); private static final int UID = 10500; private static final int UID_2 = 10501; // second uid for testing pool usage private static final int SENSOR_ID = -10000; - @SmallTest + @Test public void testSensorStartStop() throws Exception { final MockClock clocks = new MockClock(); MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -70,7 +82,7 @@ public class BatteryStatsSensorTest extends TestCase { clocks.realtime * 1000, BatteryStats.STATS_SINCE_CHARGED)); } - @SmallTest + @Test public void testCountingWhileOffBattery() throws Exception { final MockClock clocks = new MockClock(); MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -106,7 +118,7 @@ public class BatteryStatsSensorTest extends TestCase { assertEquals(0, sensorTimer.getCountLocked(BatteryStats.STATS_SINCE_CHARGED)); } - @SmallTest + @Test public void testCountingWhileOnBattery() throws Exception { final MockClock clocks = new MockClock(); MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -141,7 +153,7 @@ public class BatteryStatsSensorTest extends TestCase { assertEquals(1, sensorTimer.getCountLocked(BatteryStats.STATS_SINCE_CHARGED)); } - @SmallTest + @Test public void testBatteryStatusOnToOff() throws Exception { final MockClock clocks = new MockClock(); MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -187,7 +199,7 @@ public class BatteryStatsSensorTest extends TestCase { sensorTimer.getTotalTimeLocked(curr, BatteryStats.STATS_SINCE_CHARGED)); } - @SmallTest + @Test public void testBatteryStatusOffToOn() throws Exception { final MockClock clocks = new MockClock(); MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -238,7 +250,7 @@ public class BatteryStatsSensorTest extends TestCase { assertEquals(0, sensorTimer.getCountLocked(BatteryStats.STATS_SINCE_CHARGED)); } - @SmallTest + @Test public void testPooledBackgroundUsage() throws Exception { final MockClock clocks = new MockClock(); MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -375,7 +387,7 @@ public class BatteryStatsSensorTest extends TestCase { assertEquals(2, bgTimer2.getCountLocked(BatteryStats.STATS_SINCE_CHARGED)); } - @SmallTest + @Test public void testSensorReset() throws Exception { final MockClock clocks = new MockClock(); MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); @@ -419,7 +431,7 @@ public class BatteryStatsSensorTest extends TestCase { assertNull(sensor); } - @SmallTest + @Test public void testSensorResetTimes() throws Exception { final MockClock clocks = new MockClock(); MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsServTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsServTest.java index 200eb1d0ad15..6f683ae5e0c0 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsServTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsServTest.java @@ -18,16 +18,25 @@ package com.android.server.power.stats; import android.os.BatteryStats; import android.os.Parcel; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import junit.framework.Assert; -import junit.framework.TestCase; + +import org.junit.Rule; +import org.junit.Test; /** * Provides test cases for android.os.BatteryStats. */ -public class BatteryStatsServTest extends TestCase { +@SmallTest +public class BatteryStatsServTest { + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final String TAG = "BatteryStatsServTest"; public static class TestServ extends BatteryStatsImpl.Uid.Pkg.Serv { @@ -90,7 +99,7 @@ public class BatteryStatsServTest extends TestCase { /** * Test that the constructor and detach methods touch the time bast observer list. */ - @SmallTest + @Test public void testConstructAndDetach() throws Exception { MockBatteryStatsImpl bsi = new MockBatteryStatsImpl(); @@ -104,7 +113,7 @@ public class BatteryStatsServTest extends TestCase { /** * Test parceling and unparceling. */ - @SmallTest + @Test public void testParceling() throws Exception { MockBatteryStatsImpl bsi = new MockBatteryStatsImpl(); TestServ orig = new TestServ(bsi); @@ -133,7 +142,7 @@ public class BatteryStatsServTest extends TestCase { /** * Test getLaunchTimeToNow() */ - @SmallTest + @Test public void testLaunchTimeToNow() throws Exception { MockBatteryStatsImpl bsi = new MockBatteryStatsImpl(); TestServ serv = new TestServ(bsi); @@ -151,7 +160,7 @@ public class BatteryStatsServTest extends TestCase { /** * Test getStartTimeToNow() */ - @SmallTest + @Test public void testStartTimeToNow() throws Exception { MockBatteryStatsImpl bsi = new MockBatteryStatsImpl(); TestServ serv = new TestServ(bsi); @@ -168,7 +177,7 @@ public class BatteryStatsServTest extends TestCase { /** * Test startLaunchedLocked while not previously launched */ - @SmallTest + @Test public void testStartLaunchedLockedWhileLaunched() throws Exception { MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() { @Override @@ -197,7 +206,7 @@ public class BatteryStatsServTest extends TestCase { /** * Test startLaunchedLocked while previously launched */ - @SmallTest + @Test public void testStartLaunchedLockedWhileNotLaunched() throws Exception { MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() { @Override @@ -224,7 +233,7 @@ public class BatteryStatsServTest extends TestCase { /** * Test stopLaunchedLocked when not previously launched. */ - @SmallTest + @Test public void testStopLaunchedLockedWhileNotLaunched() throws Exception { MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() { @Override @@ -254,7 +263,7 @@ public class BatteryStatsServTest extends TestCase { * Test stopLaunchedLocked when previously launched, with measurable time between * start and stop. */ - @SmallTest + @Test public void testStopLaunchedLockedWhileLaunchedNormal() throws Exception { MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() { @Override @@ -283,7 +292,7 @@ public class BatteryStatsServTest extends TestCase { * Test stopLaunchedLocked when previously launched, with no measurable time between * start and stop. */ - @SmallTest + @Test public void testStopLaunchedLockedWhileLaunchedTooQuick() throws Exception { MockBatteryStatsImpl bsi = new MockBatteryStatsImpl(); TestServ serv = new TestServ(bsi); @@ -306,7 +315,7 @@ public class BatteryStatsServTest extends TestCase { /** * Test startRunningLocked while previously running */ - @SmallTest + @Test public void testStartRunningLockedWhileRunning() throws Exception { MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() { @Override @@ -335,7 +344,7 @@ public class BatteryStatsServTest extends TestCase { /** * Test startRunningLocked while not previously launched */ - @SmallTest + @Test public void testStartRunningLockedWhileNotRunning() throws Exception { MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() { @Override @@ -364,7 +373,7 @@ public class BatteryStatsServTest extends TestCase { * Test stopRunningLocked when previously launched, with measurable time between * start and stop. */ - @SmallTest + @Test public void testStopRunningLockedWhileRunningNormal() throws Exception { MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() { @Override @@ -393,7 +402,7 @@ public class BatteryStatsServTest extends TestCase { * Test stopRunningLocked when previously launched, with measurable time between * start and stop. */ - @SmallTest + @Test public void testStopRunningLockedWhileRunningTooQuick() throws Exception { MockBatteryStatsImpl bsi = new MockBatteryStatsImpl(); TestServ serv = new TestServ(bsi); @@ -416,7 +425,7 @@ public class BatteryStatsServTest extends TestCase { /** * Test that getBatteryStats returns the BatteryStatsImpl passed in to the contstructor. */ - @SmallTest + @Test public void testGetBatteryStats() throws Exception { MockBatteryStatsImpl bsi = new MockBatteryStatsImpl(); TestServ serv = new TestServ(bsi); @@ -427,7 +436,7 @@ public class BatteryStatsServTest extends TestCase { /** * Test getLaunches */ - @SmallTest + @Test public void testGetLaunches() throws Exception { MockBatteryStatsImpl bsi = new MockBatteryStatsImpl(); TestServ serv = new TestServ(bsi); @@ -449,7 +458,7 @@ public class BatteryStatsServTest extends TestCase { /** * Test getStartTime while running */ - @SmallTest + @Test public void testGetStartTimeRunning() throws Exception { MockBatteryStatsImpl bsi = new MockBatteryStatsImpl(); TestServ serv = new TestServ(bsi); @@ -475,7 +484,7 @@ public class BatteryStatsServTest extends TestCase { /** * Test getStartTime while not running */ - @SmallTest + @Test public void testGetStartTimeNotRunning() throws Exception { MockBatteryStatsImpl bsi = new MockBatteryStatsImpl(); TestServ serv = new TestServ(bsi); @@ -502,7 +511,7 @@ public class BatteryStatsServTest extends TestCase { /** * Test getStarts */ - @SmallTest + @Test public void testGetStarts() throws Exception { MockBatteryStatsImpl bsi = new MockBatteryStatsImpl(); TestServ serv = new TestServ(bsi); @@ -521,4 +530,3 @@ public class BatteryStatsServTest extends TestCase { Assert.assertEquals(8085, serv.getLaunches()); } } - diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java index face849620d7..05d8a005d21e 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java @@ -46,6 +46,7 @@ import java.util.concurrent.TimeUnit; @LargeTest @RunWith(AndroidJUnit4.class) +@android.platform.test.annotations.IgnoreUnderRavenwood public class BatteryStatsUserLifecycleTests { private static final long POLL_INTERVAL_MS = 500; diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java index 2e0ba0083850..6cd79bc09fb6 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.content.Context; +import android.hardware.SensorManager; import android.os.BatteryConsumer; import android.os.BatteryManager; import android.os.BatteryStats; @@ -32,6 +33,7 @@ import android.os.ConditionVariable; import android.os.Parcel; import android.os.Process; import android.os.UidBatteryConsumer; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; @@ -40,36 +42,69 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.os.BatteryStatsHistoryIterator; import com.android.internal.os.PowerProfile; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import java.util.List; -import java.util.Random; @SmallTest @RunWith(AndroidJUnit4.class) public class BatteryUsageStatsProviderTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42; private static final long MINUTE_IN_MS = 60 * 1000; private static final double PRECISION = 0.00001; - private final File mHistoryDir = createTemporaryDirectory(); + private File mHistoryDir; - @Rule + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule(12345, mHistoryDir) .setAveragePower(PowerProfile.POWER_FLASHLIGHT, 360.0) .setAveragePower(PowerProfile.POWER_AUDIO, 720.0); + private MockClock mMockClock = mStatsRule.getMockClock(); + private Context mContext; + + @Before + public void setup() throws IOException { + mHistoryDir = Files.createTempDirectory("BatteryUsageStatsProviderTest").toFile(); + clearDirectory(mHistoryDir); + + if (RavenwoodRule.isUnderRavenwood()) { + mContext = mock(Context.class); + SensorManager sensorManager = mock(SensorManager.class); + when(mContext.getSystemService(SensorManager.class)).thenReturn(sensorManager); + } else { + mContext = InstrumentationRegistry.getContext(); + } + } + + private void clearDirectory(File dir) { + if (dir.exists()) { + for (File child : dir.listFiles()) { + if (child.isDirectory()) { + clearDirectory(child); + } + child.delete(); + } + } + } @Test public void test_getBatteryUsageStats() { BatteryStatsImpl batteryStats = prepareBatteryStats(); - Context context = InstrumentationRegistry.getContext(); - BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, null, + BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null, mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), null, mMockClock); final BatteryUsageStats batteryUsageStats = @@ -105,8 +140,7 @@ public class BatteryUsageStatsProviderTest { public void test_selectPowerComponents() { BatteryStatsImpl batteryStats = prepareBatteryStats(); - Context context = InstrumentationRegistry.getContext(); - BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, null, + BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null, mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), null, mMockClock); final BatteryUsageStats batteryUsageStats = @@ -211,8 +245,7 @@ public class BatteryUsageStatsProviderTest { batteryStats.noteAlarmFinishLocked("foo", null, APP_UID, 3_001_000, 2_001_000); } - Context context = InstrumentationRegistry.getContext(); - BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, null, + BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null, mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), null, mMockClock); final BatteryUsageStats batteryUsageStats = @@ -300,8 +333,7 @@ public class BatteryUsageStatsProviderTest { } } - Context context = InstrumentationRegistry.getContext(); - BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, null, + BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null, mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), null, mMockClock); final BatteryUsageStats batteryUsageStats = @@ -311,7 +343,9 @@ public class BatteryUsageStatsProviderTest { Parcel parcel = Parcel.obtain(); parcel.writeParcelable(batteryUsageStats, 0); - assertThat(parcel.dataSize()).isAtMost(128_000); + if (!RavenwoodRule.isUnderRavenwood()) { + assertThat(parcel.dataSize()).isAtMost(128_000); + } parcel.setDataPosition(0); @@ -375,7 +409,6 @@ public class BatteryUsageStatsProviderTest { @Test public void testAggregateBatteryStats() { - Context context = InstrumentationRegistry.getContext(); BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats(); setTime(5 * MINUTE_IN_MS); @@ -384,11 +417,11 @@ public class BatteryUsageStatsProviderTest { } PowerStatsStore powerStatsStore = new PowerStatsStore( - new File(context.getCacheDir(), "BatteryUsageStatsProviderTest"), + new File(mHistoryDir, "powerstatsstore"), mStatsRule.getHandler(), null); powerStatsStore.reset(); - BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, null, + BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null, mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), powerStatsStore, mMockClock); @@ -485,7 +518,6 @@ public class BatteryUsageStatsProviderTest { @Test public void testAggregateBatteryStats_incompatibleSnapshot() { - Context context = InstrumentationRegistry.getContext(); MockBatteryStatsImpl batteryStats = mStatsRule.getBatteryStats(); batteryStats.initMeasuredEnergyStats(new String[]{"FOO", "BAR"}); @@ -511,7 +543,7 @@ public class BatteryUsageStatsProviderTest { when(powerStatsStore.loadPowerStatsSpan(1, BatteryUsageStatsSection.TYPE)) .thenReturn(span1); - BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, null, + BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null, mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), powerStatsStore, mMockClock); @@ -523,20 +555,4 @@ public class BatteryUsageStatsProviderTest { .isEqualTo(batteryStats.getCustomEnergyConsumerNames()); assertThat(stats.getStatsDuration()).isEqualTo(1234); } - - private static final Random sRandom = new Random(); - - /** - * Creates a unique new temporary directory under "java.io.tmpdir". - */ - private static File createTemporaryDirectory() { - while (true) { - String candidateName = - BatteryUsageStatsProviderTest.class.getSimpleName() + sRandom.nextInt(); - File result = new File(System.getProperty("java.io.tmpdir"), candidateName); - if (result.mkdir()) { - return result; - } - } - } } diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java index ba2b53854cd7..8bdb0292bf00 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java @@ -49,6 +49,7 @@ import org.mockito.stubbing.Answer; import java.io.File; import java.util.Arrays; +@SuppressWarnings("SynchronizeOnNonFinalField") public class BatteryUsageStatsRule implements TestRule { public static final BatteryUsageStatsQuery POWER_PROFILE_MODEL_ONLY = new BatteryUsageStatsQuery.Builder() @@ -71,6 +72,8 @@ public class BatteryUsageStatsRule implements TestRule { private int mDisplayCount = -1; private int mPerUidModemModel = -1; private NetworkStats mNetworkStats; + private boolean[] mSupportedStandardBuckets; + private String[] mCustomPowerComponentNames; public BatteryUsageStatsRule() { this(0, null); @@ -102,6 +105,11 @@ public class BatteryUsageStatsRule implements TestRule { mBatteryStats = new MockBatteryStatsImpl(mMockClock, mHistoryDir, mHandler); mBatteryStats.setPowerProfile(mPowerProfile); mBatteryStats.setCpuScalingPolicies(new CpuScalingPolicies(mCpusByPolicy, mFreqsByPolicy)); + synchronized (mBatteryStats) { + mBatteryStats.initEnergyConsumerStatsLocked(mSupportedStandardBuckets, + mCustomPowerComponentNames); + } + mBatteryStats.informThatAllExternalStatsAreFlushed(); mBatteryStats.onSystemReady(); @@ -230,13 +238,15 @@ public class BatteryUsageStatsRule implements TestRule { /** Call only after setting the power profile information. */ public BatteryUsageStatsRule initMeasuredEnergyStatsLocked( String[] customPowerComponentNames) { - final boolean[] supportedStandardBuckets = - new boolean[EnergyConsumerStats.NUMBER_STANDARD_POWER_BUCKETS]; - Arrays.fill(supportedStandardBuckets, true); - synchronized (mBatteryStats) { - mBatteryStats.initEnergyConsumerStatsLocked(supportedStandardBuckets, - customPowerComponentNames); - mBatteryStats.informThatAllExternalStatsAreFlushed(); + mCustomPowerComponentNames = customPowerComponentNames; + mSupportedStandardBuckets = new boolean[EnergyConsumerStats.NUMBER_STANDARD_POWER_BUCKETS]; + Arrays.fill(mSupportedStandardBuckets, true); + if (mBatteryStats != null) { + synchronized (mBatteryStats) { + mBatteryStats.initEnergyConsumerStatsLocked(mSupportedStandardBuckets, + mCustomPowerComponentNames); + mBatteryStats.informThatAllExternalStatsAreFlushed(); + } } return this; } diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java index 079ea2c7832f..851cf4a535a2 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java @@ -37,6 +37,7 @@ import android.os.BatteryUsageStats; import android.os.Parcel; import android.os.UidBatteryConsumer; import android.os.UserBatteryConsumer; +import android.platform.test.ravenwood.RavenwoodRule; import android.util.Xml; import androidx.test.filters.SmallTest; @@ -45,6 +46,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -62,6 +64,10 @@ import java.util.stream.Collectors; @SmallTest @RunWith(AndroidJUnit4.class) public class BatteryUsageStatsTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); private static final int USER_ID = 42; private static final int APP_UID1 = 271; @@ -115,12 +121,13 @@ public class BatteryUsageStatsTest { final Parcel parcel = Parcel.obtain(); parcel.writeParcelable(outBatteryUsageStats, 0); - assertThat(parcel.dataSize()).isLessThan(2000); + // Under ravenwood this parcel is larger. On a device, 2K would suffice + assertThat(parcel.dataSize()).isLessThan(128_000); parcel.setDataPosition(0); final BatteryUsageStats inBatteryUsageStats = - parcel.readParcelable(getClass().getClassLoader()); + parcel.readParcelable(getClass().getClassLoader(), BatteryUsageStats.class); parcel.recycle(); assertThat(inBatteryUsageStats.getUidBatteryConsumers()).hasSize(uidCount); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerCalculatorTest.java index 4d4337c16757..fe6424f91d83 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerCalculatorTest.java @@ -18,6 +18,9 @@ package com.android.server.power.stats; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import android.annotation.Nullable; import android.bluetooth.BluetoothActivityEnergyInfo; import android.bluetooth.UidTraffic; @@ -28,6 +31,7 @@ import android.os.Parcel; import android.os.Process; import android.os.UidBatteryConsumer; import android.os.WorkSource; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -46,10 +50,15 @@ import java.util.List; @SmallTest @SuppressWarnings("GuardedBy") public class BluetoothPowerCalculatorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final double PRECISION = 0.00001; private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42; - @Rule + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() .setAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_IDLE, 10.0) .setAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_RX, 50.0) @@ -331,33 +340,53 @@ public class BluetoothPowerCalculatorTest { assertThat(usageDurationMillis).isEqualTo(durationMs); } - private UidTraffic createUidTraffic(int uid, long traffic1, long traffic2) { - final Parcel uidTrafficParcel = Parcel.obtain(); - uidTrafficParcel.writeInt(uid); - uidTrafficParcel.writeLong(traffic1); - uidTrafficParcel.writeLong(traffic2); - uidTrafficParcel.setDataPosition(0); - - UidTraffic traffic = UidTraffic.CREATOR.createFromParcel(uidTrafficParcel); - uidTrafficParcel.recycle(); - return traffic; + private UidTraffic createUidTraffic(int appUid, long rxBytes, long txBytes) { + if (RavenwoodRule.isUnderRavenwood()) { + UidTraffic uidTraffic = mock(UidTraffic.class); + when(uidTraffic.getUid()).thenReturn(appUid); + when(uidTraffic.getRxBytes()).thenReturn(rxBytes); + when(uidTraffic.getTxBytes()).thenReturn(txBytes); + return uidTraffic; + } else { + final Parcel uidTrafficParcel = Parcel.obtain(); + uidTrafficParcel.writeInt(appUid); + uidTrafficParcel.writeLong(rxBytes); + uidTrafficParcel.writeLong(txBytes); + uidTrafficParcel.setDataPosition(0); + + UidTraffic traffic = UidTraffic.CREATOR.createFromParcel(uidTrafficParcel); + uidTrafficParcel.recycle(); + return traffic; + } } private BluetoothActivityEnergyInfo createBtEnergyInfo(long timestamp, int stackState, long txTime, long rxTime, long idleTime, long energyUsed, List<UidTraffic> traffic) { - final Parcel btActivityEnergyInfoParcel = Parcel.obtain(); - btActivityEnergyInfoParcel.writeLong(timestamp); - btActivityEnergyInfoParcel.writeInt(stackState); - btActivityEnergyInfoParcel.writeLong(txTime); - btActivityEnergyInfoParcel.writeLong(rxTime); - btActivityEnergyInfoParcel.writeLong(idleTime); - btActivityEnergyInfoParcel.writeLong(energyUsed); - btActivityEnergyInfoParcel.writeTypedList(traffic); - btActivityEnergyInfoParcel.setDataPosition(0); - - BluetoothActivityEnergyInfo info = BluetoothActivityEnergyInfo.CREATOR - .createFromParcel(btActivityEnergyInfoParcel); - btActivityEnergyInfoParcel.recycle(); - return info; + if (RavenwoodRule.isUnderRavenwood()) { + BluetoothActivityEnergyInfo info = mock(BluetoothActivityEnergyInfo.class); + when(info.getTimestampMillis()).thenReturn(timestamp); + when(info.getBluetoothStackState()).thenReturn(stackState); + when(info.getControllerTxTimeMillis()).thenReturn(txTime); + when(info.getControllerRxTimeMillis()).thenReturn(rxTime); + when(info.getControllerIdleTimeMillis()).thenReturn(idleTime); + when(info.getControllerEnergyUsed()).thenReturn(energyUsed); + when(info.getUidTraffic()).thenReturn(ImmutableList.copyOf(traffic)); + return info; + } else { + final Parcel btActivityEnergyInfoParcel = Parcel.obtain(); + btActivityEnergyInfoParcel.writeLong(timestamp); + btActivityEnergyInfoParcel.writeInt(stackState); + btActivityEnergyInfoParcel.writeLong(txTime); + btActivityEnergyInfoParcel.writeLong(rxTime); + btActivityEnergyInfoParcel.writeLong(idleTime); + btActivityEnergyInfoParcel.writeLong(energyUsed); + btActivityEnergyInfoParcel.writeTypedList(traffic); + btActivityEnergyInfoParcel.setDataPosition(0); + + BluetoothActivityEnergyInfo info = BluetoothActivityEnergyInfo.CREATOR + .createFromParcel(btActivityEnergyInfoParcel); + btActivityEnergyInfoParcel.recycle(); + return info; + } } } diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BstatsCpuTimesValidationTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BstatsCpuTimesValidationTest.java index ccace40bb056..29e2f5ee163a 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BstatsCpuTimesValidationTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BstatsCpuTimesValidationTest.java @@ -75,6 +75,7 @@ import java.util.regex.Pattern; @LargeTest @RunWith(AndroidJUnit4.class) +@android.platform.test.annotations.IgnoreUnderRavenwood public class BstatsCpuTimesValidationTest { private static final String TAG = BstatsCpuTimesValidationTest.class.getSimpleName(); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java index 5fce32f0598a..7225f2d3995c 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import android.os.BatteryConsumer; import android.os.Process; import android.os.UidBatteryConsumer; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -34,12 +35,17 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SmallTest public class CameraPowerCalculatorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final double PRECISION = 0.00001; private static final int APP1_UID = Process.FIRST_APPLICATION_UID + 42; private static final int APP2_UID = Process.FIRST_APPLICATION_UID + 43; - @Rule + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() .setAveragePower(PowerProfile.POWER_CAMERA, 360.0) .initMeasuredEnergyStatsLocked(); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuAggregatedPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuAggregatedPowerStatsProcessorTest.java index 993d834b9500..5c0e26887505 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuAggregatedPowerStatsProcessorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuAggregatedPowerStatsProcessorTest.java @@ -33,6 +33,7 @@ import static org.junit.Assert.fail; import android.os.BatteryConsumer; import android.os.PersistableBundle; +import android.platform.test.ravenwood.RavenwoodRule; import android.util.LongArray; import androidx.test.filters.SmallTest; @@ -55,7 +56,12 @@ import java.util.Map; @RunWith(AndroidJUnit4.class) @SmallTest public class CpuAggregatedPowerStatsProcessorTest { - @Rule + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() .setAveragePower(PowerProfile.POWER_CPU_ACTIVE, 720) .setCpuScalingPolicy(0, new int[]{0, 1}, new int[]{100, 200}) diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerCalculatorTest.java index 888bc623f669..71a65c85d9e1 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerCalculatorTest.java @@ -32,6 +32,7 @@ import android.os.BatteryStats; import android.os.BatteryUsageStatsQuery; import android.os.Process; import android.os.UidBatteryConsumer; +import android.platform.test.ravenwood.RavenwoodRule; import android.util.SparseArray; import androidx.test.filters.SmallTest; @@ -55,6 +56,11 @@ import org.mockito.MockitoAnnotations; @SmallTest @SuppressWarnings("GuardedBy") public class CpuPowerCalculatorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final double PRECISION = 0.00001; private static final int APP_UID1 = Process.FIRST_APPLICATION_UID + 42; @@ -62,7 +68,7 @@ public class CpuPowerCalculatorTest { private static final int NUM_CPU_FREQS = 2 + 2; // 2 clusters * 2 freqs each - @Rule + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() .setAveragePower(PowerProfile.POWER_CPU_ACTIVE, 720) .setCpuScalingPolicy(0, new int[]{0, 1}, new int[]{100, 200}) @@ -93,14 +99,13 @@ public class CpuPowerCalculatorTest { private SystemServerCpuThreadReader mMockSystemServerCpuThreadReader; @Mock private KernelSingleUidTimeReader mMockKernelSingleUidTimeReader; + private boolean[] mSupportedPowerBuckets; @Before public void setUp() { MockitoAnnotations.initMocks(this); - final boolean[] supportedPowerBuckets = - new boolean[EnergyConsumerStats.NUMBER_STANDARD_POWER_BUCKETS]; - supportedPowerBuckets[EnergyConsumerStats.POWER_BUCKET_CPU] = true; + mSupportedPowerBuckets = new boolean[EnergyConsumerStats.NUMBER_STANDARD_POWER_BUCKETS]; when(mMockCpuUidFreqTimeReader.isFastCpuTimesReader()).thenReturn(true); @@ -112,8 +117,7 @@ public class CpuPowerCalculatorTest { .setKernelCpuUidUserSysTimeReader(mMockKernelCpuUidUserSysTimeReader) .setKernelCpuUidActiveTimeReader(mMockKerneCpuUidActiveTimeReader) .setKernelSingleUidTimeReader(mMockKernelSingleUidTimeReader) - .setSystemServerCpuThreadReader(mMockSystemServerCpuThreadReader) - .initEnergyConsumerStatsLocked(supportedPowerBuckets, new String[0]); + .setSystemServerCpuThreadReader(mMockSystemServerCpuThreadReader); } @Test @@ -216,6 +220,10 @@ public class CpuPowerCalculatorTest { @Test public void testMeasuredEnergyBasedModel() { + mSupportedPowerBuckets[EnergyConsumerStats.POWER_BUCKET_CPU] = true; + mStatsRule.getBatteryStats().initEnergyConsumerStatsLocked(mSupportedPowerBuckets, + new String[0]); + when(mMockUserInfoProvider.exists(anyInt())).thenReturn(true); when(mMockKernelCpuSpeedReaders[0].readDelta()).thenReturn(new long[]{1000, 2000}); @@ -397,6 +405,10 @@ public class CpuPowerCalculatorTest { @Test public void testMeasuredEnergyBasedModel_perProcessState() { + mSupportedPowerBuckets[EnergyConsumerStats.POWER_BUCKET_CPU] = true; + mStatsRule.getBatteryStats().initEnergyConsumerStatsLocked(mSupportedPowerBuckets, + new String[0]); + when(mMockUserInfoProvider.exists(anyInt())).thenReturn(true); when(mMockKernelCpuSpeedReaders[0].readDelta()).thenReturn(new long[]{1000, 2000}); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java index 38a5d1943f8b..cbce7e804de5 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java @@ -52,6 +52,7 @@ import java.util.regex.Pattern; @RunWith(AndroidJUnit4.class) @LargeTest +@android.platform.test.annotations.IgnoreUnderRavenwood public class CpuPowerStatsCollectorValidationTest { @Rule public final CheckFlagsRule mCheckFlagsRule = diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java index 245faaf15cc8..4ab706e28cf8 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import android.os.BatteryConsumer; import android.os.Process; import android.os.UidBatteryConsumer; +import android.platform.test.ravenwood.RavenwoodRule; import android.util.SparseLongArray; import androidx.test.filters.SmallTest; @@ -34,11 +35,16 @@ import org.junit.runner.RunWith; @SmallTest @SuppressWarnings("GuardedBy") public class CustomEnergyConsumerPowerCalculatorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final double PRECISION = 0.00001; private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42; - @Rule + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() .initMeasuredEnergyStatsLocked(new String[]{"CUSTOM_COMPONENT1", "CUSTOM_COMPONENT2"}); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/FlashlightPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/FlashlightPowerCalculatorTest.java index 0f85fdc375fb..757025ecc6f8 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/FlashlightPowerCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/FlashlightPowerCalculatorTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import android.os.BatteryConsumer; import android.os.Process; import android.os.UidBatteryConsumer; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -34,11 +35,16 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SmallTest public class FlashlightPowerCalculatorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final double PRECISION = 0.00001; private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42; - @Rule + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() .setAveragePower(PowerProfile.POWER_FLASHLIGHT, 360.0); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/GnssPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/GnssPowerCalculatorTest.java index 3f2a6d04c1e6..3b5658c2e40a 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/GnssPowerCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/GnssPowerCalculatorTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import android.os.BatteryConsumer; import android.os.Process; import android.os.UidBatteryConsumer; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -35,12 +36,16 @@ import org.junit.runner.RunWith; @SmallTest @SuppressWarnings("GuardedBy") public class GnssPowerCalculatorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); private static final double PRECISION = 0.00001; private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42; private static final int APP_UID2 = Process.FIRST_APPLICATION_UID + 222; - @Rule + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() .setAveragePower(PowerProfile.POWER_GPS_ON, 360.0) .setAveragePower(PowerProfile.POWER_GPS_SIGNAL_QUALITY_BASED, diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/IdlePowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/IdlePowerCalculatorTest.java index 3d150af711f1..487d86446a19 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/IdlePowerCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/IdlePowerCalculatorTest.java @@ -19,6 +19,7 @@ package com.android.server.power.stats; import static com.google.common.truth.Truth.assertThat; import android.os.BatteryConsumer; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -32,9 +33,14 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SmallTest public class IdlePowerCalculatorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final double PRECISION = 0.00001; - @Rule + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() .setAveragePower(PowerProfile.POWER_CPU_IDLE, 720.0) .setAveragePower(PowerProfile.POWER_CPU_SUSPEND, 360.0); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/KernelWakelockReaderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/KernelWakelockReaderTest.java index 2edfc8e1e408..e02386656cb5 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/KernelWakelockReaderTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/KernelWakelockReaderTest.java @@ -24,6 +24,7 @@ import junit.framework.TestCase; import java.nio.charset.Charset; +@android.platform.test.annotations.IgnoreUnderRavenwood public class KernelWakelockReaderTest extends TestCase { /** * Helper class that builds the mock Kernel module file /d/wakeup_sources. diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterArrayTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterArrayTest.java index 2e962c364ed2..1807ac5c6115 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterArrayTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterArrayTest.java @@ -24,7 +24,6 @@ import static com.android.server.power.stats.BatteryStatsImpl.TimeBase; import static org.junit.Assert.assertArrayEquals; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.os.Parcel; @@ -159,7 +158,7 @@ public class LongSamplingCounterArrayTest { // Test with detachIfReset=false mCounterArray.reset(false /* detachIfReset */); assertArrayEquals(ZEROES, mCounterArray.mCounts); - verifyZeroInteractions(mTimeBase); + verifyNoMoreInteractions(mTimeBase); updateCounts(COUNTS); // Test with detachIfReset=true diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterTest.java index 0eac625051fc..4b608e35c11e 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterTest.java @@ -24,7 +24,6 @@ import static com.android.server.power.stats.BatteryStatsImpl.TimeBase; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.os.Parcel; @@ -140,7 +139,7 @@ public class LongSamplingCounterTest { // Test with detachIfReset=false mCounter.reset(false /* detachIfReset */); assertEquals(0, getCount()); - verifyZeroInteractions(mTimeBase); + verifyNoMoreInteractions(mTimeBase); mCounter.addCountLocked(COUNT, true); assertEquals(COUNT, getCount()); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MemoryPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MemoryPowerCalculatorTest.java index 2cce449c6c05..3a27188e2643 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/MemoryPowerCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MemoryPowerCalculatorTest.java @@ -19,6 +19,7 @@ package com.android.server.power.stats; import static com.google.common.truth.Truth.assertThat; import android.os.BatteryConsumer; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -32,9 +33,14 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SmallTest public class MemoryPowerCalculatorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final double PRECISION = 0.00001; - @Rule + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() .setAveragePower(PowerProfile.POWER_MEMORY, new double[] {360.0, 720.0, 1080.0}); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java index 78c4bac14a74..9f069130502f 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java @@ -26,6 +26,7 @@ import android.os.Looper; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.os.BatteryStatsHistory; import com.android.internal.os.Clock; import com.android.internal.os.CpuScalingPolicies; import com.android.internal.os.KernelCpuSpeedReader; @@ -70,7 +71,9 @@ public class MockBatteryStatsImpl extends BatteryStatsImpl { MockBatteryStatsImpl(Clock clock, File historyDirectory, Handler handler, PowerStatsUidResolver powerStatsUidResolver) { - super(clock, historyDirectory, handler, powerStatsUidResolver); + super(clock, historyDirectory, handler, powerStatsUidResolver, + mock(FrameworkStatsLogger.class), mock(BatteryStatsHistory.TraceDelegate.class), + mock(BatteryStatsHistory.EventLogger.class)); initTimersAndCounters(); setMaxHistoryBuffer(128 * 1024); @@ -107,7 +110,9 @@ public class MockBatteryStatsImpl extends BatteryStatsImpl { } public Queue<UidToRemove> getPendingRemovedUids() { - return mPendingRemovedUids; + synchronized (this) { + return mPendingRemovedUids; + } } public boolean isOnBattery() { @@ -275,6 +280,10 @@ public class MockBatteryStatsImpl extends BatteryStatsImpl { mHandler = handler; } + @Override + protected void updateBatteryPropertiesLocked() { + } + public static class DummyExternalStatsSync implements ExternalStatsSync { public int flags = 0; diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java index 22a7351d0b9e..af5b462e017d 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java @@ -60,7 +60,7 @@ public class PowerStatsAggregatorTest { public void setup() throws ParseException { mHistory = new BatteryStatsHistory(32, 1024, mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mClock, - mMonotonicClock, mock(BatteryStatsHistory.TraceDelegate.class)); + mMonotonicClock, mock(BatteryStatsHistory.TraceDelegate.class), null); AggregatedPowerStatsConfig config = new AggregatedPowerStatsConfig(); config.trackPowerComponent(TEST_POWER_COMPONENT) @@ -178,7 +178,7 @@ public class PowerStatsAggregatorTest { } @NonNull - private static CharSequence formatDateTime(long timeInMillis) { + private static String formatDateTime(long timeInMillis) { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); format.getCalendar().setTimeZone(TimeZone.getTimeZone("GMT")); return format.format(new Date(timeInMillis)); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java index 3560a2620c8f..18d7b909150b 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java @@ -99,7 +99,7 @@ public class PowerStatsExporterTest { mPowerStatsStore = new PowerStatsStore(storeDirectory, new TestHandler(), config); mHistory = new BatteryStatsHistory(Parcel.obtain(), storeDirectory, 0, 10000, mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mClock, - mMonotonicClock, null); + mMonotonicClock, null, null); mPowerStatsAggregator = new PowerStatsAggregator(config, mHistory); mCpuStatsArrayLayout = new CpuPowerStatsCollector.CpuStatsArrayLayout(); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/ScreenPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/ScreenPowerCalculatorTest.java index 372307985f04..88d4ea75501d 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/ScreenPowerCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/ScreenPowerCalculatorTest.java @@ -25,6 +25,7 @@ import android.app.ActivityManager; import android.os.BatteryConsumer; import android.os.Process; import android.os.UidBatteryConsumer; +import android.platform.test.ravenwood.RavenwoodRule; import android.view.Display; import androidx.test.filters.SmallTest; @@ -38,14 +39,18 @@ import org.junit.runner.RunWith; @SmallTest @SuppressWarnings("GuardedBy") public class ScreenPowerCalculatorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final double PRECISION = 0.00001; private static final int APP_UID1 = Process.FIRST_APPLICATION_UID + 42; private static final int APP_UID2 = Process.FIRST_APPLICATION_UID + 43; private static final long MINUTE_IN_MS = 60 * 1000; private static final long MINUTE_IN_US = 60 * 1000 * 1000; - private static final long HOUR_IN_MS = 60 * MINUTE_IN_MS; - @Rule + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() .setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_ON, 0, 36.0) .setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_FULL, 0, 48.0) diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/SensorPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/SensorPowerCalculatorTest.java index 474527040839..c01f05f43c04 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/SensorPowerCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/SensorPowerCalculatorTest.java @@ -27,6 +27,7 @@ import android.hardware.input.InputSensorInfo; import android.os.BatteryConsumer; import android.os.Process; import android.os.UidBatteryConsumer; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -40,6 +41,11 @@ import java.util.List; @RunWith(AndroidJUnit4.class) @SmallTest public class SensorPowerCalculatorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final double PRECISION = 0.00001; private static final int SENSOR_HANDLE_1 = 1; @@ -47,7 +53,7 @@ public class SensorPowerCalculatorTest { private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42; - @Rule + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule(); @Test @@ -60,10 +66,12 @@ public class SensorPowerCalculatorTest { .thenReturn(List.of(sensor1, sensor2)); final BatteryStatsImpl stats = mStatsRule.getBatteryStats(); - stats.noteStartSensorLocked(APP_UID, SENSOR_HANDLE_1, 1000, 1000); - stats.noteStopSensorLocked(APP_UID, SENSOR_HANDLE_1, 2000, 2000); - stats.noteStartSensorLocked(APP_UID, SENSOR_HANDLE_2, 3000, 3000); - stats.noteStopSensorLocked(APP_UID, SENSOR_HANDLE_2, 5000, 5000); + synchronized (stats) { + stats.noteStartSensorLocked(APP_UID, SENSOR_HANDLE_1, 1000, 1000); + stats.noteStopSensorLocked(APP_UID, SENSOR_HANDLE_1, 2000, 2000); + stats.noteStartSensorLocked(APP_UID, SENSOR_HANDLE_2, 3000, 3000); + stats.noteStopSensorLocked(APP_UID, SENSOR_HANDLE_2, 5000, 5000); + } SensorPowerCalculator calculator = new SensorPowerCalculator(sensorManager); @@ -84,11 +92,20 @@ public class SensorPowerCalculatorTest { .isWithin(PRECISION).of(0.5); } - private Sensor createSensor(int handle, int type, double power) { - return new Sensor(new InputSensorInfo("name", "vendor", 0 /* version */, - handle, type, 100.0f /*maxRange */, 0.02f /* resolution */, - (float) power, 1000 /* minDelay */, 0 /* fifoReservedEventCount */, - 0 /* fifoMaxEventCount */, "" /* stringType */, "" /* requiredPermission */, - 0 /* maxDelay */, 0 /* flags */, 0 /* id */)); + private Sensor createSensor(int handle, int type, float power) { + if (RavenwoodRule.isUnderRavenwood()) { + Sensor sensor = mock(Sensor.class); + + when(sensor.getHandle()).thenReturn(handle); + when(sensor.getType()).thenReturn(type); + when(sensor.getPower()).thenReturn(power); + return sensor; + } else { + return new Sensor(new InputSensorInfo("name", "vendor", 0 /* version */, + handle, type, 100.0f /*maxRange */, 0.02f /* resolution */, + (float) power, 1000 /* minDelay */, 0 /* fifoReservedEventCount */, + 0 /* fifoMaxEventCount */, "" /* stringType */, "" /* requiredPermission */, + 0 /* maxDelay */, 0 /* flags */, 0 /* id */)); + } } } diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/UserPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/UserPowerCalculatorTest.java index f14745ef2daa..438f0ec36177 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/UserPowerCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/UserPowerCalculatorTest.java @@ -25,6 +25,7 @@ import android.os.Process; import android.os.UidBatteryConsumer; import android.os.UserBatteryConsumer; import android.os.UserHandle; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -36,6 +37,11 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SmallTest public class UserPowerCalculatorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + public static final int USER1 = 0; public static final int USER2 = 1625; @@ -43,7 +49,7 @@ public class UserPowerCalculatorTest { private static final int APP_UID2 = Process.FIRST_APPLICATION_UID + 272; private static final int APP_UID3 = Process.FIRST_APPLICATION_UID + 314; - @Rule + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule(); @Test diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/VideoPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/VideoPowerCalculatorTest.java index f578aa3b46be..b9b710118cc7 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/VideoPowerCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/VideoPowerCalculatorTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import android.os.BatteryConsumer; import android.os.Process; import android.os.UidBatteryConsumer; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -34,11 +35,16 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SmallTest public class VideoPowerCalculatorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final double PRECISION = 0.00001; private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42; - @Rule + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() .setAveragePower(PowerProfile.POWER_VIDEO, 360.0); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerCalculatorTest.java index f1961855f12f..5b7762d7de65 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerCalculatorTest.java @@ -23,6 +23,7 @@ import android.os.BatteryStats; import android.os.Process; import android.os.UidBatteryConsumer; import android.os.WorkSource; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -36,12 +37,17 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SmallTest public class WakelockPowerCalculatorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final double PRECISION = 0.00001; private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42; private static final int APP_PID = 3145; - @Rule + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() .setAveragePower(PowerProfile.POWER_CPU_IDLE, 360.0); @@ -51,10 +57,12 @@ public class WakelockPowerCalculatorTest { BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats(); - batteryStats.noteStartWakeFromSourceLocked(new WorkSource(APP_UID), APP_PID, "awake", "", - BatteryStats.WAKE_TYPE_PARTIAL, true, 1000, 1000); - batteryStats.noteStopWakeFromSourceLocked(new WorkSource(APP_UID), APP_PID, "awake", "", - BatteryStats.WAKE_TYPE_PARTIAL, 2000, 2000); + synchronized (batteryStats) { + batteryStats.noteStartWakeFromSourceLocked(new WorkSource(APP_UID), APP_PID, "awake", + "", BatteryStats.WAKE_TYPE_PARTIAL, true, 1000, 1000); + batteryStats.noteStopWakeFromSourceLocked(new WorkSource(APP_UID), APP_PID, "awake", + "", BatteryStats.WAKE_TYPE_PARTIAL, 2000, 2000); + } mStatsRule.setTime(10_000, 6_000); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerCalculatorTest.java index 113be8b19518..8e221be261e9 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerCalculatorTest.java @@ -23,6 +23,9 @@ import static android.os.BatteryStats.POWER_DATA_UNAVAILABLE; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import android.app.usage.NetworkStatsManager; import android.net.NetworkCapabilities; import android.net.NetworkStats; @@ -33,6 +36,7 @@ import android.os.Process; import android.os.UidBatteryConsumer; import android.os.WorkSource; import android.os.connectivity.WifiActivityEnergyInfo; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -44,10 +48,17 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import java.util.List; + @RunWith(AndroidJUnit4.class) @SmallTest @SuppressWarnings("GuardedBy") public class WifiPowerCalculatorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + private static final double PRECISION = 0.00001; private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42; @@ -55,7 +66,7 @@ public class WifiPowerCalculatorTest { @Mock NetworkStatsManager mNetworkStatsManager; - @Rule + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() .setAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_IDLE, 360.0) .setAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_RX, 480.0) @@ -64,6 +75,7 @@ public class WifiPowerCalculatorTest { .setAveragePower(PowerProfile.POWER_WIFI_SCAN, 480.0) .setAveragePower(PowerProfile.POWER_WIFI_BATCHED_SCAN, 720.0) .setAveragePower(PowerProfile.POWER_WIFI_ACTIVE, 1080.0) + .setAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_OPERATING_VOLTAGE, 3700) .initMeasuredEnergyStatsLocked(); /** Sets up a batterystats object with pre-populated network values. */ @@ -78,21 +90,54 @@ public class WifiPowerCalculatorTest { return batteryStats; } - private NetworkStats buildNetworkStats(long elapsedRealtime, int rxBytes, int rxPackets, - int txBytes, int txPackets) { - return new NetworkStats(elapsedRealtime, 1) - .addEntry(new NetworkStats.Entry("wifi", APP_UID, 0, 0, - METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, rxBytes, rxPackets, - txBytes, txPackets, 100)) - .addEntry(new NetworkStats.Entry("wifi", Process.WIFI_UID, 0, 0, - METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1111, 111, - 2222, 22, 111)); + private NetworkStats buildNetworkStats(long elapsedRealtime, long rxBytes, long rxPackets, + long txBytes, long txPackets) { + if (RavenwoodRule.isUnderRavenwood()) { + NetworkStats stats = mock(NetworkStats.class); +// when(stats.getElapsedRealtime()).thenReturn(elapsedRealtime); + + NetworkStats.Entry entry1 = mock(NetworkStats.Entry.class); +// when(entry1.getIface()).thenReturn("wifi"); + when(entry1.getUid()).thenReturn(APP_UID); + when(entry1.getMetered()).thenReturn(METERED_NO); + when(entry1.getRoaming()).thenReturn(ROAMING_NO); + when(entry1.getDefaultNetwork()).thenReturn(DEFAULT_NETWORK_NO); + when(entry1.getRxBytes()).thenReturn(rxBytes); + when(entry1.getRxPackets()).thenReturn(rxPackets); + when(entry1.getTxBytes()).thenReturn(txBytes); + when(entry1.getTxPackets()).thenReturn(txPackets); + when(entry1.getOperations()).thenReturn(100L); + + NetworkStats.Entry entry2 = mock(NetworkStats.Entry.class); +// when(entry2.getIface()).thenReturn("wifi"); + when(entry2.getUid()).thenReturn(Process.WIFI_UID); + when(entry2.getMetered()).thenReturn(METERED_NO); + when(entry2.getRoaming()).thenReturn(ROAMING_NO); + when(entry2.getDefaultNetwork()).thenReturn(DEFAULT_NETWORK_NO); + when(entry2.getRxBytes()).thenReturn(1111L); + when(entry2.getRxPackets()).thenReturn(111L); + when(entry2.getTxBytes()).thenReturn(2222L); + when(entry2.getTxPackets()).thenReturn(22L); + when(entry2.getOperations()).thenReturn(111L); + + when(stats.iterator()).thenAnswer(inv->List.of(entry1, entry2).iterator()); + + return stats; + } else { + return new NetworkStats(elapsedRealtime, 1) + .addEntry(new NetworkStats.Entry("wifi", APP_UID, 0, 0, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, rxBytes, rxPackets, + txBytes, txPackets, 100)) + .addEntry(new NetworkStats.Entry("wifi", Process.WIFI_UID, 0, 0, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1111, 111, + 2222, 22, 111)); + } } /** Sets up an WifiActivityEnergyInfo for ActivityController-model-based tests. */ private WifiActivityEnergyInfo setupPowerControllerBasedModelEnergyNumbersInfo() { - return new WifiActivityEnergyInfo(10000, - WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 1000, 2000, 3000, 4000); + return buildWifiActivityEnergyInfo(10000L, WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, + 1000L, 2000L, 3000L, 4000L); } @Test @@ -142,7 +187,7 @@ public class WifiPowerCalculatorTest { uid.setProcessStateForTest( BatteryStats.Uid.PROCESS_STATE_FOREGROUND, 1000); - batteryStats.updateWifiState(new WifiActivityEnergyInfo(2000, + batteryStats.updateWifiState(buildWifiActivityEnergyInfo(2000, WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 1000, 2000, 3000, 4000), POWER_DATA_UNAVAILABLE, 2000, 2000, mNetworkStatsManager); @@ -152,7 +197,7 @@ public class WifiPowerCalculatorTest { mStatsRule.setNetworkStats(buildNetworkStats(4000, 5000, 200, 7000, 80)); - batteryStats.updateWifiState(new WifiActivityEnergyInfo(4000, + batteryStats.updateWifiState(buildWifiActivityEnergyInfo(4000, WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 5000, 6000, 7000, 8000), POWER_DATA_UNAVAILABLE, 4000, 4000, mNetworkStatsManager); @@ -231,7 +276,7 @@ public class WifiPowerCalculatorTest { uid.setProcessStateForTest( BatteryStats.Uid.PROCESS_STATE_FOREGROUND, 1000); - batteryStats.updateWifiState(new WifiActivityEnergyInfo(2000, + batteryStats.updateWifiState(buildWifiActivityEnergyInfo(2000, WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 1000, 2000, 3000, 4000), 1_000_000, 2000, 2000, mNetworkStatsManager); @@ -241,7 +286,7 @@ public class WifiPowerCalculatorTest { mStatsRule.setNetworkStats(buildNetworkStats(4000, 5000, 200, 7000, 80)); - batteryStats.updateWifiState(new WifiActivityEnergyInfo(4000, + batteryStats.updateWifiState(buildWifiActivityEnergyInfo(4000, WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 5000, 6000, 7000, 8000), 5_000_000, 4000, 4000, mNetworkStatsManager); @@ -329,4 +374,43 @@ public class WifiPowerCalculatorTest { assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_WIFI)) .isEqualTo(BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION); } + + private WifiActivityEnergyInfo buildWifiActivityEnergyInfo(long timeSinceBoot, + int stackState, long txDuration, long rxDuration, long scanDuration, + long idleDuration) { + if (RavenwoodRule.isUnderRavenwood()) { + WifiActivityEnergyInfo info = mock(WifiActivityEnergyInfo.class); + when(info.getTimeSinceBootMillis()).thenReturn(timeSinceBoot); + when(info.getStackState()).thenReturn(stackState); + when(info.getControllerTxDurationMillis()).thenReturn(txDuration); + when(info.getControllerRxDurationMillis()).thenReturn(rxDuration); + when(info.getControllerScanDurationMillis()).thenReturn(scanDuration); + when(info.getControllerIdleDurationMillis()).thenReturn(idleDuration); + long energy = calculateEnergyMicroJoules(txDuration, rxDuration, idleDuration); + when(info.getControllerEnergyUsedMicroJoules()).thenReturn(energy); + return info; + } else { + return new WifiActivityEnergyInfo(timeSinceBoot, stackState, txDuration, rxDuration, + scanDuration, idleDuration); + } + } + + // See WifiActivityEnergyInfo + private long calculateEnergyMicroJoules( + long txDurationMillis, long rxDurationMillis, long idleDurationMillis) { + PowerProfile powerProfile = mStatsRule.getPowerProfile(); + final double idleCurrent = powerProfile.getAveragePower( + PowerProfile.POWER_WIFI_CONTROLLER_IDLE); + final double rxCurrent = powerProfile.getAveragePower( + PowerProfile.POWER_WIFI_CONTROLLER_RX); + final double txCurrent = powerProfile.getAveragePower( + PowerProfile.POWER_WIFI_CONTROLLER_TX); + final double voltage = powerProfile.getAveragePower( + PowerProfile.POWER_WIFI_CONTROLLER_OPERATING_VOLTAGE) / 1000.0; + + return (long) ((txDurationMillis * txCurrent + + rxDurationMillis * rxCurrent + + idleDurationMillis * idleCurrent) + * voltage); + } } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java index ef80b59e49f8..f86cb7bae8dc 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java @@ -21,9 +21,12 @@ import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -31,10 +34,14 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.Manifest; import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.AccessibilityTrace; +import android.accessibilityservice.BrailleDisplayController; import android.accessibilityservice.GestureDescription; import android.accessibilityservice.IAccessibilityServiceClient; +import android.accessibilityservice.IBrailleDisplayConnection; +import android.accessibilityservice.IBrailleDisplayController; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -43,6 +50,9 @@ import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.hardware.display.DisplayManager; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; +import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.os.UserHandle; @@ -62,7 +72,9 @@ import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.Arrays; @@ -94,18 +106,30 @@ public class AccessibilityServiceConnectionTest { AccessibilityServiceInfo mServiceInfo; @Mock ResolveInfo mMockResolveInfo; @Mock AccessibilitySecurityPolicy mMockSecurityPolicy; - @Mock AccessibilityWindowManager mMockA11yWindowManager; - @Mock ActivityTaskManagerInternal mMockActivityTaskManagerInternal; - @Mock AbstractAccessibilityServiceConnection.SystemSupport mMockSystemSupport; - @Mock AccessibilityTrace mMockA11yTrace; - @Mock WindowManagerInternal mMockWindowManagerInternal; - @Mock SystemActionPerformer mMockSystemActionPerformer; - @Mock KeyEventDispatcher mMockKeyEventDispatcher; + @Mock + AccessibilityWindowManager mMockA11yWindowManager; + @Mock + ActivityTaskManagerInternal mMockActivityTaskManagerInternal; + @Mock + AbstractAccessibilityServiceConnection.SystemSupport mMockSystemSupport; + @Mock + AccessibilityTrace mMockA11yTrace; + @Mock + WindowManagerInternal mMockWindowManagerInternal; + @Mock + SystemActionPerformer mMockSystemActionPerformer; + @Mock + KeyEventDispatcher mMockKeyEventDispatcher; @Mock MagnificationProcessor mMockMagnificationProcessor; - @Mock IBinder mMockIBinder; - @Mock IAccessibilityServiceClient mMockServiceClient; - @Mock MotionEventInjector mMockMotionEventInjector; + @Mock + IBinder mMockIBinder; + @Mock + IAccessibilityServiceClient mMockServiceClient; + @Mock + IBrailleDisplayController mMockBrailleDisplayController; + @Mock + MotionEventInjector mMockMotionEventInjector; MessageCapturingHandler mHandler = new MessageCapturingHandler(null); @@ -134,6 +158,7 @@ public class AccessibilityServiceConnectionTest { mMockWindowManagerInternal, mMockSystemActionPerformer, mMockA11yWindowManager, mMockActivityTaskManagerInternal); when(mMockSecurityPolicy.canPerformGestures(mConnection)).thenReturn(true); + when(mMockSecurityPolicy.checkAccessibilityAccess(mConnection)).thenReturn(true); } @After @@ -291,6 +316,119 @@ public class AccessibilityServiceConnectionTest { } @Test + @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID) + public void connectBluetoothBrailleDisplay() throws Exception { + final String macAddress = "00:11:22:33:AA:BB"; + final byte[] descriptor = {0x05, 0x41}; + Bundle bd = new Bundle(); + bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, "/dev/null"); + bd.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, descriptor); + bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, macAddress); + bd.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, true); + mConnection.setTestBrailleDisplayData(List.of(bd)); + + mConnection.connectBluetoothBrailleDisplay(macAddress, mMockBrailleDisplayController); + + ArgumentCaptor<IBrailleDisplayConnection> connection = + ArgumentCaptor.forClass(IBrailleDisplayConnection.class); + verify(mMockBrailleDisplayController).onConnected(connection.capture(), eq(descriptor)); + // Cleanup the connection. + connection.getValue().disconnect(); + } + + @Test + @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID) + public void connectBluetoothBrailleDisplay_throwsForMissingBluetoothConnectPermission() { + doThrow(SecurityException.class).when(mMockContext) + .enforceCallingPermission(eq(Manifest.permission.BLUETOOTH_CONNECT), any()); + + assertThrows(SecurityException.class, + () -> mConnection.connectBluetoothBrailleDisplay("unused", + mMockBrailleDisplayController)); + } + + @Test + @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID) + public void connectBluetoothBrailleDisplay_throwsForNullMacAddress() { + assertThrows(NullPointerException.class, + () -> mConnection.connectBluetoothBrailleDisplay(null, + mMockBrailleDisplayController)); + } + + @Test + @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID) + public void connectBluetoothBrailleDisplay_throwsForMisformattedMacAddress() { + assertThrows(IllegalArgumentException.class, + () -> mConnection.connectBluetoothBrailleDisplay("12:34", + mMockBrailleDisplayController)); + } + + @Test + @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID) + public void connectUsbBrailleDisplay() throws Exception { + final String serialNumber = "myUsbDevice"; + final byte[] descriptor = {0x05, 0x41}; + Bundle bd = new Bundle(); + bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, "/dev/null"); + bd.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, descriptor); + bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, serialNumber); + bd.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, false); + mConnection.setTestBrailleDisplayData(List.of(bd)); + UsbDevice usbDevice = Mockito.mock(UsbDevice.class); + when(usbDevice.getSerialNumber()).thenReturn(serialNumber); + UsbManager usbManager = Mockito.mock(UsbManager.class); + when(mMockContext.getSystemService(Context.USB_SERVICE)).thenReturn(usbManager); + when(usbManager.hasPermission(eq(usbDevice), eq(COMPONENT_NAME.getPackageName()), + anyInt(), anyInt())).thenReturn(true); + + mConnection.connectUsbBrailleDisplay(usbDevice, mMockBrailleDisplayController); + + ArgumentCaptor<IBrailleDisplayConnection> connection = + ArgumentCaptor.forClass(IBrailleDisplayConnection.class); + verify(mMockBrailleDisplayController).onConnected(connection.capture(), eq(descriptor)); + // Cleanup the connection. + connection.getValue().disconnect(); + } + + @Test + @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID) + public void connectUsbBrailleDisplay_throwsForMissingUsbPermission() { + UsbManager usbManager = Mockito.mock(UsbManager.class); + when(mMockContext.getSystemService(Context.USB_SERVICE)).thenReturn(usbManager); + when(usbManager.hasPermission(notNull(), eq(COMPONENT_NAME.getPackageName()), + anyInt(), anyInt())).thenReturn(false); + + assertThrows(SecurityException.class, + () -> mConnection.connectUsbBrailleDisplay(Mockito.mock(UsbDevice.class), + mMockBrailleDisplayController)); + } + + @Test + @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID) + public void connectUsbBrailleDisplay_throwsForNullDevice() { + assertThrows(NullPointerException.class, + () -> mConnection.connectUsbBrailleDisplay(null, mMockBrailleDisplayController)); + } + + @Test + @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID) + public void connectUsbBrailleDisplay_callsOnConnectionFailedForEmptySerialNumber() + throws Exception { + UsbManager usbManager = Mockito.mock(UsbManager.class); + when(mMockContext.getSystemService(Context.USB_SERVICE)).thenReturn(usbManager); + when(usbManager.hasPermission(notNull(), eq(COMPONENT_NAME.getPackageName()), + anyInt(), anyInt())).thenReturn(true); + UsbDevice usbDevice = Mockito.mock(UsbDevice.class); + when(usbDevice.getSerialNumber()).thenReturn(""); + + mConnection.connectUsbBrailleDisplay(usbDevice, mMockBrailleDisplayController); + + verify(mMockBrailleDisplayController).onConnectionFailed( + BrailleDisplayController.BrailleDisplayCallback + .FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND); + } + + @Test @RequiresFlagsEnabled(Flags.FLAG_RESETTABLE_DYNAMIC_PROPERTIES) public void binderDied_resetA11yServiceInfo() { final int flag = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS; diff --git a/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java new file mode 100644 index 000000000000..7c278cedbcc5 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java @@ -0,0 +1,159 @@ +/* + * 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.accessibility; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import android.accessibilityservice.BrailleDisplayController; +import android.os.Bundle; +import android.testing.DexmakerShareClassLoaderRule; + +import com.google.common.truth.Expect; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.nio.file.Path; +import java.util.List; + +/** + * Tests for internal details of {@link BrailleDisplayConnection}. + * + * <p>Prefer adding new tests in CTS where possible. + */ +public class BrailleDisplayConnectionTest { + private static final Path NULL_PATH = Path.of("/dev/null"); + + private BrailleDisplayConnection mBrailleDisplayConnection; + @Mock + private BrailleDisplayConnection.NativeInterface mNativeInterface; + @Mock + private AccessibilityServiceConnection mServiceConnection; + + @Rule + public final Expect expect = Expect.create(); + + // To mock package-private class + @Rule + public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule = + new DexmakerShareClassLoaderRule(); + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mBrailleDisplayConnection = new BrailleDisplayConnection(new Object(), mServiceConnection); + } + + @Test + public void defaultNativeScanner_getReportDescriptor_returnsDescriptor() { + int descriptorSize = 4; + byte[] descriptor = {0xB, 0xE, 0xE, 0xF}; + when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(descriptorSize); + when(mNativeInterface.getHidrawDesc(anyInt(), eq(descriptorSize))).thenReturn(descriptor); + + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + + assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isEqualTo(descriptor); + } + + @Test + public void defaultNativeScanner_getReportDescriptor_invalidSize_returnsNull() { + when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(0); + + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + + assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isNull(); + } + + @Test + public void defaultNativeScanner_getUniqueId_returnsUniq() { + String macAddress = "12:34:56:78"; + when(mNativeInterface.getHidrawUniq(anyInt())).thenReturn(macAddress); + + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + + assertThat(scanner.getUniqueId(NULL_PATH)).isEqualTo(macAddress); + } + + @Test + public void defaultNativeScanner_getDeviceBusType_busUsb() { + when(mNativeInterface.getHidrawBusType(anyInt())) + .thenReturn(BrailleDisplayConnection.BUS_USB); + + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + + assertThat(scanner.getDeviceBusType(NULL_PATH)) + .isEqualTo(BrailleDisplayConnection.BUS_USB); + } + + @Test + public void defaultNativeScanner_getDeviceBusType_busBluetooth() { + when(mNativeInterface.getHidrawBusType(anyInt())) + .thenReturn(BrailleDisplayConnection.BUS_BLUETOOTH); + + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface); + + assertThat(scanner.getDeviceBusType(NULL_PATH)) + .isEqualTo(BrailleDisplayConnection.BUS_BLUETOOTH); + } + + // BrailleDisplayConnection#setTestData() is used to enable CTS testing with + // test Braille display data, but its own implementation should also be tested + // so that issues in this helper don't cause confusing failures in CTS. + @Test + public void setTestData_scannerReturnsTestData() { + Bundle bd1 = new Bundle(), bd2 = new Bundle(); + + Path path1 = Path.of("/dev/path1"), path2 = Path.of("/dev/path2"); + bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path1.toString()); + bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path2.toString()); + byte[] desc1 = {0xB, 0xE}, desc2 = {0xE, 0xF}; + bd1.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc1); + bd2.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc2); + String uniq1 = "uniq1", uniq2 = "uniq2"; + bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq1); + bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq2); + int bus1 = BrailleDisplayConnection.BUS_USB, bus2 = BrailleDisplayConnection.BUS_BLUETOOTH; + bd1.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, + bus1 == BrailleDisplayConnection.BUS_BLUETOOTH); + bd2.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, + bus2 == BrailleDisplayConnection.BUS_BLUETOOTH); + + BrailleDisplayConnection.BrailleDisplayScanner scanner = + mBrailleDisplayConnection.setTestData(List.of(bd1, bd2)); + + expect.that(scanner.getHidrawNodePaths()).containsExactly(path1, path2); + expect.that(scanner.getDeviceReportDescriptor(path1)).isEqualTo(desc1); + expect.that(scanner.getDeviceReportDescriptor(path2)).isEqualTo(desc2); + expect.that(scanner.getUniqueId(path1)).isEqualTo(uniq1); + expect.that(scanner.getUniqueId(path2)).isEqualTo(uniq2); + expect.that(scanner.getDeviceBusType(path1)).isEqualTo(bus1); + expect.that(scanner.getDeviceBusType(path2)).isEqualTo(bus2); + } +} 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/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 7d8eb90ea79c..ce890f6d8751 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -33,6 +33,7 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.window.TransitionInfo.FLAG_CONFIG_AT_END; import static android.window.TransitionInfo.FLAG_FILLS_TASK; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; @@ -2540,6 +2541,36 @@ public class TransitionTests extends WindowTestsBase { } @Test + public void testConfigAtEnd() { + final TransitionController controller = mDisplayContent.mTransitionController; + Transition transit = createTestTransition(TRANSIT_CHANGE, controller); + final TestTransitionPlayer player = registerTestTransitionPlayer(); + + final Task task = createTask(mDisplayContent); + final Rect taskBounds = new Rect(0, 0, 200, 300); + task.getConfiguration().windowConfiguration.setBounds(taskBounds); + final ActivityRecord activity = createActivityRecord(task); + activity.setVisibleRequested(true); + activity.setVisible(true); + + controller.moveToCollecting(transit); + transit.collect(task); + transit.setConfigAtEnd(task); + task.getRequestedOverrideConfiguration().windowConfiguration.setBounds( + new Rect(10, 10, 200, 300)); + task.onRequestedOverrideConfigurationChanged(task.getRequestedOverrideConfiguration()); + + controller.requestStartTransition(transit, task, null, null); + player.start(); + assertTrue(activity.isConfigurationDispatchPaused()); + // config-at-end flag must propagate up to task if activity was promoted. + assertTrue(player.mLastReady.getChange( + task.mRemoteToken.toWindowContainerToken()).hasFlags(FLAG_CONFIG_AT_END)); + player.finish(); + assertFalse(activity.isConfigurationDispatchPaused()); + } + + @Test public void testReadyTrackerBasics() { final TransitionController controller = new TestTransitionController( mock(ActivityTaskManagerService.class)); 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/telephony/java/android/telephony/satellite/stub/ISatellite.aidl b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl index 711be02761fe..9441fb5d02ef 100644 --- a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl +++ b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl @@ -501,4 +501,22 @@ oneway interface ISatellite { * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED */ void stopSendingNtnSignalStrength(in IIntegerConsumer resultCallback); + + /** + * Abort all outgoing satellite datagrams which vendor service has received from Telephony + * framework. + * + * This API helps modem to be in sync with framework when framework times out on sending + * datagrams. + * + * @param resultCallback The callback to receive the error code result of the operation. + * + * Valid result codes returned: + * SatelliteResult:SATELLITE_RESULT_SUCCESS + * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE + * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR + * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE + * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED + */ + void abortSendingSatelliteDatagrams(in IIntegerConsumer resultCallback); } diff --git a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java index abacd1527729..f17ff17497f2 100644 --- a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java +++ b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java @@ -265,6 +265,14 @@ public class SatelliteImplBase extends SatelliteService { "stopSendingNtnSignalStrength"); } + @Override + public void abortSendingSatelliteDatagrams(IIntegerConsumer resultCallback) + throws RemoteException { + executeMethodAsync( + () -> SatelliteImplBase.this.abortSendingSatelliteDatagrams(resultCallback), + "abortSendingSatelliteDatagrams"); + } + // Call the methods with a clean calling identity on the executor and wait indefinitely for // the future to return. private void executeMethodAsync(Runnable r, String errorLogName) throws RemoteException { @@ -783,4 +791,13 @@ public class SatelliteImplBase extends SatelliteService { public void stopSendingNtnSignalStrength(@NonNull IIntegerConsumer resultCallback){ // stub implementation } + + /** + * Requests to abort sending satellite datagrams + * + * @param resultCallback The {@link SatelliteError} result of the operation. + */ + public void abortSendingSatelliteDatagrams(@NonNull IIntegerConsumer resultCallback){ + // stub implementation + } } diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index bd47b1fc2dc0..ff2ee27abc60 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -3046,13 +3046,24 @@ interface ITelephony { boolean setSatellitePointingUiClassName(in String packageName, in String className); /** - * This API can be used by only CTS to update the timeout duration in milliseconds whether - * the device is aligned with the satellite for demo mode + * This API can be used by only CTS to override the timeout durations used by the + * DatagramController module. * * @param timeoutMillis The timeout duration in millisecond. * @return {@code true} if the timeout duration is set successfully, {@code false} otherwise. */ - boolean setSatelliteDeviceAlignedTimeoutDuration(long timeoutMillis); + boolean setDatagramControllerTimeoutDuration( + boolean reset, int timeoutType, long timeoutMillis); + + /** + * This API can be used by only CTS to override the timeout durations used by the + * SatelliteController module. + * + * @param timeoutMillis The timeout duration in millisecond. + * @return {@code true} if the timeout duration is set successfully, {@code false} otherwise. + */ + boolean setSatelliteControllerTimeoutDuration( + boolean reset, int timeoutType, long timeoutMillis); /** * This API can be used in only testing to override connectivity status in monitoring emergency 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)); + } +} diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/CursorWindow_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/CursorWindow_host.java index 631fc0273c94..eba99107f126 100644 --- a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/CursorWindow_host.java +++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/CursorWindow_host.java @@ -17,6 +17,7 @@ package com.android.hoststubgen.nativesubstitution; import android.database.Cursor; import android.database.sqlite.SQLiteException; +import android.os.Parcel; import android.util.Base64; import java.text.DecimalFormat; @@ -31,6 +32,7 @@ public class CursorWindow_host { private static final HashMap<Long, CursorWindow_host> sInstances = new HashMap<>(); private static long sNextId = 1; + private String mName; private int mColumnNum; private static class Row { String[] fields; @@ -41,6 +43,7 @@ public class CursorWindow_host { public static long nativeCreate(String name, int cursorWindowSize) { CursorWindow_host instance = new CursorWindow_host(); + instance.mName = name; long instanceId = sNextId++; sInstances.put(instanceId, instance); return instanceId; @@ -50,6 +53,10 @@ public class CursorWindow_host { sInstances.remove(windowPtr); } + public static String nativeGetName(long windowPtr) { + return sInstances.get(windowPtr).mName; + } + public static boolean nativeSetNumColumns(long windowPtr, int columnNum) { sInstances.get(windowPtr).mColumnNum = columnNum; return true; @@ -156,4 +163,30 @@ public class CursorWindow_host { return null; } } + + public static void nativeWriteToParcel(long windowPtr, Parcel parcel) { + CursorWindow_host window = sInstances.get(windowPtr); + parcel.writeString(window.mName); + parcel.writeInt(window.mColumnNum); + parcel.writeInt(window.mRows.size()); + for (int row = 0; row < window.mRows.size(); row++) { + parcel.writeStringArray(window.mRows.get(row).fields); + parcel.writeIntArray(window.mRows.get(row).types); + } + } + + public static long nativeCreateFromParcel(Parcel parcel) { + long windowPtr = nativeCreate(null, 0); + CursorWindow_host window = sInstances.get(windowPtr); + window.mName = parcel.readString(); + window.mColumnNum = parcel.readInt(); + int rowCount = parcel.readInt(); + for (int row = 0; row < rowCount; row++) { + Row r = new Row(); + r.fields = parcel.createStringArray(); + r.types = parcel.createIntArray(); + window.mRows.add(r); + } + return windowPtr; + } } diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongArrayMultiStateCounter_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongArrayMultiStateCounter_host.java index a1356237e5a4..4d39d88d58c3 100644 --- a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongArrayMultiStateCounter_host.java +++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongArrayMultiStateCounter_host.java @@ -143,6 +143,16 @@ public class LongArrayMultiStateCounter_host { updateValue(values, timestampMs); } + public void addCounts(long[] delta) { + if (!mEnabled) { + return; + } + + for (int i = 0; i < mArrayLength; i++) { + mStates[mCurrentState].mCounter[i] += delta[i]; + } + } + public void getValues(long[] values, int state) { System.arraycopy(mStates[state].mCounter, 0, values, 0, mArrayLength); } @@ -331,6 +341,10 @@ public class LongArrayMultiStateCounter_host { LongArrayContainer_host.getInstance(containerInstanceId), timestampMs); } + public static void native_addCounts(long instanceId, long containerInstanceId) { + getInstance(instanceId).addCounts(LongArrayContainer_host.getInstance(containerInstanceId)); + } + public static void native_getCounts(long instanceId, long containerInstanceId, int state) { getInstance(instanceId).getValues(LongArrayContainer_host.getInstance(containerInstanceId), state); diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongMultiStateCounter_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongMultiStateCounter_host.java new file mode 100644 index 000000000000..a5d0fc6872de --- /dev/null +++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongMultiStateCounter_host.java @@ -0,0 +1,263 @@ +/* + * 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.hoststubgen.nativesubstitution; + +import android.os.BadParcelableException; +import android.os.Parcel; + +import java.util.HashMap; + +/** + * Native implementation substitutions for the LongMultiStateCounter class. + */ +public class LongMultiStateCounter_host { + + /** + * A reimplementation of {@link com.android.internal.os.LongMultiStateCounter}, only in + * Java instead of native. The majority of the code (in C++) can be found in + * /frameworks/native/libs/battery/MultiStateCounter.h + */ + private static class LongMultiStateCounterRavenwood { + private final int mStateCount; + private int mCurrentState; + private long mLastStateChangeTimestampMs = -1; + private long mLastUpdateTimestampMs = -1; + private boolean mEnabled = true; + + private static class State { + private long mTimeInStateSinceUpdate; + private long mCounter; + } + + private final State[] mStates; + private long mValue; + + LongMultiStateCounterRavenwood(int stateCount) { + mStateCount = stateCount; + mStates = new State[stateCount]; + for (int i = 0; i < mStateCount; i++) { + mStates[i] = new State(); + } + } + + public void setEnabled(boolean enabled, long timestampMs) { + if (enabled == mEnabled) { + return; + } + + if (!enabled) { + setState(mCurrentState, timestampMs); + mEnabled = false; + } else { + if (timestampMs < mLastUpdateTimestampMs) { + timestampMs = mLastUpdateTimestampMs; + } + + if (mLastStateChangeTimestampMs >= 0) { + mLastStateChangeTimestampMs = timestampMs; + } + mEnabled = true; + } + } + + public void setState(int state, long timestampMs) { + if (mEnabled && mLastStateChangeTimestampMs >= 0 && mLastUpdateTimestampMs >= 0) { + if (timestampMs < mLastUpdateTimestampMs) { + timestampMs = mLastUpdateTimestampMs; + } + + if (timestampMs >= mLastStateChangeTimestampMs) { + mStates[mCurrentState].mTimeInStateSinceUpdate += + timestampMs - mLastStateChangeTimestampMs; + } else { + for (int i = 0; i < mStateCount; i++) { + mStates[i].mTimeInStateSinceUpdate = 0; + } + } + } + mCurrentState = state; + mLastStateChangeTimestampMs = timestampMs; + } + + public long updateValue(long value, long timestampMs) { + long returnValue = 0; + if (mEnabled || mLastUpdateTimestampMs < mLastStateChangeTimestampMs) { + if (timestampMs < mLastStateChangeTimestampMs) { + timestampMs = mLastStateChangeTimestampMs; + } + + setState(mCurrentState, timestampMs); + + if (mLastUpdateTimestampMs >= 0) { + if (timestampMs > mLastUpdateTimestampMs) { + long delta = value - mValue; + if (delta >= 0) { + returnValue = delta; + long timeSinceUpdate = timestampMs - mLastUpdateTimestampMs; + for (int i = 0; i < mStateCount; i++) { + long timeInState = mStates[i].mTimeInStateSinceUpdate; + if (timeInState > 0) { + mStates[i].mCounter += delta * timeInState / timeSinceUpdate; + mStates[i].mTimeInStateSinceUpdate = 0; + } + } + } else { + for (int i = 0; i < mStateCount; i++) { + mStates[i].mTimeInStateSinceUpdate = 0; + } + } + } else if (timestampMs < mLastUpdateTimestampMs) { + for (int i = 0; i < mStateCount; i++) { + mStates[i].mTimeInStateSinceUpdate = 0; + } + } + } + } + mValue = value; + mLastUpdateTimestampMs = timestampMs; + return returnValue; + } + + public void incrementValue(long count, long timestampMs) { + updateValue(mValue + count, timestampMs); + } + + public long getValue(int state) { + return mStates[state].mCounter; + } + + public void reset() { + mLastStateChangeTimestampMs = -1; + mLastUpdateTimestampMs = -1; + for (int i = 0; i < mStateCount; i++) { + mStates[i].mTimeInStateSinceUpdate = 0; + mStates[i].mCounter = 0; + } + } + + public void writeToParcel(Parcel parcel) { + parcel.writeInt(mStateCount); + for (int i = 0; i < mStateCount; i++) { + parcel.writeLong(mStates[i].mCounter); + } + } + + public void initFromParcel(Parcel parcel) { + try { + for (int i = 0; i < mStateCount; i++) { + mStates[i].mCounter = parcel.readLong(); + } + } catch (Exception e) { + throw new BadParcelableException(e); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("["); + for (int state = 0; state < mStateCount; state++) { + if (state != 0) { + sb.append(", "); + } + sb.append(state).append(": ").append(mStates[state].mCounter); + } + sb.append("]"); + if (mLastUpdateTimestampMs >= 0) { + sb.append(" updated: ").append(mLastUpdateTimestampMs); + } + if (mLastStateChangeTimestampMs >= 0) { + sb.append(" currentState: ").append(mCurrentState); + if (mLastStateChangeTimestampMs > mLastUpdateTimestampMs) { + sb.append(" stateChanged: ").append(mLastStateChangeTimestampMs); + } + } else { + sb.append(" currentState: none"); + } + return sb.toString(); + } + } + + private static final HashMap<Long, LongMultiStateCounterRavenwood> sInstances = + new HashMap<>(); + private static long sNextId = 1; + + public static long native_init(int stateCount) { + LongMultiStateCounterRavenwood instance = new LongMultiStateCounterRavenwood(stateCount); + long instanceId = sNextId++; + sInstances.put(instanceId, instance); + return instanceId; + } + + private static LongMultiStateCounterRavenwood getInstance(long instanceId) { + return sInstances.get(instanceId); + } + + public static void native_setEnabled(long instanceId, boolean enabled, + long timestampMs) { + getInstance(instanceId).setEnabled(enabled, timestampMs); + } + + public static int native_getStateCount(long instanceId) { + return getInstance(instanceId).mStateCount; + } + + public static long native_updateValue(long instanceId, long value, long timestampMs) { + return getInstance(instanceId).updateValue(value, timestampMs); + } + + public static void native_setState(long instanceId, int state, long timestampMs) { + getInstance(instanceId).setState(state, timestampMs); + } + + public static void native_incrementValue(long instanceId, long count, long timestampMs) { + getInstance(instanceId).incrementValue(count, timestampMs); + } + + public static long native_getCount(long instanceId, int state) { + return getInstance(instanceId).getValue(state); + } + + public static void native_reset(long instanceId) { + getInstance(instanceId).reset(); + } + + public static void native_writeToParcel(long instanceId, Parcel parcel, int flags) { + getInstance(instanceId).writeToParcel(parcel); + } + + public static long native_initFromParcel(Parcel parcel) { + int stateCount = parcel.readInt(); + if (stateCount < 0 || stateCount > 0xEFFF) { + throw new BadParcelableException("stateCount out of range"); + } + // LongMultiStateCounter.cpp uses AParcel, which throws on out-of-data. + if (parcel.dataPosition() >= parcel.dataSize()) { + throw new RuntimeException("Bad parcel"); + } + long instanceId = native_init(stateCount); + getInstance(instanceId).initFromParcel(parcel); + if (parcel.dataPosition() > parcel.dataSize()) { + throw new RuntimeException("Bad parcel"); + } + return instanceId; + } + + public static String native_toString(long instanceId) { + return getInstance(instanceId).toString(); + } +} diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt index 97e09b817ef6..06eeb47c94ed 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt @@ -49,6 +49,7 @@ import java.util.zip.ZipOutputStream class HostStubGen(val options: HostStubGenOptions) { fun run() { val errors = HostStubGenErrors() + val stats = HostStubGenStats() // Load all classes. val allClasses = loadClassStructures(options.inJar.get) @@ -80,7 +81,14 @@ class HostStubGen(val options: HostStubGenOptions) { options.enableClassChecker.get, allClasses, errors, + stats, ) + + // Dump statistics, if specified. + options.statsFile.ifSet { + PrintWriter(it).use { pw -> stats.dump(pw) } + log.i("Dump file created at $it") + } } /** @@ -237,6 +245,7 @@ class HostStubGen(val options: HostStubGenOptions) { enableChecker: Boolean, classes: ClassNodes, errors: HostStubGenErrors, + stats: HostStubGenStats, ) { log.i("Converting %s into [stub: %s, impl: %s] ...", inJar, outStubJar, outImplJar) log.i("ASM CheckClassAdapter is %s", if (enableChecker) "enabled" else "disabled") @@ -254,7 +263,8 @@ class HostStubGen(val options: HostStubGenOptions) { while (inEntries.hasMoreElements()) { val entry = inEntries.nextElement() convertSingleEntry(inZip, entry, stubOutStream, implOutStream, - filter, packageRedirector, enableChecker, classes, errors) + filter, packageRedirector, enableChecker, classes, errors, + stats) } log.i("Converted all entries.") } @@ -287,6 +297,7 @@ class HostStubGen(val options: HostStubGenOptions) { enableChecker: Boolean, classes: ClassNodes, errors: HostStubGenErrors, + stats: HostStubGenStats, ) { log.d("Entry: %s", entry.name) log.withIndent { @@ -300,7 +311,7 @@ class HostStubGen(val options: HostStubGenOptions) { // If it's a class, convert it. if (name.endsWith(".class")) { processSingleClass(inZip, entry, stubOutStream, implOutStream, filter, - packageRedirector, enableChecker, classes, errors) + packageRedirector, enableChecker, classes, errors, stats) return } @@ -354,6 +365,7 @@ class HostStubGen(val options: HostStubGenOptions) { enableChecker: Boolean, classes: ClassNodes, errors: HostStubGenErrors, + stats: HostStubGenStats, ) { val classInternalName = entry.name.replaceFirst("\\.class$".toRegex(), "") val classPolicy = filter.getPolicyForClass(classInternalName) @@ -370,7 +382,7 @@ class HostStubGen(val options: HostStubGenOptions) { stubOutStream.putNextEntry(newEntry) convertClass(classInternalName, /*forImpl=*/false, bis, stubOutStream, filter, packageRedirector, enableChecker, classes, - errors) + errors, stats) stubOutStream.closeEntry() } } @@ -383,7 +395,7 @@ class HostStubGen(val options: HostStubGenOptions) { implOutStream.putNextEntry(newEntry) convertClass(classInternalName, /*forImpl=*/true, bis, implOutStream, filter, packageRedirector, enableChecker, classes, - errors) + errors, stats) implOutStream.closeEntry() } } @@ -403,6 +415,7 @@ class HostStubGen(val options: HostStubGenOptions) { enableChecker: Boolean, classes: ClassNodes, errors: HostStubGenErrors, + stats: HostStubGenStats, ) { val cr = ClassReader(input) @@ -420,6 +433,7 @@ class HostStubGen(val options: HostStubGenOptions) { enablePostTrace = options.enablePostTrace.get, enableNonStubMethodCallDetection = options.enableNonStubMethodCallDetection.get, errors = errors, + stats = stats, ) outVisitor = BaseAdapter.getVisitor(classInternalName, classes, outVisitor, filter, packageRedirector, forImpl, visitorOptions) diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt index d2ead18b5492..9f5d524517d0 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt @@ -108,6 +108,8 @@ class HostStubGenOptions( var enablePostTrace: SetOnce<Boolean> = SetOnce(false), var enableNonStubMethodCallDetection: SetOnce<Boolean> = SetOnce(false), + + var statsFile: SetOnce<String?> = SetOnce(null), ) { companion object { @@ -252,6 +254,8 @@ class HostStubGenOptions( "--verbose-log" -> setLogFile(LogLevel.Verbose, nextArg()) "--debug-log" -> setLogFile(LogLevel.Debug, nextArg()) + "--stats-file" -> ret.statsFile.setNextStringArg() + else -> throw ArgumentsException("Unknown option: $arg") } } catch (e: SetOnce.SetMoreThanOnceException) { @@ -387,6 +391,7 @@ class HostStubGenOptions( enablePreTrace=$enablePreTrace, enablePostTrace=$enablePostTrace, enableNonStubMethodCallDetection=$enableNonStubMethodCallDetection, + statsFile=$statsFile, } """.trimIndent() } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt new file mode 100644 index 000000000000..fe4072f6b3ee --- /dev/null +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt @@ -0,0 +1,74 @@ +/* + * 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.hoststubgen + +import com.android.hoststubgen.asm.toHumanReadableClassName +import com.android.hoststubgen.filters.FilterPolicyWithReason +import java.io.PrintWriter + +open class HostStubGenStats { + data class Stats( + var supported: Int = 0, + var total: Int = 0, + val children: MutableMap<String, Stats> = mutableMapOf<String, Stats>(), + ) + + private val stats = mutableMapOf<String, Stats>() + + fun onVisitPolicyForMethod(fullClassName: String, policy: FilterPolicyWithReason) { + if (policy.isIgnoredForStats) return + + val packageName = resolvePackageName(fullClassName) + val className = resolveClassName(fullClassName) + + val packageStats = stats.getOrPut(packageName) { Stats() } + val classStats = packageStats.children.getOrPut(className) { Stats() } + + if (policy.policy.isSupported) { + packageStats.supported += 1 + classStats.supported += 1 + } + packageStats.total += 1 + classStats.total += 1 + } + + fun dump(pw: PrintWriter) { + pw.printf("PackageName,ClassName,SupportedMethods,TotalMethods\n") + stats.forEach { (packageName, packageStats) -> + if (packageStats.supported > 0) { + packageStats.children.forEach { (className, classStats) -> + pw.printf("%s,%s,%d,%d\n", packageName, className, + classStats.supported, classStats.total) + } + } + } + } + + private fun resolvePackageName(fullClassName: String): String { + val start = fullClassName.lastIndexOf('/') + return fullClassName.substring(0, start).toHumanReadableClassName() + } + + private fun resolveClassName(fullClassName: String): String { + val start = fullClassName.lastIndexOf('/') + val end = fullClassName.indexOf('$') + if (end == -1) { + return fullClassName.substring(start + 1) + } else { + return fullClassName.substring(start + 1, end) + } + } +} diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt index 93179969e968..4d211065f1c8 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt @@ -111,6 +111,16 @@ enum class FilterPolicy { } } + /** Returns whether a policy is considered supported. */ + val isSupported: Boolean + get() { + return when (this) { + // TODO: handle native method with no substitution as being unsupported + Stub, StubClass, Keep, KeepClass, SubstituteAndStub, SubstituteAndKeep -> true + else -> false + } + } + fun getSubstitutionBasePolicy(): FilterPolicy { return when (this) { SubstituteAndKeep -> Keep @@ -136,4 +146,4 @@ enum class FilterPolicy { fun withReason(reason: String): FilterPolicyWithReason { return FilterPolicyWithReason(this, reason) } -}
\ No newline at end of file +} diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt index b64a2f5fd8a5..53eb5a8c2fdc 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt @@ -63,4 +63,15 @@ data class FilterPolicyWithReason ( override fun toString(): String { return "[$policy - reason: $reason]" } -}
\ No newline at end of file + + /** Returns whether this policy should be ignored for stats. */ + val isIgnoredForStats: Boolean + get() { + return reason.contains("anonymous-inner-class") + || reason.contains("is-annotation") + || reason.contains("is-enum") + || reason.contains("is-synthetic-method") + || reason.contains("special-class") + || reason.contains("substitute-from") + } +} diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt index ea7d1d0ff391..78b13fd36f06 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt @@ -119,14 +119,14 @@ class ImplicitOutputFilter( if (cn.isEnum()) { mn?.let { mn -> if (isAutoGeneratedEnumMember(mn)) { - return memberPolicy.withReason(classPolicy.reason).wrapReason("enum") + return memberPolicy.withReason(classPolicy.reason).wrapReason("is-enum") } } } // Keep (or stub) all members of annotations. if (cn.isAnnotation()) { - return memberPolicy.withReason(classPolicy.reason).wrapReason("annotation") + return memberPolicy.withReason(classPolicy.reason).wrapReason("is-annotation") } mn?.let { @@ -134,7 +134,7 @@ class ImplicitOutputFilter( // For synthetic methods (such as lambdas), let's just inherit the class's // policy. return memberPolicy.withReason(classPolicy.reason).wrapReason( - "synthetic method") + "is-synthetic-method") } } } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt index 7fdd944770c6..6ad83fbab083 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt @@ -132,23 +132,24 @@ fun createFilterFromTextPolicyFile( throw ParseException( "Policy for AIDL classes already defined") } - aidlPolicy = policy.withReason("$FILTER_REASON (AIDL)") + aidlPolicy = policy.withReason( + "$FILTER_REASON (special-class AIDL)") } SpecialClass.FeatureFlags -> { if (featureFlagsPolicy != null) { throw ParseException( "Policy for feature flags already defined") } - featureFlagsPolicy = - policy.withReason("$FILTER_REASON (feature flags)") + featureFlagsPolicy = policy.withReason( + "$FILTER_REASON (special-class feature flags)") } SpecialClass.Sysprops -> { if (syspropsPolicy != null) { throw ParseException( "Policy for sysprops already defined") } - syspropsPolicy = - policy.withReason("$FILTER_REASON (sysprops)") + syspropsPolicy = policy.withReason( + "$FILTER_REASON (special-class sysprops)") } } } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt index 21cfd4bcd0d8..c20aa8bc70ca 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt @@ -16,6 +16,7 @@ package com.android.hoststubgen.visitors import com.android.hoststubgen.HostStubGenErrors +import com.android.hoststubgen.HostStubGenStats import com.android.hoststubgen.LogLevel import com.android.hoststubgen.asm.ClassNodes import com.android.hoststubgen.asm.UnifiedVisitor @@ -50,6 +51,7 @@ abstract class BaseAdapter ( */ data class Options ( val errors: HostStubGenErrors, + val stats: HostStubGenStats, val enablePreTrace: Boolean, val enablePostTrace: Boolean, val enableNonStubMethodCallDetection: Boolean, diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt index 416b78242899..beca945a2819 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt @@ -141,6 +141,11 @@ class ImplGeneratingAdapter( substituted: Boolean, superVisitor: MethodVisitor?, ): MethodVisitor? { + // Record statistics about visiting this method when visible. + if ((access and Opcodes.ACC_PRIVATE) == 0) { + options.stats.onVisitPolicyForMethod(currentClassName, policy) + } + // Inject method log, if needed. var innerVisitor = superVisitor |