diff options
292 files changed, 8759 insertions, 1335 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 1c6df75a4f02..7e6c30f0719a 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -54,6 +54,7 @@ aconfig_srcjars = [ ":android.service.controls.flags-aconfig-java{.generated_srcjars}", ":android.service.dreams.flags-aconfig-java{.generated_srcjars}", ":android.service.notification.flags-aconfig-java{.generated_srcjars}", + ":android.service.appprediction.flags-aconfig-java{.generated_srcjars}", ":android.service.voice.flags-aconfig-java{.generated_srcjars}", ":android.speech.flags-aconfig-java{.generated_srcjars}", ":android.systemserver.flags-aconfig-java{.generated_srcjars}", @@ -125,6 +126,7 @@ stubs_defaults { "android.provider.flags-aconfig", "android.security.flags-aconfig", "android.server.app.flags-aconfig", + "android.service.appprediction.flags-aconfig", "android.service.autofill.flags-aconfig", "android.service.chooser.flags-aconfig", "android.service.controls.flags-aconfig", @@ -726,6 +728,19 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +// App prediction +aconfig_declarations { + name: "android.service.appprediction.flags-aconfig", + package: "android.service.appprediction.flags", + srcs: ["core/java/android/service/appprediction/flags/*.aconfig"], +} + +java_aconfig_library { + name: "android.service.appprediction.flags-aconfig-java", + aconfig_declarations: "android.service.appprediction.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Controls aconfig_declarations { name: "android.service.controls.flags-aconfig", @@ -855,6 +870,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", @@ -40,4 +40,6 @@ per-file *Ravenwood* = file:ravenwood/OWNERS per-file PERFORMANCE_OWNERS = file:/PERFORMANCE_OWNERS -per-file PACKAGE_MANAGER_OWNERS = file:/PACKAGE_MANAGER_OWNERS
\ No newline at end of file +per-file PACKAGE_MANAGER_OWNERS = file:/PACKAGE_MANAGER_OWNERS + +per-file WEAR_OWNERS = file:/WEAR_OWNERS diff --git a/Ravenwood.bp b/Ravenwood.bp index 93febca49421..2babf6a56ab4 100644 --- a/Ravenwood.bp +++ b/Ravenwood.bp @@ -96,14 +96,19 @@ java_library { android_ravenwood_libgroup { name: "ravenwood-runtime", libs: [ + // Prefixed with "200" to ensure it's sorted early in Tradefed classpath + // so that we provide a concrete implementation before Mainline stubs + "200-kxml2-android", + "all-updatable-modules-system-stubs", + "android.test.mock.ravenwood", "framework-minus-apex.ravenwood", - "hoststubgen-helper-runtime.ravenwood", "hoststubgen-helper-framework-runtime.ravenwood", - "all-updatable-modules-system-stubs", + "hoststubgen-helper-runtime.ravenwood", + + // Provide runtime versions of utils linked in below "junit", "truth", "ravenwood-junit-impl", - "android.test.mock.ravenwood", "mockito-ravenwood-prebuilt", "inline-mockito-ravenwood-prebuilt", ], diff --git a/WEAR_OWNERS b/WEAR_OWNERS index 4127f996da03..da8c83ebcc98 100644 --- a/WEAR_OWNERS +++ b/WEAR_OWNERS @@ -10,3 +10,4 @@ sadrul@google.com rwmyers@google.com nalmalki@google.com shijianli@google.com +latkin@google.com 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/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 9c3196ddb190..076f016b6914 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -653,6 +653,7 @@ package android { field public static final int contentInsetRight = 16843862; // 0x1010456 field public static final int contentInsetStart = 16843859; // 0x1010453 field public static final int contentInsetStartWithNavigation = 16844066; // 0x1010522 + field @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public static final int contentSensitivity; field public static final int contextClickable = 16844007; // 0x10104e7 field public static final int contextDescription = 16844078; // 0x101052e field public static final int contextPopupMenuStyle = 16844033; // 0x1010501 @@ -5448,7 +5449,6 @@ package android.app { } @FlaggedApi("android.security.content_uri_permission_apis") public final class ComponentCaller { - ctor public ComponentCaller(@NonNull android.os.IBinder, @Nullable android.os.IBinder); method public int checkContentUriPermission(@NonNull android.net.Uri, int); method @Nullable public String getPackage(); method public int getUid(); @@ -7425,6 +7425,7 @@ package android.app { method public int onStartCommand(android.content.Intent, int, int); method public void onTaskRemoved(android.content.Intent); method public void onTimeout(int); + method @FlaggedApi("android.app.introduce_new_service_ontimeout_callback") public void onTimeout(int, int); method public void onTrimMemory(int); method public boolean onUnbind(android.content.Intent); method public final void startForeground(int, android.app.Notification); @@ -7840,6 +7841,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 @@ -10044,11 +10046,22 @@ package android.content { method public CharSequence coerceToText(android.content.Context); method public String getHtmlText(); method public android.content.Intent getIntent(); + method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @Nullable public android.app.PendingIntent getPendingIntent(); method public CharSequence getText(); method @Nullable public android.view.textclassifier.TextLinks getTextLinks(); method public android.net.Uri getUri(); } + @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") public static final class ClipData.Item.Builder { + ctor public ClipData.Item.Builder(); + method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item build(); + method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setHtmlText(@Nullable String); + method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setIntent(@Nullable android.content.Intent); + method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setPendingIntent(@Nullable android.app.PendingIntent); + method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setText(@Nullable CharSequence); + method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setUri(@Nullable android.net.Uri); + } + public class ClipDescription implements android.os.Parcelable { ctor public ClipDescription(CharSequence, String[]); ctor public ClipDescription(android.content.ClipDescription); @@ -10713,6 +10726,7 @@ package android.content { field public static final String PERFORMANCE_HINT_SERVICE = "performance_hint"; field public static final String POWER_SERVICE = "power"; field public static final String PRINT_SERVICE = "print"; + field @FlaggedApi("android.os.telemetry_apis_framework_initialization") public static final String PROFILING_SERVICE = "profiling"; field public static final int RECEIVER_EXPORTED = 2; // 0x2 field public static final int RECEIVER_NOT_EXPORTED = 4; // 0x4 field public static final int RECEIVER_VISIBLE_TO_INSTANT_APPS = 1; // 0x1 @@ -11303,10 +11317,14 @@ package android.content { field public static final String EXTRA_CHANGED_COMPONENT_NAME_LIST = "android.intent.extra.changed_component_name_list"; field public static final String EXTRA_CHANGED_PACKAGE_LIST = "android.intent.extra.changed_package_list"; field public static final String EXTRA_CHANGED_UID_LIST = "android.intent.extra.changed_uid_list"; + field @FlaggedApi("android.service.chooser.chooser_payload_toggling") public static final String EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI = "android.intent.extra.CHOOSER_ADDITIONAL_CONTENT_URI"; field @FlaggedApi("android.service.chooser.chooser_album_text") public static final String EXTRA_CHOOSER_CONTENT_TYPE_HINT = "android.intent.extra.CHOOSER_CONTENT_TYPE_HINT"; field public static final String EXTRA_CHOOSER_CUSTOM_ACTIONS = "android.intent.extra.CHOOSER_CUSTOM_ACTIONS"; + field @FlaggedApi("android.service.chooser.chooser_payload_toggling") public static final String EXTRA_CHOOSER_FOCUSED_ITEM_POSITION = "android.intent.extra.CHOOSER_FOCUSED_ITEM_POSITION"; field public static final String EXTRA_CHOOSER_MODIFY_SHARE_ACTION = "android.intent.extra.CHOOSER_MODIFY_SHARE_ACTION"; field public static final String EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER = "android.intent.extra.CHOOSER_REFINEMENT_INTENT_SENDER"; + field @FlaggedApi("android.service.chooser.enable_chooser_result") public static final String EXTRA_CHOOSER_RESULT = "android.intent.extra.CHOOSER_RESULT"; + field @FlaggedApi("android.service.chooser.enable_chooser_result") public static final String EXTRA_CHOOSER_RESULT_INTENT_SENDER = "android.intent.extra.CHOOSER_RESULT_INTENT_SENDER"; field public static final String EXTRA_CHOOSER_TARGETS = "android.intent.extra.CHOOSER_TARGETS"; field public static final String EXTRA_CHOSEN_COMPONENT = "android.intent.extra.CHOSEN_COMPONENT"; field public static final String EXTRA_CHOSEN_COMPONENT_INTENT_SENDER = "android.intent.extra.CHOSEN_COMPONENT_INTENT_SENDER"; @@ -11335,6 +11353,7 @@ package android.content { field public static final String EXTRA_LOCALE_LIST = "android.intent.extra.LOCALE_LIST"; field public static final String EXTRA_LOCAL_ONLY = "android.intent.extra.LOCAL_ONLY"; field public static final String EXTRA_LOCUS_ID = "android.intent.extra.LOCUS_ID"; + field @FlaggedApi("android.service.chooser.enable_sharesheet_metadata_extra") public static final String EXTRA_METADATA_TEXT = "android.intent.extra.METADATA_TEXT"; field public static final String EXTRA_MIME_TYPES = "android.intent.extra.MIME_TYPES"; field public static final String EXTRA_NOT_UNKNOWN_SOURCE = "android.intent.extra.NOT_UNKNOWN_SOURCE"; field public static final String EXTRA_ORIGINATING_URI = "android.intent.extra.ORIGINATING_URI"; @@ -13025,6 +13044,7 @@ package android.content.pm { field public static final String FEATURE_AUDIO_LOW_LATENCY = "android.hardware.audio.low_latency"; field public static final String FEATURE_AUDIO_OUTPUT = "android.hardware.audio.output"; field public static final String FEATURE_AUDIO_PRO = "android.hardware.audio.pro"; + field @FlaggedApi("android.media.audio.feature_spatial_audio_headtracking_low_latency") public static final String FEATURE_AUDIO_SPATIAL_HEADTRACKING_LOW_LATENCY = "android.hardware.audio.spatial.headtracking.low_latency"; field public static final String FEATURE_AUTOFILL = "android.software.autofill"; field public static final String FEATURE_AUTOMOTIVE = "android.hardware.type.automotive"; field public static final String FEATURE_BACKUP = "android.software.backup"; @@ -18846,6 +18866,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(); @@ -18897,6 +18918,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); @@ -25869,6 +25891,9 @@ package android.media.metrics { method public int describeContents(); method public int getErrorCode(); method public int getFinalState(); + method @NonNull public java.util.List<android.media.metrics.MediaItemInfo> getInputMediaItemInfos(); + method public long getOperationTypes(); + method @Nullable public android.media.metrics.MediaItemInfo getOutputMediaItemInfo(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.media.metrics.EditingEndedEvent> CREATOR; field public static final int ERROR_CODE_AUDIO_PROCESSING_FAILED = 18; // 0x12 @@ -25893,14 +25918,25 @@ package android.media.metrics { field public static final int FINAL_STATE_CANCELED = 2; // 0x2 field public static final int FINAL_STATE_ERROR = 3; // 0x3 field public static final int FINAL_STATE_SUCCEEDED = 1; // 0x1 + field public static final long OPERATION_TYPE_AUDIO_EDIT = 8L; // 0x8L + field public static final long OPERATION_TYPE_AUDIO_TRANSCODE = 2L; // 0x2L + field public static final long OPERATION_TYPE_AUDIO_TRANSMUX = 32L; // 0x20L + field public static final long OPERATION_TYPE_PAUSED = 64L; // 0x40L + field public static final long OPERATION_TYPE_RESUMED = 128L; // 0x80L + field public static final long OPERATION_TYPE_VIDEO_EDIT = 4L; // 0x4L + field public static final long OPERATION_TYPE_VIDEO_TRANSCODE = 1L; // 0x1L + field public static final long OPERATION_TYPE_VIDEO_TRANSMUX = 16L; // 0x10L field public static final int TIME_SINCE_CREATED_UNKNOWN = -1; // 0xffffffff } @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public static final class EditingEndedEvent.Builder { ctor public EditingEndedEvent.Builder(int); + method @NonNull public android.media.metrics.EditingEndedEvent.Builder addInputMediaItemInfo(@NonNull android.media.metrics.MediaItemInfo); + method @NonNull public android.media.metrics.EditingEndedEvent.Builder addOperationType(long); method @NonNull public android.media.metrics.EditingEndedEvent build(); method @NonNull public android.media.metrics.EditingEndedEvent.Builder setErrorCode(int); method @NonNull public android.media.metrics.EditingEndedEvent.Builder setMetricsBundle(@NonNull android.os.Bundle); + method @NonNull public android.media.metrics.EditingEndedEvent.Builder setOutputMediaItemInfo(@NonNull android.media.metrics.MediaItemInfo); method @NonNull public android.media.metrics.EditingEndedEvent.Builder setTimeSinceCreatedMillis(@IntRange(from=android.media.metrics.EditingEndedEvent.TIME_SINCE_CREATED_UNKNOWN) long); } @@ -25920,6 +25956,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(); @@ -27718,7 +27813,7 @@ package android.media.tv { package android.media.tv.ad { - @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public class TvAdManager { + @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public final class TvAdManager { method @NonNull public java.util.List<android.media.tv.ad.TvAdServiceInfo> getTvAdServiceList(); method public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.ad.TvAdManager.TvAdServiceCallback); method public void sendAppLinkCommand(@NonNull String, @NonNull android.os.Bundle); @@ -27729,6 +27824,14 @@ package android.media.tv.ad { field public static final String APP_LINK_KEY_COMMAND_TYPE = "command_type"; field public static final String APP_LINK_KEY_PACKAGE_NAME = "package_name"; field public static final String APP_LINK_KEY_SERVICE_ID = "service_id"; + field public static final int ERROR_BLOCKED = 5; // 0x5 + field public static final int ERROR_ENCRYPTED = 6; // 0x6 + field public static final int ERROR_NONE = 0; // 0x0 + field public static final int ERROR_NOT_SUPPORTED = 2; // 0x2 + field public static final int ERROR_RESOURCE_UNAVAILABLE = 4; // 0x4 + field public static final int ERROR_UNKNOWN = 1; // 0x1 + field public static final int ERROR_UNKNOWN_CHANNEL = 7; // 0x7 + field public static final int ERROR_WEAK_SIGNAL = 3; // 0x3 field public static final String INTENT_KEY_AD_SERVICE_ID = "ad_service_id"; field public static final String INTENT_KEY_CHANNEL_URI = "channel_uri"; field public static final String INTENT_KEY_COMMAND_TYPE = "command_type"; @@ -27741,6 +27844,9 @@ package android.media.tv.ad { field public static final String SESSION_DATA_TYPE_AD_REQUEST = "ad_request"; field public static final String SESSION_DATA_TYPE_BROADCAST_INFO_REQUEST = "broadcast_info_request"; field public static final String SESSION_DATA_TYPE_REMOVE_BROADCAST_INFO_REQUEST = "remove_broadcast_info_request"; + field public static final int SESSION_STATE_ERROR = 3; // 0x3 + field public static final int SESSION_STATE_RUNNING = 2; // 0x2 + field public static final int SESSION_STATE_STOPPED = 1; // 0x1 } public abstract static class TvAdManager.TvAdServiceCallback { @@ -27763,7 +27869,12 @@ package android.media.tv.ad { ctor public TvAdService.Session(@NonNull android.content.Context); method public boolean isMediaViewEnabled(); method @CallSuper public void layoutSurface(int, int, int, int); + method @CallSuper public void notifySessionStateChanged(int, int); method @Nullable public android.view.View onCreateMediaView(); + method public void onCurrentChannelUri(@Nullable android.net.Uri); + method public void onCurrentTvInputId(@Nullable String); + method public void onCurrentVideoBounds(@NonNull android.graphics.Rect); + method public void onError(@NonNull String, @NonNull android.os.Bundle); method public boolean onGenericMotionEvent(@NonNull android.view.MotionEvent); method public boolean onKeyDown(int, @Nullable android.view.KeyEvent); method public boolean onKeyLongPress(int, @Nullable android.view.KeyEvent); @@ -27773,12 +27884,20 @@ package android.media.tv.ad { method public abstract void onRelease(); method public void onResetAdService(); method public abstract boolean onSetSurface(@Nullable android.view.Surface); + method public void onSigningResult(@NonNull String, @NonNull byte[]); method public void onStartAdService(); method public void onStopAdService(); method public void onSurfaceChanged(int, int, int); method public boolean onTouchEvent(@NonNull android.view.MotionEvent); + method public void onTrackInfoList(@NonNull java.util.List<android.media.tv.TvTrackInfo>); method public boolean onTrackballEvent(@NonNull android.view.MotionEvent); method public void onTvInputSessionData(@NonNull String, @NonNull android.os.Bundle); + method public void onTvMessage(int, @NonNull android.os.Bundle); + method @CallSuper public void requestCurrentChannelUri(); + method @CallSuper public void requestCurrentTvInputId(); + method @CallSuper public void requestCurrentVideoBounds(); + method @CallSuper public void requestSigning(@NonNull String, @NonNull String, @NonNull String, @NonNull byte[]); + method @CallSuper public void requestTrackInfoList(); method public void sendTvAdSessionData(@NonNull String, @NonNull android.os.Bundle); method @CallSuper public void setMediaViewEnabled(boolean); } @@ -27797,9 +27916,12 @@ package android.media.tv.ad { ctor public TvAdView(@NonNull android.content.Context); ctor public TvAdView(@NonNull android.content.Context, @Nullable android.util.AttributeSet); ctor public TvAdView(@NonNull android.content.Context, @Nullable android.util.AttributeSet, int); + method public void clearCallback(); method public void clearOnUnhandledInputEventListener(); method public boolean dispatchUnhandledInputEvent(@NonNull android.view.InputEvent); method @Nullable public android.media.tv.ad.TvAdView.OnUnhandledInputEventListener getOnUnhandledInputEventListener(); + method public void notifyError(@NonNull String, @NonNull android.os.Bundle); + method public void notifyTvMessage(@NonNull int, @NonNull android.os.Bundle); method public void onAttachedToWindow(); method public void onDetachedFromWindow(); method public void onLayout(boolean, int, int, int, int); @@ -27809,16 +27931,34 @@ package android.media.tv.ad { method public void prepareAdService(@NonNull String, @NonNull String); method public void reset(); method public void resetAdService(); + method public void sendCurrentChannelUri(@Nullable android.net.Uri); + method public void sendCurrentTvInputId(@Nullable String); + method public void sendCurrentVideoBounds(@NonNull android.graphics.Rect); + method public void sendSigningResult(@NonNull String, @NonNull byte[]); + method public void sendTrackInfoList(@Nullable java.util.List<android.media.tv.TvTrackInfo>); + method public void setCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.ad.TvAdView.TvAdCallback); method public void setOnUnhandledInputEventListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.ad.TvAdView.OnUnhandledInputEventListener); method public boolean setTvView(@Nullable android.media.tv.TvView); method public void startAdService(); method public void stopAdService(); + field public static final String ERROR_KEY_ERROR_CODE = "error_code"; + field public static final String ERROR_KEY_METHOD_NAME = "method_name"; } public static interface TvAdView.OnUnhandledInputEventListener { method public boolean onUnhandledInputEvent(@NonNull android.view.InputEvent); } + public abstract static class TvAdView.TvAdCallback { + ctor public TvAdView.TvAdCallback(); + method public void onRequestCurrentChannelUri(@NonNull String); + method public void onRequestCurrentTvInputId(@NonNull String); + method public void onRequestCurrentVideoBounds(@NonNull String); + method public void onRequestSigning(@NonNull String, @NonNull String, @NonNull String, @NonNull String, @NonNull byte[]); + method public void onRequestTrackInfoList(@NonNull String); + method public void onStateChanged(@NonNull String, int, int); + } + } package android.media.tv.interactive { @@ -35392,6 +35532,7 @@ package android.provider { ctor public CallLog.Calls(); method public static String getLastOutgoingCall(android.content.Context); field public static final int ANSWERED_EXTERNALLY_TYPE = 7; // 0x7 + field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final String ASSERTED_DISPLAY_NAME = "asserted_display_name"; field public static final long AUTO_MISSED_EMERGENCY_CALL = 1L; // 0x1L field public static final long AUTO_MISSED_MAXIMUM_DIALING = 4L; // 0x4L field public static final long AUTO_MISSED_MAXIMUM_RINGING = 2L; // 0x2L @@ -35438,6 +35579,7 @@ package android.provider { field public static final int FEATURES_WIFI = 8; // 0x8 field public static final String GEOCODED_LOCATION = "geocoded_location"; field public static final int INCOMING_TYPE = 1; // 0x1 + field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final String IS_BUSINESS_CALL = "is_business_call"; field public static final String IS_READ = "is_read"; field public static final String LAST_MODIFIED = "last_modified"; field public static final String LIMIT_PARAM_KEY = "limit"; @@ -35486,7 +35628,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); @@ -35501,9 +35643,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 { @@ -40137,6 +40279,21 @@ package android.service.carrier { package android.service.chooser { + @FlaggedApi("android.service.chooser.chooser_payload_toggling") public interface AdditionalContentContract { + } + + public static interface AdditionalContentContract.Columns { + field public static final String URI = "uri"; + } + + public static interface AdditionalContentContract.CursorExtraKeys { + field public static final String POSITION = "position"; + } + + public static interface AdditionalContentContract.MethodNames { + field public static final String ON_SELECTION_CHANGED = "onSelectionChanged"; + } + public final class ChooserAction implements android.os.Parcelable { method public int describeContents(); method @NonNull public android.app.PendingIntent getAction(); @@ -40151,6 +40308,19 @@ package android.service.chooser { method @NonNull public android.service.chooser.ChooserAction build(); } + @FlaggedApi("android.service.chooser.enable_chooser_result") public final class ChooserResult implements android.os.Parcelable { + method public int describeContents(); + method @Nullable public android.content.ComponentName getSelectedComponent(); + method public int getType(); + method public boolean isShortcut(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field public static final int CHOOSER_RESULT_COPY = 1; // 0x1 + field public static final int CHOOSER_RESULT_EDIT = 2; // 0x2 + field public static final int CHOOSER_RESULT_SELECTED_COMPONENT = 0; // 0x0 + field public static final int CHOOSER_RESULT_UNKNOWN = -1; // 0xffffffff + field @NonNull public static final android.os.Parcelable.Creator<android.service.chooser.ChooserResult> CREATOR; + } + @Deprecated public final class ChooserTarget implements android.os.Parcelable { ctor @Deprecated public ChooserTarget(CharSequence, android.graphics.drawable.Icon, float, android.content.ComponentName, @Nullable android.os.Bundle); method @Deprecated public int describeContents(); @@ -41915,8 +42085,10 @@ package android.telecom { field @Deprecated public static final String AVAILABLE_PHONE_ACCOUNTS = "selectPhoneAccountAccounts"; field public static final String EVENT_CLEAR_DIAGNOSTIC_MESSAGE = "android.telecom.event.CLEAR_DIAGNOSTIC_MESSAGE"; field public static final String EVENT_DISPLAY_DIAGNOSTIC_MESSAGE = "android.telecom.event.DISPLAY_DIAGNOSTIC_MESSAGE"; + field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final String EXTRA_ASSERTED_DISPLAY_NAME = "android.telecom.extra.ASSERTED_DISPLAY_NAME"; field public static final String EXTRA_DIAGNOSTIC_MESSAGE = "android.telecom.extra.DIAGNOSTIC_MESSAGE"; field public static final String EXTRA_DIAGNOSTIC_MESSAGE_ID = "android.telecom.extra.DIAGNOSTIC_MESSAGE_ID"; + field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final String EXTRA_IS_BUSINESS_CALL = "android.telecom.extra.IS_BUSINESS_CALL"; field public static final String EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB = "android.telecom.extra.IS_SUPPRESSED_BY_DO_NOT_DISTURB"; field public static final String EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS = "android.telecom.extra.LAST_EMERGENCY_CALLBACK_TIME_MILLIS"; field public static final String EXTRA_SILENT_RINGING_REQUESTED = "android.telecom.extra.SILENT_RINGING_REQUESTED"; @@ -43544,6 +43716,7 @@ package android.telephony { field public static final String KEY_SMS_REQUIRES_DESTINATION_NUMBER_CONVERSION_BOOL = "sms_requires_destination_number_conversion_bool"; field public static final String KEY_SUBSCRIPTION_GROUP_UUID_STRING = "subscription_group_uuid_string"; field public static final String KEY_SUPPORTED_PREMIUM_CAPABILITIES_INT_ARRAY = "supported_premium_capabilities_int_array"; + field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final String KEY_SUPPORTS_BUSINESS_CALL_COMPOSER_BOOL = "supports_business_call_composer_bool"; field public static final String KEY_SUPPORTS_CALL_COMPOSER_BOOL = "supports_call_composer_bool"; field public static final String KEY_SUPPORTS_DEVICE_TO_DEVICE_COMMUNICATION_USING_DTMF_BOOL = "supports_device_to_device_communication_using_dtmf_bool"; field public static final String KEY_SUPPORTS_DEVICE_TO_DEVICE_COMMUNICATION_USING_RTP_BOOL = "supports_device_to_device_communication_using_rtp_bool"; @@ -45809,6 +45982,7 @@ package android.telephony { field public static final int AUTHTYPE_EAP_SIM = 128; // 0x80 field public static final int AUTHTYPE_GBA_BOOTSTRAP = 132; // 0x84 field public static final int AUTHTYPE_GBA_NAF_KEY_EXTERNAL = 133; // 0x85 + field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final int CALL_COMPOSER_STATUS_BUSINESS_ONLY = 2; // 0x2 field public static final int CALL_COMPOSER_STATUS_OFF = 0; // 0x0 field public static final int CALL_COMPOSER_STATUS_ON = 1; // 0x1 field public static final int CALL_STATE_IDLE = 0; // 0x0 @@ -46845,6 +47019,7 @@ package android.telephony.ims.feature { public static class MmTelFeature.MmTelCapabilities { method public final boolean isCapable(int); field public static final int CAPABILITY_TYPE_CALL_COMPOSER = 16; // 0x10 + field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final int CAPABILITY_TYPE_CALL_COMPOSER_BUSINESS_ONLY = 32; // 0x20 field public static final int CAPABILITY_TYPE_SMS = 8; // 0x8 field public static final int CAPABILITY_TYPE_UT = 4; // 0x4 field public static final int CAPABILITY_TYPE_VIDEO = 2; // 0x2 @@ -52820,9 +52995,11 @@ package android.view { field public static final int DRAG_FLAG_GLOBAL = 256; // 0x100 field public static final int DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION = 64; // 0x40 field public static final int DRAG_FLAG_GLOBAL_PREFIX_URI_PERMISSION = 128; // 0x80 + field @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") public static final int DRAG_FLAG_GLOBAL_SAME_APPLICATION = 4096; // 0x1000 field public static final int DRAG_FLAG_GLOBAL_URI_READ = 1; // 0x1 field public static final int DRAG_FLAG_GLOBAL_URI_WRITE = 2; // 0x2 field public static final int DRAG_FLAG_OPAQUE = 512; // 0x200 + field @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") public static final int DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG = 8192; // 0x2000 field @Deprecated public static final int DRAWING_CACHE_QUALITY_AUTO = 0; // 0x0 field @Deprecated public static final int DRAWING_CACHE_QUALITY_HIGH = 1048576; // 0x100000 field @Deprecated public static final int DRAWING_CACHE_QUALITY_LOW = 524288; // 0x80000 @@ -53912,8 +54089,11 @@ package android.view { method @Deprecated @NonNull public android.view.WindowInsets consumeDisplayCutout(); method @Deprecated @NonNull public android.view.WindowInsets consumeStableInsets(); method @Deprecated @NonNull public android.view.WindowInsets consumeSystemWindowInsets(); + method @FlaggedApi("android.view.flags.customizable_window_headers") @NonNull public java.util.List<android.graphics.Rect> getBoundingRects(int); + method @FlaggedApi("android.view.flags.customizable_window_headers") @NonNull public java.util.List<android.graphics.Rect> getBoundingRectsIgnoringVisibility(int); method @Nullable public android.view.DisplayCutout getDisplayCutout(); method @Nullable public android.view.DisplayShape getDisplayShape(); + method @FlaggedApi("android.view.flags.customizable_window_headers") @NonNull public android.util.Size getFrame(); method @NonNull public android.graphics.Insets getInsets(int); method @NonNull public android.graphics.Insets getInsetsIgnoringVisibility(int); method @Deprecated @NonNull public android.graphics.Insets getMandatorySystemGestureInsets(); @@ -53948,8 +54128,11 @@ package android.view { ctor public WindowInsets.Builder(); ctor public WindowInsets.Builder(@NonNull android.view.WindowInsets); method @NonNull public android.view.WindowInsets build(); + method @FlaggedApi("android.view.flags.customizable_window_headers") @NonNull public android.view.WindowInsets.Builder setBoundingRects(int, @NonNull java.util.List<android.graphics.Rect>); + method @FlaggedApi("android.view.flags.customizable_window_headers") @NonNull public android.view.WindowInsets.Builder setBoundingRectsIgnoringVisibility(int, @NonNull java.util.List<android.graphics.Rect>); method @NonNull public android.view.WindowInsets.Builder setDisplayCutout(@Nullable android.view.DisplayCutout); method @NonNull public android.view.WindowInsets.Builder setDisplayShape(@NonNull android.view.DisplayShape); + method @FlaggedApi("android.view.flags.customizable_window_headers") @NonNull public android.view.WindowInsets.Builder setFrame(int, int); method @NonNull public android.view.WindowInsets.Builder setInsets(int, @NonNull android.graphics.Insets); method @NonNull public android.view.WindowInsets.Builder setInsetsIgnoringVisibility(int, @NonNull android.graphics.Insets) throws java.lang.IllegalArgumentException; method @Deprecated @NonNull public android.view.WindowInsets.Builder setMandatorySystemGestureInsets(@NonNull android.graphics.Insets); diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index 1273da71b748..af8b708d7665 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -412,6 +412,19 @@ package android.os { field public static final int VPN_UID = 1016; // 0x3f8 } + @FlaggedApi("android.os.telemetry_apis_framework_initialization") public class ProfilingServiceManager { + method @NonNull public android.os.ProfilingServiceManager.ServiceRegisterer getProfilingServiceRegisterer(); + } + + public static class ProfilingServiceManager.ServiceNotFoundException extends java.lang.Exception { + ctor public ProfilingServiceManager.ServiceNotFoundException(@NonNull String); + } + + public static final class ProfilingServiceManager.ServiceRegisterer { + method @Nullable public android.os.IBinder get(); + method @Nullable public android.os.IBinder getOrThrow() throws android.os.ProfilingServiceManager.ServiceNotFoundException; + } + public final class ServiceManager { method @NonNull public static String[] getDeclaredInstances(@NonNull String); method public static boolean isDeclared(@NonNull String); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index aca003d7ba1c..e44eb24f131b 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -282,6 +282,7 @@ package android { field public static final String RADIO_SCAN_WITHOUT_LOCATION = "android.permission.RADIO_SCAN_WITHOUT_LOCATION"; field public static final String READ_ACTIVE_EMERGENCY_SESSION = "android.permission.READ_ACTIVE_EMERGENCY_SESSION"; field public static final String READ_APP_SPECIFIC_LOCALES = "android.permission.READ_APP_SPECIFIC_LOCALES"; + field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String READ_BLOCKED_NUMBERS = "android.permission.READ_BLOCKED_NUMBERS"; field public static final String READ_CARRIER_APP_INFO = "android.permission.READ_CARRIER_APP_INFO"; field public static final String READ_CELL_BROADCASTS = "android.permission.READ_CELL_BROADCASTS"; field public static final String READ_CLIPBOARD_IN_BACKGROUND = "android.permission.READ_CLIPBOARD_IN_BACKGROUND"; @@ -409,6 +410,7 @@ package android { field public static final String WIFI_UPDATE_COEX_UNSAFE_CHANNELS = "android.permission.WIFI_UPDATE_COEX_UNSAFE_CHANNELS"; field public static final String WIFI_UPDATE_USABILITY_STATS_SCORE = "android.permission.WIFI_UPDATE_USABILITY_STATS_SCORE"; field public static final String WRITE_ALLOWLISTED_DEVICE_CONFIG = "android.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG"; + field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String WRITE_BLOCKED_NUMBERS = "android.permission.WRITE_BLOCKED_NUMBERS"; field public static final String WRITE_DEVICE_CONFIG = "android.permission.WRITE_DEVICE_CONFIG"; field public static final String WRITE_DREAM_STATE = "android.permission.WRITE_DREAM_STATE"; field public static final String WRITE_EMBEDDED_SUBSCRIPTIONS = "android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS"; @@ -1389,6 +1391,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 @@ -2204,6 +2207,7 @@ package android.app.prediction { method public void notifyLaunchLocationShown(@NonNull String, @NonNull java.util.List<android.app.prediction.AppTargetId>); method public void registerPredictionUpdates(@NonNull java.util.concurrent.Executor, @NonNull android.app.prediction.AppPredictor.Callback); method public void requestPredictionUpdate(); + method @FlaggedApi("android.service.appprediction.flags.service_features_api") public void requestServiceFeatures(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.os.Bundle>); method @Nullable public void sortTargets(@NonNull java.util.List<android.app.prediction.AppTarget>, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.app.prediction.AppTarget>>); method public void unregisterPredictionUpdates(@NonNull android.app.prediction.AppPredictor.Callback); } @@ -6640,6 +6644,7 @@ package android.hardware.usb { method public int getSupportedRoleCombinations(); method public int getUsbDataStatus(); method public boolean isConnected(); + method @FlaggedApi("android.hardware.usb.flags.enable_is_pd_compliant_api") public boolean isPdCompliant(); method public boolean isPowerTransferLimited(); method public boolean isRoleCombinationSupported(int, int); method public void writeToParcel(android.os.Parcel, int); @@ -11064,6 +11069,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"; } @@ -11454,6 +11460,29 @@ package android.printservice.recommendation { package android.provider { + public static class BlockedNumberContract.BlockedNumbers { + method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static void endBlockSuppression(@NonNull android.content.Context); + method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @NonNull @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static android.provider.BlockedNumberContract.BlockedNumbers.BlockSuppressionStatus getBlockSuppressionStatus(@NonNull android.content.Context); + method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static boolean getBlockedNumberSetting(@NonNull android.content.Context, @NonNull String); + method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static void notifyEmergencyContact(@NonNull android.content.Context); + method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static void setBlockedNumberSetting(@NonNull android.content.Context, @NonNull String, boolean); + method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static boolean shouldShowEmergencyCallNotification(@NonNull android.content.Context); + method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static int shouldSystemBlockNumber(@NonNull android.content.Context, @NonNull String, int, boolean); + field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ACTION_BLOCK_SUPPRESSION_STATE_CHANGED = "android.provider.action.BLOCK_SUPPRESSION_STATE_CHANGED"; + field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ENHANCED_SETTING_KEY_BLOCK_PAYPHONE = "block_payphone_calls_setting"; + field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ENHANCED_SETTING_KEY_BLOCK_PRIVATE = "block_private_number_calls_setting"; + field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE = "block_unavailable_calls_setting"; + field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ENHANCED_SETTING_KEY_BLOCK_UNKNOWN = "block_unknown_calls_setting"; + field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED = "block_numbers_not_in_contacts_setting"; + field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION = "show_emergency_call_notification"; + } + + @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final class BlockedNumberContract.BlockedNumbers.BlockSuppressionStatus { + ctor public BlockedNumberContract.BlockedNumbers.BlockSuppressionStatus(boolean, long); + method public boolean getIsSuppressed(); + method public long getUntilTimestampMillis(); + } + public class CallLog { method @RequiresPermission(allOf={android.Manifest.permission.WRITE_CALL_LOG, android.Manifest.permission.INTERACT_ACROSS_USERS}) public static void storeCallComposerPicture(@NonNull android.content.Context, @NonNull java.io.InputStream, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.Uri,android.provider.CallLog.CallComposerLoggingException>); } @@ -11467,7 +11496,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); @@ -12057,6 +12086,7 @@ package android.service.appprediction { method @MainThread public void onDestroyPredictionSession(@NonNull android.app.prediction.AppPredictionSessionId); method @MainThread public abstract void onLaunchLocationShown(@NonNull android.app.prediction.AppPredictionSessionId, @NonNull String, @NonNull java.util.List<android.app.prediction.AppTargetId>); method @MainThread public abstract void onRequestPredictionUpdate(@NonNull android.app.prediction.AppPredictionSessionId); + method @FlaggedApi("android.service.appprediction.flags.service_features_api") @MainThread public void onRequestServiceFeatures(@NonNull android.app.prediction.AppPredictionSessionId, @NonNull java.util.function.Consumer<android.os.Bundle>); method @MainThread public abstract void onSortAppTargets(@NonNull android.app.prediction.AppPredictionSessionId, @NonNull java.util.List<android.app.prediction.AppTarget>, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<java.util.List<android.app.prediction.AppTarget>>); method @MainThread public void onStartPredictionUpdates(); method @MainThread public void onStopPredictionUpdates(); @@ -16114,6 +16144,7 @@ package android.telephony.ims { field public static final int DIALSTRING_USSD = 2; // 0x2 field public static final String EXTRA_ADDITIONAL_CALL_INFO = "AdditionalCallInfo"; field public static final String EXTRA_ADDITIONAL_SIP_INVITE_FIELDS = "android.telephony.ims.extra.ADDITIONAL_SIP_INVITE_FIELDS"; + field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final String EXTRA_ASSERTED_DISPLAY_NAME = "android.telephony.ims.extra.ASSERTED_DISPLAY_NAME"; field public static final String EXTRA_CALL_DISCONNECT_CAUSE = "android.telephony.ims.extra.CALL_DISCONNECT_CAUSE"; field public static final String EXTRA_CALL_NETWORK_TYPE = "android.telephony.ims.extra.CALL_NETWORK_TYPE"; field @Deprecated public static final String EXTRA_CALL_RAT_TYPE = "CallRadioTech"; diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 19b265da7803..dd79c4a40cba 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -2089,7 +2089,7 @@ package android.media.metrics { package android.media.projection { public final class MediaProjectionManager { - method @NonNull public android.content.Intent createScreenCaptureIntent(@Nullable android.app.ActivityOptions.LaunchCookie); + method @NonNull public android.content.Intent createScreenCaptureIntent(@NonNull android.app.ActivityOptions.LaunchCookie); } } @@ -2462,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/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 2c00c99cec91..b25d5ebf61a0 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -146,6 +146,8 @@ import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; import android.os.Process; +import android.os.ProfilingFrameworkInitializer; +import android.os.ProfilingServiceManager; import android.os.RemoteCallback; import android.os.RemoteException; import android.os.ServiceManager; @@ -1232,6 +1234,15 @@ public final class ActivityThread extends ClientTransactionHandler } @Override + public final void scheduleTimeoutServiceForType(IBinder token, int startId, int fgsType) { + if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { + Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, + "scheduleTimeoutServiceForType. token=" + token); + } + sendMessage(H.TIMEOUT_SERVICE_FOR_TYPE, token, startId, fgsType); + } + + @Override public final void bindApplication( String processName, ApplicationInfo appInfo, @@ -2288,6 +2299,8 @@ public final class ActivityThread extends ClientTransactionHandler public static final int INSTRUMENT_WITHOUT_RESTART = 170; public static final int FINISH_INSTRUMENTATION_WITHOUT_RESTART = 171; + public static final int TIMEOUT_SERVICE_FOR_TYPE = 172; + String codeToString(int code) { if (DEBUG_MESSAGES) { switch (code) { @@ -2341,6 +2354,7 @@ public final class ActivityThread extends ClientTransactionHandler case DUMP_RESOURCES: return "DUMP_RESOURCES"; case TIMEOUT_SERVICE: return "TIMEOUT_SERVICE"; case PING: return "PING"; + case TIMEOUT_SERVICE_FOR_TYPE: return "TIMEOUT_SERVICE_FOR_TYPE"; } } return Integer.toString(code); @@ -2427,6 +2441,14 @@ public final class ActivityThread extends ClientTransactionHandler case PING: ((RemoteCallback) msg.obj).sendResult(null); break; + case TIMEOUT_SERVICE_FOR_TYPE: + if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { + Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, + "serviceTimeoutForType: " + msg.obj); + } + handleTimeoutServiceForType((IBinder) msg.obj, msg.arg1, msg.arg2); + Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); + break; case CONFIGURATION_CHANGED: mConfigurationController.handleConfigurationChanged((Configuration) msg.obj); break; @@ -5136,6 +5158,26 @@ public final class ActivityThread extends ClientTransactionHandler Slog.wtf(TAG, "handleTimeoutService: token=" + token + " not found."); } } + + private void handleTimeoutServiceForType(IBinder token, int startId, int fgsType) { + Service s = mServices.get(token); + if (s != null) { + try { + if (localLOGV) Slog.v(TAG, "Timeout service " + s); + + s.callOnTimeLimitExceeded(startId, fgsType); + } catch (Exception e) { + if (!mInstrumentation.onException(s, e)) { + throw new RuntimeException( + "Unable to call onTimeLimitExceeded on service " + s + ": " + e, e); + } + Slog.i(TAG, "handleTimeoutServiceForType: exception for " + token, e); + } + } else { + Slog.wtf(TAG, "handleTimeoutServiceForType: token=" + token + " not found."); + } + } + /** * Resume the activity. * @param r Target activity record. @@ -8548,6 +8590,9 @@ public final class ActivityThread extends ClientTransactionHandler NfcFrameworkInitializer.setNfcServiceManager(new NfcServiceManager()); DeviceConfigInitializer.setDeviceConfigServiceManager(new DeviceConfigServiceManager()); SeFrameworkInitializer.setSeServiceManager(new SeServiceManager()); + if (android.server.Flags.telemetryApisService()) { + ProfilingFrameworkInitializer.setProfilingServiceManager(new ProfilingServiceManager()); + } } private void purgePendingResources() { diff --git a/core/java/android/app/ComponentCaller.java b/core/java/android/app/ComponentCaller.java index a440dbc48db6..44e8a0a3a20c 100644 --- a/core/java/android/app/ComponentCaller.java +++ b/core/java/android/app/ComponentCaller.java @@ -42,6 +42,9 @@ public final class ComponentCaller { private final IBinder mActivityToken; private final IBinder mCallerToken; + /** + * @hide + */ public ComponentCaller(@NonNull IBinder activityToken, @Nullable IBinder callerToken) { mActivityToken = activityToken; mCallerToken = callerToken; diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index ceeaf5dea7d3..cc0aafde1f5c 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -942,6 +942,8 @@ interface IActivityManager { /** Returns if the service is a short-service is still "alive" and past the timeout. */ boolean shouldServiceTimeOut(in ComponentName className, in IBinder token); + /** Returns if the service has a time-limit restricted type and is past the time limit. */ + boolean hasServiceTimeLimitExceeded(in ComponentName className, in IBinder token); void registerUidFrozenStateChangedCallback(in IUidFrozenStateChangedCallback callback); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)") diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl index 59e0e9962fac..a04620cafd75 100644 --- a/core/java/android/app/IApplicationThread.aidl +++ b/core/java/android/app/IApplicationThread.aidl @@ -178,5 +178,6 @@ oneway interface IApplicationThread { in TranslationSpec targetSpec, in List<AutofillId> viewIds, in UiTranslationSpec uiTranslationSpec); void scheduleTimeoutService(IBinder token, int startId); + void scheduleTimeoutServiceForType(IBinder token, int startId, int fgsType); void schedulePing(in RemoteCallback pong); } diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java index a1554572c7df..d4702998ce3e 100644 --- a/core/java/android/app/Service.java +++ b/core/java/android/app/Service.java @@ -20,6 +20,7 @@ import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST; import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; import static android.text.TextUtils.formatSimple; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -1161,4 +1162,37 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac */ public void onTimeout(int startId) { } + + /** @hide */ + public final void callOnTimeLimitExceeded(int startId, int fgsType) { + // Note, because all the service callbacks (and other similar callbacks, e.g. activity + // callbacks) are delivered using the main handler, it's possible the service is already + // stopped when before this method is called, so we do a double check here. + if (mToken == null) { + Log.w(TAG, "Service already destroyed, skipping onTimeLimitExceeded()"); + return; + } + try { + if (!mActivityManager.hasServiceTimeLimitExceeded( + new ComponentName(this, mClassName), mToken)) { + Log.w(TAG, "Service no longer relevant, skipping onTimeLimitExceeded()"); + return; + } + } catch (RemoteException ex) { + } + if (Flags.introduceNewServiceOntimeoutCallback()) { + onTimeout(startId, fgsType); + } + } + + /** + * Callback called when a particular foreground service type has timed out. + * + * @param startId the startId passed to {@link #onStartCommand(Intent, int, int)} when + * the service started. + * @param fgsType the foreground service type which caused the timeout. + */ + @FlaggedApi(Flags.FLAG_INTRODUCE_NEW_SERVICE_ONTIMEOUT_CALLBACK) + public void onTimeout(int startId, int fgsType) { + } } diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index ba9c8952892b..9d35dc3627c3 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -186,6 +186,7 @@ import android.os.IncidentManager; import android.os.PerformanceHintManager; import android.os.PermissionEnforcer; import android.os.PowerManager; +import android.os.ProfilingFrameworkInitializer; import android.os.RecoverySystem; import android.os.SecurityStateManager; import android.os.ServiceManager; @@ -1662,6 +1663,9 @@ public final class SystemServiceRegistry { if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()) { EnhancedConfirmationFrameworkInitializer.registerServiceWrappers(); } + if (android.server.Flags.telemetryApisService()) { + ProfilingFrameworkInitializer.registerServiceWrappers(); + } } finally { // If any of the above code throws, we're in a pretty bad shape and the process // will likely crash, but we'll reset it just in case there's an exception handler... diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig index c0b299b77806..ff23f09f78a5 100644 --- a/core/java/android/app/activity_manager.aconfig +++ b/core/java/android/app/activity_manager.aconfig @@ -27,3 +27,10 @@ flag { description: "API to add OnUidImportanceListener with targetted UIDs" bug: "286258140" } + +flag { + namespace: "backstage_power" + name: "introduce_new_service_ontimeout_callback" + description: "Add a new callback in Service to indicate a FGS has reached its timeout." + bug: "317799821" +} 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/app/prediction/AppPredictor.java b/core/java/android/app/prediction/AppPredictor.java index d628b7f92c6c..0c1a28a9eee1 100644 --- a/core/java/android/app/prediction/AppPredictor.java +++ b/core/java/android/app/prediction/AppPredictor.java @@ -16,6 +16,7 @@ package android.app.prediction; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; @@ -24,9 +25,12 @@ import android.app.prediction.IPredictionCallback.Stub; import android.content.Context; import android.content.pm.ParceledListSlice; import android.os.Binder; +import android.os.Bundle; import android.os.IBinder; +import android.os.IRemoteCallback; import android.os.RemoteException; import android.os.ServiceManager; +import android.service.appprediction.flags.Flags; import android.util.ArrayMap; import android.util.Log; @@ -263,6 +267,34 @@ public final class AppPredictor { } /** + * Requests a Bundle which includes service features info or {@code null} if the service is not + * available. + * + * @param callbackExecutor The callback executor to use when calling the callback. It cannot be + * null. + * @param callback The callback to return the Bundle which includes service features info. It + * cannot be null. + * + * @throws IllegalStateException If this AppPredictor has already been destroyed. + * @throws RuntimeException If there is a failure communicating with the remote service. + */ + @FlaggedApi(Flags.FLAG_SERVICE_FEATURES_API) + public void requestServiceFeatures(@NonNull Executor callbackExecutor, + @NonNull Consumer<Bundle> callback) { + if (mIsClosed.get()) { + throw new IllegalStateException("This client has already been destroyed."); + } + + try { + mPredictionManager.requestServiceFeatures(mSessionId, + new RemoteCallbackWrapper(callbackExecutor, callback)); + } catch (RemoteException e) { + Log.e(TAG, "Failed to request service feature info", e); + e.rethrowAsRuntimeException(); + } + } + + /** * Destroys the client and unregisters the callback. Any method on this class after this call * with throw {@link IllegalStateException}. */ @@ -347,6 +379,28 @@ public final class AppPredictor { } } + static class RemoteCallbackWrapper extends IRemoteCallback.Stub { + + private final Consumer<Bundle> mCallback; + private final Executor mExecutor; + + RemoteCallbackWrapper(@NonNull Executor callbackExecutor, + @NonNull Consumer<Bundle> callback) { + mExecutor = callbackExecutor; + mCallback = callback; + } + + @Override + public void sendResult(Bundle result) { + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mCallback.accept(result)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + private static class Token { static final IBinder sBinder = new Binder(TAG); } diff --git a/core/java/android/app/prediction/IPredictionManager.aidl b/core/java/android/app/prediction/IPredictionManager.aidl index 863fc6f952dd..94b4f5b2f30e 100644 --- a/core/java/android/app/prediction/IPredictionManager.aidl +++ b/core/java/android/app/prediction/IPredictionManager.aidl @@ -22,6 +22,7 @@ import android.app.prediction.AppPredictionContext; import android.app.prediction.AppPredictionSessionId; import android.app.prediction.IPredictionCallback; import android.content.pm.ParceledListSlice; +import android.os.IRemoteCallback; /** * @hide @@ -48,4 +49,6 @@ interface IPredictionManager { void requestPredictionUpdate(in AppPredictionSessionId sessionId); void onDestroyPredictionSession(in AppPredictionSessionId sessionId); + + void requestServiceFeatures(in AppPredictionSessionId sessionId, in IRemoteCallback callback); } diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java index 67759f4aa76d..eb357fe09a31 100644 --- a/core/java/android/content/ClipData.java +++ b/core/java/android/content/ClipData.java @@ -21,7 +21,13 @@ import static android.content.ContentResolver.SCHEME_ANDROID_RESOURCE; import static android.content.ContentResolver.SCHEME_CONTENT; import static android.content.ContentResolver.SCHEME_FILE; +import static com.android.window.flags.Flags.FLAG_DELEGATE_UNHANDLED_DRAGS; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.app.PendingIntent; import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.ActivityInfo; import android.content.res.AssetFileDescriptor; @@ -207,6 +213,7 @@ public class ClipData implements Parcelable { final CharSequence mText; final String mHtmlText; final Intent mIntent; + final PendingIntent mPendingIntent; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) Uri mUri; private TextLinks mTextLinks; @@ -214,12 +221,91 @@ public class ClipData implements Parcelable { // if the data is obtained from {@link #copyForTransferWithActivityInfo} private ActivityInfo mActivityInfo; + /** + * A builder for a ClipData Item. + */ + @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS) + @SuppressLint("PackageLayering") + public static final class Builder { + private CharSequence mText; + private String mHtmlText; + private Intent mIntent; + private PendingIntent mPendingIntent; + private Uri mUri; + + /** + * Sets the text for the item to be constructed. + */ + @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS) + @NonNull + public Builder setText(@Nullable CharSequence text) { + mText = text; + return this; + } + + /** + * Sets the HTML text for the item to be constructed. + */ + @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS) + @NonNull + public Builder setHtmlText(@Nullable String htmlText) { + mHtmlText = htmlText; + return this; + } + + /** + * Sets the Intent for the item to be constructed. + */ + @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS) + @NonNull + public Builder setIntent(@Nullable Intent intent) { + mIntent = intent; + return this; + } + + /** + * Sets the PendingIntent for the item to be constructed. To prevent receiving apps from + * improperly manipulating the intent to launch another activity as this caller, the + * provided PendingIntent must be immutable (see {@link PendingIntent#FLAG_IMMUTABLE}). + * The system will clean up the PendingIntent when it is no longer used. + */ + @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS) + @NonNull + public Builder setPendingIntent(@Nullable PendingIntent pendingIntent) { + if (pendingIntent != null && !pendingIntent.isImmutable()) { + throw new IllegalArgumentException("Expected pending intent to be immutable"); + } + mPendingIntent = pendingIntent; + return this; + } + + /** + * Sets the URI for the item to be constructed. + */ + @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS) + @NonNull + public Builder setUri(@Nullable Uri uri) { + mUri = uri; + return this; + } + + /** + * Constructs a new Item with the properties set on this builder. + */ + @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS) + @NonNull + public Item build() { + return new Item(mText, mHtmlText, mIntent, mPendingIntent, mUri); + } + } + /** @hide */ public Item(Item other) { mText = other.mText; mHtmlText = other.mHtmlText; mIntent = other.mIntent; + mPendingIntent = other.mPendingIntent; mUri = other.mUri; mActivityInfo = other.mActivityInfo; mTextLinks = other.mTextLinks; @@ -229,10 +315,7 @@ public class ClipData implements Parcelable { * Create an Item consisting of a single block of (possibly styled) text. */ public Item(CharSequence text) { - mText = text; - mHtmlText = null; - mIntent = null; - mUri = null; + this(text, null, null, null, null); } /** @@ -245,30 +328,21 @@ public class ClipData implements Parcelable { * </p> */ public Item(CharSequence text, String htmlText) { - mText = text; - mHtmlText = htmlText; - mIntent = null; - mUri = null; + this(text, htmlText, null, null, null); } /** * Create an Item consisting of an arbitrary Intent. */ public Item(Intent intent) { - mText = null; - mHtmlText = null; - mIntent = intent; - mUri = null; + this(null, null, intent, null, null); } /** * Create an Item consisting of an arbitrary URI. */ public Item(Uri uri) { - mText = null; - mHtmlText = null; - mIntent = null; - mUri = uri; + this(null, null, null, null, uri); } /** @@ -276,10 +350,7 @@ public class ClipData implements Parcelable { * text, Intent, and/or URI. */ public Item(CharSequence text, Intent intent, Uri uri) { - mText = text; - mHtmlText = null; - mIntent = intent; - mUri = uri; + this(text, null, intent, null, uri); } /** @@ -289,6 +360,14 @@ public class ClipData implements Parcelable { * will not be done from HTML formatted text into plain text. */ public Item(CharSequence text, String htmlText, Intent intent, Uri uri) { + this(text, htmlText, intent, null, uri); + } + + /** + * Builder ctor. + */ + private Item(CharSequence text, String htmlText, Intent intent, PendingIntent pendingIntent, + Uri uri) { if (htmlText != null && text == null) { throw new IllegalArgumentException( "Plain text must be supplied if HTML text is supplied"); @@ -296,6 +375,7 @@ public class ClipData implements Parcelable { mText = text; mHtmlText = htmlText; mIntent = intent; + mPendingIntent = pendingIntent; mUri = uri; } @@ -321,6 +401,15 @@ public class ClipData implements Parcelable { } /** + * Returns the pending intent in this Item. + */ + @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS) + @Nullable + public PendingIntent getPendingIntent() { + return mPendingIntent; + } + + /** * Retrieve the raw URI contained in this Item. */ public Uri getUri() { @@ -777,7 +866,7 @@ public class ClipData implements Parcelable { throw new NullPointerException("item is null"); } mIcon = null; - mItems = new ArrayList<Item>(); + mItems = new ArrayList<>(); mItems.add(item); mClipDescription.setIsStyledText(isStyledText()); } @@ -794,7 +883,7 @@ public class ClipData implements Parcelable { throw new NullPointerException("item is null"); } mIcon = null; - mItems = new ArrayList<Item>(); + mItems = new ArrayList<>(); mItems.add(item); mClipDescription.setIsStyledText(isStyledText()); } @@ -826,7 +915,7 @@ public class ClipData implements Parcelable { public ClipData(ClipData other) { mClipDescription = other.mClipDescription; mIcon = other.mIcon; - mItems = new ArrayList<Item>(other.mItems); + mItems = new ArrayList<>(other.mItems); } /** @@ -1042,6 +1131,35 @@ public class ClipData implements Parcelable { } /** + * Checks if this clip data has a pending intent that is an activity type. + * @hide + */ + public boolean hasActivityPendingIntents() { + final int size = mItems.size(); + for (int i = 0; i < size; i++) { + final Item item = mItems.get(i); + if (item.mPendingIntent != null && item.mPendingIntent.isActivity()) { + return true; + } + } + return false; + } + + /** + * Cleans up all pending intents in the ClipData. + * @hide + */ + public void cleanUpPendingIntents() { + final int size = mItems.size(); + for (int i = 0; i < size; i++) { + final Item item = mItems.get(i); + if (item.mPendingIntent != null) { + item.mPendingIntent.cancel(); + } + } + } + + /** * Prepare this {@link ClipData} to leave an app process. * * @hide @@ -1243,6 +1361,7 @@ public class ClipData implements Parcelable { TextUtils.writeToParcel(item.mText, dest, flags); dest.writeString8(item.mHtmlText); dest.writeTypedObject(item.mIntent, flags); + dest.writeTypedObject(item.mPendingIntent, flags); dest.writeTypedObject(item.mUri, flags); dest.writeTypedObject(mParcelItemActivityInfos ? item.mActivityInfo : null, flags); dest.writeTypedObject(item.mTextLinks, flags); @@ -1262,10 +1381,11 @@ public class ClipData implements Parcelable { CharSequence text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); String htmlText = in.readString8(); Intent intent = in.readTypedObject(Intent.CREATOR); + PendingIntent pendingIntent = in.readTypedObject(PendingIntent.CREATOR); Uri uri = in.readTypedObject(Uri.CREATOR); ActivityInfo info = in.readTypedObject(ActivityInfo.CREATOR); TextLinks textLinks = in.readTypedObject(TextLinks.CREATOR); - Item item = new Item(text, htmlText, intent, uri); + Item item = new Item(text, htmlText, intent, pendingIntent, uri); item.setActivityInfo(info); item.setTextLinks(textLinks); mItems.add(item); @@ -1273,7 +1393,7 @@ public class ClipData implements Parcelable { } public static final @android.annotation.NonNull Parcelable.Creator<ClipData> CREATOR = - new Parcelable.Creator<ClipData>() { + new Parcelable.Creator<>() { @Override public ClipData createFromParcel(Parcel source) { diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index b8d754348211..c89c73523a9c 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -6553,6 +6553,15 @@ public abstract class Context { public static final String CONTACT_KEYS_SERVICE = "contact_keys"; /** + * Use with {@link #getSystemService(String)} to retrieve an + * {@link android.os.ProfilingManager}. + * + * @see #getSystemService(String) + */ + @FlaggedApi(android.os.Flags.FLAG_TELEMETRY_APIS_FRAMEWORK_INITIALIZATION) + public static final String PROFILING_SERVICE = "profiling"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 08871d4644c0..e8031a374310 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -19,6 +19,7 @@ package android.content; import static android.app.sdksandbox.SdkSandboxManager.ACTION_START_SANDBOXED_ACTIVITY; import static android.content.ContentProvider.maybeAddUserId; import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE; +import static android.service.chooser.Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA; import android.Manifest; import android.accessibilityservice.AccessibilityService; @@ -55,6 +56,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.BundleMerger; +import android.os.CancellationSignal; import android.os.IBinder; import android.os.IncidentManager; import android.os.Parcel; @@ -72,7 +74,9 @@ import android.provider.DocumentsContract; import android.provider.DocumentsProvider; import android.provider.MediaStore; import android.provider.OpenableColumns; +import android.service.chooser.AdditionalContentContract; import android.service.chooser.ChooserAction; +import android.service.chooser.ChooserResult; import android.telecom.PhoneAccount; import android.telecom.TelecomManager; import android.text.TextUtils; @@ -1059,7 +1063,7 @@ public class Intent implements Parcelable, Cloneable { } if (sender != null) { - intent.putExtra(EXTRA_CHOSEN_COMPONENT_INTENT_SENDER, sender); + intent.putExtra(EXTRA_CHOOSER_RESULT_INTENT_SENDER, sender); } // Migrate any clip data and flags from target. @@ -6064,6 +6068,62 @@ public class Intent implements Parcelable, Cloneable { public static final int CHOOSER_CONTENT_TYPE_ALBUM = 1; /** + * Optional argument used to provide a {@link ContentProvider} {@link Uri} to an + * {@link #ACTION_CHOOSER} Intent which allows additional toggleable items to be included + * in the sharing UI. + * <p> + * For example, this could be used to show photos being shared in the context of the user's + * entire photo roll, with the option to change the set of photos being shared. + * <p> + * When this is provided in an {@link #ACTION_CHOOSER} Intent with an {@link #ACTION_SEND} or + * {@link #ACTION_SEND_MULTIPLE} target Intent, the sharesheet will query (see + * {@link ContentProvider#query(Uri, String[], Bundle, CancellationSignal)}) this URI to + * retrieve a set of additional items available for selection. The set of items returned by the + * content provider is expected to contain all the items from the {@link #EXTRA_STREAM} + * argument, in their relative order, which will be marked as selected. The URI's authority + * must be different from any shared items URI provided in {@link #EXTRA_STREAM} or returned by + * the provider. + * + * <p>The {@link Bundle} argument of the + * {@link ContentProvider#query(Uri, String[], Bundle, CancellationSignal)} + * method will contains the original intent Chooser has been launched with under the + * {@link #EXTRA_INTENT} key as a context for the current sharing session. The returned + * {@link android.database.Cursor} should contain + * {@link android.service.chooser.AdditionalContentContract.Columns#URI} column for the item URI + * and, optionally, {@link AdditionalContentContract.CursorExtraKeys#POSITION} extra that + * specifies the cursor starting position; the item at this position is expected to match the + * item specified by {@link #EXTRA_CHOOSER_FOCUSED_ITEM_POSITION}.</p> + * + * <p>When the user makes a selection change, + * {@link ContentProvider#call(String, String, Bundle)} method will be invoked with the "method" + * argument set to + * {@link android.service.chooser.AdditionalContentContract.MethodNames#ON_SELECTION_CHANGED}, + * the "arg" argument set to this argument's value, and the "extras" {@link Bundle} argument + * containing {@link #EXTRA_INTENT} key containing the original intent Chooser has been launched + * with but with the modified target intent --Chooser will modify the target intent according to + * the selection changes made by the user. + * Applications may implement this method to change any of the following Chooser arguments by + * returning new values in the result bundle: + * {@link #EXTRA_CHOOSER_TARGETS}, {@link #EXTRA_ALTERNATE_INTENTS}, + * {@link #EXTRA_CHOOSER_CUSTOM_ACTIONS}, + * {@link #EXTRA_CHOOSER_MODIFY_SHARE_ACTION}, + * {@link #EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER}.</p> + */ + @FlaggedApi(android.service.chooser.Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING) + public static final String EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI = + "android.intent.extra.CHOOSER_ADDITIONAL_CONTENT_URI"; + + /** + * Optional argument to be used with {@link #EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI}, used in + * combination with {@link #EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI}. + * An integer, zero-based index into {@link #EXTRA_STREAM} argument indicating the item that + * should be focused by the Chooser in preview. + */ + @FlaggedApi(android.service.chooser.Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING) + public static final String EXTRA_CHOOSER_FOCUSED_ITEM_POSITION = + "android.intent.extra.CHOOSER_FOCUSED_ITEM_POSITION"; + + /** * An {@code ArrayList} of {@code String} annotations describing content for * {@link #ACTION_CHOOSER}. * @@ -6156,6 +6216,17 @@ public class Intent implements Parcelable, Cloneable { public static final String EXTRA_INITIAL_INTENTS = "android.intent.extra.INITIAL_INTENTS"; /** + * A CharSequence of additional text describing the content being shared. This text will be + * displayed to the user as a part of the sharesheet when included in an + * {@link #ACTION_CHOOSER} {@link Intent}. + * + * <p>e.g. When sharing a photo, metadata could inform the user that location data is included + * in the photo they are sharing.</p> + */ + @FlaggedApi(FLAG_ENABLE_SHARESHEET_METADATA_EXTRA) + public static final String EXTRA_METADATA_TEXT = "android.intent.extra.METADATA_TEXT"; + + /** * A {@link IntentSender} to start after instant app installation success. * @hide */ @@ -6296,6 +6367,25 @@ public class Intent implements Parcelable, Cloneable { "android.intent.extra.CHOSEN_COMPONENT_INTENT_SENDER"; /** + * An {@link IntentSender} that will be notified when a user successfully chooses a target + * component or initiates an action such as copy or edit within an {@link #ACTION_CHOOSER} + * activity. The IntentSender will have the extra {@link #EXTRA_CHOOSER_RESULT} describing + * the result. + */ + @FlaggedApi(android.service.chooser.Flags.FLAG_ENABLE_CHOOSER_RESULT) + public static final String EXTRA_CHOOSER_RESULT_INTENT_SENDER = + "android.intent.extra.CHOOSER_RESULT_INTENT_SENDER"; + + /** + * A {@link ChooserResult} which describes how the sharing session completed. + * <p> + * An instance is supplied to the optional IntentSender provided to + * {@link #createChooser(Intent, CharSequence, IntentSender)} when the session completes. + */ + @FlaggedApi(android.service.chooser.Flags.FLAG_ENABLE_CHOOSER_RESULT) + public static final String EXTRA_CHOOSER_RESULT = "android.intent.extra.CHOOSER_RESULT"; + + /** * The {@link ComponentName} chosen by the user to complete an action. * * @see #EXTRA_CHOSEN_COMPONENT_INTENT_SENDER diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 407ffbb7288f..49c8a7cad57a 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -16,6 +16,8 @@ package android.content.pm; +import static android.media.audio.Flags.FLAG_FEATURE_SPATIAL_AUDIO_HEADTRACKING_LOW_LATENCY; + import static com.android.internal.pm.pkg.parsing.ParsingPackageUtils.PARSE_COLLECT_CERTIFICATES; import android.Manifest; @@ -3065,6 +3067,17 @@ public abstract class PackageManager { public static final String FEATURE_AUDIO_PRO = "android.hardware.audio.pro"; /** + * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature} + * which indicates whether head tracking for spatial audio operates with low-latency, + * as defined by the CDD criteria for the feature. + * + */ + @SdkConstant(SdkConstantType.FEATURE) + @FlaggedApi(FLAG_FEATURE_SPATIAL_AUDIO_HEADTRACKING_LOW_LATENCY) + public static final String FEATURE_AUDIO_SPATIAL_HEADTRACKING_LOW_LATENCY = + "android.hardware.audio.spatial.headtracking.low_latency"; + + /** * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature}: The device is capable of communicating with * other devices via Bluetooth. diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index 2b378b1f09d0..5b0cee75e591 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -142,6 +142,28 @@ public class ServiceInfo extends ComponentInfo * the {@link android.R.attr#foregroundServiceType} attribute. * Data(photo, file, account) upload/download, backup/restore, import/export, fetch, * transfer over network between device and cloud. + * + * <p>This type has time limit of 6 hours starting from Android version + * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}. + * A foreground service of this type must be stopped within the timeout by + * {@link android.app.Service#stopSelf()}, + * {@link android.content.Context#stopService(android.content.Intent)} or their overloads). + * {@link android.app.Service#stopForeground(int)} will also work, which will demote the + * service to a "background" service, which will soon be stopped by the system. + * + * <p>If the service isn't stopped within the timeout, + * {@link android.app.Service#onTimeout(int, int)} will be called. + * + * <p>Also note, even though + * {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC} can be used on + * Android versions prior to {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, since + * {@link android.app.Service#onTimeout(int, int)} did not exist on such versions, it will + * never be called. + * + * Because of this, developers must make sure to stop the foreground service even if + * {@link android.app.Service#onTimeout(int, int)} is not called on such versions. + * + * @see android.app.Service#onTimeout(int, int) */ @RequiresPermission( value = Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, @@ -483,6 +505,27 @@ public class ServiceInfo extends ComponentInfo * Constant corresponding to {@code mediaProcessing} in * the {@link android.R.attr#foregroundServiceType} attribute. * Media processing use cases such as video or photo editing and processing. + * + * This type has time limit of 6 hours. + * A foreground service of this type must be stopped within the timeout by + * {@link android.app.Service#stopSelf()}, + * {@link android.content.Context#stopService(android.content.Intent)} or their overloads). + * {@link android.app.Service#stopForeground(int)} will also work, which will demote the + * service to a "background" service, which will soon be stopped by the system. + * + * <p>If the service isn't stopped within the timeout, + * {@link android.app.Service#onTimeout(int, int)} will be called. + * + * <p>Also note, even though + * {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING} was added in + * Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, it can be also used + * on prior android versions (just like other new foreground service types can be used). + * However, because {@link android.app.Service#onTimeout(int, int)} did not exist on prior + * versions, it will never be called on such versions. + * Because of this, developers must make sure to stop the foreground service even if + * {@link android.app.Service#onTimeout(int, int)} is not called on such versions. + * + * @see android.app.Service#onTimeout(int, int) */ @RequiresPermission( value = Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING 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/services/core/java/com/android/server/devicestate/DeviceState.java b/core/java/android/hardware/devicestate/DeviceState.java index 2ba59b0d659a..5a349050a28f 100644 --- a/services/core/java/com/android/server/devicestate/DeviceState.java +++ b/core/java/android/hardware/devicestate/DeviceState.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server.devicestate; +package android.hardware.devicestate; import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE; import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE; @@ -22,7 +22,6 @@ import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STA import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; -import android.hardware.devicestate.DeviceStateManager; import com.android.internal.util.Preconditions; @@ -38,9 +37,9 @@ import java.util.Objects; * state of the system. This is useful for variable-state devices, like foldable or rollable * devices, that can be configured by users into differing hardware states, which each may have a * different expected use case. + * @hide * - * @see DeviceStateProvider - * @see DeviceStateManagerService + * @see DeviceStateManager */ public final class DeviceState { /** diff --git a/core/java/android/hardware/usb/UsbPortStatus.java b/core/java/android/hardware/usb/UsbPortStatus.java index 4a5c4c8acd25..8616b6b15de6 100644 --- a/core/java/android/hardware/usb/UsbPortStatus.java +++ b/core/java/android/hardware/usb/UsbPortStatus.java @@ -16,13 +16,11 @@ package android.hardware.usb; -import android.Manifest; import android.annotation.CheckResult; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.hardware.usb.flags.Flags; import android.os.Parcel; @@ -30,7 +28,6 @@ import android.os.Parcelable; import com.android.internal.annotations.Immutable; -import java.lang.StringBuilder; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -580,6 +577,21 @@ public final class UsbPortStatus implements Parcelable { } /** + * This function checks if the port is USB Power Delivery (PD) compliant - + * https://www.usb.org/usb-charger-pd. All of the power and data roles must be supported for a + * port to be PD compliant. + * + * @return true if the port is PD compliant. + */ + @FlaggedApi(Flags.FLAG_ENABLE_IS_PD_COMPLIANT_API) + public boolean isPdCompliant() { + return isRoleCombinationSupported(POWER_ROLE_SINK, DATA_ROLE_DEVICE) + && isRoleCombinationSupported(POWER_ROLE_SINK, DATA_ROLE_HOST) + && isRoleCombinationSupported(POWER_ROLE_SOURCE, DATA_ROLE_DEVICE) + && isRoleCombinationSupported(POWER_ROLE_SOURCE, DATA_ROLE_HOST); + } + + /** * Get the supported role combinations. */ public int getSupportedRoleCombinations() { diff --git a/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig b/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig new file mode 100644 index 000000000000..cc56a311e9a4 --- /dev/null +++ b/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig @@ -0,0 +1,8 @@ +package: "android.hardware.usb.flags" + +flag { + name: "enable_is_pd_compliant_api" + namespace: "usb" + description: "Feature flag for the api to check if a port is PD compliant" + bug: "323470419" +} diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index a6b2ed09a64d..e09094203ad4 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -1899,7 +1899,7 @@ public abstract class BatteryStats { public short batteryTemperature; // Battery voltage in millivolts (mV). @UnsupportedAppUsage - public char batteryVoltage; + public short batteryVoltage; // The charge of the battery in micro-Ampere-hours. public int batteryChargeUah; @@ -2161,7 +2161,7 @@ public abstract class BatteryStats { batteryPlugType = (byte)((bat>>24)&0xf); int bat2 = src.readInt(); batteryTemperature = (short)(bat2&0xffff); - batteryVoltage = (char)((bat2>>16)&0xffff); + batteryVoltage = (short) ((bat2 >> 16) & 0xffff); batteryChargeUah = src.readInt(); modemRailChargeMah = src.readDouble(); wifiRailChargeMah = src.readDouble(); diff --git a/core/java/android/os/ProfilingServiceManager.java b/core/java/android/os/ProfilingServiceManager.java new file mode 100644 index 000000000000..cc77f5b82c90 --- /dev/null +++ b/core/java/android/os/ProfilingServiceManager.java @@ -0,0 +1,97 @@ +/* + * 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 android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.annotation.SystemApi.Client; + +/** + * Provides a way to register and obtain the system service binder objects managed by the profiling + * service. + * + * <p> Only the profiling mainline module will be able to access an instance of this class. + * @hide + */ +@FlaggedApi(Flags.FLAG_TELEMETRY_APIS_FRAMEWORK_INITIALIZATION) +@SystemApi(client = Client.MODULE_LIBRARIES) +public class ProfilingServiceManager { + + /** @hide */ + public ProfilingServiceManager() {} + + /** + * A class that exposes the methods to register and obtain each system service. + */ + public static final class ServiceRegisterer { + private final String mServiceName; + + /** @hide */ + public ServiceRegisterer(String serviceName) { + mServiceName = serviceName; + } + + /** + * Get the system server binding object for ProfilingService. + * + * <p> This blocks until the service instance is ready. + * or a timeout happens, in which case it returns null. + */ + @Nullable + public IBinder get() { + return ServiceManager.getService(mServiceName); + } + + /** + * Get the system server binding object for a service. + * + * <p>This blocks until the service instance is ready, + * or a timeout happens, in which case it throws {@link ServiceNotFoundException}. + */ + @Nullable + public IBinder getOrThrow() throws ServiceNotFoundException { + try { + return ServiceManager.getServiceOrThrow(mServiceName); + } catch (ServiceManager.ServiceNotFoundException e) { + throw new ServiceNotFoundException(mServiceName); + } + } + } + + /** + * See {@link ServiceRegisterer#getOrThrow()} + */ + public static class ServiceNotFoundException extends ServiceManager.ServiceNotFoundException { + /** + * Constructor + * + * @param name the name of the binder service that cannot be found. + */ + public ServiceNotFoundException(@NonNull String name) { + super(name); + } + } + + /** + * Returns {@link ServiceRegisterer} for the "profiling" service. + */ + @NonNull + public ServiceRegisterer getProfilingServiceRegisterer() { + return new ServiceRegisterer("profiling_service"); + } +} diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 0fbdbc48fd99..de32423eab2e 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"; /** @@ -1424,8 +1427,8 @@ public class UserManager { public static final String DISALLOW_RECORD_AUDIO = "no_record_audio"; /** - * Specifies if a user is not allowed to run in the background and should be stopped during - * user switch. The default value is <code>false</code>. + * Specifies if a user is not allowed to run in the background and should be stopped and locked + * during user switch. The default value is <code>false</code>. * * <p>This restriction can be set by device owners and profile owners. * diff --git a/core/java/android/provider/BlockedNumberContract.java b/core/java/android/provider/BlockedNumberContract.java index 5d00b29eb3c8..4075e9009acd 100644 --- a/core/java/android/provider/BlockedNumberContract.java +++ b/core/java/android/provider/BlockedNumberContract.java @@ -15,12 +15,20 @@ */ package android.provider; +import android.Manifest; +import android.annotation.FlaggedApi; import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; import android.annotation.WorkerThread; import android.content.Context; import android.net.Uri; import android.os.Bundle; import android.telecom.Log; +import android.telecom.TelecomManager; + +import com.android.server.telecom.flags.Flags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -214,6 +222,333 @@ public class BlockedNumberContract { * <p>TYPE: String</p> */ public static final String COLUMN_E164_NUMBER = "e164_number"; + + /** + * A protected broadcast intent action for letting components with + * {@link android.Manifest.permission#READ_BLOCKED_NUMBERS} know that the block suppression + * status as returned by {@link #getBlockSuppressionStatus(Context)} has been updated. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + public static final String ACTION_BLOCK_SUPPRESSION_STATE_CHANGED = + "android.provider.action.BLOCK_SUPPRESSION_STATE_CHANGED"; + + /** + * Preference key of block numbers not in contacts setting. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + public static final String ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED = + "block_numbers_not_in_contacts_setting"; + + /** + * Preference key of block private number calls setting. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + public static final String ENHANCED_SETTING_KEY_BLOCK_PRIVATE = + "block_private_number_calls_setting"; + + /** + * Preference key of block payphone calls setting. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + public static final String ENHANCED_SETTING_KEY_BLOCK_PAYPHONE = + "block_payphone_calls_setting"; + + /** + * Preference key of block unknown calls setting. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + public static final String ENHANCED_SETTING_KEY_BLOCK_UNKNOWN = + "block_unknown_calls_setting"; + + /** + * Preference key for whether should show an emergency call notification. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + public static final String ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION = + "show_emergency_call_notification"; + + /** + * Preference key of block unavailable calls setting. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + public static final String ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE = + "block_unavailable_calls_setting"; + + /** + * Notifies the provider that emergency services were contacted by the user. + * <p> This results in {@link #shouldSystemBlockNumber} returning {@code false} independent + * of the contents of the provider for a duration defined by + * {@link android.telephony.CarrierConfigManager#KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT} + * the provider unless {@link #endBlockSuppression(Context)} is called. + * @hide + */ + @SystemApi + @RequiresPermission(allOf = { + android.Manifest.permission.READ_BLOCKED_NUMBERS, + android.Manifest.permission.WRITE_BLOCKED_NUMBERS + }) + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + public static void notifyEmergencyContact(@NonNull Context context) { + verifyBlockedNumbersPermission(context); + try { + Log.i(LOG_TAG, "notifyEmergencyContact; caller=%s", context.getOpPackageName()); + context.getContentResolver().call( + AUTHORITY_URI, SystemContract.METHOD_NOTIFY_EMERGENCY_CONTACT, null, null); + } catch (NullPointerException | IllegalArgumentException ex) { + // The content resolver can throw an NPE or IAE; we don't want to crash Telecom if + // either of these happen. + Log.w(null, "notifyEmergencyContact: provider not ready."); + } + } + + /** + * Notifies the provider to disable suppressing blocking. If emergency services were not + * contacted recently at all, calling this method is a no-op. + * @hide + */ + @SystemApi + @RequiresPermission(allOf = { + android.Manifest.permission.READ_BLOCKED_NUMBERS, + android.Manifest.permission.WRITE_BLOCKED_NUMBERS + }) + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + public static void endBlockSuppression(@NonNull Context context) { + verifyBlockedNumbersPermission(context); + String caller = context.getOpPackageName(); + Log.i(LOG_TAG, "endBlockSuppression: caller=%s", caller); + context.getContentResolver().call( + AUTHORITY_URI, SystemContract.METHOD_END_BLOCK_SUPPRESSION, null, null); + } + + /** + * Returns {@code true} if {@code phoneNumber} is blocked taking + * {@link #notifyEmergencyContact(Context)} into consideration. If emergency services + * have not been contacted recently and enhanced call blocking not been enabled, this + * method is equivalent to {@link #isBlocked(Context, String)}. + * + * @param context the context of the caller. + * @param phoneNumber the number to check. + * @param numberPresentation the presentation code associated with the call. + * @param isNumberInContacts indicates if the provided number exists as a contact. + * @return result code indicating if the number should be blocked, and if so why. + * Valid values are: {@link #STATUS_NOT_BLOCKED}, {@link #STATUS_BLOCKED_IN_LIST}, + * {@link #STATUS_BLOCKED_NOT_IN_CONTACTS}, {@link #STATUS_BLOCKED_PAYPHONE}, + * {@link #STATUS_BLOCKED_RESTRICTED}, {@link #STATUS_BLOCKED_UNKNOWN_NUMBER}. + * @hide + */ + @SystemApi + @RequiresPermission(allOf = { + android.Manifest.permission.READ_BLOCKED_NUMBERS, + android.Manifest.permission.WRITE_BLOCKED_NUMBERS + }) + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + public static int shouldSystemBlockNumber(@NonNull Context context, + @NonNull String phoneNumber, @TelecomManager.Presentation int numberPresentation, + boolean isNumberInContacts) { + verifyBlockedNumbersPermission(context); + try { + String caller = context.getOpPackageName(); + Bundle extras = new Bundle(); + extras.putInt(BlockedNumberContract.EXTRA_CALL_PRESENTATION, numberPresentation); + extras.putBoolean(BlockedNumberContract.EXTRA_CONTACT_EXIST, isNumberInContacts); + final Bundle res = context.getContentResolver().call(AUTHORITY_URI, + SystemContract.METHOD_SHOULD_SYSTEM_BLOCK_NUMBER, phoneNumber, extras); + int blockResult = res != null ? res.getInt(RES_BLOCK_STATUS, STATUS_NOT_BLOCKED) : + BlockedNumberContract.STATUS_NOT_BLOCKED; + Log.d(LOG_TAG, "shouldSystemBlockNumber: number=%s, caller=%s, result=%s", + Log.piiHandle(phoneNumber), caller, + SystemContract.blockStatusToString(blockResult)); + return blockResult; + } catch (NullPointerException | IllegalArgumentException ex) { + // The content resolver can throw an NPE or IAE; we don't want to crash Telecom if + // either of these happen. + Log.w(null, "shouldSystemBlockNumber: provider not ready."); + return BlockedNumberContract.STATUS_NOT_BLOCKED; + } + } + + /** + * Returns the current status of block suppression. + * @hide + */ + @SystemApi + @RequiresPermission(allOf = { + android.Manifest.permission.READ_BLOCKED_NUMBERS, + android.Manifest.permission.WRITE_BLOCKED_NUMBERS + }) + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + public static @NonNull BlockSuppressionStatus getBlockSuppressionStatus( + @NonNull Context context) { + verifyBlockedNumbersPermission(context); + final Bundle res = context.getContentResolver().call( + AUTHORITY_URI, SystemContract.METHOD_GET_BLOCK_SUPPRESSION_STATUS, null, null); + BlockSuppressionStatus blockSuppressionStatus = new BlockSuppressionStatus( + res.getBoolean(SystemContract.RES_IS_BLOCKING_SUPPRESSED, false), + res.getLong(SystemContract.RES_BLOCKING_SUPPRESSED_UNTIL_TIMESTAMP, 0)); + Log.d(LOG_TAG, "getBlockSuppressionStatus: caller=%s, status=%s", + context.getOpPackageName(), blockSuppressionStatus); + return blockSuppressionStatus; + } + + /** + * Check whether should show the emergency call notification. + * + * @param context the context of the caller. + * @return {@code true} if should show emergency call notification. {@code false} otherwise. + * @hide + */ + @SystemApi + @RequiresPermission(allOf = { + android.Manifest.permission.READ_BLOCKED_NUMBERS, + android.Manifest.permission.WRITE_BLOCKED_NUMBERS + }) + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + public static boolean shouldShowEmergencyCallNotification(@NonNull Context context) { + verifyBlockedNumbersPermission(context); + try { + final Bundle res = context.getContentResolver().call(AUTHORITY_URI, + SystemContract.METHOD_SHOULD_SHOW_EMERGENCY_CALL_NOTIFICATION, null, null); + return res != null && res.getBoolean(RES_SHOW_EMERGENCY_CALL_NOTIFICATION, false); + } catch (NullPointerException | IllegalArgumentException ex) { + // The content resolver can throw an NPE or IAE; we don't want to crash Telecom if + // either of these happen. + Log.w(null, "shouldShowEmergencyCallNotification: provider not ready."); + return false; + } + } + + /** + * Check whether the enhanced block setting is enabled. + * + * @param context the context of the caller. + * @param key the key of the setting to check, can be + * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED} + * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_PRIVATE} + * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_PAYPHONE} + * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_UNKNOWN} + * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE} + * {@link SystemContract#ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION} + * @return {@code true} if the setting is enabled. {@code false} otherwise. + * @hide + */ + @SystemApi + @RequiresPermission(allOf = { + android.Manifest.permission.READ_BLOCKED_NUMBERS, + android.Manifest.permission.WRITE_BLOCKED_NUMBERS + }) + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + public static boolean getBlockedNumberSetting( + @NonNull Context context, @NonNull String key) { + verifyBlockedNumbersPermission(context); + Bundle extras = new Bundle(); + extras.putString(EXTRA_ENHANCED_SETTING_KEY, key); + try { + final Bundle res = context.getContentResolver().call(AUTHORITY_URI, + SystemContract.METHOD_GET_ENHANCED_BLOCK_SETTING, null, extras); + return res != null && res.getBoolean(RES_ENHANCED_SETTING_IS_ENABLED, false); + } catch (NullPointerException | IllegalArgumentException ex) { + // The content resolver can throw an NPE or IAE; we don't want to crash Telecom if + // either of these happen. + Log.w(null, "getEnhancedBlockSetting: provider not ready."); + return false; + } + } + + /** + * Set the enhanced block setting enabled status. + * + * @param context the context of the caller. + * @param key the key of the setting to set, can be + * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED} + * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_PRIVATE} + * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_PAYPHONE} + * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_UNKNOWN} + * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE} + * {@link SystemContract#ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION} + * @param value the enabled statue of the setting to set. + * @hide + */ + @SystemApi + @RequiresPermission(allOf = { + android.Manifest.permission.READ_BLOCKED_NUMBERS, + android.Manifest.permission.WRITE_BLOCKED_NUMBERS + }) + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + public static void setBlockedNumberSetting(@NonNull Context context, + @NonNull String key, boolean value) { + verifyBlockedNumbersPermission(context); + Bundle extras = new Bundle(); + extras.putString(EXTRA_ENHANCED_SETTING_KEY, key); + extras.putBoolean(EXTRA_ENHANCED_SETTING_VALUE, value); + context.getContentResolver().call(AUTHORITY_URI, + SystemContract.METHOD_SET_ENHANCED_BLOCK_SETTING, null, extras); + } + + /** + * Represents the current status of + * {@link #shouldSystemBlockNumber(Context, String, int, boolean)}. If emergency services + * have been contacted recently, {@link #mIsSuppressed} is {@code true}, and blocking + * is disabled until the timestamp {@link #mUntilTimestampMillis}. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + public static final class BlockSuppressionStatus { + private boolean mIsSuppressed; + + /** + * Timestamp in milliseconds from epoch. + */ + private long mUntilTimestampMillis; + + public BlockSuppressionStatus(boolean isSuppressed, long untilTimestampMillis) { + this.mIsSuppressed = isSuppressed; + this.mUntilTimestampMillis = untilTimestampMillis; + } + + @Override + public String toString() { + return "[BlockSuppressionStatus; isSuppressed=" + mIsSuppressed + ", until=" + + mUntilTimestampMillis + "]"; + } + + public boolean getIsSuppressed() { + return mIsSuppressed; + } + + public long getUntilTimestampMillis() { + return mUntilTimestampMillis; + } + } + + /** + * Verifies that the caller holds both the + * {@link android.Manifest.permission#READ_BLOCKED_NUMBERS} permission and the + * {@link android.Manifest.permission#WRITE_BLOCKED_NUMBERS} permission. + * + * @param context + * @throws SecurityException if the caller is missing the necessary permissions + */ + private static void verifyBlockedNumbersPermission(Context context) { + context.enforceCallingOrSelfPermission(Manifest.permission.READ_BLOCKED_NUMBERS, + "Caller does not have the android.permission.READ_BLOCKED_NUMBERS permission"); + context.enforceCallingOrSelfPermission(Manifest.permission.WRITE_BLOCKED_NUMBERS, + "Caller does not have the android.permission.WRITE_BLOCKED_NUMBERS permission"); + } } /** @hide */ @@ -558,7 +893,7 @@ public class BlockedNumberContract { * {@link #ENHANCED_SETTING_KEY_BLOCK_PAYPHONE} * {@link #ENHANCED_SETTING_KEY_BLOCK_UNKNOWN} * {@link #ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE} - * {@link #ENHANCED_SETTING_KEY_EMERGENCY_CALL_NOTIFICATION_SHOWING} + * {@link #ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION} * @return {@code true} if the setting is enabled. {@code false} otherwise. */ public static boolean getEnhancedBlockSetting(Context context, String key) { @@ -586,7 +921,7 @@ public class BlockedNumberContract { * {@link #ENHANCED_SETTING_KEY_BLOCK_PAYPHONE} * {@link #ENHANCED_SETTING_KEY_BLOCK_UNKNOWN} * {@link #ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE} - * {@link #ENHANCED_SETTING_KEY_EMERGENCY_CALL_NOTIFICATION_SHOWING} + * {@link #ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION} * @param value the enabled statue of the setting to set. */ public static void setEnhancedBlockSetting(Context context, String key, boolean value) { diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java index 7d127ad660c5..c13dd363d79e 100644 --- a/core/java/android/provider/CallLog.java +++ b/core/java/android/provider/CallLog.java @@ -19,6 +19,7 @@ package android.provider; import android.Manifest; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.LongDef; import android.annotation.NonNull; @@ -55,6 +56,8 @@ import android.telephony.PhoneNumberUtils; import android.text.TextUtils; import android.util.Log; +import com.android.server.telecom.flags.Flags; + import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -427,6 +430,8 @@ public class CallLog { private double mLongitude = Double.NaN; private Uri mPictureUri; private int mIsPhoneAccountMigrationPending; + private boolean mIsBusinessCall; + private String mBusinessName; /** * @param callerInfo the CallerInfo object to get the target contact from. @@ -645,15 +650,44 @@ public class CallLog { } /** + * @param isBusinessCall should be set if the caller is a business call + */ + @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER) + public @NonNull AddCallParametersBuilder setIsBusinessCall(boolean isBusinessCall) { + mIsBusinessCall = isBusinessCall; + return this; + } + + /** + * @param businessName should be set if the caller is a business call + */ + @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER) + public @NonNull AddCallParametersBuilder setBusinessName(String businessName) { + mBusinessName = businessName; + return this; + } + + /** * Builds the object */ public @NonNull AddCallParams build() { - return new AddCallParams(mCallerInfo, mNumber, mPostDialDigits, mViaNumber, - mPresentation, mCallType, mFeatures, mAccountHandle, mStart, mDuration, - mDataUsage, mAddForAllUsers, mUserToBeInsertedTo, mIsRead, mCallBlockReason, - mCallScreeningAppName, mCallScreeningComponentName, mMissedReason, - mPriority, mSubject, mLatitude, mLongitude, mPictureUri, - mIsPhoneAccountMigrationPending); + if (Flags.businessCallComposer()) { + return new AddCallParams(mCallerInfo, mNumber, mPostDialDigits, mViaNumber, + mPresentation, mCallType, mFeatures, mAccountHandle, mStart, mDuration, + mDataUsage, mAddForAllUsers, mUserToBeInsertedTo, mIsRead, + mCallBlockReason, + mCallScreeningAppName, mCallScreeningComponentName, mMissedReason, + mPriority, mSubject, mLatitude, mLongitude, mPictureUri, + mIsPhoneAccountMigrationPending, mIsBusinessCall, mBusinessName); + } else { + return new AddCallParams(mCallerInfo, mNumber, mPostDialDigits, mViaNumber, + mPresentation, mCallType, mFeatures, mAccountHandle, mStart, mDuration, + mDataUsage, mAddForAllUsers, mUserToBeInsertedTo, mIsRead, + mCallBlockReason, + mCallScreeningAppName, mCallScreeningComponentName, mMissedReason, + mPriority, mSubject, mLatitude, mLongitude, mPictureUri, + mIsPhoneAccountMigrationPending); + } } } @@ -681,6 +715,8 @@ public class CallLog { private double mLongitude = Double.NaN; private Uri mPictureUri; private int mIsPhoneAccountMigrationPending; + private boolean mIsBusinessCall; + private String mBusinessName; private AddCallParams(CallerInfo callerInfo, String number, String postDialDigits, String viaNumber, int presentation, int callType, int features, @@ -717,6 +753,43 @@ public class CallLog { mIsPhoneAccountMigrationPending = isPhoneAccountMigrationPending; } + private AddCallParams(CallerInfo callerInfo, String number, String postDialDigits, + String viaNumber, int presentation, int callType, int features, + PhoneAccountHandle accountHandle, long start, int duration, long dataUsage, + boolean addForAllUsers, UserHandle userToBeInsertedTo, boolean isRead, + int callBlockReason, + CharSequence callScreeningAppName, String callScreeningComponentName, + long missedReason, + int priority, String subject, double latitude, double longitude, Uri pictureUri, + int isPhoneAccountMigrationPending, boolean isBusinessCall, String businessName) { + mCallerInfo = callerInfo; + mNumber = number; + mPostDialDigits = postDialDigits; + mViaNumber = viaNumber; + mPresentation = presentation; + mCallType = callType; + mFeatures = features; + mAccountHandle = accountHandle; + mStart = start; + mDuration = duration; + mDataUsage = dataUsage; + mAddForAllUsers = addForAllUsers; + mUserToBeInsertedTo = userToBeInsertedTo; + mIsRead = isRead; + mCallBlockReason = callBlockReason; + mCallScreeningAppName = callScreeningAppName; + mCallScreeningComponentName = callScreeningComponentName; + mMissedReason = missedReason; + mPriority = priority; + mSubject = subject; + mLatitude = latitude; + mLongitude = longitude; + mPictureUri = pictureUri; + mIsPhoneAccountMigrationPending = isPhoneAccountMigrationPending; + mIsBusinessCall = isBusinessCall; + mBusinessName = businessName; + } + } /** @@ -915,6 +988,19 @@ public class CallLog { */ public static final String NUMBER = "number"; + + /** + * Boolean indicating whether the call is a business call. + */ + @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER) + public static final String IS_BUSINESS_CALL = "is_business_call"; + + /** + * String that stores the asserted display name associated with business call. + */ + @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER) + public static final String ASSERTED_DISPLAY_NAME = "asserted_display_name"; + /** * The number presenting rules set by the network. * @@ -1713,7 +1799,6 @@ public class CallLog { } ContentValues values = new ContentValues(14); - values.put(NUMBER, params.mNumber); values.put(POST_DIAL_DIGITS, params.mPostDialDigits); values.put(VIA_NUMBER, params.mViaNumber); @@ -1746,7 +1831,10 @@ public class CallLog { values.put(COMPOSER_PHOTO_URI, params.mPictureUri.toString()); } values.put(IS_PHONE_ACCOUNT_MIGRATION_PENDING, params.mIsPhoneAccountMigrationPending); - + if (Flags.businessCallComposer()) { + values.put(IS_BUSINESS_CALL, Integer.valueOf(params.mIsBusinessCall ? 1 : 0)); + values.put(ASSERTED_DISPLAY_NAME, params.mBusinessName); + } if ((params.mCallerInfo != null) && (params.mCallerInfo.getContactId() > 0)) { // Update usage information for the number associated with the contact ID. // We need to use both the number and the ID for obtaining a data ID since other 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 b026ce94b3fd..0db68b4d0b0c 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -12286,6 +12286,14 @@ public final class Settings { "extra_automatic_power_save_mode"; /** + * Whether contextual screen timeout is enabled. + * + * @hide + */ + public static final String CONTEXTUAL_SCREEN_TIMEOUT_ENABLED = + "contextual_screen_timeout_enabled"; + + /** * Whether lockscreen weather is enabled. * * @hide diff --git a/core/java/android/service/appprediction/AppPredictionService.java b/core/java/android/service/appprediction/AppPredictionService.java index a2ffa5d34219..2402cfda3fe1 100644 --- a/core/java/android/service/appprediction/AppPredictionService.java +++ b/core/java/android/service/appprediction/AppPredictionService.java @@ -18,6 +18,7 @@ package android.service.appprediction; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import android.annotation.CallSuper; +import android.annotation.FlaggedApi; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; @@ -31,12 +32,15 @@ import android.app.prediction.AppTargetId; import android.app.prediction.IPredictionCallback; import android.content.Intent; import android.content.pm.ParceledListSlice; +import android.os.Bundle; import android.os.CancellationSignal; import android.os.Handler; import android.os.IBinder; +import android.os.IRemoteCallback; import android.os.Looper; import android.os.RemoteException; import android.service.appprediction.IPredictionService.Stub; +import android.service.appprediction.flags.Flags; import android.util.ArrayMap; import android.util.Log; import android.util.Slog; @@ -134,6 +138,16 @@ public abstract class AppPredictionService extends Service { obtainMessage(AppPredictionService::doDestroyPredictionSession, AppPredictionService.this, sessionId)); } + + @FlaggedApi(Flags.FLAG_SERVICE_FEATURES_API) + @Override + public void requestServiceFeatures(AppPredictionSessionId sessionId, + IRemoteCallback callback) { + mHandler.sendMessage( + obtainMessage(AppPredictionService::onRequestServiceFeatures, + AppPredictionService.this, sessionId, + new RemoteCallbackWrapper(callback, null))); + } }; @CallSuper @@ -277,6 +291,18 @@ public abstract class AppPredictionService extends Service { public void onDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) {} /** + * Called by the client app to request {@link AppPredictionService} features info. + * + * @param sessionId the session's Id. It is @NonNull. + * @param callback the callback to return the Bundle which includes service features info. It + * is @NonNull. + */ + @FlaggedApi(Flags.FLAG_SERVICE_FEATURES_API) + @MainThread + public void onRequestServiceFeatures(@NonNull AppPredictionSessionId sessionId, + @NonNull Consumer<Bundle> callback) {} + + /** * Used by the prediction factory to send back results the client app. The can be called * in response to {@link #onRequestPredictionUpdate(AppPredictionSessionId)} or proactively as * a result of changes in predictions. @@ -357,4 +383,50 @@ public abstract class AppPredictionService extends Service { } } } + + private static final class RemoteCallbackWrapper implements Consumer<Bundle>, + IBinder.DeathRecipient { + + private IRemoteCallback mCallback; + private final Consumer<RemoteCallbackWrapper> mOnBinderDied; + + RemoteCallbackWrapper(IRemoteCallback callback, + @Nullable Consumer<RemoteCallbackWrapper> onBinderDied) { + mCallback = callback; + mOnBinderDied = onBinderDied; + if (mOnBinderDied != null) { + try { + mCallback.asBinder().linkToDeath(this, 0); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to link to death: " + e); + } + } + } + + public void destroy() { + if (mCallback != null && mOnBinderDied != null) { + mCallback.asBinder().unlinkToDeath(this, 0); + } + } + + @Override + public void accept(Bundle bundle) { + try { + if (mCallback != null) { + mCallback.sendResult(bundle); + } + } catch (RemoteException e) { + Slog.e(TAG, "Error sending result:" + e); + } + } + + @Override + public void binderDied() { + destroy(); + mCallback = null; + if (mOnBinderDied != null) { + mOnBinderDied.accept(this); + } + } + } } diff --git a/core/java/android/service/appprediction/IPredictionService.aidl b/core/java/android/service/appprediction/IPredictionService.aidl index 0f3df8561743..e144dfa4b22c 100644 --- a/core/java/android/service/appprediction/IPredictionService.aidl +++ b/core/java/android/service/appprediction/IPredictionService.aidl @@ -22,6 +22,7 @@ import android.app.prediction.AppPredictionContext; import android.app.prediction.AppPredictionSessionId; import android.app.prediction.IPredictionCallback; import android.content.pm.ParceledListSlice; +import android.os.IRemoteCallback; /** * Interface from the system to a prediction service. @@ -50,4 +51,6 @@ oneway interface IPredictionService { void requestPredictionUpdate(in AppPredictionSessionId sessionId); void onDestroyPredictionSession(in AppPredictionSessionId sessionId); + + void requestServiceFeatures(in AppPredictionSessionId sessionId, in IRemoteCallback callback); } diff --git a/core/java/android/service/appprediction/flags/flags.aconfig b/core/java/android/service/appprediction/flags/flags.aconfig new file mode 100644 index 000000000000..c7e47d4b3627 --- /dev/null +++ b/core/java/android/service/appprediction/flags/flags.aconfig @@ -0,0 +1,8 @@ +package: "android.service.appprediction.flags" + +flag { + name: "service_features_api" + namespace: "systemui" + description: "Guards the new requestServiceFeatures api" + bug: "292565550" +}
\ No newline at end of file diff --git a/core/java/android/service/chooser/AdditionalContentContract.java b/core/java/android/service/chooser/AdditionalContentContract.java new file mode 100644 index 000000000000..f679e8a6ff6b --- /dev/null +++ b/core/java/android/service/chooser/AdditionalContentContract.java @@ -0,0 +1,60 @@ +/* + * Copyright 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.service.chooser; + +import android.annotation.FlaggedApi; + +/** + * Specifies constants used by Chooser when interacting with the additional content provider, + * see {@link android.content.Intent#EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI}. + */ +@FlaggedApi(android.service.chooser.Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING) +public interface AdditionalContentContract { + + interface Columns { + /** + * Content URI for this item. + * <p> + * Note that this content URI must have a different authority from the content provided + * given in {@link android.content.Intent#EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI}. + */ + String URI = "uri"; + } + + /** + * Constants for {@link android.database.Cursor#getExtras} keys. + */ + interface CursorExtraKeys { + /** + * An integer, zero-based cursor position that corresponds to the URI specified + * with the {@link android.content.Intent#EXTRA_CHOOSER_FOCUSED_ITEM_POSITION} index into + * the @link android.content.Intent#EXTRA_STREAM} array. + */ + String POSITION = "position"; + } + + /** + * Constants for method names used with {@link android.content.ContentResolver#call} method. + */ + interface MethodNames { + /** + * A method name Chooser is using to notify the sharing app about a shared items selection + * change. + */ + String ON_SELECTION_CHANGED = "onSelectionChanged"; + } +} diff --git a/core/java/android/service/chooser/ChooserResult.java b/core/java/android/service/chooser/ChooserResult.java new file mode 100644 index 000000000000..4603be114508 --- /dev/null +++ b/core/java/android/service/chooser/ChooserResult.java @@ -0,0 +1,173 @@ +/* + * 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.service.chooser; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; +import android.compat.annotation.Overridable; +import android.content.ComponentName; +import android.content.Intent; +import android.content.IntentSender; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * An event reported to a supplied [IntentSender] by the system chooser when an activity is selected + * or other actions are taken to complete the session. + * + * @see Intent#EXTRA_CHOOSER_RESULT_INTENT_SENDER + */ +@FlaggedApi(android.service.chooser.Flags.FLAG_ENABLE_CHOOSER_RESULT) +public final class ChooserResult implements Parcelable { + + /** + * Controls whether to send ChooserResult to the optional IntentSender supplied to the Chooser. + * <p> + * When enabled, ChooserResult is added to the provided Intent as + * {@link Intent#EXTRA_CHOOSER_RESULT}, and sent for actions such as copy and edit, in addition + * to activity selection. When disabled, only the selected component + * is provided in {@link Intent#EXTRA_CHOSEN_COMPONENT}. + * <p> + * See: {@link Intent#createChooser(Intent, CharSequence, IntentSender)} + * + * @hide + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) + @Overridable + public static final long SEND_CHOOSER_RESULT = 263474465L; + + /** @hide */ + @IntDef({ + CHOOSER_RESULT_UNKNOWN, + CHOOSER_RESULT_SELECTED_COMPONENT, + CHOOSER_RESULT_COPY, + CHOOSER_RESULT_EDIT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ResultType { } + + /** An unknown action was taken to complete the session. */ + public static final int CHOOSER_RESULT_UNKNOWN = -1; + /** The session was completed by selecting an activity to launch. */ + public static final int CHOOSER_RESULT_SELECTED_COMPONENT = 0; + /** The session was completed by invoking the copy action. */ + public static final int CHOOSER_RESULT_COPY = 1; + /** The session was completed by invoking the edit action. */ + public static final int CHOOSER_RESULT_EDIT = 2; + + @ResultType + private final int mType; + private final ComponentName mSelectedComponent; + private final boolean mIsShortcut; + + private ChooserResult(@NonNull Parcel source) { + mType = source.readInt(); + mSelectedComponent = ComponentName.readFromParcel(source); + mIsShortcut = source.readBoolean(); + } + + /** @hide */ + public ChooserResult(@ResultType int type, @Nullable ComponentName componentName, + boolean isShortcut) { + mType = type; + mSelectedComponent = componentName; + mIsShortcut = isShortcut; + } + + /** + * The type of the result. + * + * @return the type of the result + */ + @ResultType + public int getType() { + return mType; + } + + /** + * Provides the component of the Activity selected for results with type + * when type is {@link ChooserResult#CHOOSER_RESULT_SELECTED_COMPONENT}. + * <p> + * For all other types, this value is null. + * + * @return the component name selected + */ + @Nullable + public ComponentName getSelectedComponent() { + return mSelectedComponent; + } + + /** + * Whether the selected component was provided by the app from as a shortcut. + * + * @return true if the selected component is a shortcut, false otherwise + */ + public boolean isShortcut() { + return mIsShortcut; + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull + public static final Parcelable.Creator<ChooserResult> CREATOR = + new Creator<>() { + @Override + public ChooserResult createFromParcel(Parcel source) { + return new ChooserResult(source); + } + + @Override + public ChooserResult[] newArray(int size) { + return new ChooserResult[0]; + } + }; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mType); + ComponentName.writeToParcel(mSelectedComponent, dest); + dest.writeBoolean(mIsShortcut); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ChooserResult that = (ChooserResult) o; + return mType == that.mType + && mIsShortcut == that.mIsShortcut + && Objects.equals(mSelectedComponent, that.mSelectedComponent); + } + + @Override + public int hashCode() { + return Objects.hash(mType, mSelectedComponent, mIsShortcut); + } +} 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 6410609a589d..2028c4057c01 100644 --- a/core/java/android/text/BoringLayout.java +++ b/core/java/android/text/BoringLayout.java @@ -711,18 +711,21 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback public void draw(Canvas c, Path highlight, Paint highlightpaint, int cursorOffset) { if (mDirect != null && highlight == null) { + float leftShift = 0; if (getUseBoundsForWidth()) { - c.save(); RectF drawingRect = computeDrawingBoundingBox(); if (drawingRect.left < 0) { - c.translate(-drawingRect.left, 0); + leftShift = -drawingRect.left; + c.translate(leftShift, 0); } } c.drawText(mDirect, 0, mBottom - mDesc, mPaint); - if (getUseBoundsForWidth()) { - c.restore(); + if (leftShift != 0) { + // Manually translate back to the original position because of b/324498002, using + // save/restore disappears the toggle switch drawables. + c.translate(-leftShift, 0); } } else { super.draw(c, highlight, highlightpaint, cursorOffset); diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 8ddb42d6b3be..e5d199ad8e46 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -464,11 +464,12 @@ public abstract class Layout { @Nullable Path selectionPath, @Nullable Paint selectionPaint, int cursorOffsetVertical) { + float leftShift = 0; if (mUseBoundsForWidth) { - canvas.save(); RectF drawingRect = computeDrawingBoundingBox(); if (drawingRect.left < 0) { - canvas.translate(-drawingRect.left, 0); + leftShift = -drawingRect.left; + canvas.translate(leftShift, 0); } } final long lineRange = getLineRangeForDraw(canvas); @@ -479,8 +480,10 @@ public abstract class Layout { drawWithoutText(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint, cursorOffsetVertical, firstLine, lastLine); drawText(canvas, firstLine, lastLine); - if (mUseBoundsForWidth) { - canvas.restore(); + if (leftShift != 0) { + // Manually translate back to the original position because of b/324498002, using + // save/restore disappears the toggle switch drawables. + canvas.translate(-leftShift, 0); } } diff --git a/core/java/android/tracing/perfetto/TracingContext.java b/core/java/android/tracing/perfetto/TracingContext.java index 0bce26e007a1..c1a61a7c2c34 100644 --- a/core/java/android/tracing/perfetto/TracingContext.java +++ b/core/java/android/tracing/perfetto/TracingContext.java @@ -105,6 +105,5 @@ public class TracingContext<DataSourceInstanceType extends DataSourceInstance, return res; } - // private static native void nativeFlush(long nativeDataSourcePointer); private static native void nativeFlush(TracingContext thiz, long ctxPointer); } diff --git a/core/java/android/util/Xml.java b/core/java/android/util/Xml.java index ec6e90b4153e..50d419f75a09 100644 --- a/core/java/android/util/Xml.java +++ b/core/java/android/util/Xml.java @@ -53,9 +53,12 @@ import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import javax.xml.parsers.SAXParserFactory; + /** * XML utility methods. */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class Xml { private Xml() {} @@ -73,8 +76,33 @@ public class Xml { * * @hide */ - public static final boolean ENABLE_BINARY_DEFAULT = SystemProperties - .getBoolean("persist.sys.binary_xml", true); + public static final boolean ENABLE_BINARY_DEFAULT = shouldEnableBinaryDefault(); + + @android.ravenwood.annotation.RavenwoodReplace + private static boolean shouldEnableBinaryDefault() { + return SystemProperties.getBoolean("persist.sys.binary_xml", true); + } + + private static boolean shouldEnableBinaryDefault$ravenwood() { + return true; + } + + /** + * Feature flag: when set, {@link #resolvePullParser(InputStream)}} will attempt to sniff + * using {@code pread} optimization. + * + * @hide + */ + public static final boolean ENABLE_RESOLVE_OPTIMIZATIONS = shouldEnableResolveOptimizations(); + + @android.ravenwood.annotation.RavenwoodReplace + private static boolean shouldEnableResolveOptimizations() { + return true; + } + + private static boolean shouldEnableResolveOptimizations$ravenwood() { + return false; + } /** * Parses the given xml string and fires events on the given SAX handler. @@ -82,7 +110,7 @@ public class Xml { public static void parse(String xml, ContentHandler contentHandler) throws SAXException { try { - XMLReader reader = XmlObjectFactory.newXMLReader(); + XMLReader reader = newXMLReader(); reader.setContentHandler(contentHandler); reader.parse(new InputSource(new StringReader(xml))); } catch (IOException e) { @@ -96,7 +124,7 @@ public class Xml { */ public static void parse(Reader in, ContentHandler contentHandler) throws IOException, SAXException { - XMLReader reader = XmlObjectFactory.newXMLReader(); + XMLReader reader = newXMLReader(); reader.setContentHandler(contentHandler); reader.parse(new InputSource(in)); } @@ -107,7 +135,7 @@ public class Xml { */ public static void parse(InputStream in, Encoding encoding, ContentHandler contentHandler) throws IOException, SAXException { - XMLReader reader = XmlObjectFactory.newXMLReader(); + XMLReader reader = newXMLReader(); reader.setContentHandler(contentHandler); InputSource source = new InputSource(in); source.setEncoding(encoding.expatName); @@ -120,19 +148,26 @@ public class Xml { @android.ravenwood.annotation.RavenwoodReplace public static XmlPullParser newPullParser() { try { - XmlPullParser parser = XmlObjectFactory.newXmlPullParser(); + XmlPullParser parser = newXmlPullParser(); parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); return parser; } catch (XmlPullParserException e) { - throw new AssertionError(); + throw new AssertionError(e); } } /** @hide */ public static XmlPullParser newPullParser$ravenwood() { - // TODO: remove once we're linking against libcore - return new BinaryXmlPullParser(); + try { + // Prebuilt kxml2-android does not support FEATURE_PROCESS_DOCDECL, so omit here; + // it's quite rare and all tests are passing + XmlPullParser parser = newXmlPullParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + return parser; + } catch (XmlPullParserException e) { + throw new AssertionError(e); + } } /** @@ -145,17 +180,10 @@ public class Xml { * @hide */ @SuppressWarnings("AndroidFrameworkEfficientXml") - @android.ravenwood.annotation.RavenwoodReplace public static @NonNull TypedXmlPullParser newFastPullParser() { return XmlUtils.makeTyped(newPullParser()); } - /** @hide */ - public static TypedXmlPullParser newFastPullParser$ravenwood() { - // TODO: remove once we're linking against libcore - return new BinaryXmlPullParser(); - } - /** * Creates a new {@link XmlPullParser} that reads XML documents using a * custom binary wire protocol which benchmarking has shown to be 8.5x @@ -189,11 +217,10 @@ public class Xml { * * @hide */ - @android.ravenwood.annotation.RavenwoodReplace public static @NonNull TypedXmlPullParser resolvePullParser(@NonNull InputStream in) throws IOException { final byte[] magic = new byte[4]; - if (in instanceof FileInputStream) { + if (ENABLE_RESOLVE_OPTIMIZATIONS && in instanceof FileInputStream) { try { Os.pread(((FileInputStream) in).getFD(), magic, 0, magic.length, 0); } catch (ErrnoException e) { @@ -222,31 +249,11 @@ public class Xml { return xml; } - /** @hide */ - public static @NonNull TypedXmlPullParser resolvePullParser$ravenwood(@NonNull InputStream in) - throws IOException { - // TODO: remove once we're linking against libcore - final TypedXmlPullParser xml = new BinaryXmlPullParser(); - try { - xml.setInput(in, StandardCharsets.UTF_8.name()); - } catch (XmlPullParserException e) { - throw new IOException(e); - } - return xml; - } - /** * Creates a new xml serializer. */ - @android.ravenwood.annotation.RavenwoodReplace public static XmlSerializer newSerializer() { - return XmlObjectFactory.newXmlSerializer(); - } - - /** @hide */ - public static XmlSerializer newSerializer$ravenwood() { - // TODO: remove once we're linking against libcore - return new BinaryXmlSerializer(); + return newXmlSerializer(); } /** @@ -259,17 +266,10 @@ public class Xml { * @hide */ @SuppressWarnings("AndroidFrameworkEfficientXml") - @android.ravenwood.annotation.RavenwoodReplace public static @NonNull TypedXmlSerializer newFastSerializer() { return XmlUtils.makeTyped(new FastXmlSerializer()); } - /** @hide */ - public static @NonNull TypedXmlSerializer newFastSerializer$ravenwood() { - // TODO: remove once we're linking against libcore - return new BinaryXmlSerializer(); - } - /** * Creates a new {@link XmlSerializer} that writes XML documents using a * custom binary wire protocol which benchmarking has shown to be 4.4x @@ -334,7 +334,6 @@ public class Xml { * * @hide */ - @android.ravenwood.annotation.RavenwoodKeep public static void copy(@NonNull XmlPullParser in, @NonNull XmlSerializer out) throws XmlPullParserException, IOException { // Some parsers may have already consumed the event that starts the @@ -394,7 +393,6 @@ public class Xml { * unsupported, which can confuse serializers. This method normalizes empty * strings to be {@code null}. */ - @android.ravenwood.annotation.RavenwoodKeep private static @Nullable String normalizeNamespace(@Nullable String namespace) { if (namespace == null || namespace.isEmpty()) { return null; @@ -457,4 +455,45 @@ public class Xml { ? (AttributeSet) parser : new XmlPullAttributes(parser); } + + @android.ravenwood.annotation.RavenwoodReplace + private static @NonNull XmlSerializer newXmlSerializer() { + return XmlObjectFactory.newXmlSerializer(); + } + + private static @NonNull XmlSerializer newXmlSerializer$ravenwood() { + try { + return XmlPullParserFactory.newInstance().newSerializer(); + } catch (XmlPullParserException e) { + throw new UnsupportedOperationException(e); + } + } + + @android.ravenwood.annotation.RavenwoodReplace + private static @NonNull XmlPullParser newXmlPullParser() { + return XmlObjectFactory.newXmlPullParser(); + } + + private static @NonNull XmlPullParser newXmlPullParser$ravenwood() { + try { + return XmlPullParserFactory.newInstance().newPullParser(); + } catch (XmlPullParserException e) { + throw new UnsupportedOperationException(e); + } + } + + @android.ravenwood.annotation.RavenwoodReplace + private static @NonNull XMLReader newXMLReader() { + return XmlObjectFactory.newXMLReader(); + } + + private static @NonNull XMLReader newXMLReader$ravenwood() { + try { + final SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setNamespaceAware(true); + return factory.newSAXParser().getXMLReader(); + } catch (Exception e) { + throw new UnsupportedOperationException(e); + } + } } 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/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index b5b81d17013a..29cc8594deec 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -73,6 +73,7 @@ import android.window.IScreenRecordingCallback; import android.window.ISurfaceSyncGroupCompletedListener; import android.window.ITaskFpsCallback; import android.window.ITrustedPresentationListener; +import android.window.IUnhandledDragListener; import android.window.InputTransferToken; import android.window.ScreenCapture; import android.window.TrustedPresentationThresholds; @@ -1091,4 +1092,10 @@ interface IWindowManager @EnforcePermission("DETECT_SCREEN_RECORDING") void unregisterScreenRecordingCallback(IScreenRecordingCallback callback); + + /** + * Sets the listener to be called back when a cross-window drag and drop operation is unhandled + * (ie. not handled by any window which can handle the drag). + */ + void setUnhandledDragListener(IUnhandledDragListener listener); } diff --git a/core/java/android/view/InsetsFrameProvider.java b/core/java/android/view/InsetsFrameProvider.java index 83bdb087a539..fe98faba98c3 100644 --- a/core/java/android/view/InsetsFrameProvider.java +++ b/core/java/android/view/InsetsFrameProvider.java @@ -18,6 +18,7 @@ package android.view; import android.annotation.IntRange; import android.annotation.NonNull; +import android.annotation.Nullable; import android.graphics.Insets; import android.graphics.Rect; import android.os.IBinder; @@ -110,6 +111,12 @@ public class InsetsFrameProvider implements Parcelable { private Insets mMinimalInsetsSizeInDisplayCutoutSafe = null; /** + * Indicates the bounding rectangles within the provided insets frame, in relative coordinates + * to the source frame. + */ + private Rect[] mBoundingRects = null; + + /** * Creates an InsetsFrameProvider which describes what frame an insets source should have. * * @param owner the owner of this provider. We might have multiple sources with the same type on @@ -205,6 +212,22 @@ public class InsetsFrameProvider implements Parcelable { return mMinimalInsetsSizeInDisplayCutoutSafe; } + /** + * Sets the bounding rectangles within and relative to the source frame. + */ + public InsetsFrameProvider setBoundingRects(@Nullable Rect[] boundingRects) { + mBoundingRects = boundingRects == null ? null : boundingRects.clone(); + return this; + } + + /** + * Returns the arbitrary bounding rects, or null if none were set. + */ + @Nullable + public Rect[] getBoundingRects() { + return mBoundingRects; + } + @Override public int describeContents() { return 0; @@ -231,6 +254,9 @@ public class InsetsFrameProvider implements Parcelable { sb.append(", mMinimalInsetsSizeInDisplayCutoutSafe=") .append(mMinimalInsetsSizeInDisplayCutoutSafe); } + if (mBoundingRects != null) { + sb.append(", mBoundingRects=").append(Arrays.toString(mBoundingRects)); + } sb.append("}"); return sb.toString(); } @@ -257,6 +283,7 @@ public class InsetsFrameProvider implements Parcelable { mInsetsSizeOverrides = in.createTypedArray(InsetsSizeOverride.CREATOR); mArbitraryRectangle = in.readTypedObject(Rect.CREATOR); mMinimalInsetsSizeInDisplayCutoutSafe = in.readTypedObject(Insets.CREATOR); + mBoundingRects = in.createTypedArray(Rect.CREATOR); } @Override @@ -268,6 +295,7 @@ public class InsetsFrameProvider implements Parcelable { out.writeTypedArray(mInsetsSizeOverrides, flags); out.writeTypedObject(mArbitraryRectangle, flags); out.writeTypedObject(mMinimalInsetsSizeInDisplayCutoutSafe, flags); + out.writeTypedArray(mBoundingRects, flags); } public boolean idEquals(InsetsFrameProvider o) { @@ -288,14 +316,15 @@ public class InsetsFrameProvider implements Parcelable { && Arrays.equals(mInsetsSizeOverrides, other.mInsetsSizeOverrides) && Objects.equals(mArbitraryRectangle, other.mArbitraryRectangle) && Objects.equals(mMinimalInsetsSizeInDisplayCutoutSafe, - other.mMinimalInsetsSizeInDisplayCutoutSafe); + other.mMinimalInsetsSizeInDisplayCutoutSafe) + && Arrays.equals(mBoundingRects, other.mBoundingRects); } @Override public int hashCode() { return Objects.hash(mId, mSource, mFlags, mInsetsSize, Arrays.hashCode(mInsetsSizeOverrides), mArbitraryRectangle, - mMinimalInsetsSizeInDisplayCutoutSafe); + mMinimalInsetsSizeInDisplayCutoutSafe, Arrays.hashCode(mBoundingRects)); } public static final @NonNull Parcelable.Creator<InsetsFrameProvider> CREATOR = diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java index bc33d5e2f6b1..f9eba2948913 100644 --- a/core/java/android/view/InsetsSource.java +++ b/core/java/android/view/InsetsSource.java @@ -38,6 +38,8 @@ import android.view.WindowInsets.Type.InsetsType; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Objects; import java.util.StringJoiner; @@ -105,6 +107,12 @@ public class InsetsSource implements Parcelable { }) public @interface Flags {} + /** + * Used when there are no bounding rects to describe an inset, which is only possible when the + * insets itself is {@link Insets#NONE}. + */ + private static final Rect[] NO_BOUNDING_RECTS = new Rect[0]; + private @Flags int mFlags; /** @@ -117,6 +125,7 @@ public class InsetsSource implements Parcelable { /** Frame of the source in screen coordinate space */ private final Rect mFrame; private @Nullable Rect mVisibleFrame; + private @Nullable Rect[] mBoundingRects; private boolean mVisible; @@ -127,6 +136,7 @@ public class InsetsSource implements Parcelable { private @InternalInsetsSide int mSideHint = SIDE_NONE; private final Rect mTmpFrame = new Rect(); + private final Rect mTmpBoundingRect = new Rect(); public InsetsSource(int id, @InsetsType int type) { mId = id; @@ -145,6 +155,9 @@ public class InsetsSource implements Parcelable { : null; mFlags = other.mFlags; mSideHint = other.mSideHint; + mBoundingRects = other.mBoundingRects != null + ? other.mBoundingRects.clone() + : null; } public void set(InsetsSource other) { @@ -155,6 +168,9 @@ public class InsetsSource implements Parcelable { : null; mFlags = other.mFlags; mSideHint = other.mSideHint; + mBoundingRects = other.mBoundingRects != null + ? other.mBoundingRects.clone() + : null; } public InsetsSource setFrame(int left, int top, int right, int bottom) { @@ -199,6 +215,15 @@ public class InsetsSource implements Parcelable { return this; } + /** + * Set the bounding rectangles of this source. They are expected to be relative to the source + * frame. + */ + public InsetsSource setBoundingRects(@Nullable Rect[] rects) { + mBoundingRects = rects != null ? rects.clone() : null; + return this; + } + public int getId() { return mId; } @@ -228,6 +253,13 @@ public class InsetsSource implements Parcelable { } /** + * Returns the bounding rectangles of this source. + */ + public @Nullable Rect[] getBoundingRects() { + return mBoundingRects; + } + + /** * Calculates the insets this source will cause to a client window. * * @param relativeFrame The frame to calculate the insets relative to. @@ -313,6 +345,82 @@ public class InsetsSource implements Parcelable { } /** + * Calculates the bounding rects the source will cause to a client window. + */ + public @NonNull Rect[] calculateBoundingRects(Rect relativeFrame, boolean ignoreVisibility) { + if (!ignoreVisibility && !mVisible) { + return NO_BOUNDING_RECTS; + } + + final Rect frame = getFrame(); + if (mBoundingRects == null) { + // No bounding rects set, make a single bounding rect that covers the intersection of + // the |frame| and the |relativeFrame|. + return mTmpBoundingRect.setIntersect(frame, relativeFrame) + ? new Rect[]{ new Rect(mTmpBoundingRect) } + : NO_BOUNDING_RECTS; + + } + + // Special treatment for captionBar inset type. During drag-resizing, the |frame| and + // |boundingRects| may not get updated as quickly as |relativeFrame|, so just assume the + // |frame| will always be either at the top or bottom of |relativeFrame|. This means some + // calculations to make |boundingRects| relative to |relativeFrame| can be skipped or + // simplified. + // TODO(b/254128050): remove special treatment. + if (getType() == WindowInsets.Type.captionBar()) { + final ArrayList<Rect> validBoundingRects = new ArrayList<>(); + for (final Rect boundingRect : mBoundingRects) { + // Assume that the caption |frame| and |relativeFrame| perfectly align at the top + // or bottom, meaning that the provided |boundingRect|, which is relative to the + // |frame| either is already relative to |relativeFrame| (for top captionBar()), or + // just needs to be made relative to |relativeFrame| for bottom bars. + final int frameHeight = frame.height(); + mTmpBoundingRect.set(boundingRect); + if (getId() == ID_IME_CAPTION_BAR) { + mTmpBoundingRect.offset(0, relativeFrame.height() - frameHeight); + } + validBoundingRects.add(new Rect(mTmpBoundingRect)); + } + return validBoundingRects.toArray(new Rect[validBoundingRects.size()]); + } + + // Regular treatment for non-captionBar inset types. + final ArrayList<Rect> validBoundingRects = new ArrayList<>(); + for (final Rect boundingRect : mBoundingRects) { + // |boundingRect| was provided relative to |frame|. Make it absolute to be in the same + // coordinate system as |frame|. + final Rect absBoundingRect = new Rect( + boundingRect.left + frame.left, + boundingRect.top + frame.top, + boundingRect.right + frame.left, + boundingRect.bottom + frame.top + ); + // Now find the intersection of that |absBoundingRect| with |relativeFrame|. In other + // words, whichever part of the bounding rect is inside the window frame. + if (!mTmpBoundingRect.setIntersect(absBoundingRect, relativeFrame)) { + // It's possible for this to be empty if the frame and bounding rects were larger + // than the |relativeFrame|, such as when a system window is wider than the app + // window width. Just ignore that rect since it will have no effect on the + // window insets. + continue; + } + // At this point, |mTmpBoundingRect| is a valid bounding rect located fully inside the + // window, convert it to be relative to the window so that apps don't need to know the + // location of the window to understand bounding rects. + validBoundingRects.add(new Rect( + mTmpBoundingRect.left - relativeFrame.left, + mTmpBoundingRect.top - relativeFrame.top, + mTmpBoundingRect.right - relativeFrame.left, + mTmpBoundingRect.bottom - relativeFrame.top)); + } + if (validBoundingRects.isEmpty()) { + return NO_BOUNDING_RECTS; + } + return validBoundingRects.toArray(new Rect[validBoundingRects.size()]); + } + + /** * Outputs the intersection of two rectangles. The shared edges will also be counted in the * intersection. * @@ -467,6 +575,7 @@ public class InsetsSource implements Parcelable { pw.print(" visible="); pw.print(mVisible); pw.print(" flags="); pw.print(flagsToString(mFlags)); pw.print(" sideHint="); pw.print(sideToString(mSideHint)); + pw.print(" boundingRects="); pw.print(Arrays.toString(mBoundingRects)); pw.println(); } @@ -492,12 +601,14 @@ public class InsetsSource implements Parcelable { if (mSideHint != that.mSideHint) return false; if (excludeInvisibleImeFrames && !mVisible && mType == WindowInsets.Type.ime()) return true; if (!Objects.equals(mVisibleFrame, that.mVisibleFrame)) return false; - return mFrame.equals(that.mFrame); + if (!mFrame.equals(that.mFrame)) return false; + return Arrays.equals(mBoundingRects, that.mBoundingRects); } @Override public int hashCode() { - return Objects.hash(mId, mType, mFrame, mVisibleFrame, mVisible, mFlags, mSideHint); + return Objects.hash(mId, mType, mFrame, mVisibleFrame, mVisible, mFlags, mSideHint, + Arrays.hashCode(mBoundingRects)); } public InsetsSource(Parcel in) { @@ -512,6 +623,7 @@ public class InsetsSource implements Parcelable { mVisible = in.readBoolean(); mFlags = in.readInt(); mSideHint = in.readInt(); + mBoundingRects = in.createTypedArray(Rect.CREATOR); } @Override @@ -533,6 +645,7 @@ public class InsetsSource implements Parcelable { dest.writeBoolean(mVisible); dest.writeInt(mFlags); dest.writeInt(mSideHint); + dest.writeTypedArray(mBoundingRects, flags); } @Override @@ -543,6 +656,7 @@ public class InsetsSource implements Parcelable { + " mVisible=" + mVisible + " mFlags=" + flagsToString(mFlags) + " mSideHint=" + sideToString(mSideHint) + + " mBoundingRects=" + Arrays.toString(mBoundingRects) + "}"; } diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java index c88da9e2eb4f..21eec67bfe10 100644 --- a/core/java/android/view/InsetsState.java +++ b/core/java/android/view/InsetsState.java @@ -128,6 +128,8 @@ public class InsetsState implements Parcelable { final Rect relativeFrameMax = new Rect(frame); @InsetsType int forceConsumingTypes = 0; @InsetsType int suppressScrimTypes = 0; + final Rect[][] typeBoundingRectsMap = new Rect[Type.SIZE][]; + final Rect[][] typeMaxBoundingRectsMap = new Rect[Type.SIZE][]; for (int i = mSources.size() - 1; i >= 0; i--) { final InsetsSource source = mSources.valueAt(i); final @InsetsType int type = source.getType(); @@ -141,7 +143,7 @@ public class InsetsState implements Parcelable { } processSource(source, relativeFrame, false /* ignoreVisibility */, typeInsetsMap, - idSideMap, typeVisibilityMap); + idSideMap, typeVisibilityMap, typeBoundingRectsMap); // IME won't be reported in max insets as the size depends on the EditorInfo of the IME // target. @@ -154,7 +156,7 @@ public class InsetsState implements Parcelable { } processSource(ignoringVisibilitySource, relativeFrameMax, true /* ignoreVisibility */, typeMaxInsetsMap, null /* idSideMap */, - null /* typeVisibilityMap */); + null /* typeVisibilityMap */, typeMaxBoundingRectsMap); } } final int softInputAdjustMode = legacySoftInputMode & SOFT_INPUT_MASK_ADJUST; @@ -175,7 +177,8 @@ public class InsetsState implements Parcelable { calculateRelativeRoundedCorners(frame), calculateRelativePrivacyIndicatorBounds(frame), calculateRelativeDisplayShape(frame), - compatInsetsTypes, (legacySystemUiFlags & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0); + compatInsetsTypes, (legacySystemUiFlags & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0, + typeBoundingRectsMap, typeMaxBoundingRectsMap, frame.width(), frame.height()); } private DisplayCutout calculateRelativeCutout(Rect frame) { @@ -328,12 +331,13 @@ public class InsetsState implements Parcelable { private void processSource(InsetsSource source, Rect relativeFrame, boolean ignoreVisibility, Insets[] typeInsetsMap, @Nullable @InternalInsetsSide SparseIntArray idSideMap, - @Nullable boolean[] typeVisibilityMap) { + @Nullable boolean[] typeVisibilityMap, Rect[][] typeBoundingRectsMap) { Insets insets = source.calculateInsets(relativeFrame, ignoreVisibility); + final Rect[] boundingRects = source.calculateBoundingRects(relativeFrame, ignoreVisibility); final int type = source.getType(); processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap, - insets, type); + typeBoundingRectsMap, insets, boundingRects, type); if (type == Type.MANDATORY_SYSTEM_GESTURES) { // Mandatory system gestures are also system gestures. @@ -342,24 +346,25 @@ public class InsetsState implements Parcelable { // ability to set systemGestureInsets() independently from // mandatorySystemGestureInsets() in the Builder. processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap, - insets, Type.SYSTEM_GESTURES); + typeBoundingRectsMap, insets, boundingRects, Type.SYSTEM_GESTURES); } if (type == Type.CAPTION_BAR) { // Caption should also be gesture and tappable elements. This should not be needed when // the caption is added from the shell, as the shell can add other types at the same // time. processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap, - insets, Type.SYSTEM_GESTURES); + typeBoundingRectsMap, insets, boundingRects, Type.SYSTEM_GESTURES); processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap, - insets, Type.MANDATORY_SYSTEM_GESTURES); + typeBoundingRectsMap, insets, boundingRects, Type.MANDATORY_SYSTEM_GESTURES); processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap, - insets, Type.TAPPABLE_ELEMENT); + typeBoundingRectsMap, insets, boundingRects, Type.TAPPABLE_ELEMENT); } } private void processSourceAsPublicType(InsetsSource source, Insets[] typeInsetsMap, @InternalInsetsSide @Nullable SparseIntArray idSideMap, - @Nullable boolean[] typeVisibilityMap, Insets insets, int type) { + @Nullable boolean[] typeVisibilityMap, Rect[][] typeBoundingRectsMap, + Insets insets, Rect[] boundingRects, int type) { int index = indexOf(type); // Don't put Insets.NONE into typeInsetsMap. Otherwise, two WindowInsets can be considered @@ -384,6 +389,22 @@ public class InsetsState implements Parcelable { idSideMap.put(source.getId(), insetSide); } } + + if (typeBoundingRectsMap != null && boundingRects.length > 0) { + final Rect[] existing = typeBoundingRectsMap[index]; + if (existing == null) { + typeBoundingRectsMap[index] = boundingRects; + } else { + typeBoundingRectsMap[index] = concatenate(existing, boundingRects); + } + } + } + + private static Rect[] concatenate(Rect[] a, Rect[] b) { + final Rect[] c = new Rect[a.length + b.length]; + System.arraycopy(a, 0, c, 0, a.length); + System.arraycopy(b, 0, c, a.length, b.length); + return c; } /** diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index c22986b3473b..dc0b1a77de80 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -43,6 +43,7 @@ import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFI import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS; import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP; import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION; +import static com.android.window.flags.Flags.FLAG_DELEGATE_UNHANDLED_DRAGS; import static java.lang.Math.max; @@ -68,6 +69,7 @@ import android.annotation.SystemApi; import android.annotation.TestApi; import android.annotation.UiContext; import android.annotation.UiThread; +import android.app.PendingIntent; import android.compat.annotation.UnsupportedAppUsage; import android.content.AutofillOptions; import android.content.ClipData; @@ -5329,6 +5331,34 @@ public class View implements Drawable.Callback, KeyEvent.Callback, public static final int DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION = 1 << 11; /** + * Flag indicating that a drag can cross window boundaries (within the same application). When + * {@link #startDragAndDrop(ClipData, DragShadowBuilder, Object, int)} is called + * with this flag set, only visible windows belonging to the same application (ie. share the + * same UID) with targetSdkVersion >= {@link android.os.Build.VERSION_CODES#N API 24} will be + * able to participate in the drag operation and receive the dragged content. + * + * If both DRAG_FLAG_GLOBAL_SAME_APPLICATION and DRAG_FLAG_GLOBAL are set, then + * DRAG_FLAG_GLOBAL_SAME_APPLICATION takes precedence and the drag will only go to visible + * windows from the same application. + */ + @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS) + public static final int DRAG_FLAG_GLOBAL_SAME_APPLICATION = 1 << 12; + + /** + * Flag indicating that an unhandled drag should be delegated to the system to be started if no + * visible window wishes to handle the drop. When using this flag, the caller must provide + * ClipData with an Item that contains an immutable PendingIntent to an activity to be launched + * (not a broadcast, service, etc). See + * {@link ClipData.Item.Builder#setPendingIntent(PendingIntent)}. + * + * The system can decide to launch the intent or not based on factors like the current screen + * size or windowing mode. If the system does not launch the intent, it will be canceled via the + * normal drag and drop flow. + */ + @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS) + public static final int DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG = 1 << 13; + + /** * Vertical scroll factor cached by {@link #getVerticalScrollFactor}. */ private float mVerticalScrollFactor; @@ -6364,6 +6394,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, case R.styleable.View_handwritingBoundsOffsetBottom: mHandwritingBoundsOffsetBottom = a.getDimension(attr, 0); break; + case R.styleable.View_contentSensitivity: + setContentSensitivity(a.getInt(attr, CONTENT_SENSITIVITY_AUTO)); + break; } } @@ -22269,6 +22302,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Retrieve a unique token identifying the window this view is attached to. * @return Return the window's token for use in * {@link WindowManager.LayoutParams#token WindowManager.LayoutParams.token}. + * This token maybe null if this view is not attached to a window. + * @see #isAttachedToWindow() for current window attach state + * @see OnAttachStateChangeListener to listen to window attach/detach state changes */ public IBinder getWindowToken() { return mAttachInfo != null ? mAttachInfo.mWindowToken : null; @@ -28496,9 +28532,29 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Log.w(VIEW_LOG_TAG, "startDragAndDrop called with an invalid surface."); return false; } + if ((flags & DRAG_FLAG_GLOBAL) != 0 && ((flags & DRAG_FLAG_GLOBAL_SAME_APPLICATION) != 0)) { + Log.w(VIEW_LOG_TAG, "startDragAndDrop called with both DRAG_FLAG_GLOBAL " + + "and DRAG_FLAG_GLOBAL_SAME_APPLICATION, the drag will default to " + + "DRAG_FLAG_GLOBAL_SAME_APPLICATION"); + flags &= ~DRAG_FLAG_GLOBAL; + } if (data != null) { - data.prepareToLeaveProcess((flags & View.DRAG_FLAG_GLOBAL) != 0); + if (com.android.window.flags.Flags.delegateUnhandledDrags()) { + data.prepareToLeaveProcess( + (flags & (DRAG_FLAG_GLOBAL_SAME_APPLICATION | DRAG_FLAG_GLOBAL)) != 0); + if ((flags & DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG) != 0) { + if (!data.hasActivityPendingIntents()) { + // Reset the flag if there is no launchable activity intent + flags &= ~DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG; + Log.w(VIEW_LOG_TAG, "startDragAndDrop called with " + + "DRAG_FLAG_START_INTENT_ON_UNHANDLED_DRAG but the clip data " + + "contains non-activity PendingIntents"); + } + } + } else { + data.prepareToLeaveProcess((flags & DRAG_FLAG_GLOBAL) != 0); + } } Rect bounds = new Rect(); @@ -28524,6 +28580,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (token != null) { root.setLocalDragState(myLocalState); mAttachInfo.mDragToken = token; + mAttachInfo.mDragData = data; mAttachInfo.mViewRootImpl.setDragStartedViewForAccessibility(this); setAccessibilityDragStarted(true); } @@ -28601,8 +28658,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (mAttachInfo.mDragSurface != null) { mAttachInfo.mDragSurface.release(); } + if (mAttachInfo.mDragData != null) { + mAttachInfo.mDragData.cleanUpPendingIntents(); + } mAttachInfo.mDragSurface = surface; mAttachInfo.mDragToken = token; + mAttachInfo.mDragData = data; // Cache the local state object for delivery with DragEvents root.setLocalDragState(myLocalState); if (a11yEnabled) { @@ -31516,11 +31577,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, IBinder mDragToken; /** + * Used to track the data of the current drag operation for cleanup later. + */ + ClipData mDragData; + + /** * The drag shadow surface for the current drag operation. */ public Surface mDragSurface; - /** * The view that currently has a tooltip displayed. */ diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 07c97950e9bb..28a73344b731 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -8599,6 +8599,10 @@ public final class ViewRootImpl implements ViewParent, mAttachInfo.mDragSurface.release(); mAttachInfo.mDragSurface = null; } + if (mAttachInfo.mDragData != null) { + mAttachInfo.mDragData.cleanUpPendingIntents(); + mAttachInfo.mDragData = null; + } } } } diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java index 921afaa99277..fbebe1e726b7 100644 --- a/core/java/android/view/WindowInsets.java +++ b/core/java/android/view/WindowInsets.java @@ -34,6 +34,7 @@ import static android.view.WindowInsets.Type.ime; import static android.view.WindowInsets.Type.indexOf; import static android.view.WindowInsets.Type.systemBars; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; @@ -44,8 +45,10 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.Intent; import android.graphics.Insets; import android.graphics.Rect; +import android.util.Size; import android.view.View.OnApplyWindowInsetsListener; import android.view.WindowInsets.Type.InsetsType; +import android.view.flags.Flags; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethod; @@ -54,7 +57,10 @@ import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Objects; /** @@ -78,6 +84,8 @@ public final class WindowInsets { private final Insets[] mTypeInsetsMap; private final Insets[] mTypeMaxInsetsMap; private final boolean[] mTypeVisibilityMap; + private final Rect[][] mTypeBoundingRectsMap; + private final Rect[][] mTypeMaxBoundingRectsMap; @Nullable private Rect mTempRect; private final boolean mIsRound; @@ -85,6 +93,8 @@ public final class WindowInsets { @Nullable private final RoundedCorners mRoundedCorners; @Nullable private final PrivacyIndicatorBounds mPrivacyIndicatorBounds; @Nullable private final DisplayShape mDisplayShape; + private final int mFrameWidth; + private final int mFrameHeight; private final @InsetsType int mForceConsumingTypes; private final @InsetsType int mSuppressScrimTypes; @@ -114,7 +124,7 @@ public final class WindowInsets { static { CONSUMED = new WindowInsets(createCompatTypeMap(null), createCompatTypeMap(null), createCompatVisibilityMap(createCompatTypeMap(null)), false, 0, 0, null, - null, null, null, systemBars(), false); + null, null, null, systemBars(), false, null, null, 0, 0); } /** @@ -139,7 +149,10 @@ public final class WindowInsets { RoundedCorners roundedCorners, PrivacyIndicatorBounds privacyIndicatorBounds, DisplayShape displayShape, - @InsetsType int compatInsetsTypes, boolean compatIgnoreVisibility) { + @InsetsType int compatInsetsTypes, boolean compatIgnoreVisibility, + Rect[][] typeBoundingRectsMap, + Rect[][] typeMaxBoundingRectsMap, + int frameWidth, int frameHeight) { mSystemWindowInsetsConsumed = typeInsetsMap == null; mTypeInsetsMap = mSystemWindowInsetsConsumed ? new Insets[SIZE] @@ -164,6 +177,14 @@ public final class WindowInsets { mRoundedCorners = roundedCorners; mPrivacyIndicatorBounds = privacyIndicatorBounds; mDisplayShape = displayShape; + mTypeBoundingRectsMap = (mSystemWindowInsetsConsumed || typeBoundingRectsMap == null) + ? new Rect[SIZE][] + : typeBoundingRectsMap.clone(); + mTypeMaxBoundingRectsMap = (mStableInsetsConsumed || typeMaxBoundingRectsMap == null) + ? new Rect[SIZE][] + : typeMaxBoundingRectsMap.clone(); + mFrameWidth = frameWidth; + mFrameHeight = frameHeight; } /** @@ -181,7 +202,11 @@ public final class WindowInsets { src.mPrivacyIndicatorBounds, src.mDisplayShape, src.mCompatInsetsTypes, - src.mCompatIgnoreVisibility); + src.mCompatIgnoreVisibility, + src.mSystemWindowInsetsConsumed ? null : src.mTypeBoundingRectsMap, + src.mStableInsetsConsumed ? null : src.mTypeMaxBoundingRectsMap, + src.mFrameWidth, + src.mFrameHeight); } private static DisplayCutout displayCutoutCopyConstructorArgument(WindowInsets w) { @@ -233,7 +258,8 @@ public final class WindowInsets { @UnsupportedAppUsage public WindowInsets(Rect systemWindowInsets) { this(createCompatTypeMap(systemWindowInsets), null, new boolean[SIZE], false, 0, 0, - null, null, null, null, systemBars(), false /* compatIgnoreVisibility */); + null, null, null, null, systemBars(), false /* compatIgnoreVisibility */, + new Rect[SIZE][], null, 0, 0); } /** @@ -475,6 +501,111 @@ public final class WindowInsets { } /** + * Returns a list of {@link Rect}s, each of which is the bounding rectangle for an area + * that is being partially or fully obscured inside the window. + * + * <p> + * May be used with or instead of {@link Insets} for finer avoidance of regions that may be + * partially obscuring the window but may be smaller than those provided by + * {@link #getInsets(int)}. + * </p> + * + * <p> + * The {@link Rect}s returned are always cropped to the bounds of the window frame and their + * coordinate values are relative to the {@link #getFrame()}, regardless of the window's + * position on screen. + * </p> + * + * <p> + * If inset by {@link #inset(Insets)}, bounding rects that intersect with the provided insets + * will be resized to only include the intersection with the remaining frame. Bounding rects + * may be completely removed if they no longer intersect with the new instance. + * </p> + * + * @param typeMask the insets type for which to obtain the bounding rectangles + * @return the bounding rectangles + */ + @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS) + @NonNull + public List<Rect> getBoundingRects(@InsetsType int typeMask) { + Rect[] allRects = null; + for (int i = FIRST; i <= LAST; i = i << 1) { + if ((typeMask & i) == 0) { + continue; + } + final Rect[] rects = mTypeBoundingRectsMap[indexOf(i)]; + if (rects == null) { + continue; + } + if (allRects == null) { + allRects = rects; + } else { + final Rect[] concat = new Rect[allRects.length + rects.length]; + System.arraycopy(allRects, 0, concat, 0, allRects.length); + System.arraycopy(rects, 0, concat, allRects.length, rects.length); + allRects = concat; + } + } + if (allRects == null) { + return Collections.emptyList(); + } + return Arrays.asList(allRects); + } + + /** + * Returns a list of {@link Rect}s, each of which is the bounding rectangle for an area that + * can be partially or fully obscured inside the window, regardless of whether + * that type is currently visible or not. + * + * <p> The bounding rects represent areas of a window that <b>may</b> be partially or fully + * obscured by the {@code type}. This value does not change based on the visibility state of + * those elements. For example, if the status bar is normally shown, but temporarily hidden, + * the bounding rects returned here will provide the rects associated with the status bar being + * shown.</p> + * + * <p> + * May be used with or instead of {@link Insets} for finer avoidance of regions that may be + * partially obscuring the window but may be smaller than those provided by + * {@link #getInsetsIgnoringVisibility(int)}. + * </p> + * + * <p> + * The {@link Rect}s returned are always cropped to the bounds of the window frame and their + * coordinate values are relative to the {@link #getFrame()}, regardless of the window's + * position on screen. + * </p> + * + * @param typeMask the insets type for which to obtain the bounding rectangles + * @return the bounding rectangles + */ + @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS) + @NonNull + public List<Rect> getBoundingRectsIgnoringVisibility(@InsetsType int typeMask) { + Rect[] allRects = null; + for (int i = FIRST; i <= LAST; i = i << 1) { + if ((typeMask & i) == 0) { + continue; + } + final Rect[] rects = mTypeMaxBoundingRectsMap[indexOf(i)]; + if (rects == null) { + continue; + } + if (allRects == null) { + allRects = rects; + } else { + final Rect[] concat = new Rect[allRects.length + rects.length]; + System.arraycopy(allRects, 0, concat, 0, allRects.length); + System.arraycopy(rects, 0, concat, allRects.length, rects.length); + allRects = concat; + } + } + if (allRects == null) { + return Collections.emptyList(); + } + return Arrays.asList(allRects); + } + + /** * Returns the display cutout if there is one. * * <p>Note: the display cutout will already be {@link #consumeDisplayCutout consumed} during @@ -555,7 +686,10 @@ public final class WindowInsets { mTypeVisibilityMap, mIsRound, mForceConsumingTypes, mSuppressScrimTypes, null /* displayCutout */, mRoundedCorners, mPrivacyIndicatorBounds, mDisplayShape, - mCompatInsetsTypes, mCompatIgnoreVisibility); + mCompatInsetsTypes, mCompatIgnoreVisibility, + mSystemWindowInsetsConsumed ? null : mTypeBoundingRectsMap, + mStableInsetsConsumed ? null : mTypeMaxBoundingRectsMap, + mFrameWidth, mFrameHeight); } @@ -610,7 +744,7 @@ public final class WindowInsets { (mCompatInsetsTypes & displayCutout()) != 0 ? null : displayCutoutCopyConstructorArgument(this), mRoundedCorners, mPrivacyIndicatorBounds, mDisplayShape, mCompatInsetsTypes, - mCompatIgnoreVisibility); + mCompatIgnoreVisibility, null, null, mFrameWidth, mFrameHeight); } // TODO(b/119190588): replace @code with @link below @@ -914,6 +1048,10 @@ public final class WindowInsets { result.append(Type.toString(1 << i)).append("=").append(insets) .append(" max=").append(maxInsets) .append(" vis=").append(visible) + .append(" boundingRects=") + .append(Arrays.toString(mTypeBoundingRectsMap[i])) + .append(" maxBoundingRects=") + .append(Arrays.toString(mTypeMaxBoundingRectsMap[i])) .append("\n "); } } @@ -942,6 +1080,10 @@ public final class WindowInsets { result.append("displayCutoutConsumed=" + mDisplayCutoutConsumed); result.append("\n "); result.append(isRound() ? "round" : ""); + result.append("\n "); + result.append("frameWidth=" + mFrameWidth); + result.append("\n "); + result.append("frameHeight=" + mFrameHeight); result.append("}"); return result.toString(); } @@ -1013,6 +1155,27 @@ public final class WindowInsets { } /** + * Returns the assumed size of the window, relative to which the {@link #getInsets} and + * {@link #getBoundingRects} have been calculated. + * + * <p> May be used with {@link #getBoundingRects} to better understand their position within + * the window, such as the area between the edge of a bounding rect and the edge of the window. + * + * <p>Note: the size may not match the actual size of the window, which is determined during + * the layout pass - as {@link WindowInsets} are dispatched before layout. + * + * <p>Caution: using this value in determining the actual window size may make the result of + * layout passes unstable and should be avoided. + * + * @return the assumed size of the window during the inset calculation + */ + @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS) + @NonNull + public Size getFrame() { + return new Size(mFrameWidth, mFrameHeight); + } + + /** * @see #inset(int, int, int, int) * @hide */ @@ -1039,7 +1202,17 @@ public final class WindowInsets { ? null : mPrivacyIndicatorBounds.inset(left, top, right, bottom), mDisplayShape, - mCompatInsetsTypes, mCompatIgnoreVisibility); + mCompatInsetsTypes, mCompatIgnoreVisibility, + mSystemWindowInsetsConsumed + ? null + : insetBoundingRects(mTypeBoundingRectsMap, left, top, right, bottom, + mFrameWidth, mFrameHeight), + mStableInsetsConsumed + ? null + : insetBoundingRects(mTypeMaxBoundingRectsMap, left, top, right, bottom, + mFrameWidth, mFrameHeight), + Math.max(0, mFrameWidth - left - right), + Math.max(0, mFrameHeight - top - bottom)); } @Override @@ -1060,7 +1233,11 @@ public final class WindowInsets { && Objects.equals(mDisplayCutout, that.mDisplayCutout) && Objects.equals(mRoundedCorners, that.mRoundedCorners) && Objects.equals(mPrivacyIndicatorBounds, that.mPrivacyIndicatorBounds) - && Objects.equals(mDisplayShape, that.mDisplayShape); + && Objects.equals(mDisplayShape, that.mDisplayShape) + && Arrays.deepEquals(mTypeBoundingRectsMap, that.mTypeBoundingRectsMap) + && Arrays.deepEquals(mTypeMaxBoundingRectsMap, that.mTypeMaxBoundingRectsMap) + && mFrameWidth == that.mFrameWidth + && mFrameHeight == that.mFrameHeight; } @Override @@ -1069,7 +1246,8 @@ public final class WindowInsets { Arrays.hashCode(mTypeVisibilityMap), mIsRound, mDisplayCutout, mRoundedCorners, mForceConsumingTypes, mSuppressScrimTypes, mSystemWindowInsetsConsumed, mStableInsetsConsumed, mDisplayCutoutConsumed, mPrivacyIndicatorBounds, - mDisplayShape); + mDisplayShape, Arrays.deepHashCode(mTypeBoundingRectsMap), + Arrays.deepHashCode(mTypeMaxBoundingRectsMap), mFrameWidth, mFrameHeight); } @@ -1110,6 +1288,68 @@ public final class WindowInsets { return Insets.of(newLeft, newTop, newRight, newBottom); } + static Rect[][] insetBoundingRects(Rect[][] typeBoundingRectsMap, + int insetLeft, int insetTop, int insetRight, int insetBottom, int frameWidth, + int frameHeight) { + if (insetLeft == 0 && insetTop == 0 && insetRight == 0 && insetBottom == 0) { + return typeBoundingRectsMap; + } + boolean cloned = false; + for (int i = 0; i < SIZE; i++) { + final Rect[] boundingRects = typeBoundingRectsMap[i]; + if (boundingRects == null) { + continue; + } + final Rect[] insetBoundingRects = insetBoundingRects(boundingRects, + insetLeft, insetTop, insetRight, insetBottom, frameWidth, frameHeight); + if (!Arrays.equals(insetBoundingRects, boundingRects)) { + if (!cloned) { + typeBoundingRectsMap = typeBoundingRectsMap.clone(); + cloned = true; + } + typeBoundingRectsMap[i] = insetBoundingRects; + } + } + return typeBoundingRectsMap; + } + + static Rect[] insetBoundingRects(Rect[] boundingRects, + int left, int top, int right, int bottom, int frameWidth, int frameHeight) { + final List<Rect> insetBoundingRectsList = new ArrayList<>(); + for (int i = 0; i < boundingRects.length; i++) { + final Rect insetRect = insetRect(boundingRects[i], left, top, right, bottom, + frameWidth, frameHeight); + if (insetRect != null) { + insetBoundingRectsList.add(insetRect); + } + } + return insetBoundingRectsList.toArray(new Rect[0]); + } + + private static Rect insetRect(Rect orig, int insetLeft, int insetTop, int insetRight, + int insetBottom, int frameWidth, int frameHeight) { + if (orig == null) { + return null; + } + + // Calculate the inset frame, and leave it in that coordinate space for easier comparison + // against the |orig| rect. + final Rect insetFrame = new Rect(insetLeft, insetTop, frameWidth - insetRight, + frameHeight - insetBottom); + // Then the intersecting portion of |orig| with the inset |insetFrame|. + final Rect insetRect = new Rect(); + if (insetRect.setIntersect(insetFrame, orig)) { + // The intersection is the inset rect, but its position must be shifted to be relative + // to the frame. Since the new frame will start at left=|insetLeft| and top=|insetTop|, + // just offset that much back in the direction of the origin of the frame. + insetRect.offset(-insetLeft, -insetTop); + return insetRect; + } else { + // The |orig| rect does not intersect with the new frame at all, so don't report it. + return null; + } + } + /** * @return whether system window insets have been consumed. */ @@ -1125,6 +1365,8 @@ public final class WindowInsets { private final Insets[] mTypeInsetsMap; private final Insets[] mTypeMaxInsetsMap; private final boolean[] mTypeVisibilityMap; + private final Rect[][] mTypeBoundingRectsMap; + private final Rect[][] mTypeMaxBoundingRectsMap; private boolean mSystemInsetsConsumed = true; private boolean mStableInsetsConsumed = true; @@ -1137,6 +1379,8 @@ public final class WindowInsets { private @InsetsType int mSuppressScrimTypes; private PrivacyIndicatorBounds mPrivacyIndicatorBounds = new PrivacyIndicatorBounds(); + private int mFrameWidth; + private int mFrameHeight; /** * Creates a builder where all insets are initially consumed. @@ -1145,6 +1389,8 @@ public final class WindowInsets { mTypeInsetsMap = new Insets[SIZE]; mTypeMaxInsetsMap = new Insets[SIZE]; mTypeVisibilityMap = new boolean[SIZE]; + mTypeBoundingRectsMap = new Rect[SIZE][]; + mTypeMaxBoundingRectsMap = new Rect[SIZE][]; } /** @@ -1165,6 +1411,10 @@ public final class WindowInsets { mSuppressScrimTypes = insets.mSuppressScrimTypes; mPrivacyIndicatorBounds = insets.mPrivacyIndicatorBounds; mDisplayShape = insets.mDisplayShape; + mTypeBoundingRectsMap = insets.mTypeBoundingRectsMap.clone(); + mTypeMaxBoundingRectsMap = insets.mTypeMaxBoundingRectsMap.clone(); + mFrameWidth = insets.mFrameWidth; + mFrameHeight = insets.mFrameHeight; } /** @@ -1452,6 +1702,68 @@ public final class WindowInsets { } /** + * Sets the bounding rects. + * + * @param typeMask the inset types to which these rects apply. + * @param rects the bounding rects. + * @return itself. + */ + @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS) + @NonNull + public Builder setBoundingRects(@InsetsType int typeMask, @NonNull List<Rect> rects) { + for (int i = FIRST; i <= LAST; i = i << 1) { + if ((typeMask & i) == 0) { + continue; + } + mTypeBoundingRectsMap[indexOf(i)] = rects.toArray(new Rect[0]); + } + return this; + } + + /** + * Sets the bounding rects while ignoring their visibility state. + * + * @param typeMask the inset types to which these rects apply. + * @param rects the bounding rects. + * @return itself. + * + * @throws IllegalArgumentException If {@code typeMask} contains {@link Type#ime()}. + * Maximum bounding rects are not available for this type as the height of the IME is + * dynamic depending on the {@link EditorInfo} of the currently focused view, as well as + * the UI state of the IME. + */ + @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS) + @NonNull + public Builder setBoundingRectsIgnoringVisibility(@InsetsType int typeMask, + @NonNull List<Rect> rects) { + if (typeMask == IME) { + throw new IllegalArgumentException("Maximum bounding rects not available for IME"); + } + for (int i = FIRST; i <= LAST; i = i << 1) { + if ((typeMask & i) == 0) { + continue; + } + mTypeMaxBoundingRectsMap[indexOf(i)] = rects.toArray(new Rect[0]); + } + return this; + } + + /** + * Set the frame size. + * + * @param width the width of the frame. + * @param height the height of the frame. + * @return itself. + */ + @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS) + @NonNull + public Builder setFrame(int width, int height) { + mFrameWidth = width; + mFrameHeight = height; + return this; + } + + /** * Builds a {@link WindowInsets} instance. * * @return the {@link WindowInsets} instance. @@ -1462,7 +1774,10 @@ public final class WindowInsets { mStableInsetsConsumed ? null : mTypeMaxInsetsMap, mTypeVisibilityMap, mIsRound, mForceConsumingTypes, mSuppressScrimTypes, mDisplayCutout, mRoundedCorners, mPrivacyIndicatorBounds, mDisplayShape, systemBars(), - false /* compatIgnoreVisibility */); + false /* compatIgnoreVisibility */, + mSystemInsetsConsumed ? null : mTypeBoundingRectsMap, + mStableInsetsConsumed ? null : mTypeMaxBoundingRectsMap, + mFrameWidth, mFrameHeight); } } diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index ae00b7072684..2fb5213279a6 100644 --- a/core/java/android/view/WindowManagerImpl.java +++ b/core/java/android/view/WindowManagerImpl.java @@ -520,11 +520,16 @@ public final class WindowManagerImpl implements WindowManager { public void registerTrustedPresentationListener(@NonNull IBinder window, @NonNull TrustedPresentationThresholds thresholds, @NonNull Executor executor, @NonNull Consumer<Boolean> listener) { + Objects.requireNonNull(window, "window must not be null"); + Objects.requireNonNull(thresholds, "thresholds must not be null"); + Objects.requireNonNull(executor, "executor must not be null"); + Objects.requireNonNull(listener, "listener must not be null"); mGlobal.registerTrustedPresentationListener(window, thresholds, executor, listener); } @Override public void unregisterTrustedPresentationListener(@NonNull Consumer<Boolean> listener) { + Objects.requireNonNull(listener, "listener must not be null"); mGlobal.unregisterTrustedPresentationListener(listener); } 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..55986e7ada47 100644 --- a/core/java/android/view/inputmethod/flags.aconfig +++ b/core/java/android/view/inputmethod/flags.aconfig @@ -62,3 +62,19 @@ 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 +} + +flag { + name: "connectionless_handwriting" + namespace: "input_method" + description: "Feature flag for connectionless stylus handwriting APIs" + bug: "300979854" + is_fixed_read_only: true +} 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/IUnhandledDragCallback.aidl b/core/java/android/window/IUnhandledDragCallback.aidl new file mode 100644 index 000000000000..7806b1fe771a --- /dev/null +++ b/core/java/android/window/IUnhandledDragCallback.aidl @@ -0,0 +1,33 @@ +/* + * 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.window; + +import android.view.DragEvent; + +/** + * A callback for notifying the system when the unhandled drop is complete. + * {@hide} + */ +oneway interface IUnhandledDragCallback { + /** + * Called when the IUnhandledDropListener has fully handled the drop, and the drag can be + * cleaned up. If handled is `true`, then cleanup of the drag and drag surface will be + * immediate, otherwise, the system will treat the drag as a cancel back to the start of the + * drag. + */ + void notifyUnhandledDropComplete(boolean handled); +} diff --git a/core/java/android/window/IUnhandledDragListener.aidl b/core/java/android/window/IUnhandledDragListener.aidl new file mode 100644 index 000000000000..52e98952971d --- /dev/null +++ b/core/java/android/window/IUnhandledDragListener.aidl @@ -0,0 +1,35 @@ +/* + * 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.window; + +import android.view.DragEvent; +import android.window.IUnhandledDragCallback; + +/** + * An interface to a handler for global drags that are not consumed (ie. not handled by any window). + * {@hide} + */ +oneway interface IUnhandledDragListener { + /** + * Called when the user finishes the drag gesture but no windows have reported handling the + * drop. The DragEvent is populated with the drag surface for the listener to animate. The + * listener *MUST* call the provided callback exactly once when it has finished handling the + * drop. If the listener calls the callback with `true` then it is responsible for removing + * and releasing the drag surface passed through the DragEvent. + */ + void onUnhandledDrop(in DragEvent event, in IUnhandledDragCallback callback); +} diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index efc71d7b43de..76a34aec7e58 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -676,18 +676,20 @@ public final class WindowContainerTransaction implements Parcelable { * This identifies them. * @param type The {@link InsetsType} of the insets source. * @param frame The rectangle area of the insets source. + * @param boundingRects The bounding rects within this inset, relative to the |frame|. * @hide */ @NonNull public WindowContainerTransaction addInsetsSource( @NonNull WindowContainerToken receiver, - IBinder owner, int index, @InsetsType int type, Rect frame) { + IBinder owner, int index, @InsetsType int type, Rect frame, Rect[] boundingRects) { final HierarchyOp hierarchyOp = new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_ADD_INSETS_FRAME_PROVIDER) .setContainer(receiver.asBinder()) .setInsetsFrameProvider(new InsetsFrameProvider(owner, index, type) .setSource(InsetsFrameProvider.SOURCE_ARBITRARY_RECTANGLE) - .setArbitraryRectangle(frame)) + .setArbitraryRectangle(frame) + .setBoundingRects(boundingRects)) .setInsetsFrameOwner(owner) .build(); mHierarchyOps.add(hierarchyOp); diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java index 0b7593a1a5a2..c5c17cffa48b 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistory.java +++ b/core/java/com/android/internal/os/BatteryStatsHistory.java @@ -1146,7 +1146,7 @@ public class BatteryStatsHistory { mHistoryCur.batteryHealth = (byte) health; mHistoryCur.batteryPlugType = (byte) plugType; mHistoryCur.batteryTemperature = (short) temperature; - mHistoryCur.batteryVoltage = (char) voltageMv; + mHistoryCur.batteryVoltage = (short) voltageMv; mHistoryCur.batteryChargeUah = chargeUah; } } @@ -2010,7 +2010,11 @@ public class BatteryStatsHistory { int bits = 0; bits = setBitField(bits, h.batteryLevel, 25, 0xfe000000 /* 7F << 25 */); bits = setBitField(bits, h.batteryTemperature, 15, 0x01ff8000 /* 3FF << 15 */); - bits = setBitField(bits, h.batteryVoltage, 1, 0x00007ffe /* 3FFF << 1 */); + short voltage = (short) h.batteryVoltage; + if (voltage == -1) { + voltage = 0x3FFF; + } + bits = setBitField(bits, voltage, 1, 0x00007ffe /* 3FFF << 1 */); return bits; } diff --git a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java index 739ee48295a9..b2a6a934ba3f 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java +++ b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java @@ -309,7 +309,12 @@ public class BatteryStatsHistoryIterator implements Iterator<BatteryStats.Histor private static void readBatteryLevelInt(int batteryLevelInt, BatteryStats.HistoryItem out) { out.batteryLevel = (byte) ((batteryLevelInt & 0xfe000000) >>> 25); out.batteryTemperature = (short) ((batteryLevelInt & 0x01ff8000) >>> 15); - out.batteryVoltage = (char) ((batteryLevelInt & 0x00007ffe) >>> 1); + int voltage = ((batteryLevelInt & 0x00007ffe) >>> 1); + if (voltage == 0x3FFF) { + voltage = -1; + } + + out.batteryVoltage = (short) voltage; } /** diff --git a/core/java/com/android/internal/os/TimeoutRecord.java b/core/java/com/android/internal/os/TimeoutRecord.java index e9a8d4b75f16..1f4abc19571a 100644 --- a/core/java/com/android/internal/os/TimeoutRecord.java +++ b/core/java/com/android/internal/os/TimeoutRecord.java @@ -45,6 +45,7 @@ public class TimeoutRecord { TimeoutKind.APP_REGISTERED, TimeoutKind.SHORT_FGS_TIMEOUT, TimeoutKind.JOB_SERVICE, + TimeoutKind.FGS_TIMEOUT, }) @Retention(RetentionPolicy.SOURCE) @@ -59,6 +60,7 @@ public class TimeoutRecord { int SHORT_FGS_TIMEOUT = 8; int JOB_SERVICE = 9; int APP_START = 10; + int FGS_TIMEOUT = 11; } /** Kind of timeout, e.g. BROADCAST_RECEIVER, etc. */ @@ -186,6 +188,12 @@ public class TimeoutRecord { return TimeoutRecord.endingNow(TimeoutKind.SHORT_FGS_TIMEOUT, reason); } + /** Record for a "foreground service" timeout. */ + @NonNull + public static TimeoutRecord forFgsTimeout(String reason) { + return TimeoutRecord.endingNow(TimeoutKind.FGS_TIMEOUT, reason); + } + /** Record for a job related timeout. */ @NonNull public static TimeoutRecord forJobService(String reason) { diff --git a/core/java/com/android/internal/os/anr/AnrLatencyTracker.java b/core/java/com/android/internal/os/anr/AnrLatencyTracker.java index f62ff38528f7..e11067dbd722 100644 --- a/core/java/com/android/internal/os/anr/AnrLatencyTracker.java +++ b/core/java/com/android/internal/os/anr/AnrLatencyTracker.java @@ -22,6 +22,7 @@ import static com.android.internal.os.TimeoutRecord.TimeoutKind; import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__BROADCAST_OF_INTENT; import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__CONTENT_PROVIDER_NOT_RESPONDING; import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__EXECUTING_SERVICE; +import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__FGS_TIMEOUT; import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__INPUT_DISPATCHING_TIMEOUT; import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__INPUT_DISPATCHING_TIMEOUT_NO_FOCUSED_WINDOW; import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__JOB_SERVICE; @@ -548,6 +549,8 @@ public class AnrLatencyTracker implements AutoCloseable { return ANRLATENCY_REPORTED__ANR_TYPE__SHORT_FGS_TIMEOUT; case TimeoutKind.JOB_SERVICE: return ANRLATENCY_REPORTED__ANR_TYPE__JOB_SERVICE; + case TimeoutKind.FGS_TIMEOUT: + return ANRLATENCY_REPORTED__ANR_TYPE__FGS_TIMEOUT; default: return ANRLATENCY_REPORTED__ANR_TYPE__UNKNOWN_ANR_TYPE; } diff --git a/core/java/com/android/internal/widget/NotificationOptimizedLinearLayout.java b/core/java/com/android/internal/widget/NotificationOptimizedLinearLayout.java index b5e9b8f537e4..0ceba25ede2c 100644 --- a/core/java/com/android/internal/widget/NotificationOptimizedLinearLayout.java +++ b/core/java/com/android/internal/widget/NotificationOptimizedLinearLayout.java @@ -53,6 +53,7 @@ import java.util.List; * - LinearLayout doesn't have <code>weightSum</code>. * - Horizontal LinearLayout's width should be measured EXACTLY. * - Horizontal LinearLayout shouldn't need baseLineAlignment. + * - Horizontal LinearLayout shouldn't have any child that has negative left or right margin. * - Vertical LinearLayout shouldn't have MATCH_PARENT children when it is not measured EXACTLY. * * @hide @@ -88,7 +89,7 @@ public class NotificationOptimizedLinearLayout extends LinearLayout { final View weightedChildView = getSingleWeightedChild(); mShouldUseOptimizedLayout = isUseOptimizedLinearLayoutFlagEnabled() && weightedChildView != null - && isLinearLayoutUsable(widthMeasureSpec, heightMeasureSpec); + && isOptimizationPossible(widthMeasureSpec, heightMeasureSpec); if (mShouldUseOptimizedLayout) { onMeasureOptimized(weightedChildView, widthMeasureSpec, heightMeasureSpec); @@ -118,7 +119,7 @@ public class NotificationOptimizedLinearLayout extends LinearLayout { * @param heightMeasureSpec The height measurement specification. * @return `true` if optimization is possible, `false` otherwise. */ - private boolean isLinearLayoutUsable(int widthMeasureSpec, int heightMeasureSpec) { + private boolean isOptimizationPossible(int widthMeasureSpec, int heightMeasureSpec) { final boolean hasWeightSum = getWeightSum() > 0.0f; if (hasWeightSum) { logSkipOptimizedOnMeasure("Has weightSum."); @@ -142,10 +143,36 @@ public class NotificationOptimizedLinearLayout extends LinearLayout { logSkipOptimizedOnMeasure("Need to apply baseline."); return false; } + + if (requiresNegativeMarginHandlingForHorizontalLinearLayout()) { + logSkipOptimizedOnMeasure("Need to handle negative margins."); + return false; + } return true; } /** + * @return if the horizontal linearlayout requires to handle negative margins in its children. + * In that case, we can't use excessSpace because LinearLayout negative margin handling for + * excess space and WRAP_CONTENT is different. + */ + private boolean requiresNegativeMarginHandlingForHorizontalLinearLayout() { + if (getOrientation() == VERTICAL) { + return false; + } + + final List<View> activeChildren = getActiveChildren(); + for (int i = 0; i < activeChildren.size(); i++) { + final View child = activeChildren.get(i); + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + if (lp.leftMargin < 0 || lp.rightMargin < 0) { + return true; + } + } + return false; + } + + /** * @return if the vertical linearlayout requires match_parent children remeasure */ private boolean requiresMatchParentRemeasureForVerticalLinearLayout(int widthMeasureSpec) { @@ -337,94 +364,81 @@ public class NotificationOptimizedLinearLayout extends LinearLayout { */ private void measureVerticalOptimized(@NonNull View weightedChildView, int widthMeasureSpec, int heightMeasureSpec) { - final int widthMode = MeasureSpec.getMode(widthMeasureSpec); - final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int totalLength = 0; int maxWidth = 0; - int usedHeight = 0; - final List<View> activeChildren = getActiveChildren(); - final int activeChildCount = activeChildren.size(); - - final boolean isContentFirstItem = !activeChildren.isEmpty() && activeChildren.get(0) - == weightedChildView; - - final boolean isContentLastItem = !activeChildren.isEmpty() && activeChildren.get( - activeChildCount - 1) == weightedChildView; - - final int horizontalPaddings = getPaddingLeft() + getPaddingRight(); + final int availableHeight = MeasureSpec.getSize(heightMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); - // 1. Measure other child views. - for (int i = 0; i < activeChildCount; i++) { - final View child = activeChildren.get(i); - if (child == weightedChildView) { + // 1. Measure all unweighted children + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child == null || child.getVisibility() == GONE) { continue; } + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); - int requiredVerticalPadding = lp.topMargin + lp.bottomMargin; - if (!isContentFirstItem && i == 0) { - requiredVerticalPadding += getPaddingTop(); - } - if (!isContentLastItem && i == activeChildCount - 1) { - requiredVerticalPadding += getPaddingBottom(); + if (child == weightedChildView) { + // In excessMode, LinearLayout add weighted child top and bottom margins to + // totalLength when their sum is positive. + if (lp.height == 0 && heightMode == MeasureSpec.EXACTLY) { + totalLength = Math.max(totalLength, totalLength + lp.topMargin + + lp.bottomMargin); + } + continue; } - child.measure(ViewGroup.getChildMeasureSpec(widthMeasureSpec, - horizontalPaddings + lp.leftMargin + lp.rightMargin, - child.getLayoutParams().width), - ViewGroup.getChildMeasureSpec(heightMeasureSpec, requiredVerticalPadding, - lp.height)); + measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); + // LinearLayout only adds measured children heights and its top and bottom margins + // to totalLength when their sum is positive. + totalLength = Math.max(totalLength, + totalLength + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); - usedHeight += child.getMeasuredHeight() + requiredVerticalPadding; } - // measure content - final MarginLayoutParams lp = (MarginLayoutParams) weightedChildView.getLayoutParams(); + // Add padding to totalLength that we are going to use for remaining space. + totalLength += mPaddingTop + mPaddingBottom; - int usedSpace = usedHeight + lp.topMargin + lp.bottomMargin; - if (isContentFirstItem) { - usedSpace += getPaddingTop(); - } - if (isContentLastItem) { - usedSpace += getPaddingBottom(); + // 2. generate measure spec for weightedChildView. + final MarginLayoutParams lp = (MarginLayoutParams) weightedChildView.getLayoutParams(); + // height should be AT_MOST for non EXACT cases. + final int childHeightMeasureMode = + heightMode == MeasureSpec.EXACTLY ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST; + final int childHeightMeasureSpec; + + // In excess mode, LinearLayout measures weighted children with remaining space. Otherwise, + // it is measured with remaining space just like other children. + if (lp.height == 0 && heightMode == MeasureSpec.EXACTLY) { + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( + Math.max(0, availableHeight - totalLength), childHeightMeasureMode); + } else { + final int usedHeight = lp.topMargin + lp.bottomMargin + totalLength; + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( + Math.max(0, availableHeight - usedHeight), childHeightMeasureMode); } + final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, + mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin, lp.width); - final int availableWidth = MeasureSpec.getSize(widthMeasureSpec); - final int availableHeight = MeasureSpec.getSize(heightMeasureSpec); - - final int childWidthMeasureSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, - horizontalPaddings + lp.leftMargin + lp.rightMargin, lp.width); + // 3. Measure weightedChildView with the remaining space. + weightedChildView.measure(childWidthMeasureSpec, childHeightMeasureSpec); - // 2. Calculate remaining height for weightedChildView. - final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( - Math.max(0, availableHeight - usedSpace), MeasureSpec.AT_MOST); + totalLength = Math.max(totalLength, + totalLength + weightedChildView.getMeasuredHeight() + lp.topMargin + + lp.bottomMargin); - // 3. Measure weightedChildView with the remaining remaining space. - weightedChildView.measure(childWidthMeasureSpec, childHeightMeasureSpec); maxWidth = Math.max(maxWidth, weightedChildView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); - final int totalUsedHeight = usedSpace + weightedChildView.getMeasuredHeight(); - - final int measuredWidth; - if (widthMode == MeasureSpec.EXACTLY) { - measuredWidth = availableWidth; - } else { - measuredWidth = maxWidth + getPaddingStart() + getPaddingEnd(); - } - - final int measuredHeight; - if (heightMode == MeasureSpec.EXACTLY) { - measuredHeight = availableHeight; - } else { - measuredHeight = totalUsedHeight; - } + // Add padding to width + maxWidth += getPaddingLeft() + getPaddingRight(); - // 4. Set the container size - setMeasuredDimension( - resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth), - widthMeasureSpec), - Math.max(getSuggestedMinimumHeight(), measuredHeight)); + // Resolve final dimensions + final int finalWidth = resolveSizeAndState(Math.max(maxWidth, getSuggestedMinimumWidth()), + widthMeasureSpec, 0); + final int finalHeight = resolveSizeAndState( + Math.max(totalLength, getSuggestedMinimumHeight()), heightMeasureSpec, 0); + setMeasuredDimension(finalWidth, finalHeight); } @NonNull diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index a425bb0e7461..a2ce212a5a53 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3829,6 +3829,7 @@ @hide This is not a third-party API (intended for OEMs and system apps). --> <permission android:name="android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES" android:protectionLevel="signature|installer" /> + <uses-permission android:name="android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES" /> <!-- @SystemApi @hide Allows an application to set a device owner on retail demo devices.--> <permission android:name="android.permission.PROVISION_DEMO_DEVICE" @@ -7033,12 +7034,16 @@ <!-- Allows the holder to read blocked numbers. See {@link android.provider.BlockedNumberContract}. + @SystemApi + @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @hide --> <permission android:name="android.permission.READ_BLOCKED_NUMBERS" android:protectionLevel="signature" /> <!-- Allows the holder to write blocked numbers. See {@link android.provider.BlockedNumberContract}. + @SystemApi + @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @hide --> <permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" android:protectionLevel="signature" /> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 41bc8251694f..3cd18939e2ca 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -3641,6 +3641,18 @@ <p> The default value is 40dp for {@link android.widget.TextView} and {@link android.widget.EditText}, and 0dp for all other views. --> <attr name="handwritingBoundsOffsetBottom" format="dimension" /> + + <!-- Sets whether this view renders sensitive content. --> + <!-- @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") --> + <attr name="contentSensitivity"> + <!-- Let the Android System use its heuristics to determine if the view renders + sensitive content. --> + <enum name="auto" value="0" /> + <!-- This view renders sensitive content. --> + <enum name="sensitive" value="0x1" /> + <!-- This view doesn't render sensitive content. --> + <enum name="notSensitive" value="0x2" /> + </attr> </declare-styleable> <!-- Attributes that can be assigned to a tag for a particular View. --> diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index c14fe572aba1..104b7cd5450b 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -58,6 +58,12 @@ <integer name="auto_data_switch_availability_stability_time_threshold_millis">10000</integer> <java-symbol type="integer" name="auto_data_switch_availability_stability_time_threshold_millis" /> + <!-- Define the bar of considering the RAT and signal strength advantage of a subscription to be + stable in milliseconds, where 0 means immediate switch, and negative milliseconds indicates the + switch base on RAT and signal strength feature is disabled.--> + <integer name="auto_data_switch_performance_stability_time_threshold_millis">120000</integer> + <java-symbol type="integer" name="auto_data_switch_performance_stability_time_threshold_millis" /> + <!-- Define the maximum retry times when a validation for switching failed.--> <integer name="auto_data_switch_validation_max_retry">7</integer> <java-symbol type="integer" name="auto_data_switch_validation_max_retry" /> @@ -82,6 +88,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> @@ -212,6 +225,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 5ee555543387..56743908e281 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -155,6 +155,8 @@ <public name="languageSettingsActivity"/> <!-- @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") --> <public name="useLocalePreferredLineHeightForMinimum"/> + <!-- @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") --> + <public name="contentSensitivity" /> </staging-public-group> <staging-public-group type="id" first-id="0x01bc0000"> diff --git a/core/tests/coretests/src/android/view/InsetsSourceTest.java b/core/tests/coretests/src/android/view/InsetsSourceTest.java index e1bcd4a0727b..936f4d7d5152 100644 --- a/core/tests/coretests/src/android/view/InsetsSourceTest.java +++ b/core/tests/coretests/src/android/view/InsetsSourceTest.java @@ -279,5 +279,204 @@ public class InsetsSourceTest { } } + @Test + public void testCalculateBoundingRects_noBoundingRects_createsSingleRect() { + mSource.setFrame(new Rect(0, 0, 1000, 100)); + mSource.setBoundingRects(null); + + final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 1000, 1000), false); + + assertEquals(1, rects.length); + assertEquals(new Rect(0, 0, 1000, 100), rects[0]); + } + + @Test + public void testCalculateBoundingRects_noBoundingRectsAndLargerFrame_singleRectFitsRelFrame() { + mSource.setFrame(new Rect(0, 0, 1000, 100)); + mSource.setBoundingRects(null); + + final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 500, 1000), false); + + assertEquals(1, rects.length); + assertEquals(new Rect(0, 0, 500, 100), rects[0]); + } + + @Test + public void testCalculateBoundingRects_frameAtOrigin_resultRelativeToRelFrame() { + mSource.setFrame(new Rect(0, 0, 1000, 100)); + mSource.setBoundingRects(new Rect[]{ + new Rect(0, 0, 300, 100), + new Rect(800, 0, 1000, 100), + }); + + final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 1000, 1000), false); + + assertEquals(2, rects.length); + assertEquals(new Rect(0, 0, 300, 100), rects[0]); + assertEquals(new Rect(800, 0, 1000, 100), rects[1]); + } + + @Test + public void testCalculateBoundingRects_notAtOrigin_resultRelativeToRelFrame() { + mSource.setFrame(new Rect(100, 100, 1100, 200)); + mSource.setBoundingRects(new Rect[]{ + new Rect(0, 0, 300, 100), // 300x100, aligned left + new Rect(800, 0, 1000, 100), // 200x100, aligned right + }); + + final Rect[] rects = mSource.calculateBoundingRects(new Rect(100, 100, 1100, 1100), false); + + assertEquals(2, rects.length); + assertEquals(new Rect(0, 0, 300, 100), rects[0]); + assertEquals(new Rect(800, 0, 1000, 100), rects[1]); + } + + @Test + public void testCalculateBoundingRects_boundingRectFullyInsideFrameInWindow() { + mSource.setFrame(new Rect(0, 0, 1000, 100)); + mSource.setBoundingRects(new Rect[]{ + new Rect(100, 0, 400, 100), // Inside |frame| and |relativeFrame|. + }); + + final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 500, 100), false); + + assertEquals(1, rects.length); + assertEquals(new Rect(100, 0, 400, 100), rects[0]); + } + + @Test + public void testCalculateBoundingRects_boundingRectOutsideFrameInWindow_dropped() { + mSource.setFrame(new Rect(0, 0, 1000, 100)); + mSource.setBoundingRects(new Rect[]{ + new Rect(700, 0, 1000, 100), // Inside |frame|, but outside |relativeFrame|. + }); + + final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 500, 100), false); + + assertEquals(0, rects.length); + } + + @Test + public void testCalculateBoundingRects_boundingRectPartlyOutsideFrameInWindow_cropped() { + mSource.setFrame(new Rect(0, 0, 1000, 100)); + mSource.setBoundingRects(new Rect[]{ + new Rect(400, 0, 600, 100), // Inside |frame|, and only half inside |relativeFrame|. + }); + + final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 500, 100), false); + + assertEquals(1, rects.length); + assertEquals(new Rect(400, 0, 500, 100), rects[0]); + } + + @Test + public void testCalculateBoundingRects_framesNotAtOrigin_resultRelativeToWindowFrame() { + mSource.setFrame(new Rect(100, 100, 1100, 200)); + mSource.setBoundingRects(new Rect[]{ + new Rect(0, 0, 300, 100), // 300x100 aligned to left. + new Rect(800, 0, 1000, 100) // 200x100 align to right. + }); + + final Rect[] rects = mSource.calculateBoundingRects(new Rect(100, 100, 1100, 1100), false); + + assertEquals(2, rects.length); + assertEquals(new Rect(0, 0, 300, 100), rects[0]); + assertEquals(new Rect(800, 0, 1000, 100), rects[1]); + } + + @Test + public void testCalculateBoundingRects_captionBar() { + mCaptionSource.setFrame(new Rect(0, 0, 1000, 100)); + mCaptionSource.setBoundingRects(new Rect[]{ + new Rect(0, 0, 200, 100), // 200x100, aligned left. + new Rect(800, 0, 1000, 100) // 200x100, aligned right. + }); + + final Rect[] rects = mCaptionSource.calculateBoundingRects( + new Rect(0, 0, 1000, 1000), false); + + assertEquals(2, rects.length); + assertEquals(new Rect(0, 0, 200, 100), rects[0]); + assertEquals(new Rect(800, 0, 1000, 100), rects[1]); + } + + @Test + public void testCalculateBoundingRects_captionBarFrameMisaligned_rectsFixedToTop() { + mCaptionSource.setFrame(new Rect(500, 500, 1500, 600)); + mCaptionSource.setBoundingRects(new Rect[]{ + new Rect(0, 0, 100, 100), // 100x100, aligned to left/top of frame + }); + + final Rect[] rects = mCaptionSource.calculateBoundingRects( + new Rect(495, 495, 1500, 1500), false); + + assertEquals(1, rects.length); + // rect should be aligned to the top of relative frame, as if the caption frame had been + // corrected to be aligned at the top. + assertEquals(new Rect(0, 0, 100, 100), rects[0]); + } + + @Test + public void testCalculateBoundingRects_imeCaptionBarFrameMisaligned_rectsFixedToBottom() { + mImeCaptionSource.setFrame(new Rect(500, 1400, 1500, 1500)); + mImeCaptionSource.setBoundingRects(new Rect[]{ + new Rect(0, 0, 100, 100), // 100x100, aligned to left/top of frame + }); + + final Rect[] rects = mImeCaptionSource.calculateBoundingRects( + new Rect(495, 495, 1500, 1500), false); + + assertEquals(1, rects.length); + // rect should be aligned to the bottom of relative frame, as if the ime caption frame had + // been corrected to be aligned at the top. + assertEquals(new Rect(0, 905, 100, 1005), rects[0]); + } + + @Test + public void testCalculateBoundingRects_imeCaptionBar() { + mImeCaptionSource.setFrame(new Rect(0, 900, 1000, 1000)); // Frame at the bottom. + mImeCaptionSource.setBoundingRects(new Rect[]{ + new Rect(0, 0, 200, 100), // 200x100, aligned left. + }); + + final Rect[] rects = mImeCaptionSource.calculateBoundingRects( + new Rect(0, 0, 1000, 1000), false); + + assertEquals(1, rects.length); + assertEquals(new Rect(0, 900, 200, 1000), rects[0]); + } + + @Test + public void testCalculateBoundingRects_invisible() { + mSource.setFrame(new Rect(0, 0, 1000, 100)); + mSource.setBoundingRects(new Rect[]{ + new Rect(0, 0, 300, 100), + new Rect(800, 0, 1000, 100), + }); + mSource.setVisible(false); + + final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 1000, 1000), + false /* ignoreVisibility */); + + assertEquals(0, rects.length); + } + + @Test + public void testCalculateBoundingRects_ignoreVisibility() { + mSource.setFrame(new Rect(0, 0, 1000, 100)); + mSource.setBoundingRects(new Rect[]{ + new Rect(0, 0, 300, 100), + new Rect(800, 0, 1000, 100), + }); + mSource.setVisible(false); + + final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 1000, 1000), + true /* ignoreVisibility */); + + assertEquals(2, rects.length); + assertEquals(new Rect(0, 0, 300, 100), rects[0]); + assertEquals(new Rect(800, 0, 1000, 100), rects[1]); + } + // Parcel and equals already tested via InsetsStateTest } diff --git a/core/tests/coretests/src/android/view/InsetsStateTest.java b/core/tests/coretests/src/android/view/InsetsStateTest.java index 672875a19f9f..16bd20a42b2a 100644 --- a/core/tests/coretests/src/android/view/InsetsStateTest.java +++ b/core/tests/coretests/src/android/view/InsetsStateTest.java @@ -63,6 +63,8 @@ import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.List; + /** * Tests for {@link InsetsState}. * @@ -88,6 +90,8 @@ public class InsetsStateTest { null /* owner */, 1 /* index */, navigationBars()); private static final int ID_BOTTOM_GESTURES = InsetsSource.createId( null /* owner */, 0 /* index */, systemGestures()); + private static final int ID_EXTRA_CAPTION_BAR = InsetsSource.createId( + null /* owner */, 2 /* index */, captionBar()); private final InsetsState mState = new InsetsState(); private final InsetsState mState2 = new InsetsState(); @@ -420,9 +424,11 @@ public class InsetsStateTest { public void testEquals_visibility() { mState.getOrCreateSource(ID_IME, ime()) .setFrame(new Rect(0, 0, 100, 100)) + .setBoundingRects(new Rect[]{ new Rect(0, 0, 10, 10) }) .setVisible(true); mState2.getOrCreateSource(ID_IME, ime()) - .setFrame(new Rect(0, 0, 100, 100)); + .setFrame(new Rect(0, 0, 100, 100)) + .setBoundingRects(new Rect[]{ new Rect(0, 0, 10, 10) }); assertNotEqualsAndHashCode(); } @@ -441,6 +447,30 @@ public class InsetsStateTest { } @Test + public void testEquals_sameBoundingRects() { + mState.getOrCreateSource(ID_CAPTION_BAR, captionBar()) + .setFrame(new Rect(0, 0, 100, 100)) + .setBoundingRects(new Rect[]{ new Rect(0, 0, 10, 10) }) + .setVisible(true); + mState2.getOrCreateSource(ID_CAPTION_BAR, captionBar()) + .setFrame(new Rect(0, 0, 100, 100)) + .setBoundingRects(new Rect[]{ new Rect(0, 0, 10, 10) }); + assertEqualsAndHashCode(); + } + + @Test + public void testEquals_differentBoundingRects() { + mState.getOrCreateSource(ID_CAPTION_BAR, captionBar()) + .setFrame(new Rect(0, 0, 100, 100)) + .setBoundingRects(new Rect[]{ new Rect(0, 0, 10, 10) }) + .setVisible(true); + mState2.getOrCreateSource(ID_CAPTION_BAR, captionBar()) + .setFrame(new Rect(0, 0, 100, 100)) + .setBoundingRects(new Rect[]{ new Rect(0, 0, 20, 20) }); + assertNotEqualsAndHashCode(); + } + + @Test public void testEquals_samePrivacyIndicator() { Rect one = new Rect(0, 1, 2, 3); Rect two = new Rect(4, 5, 6, 7); @@ -734,4 +764,94 @@ public class InsetsStateTest { assertEquals(1, onIdNotFoundInState2Called[0]); // 1000. assertEquals(1, onFinishCalled[0]); } + + @Test + public void testCalculateBoundingRects() { + mState.getOrCreateSource(ID_STATUS_BAR, statusBars()) + .setFrame(new Rect(0, 0, 1000, 100)) + .setBoundingRects(null) + .setVisible(true); + mState.getOrCreateSource(ID_CAPTION_BAR, captionBar()) + .setFrame(new Rect(0, 0, 1000, 100)) + .setBoundingRects(new Rect[]{ + new Rect(0, 0, 200, 100), + new Rect(800, 0, 1000, 100) + }) + .setVisible(true); + SparseIntArray typeSideMap = new SparseIntArray(); + + WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 1000, 1000), null, false, + SOFT_INPUT_ADJUST_RESIZE, 0, 0, TYPE_APPLICATION, ACTIVITY_TYPE_UNDEFINED, + typeSideMap); + + assertEquals( + List.of(new Rect(0, 0, 1000, 100)), + insets.getBoundingRects(Type.statusBars()) + ); + assertEquals( + List.of( + new Rect(0, 0, 200, 100), + new Rect(800, 0, 1000, 100) + ), + insets.getBoundingRects(Type.captionBar()) + ); + } + + @Test + public void testCalculateBoundingRects_multipleSourcesOfSameType_concatenated() { + mState.getOrCreateSource(ID_CAPTION_BAR, captionBar()) + .setFrame(new Rect(0, 0, 1000, 100)) + .setBoundingRects(new Rect[]{new Rect(0, 0, 200, 100)}) + .setVisible(true); + mState.getOrCreateSource(ID_EXTRA_CAPTION_BAR, captionBar()) + .setFrame(new Rect(0, 0, 1000, 100)) + .setBoundingRects(new Rect[]{new Rect(800, 0, 1000, 100)}) + .setVisible(true); + SparseIntArray typeSideMap = new SparseIntArray(); + + WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 1000, 1000), null, false, + SOFT_INPUT_ADJUST_RESIZE, 0, 0, TYPE_APPLICATION, ACTIVITY_TYPE_UNDEFINED, + typeSideMap); + + final List<Rect> expected = List.of( + new Rect(0, 0, 200, 100), + new Rect(800, 0, 1000, 100) + ); + final List<Rect> actual = insets.getBoundingRects(captionBar()); + assertEquals(expected.size(), actual.size()); + + // Order does not matter. + assertTrue(actual.containsAll(expected)); + } + + @Test + public void testCalculateBoundingRects_captionBar_reportedAsSysGesturesAndTappableElement() { + mState.getOrCreateSource(ID_CAPTION_BAR, captionBar()) + .setFrame(new Rect(0, 0, 1000, 100)) + .setBoundingRects(new Rect[]{new Rect(0, 0, 200, 100)}) + .setVisible(true); + SparseIntArray typeSideMap = new SparseIntArray(); + + WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 1000, 1000), null, false, + SOFT_INPUT_ADJUST_RESIZE, 0, 0, TYPE_APPLICATION, ACTIVITY_TYPE_UNDEFINED, + typeSideMap); + + assertEquals( + List.of(new Rect(0, 0, 200, 100)), + insets.getBoundingRects(Type.captionBar()) + ); + assertEquals( + List.of(new Rect(0, 0, 200, 100)), + insets.getBoundingRects(Type.systemGestures()) + ); + assertEquals( + List.of(new Rect(0, 0, 200, 100)), + insets.getBoundingRects(Type.mandatorySystemGestures()) + ); + assertEquals( + List.of(new Rect(0, 0, 200, 100)), + insets.getBoundingRects(Type.tappableElement()) + ); + + } } diff --git a/core/tests/coretests/src/android/view/WindowInsetsTest.java b/core/tests/coretests/src/android/view/WindowInsetsTest.java index 69abf5f3204f..ab4543cb8001 100644 --- a/core/tests/coretests/src/android/view/WindowInsetsTest.java +++ b/core/tests/coretests/src/android/view/WindowInsetsTest.java @@ -41,14 +41,14 @@ public class WindowInsetsTest { public void systemWindowInsets_afterConsuming_isConsumed() { assertTrue(new WindowInsets(WindowInsets.createCompatTypeMap(new Rect(1, 2, 3, 4)), null, null, false, 0, 0, null, null, null, null, - WindowInsets.Type.systemBars(), false) + WindowInsets.Type.systemBars(), false, null, null, 0, 0) .consumeSystemWindowInsets().isConsumed()); } @Test public void multiNullConstructor_isConsumed() { assertTrue(new WindowInsets(null, null, null, false, 0, 0, null, null, null, null, - WindowInsets.Type.systemBars(), false).isConsumed()); + WindowInsets.Type.systemBars(), false, null, null, 0, 0).isConsumed()); } @Test @@ -65,7 +65,7 @@ public class WindowInsetsTest { WindowInsets.assignCompatInsets(insets, new Rect(0, 0, 0, 0)); WindowInsets windowInsets = new WindowInsets(insets, maxInsets, visible, false, 0, 0, null, null, null, DisplayShape.NONE, systemBars(), - true /* compatIgnoreVisibility */); + true /* compatIgnoreVisibility */, null, null, 0, 0); assertEquals(Insets.of(0, 10, 0, 0), windowInsets.getSystemWindowInsets()); } } 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/widget/ActionBarOverlayLayoutTest.java b/core/tests/coretests/src/com/android/internal/widget/ActionBarOverlayLayoutTest.java index 84dd2740e8b7..8d66cfc7a3dc 100644 --- a/core/tests/coretests/src/com/android/internal/widget/ActionBarOverlayLayoutTest.java +++ b/core/tests/coretests/src/com/android/internal/widget/ActionBarOverlayLayoutTest.java @@ -169,7 +169,8 @@ public class ActionBarOverlayLayoutTest { private WindowInsets insetsWith(Insets content, DisplayCutout cutout) { return new WindowInsets(WindowInsets.createCompatTypeMap(content.toRect()), null, null, - false, 0, 0, cutout, null, null, null, WindowInsets.Type.systemBars(), false); + false, 0, 0, cutout, null, null, null, WindowInsets.Type.systemBars(), false, + null, null, 0, 0); } private ViewGroup createViewGroupWithId(int id) { diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationOptimizedLinearLayoutComparisonTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationOptimizedLinearLayoutComparisonTest.java index 08333ecd99a3..bf9221a27e74 100644 --- a/core/tests/coretests/src/com/android/internal/widget/NotificationOptimizedLinearLayoutComparisonTest.java +++ b/core/tests/coretests/src/com/android/internal/widget/NotificationOptimizedLinearLayoutComparisonTest.java @@ -31,6 +31,7 @@ import android.view.View; import android.view.View.MeasureSpec; import android.view.ViewGroup; import android.widget.LinearLayout; +import android.widget.TextView; import android.widget.flags.Flags; import androidx.test.InstrumentationRegistry; @@ -73,7 +74,7 @@ public class NotificationOptimizedLinearLayoutComparisonTest { private static final int[] LAYOUT_PARAMS = {MATCH_PARENT, WRAP_CONTENT, 0, 50}; private static final int[] CHILD_WEIGHTS = {0, 1}; - + private static final int[] CHILD_MARGINS = {0, 10, -10}; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -84,35 +85,96 @@ public class NotificationOptimizedLinearLayoutComparisonTest { mContext = InstrumentationRegistry.getTargetContext(); } + @Test public void test() throws Throwable { + final List<View> controlChildren = + new ArrayList<>(); + final List<View> testChildren = + new ArrayList<>(); + + final View controlChild1 = buildChildView(); + final View controlChild2 = buildChildView(); + controlChildren.add(controlChild1); + controlChildren.add(controlChild2); + + final View testChild1 = buildChildView(); + final View testChild2 = buildChildView(); + testChildren.add(testChild1); + testChildren.add(testChild2); + + final LinearLayout controlContainer = buildLayout(false, controlChildren); + + final LinearLayout testContainer = buildLayout(true, testChildren); + + final LinearLayout.LayoutParams firstChildLayoutParams = new LinearLayout.LayoutParams(0, + 0); + final LinearLayout.LayoutParams secondChildLayoutParams = new LinearLayout.LayoutParams(0, + 0); + controlChild1.setLayoutParams(firstChildLayoutParams); + controlChild2.setLayoutParams(secondChildLayoutParams); + testChild1.setLayoutParams(firstChildLayoutParams); + testChild2.setLayoutParams(secondChildLayoutParams); + for (int orientation : ORIENTATIONS) { - for (int widthSpec : MEASURE_SPECS) { - for (int heightSpec : MEASURE_SPECS) { - for (int firstChildGravity : GRAVITIES) { - for (int secondChildGravity : GRAVITIES) { - for (int firstChildLayoutWidth : LAYOUT_PARAMS) { - for (int firstChildLayoutHeight : LAYOUT_PARAMS) { - for (int secondChildLayoutWidth : LAYOUT_PARAMS) { - for (int secondChildLayoutHeight : LAYOUT_PARAMS) { + controlContainer.setOrientation(orientation); + testContainer.setOrientation(orientation); + + for (int firstChildLayoutWidth : LAYOUT_PARAMS) { + firstChildLayoutParams.width = firstChildLayoutWidth; + for (int firstChildLayoutHeight : LAYOUT_PARAMS) { + firstChildLayoutParams.height = firstChildLayoutHeight; + + for (int secondChildLayoutWidth : LAYOUT_PARAMS) { + secondChildLayoutParams.width = secondChildLayoutWidth; + for (int secondChildLayoutHeight : LAYOUT_PARAMS) { + secondChildLayoutParams.height = secondChildLayoutHeight; + + for (int firstChildMargin : CHILD_MARGINS) { + firstChildLayoutParams.setMargins(firstChildMargin, + firstChildMargin, firstChildMargin, firstChildMargin); + for (int secondChildMargin : CHILD_MARGINS) { + secondChildLayoutParams.setMargins(secondChildMargin, + secondChildMargin, secondChildMargin, + secondChildMargin); + + for (int firstChildGravity : GRAVITIES) { + firstChildLayoutParams.gravity = firstChildGravity; + for (int secondChildGravity : GRAVITIES) { + secondChildLayoutParams.gravity = secondChildGravity; + for (int firstChildWeight : CHILD_WEIGHTS) { + firstChildLayoutParams.weight = firstChildWeight; for (int secondChildWeight : CHILD_WEIGHTS) { - executeTest(/*testSpec =*/createTestSpec( - orientation, - widthSpec, heightSpec, - firstChildLayoutWidth, - firstChildLayoutHeight, - secondChildLayoutWidth, - secondChildLayoutHeight, - firstChildGravity, - secondChildGravity, - firstChildWeight, - secondChildWeight)); + secondChildLayoutParams.weight = + secondChildWeight; + + for (int widthSpec : MEASURE_SPECS) { + for (int heightSpec : MEASURE_SPECS) { + executeTest(controlContainer, + testContainer, + createTestSpec( + orientation, + widthSpec, heightSpec, + firstChildLayoutWidth, + firstChildLayoutHeight, + secondChildLayoutWidth, + secondChildLayoutHeight, + firstChildGravity, + secondChildGravity, + firstChildWeight, + secondChildWeight, + firstChildMargin, + secondChildMargin) + ); + } + } } } } } } + } } } @@ -121,47 +183,8 @@ public class NotificationOptimizedLinearLayoutComparisonTest { } } - private void executeTest(TestSpec testSpec) { - // GIVEN - final List<View> controlChildren = - new ArrayList<>(); - final List<View> testChildren = - new ArrayList<>(); - - controlChildren.add( - buildChildView( - testSpec.mFirstChildLayoutWidth, - testSpec.mFirstChildLayoutHeight, - testSpec.mFirstChildGravity, - testSpec.mFirstChildWeight)); - controlChildren.add( - buildChildView( - testSpec.mSecondChildLayoutWidth, - testSpec.mSecondChildLayoutHeight, - testSpec.mSecondChildGravity, - testSpec.mSecondChildWeight)); - - testChildren.add( - buildChildView( - testSpec.mFirstChildLayoutWidth, - testSpec.mFirstChildLayoutHeight, - testSpec.mFirstChildGravity, - testSpec.mFirstChildWeight)); - testChildren.add( - buildChildView( - testSpec.mSecondChildLayoutWidth, - testSpec.mSecondChildLayoutHeight, - testSpec.mSecondChildGravity, - testSpec.mSecondChildWeight)); - - final LinearLayout controlContainer = buildLayout(false, - testSpec.mOrientation, - controlChildren); - - final LinearLayout testContainer = buildLayout(true, - testSpec.mOrientation, - testChildren); - + private void executeTest(LinearLayout controlContainer, LinearLayout testContainer, + TestSpec testSpec) { // WHEN controlContainer.measure(testSpec.mWidthSpec, testSpec.mHeightSpec); testContainer.measure(testSpec.mWidthSpec, testSpec.mHeightSpec); @@ -171,6 +194,7 @@ public class NotificationOptimizedLinearLayoutComparisonTest { assertLayoutsEqual("Test Case:" + testSpec, controlContainer, testContainer); } + private static class TestSpec { private final int mOrientation; private final int mWidthSpec; @@ -183,6 +207,8 @@ public class NotificationOptimizedLinearLayoutComparisonTest { private final int mSecondChildGravity; private final int mFirstChildWeight; private final int mSecondChildWeight; + private final int mFirstChildMargin; + private final int mSecondChildMargin; TestSpec( int orientation, @@ -195,7 +221,9 @@ public class NotificationOptimizedLinearLayoutComparisonTest { int firstChildGravity, int secondChildGravity, int firstChildWeight, - int secondChildWeight) { + int secondChildWeight, + int firstChildMargin, + int secondChildMargin) { mOrientation = orientation; mWidthSpec = widthSpec; mHeightSpec = heightSpec; @@ -207,6 +235,8 @@ public class NotificationOptimizedLinearLayoutComparisonTest { mSecondChildGravity = secondChildGravity; mFirstChildWeight = firstChildWeight; mSecondChildWeight = secondChildWeight; + mFirstChildMargin = firstChildMargin; + mSecondChildMargin = secondChildMargin; } @Override @@ -223,6 +253,8 @@ public class NotificationOptimizedLinearLayoutComparisonTest { + ", mSecondChildGravity=" + mSecondChildGravity + ", mFirstChildWeight=" + mFirstChildWeight + ", mSecondChildWeight=" + mSecondChildWeight + + ", mFirstChildMargin=" + mFirstChildMargin + + ", mSecondChildMargin=" + mSecondChildMargin + '}'; } @@ -246,15 +278,13 @@ public class NotificationOptimizedLinearLayoutComparisonTest { } } - private LinearLayout buildLayout(boolean isNotificationOptimized, - @LinearLayout.OrientationMode int orientation, List<View> children) { + private LinearLayout buildLayout(boolean isNotificationOptimized, List<View> children) { final LinearLayout linearLayout; if (isNotificationOptimized) { linearLayout = new NotificationOptimizedLinearLayout(mContext); } else { linearLayout = new LinearLayout(mContext); } - linearLayout.setOrientation(orientation); for (int i = 0; i < children.size(); i++) { linearLayout.addView(children.get(i)); } @@ -262,7 +292,8 @@ public class NotificationOptimizedLinearLayoutComparisonTest { } private void assertLayoutsEqual(String testCase, View controlView, View testView) { - mExpect.withMessage("MeasuredWidths are not equal. Test Case:" + testCase) + mExpect.withMessage( + "MeasuredWidths are not equal. Test Case:" + testCase) .that(testView.getMeasuredWidth()).isEqualTo(controlView.getMeasuredWidth()); mExpect.withMessage("MeasuredHeights are not equal. Test Case:" + testCase) .that(testView.getMeasuredHeight()).isEqualTo(controlView.getMeasuredHeight()); @@ -286,23 +317,12 @@ public class NotificationOptimizedLinearLayoutComparisonTest { } } - private static class TestView extends View { - TestView(Context context) { - super(context); - } - - @Override - public int getBaseline() { - return 5; - } - } - - private TestSpec createTestSpec(int orientation, int widthSpec, int heightSpec, int firstChildLayoutWidth, int firstChildLayoutHeight, int secondChildLayoutWidth, int secondChildLayoutHeight, int firstChildGravity, int secondChildGravity, - int firstChildWeight, int secondChildWeight) { + int firstChildWeight, int secondChildWeight, int firstChildMargin, + int secondChildMargin) { return new TestSpec( orientation, @@ -314,16 +334,16 @@ public class NotificationOptimizedLinearLayoutComparisonTest { firstChildGravity, secondChildGravity, firstChildWeight, - secondChildWeight); + secondChildWeight, + firstChildMargin, + secondChildMargin); } - private View buildChildView(int childLayoutWidth, int childLayoutHeight, - int childGravity, int childWeight) { - final View childView = new TestView(mContext); - // Set desired size using LayoutParams - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(childLayoutWidth, - childLayoutHeight, childWeight); - params.gravity = childGravity; + private View buildChildView() { + final View childView = new TextView(mContext); + // this is initial value. We are going to mutate this layout params during the test. + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(MATCH_PARENT, + WRAP_CONTENT); childView.setLayoutParams(params); return childView; } diff --git a/core/tests/devicestatetests/Android.bp b/core/tests/devicestatetests/Android.bp index 7748de57c2bc..60848b31eaec 100644 --- a/core/tests/devicestatetests/Android.bp +++ b/core/tests/devicestatetests/Android.bp @@ -29,6 +29,8 @@ android_test { "androidx.test.rules", "frameworks-base-testutils", "mockito-target-minus-junit4", + "platform-test-annotations", + "testng", ], libs: ["android.test.runner"], platform_apis: true, diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateTest.java b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateTest.java index d54524e94139..396d403a1ada 100644 --- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateTest.java +++ b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server.devicestate; +package android.hardware.devicestate; import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE; import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE; @@ -25,18 +25,17 @@ import static org.testng.Assert.assertThrows; import android.platform.test.annotations.Presubmit; -import androidx.test.runner.AndroidJUnit4; - import org.junit.Test; import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; /** - * Unit tests for {@link DeviceState}. + * Unit tests for {@link android.hardware.devicestate.DeviceState}. * <p/> * Run with <code>atest DeviceStateTest</code>. */ @Presubmit -@RunWith(AndroidJUnit4.class) +@RunWith(JUnit4.class) public final class DeviceStateTest { @Test public void testConstruct() { diff --git a/core/tests/utiltests/src/com/android/internal/util/FastXmlSerializerTest.java b/core/tests/utiltests/src/com/android/internal/util/FastXmlSerializerTest.java index 7723d589a723..f91172df4f74 100644 --- a/core/tests/utiltests/src/com/android/internal/util/FastXmlSerializerTest.java +++ b/core/tests/utiltests/src/com/android/internal/util/FastXmlSerializerTest.java @@ -45,7 +45,6 @@ import java.nio.charset.StandardCharsets; */ @SmallTest @RunWith(AndroidJUnit4.class) -@IgnoreUnderRavenwood(blockedBy = Xml.class) public class FastXmlSerializerTest { private static final String TAG = "FastXmlSerializerTest"; @@ -146,6 +145,7 @@ public class FastXmlSerializerTest { @Test @LargeTest + @IgnoreUnderRavenwood(reason = "Long test runtime") public void testAllCharacters() throws Exception { boolean ok = true; for (int i = 0; i < 0xffff; i++) { diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index d8713f7a13f8..fdb520835810 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -575,6 +575,9 @@ 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 BlockedNumberContractTest --> + <permission name="android.permission.WRITE_BLOCKED_NUMBERS" /> + <permission name="android.permission.READ_BLOCKED_NUMBERS" /> <!-- Permission required for CTS test - PackageManagerTest --> <permission name="android.permission.DOMAIN_VERIFICATION_AGENT"/> </privapp-permissions> diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt index ea7c6edd4b56..5825bbfbfefa 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt @@ -165,6 +165,25 @@ class BubblePositionerTest { } @Test + fun testGetRestingPosition_afterBoundsChange() { + positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, + windowBounds = Rect(0, 0, 2000, 1600))) + + // Set the resting position to the right side + var allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + val restingPosition = PointF(allowableStackRegion.right, allowableStackRegion.centerY()) + positioner.restingPosition = restingPosition + + // Now make the device smaller + positioner.update(defaultDeviceConfig.copy(isLargeScreen = false, + windowBounds = Rect(0, 0, 1000, 1600))) + + // Check the resting position is on the correct side + allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) + assertThat(positioner.restingPosition.x).isEqualTo(allowableStackRegion.right) + } + + @Test fun testHasUserModifiedDefaultPosition_false() { positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index c03b6f805cf7..cda29c95281d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -120,6 +120,13 @@ public class BubblePositioner { @VisibleForTesting public void updateInternal(int rotation, Insets insets, Rect bounds) { + BubbleStackView.RelativeStackPosition prevStackPosition = null; + if (mRestingStackPosition != null && mScreenRect != null && !mScreenRect.equals(bounds)) { + // Save the resting position as a relative position with the previous bounds, at the + // end of the update we'll restore it based on the new bounds. + prevStackPosition = new BubbleStackView.RelativeStackPosition(getRestingPosition(), + getAllowableStackPositionRegion(1)); + } mRotation = rotation; mInsets = insets; @@ -182,6 +189,12 @@ public class BubblePositioner { R.dimen.bubbles_flyout_min_width_large_screen); mMaxBubbles = calculateMaxBubbles(); + + if (prevStackPosition != null) { + // Get the new resting position based on the updated values + mRestingStackPosition = prevStackPosition.getAbsolutePositionInRegion( + getAllowableStackPositionRegion(1)); + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index ead5ad23c8f3..bd9d89c9892e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -64,6 +64,7 @@ import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler; import com.android.wm.shell.draganddrop.DragAndDropController; +import com.android.wm.shell.draganddrop.UnhandledDragController; import com.android.wm.shell.freeform.FreeformComponents; import com.android.wm.shell.freeform.FreeformTaskListener; import com.android.wm.shell.freeform.FreeformTaskTransitionHandler; @@ -558,6 +559,14 @@ public abstract class WMShellModule { @WMSingleton @Provides + static UnhandledDragController provideUnhandledDragController( + IWindowManager wmService, + @ShellMainThread ShellExecutor mainExecutor) { + return new UnhandledDragController(wmService, mainExecutor); + } + + @WMSingleton + @Provides static DragAndDropController provideDragAndDropController(Context context, ShellInit shellInit, ShellController shellController, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index e8728498ad64..42c8d7417611 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -70,8 +70,8 @@ import com.android.wm.shell.sysui.ShellSharedConstants import com.android.wm.shell.transition.OneShotRemoteHandler import com.android.wm.shell.transition.Transitions import com.android.wm.shell.util.KtProtoLog -import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration import com.android.wm.shell.windowdecor.MoveToDesktopAnimator +import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener import java.io.PrintWriter import java.util.concurrent.Executor import java.util.function.Consumer @@ -175,6 +175,12 @@ class DesktopTasksController( ) } + fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) { + toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) + enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) + dragToDesktopTransitionHandler.setOnTaskResizeAnimatorListener(listener) + } + /** Setter needed to avoid cyclic dependency. */ fun setSplitScreenController(controller: SplitScreenController) { splitScreenController = controller @@ -236,12 +242,11 @@ class DesktopTasksController( /** Move a task with given `taskId` to desktop */ fun moveToDesktop( - decor: DesktopModeWindowDecoration, taskId: Int, wct: WindowContainerTransaction = WindowContainerTransaction() ) { shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { - task -> moveToDesktop(decor, task, wct) + task -> moveToDesktop(task, wct) } } @@ -283,7 +288,6 @@ class DesktopTasksController( * Move a task to desktop */ fun moveToDesktop( - decor: DesktopModeWindowDecoration, task: RunningTaskInfo, wct: WindowContainerTransaction = WindowContainerTransaction() ) { @@ -298,7 +302,7 @@ class DesktopTasksController( addMoveToDesktopChanges(wct, task) if (Transitions.ENABLE_SHELL_TRANSITIONS) { - enterDesktopTaskTransitionHandler.moveToDesktop(wct, decor) + enterDesktopTaskTransitionHandler.moveToDesktop(wct) } else { shellTaskOrganizer.applyTransaction(wct) } @@ -311,7 +315,6 @@ class DesktopTasksController( fun startDragToDesktop( taskInfo: RunningTaskInfo, dragToDesktopValueAnimator: MoveToDesktopAnimator, - windowDecor: DesktopModeWindowDecoration ) { KtProtoLog.v( WM_SHELL_DESKTOP_MODE, @@ -320,8 +323,7 @@ class DesktopTasksController( ) dragToDesktopTransitionHandler.startDragToDesktopTransition( taskInfo.taskId, - dragToDesktopValueAnimator, - windowDecor + dragToDesktopValueAnimator ) } @@ -522,7 +524,7 @@ class DesktopTasksController( } /** Quick-resizes a desktop task, toggling between the stable bounds and the default bounds. */ - fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo, windowDecor: DesktopModeWindowDecoration) { + fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo) { val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return val stableBounds = Rect() @@ -543,11 +545,7 @@ class DesktopTasksController( val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds) if (Transitions.ENABLE_SHELL_TRANSITIONS) { - toggleResizeDesktopTaskTransitionHandler.startTransition( - wct, - taskInfo.taskId, - windowDecor - ) + toggleResizeDesktopTaskTransitionHandler.startTransition(wct) } else { shellTaskOrganizer.applyTransaction(wct) } @@ -558,11 +556,7 @@ class DesktopTasksController( * * @param position the portion of the screen (RIGHT or LEFT) we want to snap the task to. */ - fun snapToHalfScreen( - taskInfo: RunningTaskInfo, - windowDecor: DesktopModeWindowDecoration, - position: SnapPosition - ) { + fun snapToHalfScreen(taskInfo: RunningTaskInfo, position: SnapPosition) { val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return val stableBounds = Rect() @@ -592,11 +586,7 @@ class DesktopTasksController( val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds) if (Transitions.ENABLE_SHELL_TRANSITIONS) { - toggleResizeDesktopTaskTransitionHandler.startTransition( - wct, - taskInfo.taskId, - windowDecor - ) + toggleResizeDesktopTaskTransitionHandler.startTransition(wct) } else { shellTaskOrganizer.applyTransaction(wct) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index 39610e3c5c80..af26e2980afe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -34,9 +34,9 @@ import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_END_DRAG import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP import com.android.wm.shell.transition.Transitions.TransitionHandler import com.android.wm.shell.util.KtProtoLog -import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration import com.android.wm.shell.windowdecor.MoveToDesktopAnimator import com.android.wm.shell.windowdecor.MoveToDesktopAnimator.Companion.DRAG_FREEFORM_SCALE +import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener import java.util.function.Supplier /** @@ -69,6 +69,7 @@ class DragToDesktopTransitionHandler( private var dragToDesktopStateListener: DragToDesktopStateListener? = null private var splitScreenController: SplitScreenController? = null private var transitionState: TransitionState? = null + private lateinit var onTaskResizeAnimationListener: OnTaskResizeAnimationListener /** Whether a drag-to-desktop transition is in progress. */ val inProgress: Boolean @@ -84,6 +85,10 @@ class DragToDesktopTransitionHandler( splitScreenController = controller } + fun setOnTaskResizeAnimatorListener(listener: OnTaskResizeAnimationListener) { + onTaskResizeAnimationListener = listener + } + /** * Starts a transition that performs a transient launch of Home so that Home is brought to the * front while still keeping the currently focused task that is being dragged resumed. This @@ -96,7 +101,6 @@ class DragToDesktopTransitionHandler( fun startDragToDesktopTransition( taskId: Int, dragToDesktopAnimator: MoveToDesktopAnimator, - windowDecoration: DesktopModeWindowDecoration ) { if (inProgress) { KtProtoLog.v( @@ -128,14 +132,12 @@ class DragToDesktopTransitionHandler( TransitionState.FromSplit( draggedTaskId = taskId, dragAnimator = dragToDesktopAnimator, - windowDecoration = windowDecoration, startTransitionToken = startTransitionToken ) } else { TransitionState.FromFullscreen( draggedTaskId = taskId, dragAnimator = dragToDesktopAnimator, - windowDecoration = windowDecoration, startTransitionToken = startTransitionToken ) } @@ -405,7 +407,7 @@ class DragToDesktopTransitionHandler( // Accept the merge by applying the merging transaction (applied by #showResizeVeil) // and finish callback. Show the veil and position the task at the first frame before // starting the final animation. - state.windowDecoration.showResizeVeil(t, animStartBounds) + onTaskResizeAnimationListener.onAnimationStart(state.draggedTaskId, t, animStartBounds) finishCallback.onTransitionFinished(null /* wct */) // Because the task surface was scaled down during the drag, we must use the animated @@ -429,11 +431,15 @@ class DragToDesktopTransitionHandler( animBounds.height() ) } - state.windowDecoration.updateResizeVeil(tx, animBounds) + onTaskResizeAnimationListener.onBoundsChange( + state.draggedTaskId, + tx, + animBounds + ) } addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { - state.windowDecoration.hideResizeVeil() + onTaskResizeAnimationListener.onAnimationEnd(state.draggedTaskId) startTransitionFinishCb.onTransitionFinished(null /* null */) clearState() } @@ -576,7 +582,6 @@ class DragToDesktopTransitionHandler( sealed class TransitionState { abstract val draggedTaskId: Int abstract val dragAnimator: MoveToDesktopAnimator - abstract val windowDecoration: DesktopModeWindowDecoration abstract val startTransitionToken: IBinder abstract var startTransitionFinishCb: Transitions.TransitionFinishCallback? abstract var startTransitionFinishTransaction: SurfaceControl.Transaction? @@ -589,7 +594,6 @@ class DragToDesktopTransitionHandler( data class FromFullscreen( override val draggedTaskId: Int, override val dragAnimator: MoveToDesktopAnimator, - override val windowDecoration: DesktopModeWindowDecoration, override val startTransitionToken: IBinder, override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null, override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null, @@ -603,7 +607,6 @@ class DragToDesktopTransitionHandler( data class FromSplit( override val draggedTaskId: Int, override val dragAnimator: MoveToDesktopAnimator, - override val windowDecoration: DesktopModeWindowDecoration, override val startTransitionToken: IBinder, override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null, override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java index 605600f54823..07cf202ddfac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java @@ -38,7 +38,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.wm.shell.transition.Transitions; -import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration; +import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener; import java.util.ArrayList; import java.util.List; @@ -59,8 +59,8 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition public static final int FREEFORM_ANIMATION_DURATION = 336; private final List<IBinder> mPendingTransitionTokens = new ArrayList<>(); - private DesktopModeWindowDecoration mDesktopModeWindowDecoration; + private OnTaskResizeAnimationListener mOnTaskResizeAnimationListener; public EnterDesktopTaskTransitionHandler( Transitions transitions) { this(transitions, SurfaceControl.Transaction::new); @@ -73,14 +73,15 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition mTransactionSupplier = supplier; } + void setOnTaskResizeAnimationListener(OnTaskResizeAnimationListener listener) { + mOnTaskResizeAnimationListener = listener; + } + /** * Starts Transition of type TRANSIT_MOVE_TO_DESKTOP * @param wct WindowContainerTransaction for transition - * @param decor {@link DesktopModeWindowDecoration} of task being animated */ - public void moveToDesktop(@NonNull WindowContainerTransaction wct, - DesktopModeWindowDecoration decor) { - mDesktopModeWindowDecoration = decor; + public void moveToDesktop(@NonNull WindowContainerTransaction wct) { final IBinder token = mTransitions.startTransition(TRANSIT_MOVE_TO_DESKTOP, wct, this); mPendingTransitionTokens.add(token); } @@ -136,33 +137,33 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition @NonNull TransitionInfo.Change change, @NonNull SurfaceControl.Transaction startT, @NonNull Transitions.TransitionFinishCallback finishCallback) { - if (mDesktopModeWindowDecoration == null) { - Slog.e(TAG, "Window Decoration is not available for this transition"); + final SurfaceControl leash = change.getLeash(); + final Rect startBounds = change.getStartAbsBounds(); + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (mOnTaskResizeAnimationListener == null) { + Slog.e(TAG, "onTaskResizeAnimationListener is not available for this transition"); return false; } - final SurfaceControl leash = change.getLeash(); - final Rect startBounds = change.getStartAbsBounds(); - startT.setPosition(leash, startBounds.left, startBounds.right) + startT.setPosition(leash, startBounds.left, startBounds.top) .setWindowCrop(leash, startBounds.width(), startBounds.height()) .show(leash); - mDesktopModeWindowDecoration.showResizeVeil(startT, startBounds); - + mOnTaskResizeAnimationListener.onAnimationStart(taskInfo.taskId, startT, startBounds); final ValueAnimator animator = ValueAnimator.ofObject(new RectEvaluator(), change.getStartAbsBounds(), change.getEndAbsBounds()); animator.setDuration(FREEFORM_ANIMATION_DURATION); SurfaceControl.Transaction t = mTransactionSupplier.get(); animator.addUpdateListener(animation -> { final Rect animationValue = (Rect) animator.getAnimatedValue(); - t.setPosition(leash, animationValue.left, animationValue.right) + t.setPosition(leash, animationValue.left, animationValue.top) .setWindowCrop(leash, animationValue.width(), animationValue.height()) .show(leash); - mDesktopModeWindowDecoration.updateResizeVeil(t, animationValue); + mOnTaskResizeAnimationListener.onBoundsChange(taskInfo.taskId, t, animationValue); }); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - mDesktopModeWindowDecoration.hideResizeVeil(); + mOnTaskResizeAnimationListener.onAnimationEnd(taskInfo.taskId); mTransitions.getMainExecutor().execute( () -> finishCallback.onTransitionFinished(null)); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt index 0218493589b0..c469e652b117 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt @@ -21,7 +21,6 @@ import android.animation.RectEvaluator import android.animation.ValueAnimator import android.graphics.Rect import android.os.IBinder -import android.util.SparseArray import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE import android.window.TransitionInfo @@ -30,7 +29,7 @@ import android.window.WindowContainerTransaction import androidx.core.animation.addListener import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE -import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration +import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener import java.util.function.Supplier /** Handles the animation of quick resizing of desktop tasks. */ @@ -40,7 +39,7 @@ class ToggleResizeDesktopTaskTransitionHandler( ) : Transitions.TransitionHandler { private val rectEvaluator = RectEvaluator(Rect()) - private val taskToDecorationMap = SparseArray<DesktopModeWindowDecoration>() + private lateinit var onTaskResizeAnimationListener: OnTaskResizeAnimationListener private var boundsAnimator: Animator? = null @@ -49,13 +48,12 @@ class ToggleResizeDesktopTaskTransitionHandler( ) : this(transitions, Supplier { SurfaceControl.Transaction() }) /** Starts a quick resize transition. */ - fun startTransition( - wct: WindowContainerTransaction, - taskId: Int, - windowDecoration: DesktopModeWindowDecoration - ) { + fun startTransition(wct: WindowContainerTransaction) { transitions.startTransition(TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE, wct, this) - taskToDecorationMap.put(taskId, windowDecoration) + } + + fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) { + onTaskResizeAnimationListener = listener } override fun startAnimation( @@ -70,9 +68,6 @@ class ToggleResizeDesktopTaskTransitionHandler( val taskId = checkNotNull(change.taskInfo).taskId val startBounds = change.startAbsBounds val endBounds = change.endAbsBounds - val windowDecor = - taskToDecorationMap.removeReturnOld(taskId) - ?: throw IllegalStateException("Window decoration not found for task $taskId") val tx = transactionSupplier.get() boundsAnimator?.cancel() @@ -90,7 +85,11 @@ class ToggleResizeDesktopTaskTransitionHandler( ) .setWindowCrop(leash, startBounds.width(), startBounds.height()) .show(leash) - windowDecor.showResizeVeil(startTransaction, startBounds) + onTaskResizeAnimationListener.onAnimationStart( + taskId, + startTransaction, + startBounds + ) }, onEnd = { finishTransaction @@ -101,7 +100,7 @@ class ToggleResizeDesktopTaskTransitionHandler( ) .setWindowCrop(leash, endBounds.width(), endBounds.height()) .show(leash) - windowDecor.hideResizeVeil() + onTaskResizeAnimationListener.onAnimationEnd(taskId) finishCallback.onTransitionFinished(null) boundsAnimator = null } @@ -111,7 +110,7 @@ class ToggleResizeDesktopTaskTransitionHandler( tx.setPosition(leash, rect.left.toFloat(), rect.top.toFloat()) .setWindowCrop(leash, rect.width(), rect.height()) .show(leash) - windowDecor.updateResizeVeil(tx, rect) + onTaskResizeAnimationListener.onBoundsChange(taskId, tx, rect) } start() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/UnhandledDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/UnhandledDragController.kt new file mode 100644 index 000000000000..ccf48d0de9ed --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/UnhandledDragController.kt @@ -0,0 +1,100 @@ +/* + * 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.wm.shell.draganddrop + +import android.os.RemoteException +import android.util.Log +import android.view.DragEvent +import android.view.IWindowManager +import android.window.IUnhandledDragCallback +import android.window.IUnhandledDragListener +import androidx.annotation.VisibleForTesting +import com.android.internal.protolog.common.ProtoLog +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.protolog.ShellProtoLogGroup +import java.util.function.Consumer + +/** + * Manages the listener and callbacks for unhandled global drags. + */ +class UnhandledDragController( + val wmService: IWindowManager, + mainExecutor: ShellExecutor +) { + private var callback: UnhandledDragAndDropCallback? = null + + private val unhandledDragListener: IUnhandledDragListener = + object : IUnhandledDragListener.Stub() { + override fun onUnhandledDrop(event: DragEvent, callback: IUnhandledDragCallback) { + mainExecutor.execute() { + this@UnhandledDragController.onUnhandledDrop(event, callback) + } + } + } + + /** + * Listener called when an unhandled drag is started. + */ + interface UnhandledDragAndDropCallback { + /** + * Called when a global drag is unhandled (ie. dropped outside of all visible windows, or + * dropped on a window that does not want to handle it). + * + * The implementer _must_ call onFinishedCallback, and if it consumes the drop, then it is + * also responsible for releasing up the drag surface provided via the drag event. + */ + fun onUnhandledDrop(dragEvent: DragEvent, onFinishedCallback: Consumer<Boolean>) {} + } + + /** + * Sets a listener for callbacks when an unhandled drag happens. + */ + fun setListener(listener: UnhandledDragAndDropCallback?) { + val updateWm = (callback == null && listener != null) + || (callback != null && listener == null) + callback = listener + if (updateWm) { + try { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "%s unhandled drag listener", + if (callback != null) "Registering" else "Unregistering") + wmService.setUnhandledDragListener( + if (callback != null) unhandledDragListener else null) + } catch (e: RemoteException) { + Log.e(TAG, "Failed to set unhandled drag listener") + } + } + } + + @VisibleForTesting + fun onUnhandledDrop(dragEvent: DragEvent, wmCallback: IUnhandledDragCallback) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "onUnhandledDrop: %s", dragEvent) + if (callback == null) { + wmCallback.notifyUnhandledDropComplete(false) + } + + callback?.onUnhandledDrop(dragEvent) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Notifying onUnhandledDrop complete: %b", it) + wmCallback.notifyUnhandledDropComplete(it) + } + } + + companion object { + private val TAG = UnhandledDragController::class.java.simpleName + } +} 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/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java index 93d763608b5f..196e04edbb10 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java @@ -480,7 +480,7 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { WindowContainerTransaction wct = new WindowContainerTransaction(); if (mCaptionInsets != null) { wct.addInsetsSource(mTaskToken, mCaptionInsetsOwner, 0, - WindowInsets.Type.captionBar(), mCaptionInsets); + WindowInsets.Type.captionBar(), mCaptionInsets, null /* boundingRects */); } else { wct.removeInsetsSource(mTaskToken, mCaptionInsetsOwner, 0, WindowInsets.Type.captionBar()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 891eea072c0d..7db3d382ed8e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -211,6 +211,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mShellCommandHandler.addDumpCallback(this::dump, this); mDisplayInsetsController.addInsetsChangedListener(mContext.getDisplayId(), new DesktopModeOnInsetsChangedListener()); + mDesktopTasksController.ifPresent(c -> c.setOnTaskResizeAnimationListener( + new DeskopModeOnTaskResizeAnimationListener())); } @Override @@ -356,7 +358,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { // App sometimes draws before the insets from WindowDecoration#relayout have // been added, so they must be added here mWindowDecorByTaskId.get(mTaskId).addCaptionInset(wct); - mDesktopTasksController.get().moveToDesktop(decoration, mTaskId, wct); + mDesktopTasksController.get().moveToDesktop(mTaskId, wct); closeOtherSplitTask(mTaskId); } decoration.closeHandleMenu(); @@ -387,25 +389,23 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { return; } final RunningTaskInfo taskInfo = decoration.mTaskInfo; - mDesktopTasksController.ifPresent(c -> c.toggleDesktopTaskSize( - taskInfo, decoration)); + mDesktopTasksController.ifPresent(c -> c.toggleDesktopTaskSize(taskInfo)); decoration.closeHandleMenu(); } else if (id == R.id.maximize_menu_maximize_button) { final RunningTaskInfo taskInfo = decoration.mTaskInfo; - mDesktopTasksController.ifPresent(c -> c.toggleDesktopTaskSize( - taskInfo, mWindowDecorByTaskId.get(taskInfo.taskId))); + mDesktopTasksController.ifPresent(c -> c.toggleDesktopTaskSize(taskInfo)); decoration.closeHandleMenu(); decoration.closeMaximizeMenu(); } else if (id == R.id.maximize_menu_snap_left_button) { final RunningTaskInfo taskInfo = decoration.mTaskInfo; mDesktopTasksController.ifPresent(c -> c.snapToHalfScreen( - taskInfo, mWindowDecorByTaskId.get(taskInfo.taskId), SnapPosition.LEFT)); + taskInfo, SnapPosition.LEFT)); decoration.closeHandleMenu(); decoration.closeMaximizeMenu(); } else if (id == R.id.maximize_menu_snap_right_button) { final RunningTaskInfo taskInfo = decoration.mTaskInfo; mDesktopTasksController.ifPresent(c -> c.snapToHalfScreen( - taskInfo, mWindowDecorByTaskId.get(taskInfo.taskId), SnapPosition.RIGHT)); + taskInfo, SnapPosition.RIGHT)); decoration.closeHandleMenu(); decoration.closeMaximizeMenu(); } @@ -558,7 +558,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } mDesktopTasksController.ifPresent(c -> { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); - c.toggleDesktopTaskSize(decoration.mTaskInfo, decoration); + c.toggleDesktopTaskSize(decoration.mTaskInfo); }); return true; } @@ -761,7 +761,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { relevantDecor.mTaskInfo, relevantDecor.mTaskSurface); mDesktopTasksController.ifPresent( c -> c.startDragToDesktop(relevantDecor.mTaskInfo, - mMoveToDesktopAnimator, relevantDecor)); + mMoveToDesktopAnimator)); } } if (mMoveToDesktopAnimator != null) { @@ -1020,6 +1020,34 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { pw.println(innerPrefix + "mWindowDecorByTaskId=" + mWindowDecorByTaskId); } + private class DeskopModeOnTaskResizeAnimationListener + implements OnTaskResizeAnimationListener { + @Override + public void onAnimationStart(int taskId, Transaction t, Rect bounds) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration == null) { + t.apply(); + return; + } + decoration.showResizeVeil(t, bounds); + } + + @Override + public void onBoundsChange(int taskId, Transaction t, Rect bounds) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration == null) return; + decoration.updateResizeVeil(t, bounds); + } + + @Override + public void onAnimationEnd(int taskId) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration == null) return; + decoration.hideResizeVeil(); + } + } + + private class DragStartListenerImpl implements DragPositioningCallbackUtility.DragStartListener { @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OnTaskResizeAnimationListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OnTaskResizeAnimationListener.kt new file mode 100644 index 000000000000..09c62bfc9da2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OnTaskResizeAnimationListener.kt @@ -0,0 +1,41 @@ +/* + * 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.wm.shell.windowdecor + +import android.graphics.Rect +import android.view.SurfaceControl + +import com.android.wm.shell.transition.Transitions.TransitionHandler +/** + * Listener that allows implementations of [TransitionHandler] to notify when an + * animation that is resizing a task is starting, updating, and finishing the animation. + */ +interface OnTaskResizeAnimationListener { + /** + * Notifies that a transition animation is about to be started with the given bounds. + */ + fun onAnimationStart(taskId: Int, t: SurfaceControl.Transaction, bounds: Rect) + + /** + * Notifies that a transition animation is expanding or shrinking the task to the given bounds. + */ + fun onBoundsChange(taskId: Int, t: SurfaceControl.Transaction, bounds: Rect) + + /** + * Notifies that a transition animation is about to be finished. + */ + fun onAnimationEnd(taskId: Int) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index afe837e834f0..35d59401ac1a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -298,10 +298,11 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mCaptionInsetsRect.bottom = mCaptionInsetsRect.top + outResult.mCaptionHeight; wct.addInsetsSource(mTaskInfo.token, - mOwner, 0 /* index */, WindowInsets.Type.captionBar(), mCaptionInsetsRect); + mOwner, 0 /* index */, WindowInsets.Type.captionBar(), mCaptionInsetsRect, + null /* boundingRects */); wct.addInsetsSource(mTaskInfo.token, mOwner, 0 /* index */, WindowInsets.Type.mandatorySystemGestures(), - mCaptionInsetsRect); + mCaptionInsetsRect, null /* boundingRects */); } else { wct.removeInsetsSource(mTaskInfo.token, mOwner, 0 /* index */, WindowInsets.Type.captionBar()); @@ -546,7 +547,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> final int captionHeight = loadDimensionPixelSize(mContext.getResources(), captionHeightId); final Rect captionInsets = new Rect(0, 0, 0, captionHeight); wct.addInsetsSource(mTaskInfo.token, mOwner, 0 /* index */, WindowInsets.Type.captionBar(), - captionInsets); + captionInsets, null /* boundingRects */); } static class RelayoutParams { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 9249b0a0dfda..79634e6040c4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -303,7 +303,7 @@ class DesktopTasksControllerTest : ShellTestCase() { fun moveToDesktop_displayFullscreen_windowingModeSetToFreeform() { val task = setUpFullscreenTask() task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN - controller.moveToDesktop(desktopModeWindowDecoration, task) + controller.moveToDesktop(task) val wct = getLatestMoveToDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FREEFORM) @@ -313,7 +313,7 @@ class DesktopTasksControllerTest : ShellTestCase() { fun moveToDesktop_displayFreeform_windowingModeSetToUndefined() { val task = setUpFullscreenTask() task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM - controller.moveToDesktop(desktopModeWindowDecoration, task) + controller.moveToDesktop(task) val wct = getLatestMoveToDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_UNDEFINED) @@ -321,7 +321,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun moveToDesktop_nonExistentTask_doesNothing() { - controller.moveToDesktop(desktopModeWindowDecoration, 999) + controller.moveToDesktop(999) verifyWCTNotExecuted() } @@ -332,7 +332,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val fullscreenTask = setUpFullscreenTask() markTaskHidden(freeformTask) - controller.moveToDesktop(desktopModeWindowDecoration, fullscreenTask) + controller.moveToDesktop(fullscreenTask) with(getLatestMoveToDesktopWct()) { // Operations should include home task, freeform task @@ -354,7 +354,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY) markTaskHidden(freeformTaskSecond) - controller.moveToDesktop(desktopModeWindowDecoration, fullscreenTaskDefault) + controller.moveToDesktop(fullscreenTaskDefault) with(getLatestMoveToDesktopWct()) { // Check that hierarchy operations do not include tasks from second display @@ -368,7 +368,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun moveToDesktop_splitTaskExitsSplit() { val task = setUpSplitScreenTask() - controller.moveToDesktop(desktopModeWindowDecoration, task) + controller.moveToDesktop(task) val wct = getLatestMoveToDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FREEFORM) @@ -380,7 +380,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun moveToDesktop_fullscreenTaskDoesNotExitSplit() { val task = setUpFullscreenTask() - controller.moveToDesktop(desktopModeWindowDecoration, task) + controller.moveToDesktop(task) val wct = getLatestMoveToDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FREEFORM) @@ -802,7 +802,7 @@ class DesktopTasksControllerTest : ShellTestCase() { private fun getLatestMoveToDesktopWct(): WindowContainerTransaction { val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) if (ENABLE_SHELL_TRANSITIONS) { - verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any()) + verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture()) } else { verify(shellTaskOrganizer).applyTransaction(arg.capture()) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index be639e867e0b..98e90d60b3b6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -24,7 +24,6 @@ import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_D import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP import com.android.wm.shell.windowdecor.MoveToDesktopAnimator -import java.util.function.Supplier import junit.framework.Assert.assertFalse import org.junit.Before import org.junit.Test @@ -38,6 +37,7 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyZeroInteractions import org.mockito.kotlin.whenever +import java.util.function.Supplier /** Tests of [DragToDesktopTransitionHandler]. */ @SmallTest @@ -246,7 +246,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { ) ) .thenReturn(token) - handler.startDragToDesktopTransition(task.taskId, dragAnimator, mock()) + handler.startDragToDesktopTransition(task.taskId, dragAnimator) return token } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/UnhandledDragControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/UnhandledDragControllerTest.kt new file mode 100644 index 000000000000..522f05233f3a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/UnhandledDragControllerTest.kt @@ -0,0 +1,115 @@ +/* + * 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.wm.shell.draganddrop + +import android.os.RemoteException +import android.view.DragEvent +import android.view.DragEvent.ACTION_DROP +import android.view.IWindowManager +import android.view.SurfaceControl +import android.window.IUnhandledDragCallback +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.draganddrop.UnhandledDragController.UnhandledDragAndDropCallback +import java.util.function.Consumer +import junit.framework.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +/** + * Tests for the unhandled drag controller. + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class UnhandledDragControllerTest : ShellTestCase() { + @Mock + private lateinit var mIWindowManager: IWindowManager + + @Mock + private lateinit var mMainExecutor: ShellExecutor + + private lateinit var mController: UnhandledDragController + + @Before + @Throws(RemoteException::class) + fun setUp() { + MockitoAnnotations.initMocks(this) + mController = UnhandledDragController(mIWindowManager, mMainExecutor) + } + + @Test + fun setListener_registersUnregistersWithWM() { + mController.setListener(object : UnhandledDragAndDropCallback {}) + mController.setListener(object : UnhandledDragAndDropCallback {}) + mController.setListener(object : UnhandledDragAndDropCallback {}) + verify(mIWindowManager, Mockito.times(1)) + .setUnhandledDragListener(ArgumentMatchers.any()) + + reset(mIWindowManager) + mController.setListener(null) + mController.setListener(null) + mController.setListener(null) + verify(mIWindowManager, Mockito.times(1)) + .setUnhandledDragListener(ArgumentMatchers.isNull()) + } + + @Test + fun onUnhandledDrop_noListener_expectNotifyUnhandled() { + // Simulate an unhandled drop + val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, null, null, null, + null, null, false) + val wmCallback = mock(IUnhandledDragCallback::class.java) + mController.onUnhandledDrop(dropEvent, wmCallback) + + verify(wmCallback).notifyUnhandledDropComplete(ArgumentMatchers.eq(false)) + } + + @Test + fun onUnhandledDrop_withListener_expectNotifyHandled() { + val lastDragEvent = arrayOfNulls<DragEvent>(1) + + // Set a listener to listen for unhandled drops + mController.setListener(object : UnhandledDragAndDropCallback { + override fun onUnhandledDrop(dragEvent: DragEvent, + onFinishedCallback: Consumer<Boolean>) { + lastDragEvent[0] = dragEvent + onFinishedCallback.accept(true) + dragEvent.dragSurface.release() + } + }) + + // Simulate an unhandled drop + val dragSurface = mock(SurfaceControl::class.java) + val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, null, null, null, + dragSurface, null, false) + val wmCallback = mock(IUnhandledDragCallback::class.java) + mController.onUnhandledDrop(dropEvent, wmCallback) + + verify(wmCallback).notifyUnhandledDropComplete(ArgumentMatchers.eq(true)) + verify(dragSurface).release() + assertEquals(lastDragEvent.get(0), dropEvent) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index 7b53f70a771c..228b25ccb1ba 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -259,7 +259,8 @@ public class WindowDecorationTests extends ShellTestCase { any(), eq(0 /* index */), eq(WindowInsets.Type.captionBar()), - eq(new Rect(100, 300, 400, 364))); + eq(new Rect(100, 300, 400, 364)), + any()); } verify(mMockSurfaceControlStartT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS); @@ -569,9 +570,9 @@ public class WindowDecorationTests extends ShellTestCase { windowDecor.relayout(taskInfo); verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(), - eq(0) /* index */, eq(captionBar()), any()); + eq(0) /* index */, eq(captionBar()), any(), any()); verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(), - eq(0) /* index */, eq(mandatorySystemGestures()), any()); + eq(0) /* index */, eq(mandatorySystemGestures()), any(), any()); } @Test 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/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java index e3290d604794..2a0648d87c85 100644 --- a/media/java/android/media/projection/MediaProjectionManager.java +++ b/media/java/android/media/projection/MediaProjectionManager.java @@ -180,7 +180,7 @@ public final class MediaProjectionManager { @SuppressLint("UnflaggedApi") @TestApi @NonNull - public Intent createScreenCaptureIntent(@Nullable LaunchCookie launchCookie) { + public Intent createScreenCaptureIntent(@NonNull LaunchCookie launchCookie) { Intent i = createScreenCaptureIntent(); i.putExtra(EXTRA_LAUNCH_COOKIE, launchCookie); return i; diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java index 59b10c6bcd93..76664a66ad53 100644 --- a/media/java/android/media/tv/ad/TvAdManager.java +++ b/media/java/android/media/tv/ad/TvAdManager.java @@ -18,6 +18,7 @@ package android.media.tv.ad; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StringDef; @@ -59,7 +60,7 @@ import java.util.concurrent.Executor; */ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW) @SystemService(Context.TV_AD_SERVICE) -public class TvAdManager { +public final class TvAdManager { // TODO: implement more methods and unhide APIs. private static final String TAG = "TvAdManager"; @@ -237,6 +238,76 @@ public class TvAdManager { */ public static final String SESSION_DATA_KEY_REQUEST_ID = "request_id"; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = false, prefix = "SESSION_STATE_", value = { + SESSION_STATE_STOPPED, + SESSION_STATE_RUNNING, + SESSION_STATE_ERROR}) + public @interface SessionState {} + + /** + * Stopped (or not started) state of AD service session. + */ + public static final int SESSION_STATE_STOPPED = 1; + /** + * Running state of AD service session. + */ + public static final int SESSION_STATE_RUNNING = 2; + /** + * Error state of AD service session. + */ + public static final int SESSION_STATE_ERROR = 3; + + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = false, prefix = "ERROR_", value = { + ERROR_NONE, + ERROR_UNKNOWN, + ERROR_NOT_SUPPORTED, + ERROR_WEAK_SIGNAL, + ERROR_RESOURCE_UNAVAILABLE, + ERROR_BLOCKED, + ERROR_ENCRYPTED, + ERROR_UNKNOWN_CHANNEL, + }) + public @interface ErrorCode {} + + /** + * No error. + */ + public static final int ERROR_NONE = 0; + /** + * Unknown error code. + */ + public static final int ERROR_UNKNOWN = 1; + /** + * Error code for an unsupported channel. + */ + public static final int ERROR_NOT_SUPPORTED = 2; + /** + * Error code for weak signal. + */ + public static final int ERROR_WEAK_SIGNAL = 3; + /** + * Error code when resource (e.g. tuner) is unavailable. + */ + public static final int ERROR_RESOURCE_UNAVAILABLE = 4; + /** + * Error code for blocked contents. + */ + public static final int ERROR_BLOCKED = 5; + /** + * Error code when the key or module is missing for the encrypted channel. + */ + public static final int ERROR_ENCRYPTED = 6; + /** + * Error code when the current channel is an unknown channel. + */ + public static final int ERROR_UNKNOWN_CHANNEL = 7; + private final ITvAdManager mService; private final int mUserId; diff --git a/media/java/android/media/tv/ad/TvAdService.java b/media/java/android/media/tv/ad/TvAdService.java index 6c8a8fd4f9e4..2bba0f395a7d 100644 --- a/media/java/android/media/tv/ad/TvAdService.java +++ b/media/java/android/media/tv/ad/TvAdService.java @@ -280,7 +280,6 @@ public abstract class TvAdService extends Service { /** * Requests the bounds of the current video. - * @hide */ @CallSuper public void requestCurrentVideoBounds() { @@ -304,7 +303,6 @@ public abstract class TvAdService extends Service { /** * Requests the URI of the current channel. - * @hide */ @CallSuper public void requestCurrentChannelUri() { @@ -328,7 +326,6 @@ public abstract class TvAdService extends Service { /** * Requests the list of {@link TvTrackInfo}. - * @hide */ @CallSuper public void requestTrackInfoList() { @@ -354,7 +351,6 @@ public abstract class TvAdService extends Service { * Requests current TV input ID. * * @see android.media.tv.TvInputInfo - * @hide */ @CallSuper public void requestCurrentTvInputId() { @@ -393,7 +389,6 @@ public abstract class TvAdService extends Service { * @param data the original bytes to be signed. * * @see #onSigningResult(String, byte[]) - * @hide */ @CallSuper public void requestSigning(@NonNull String signingId, @NonNull String algorithm, @@ -535,28 +530,24 @@ public abstract class TvAdService extends Service { * Receives current video bounds. * * @param bounds the rectangle area for rendering the current video. - * @hide */ public void onCurrentVideoBounds(@NonNull Rect bounds) { } /** * Receives current channel URI. - * @hide */ public void onCurrentChannelUri(@Nullable Uri channelUri) { } /** * Receives track list. - * @hide */ public void onTrackInfoList(@NonNull List<TvTrackInfo> tracks) { } /** * Receives current TV input ID. - * @hide */ public void onCurrentTvInputId(@Nullable String inputId) { } @@ -569,7 +560,6 @@ public abstract class TvAdService extends Service { * @param result the signed result. * * @see #requestSigning(String, String, String, byte[]) - * @hide */ public void onSigningResult(@NonNull String signingId, @NonNull byte[] result) { } @@ -584,7 +574,6 @@ public abstract class TvAdService extends Service { * "onRequestSigning" can also be added to the params. * * @see TvAdView#ERROR_KEY_METHOD_NAME - * @hide */ public void onError(@NonNull String errMsg, @NonNull Bundle params) { } @@ -601,7 +590,6 @@ public abstract class TvAdService extends Service { * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}. * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on * how to parse this data. - * @hide */ public void onTvMessage(@TvInputManager.TvMessageType int type, @NonNull Bundle data) { @@ -671,6 +659,30 @@ public abstract class TvAdService extends Service { } /** + * Notifies when the session state is changed. + * + * @param state the current session state. + * @param err the error code for error state. {@link TvAdManager#ERROR_NONE} is + * used when the state is not {@link TvAdManager#SESSION_STATE_ERROR}. + */ + @CallSuper + public void notifySessionStateChanged( + @TvAdManager.SessionState int state, + @TvAdManager.ErrorCode int err) { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread + @Override + public void run() { + if (DEBUG) { + Log.d(TAG, "notifySessionStateChanged (state=" + + state + "; err=" + err + ")"); + } + // TODO: handle session callback + } + }); + } + + /** * Takes care of dispatching incoming input events and tells whether the event was handled. */ int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) { diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java index ee01468b8f87..2fac8ce4950d 100644 --- a/media/java/android/media/tv/ad/TvAdView.java +++ b/media/java/android/media/tv/ad/TvAdView.java @@ -61,10 +61,19 @@ public class TvAdView extends ViewGroup { * The name of the method where the error happened, if applicable. For example, if there is an * error during signing, the request name is "onRequestSigning". * @see #notifyError(String, Bundle) - * @hide */ public static final String ERROR_KEY_METHOD_NAME = "method_name"; + /** + * The error code of an error. + * + * <p>It can be {@link TvAdManager#ERROR_WEAK_SIGNAL}, + * {@link TvAdManager#ERROR_RESOURCE_UNAVAILABLE}, etc. + * + * @see #notifyError(String, Bundle) + */ + public static final String ERROR_KEY_ERROR_CODE = "error_code"; + private final TvAdManager mTvAdManager; private final Handler mHandler = new Handler(); @@ -486,7 +495,6 @@ public class TvAdView extends ViewGroup { * Sends current video bounds to related TV AD service. * * @param bounds the rectangle area for rendering the current video. - * @hide */ public void sendCurrentVideoBounds(@NonNull Rect bounds) { if (DEBUG) { @@ -502,7 +510,6 @@ public class TvAdView extends ViewGroup { * * @param channelUri The current channel URI; {@code null} if there is no currently tuned * channel. - * @hide */ public void sendCurrentChannelUri(@Nullable Uri channelUri) { if (DEBUG) { @@ -515,7 +522,6 @@ public class TvAdView extends ViewGroup { /** * Sends track info list to related TV AD service. - * @hide */ public void sendTrackInfoList(@Nullable List<TvTrackInfo> tracks) { if (DEBUG) { @@ -532,7 +538,6 @@ public class TvAdView extends ViewGroup { * @param inputId The current TV input ID whose channel is tuned. {@code null} if no channel is * tuned. * @see android.media.tv.TvInputInfo - * @hide */ public void sendCurrentTvInputId(@Nullable String inputId) { if (DEBUG) { @@ -553,7 +558,6 @@ public class TvAdView extends ViewGroup { * @param signingId the ID to identify the request. It's the same as the corresponding ID in * {@link TvAdService.Session#requestSigning(String, String, String, byte[])} * @param result the signed result. - * @hide */ public void sendSigningResult(@NonNull String signingId, @NonNull byte[] result) { if (DEBUG) { @@ -574,7 +578,7 @@ public class TvAdView extends ViewGroup { * can also be added to the params. * * @see #ERROR_KEY_METHOD_NAME - * @hide + * @see #ERROR_KEY_ERROR_CODE */ public void notifyError(@NonNull String errMsg, @NonNull Bundle params) { if (DEBUG) { @@ -597,7 +601,6 @@ public class TvAdView extends ViewGroup { * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}. * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on * how to parse this data. - * @hide */ public void notifyTvMessage(@NonNull @TvInputManager.TvMessageType int type, @NonNull Bundle data) { @@ -633,7 +636,6 @@ public class TvAdView extends ViewGroup { * @param callback the callback to receive events. MUST NOT be {@code null}. * * @see #clearCallback() - * @hide */ public void setCallback( @NonNull @CallbackExecutor Executor executor, @@ -649,7 +651,6 @@ public class TvAdView extends ViewGroup { * Clears the callback. * * @see #setCallback(Executor, TvAdCallback) - * @hide */ public void clearCallback() { synchronized (mCallbackLock) { @@ -845,7 +846,6 @@ public class TvAdView extends ViewGroup { /** * Callback used to receive various status updates on the {@link TvAdView}. - * @hide */ public abstract static class TvAdCallback { @@ -898,5 +898,20 @@ public class TvAdView extends ViewGroup { public void onRequestSigning(@NonNull String serviceId, @NonNull String signingId, @NonNull String algorithm, @NonNull String alias, @NonNull byte[] data) { } + + /** + * This is called when the state of corresponding AD service is changed. + * + * @param serviceId The ID of the AD service bound to this view. + * @param state the current state. + * @param err the error code for error state. {@link TvAdManager#ERROR_NONE} + * is used when the state is not + * {@link TvAdManager#SESSION_STATE_ERROR}. + */ + public void onStateChanged( + @NonNull String serviceId, + @TvAdManager.SessionState int state, + @TvAdManager.ErrorCode int err) { + } } } diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java index eba26d470737..f332f8102013 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java @@ -873,6 +873,9 @@ public abstract class TvInteractiveAppService extends Service { /** * Called when the corresponding TV input selected to a track. + * + * If the track is deselected and no track is currently selected, + * trackId is an empty string. */ public void onTrackSelected(@TvTrackInfo.Type int type, @NonNull String trackId) { } @@ -1845,6 +1848,10 @@ public abstract class TvInteractiveAppService extends Service { if (DEBUG) { Log.d(TAG, "notifyTrackSelected (type=" + type + "trackId=" + trackId + ")"); } + // TvInputService accepts a Null String, but onTrackSelected expects NonNull. + if (trackId == null) { + trackId = ""; + } onTrackSelected(type, trackId); } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt index 5590219bc011..7cd6bb3a6cef 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt @@ -48,8 +48,6 @@ fun CredentialsScreenChip( { Text( text = label, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, overflow = TextOverflow.Ellipsis, maxLines = if (secondaryLabel != null) 1 else 2, ) 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/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/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 8ae117e4143c..4feb6e6e2023 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -247,6 +247,7 @@ public class SecureSettings { Settings.Secure.BLUETOOTH_LE_BROADCAST_FALLBACK_ACTIVE_DEVICE_ADDRESS, Settings.Secure.CUSTOM_BUGREPORT_HANDLER_APP, Settings.Secure.CUSTOM_BUGREPORT_HANDLER_USER, + Settings.Secure.CONTEXTUAL_SCREEN_TIMEOUT_ENABLED, Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED, Settings.Secure.HEARING_AID_RINGTONE_ROUTING, Settings.Secure.HEARING_AID_CALL_ROUTING, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 5adae37533ff..d0ed656a5059 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -397,6 +397,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.CUSTOM_BUGREPORT_HANDLER_APP, ANY_STRING_VALIDATOR); VALIDATORS.put(Secure.CUSTOM_BUGREPORT_HANDLER_USER, ANY_INTEGER_VALIDATOR); VALIDATORS.put(Secure.LOCK_SCREEN_WEATHER_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.CONTEXTUAL_SCREEN_TIMEOUT_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.HEARING_AID_RINGTONE_ROUTING, new DiscreteValueValidator(new String[] {"0", "1", "2"})); VALIDATORS.put(Secure.HEARING_AID_CALL_ROUTING, diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 926e181943e4..c086baa7c8db 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -862,6 +862,8 @@ <!-- Permission required for CTS test - CtsTelephonyProviderTestCases --> <uses-permission android:name="android.permission.WRITE_APN_SETTINGS" /> + <uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" /> + <uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" /> <uses-permission android:name="android.permission.LOG_FOREGROUND_RESOURCE_USE" /> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/TEST_MAPPING b/packages/SystemUI/accessibility/accessibilitymenu/TEST_MAPPING index 2bd52b552698..29a25ad04cbe 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/TEST_MAPPING +++ b/packages/SystemUI/accessibility/accessibilitymenu/TEST_MAPPING @@ -4,8 +4,15 @@ "name": "AccessibilityMenuServiceTests", "options": [ { - "include-annotation": "android.platform.test.annotations.Presubmit" - }, + "exclude-annotation": "android.support.test.filters.FlakyTest" + } + ] + } + ], + "postsubmit": [ + { + "name": "AccessibilityMenuServiceTests", + "options": [ { "exclude-annotation": "android.support.test.filters.FlakyTest" } diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java index 9d1af0e2375a..72c1092b92d6 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java @@ -36,6 +36,7 @@ import android.app.Instrumentation; import android.app.KeyguardManager; import android.app.UiAutomation; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -57,6 +58,7 @@ import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortc import org.junit.After; import org.junit.AfterClass; +import org.junit.Assume; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -330,8 +332,10 @@ public class AccessibilityMenuServiceTest { AccessibilityNodeInfo assistantButton = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_ASSISTANT_VALUE.ordinal())); Intent expectedIntent = new Intent(Intent.ACTION_VOICE_COMMAND); - String expectedPackage = expectedIntent.resolveActivity( - sInstrumentation.getContext().getPackageManager()).getPackageName(); + ComponentName componentName = expectedIntent.resolveActivity( + sInstrumentation.getContext().getPackageManager()); + Assume.assumeNotNull(componentName); + String expectedPackage = componentName.getPackageName(); sUiAutomation.executeAndWaitForEvent( () -> assistantButton.performAction(CLICK_ID), diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeAwareExtensions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeAwareExtensions.kt new file mode 100644 index 000000000000..a7de1eede1f4 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeAwareExtensions.kt @@ -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. + */ + +package com.android.systemui.scene.ui.composable + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntSize +import com.android.compose.animation.scene.Back +import com.android.compose.animation.scene.Edge as ComposeAwareEdge +import com.android.compose.animation.scene.SceneKey as ComposeAwareSceneKey +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.SwipeDirection +import com.android.compose.animation.scene.TransitionKey as ComposeAwareTransitionKey +import com.android.compose.animation.scene.UserAction as ComposeAwareUserAction +import com.android.compose.animation.scene.UserActionDistance as ComposeAwareUserActionDistance +import com.android.compose.animation.scene.UserActionResult as ComposeAwareUserActionResult +import com.android.systemui.scene.shared.model.Direction +import com.android.systemui.scene.shared.model.Edge +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.TransitionKey +import com.android.systemui.scene.shared.model.UserAction +import com.android.systemui.scene.shared.model.UserActionDistance +import com.android.systemui.scene.shared.model.UserActionResult + +// TODO(b/293899074): remove this file once we can use the types from SceneTransitionLayout. + +fun SceneKey.asComposeAware(): ComposeAwareSceneKey { + return ComposeAwareSceneKey( + debugName = toString(), + identity = this, + ) +} + +fun TransitionKey.asComposeAware(): ComposeAwareTransitionKey { + return ComposeAwareTransitionKey( + debugName = debugName, + identity = this, + ) +} + +fun UserAction.asComposeAware(): ComposeAwareUserAction { + return when (this) { + is UserAction.Swipe -> + Swipe( + pointerCount = pointerCount, + fromSource = + when (this.fromEdge) { + null -> null + Edge.LEFT -> ComposeAwareEdge.Left + Edge.TOP -> ComposeAwareEdge.Top + Edge.RIGHT -> ComposeAwareEdge.Right + Edge.BOTTOM -> ComposeAwareEdge.Bottom + }, + direction = + when (this.direction) { + Direction.LEFT -> SwipeDirection.Left + Direction.UP -> SwipeDirection.Up + Direction.RIGHT -> SwipeDirection.Right + Direction.DOWN -> SwipeDirection.Down + } + ) + is UserAction.Back -> Back + } +} + +fun UserActionResult.asComposeAware(): ComposeAwareUserActionResult { + val composeUnaware = this + return ComposeAwareUserActionResult( + toScene = composeUnaware.toScene.asComposeAware(), + transitionKey = composeUnaware.transitionKey?.asComposeAware(), + distance = composeUnaware.distance?.asComposeAware(), + ) +} + +fun UserActionDistance.asComposeAware(): ComposeAwareUserActionDistance { + val composeUnware = this + return object : ComposeAwareUserActionDistance { + override fun Density.absoluteDistance( + fromSceneSize: IntSize, + orientation: Orientation, + ): Float { + return composeUnware.absoluteDistance( + fromSceneWidth = fromSceneSize.width, + fromSceneHeight = fromSceneSize.height, + isHorizontal = orientation == Orientation.Horizontal, + ) + } + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeUnawareExtensions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeUnawareExtensions.kt new file mode 100644 index 000000000000..4c03664fc244 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeUnawareExtensions.kt @@ -0,0 +1,41 @@ +/* + * 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.scene.ui.composable + +import com.android.compose.animation.scene.ObservableTransitionState as ComposeAwareObservableTransitionState +import com.android.compose.animation.scene.SceneKey as ComposeAwareSceneKey +import com.android.systemui.scene.shared.model.ObservableTransitionState +import com.android.systemui.scene.shared.model.SceneKey + +fun ComposeAwareSceneKey.asComposeUnaware(): SceneKey { + return this.identity as SceneKey +} + +fun ComposeAwareObservableTransitionState.asComposeUnaware(): ObservableTransitionState { + return when (this) { + is ComposeAwareObservableTransitionState.Idle -> + ObservableTransitionState.Idle(scene.asComposeUnaware()) + is ComposeAwareObservableTransitionState.Transition -> + ObservableTransitionState.Transition( + fromScene = fromScene.asComposeUnaware(), + toScene = toScene.asComposeUnaware(), + progress = progress, + isInitiatedByUserInput = isInitiatedByUserInput, + isUserInputOngoing = isUserInputOngoing, + ) + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt new file mode 100644 index 000000000000..60c0b7719a25 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt @@ -0,0 +1,81 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.scene.ui.composable + +import com.android.compose.animation.scene.MutableSceneTransitionLayoutState +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.compose.animation.scene.observableTransitionState +import com.android.systemui.scene.shared.model.SceneDataSource +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.TransitionKey +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** + * An implementation of [SceneDataSource] that's backed by a [MutableSceneTransitionLayoutState]. + */ +class SceneTransitionLayoutDataSource( + private val state: MutableSceneTransitionLayoutState, + + /** + * The [CoroutineScope] of the @Composable that's using this, it's critical that this is *not* + * the application scope. + */ + private val coroutineScope: CoroutineScope, +) : SceneDataSource { + override val currentScene: StateFlow<SceneKey> = + state + .observableTransitionState() + .flatMapLatest { observableTransitionState -> + when (observableTransitionState) { + is ObservableTransitionState.Idle -> flowOf(observableTransitionState.scene) + is ObservableTransitionState.Transition -> + observableTransitionState.isUserInputOngoing.map { isUserInputOngoing -> + if (isUserInputOngoing) { + observableTransitionState.fromScene + } else { + observableTransitionState.toScene + } + } + } + } + .map { it.asComposeUnaware() } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(), + initialValue = state.transitionState.currentScene.asComposeUnaware(), + ) + + override fun changeScene( + toScene: SceneKey, + transitionKey: TransitionKey?, + ) { + state.setTargetScene( + targetScene = toScene.asComposeAware(), + transitionKey = transitionKey?.asComposeAware(), + coroutineScope = coroutineScope, + ) + } +} 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/keyguard/data/repository/LightRevealScrimRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt index 7242cb20dc77..f6c056698967 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt @@ -54,7 +54,7 @@ class LightRevealScrimRepositoryTest : SysuiTestCase() { private lateinit var powerInteractor: PowerInteractor private lateinit var underTest: LightRevealScrimRepositoryImpl - @get:Rule val animatorTestRule = AnimatorTestRule() + @get:Rule val animatorTestRule = AnimatorTestRule(this) @Before fun setUp() { 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/scene/shared/model/SceneDataSourceDelegatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegatorTest.kt new file mode 100644 index 000000000000..ed4b1e6a43c9 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegatorTest.kt @@ -0,0 +1,90 @@ +/* + * 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.scene.shared.model + +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.kosmos.testScope +import com.android.systemui.scene.initialSceneKey +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SceneDataSourceDelegatorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val initialSceneKey = kosmos.initialSceneKey + private val fakeSceneDataSource = kosmos.fakeSceneDataSource + + private val underTest = kosmos.sceneDataSourceDelegator + + @Test + fun currentScene_withoutDelegate_startsWithInitialScene() = + testScope.runTest { + val currentScene by collectLastValue(underTest.currentScene) + underTest.setDelegate(null) + + assertThat(currentScene).isEqualTo(initialSceneKey) + } + + @Test + fun currentScene_withoutDelegate_doesNothing() = + testScope.runTest { + val currentScene by collectLastValue(underTest.currentScene) + underTest.setDelegate(null) + assertThat(currentScene).isNotEqualTo(SceneKey.Bouncer) + + underTest.changeScene(toScene = SceneKey.Bouncer) + + assertThat(currentScene).isEqualTo(initialSceneKey) + } + + @Test + fun currentScene_withDelegate_startsWithInitialScene() = + testScope.runTest { + val currentScene by collectLastValue(underTest.currentScene) + assertThat(currentScene).isEqualTo(initialSceneKey) + } + + @Test + fun currentScene_withDelegate_changesScenes() = + testScope.runTest { + val currentScene by collectLastValue(underTest.currentScene) + assertThat(currentScene).isNotEqualTo(SceneKey.Bouncer) + + underTest.changeScene(toScene = SceneKey.Bouncer) + + assertThat(currentScene).isEqualTo(SceneKey.Bouncer) + } + + @Test + fun currentScene_reflectsDelegate() = + testScope.runTest { + val currentScene by collectLastValue(underTest.currentScene) + + fakeSceneDataSource.changeScene(toScene = SceneKey.Bouncer) + + assertThat(currentScene).isEqualTo(SceneKey.Bouncer) + } +} 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/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/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/dreams/homecontrols/TaskFragmentComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt index 6f7dcb173156..297ad847caa6 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt @@ -17,7 +17,7 @@ package com.android.systemui.dreams.homecontrols import android.app.Activity -import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.content.Intent import android.graphics.Rect import android.os.Binder @@ -122,15 +122,14 @@ constructor( /** Creates the task fragment */ fun createTaskFragment() { - val taskBounds = Rect(activity.resources.configuration.windowConfiguration.bounds) val fragmentOptions = TaskFragmentCreationParams.Builder( organizer.organizerToken, fragmentToken, activity.activityToken!! ) - .setInitialRelativeBounds(taskBounds) - .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW) + .setInitialRelativeBounds(Rect()) + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) .build() organizer.applyTransaction( WindowContainerTransaction().createTaskFragment(fragmentOptions), 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/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/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/scene/shared/model/SceneDataSource.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt new file mode 100644 index 000000000000..f7b45e547b7f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt @@ -0,0 +1,41 @@ +/* + * 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.scene.shared.model + +import kotlinx.coroutines.flow.StateFlow + +/** Defines interface for classes that provide access to scene state. */ +interface SceneDataSource { + + /** + * The current scene, as seen by the real data source in the UI layer. + * + * During a transition between two scenes, the original scene will still be reflected in + * [currentScene] until a time when the UI layer decides to commit the change, which is when + * [currentScene] will have the value of the target/new scene. + */ + val currentScene: StateFlow<SceneKey> + + /** + * Asks for an asynchronous scene switch to [toScene], which will use the corresponding + * installed transition or the one specified by [transitionKey], if provided. + */ + fun changeScene( + toScene: SceneKey, + transitionKey: TransitionKey? = null, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt new file mode 100644 index 000000000000..a50830c1f212 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt @@ -0,0 +1,85 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.scene.shared.model + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.stateIn + +/** + * Delegates calls to a runtime-provided [SceneDataSource] or to a no-op implementation if a + * delegate isn't set. + */ +@SysUISingleton +class SceneDataSourceDelegator +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + config: SceneContainerConfig, +) : SceneDataSource { + + private val noOpDelegate = NoOpSceneDataSource(config.initialSceneKey) + private val delegateMutable = MutableStateFlow<SceneDataSource>(noOpDelegate) + + override val currentScene: StateFlow<SceneKey> = + delegateMutable + .flatMapLatest { delegate -> delegate.currentScene } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = config.initialSceneKey, + ) + + override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) { + delegateMutable.value.changeScene( + toScene = toScene, + transitionKey = transitionKey, + ) + } + + /** + * Binds the current, dependency injection provided [SceneDataSource] to the given object. + * + * In other words: once this is invoked, the state and functionality of the [SceneDataSource] + * will be served by the given [delegate]. + * + * If `null` is passed in, the delegator will use a no-op implementation of [SceneDataSource]. + * + * This removes any previously set delegate. + */ + fun setDelegate(delegate: SceneDataSource?) { + delegateMutable.value = delegate ?: noOpDelegate + } + + private class NoOpSceneDataSource( + initialSceneKey: SceneKey, + ) : SceneDataSource { + override val currentScene: StateFlow<SceneKey> = + MutableStateFlow(initialSceneKey).asStateFlow() + override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) = Unit + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKey.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKey.kt new file mode 100644 index 000000000000..87332ae8e084 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKey.kt @@ -0,0 +1,26 @@ +/* + * 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.scene.shared.model + +/** + * Key for a transition. This can be used to specify which transition spec should be used when + * starting the transition between two scenes. + */ +data class TransitionKey( + val debugName: String, + val identity: Any = Object(), +) diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/UserActionDistance.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/UserActionDistance.kt new file mode 100644 index 000000000000..b93f8378c22f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/UserActionDistance.kt @@ -0,0 +1,26 @@ +/* + * 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.scene.shared.model + +interface UserActionDistance { + + /** + * Return the **absolute** distance of the user action (in pixels) given the size of the scene + * we are animating from and the orientation. + */ + fun absoluteDistance(fromSceneWidth: Int, fromSceneHeight: Int, isHorizontal: Boolean): Float +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/UserActionResult.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/UserActionResult.kt new file mode 100644 index 000000000000..e1b96e4db938 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/UserActionResult.kt @@ -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.systemui.scene.shared.model + +data class UserActionResult( + + /** The scene we should be transitioning due to the [UserAction]. */ + val toScene: SceneKey, + + /** + * The distance the action takes to animate from 0% to 100%. + * + * If `null`, a default distance will be used depending on the [UserAction] performed. + */ + val distance: UserActionDistance? = null, + + /** + * The key of the transition that should be used, if a specific one should be used. + * + * If `null`, the transition used will be the corresponding transition from the collection + * passed into the UI layer. + */ + val transitionKey: TransitionKey? = null, +) 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/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt index de3a6262eafd..c8d6abe18ac3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt @@ -311,12 +311,13 @@ object NotificationIconContainerViewBinder { boundViewsByNotifKey[it.notifKey]?.first } val childCount = view.childCount + val toRemove = mutableListOf<View>() for (i in 0 until childCount) { val actual = view.getChildAt(i) val expected = expectedChildren.getOrNull(i) if (expected == null) { Log.wtf(TAG, "[$logTag] Unexpected child $actual") - view.removeView(actual) + toRemove.add(actual) continue } if (actual === expected) { @@ -325,6 +326,9 @@ object NotificationIconContainerViewBinder { view.removeView(expected) view.addView(expected, i) } + for (child in toRemove) { + view.removeView(child) + } } } } 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/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java index 9f4a90658b2e..f39762766633 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java @@ -444,7 +444,8 @@ public class SecurityControllerImpl implements SecurityController { UserHandle.of(userId))) { boolean hasCACerts = !(conn.getService().getUserCaAliases().getList().isEmpty()); idWithCert = new Pair<Integer, Boolean>(userId, hasCACerts); - } catch (RemoteException | InterruptedException | AssertionError e) { + } catch (RemoteException | InterruptedException | AssertionError + | IllegalStateException e) { Log.i(TAG, "failed to get CA certs", e); idWithCert = new Pair<Integer, Boolean>(userId, null); } finally { diff --git a/packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt b/packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt index 0fe2283c8543..f23fbee18904 100644 --- a/packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt +++ b/packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt @@ -34,7 +34,7 @@ import org.junit.runner.RunWith @RunWithLooper class AnimatorTestRuleIsolationTest : SysuiTestCase() { - @get:Rule val animatorTestRule = AnimatorTestRule() + @get:Rule val animatorTestRule = AnimatorTestRule(this) @Test fun testA() { diff --git a/packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt b/packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt index cc7f7e4067d9..fd5f1573ba5e 100644 --- a/packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt +++ b/packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt @@ -31,7 +31,7 @@ import org.junit.runner.RunWith @RunWithLooper class AnimatorTestRulePrecisionTest : SysuiTestCase() { - @get:Rule val animatorTestRule = AnimatorTestRule() + @get:Rule val animatorTestRule = AnimatorTestRule(this) var value1: Float = -1f var value2: Float = -1f 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/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java index fad85526dec9..e893eb196039 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java @@ -56,7 +56,7 @@ import java.lang.reflect.Field; public class KeyguardStatusViewControllerTest extends KeyguardStatusViewControllerBaseTest { @Rule - public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this); @Test public void dozeTimeTick_updatesSlice() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java index ba27fcd49fac..dd428f562bd1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java @@ -48,7 +48,7 @@ import org.junit.runner.RunWith; public class ExpandHelperTest extends SysuiTestCase { @Rule - public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this); private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); private ExpandableNotificationRow mRow; diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java index e006d59cad82..64936862bc20 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java @@ -82,7 +82,7 @@ import java.util.function.Supplier; public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { @Rule - public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this); @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private static final float DEFAULT_SCALE = 4.0f; diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java index 2225ad6e49d7..f1b0c1874511 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java @@ -129,7 +129,8 @@ import java.util.concurrent.atomic.AtomicInteger; public class WindowMagnificationControllerTest extends SysuiTestCase { @Rule - public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + // NOTE: pass 'null' to allow this test advances time on the main thread. + public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(null); @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java index a35a50982a97..08b49e7d2745 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java @@ -132,7 +132,7 @@ import java.util.function.Supplier; public class WindowMagnificationControllerWindowlessMagnifierTest extends SysuiTestCase { @Rule - public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this); @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt index 2b51ac5e3187..f07932c0de69 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt @@ -19,7 +19,6 @@ package com.android.systemui.animation import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import androidx.core.animation.doOnEnd -import androidx.test.filters.FlakyTest import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.util.doOnEnd @@ -31,10 +30,9 @@ import org.junit.runner.RunWith @RunWith(AndroidTestingRunner::class) @SmallTest @RunWithLooper -@FlakyTest(bugId = 302149604) class AnimatorTestRuleOrderTest : SysuiTestCase() { - @get:Rule val animatorTestRule = AnimatorTestRule() + @get:Rule val animatorTestRule = AnimatorTestRule(this) var value1: Float = -1f var value2: Float = -1f 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/binder/KeyguardSurfaceBehindParamsApplierTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplierTest.kt index 5b29a867ceaf..7787a7fa6af1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplierTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplierTest.kt @@ -43,7 +43,7 @@ import org.mockito.MockitoAnnotations @RunWithLooper(setAsMainLooper = true) @kotlinx.coroutines.ExperimentalCoroutinesApi class KeyguardSurfaceBehindParamsApplierTest : SysuiTestCase() { - @get:Rule val animatorTestRule = AnimatorTestRule() + @get:Rule val animatorTestRule = AnimatorTestRule(this) private lateinit var underTest: KeyguardSurfaceBehindParamsApplier private lateinit var executor: FakeExecutor 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/tileimpl/QSIconViewImplTest_311121830.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt index bbae0c90170a..ae2a9add4fb7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt @@ -40,7 +40,7 @@ import org.junit.runner.RunWith @SmallTest class QSIconViewImplTest_311121830 : SysuiTestCase() { - @get:Rule val animatorRule = AnimatorTestRule() + @get:Rule val animatorRule = AnimatorTestRule(this) @Test fun alwaysLastIcon() { 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/events/SystemEventChipAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt index 8be2ef008039..452302d4db8a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt @@ -51,7 +51,7 @@ import org.mockito.MockitoAnnotations class SystemEventChipAnimationControllerTest : SysuiTestCase() { private lateinit var controller: SystemEventChipAnimationController - @get:Rule val animatorTestRule = AnimatorTestRule() + @get:Rule val animatorTestRule = AnimatorTestRule(this) @Mock private lateinit var sbWindowController: StatusBarWindowController @Mock private lateinit var insetsProvider: StatusBarContentInsetsProvider diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt index 875fe58e5b50..cacfa8dc0be4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt @@ -76,7 +76,7 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { private lateinit var chipAnimationController: SystemEventChipAnimationController private lateinit var systemStatusAnimationScheduler: SystemStatusAnimationScheduler - @get:Rule val animatorTestRule = AnimatorTestRule() + @get:Rule val animatorTestRule = AnimatorTestRule(this) @Before fun setup() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt index 039fef9c1df5..82093adcb75c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt @@ -54,7 +54,7 @@ import org.mockito.Mockito.verifyNoMoreInteractions @TestableLooper.RunWithLooper(setAsMainLooper = true) class NotificationWakeUpCoordinatorTest : SysuiTestCase() { - @get:Rule val animatorTestRule = AnimatorTestRule() + @get:Rule val animatorTestRule = AnimatorTestRule(this) private val kosmos = Kosmos() 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..269b70fe6dfb 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()) @@ -232,6 +257,10 @@ class PhoneStatusBarViewTest : SysuiTestCase() { /* privacyIndicatorBounds = */ PrivacyIndicatorBounds(), /* displayShape = */ DisplayShape.NONE, /* compatInsetsTypes = */ 0, - /* compatIgnoreVisibility = */ false + /* compatIgnoreVisibility = */ false, + /* typeBoundingRectsMap = */ arrayOf(), + /* typeMaxBoundingRectsMap = */ arrayOf(), + /* frameWidth = */ 0, + /* frameHeight = */ 0 ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index 54d3607c7a83..3da5ab9f9d3d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -131,7 +131,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Rule - public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this); private List<StatusBarWindowStateListener> mStatusBarWindowStateListeners = new ArrayList<>(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt index 2ce060c5d097..997c00cf49a4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt @@ -42,7 +42,7 @@ class MultiSourceMinAlphaControllerTest : SysuiTestCase() { private val multiSourceMinAlphaController = MultiSourceMinAlphaController(view, initialAlpha = INITIAL_ALPHA) - @get:Rule val animatorTestRule = AnimatorTestRule() + @get:Rule val animatorTestRule = AnimatorTestRule(this) @Before fun setup() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java index 658e6b01d7b4..13167b2d281f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java @@ -112,7 +112,7 @@ public class RemoteInputViewTest extends SysuiTestCase { private final UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake(); @Rule - public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this); @Before public void setUp() throws Exception { diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index 8a33778f320a..25a0bc4fba61 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -157,7 +157,7 @@ public class VolumeDialogImplTest extends SysuiTestCase { private FakeSettings mSecureSettings; @Rule - public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this); @Before public void setup() throws Exception { diff --git a/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java index 1afe56f27be7..5860c2dd4cdc 100644 --- a/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java +++ b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java @@ -16,14 +16,21 @@ package android.animation; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + import android.animation.AnimationHandler.AnimationFrameCallback; import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Looper; import android.os.SystemClock; +import android.testing.TestableLooper; +import android.testing.TestableLooper.RunnableWithException; import android.util.AndroidRuntimeException; import android.util.Singleton; import android.view.Choreographer; +import android.view.animation.AnimationUtils; + +import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.util.Preconditions; @@ -74,25 +81,31 @@ public final class AnimatorTestRule implements TestRule { return new TestHandler(); } }; + private final Object mTest; private final long mStartTime; private long mTotalTimeDelta = 0; + private volatile boolean mCanLockAnimationClock; + private Looper mLooperWithLockedAnimationClock; /** - * Construct an AnimatorTestRule with a custom start time. - * @see #AnimatorTestRule() + * Construct an AnimatorTestRule with access to the test instance and a custom start time. + * @see #AnimatorTestRule(Object) */ - public AnimatorTestRule(long startTime) { + public AnimatorTestRule(Object test, long startTime) { + mTest = test; mStartTime = startTime; } /** - * Construct an AnimatorTestRule with a start time of {@link SystemClock#uptimeMillis()}. - * Initializing the start time with this clock reduces the discrepancies with various internals - * of classes like ValueAnimator which can sometimes read that clock via - * {@link android.view.animation.AnimationUtils#currentAnimationTimeMillis()}. + * Construct an AnimatorTestRule for the given test instance with a start time of + * {@link SystemClock#uptimeMillis()}. Initializing the start time with this clock reduces the + * discrepancies with various internals of classes like ValueAnimator which can sometimes read + * that clock via {@link android.view.animation.AnimationUtils#currentAnimationTimeMillis()}. + * + * @param test the test instance used to access the {@link TestableLooper} used by the class. */ - public AnimatorTestRule() { - this(SystemClock.uptimeMillis()); + public AnimatorTestRule(Object test) { + this(test, SystemClock.uptimeMillis()); } @NonNull @@ -102,10 +115,16 @@ public final class AnimatorTestRule implements TestRule { @Override public void evaluate() throws Throwable { final TestHandler testHandler = mTestHandler.get(); - AnimationHandler objAtStart = AnimationHandler.setTestHandler(testHandler); + final AnimationHandler objAtStart = AnimationHandler.setTestHandler(testHandler); + final RunnableWithException lockClock = + wrapWithRunBlocking(new LockAnimationClockRunnable()); + final RunnableWithException unlockClock = + wrapWithRunBlocking(new UnlockAnimationClockRunnable()); try { + lockClock.run(); base.evaluate(); } finally { + unlockClock.run(); AnimationHandler objAtEnd = AnimationHandler.setTestHandler(objAtStart); if (testHandler != objAtEnd) { // pass or fail, inner logic not restoring the handler needs to be reported. @@ -118,6 +137,78 @@ public final class AnimatorTestRule implements TestRule { }; } + private RunnableWithException wrapWithRunBlocking(RunnableWithException runnable) { + RunnableWithException wrapped = TestableLooper.wrapWithRunBlocking(mTest, runnable); + if (wrapped != null) { + return wrapped; + } + return () -> runOnMainThrowing(runnable); + } + + private static void runOnMainThrowing(RunnableWithException runnable) throws Exception { + if (Looper.myLooper() == Looper.getMainLooper()) { + runnable.run(); + } else { + final Throwable[] throwableBox = new Throwable[1]; + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + try { + runnable.run(); + } catch (Throwable t) { + throwableBox[0] = t; + } + }); + if (throwableBox[0] == null) { + return; + } else if (throwableBox[0] instanceof RuntimeException ex) { + throw ex; + } else if (throwableBox[0] instanceof Error err) { + throw err; + } else { + throw new RuntimeException(throwableBox[0]); + } + } + } + + private class LockAnimationClockRunnable implements RunnableWithException { + @Override + public void run() { + mLooperWithLockedAnimationClock = Looper.myLooper(); + mCanLockAnimationClock = true; + lockAnimationClockToCurrentTime(); + } + } + + private class UnlockAnimationClockRunnable implements RunnableWithException { + @Override + public void run() { + mCanLockAnimationClock = false; + mLooperWithLockedAnimationClock = null; + AnimationUtils.unlockAnimationClock(); + } + } + + private void lockAnimationClockToCurrentTime() { + if (!mCanLockAnimationClock) { + throw new AssertionError("Unable to lock the animation clock; " + + "has the test started? already finished?"); + } + if (mLooperWithLockedAnimationClock != Looper.myLooper()) { + throw new AssertionError("Animation clock being locked on " + Looper.myLooper() + + " but should only be locked on " + mLooperWithLockedAnimationClock); + } + long desiredTime = getCurrentTime(); + AnimationUtils.lockAnimationClock(desiredTime); + if (!mCanLockAnimationClock) { + AnimationUtils.unlockAnimationClock(); + throw new AssertionError("Threading error when locking the animation clock"); + } + long outputTime = AnimationUtils.currentAnimationTimeMillis(); + if (outputTime != desiredTime) { + throw new AssertionError("currentAnimationTimeMillis() is " + outputTime + + " after locking to " + desiredTime); + } + } + /** * If any new {@link Animator}s have been registered since the last time the frame time was * advanced, initialize them with the current frame time. Failing to do this will result in the @@ -178,6 +269,7 @@ public final class AnimatorTestRule implements TestRule { // advance time mTotalTimeDelta += timeDelta; } + lockAnimationClockToCurrentTime(); if (preFrameAction != null) { preFrameAction.accept(timeDelta); // After letting other code run, clear any new callbacks to avoid double-ticking them diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/AnimatorTestRule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/AnimatorTestRule.kt index ba9c5eda1b63..e2fc44fd2d0d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/AnimatorTestRule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/AnimatorTestRule.kt @@ -26,12 +26,16 @@ import org.junit.runners.model.Statement * A rule that wraps both [androidx.core.animation.AnimatorTestRule] and * [android.animation.AnimatorTestRule] such that the clocks of the two animation handlers can be * advanced together. + * + * @param test the instance of the test used to look up the TestableLooper. If a TestableLooper is + * found, the time can only be advanced on that thread; otherwise the time must be advanced on the + * main thread. */ -class AnimatorTestRule : TestRule { +class AnimatorTestRule(test: Any?) : TestRule { // Create the androidx rule, which initializes start time to SystemClock.uptimeMillis(), // then copy that time to the platform rule so that the two clocks are in sync. private val androidxRule = androidx.core.animation.AnimatorTestRule() - private val platformRule = android.animation.AnimatorTestRule(androidxRule.startTime) + private val platformRule = android.animation.AnimatorTestRule(test, androidxRule.startTime) private val advanceAndroidXTimeBy = Consumer<Long> { timeDelta -> androidxRule.advanceTimeBy(timeDelta) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt new file mode 100644 index 000000000000..c208aad78295 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt @@ -0,0 +1,82 @@ +/* + * 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.scene.shared.model + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeSceneDataSource( + initialSceneKey: SceneKey, +) : SceneDataSource { + + private val _currentScene = MutableStateFlow(initialSceneKey) + override val currentScene: StateFlow<SceneKey> = _currentScene.asStateFlow() + + var isPaused = false + private set + var pendingScene: SceneKey? = null + private set + + override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) { + if (isPaused) { + pendingScene = toScene + } else { + _currentScene.value = toScene + } + } + + /** + * Pauses scene changes. + * + * Any following calls to [changeScene] will be conflated and the last one will be remembered. + */ + fun pause() { + check(!isPaused) { "Can't pause what's already paused!" } + + isPaused = true + } + + /** + * Unpauses scene changes. + * + * If there were any calls to [changeScene] since [pause] was called, the latest of the bunch + * will be replayed. + * + * If [force] is `true`, there will be no check that [isPaused] is true. + * + * If [expectedScene] is provided, will assert that it's indeed the latest called. + */ + fun unpause( + force: Boolean = false, + expectedScene: SceneKey? = null, + ) { + check(force || isPaused) { "Can't unpause what's already not paused!" } + + isPaused = false + pendingScene?.let { _currentScene.value = it } + pendingScene = null + + check(expectedScene == null || currentScene.value == expectedScene) { + """ + Unexpected scene while unpausing. + Expected $expectedScene but was $currentScene. + """ + .trimIndent() + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneDataSourceKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneDataSourceKosmos.kt new file mode 100644 index 000000000000..f5196866ae6f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneDataSourceKosmos.kt @@ -0,0 +1,39 @@ +/* + * 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.scene.shared.model + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.scene.initialSceneKey +import com.android.systemui.scene.sceneContainerConfig + +val Kosmos.fakeSceneDataSource by Fixture { + FakeSceneDataSource( + initialSceneKey = initialSceneKey, + ) +} + +val Kosmos.sceneDataSourceDelegator by Fixture { + SceneDataSourceDelegator( + applicationScope = applicationCoroutineScope, + config = sceneContainerConfig, + ) + .apply { setDelegate(fakeSceneDataSource) } +} + +val Kosmos.sceneDataSource by Fixture { sceneDataSourceDelegator } diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp index 1ac69f6c4fc8..35ce4814f97d 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -58,11 +58,14 @@ java_library { visibility: ["//visibility:public"], } -java_host_for_device { - name: "core-xml-for-device", - libs: [ - "core-xml-for-host", +java_library { + // Prefixed with "200" to ensure it's sorted early in Tradefed classpath + // so that we provide a concrete implementation before Mainline stubs + name: "200-kxml2-android", + static_libs: [ + "kxml2-android", ], + visibility: ["//frameworks/base"], } java_host_for_device { diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING index 031984829e77..a5b28ad550ba 100644 --- a/ravenwood/TEST_MAPPING +++ b/ravenwood/TEST_MAPPING @@ -2,6 +2,9 @@ "presubmit": [ { "name": "RavenwoodMockitoTest_device" + }, + { + "name": "RavenwoodBivalentTest_device" } ], "ravenwood-presubmit": [ @@ -16,6 +19,14 @@ { "name": "CtsUtilTestCasesRavenwood", "host": true + }, + { + "name": "RavenwoodCoreTest", + "host": true + }, + { + "name": "RavenwoodBivalentTest", + "host": true } ] } diff --git a/ravenwood/bivalenttest/Android.bp b/ravenwood/bivalenttest/Android.bp new file mode 100644 index 000000000000..acf85331601e --- /dev/null +++ b/ravenwood/bivalenttest/Android.bp @@ -0,0 +1,47 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_ravenwood_test { + name: "RavenwoodBivalentTest", + + static_libs: [ + "androidx.annotation_annotation", + "androidx.test.ext.junit", + "androidx.test.rules", + ], + srcs: [ + "test/**/*.java", + ], + sdk_version: "test_current", + auto_gen_config: true, +} + +android_test { + name: "RavenwoodBivalentTest_device", + + srcs: [ + "test/**/*.java", + ], + static_libs: [ + "junit", + "truth", + + "androidx.annotation_annotation", + "androidx.test.ext.junit", + "androidx.test.rules", + + "ravenwood-junit", + ], + test_suites: [ + "device-tests", + ], + optimize: { + enabled: false, + }, +} diff --git a/ravenwood/bivalenttest/AndroidManifest.xml b/ravenwood/bivalenttest/AndroidManifest.xml new file mode 100644 index 000000000000..474c03f4f3c9 --- /dev/null +++ b/ravenwood/bivalenttest/AndroidManifest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.ravenwood.bivalenttest"> + + <application android:debuggable="true" > + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.ravenwood.bivalenttest" + /> +</manifest> diff --git a/ravenwood/bivalenttest/AndroidTest.xml b/ravenwood/bivalenttest/AndroidTest.xml new file mode 100644 index 000000000000..ac4695bb9a50 --- /dev/null +++ b/ravenwood/bivalenttest/AndroidTest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<configuration description="Runs Frameworks Services Tests."> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-instrumentation" /> + + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="RavenwoodBivalentTest_device.apk" /> + </target_preparer> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.ravenwood.bivalenttest" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + </test> +</configuration> diff --git a/ravenwood/bivalenttest/README.md b/ravenwood/bivalenttest/README.md new file mode 100644 index 000000000000..71535556ec75 --- /dev/null +++ b/ravenwood/bivalenttest/README.md @@ -0,0 +1,3 @@ +# Ravenwood bivalent test + +This test contains bivalent tests for Ravenwood itself.
\ No newline at end of file diff --git a/ravenwood/bivalenttest/test/com/android/platform/test/ravenwood/bivalenttest/RavenwoodRuleTest.java b/ravenwood/bivalenttest/test/com/android/platform/test/ravenwood/bivalenttest/RavenwoodRuleTest.java new file mode 100644 index 000000000000..4b650b4bc5e1 --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/platform/test/ravenwood/bivalenttest/RavenwoodRuleTest.java @@ -0,0 +1,47 @@ +/* + * 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.platform.test.ravenwood.bivalenttest; + +import android.platform.test.annotations.DisabledOnNonRavenwood; +import android.platform.test.annotations.DisabledOnRavenwood; +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class RavenwoodRuleTest { + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule(); + + @Test + @DisabledOnRavenwood + public void testDeviceOnly() { + Assert.assertFalse(RavenwoodRule.isOnRavenwood()); + } + + @Test + @DisabledOnNonRavenwood + public void testRavenwoodOnly() { + Assert.assertTrue(RavenwoodRule.isOnRavenwood()); + } + + // TODO: Add more tests +} diff --git a/ravenwood/coretest/Android.bp b/ravenwood/coretest/Android.bp index 9b7f8f7d2171..a78c5c1e8227 100644 --- a/ravenwood/coretest/Android.bp +++ b/ravenwood/coretest/Android.bp @@ -12,9 +12,9 @@ android_ravenwood_test { static_libs: [ "androidx.annotation_annotation", + "androidx.test.ext.junit", "androidx.test.rules", ], - srcs: [ "test/**/*.java", ], diff --git a/ravenwood/coretest/README.md b/ravenwood/coretest/README.md new file mode 100644 index 000000000000..b60bfbfcb6f4 --- /dev/null +++ b/ravenwood/coretest/README.md @@ -0,0 +1,3 @@ +# Ravenwood core test + +This test contains (non-bivalent) tests for Ravenwood itself -- e.g. tests for the ravenwood rules.
\ No newline at end of file diff --git a/ravenwood/coretest/test/com/android/platform/test/ravenwood/coretest/RavenwoodTestRunnerValidationTest.java b/ravenwood/coretest/test/com/android/platform/test/ravenwood/coretest/RavenwoodTestRunnerValidationTest.java index e58c282fb690..2cd585ff6c9c 100644 --- a/ravenwood/coretest/test/com/android/platform/test/ravenwood/coretest/RavenwoodTestRunnerValidationTest.java +++ b/ravenwood/coretest/test/com/android/platform/test/ravenwood/coretest/RavenwoodTestRunnerValidationTest.java @@ -17,7 +17,7 @@ package com.android.platform.test.ravenwood.coretest; import android.platform.test.ravenwood.RavenwoodRule; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.runner.AndroidJUnit4; // Intentionally use the deprecated one. import org.junit.Assume; import org.junit.Rule; diff --git a/ravenwood/framework-minus-apex-ravenwood-policies.txt b/ravenwood/framework-minus-apex-ravenwood-policies.txt index 022961164b84..2eef0cdaf24b 100644 --- a/ravenwood/framework-minus-apex-ravenwood-policies.txt +++ b/ravenwood/framework-minus-apex-ravenwood-policies.txt @@ -89,6 +89,7 @@ class android.util.UtilConfig stubclass # Internals class com.android.internal.util.FileRotator stubclass +class com.android.internal.util.FastXmlSerializer stubclass class com.android.internal.util.HexDump stubclass class com.android.internal.util.MessageUtils stubclass class com.android.internal.util.Preconditions stubclass diff --git a/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java b/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java new file mode 100644 index 000000000000..8ca34bafc2c6 --- /dev/null +++ b/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java @@ -0,0 +1,47 @@ +/* + * 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.platform.test.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Tests marked with this annotation are only executed when running on Ravenwood, but not + * on a device. + * + * This is basically equivalent to the opposite of {@link DisabledOnRavenwood}, but in order to + * avoid complex structure, and there's no equivalent to the opposite {@link EnabledOnRavenwood}, + * which means if a test class has this annotation, you can't negate it in subclasses or + * on a per-method basis. + * + * The {@code RAVENWOOD_RUN_DISABLED_TESTS} environmental variable won't work because it won't be + * propagated to the device. (We may support it in the future, possibly using a debug. sysprop.) + * + * @hide + */ +@Inherited +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface DisabledOnNonRavenwood { + /** + * General free-form description of why this test is being ignored. + */ + String reason() default ""; +} diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java index 8d76970f9ad4..9a4d4886d6f0 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java @@ -18,6 +18,7 @@ package android.platform.test.ravenwood; import static android.platform.test.ravenwood.RavenwoodRule.ENABLE_PROBE_IGNORED; import static android.platform.test.ravenwood.RavenwoodRule.IS_ON_RAVENWOOD; +import static android.platform.test.ravenwood.RavenwoodRule.shouldEnableOnDevice; import static android.platform.test.ravenwood.RavenwoodRule.shouldEnableOnRavenwood; import static android.platform.test.ravenwood.RavenwoodRule.shouldStillIgnoreInProbeIgnoreMode; @@ -42,6 +43,7 @@ public class RavenwoodClassRule implements TestRule { public Statement apply(Statement base, Description description) { // No special treatment when running outside Ravenwood; run tests as-is if (!IS_ON_RAVENWOOD) { + Assume.assumeTrue(shouldEnableOnDevice(description)); return base; } diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java index 764573defd7a..b90f112c1655 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java @@ -22,6 +22,7 @@ import static android.os.UserHandle.USER_SYSTEM; import static org.junit.Assert.fail; +import android.platform.test.annotations.DisabledOnNonRavenwood; import android.platform.test.annotations.DisabledOnRavenwood; import android.platform.test.annotations.EnabledOnRavenwood; import android.platform.test.annotations.IgnoreUnderRavenwood; @@ -211,6 +212,15 @@ public class RavenwoodRule implements TestRule { return IS_ON_RAVENWOOD; } + static boolean shouldEnableOnDevice(Description description) { + if (description.isTest()) { + if (description.getAnnotation(DisabledOnNonRavenwood.class) != null) { + return false; + } + } + return true; + } + /** * Determine if the given {@link Description} should be enabled when running on the * Ravenwood test environment. @@ -271,6 +281,7 @@ public class RavenwoodRule implements TestRule { public Statement apply(Statement base, Description description) { // No special treatment when running outside Ravenwood; run tests as-is if (!IS_ON_RAVENWOOD) { + Assume.assumeTrue(shouldEnableOnDevice(description)); return base; } diff --git a/ravenwood/minimum-test/README.md b/ravenwood/minimum-test/README.md new file mode 100644 index 000000000000..6b0abe968053 --- /dev/null +++ b/ravenwood/minimum-test/README.md @@ -0,0 +1,3 @@ +# Ravenwood minimum test + +This directory contains a minimum possible ravenwood test -- no extra dependencies, etc.
\ No newline at end of file diff --git a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java b/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java index 03cfad5c7a70..b4771171815a 100644 --- a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java +++ b/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java @@ -28,7 +28,7 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class RavenwoodMinimumTest { @Rule - public RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() .setProcessApp() .build(); 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/AndroidTest.xml b/ravenwood/mockito/AndroidTest.xml index 96bc2752fe95..5ba9b1ff2cd8 100644 --- a/ravenwood/mockito/AndroidTest.xml +++ b/ravenwood/mockito/AndroidTest.xml @@ -22,8 +22,6 @@ <option name="test-file-name" value="RavenwoodMockitoTest_device.apk" /> </target_preparer> - <option name="test-tag" value="FrameworksMockingServicesTests" /> - <test class="com.android.tradefed.testtype.AndroidJUnitTest" > <option name="package" value="com.android.ravenwood.mockitotest" /> <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> diff --git a/ravenwood/mockito/README.md b/ravenwood/mockito/README.md new file mode 100644 index 000000000000..4ceb795fe14a --- /dev/null +++ b/ravenwood/mockito/README.md @@ -0,0 +1,3 @@ +# Ravenwood mockito test + +This directory contains a sample bivalent test using Mockito.
\ No newline at end of file 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/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java index 2c5038940e98..df4e699e3926 100644 --- a/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java +++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java @@ -35,6 +35,7 @@ import android.content.Context; import android.content.pm.ParceledListSlice; import android.os.Binder; import android.os.IBinder; +import android.os.IRemoteCallback; import android.os.Process; import android.os.ResultReceiver; import android.os.ShellCallback; @@ -162,6 +163,13 @@ public class AppPredictionManagerService extends (service) -> service.onDestroyPredictionSessionLocked(sessionId)); } + @Override + public void requestServiceFeatures(@NonNull AppPredictionSessionId sessionId, + IRemoteCallback callback) { + runForUserLocked("requestServiceFeatures", sessionId, + (service) -> service.requestServiceFeaturesLocked(sessionId, callback)); + } + public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out, @Nullable FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback callback, diff --git a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java index 84707a8d9c00..a0198f2fe240 100644 --- a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java +++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java @@ -31,6 +31,7 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ParceledListSlice; import android.content.pm.ServiceInfo; import android.os.IBinder; +import android.os.IRemoteCallback; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.provider.DeviceConfig; @@ -237,6 +238,18 @@ public class AppPredictionPerUserService extends sessionInfo.destroy(); } + /** + * Requests the service to provide AppPredictionService features info. + */ + @GuardedBy("mLock") + public void requestServiceFeaturesLocked(@NonNull AppPredictionSessionId sessionId, + @NonNull IRemoteCallback callback) { + final AppPredictionSessionInfo sessionInfo = mSessionInfos.get(sessionId); + if (sessionInfo == null) return; + resolveService(sessionId, true, sessionInfo.mUsesPeopleService, + s -> s.requestServiceFeatures(sessionId, callback)); + } + @Override public void onFailureOrTimeout(boolean timedOut) { if (isDebug()) { diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index cd45b03ba7ad..b8f6b3f3a988 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -480,9 +480,14 @@ public final class ActiveServices { /** * The available ANR timers. */ + // ActivityManagerConstants.SERVICE_TIMEOUT/ActivityManagerConstants.SERVICE_BACKGROUND_TIMEOUT private final ProcessAnrTimer mActiveServiceAnrTimer; + // see ServiceRecord$ShortFgsInfo#getAnrTime() private final ServiceAnrTimer mShortFGSAnrTimer; + // ActivityManagerConstants.DEFAULT_SERVICE_START_FOREGROUND_TIMEOUT_MS private final ServiceAnrTimer mServiceFGAnrTimer; + // see ServiceRecord#getEarliestStopTypeAndTime() + private final ServiceAnrTimer mFGSAnrTimer; // allowlisted packageName. ArraySet<String> mAllowListWhileInUsePermissionInFgs = new ArraySet<>(); @@ -744,10 +749,13 @@ public final class ActiveServices { "SERVICE_TIMEOUT"); this.mShortFGSAnrTimer = new ServiceAnrTimer(service, ActivityManagerService.SERVICE_SHORT_FGS_ANR_TIMEOUT_MSG, - "FGS_TIMEOUT"); + "SHORT_FGS_TIMEOUT"); this.mServiceFGAnrTimer = new ServiceAnrTimer(service, ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG, "SERVICE_FOREGROUND_TIMEOUT"); + this.mFGSAnrTimer = new ServiceAnrTimer(service, + ActivityManagerService.SERVICE_FGS_ANR_TIMEOUT_MSG, + "FGS_TIMEOUT"); } void systemServicesReady() { @@ -1570,6 +1578,7 @@ public final class ActiveServices { } maybeStopShortFgsTimeoutLocked(service); + maybeStopFgsTimeoutLocked(service); final int uid = service.appInfo.uid; final String packageName = service.name.getPackageName(); @@ -1758,6 +1767,7 @@ public final class ActiveServices { } maybeStopShortFgsTimeoutLocked(r); + maybeStopFgsTimeoutLocked(r); final int uid = r.appInfo.uid; final String packageName = r.name.getPackageName(); @@ -2243,6 +2253,8 @@ public final class ActiveServices { // Whether to extend the SHORT_SERVICE time out. boolean extendShortServiceTimeout = false; + // Whether to extend the timeout for a time-limited FGS type. + boolean extendFgsTimeout = false; // Whether setFgsRestrictionLocked() is called in here. Only used for logging. boolean fgsRestrictionRecalculated = false; @@ -2287,6 +2299,19 @@ public final class ActiveServices { final boolean isOldTypeShortFgsAndTimedOut = r.shouldTriggerShortFgsTimeout(nowUptime); + // Calling startForeground on a FGS type which has a time limit will only be + // allowed if the app is in a state where it can normally start another FGS. + // The timeout will behave as follows: + // A) <TIME_LIMITED_TYPE> -> another <TIME_LIMITED_TYPE> + // - If the start succeeds, the timeout is reset. + // B) <TIME_LIMITED_TYPE> -> non-time-limited type + // - If the start succeeds, the timeout will stop. + // C) non-time-limited type -> <TIME_LIMITED_TYPE> + // - If the start succeeds, the timeout will start. + final boolean isOldTypeTimeLimited = r.isFgsTimeLimited(); + final boolean isNewTypeTimeLimited = + r.canFgsTypeTimeOut(foregroundServiceType); + // If true, we skip the BFSL check. boolean bypassBfslCheck = false; @@ -2355,6 +2380,35 @@ public final class ActiveServices { // "if (r.mAllowStartForeground == REASON_DENIED...)" block below. } } + } else if (r.isForeground && isOldTypeTimeLimited) { + + // See if the app could start an FGS or not. + r.clearFgsAllowStart(); + setFgsRestrictionLocked(r.serviceInfo.packageName, r.app.getPid(), + r.appInfo.uid, r.intent.getIntent(), r, r.userId, + BackgroundStartPrivileges.NONE, false /* isBindService */); + fgsRestrictionRecalculated = true; + + final boolean fgsStartAllowed = !isBgFgsRestrictionEnabledForService + || r.isFgsAllowedStart(); + + if (fgsStartAllowed) { + if (isNewTypeTimeLimited) { + // Note: in the future, we may want to look into metrics to see if + // apps are constantly switching between a time-limited type and a + // non-time-limited type or constantly calling startForeground() + // opportunistically on the same type to gain runtime and apply the + // stricter timeout. For now, always extend the timeout if the app + // is in a state where it's allowed to start a FGS. + extendFgsTimeout = true; + } else { + // FGS type is changing from a time-restricted type to one without + // a time limit so proceed as normal. + // The timeout will stop later, in maybeUpdateFgsTrackingLocked(). + } + } else { + // This case will be handled in the BFSL check below. + } } else if (r.mStartForegroundCount == 0) { /* If the service was started with startService(), not @@ -2596,6 +2650,7 @@ public final class ActiveServices { maybeUpdateShortFgsTrackingLocked(r, extendShortServiceTimeout); + maybeUpdateFgsTrackingLocked(r, extendFgsTimeout); } else { if (DEBUG_FOREGROUND_SERVICE) { Slog.d(TAG, "Suppressing startForeground() for FAS " + r); @@ -2631,6 +2686,7 @@ public final class ActiveServices { } maybeStopShortFgsTimeoutLocked(r); + maybeStopFgsTimeoutLocked(r); // Adjust notification handling before setting isForeground to false, because // that state is relevant to the notification policy side. @@ -3608,6 +3664,116 @@ public final class ActiveServices { } } + void onFgsTimeout(ServiceRecord sr) { + synchronized (mAm) { + final long nowUptime = SystemClock.uptimeMillis(); + final int fgsType = sr.getTimedOutFgsType(nowUptime); + if (fgsType == -1) { + mFGSAnrTimer.discard(sr); + return; + } + Slog.e(TAG_SERVICE, "FGS (" + ServiceInfo.foregroundServiceTypeToLabel(fgsType) + + ") timed out: " + sr); + mFGSAnrTimer.accept(sr); + traceInstant("FGS timed out: ", sr); + + logFGSStateChangeLocked(sr, + FOREGROUND_SERVICE_STATE_CHANGED__STATE__TIMED_OUT, + nowUptime > sr.mFgsEnterTime ? (int) (nowUptime - sr.mFgsEnterTime) : 0, + FGS_STOP_REASON_UNKNOWN, + FGS_TYPE_POLICY_CHECK_UNKNOWN, + FOREGROUND_SERVICE_STATE_CHANGED__FGS_START_API__FGSSTARTAPI_NA, + false /* fgsRestrictionRecalculated */ + ); + try { + sr.app.getThread().scheduleTimeoutServiceForType(sr, sr.getLastStartId(), fgsType); + } catch (RemoteException e) { + Slog.w(TAG_SERVICE, "Exception from scheduleTimeoutServiceForType: " + e); + } + + // ANR the service after giving the service some time to clean up. + // ServiceRecord.getEarliestStopTypeAndTime() is an absolute time with a reference that + // is not "now". Compute the time from "now" when starting the anr timer. + final long anrTime = sr.getEarliestStopTypeAndTime().second + + mAm.mConstants.mFgsAnrExtraWaitDuration - SystemClock.uptimeMillis(); + mFGSAnrTimer.start(sr, anrTime); + } + } + + private void maybeUpdateFgsTrackingLocked(ServiceRecord sr, boolean extendTimeout) { + if (!sr.isFgsTimeLimited()) { + // Reset timers if they exist. + sr.setIsFgsTimeLimited(false); + mFGSAnrTimer.cancel(sr); + mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr); + return; + } + + if (extendTimeout || !sr.wasFgsPreviouslyTimeLimited()) { + traceInstant("FGS start: ", sr); + sr.setIsFgsTimeLimited(true); + + // We'll restart the timeout. + mFGSAnrTimer.cancel(sr); + mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr); + + final Message msg = mAm.mHandler.obtainMessage( + ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr); + mAm.mHandler.sendMessageAtTime(msg, sr.getEarliestStopTypeAndTime().second); + } + } + + private void maybeStopFgsTimeoutLocked(ServiceRecord sr) { + sr.setIsFgsTimeLimited(false); // reset fgs boolean holding time-limited type state. + if (!sr.isFgsTimeLimited()) { + return; // if none of the types are time-limited, return. + } + Slog.d(TAG_SERVICE, "Stop FGS timeout: " + sr); + mFGSAnrTimer.cancel(sr); + mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr); + } + + boolean hasServiceTimedOutLocked(ComponentName className, IBinder token) { + final int userId = UserHandle.getCallingUserId(); + final long ident = mAm.mInjector.clearCallingIdentity(); + try { + ServiceRecord sr = findServiceLocked(className, token, userId); + if (sr == null) { + return false; + } + final long nowUptime = SystemClock.uptimeMillis(); + return sr.getTimedOutFgsType(nowUptime) != -1; + } finally { + mAm.mInjector.restoreCallingIdentity(ident); + } + } + + void onFgsAnrTimeout(ServiceRecord sr) { + final long nowUptime = SystemClock.uptimeMillis(); + final int fgsType = sr.getTimedOutFgsType(nowUptime); + if (fgsType == -1 || !sr.wasFgsPreviouslyTimeLimited()) { + return; // no timed out FGS type was found + } + final String reason = "A foreground service of type " + + ServiceInfo.foregroundServiceTypeToLabel(fgsType) + + " did not stop within a timeout: " + sr.getComponentName(); + + final TimeoutRecord tr = TimeoutRecord.forFgsTimeout(reason); + + tr.mLatencyTracker.waitingOnAMSLockStarted(); + synchronized (mAm) { + tr.mLatencyTracker.waitingOnAMSLockEnded(); + + Slog.e(TAG_SERVICE, "FGS ANR'ed: " + sr); + traceInstant("FGS ANR: ", sr); + mAm.appNotResponding(sr.app, tr); + + // TODO: Can we close the ANR dialog here, if it's still shown? Currently, the ANR + // dialog really doesn't remember the "cause" (especially if there have been multiple + // ANRs), so it's not doable. + } + } + private void updateAllowlistManagerLocked(ProcessServiceRecord psr) { psr.mAllowlistManager = false; for (int i = psr.numberOfRunningServices() - 1; i >= 0; i--) { @@ -3621,6 +3787,7 @@ public final class ActiveServices { private void stopServiceAndUpdateAllowlistManagerLocked(ServiceRecord service) { maybeStopShortFgsTimeoutLocked(service); + maybeStopFgsTimeoutLocked(service); final ProcessServiceRecord psr = service.app.mServices; psr.stopService(service); psr.updateBoundClientUids(); @@ -5893,6 +6060,7 @@ public final class ActiveServices { Slog.w(TAG_SERVICE, "Short FGS brought down without stopping: " + r); maybeStopShortFgsTimeoutLocked(r); } + maybeStopFgsTimeoutLocked(r); // Report to all of the connections that the service is no longer // available. @@ -6015,6 +6183,7 @@ public final class ActiveServices { final boolean exitingFg = r.isForeground; if (exitingFg) { maybeStopShortFgsTimeoutLocked(r); + maybeStopFgsTimeoutLocked(r); decActiveForegroundAppLocked(smap, r); synchronized (mAm.mProcessStats.mLock) { ServiceState stracker = r.getTracker(); @@ -8643,7 +8812,7 @@ public final class ActiveServices { event = EventLogTags.AM_FOREGROUND_SERVICE_DENIED; } else if (state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__TIMED_OUT) { event = EventLogTags.AM_FOREGROUND_SERVICE_TIMED_OUT; - }else { + } else { // Unknown event. return; } diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java index 72e62c37106d..d97731c85d38 100644 --- a/services/core/java/com/android/server/am/ActivityManagerConstants.java +++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java @@ -1052,6 +1052,27 @@ final class ActivityManagerConstants extends ContentObserver { public volatile long mShortFgsProcStateExtraWaitDuration = DEFAULT_SHORT_FGS_PROC_STATE_EXTRA_WAIT_DURATION; + /** Timeout for a mediaProcessing FGS, in milliseconds. */ + private static final String KEY_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION = + "media_processing_fgs_timeout_duration"; + + /** @see #KEY_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION */ + static final long DEFAULT_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION = 6 * 60 * 60_000; // 6 hours + + /** @see #KEY_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION */ + public volatile long mMediaProcessingFgsTimeoutDuration = + DEFAULT_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION; + + /** Timeout for a dataSync FGS, in milliseconds. */ + private static final String KEY_DATA_SYNC_FGS_TIMEOUT_DURATION = + "data_sync_fgs_timeout_duration"; + + /** @see #KEY_DATA_SYNC_FGS_TIMEOUT_DURATION */ + static final long DEFAULT_DATA_SYNC_FGS_TIMEOUT_DURATION = 6 * 60 * 60_000; // 6 hours + + /** @see #KEY_DATA_SYNC_FGS_TIMEOUT_DURATION */ + public volatile long mDataSyncFgsTimeoutDuration = DEFAULT_DATA_SYNC_FGS_TIMEOUT_DURATION; + /** * If enabled, when starting an application, the system will wait for a * {@link ActivityManagerService#finishAttachApplication} from the app before scheduling @@ -1082,6 +1103,20 @@ final class ActivityManagerConstants extends ContentObserver { public volatile long mShortFgsAnrExtraWaitDuration = DEFAULT_SHORT_FGS_ANR_EXTRA_WAIT_DURATION; + /** + * If a service of a timeout-enforced type doesn't finish within this duration after its + * timeout, then we'll declare an ANR. + * i.e. if the time limit for a type is 1 hour, and this extra duration is 10 seconds, then + * the app will be ANR'ed 1 hour and 10 seconds after it started. + */ + private static final String KEY_FGS_ANR_EXTRA_WAIT_DURATION = "fgs_anr_extra_wait_duration"; + + /** @see #KEY_FGS_ANR_EXTRA_WAIT_DURATION */ + static final long DEFAULT_FGS_ANR_EXTRA_WAIT_DURATION = 10_000; + + /** @see #KEY_FGS_ANR_EXTRA_WAIT_DURATION */ + public volatile long mFgsAnrExtraWaitDuration = DEFAULT_FGS_ANR_EXTRA_WAIT_DURATION; + /** @see #KEY_USE_TIERED_CACHED_ADJ */ public boolean USE_TIERED_CACHED_ADJ = DEFAULT_USE_TIERED_CACHED_ADJ; @@ -1264,9 +1299,18 @@ final class ActivityManagerConstants extends ContentObserver { case KEY_SHORT_FGS_PROC_STATE_EXTRA_WAIT_DURATION: updateShortFgsProcStateExtraWaitDuration(); break; + case KEY_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION: + updateMediaProcessingFgsTimeoutDuration(); + break; + case KEY_DATA_SYNC_FGS_TIMEOUT_DURATION: + updateDataSyncFgsTimeoutDuration(); + break; case KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION: updateShortFgsAnrExtraWaitDuration(); break; + case KEY_FGS_ANR_EXTRA_WAIT_DURATION: + updateFgsAnrExtraWaitDuration(); + break; case KEY_PROACTIVE_KILLS_ENABLED: updateProactiveKillsEnabled(); break; @@ -2064,6 +2108,27 @@ final class ActivityManagerConstants extends ContentObserver { DEFAULT_SHORT_FGS_ANR_EXTRA_WAIT_DURATION); } + private void updateMediaProcessingFgsTimeoutDuration() { + mMediaProcessingFgsTimeoutDuration = DeviceConfig.getLong( + DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, + KEY_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION, + DEFAULT_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION); + } + + private void updateDataSyncFgsTimeoutDuration() { + mDataSyncFgsTimeoutDuration = DeviceConfig.getLong( + DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, + KEY_DATA_SYNC_FGS_TIMEOUT_DURATION, + DEFAULT_DATA_SYNC_FGS_TIMEOUT_DURATION); + } + + private void updateFgsAnrExtraWaitDuration() { + mFgsAnrExtraWaitDuration = DeviceConfig.getLong( + DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, + KEY_FGS_ANR_EXTRA_WAIT_DURATION, + DEFAULT_FGS_ANR_EXTRA_WAIT_DURATION); + } + private void updateEnableWaitForFinishAttachApplication() { mEnableWaitForFinishAttachApplication = DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, @@ -2295,6 +2360,13 @@ final class ActivityManagerConstants extends ContentObserver { pw.print(" "); pw.print(KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION); pw.print("="); pw.println(mShortFgsAnrExtraWaitDuration); + pw.print(" "); pw.print(KEY_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION); + pw.print("="); pw.println(mMediaProcessingFgsTimeoutDuration); + pw.print(" "); pw.print(KEY_DATA_SYNC_FGS_TIMEOUT_DURATION); + pw.print("="); pw.println(mDataSyncFgsTimeoutDuration); + pw.print(" "); pw.print(KEY_FGS_ANR_EXTRA_WAIT_DURATION); + pw.print("="); pw.println(mFgsAnrExtraWaitDuration); + pw.print(" "); pw.print(KEY_USE_TIERED_CACHED_ADJ); pw.print("="); pw.println(USE_TIERED_CACHED_ADJ); pw.print(" "); pw.print(KEY_TIERED_CACHED_ADJ_DECAY_TIME); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 2750344b487e..bfdcb95b099e 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -1702,6 +1702,8 @@ public class ActivityManagerService extends IActivityManager.Stub static final int REMOVE_UID_FROM_OBSERVER_MSG = 81; static final int BIND_APPLICATION_TIMEOUT_SOFT_MSG = 82; static final int BIND_APPLICATION_TIMEOUT_HARD_MSG = 83; + static final int SERVICE_FGS_TIMEOUT_MSG = 84; + static final int SERVICE_FGS_ANR_TIMEOUT_MSG = 85; static final int FIRST_BROADCAST_QUEUE_MSG = 200; @@ -2064,6 +2066,12 @@ public class ActivityManagerService extends IActivityManager.Stub case BIND_APPLICATION_TIMEOUT_HARD_MSG: { handleBindApplicationTimeoutHard((ProcessRecord) msg.obj); } break; + case SERVICE_FGS_TIMEOUT_MSG: { + mServices.onFgsTimeout((ServiceRecord) msg.obj); + } break; + case SERVICE_FGS_ANR_TIMEOUT_MSG: { + mServices.onFgsAnrTimeout((ServiceRecord) msg.obj); + } break; } } } @@ -13794,6 +13802,13 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override + public boolean hasServiceTimeLimitExceeded(ComponentName className, IBinder token) { + synchronized (this) { + return mServices.hasServiceTimedOutLocked(className, token); + } + } + + @Override public int handleIncomingUser(int callingPid, int callingUid, int userId, boolean allowAll, boolean requireFull, String name, String callerPackage) { return mUserController.handleIncomingUser(callingPid, callingUid, userId, allowAll, diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java index 2771572edb01..3c8d7fc833dc 100644 --- a/services/core/java/com/android/server/am/ServiceRecord.java +++ b/services/core/java/com/android/server/am/ServiceRecord.java @@ -56,6 +56,7 @@ import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; import android.util.ArrayMap; +import android.util.Pair; import android.util.Slog; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; @@ -236,6 +237,8 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN boolean mFgsNotificationShown; // Whether FGS package has permissions to show notifications. boolean mFgsHasNotificationPermission; + // Whether the FGS contains a type that is time limited. + private boolean mFgsIsTimeLimited; // allow the service becomes foreground service? Service started from background may not be // allowed to become a foreground service. @@ -915,6 +918,7 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN pw.print(prefix); pw.print("isForeground="); pw.print(isForeground); pw.print(" foregroundId="); pw.print(foregroundId); pw.printf(" types=%08X", foregroundServiceType); + pw.print(" fgsHasTimeLimitedType="); pw.print(mFgsIsTimeLimited); pw.print(" foregroundNoti="); pw.println(foregroundNoti); if (isShortFgs() && mShortFgsInfo != null) { @@ -1789,6 +1793,83 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN + " " + (mShortFgsInfo == null ? "" : mShortFgsInfo.getDescription()); } + /** + * @return true if one of the types of this FGS has a time limit. + */ + public boolean isFgsTimeLimited() { + return startRequested && isForeground && canFgsTypeTimeOut(foregroundServiceType); + } + + /** + * Called when a FGS with a time-limited type starts ({@code true}) or stops ({@code false}). + */ + public void setIsFgsTimeLimited(boolean fgsIsTimeLimited) { + this.mFgsIsTimeLimited = fgsIsTimeLimited; + } + + /** + * @return whether {@link #mFgsIsTimeLimited} was previously set or not. + */ + public boolean wasFgsPreviouslyTimeLimited() { + return mFgsIsTimeLimited; + } + + /** + * @return the FGS type if the service has reached its time limit, otherwise -1. + */ + public int getTimedOutFgsType(long nowUptime) { + if (!isAppAlive() || !isFgsTimeLimited()) { + return -1; + } + + final Pair<Integer, Long> fgsTypeAndStopTime = getEarliestStopTypeAndTime(); + if (fgsTypeAndStopTime.first != -1 && fgsTypeAndStopTime.second <= nowUptime) { + return fgsTypeAndStopTime.first; + } + return -1; // no fgs type exceeded time limit + } + + /** + * @return a {@code Pair<fgs_type, stop_time>}, representing the earliest time at which the FGS + * should be stopped (fgs start time + time limit for most restrictive type) + */ + Pair<Integer, Long> getEarliestStopTypeAndTime() { + int fgsType = -1; + long timeout = 0; + if ((foregroundServiceType & ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING) + == ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING) { + fgsType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING; + timeout = ams.mConstants.mMediaProcessingFgsTimeoutDuration; + } + if ((foregroundServiceType & ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) + == ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) { + // update the timeout and type if this type has a more restrictive time limit + if (timeout == 0 || ams.mConstants.mDataSyncFgsTimeoutDuration < timeout) { + fgsType = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC; + timeout = ams.mConstants.mDataSyncFgsTimeoutDuration; + } + } + // Add the logic for time limits introduced in the future for other fgs types here. + return Pair.create(fgsType, timeout == 0 ? 0 : (mFgsEnterTime + timeout)); + } + + /** + * Check if the given types contain a type which is time restricted. + */ + boolean canFgsTypeTimeOut(int fgsType) { + // The below conditionals are not simplified on purpose to help with readability. + if ((fgsType & ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING) + == ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING) { + return true; + } + if ((fgsType & ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) + == ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) { + return true; + } + // Additional types which have time limits should be added here in the future. + return false; + } + private boolean isAppAlive() { if (app == null) { return false; diff --git a/services/core/java/com/android/server/am/TEST_MAPPING b/services/core/java/com/android/server/am/TEST_MAPPING index bd3c8e0b3fe6..feab2c05cad6 100644 --- a/services/core/java/com/android/server/am/TEST_MAPPING +++ b/services/core/java/com/android/server/am/TEST_MAPPING @@ -157,7 +157,6 @@ ] }, { - "file_patterns": ["Broadcast.*"], "name": "CtsContentTestCases", "options": [ { "include-filter": "android.content.cts.BroadcastReceiverTest" }, diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 96c6be824561..55ac4cf37283 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -1350,41 +1350,57 @@ class UserController implements Handler.Callback { } /** - * For mDelayUserDataLocking mode, storage once unlocked is kept unlocked. - * Total number of unlocked user storage is limited by mMaxRunningUsers. - * If there are more unlocked users, evict and lock the least recently stopped user and - * lock that user's data. Regardless of the mode, ephemeral user is always locked - * immediately. + * Returns which user, if any, should be locked when the given user is stopped. + * + * For typical (non-mDelayUserDataLocking) devices and users, this will be the provided user. + * + * However, for some devices or users (based on {@link #canDelayDataLockingForUser(int)}), + * storage once unlocked is kept unlocked, even after the user is stopped, so the user to be + * locked (if any) may differ. + * + * For mDelayUserDataLocking devices, the total number of unlocked user storage is limited + * (currently by mMaxRunningUsers). If there are more unlocked users, evict and lock the least + * recently stopped user and lock that user's data. + * + * Regardless of the mode, ephemeral user is always locked immediately. * * @return user id to lock. UserHandler.USER_NULL will be returned if no user should be locked. */ @GuardedBy("mLock") private int updateUserToLockLU(@UserIdInt int userId, boolean allowDelayedLocking) { - int userIdToLock = userId; - // TODO: Decouple the delayed locking flows from mMaxRunningUsers or rename the property to - // state maximum running unlocked users specifically - if (canDelayDataLockingForUser(userIdToLock) && allowDelayedLocking - && !getUserInfo(userId).isEphemeral() - && !hasUserRestriction(UserManager.DISALLOW_RUN_IN_BACKGROUND, userId)) { + if (!canDelayDataLockingForUser(userId) + || !allowDelayedLocking + || getUserInfo(userId).isEphemeral() + || hasUserRestriction(UserManager.DISALLOW_RUN_IN_BACKGROUND, userId)) { + return userId; + } + + // Once we reach here, we are in a delayed locking scenario. + // Now, no user will be locked, unless the device's policy dictates we should based on the + // maximum of such users allowed for the device. + if (mDelayUserDataLocking) { // arg should be object, not index mLastActiveUsersForDelayedLocking.remove((Integer) userId); mLastActiveUsersForDelayedLocking.add(0, userId); int totalUnlockedUsers = mStartedUsers.size() + mLastActiveUsersForDelayedLocking.size(); + // TODO: Decouple the delayed locking flows from mMaxRunningUsers. These users aren't + // running so this calculation shouldn't be based on this parameter. Also note that + // that if these devices ever support background running users (such as profiles), the + // implementation is incorrect since starting such users can cause the max to be + // exceeded. if (totalUnlockedUsers > mMaxRunningUsers) { // should lock a user - userIdToLock = mLastActiveUsersForDelayedLocking.get( + final int userIdToLock = mLastActiveUsersForDelayedLocking.get( mLastActiveUsersForDelayedLocking.size() - 1); mLastActiveUsersForDelayedLocking .remove(mLastActiveUsersForDelayedLocking.size() - 1); - Slogf.i(TAG, "finishUserStopped, stopping user:" + userId - + " lock user:" + userIdToLock); - } else { - Slogf.i(TAG, "finishUserStopped, user:" + userId + ", skip locking"); - // do not lock - userIdToLock = UserHandle.USER_NULL; + Slogf.i(TAG, "finishUserStopped: should stop user " + userId + + " but should lock user " + userIdToLock); + return userIdToLock; } } - return userIdToLock; + Slogf.i(TAG, "finishUserStopped: should stop user " + userId + " but without any locking"); + return UserHandle.USER_NULL; } /** diff --git a/services/core/java/com/android/server/appop/LegacyAppOpStateParser.java b/services/core/java/com/android/server/appop/LegacyAppOpStateParser.java index a6d505021090..9ed3a99013ea 100644 --- a/services/core/java/com/android/server/appop/LegacyAppOpStateParser.java +++ b/services/core/java/com/android/server/appop/LegacyAppOpStateParser.java @@ -49,15 +49,7 @@ class LegacyAppOpStateParser { */ public int readState(AtomicFile file, SparseArray<SparseIntArray> uidModes, SparseArray<ArrayMap<String, SparseIntArray>> userPackageModes) { - FileInputStream stream; - try { - stream = file.openRead(); - } catch (FileNotFoundException e) { - Slog.i(TAG, "No existing app ops " + file.getBaseFile() + "; starting empty"); - return NO_FILE_VERSION; - } - - try { + try (FileInputStream stream = file.openRead()) { TypedXmlPullParser parser = Xml.resolvePullParser(stream); int type; while ((type = parser.next()) != XmlPullParser.START_TAG @@ -95,6 +87,9 @@ class LegacyAppOpStateParser { } } return versionAtBoot; + } catch (FileNotFoundException e) { + Slog.i(TAG, "No existing app ops " + file.getBaseFile() + "; starting empty"); + return NO_FILE_VERSION; } catch (XmlPullParserException e) { throw new RuntimeException(e); } catch (IOException e) { diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 53255a0639fe..c59f4f7888ce 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -32,6 +32,7 @@ import static android.media.AudioManager.RINGER_MODE_VIBRATE; import static android.media.AudioManager.STREAM_SYSTEM; import static android.media.audio.Flags.autoPublicVolumeApiHardening; import static android.media.audio.Flags.automaticBtDeviceType; +import static android.media.audio.Flags.featureSpatialAudioHeadtrackingLowLatency; import static android.media.audio.Flags.focusFreezeTestApi; import static android.media.audiopolicy.Flags.enableFadeManagerConfiguration; import static android.os.Process.FIRST_APPLICATION_UID; @@ -4519,6 +4520,10 @@ public class AudioService extends IAudioService.Stub pw.println("\nFun with Flags: "); pw.println("\tandroid.media.audio.autoPublicVolumeApiHardening:" + autoPublicVolumeApiHardening()); + pw.println("\tandroid.media.audio.Flags.automaticBtDeviceType:" + + automaticBtDeviceType()); + pw.println("\tandroid.media.audio.featureSpatialAudioHeadtrackingLowLatency:" + + featureSpatialAudioHeadtrackingLowLatency()); pw.println("\tandroid.media.audio.focusFreezeTestApi:" + focusFreezeTestApi()); pw.println("\tcom.android.media.audio.disablePrescaleAbsoluteVolume:" diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index 8d767313d5ab..3b5fa7f00891 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -26,6 +26,7 @@ import static com.android.media.audio.Flags.dsaOverBtLeAudio; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.content.pm.PackageManager; import android.hardware.Sensor; import android.hardware.SensorManager; import android.media.AudioAttributes; @@ -1611,6 +1612,9 @@ public class SpatializerHelper { pw.println("\tsupports binaural:" + mBinauralSupported + " / transaural:" + mTransauralSupported); pw.println("\tmSpatOutput:" + mSpatOutput); + pw.println("\thas FEATURE_AUDIO_SPATIAL_HEADTRACKING_LOW_LATENCY:" + + mAudioService.mContext.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_AUDIO_SPATIAL_HEADTRACKING_LOW_LATENCY)); } private static String spatStateString(int state) { diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java index cb15abcc65fc..cd064ae38aa5 100644 --- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java +++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java @@ -19,11 +19,11 @@ package com.android.server.devicestate; import static android.Manifest.permission.CONTROL_DEVICE_STATE; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.hardware.devicestate.DeviceState.FLAG_CANCEL_OVERRIDE_REQUESTS; import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE; import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE; -import static com.android.server.devicestate.DeviceState.FLAG_CANCEL_OVERRIDE_REQUESTS; import static com.android.server.devicestate.OverrideRequest.OVERRIDE_REQUEST_TYPE_BASE_STATE; import static com.android.server.devicestate.OverrideRequest.OVERRIDE_REQUEST_TYPE_EMULATED_STATE; import static com.android.server.devicestate.OverrideRequestController.FLAG_POWER_SAVE_ENABLED; @@ -40,6 +40,7 @@ import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.IProcessObserver; import android.content.Context; +import android.hardware.devicestate.DeviceState; import android.hardware.devicestate.DeviceStateInfo; import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.DeviceStateManagerInternal; diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java index 8c6068d89296..02c9bb3b9deb 100644 --- a/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java +++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java @@ -18,6 +18,7 @@ package com.android.server.devicestate; import android.annotation.NonNull; import android.annotation.Nullable; +import android.hardware.devicestate.DeviceState; import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.DeviceStateRequest; import android.os.Binder; diff --git a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java index d5945f4e8b52..65b393ad94b9 100644 --- a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java +++ b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java @@ -21,6 +21,7 @@ import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STA import android.annotation.IntDef; import android.annotation.IntRange; +import android.hardware.devicestate.DeviceState; import android.util.Dumpable; import java.lang.annotation.Retention; diff --git a/services/core/java/com/android/server/devicestate/OverrideRequest.java b/services/core/java/com/android/server/devicestate/OverrideRequest.java index 20485c1ac102..d92629f77a95 100644 --- a/services/core/java/com/android/server/devicestate/OverrideRequest.java +++ b/services/core/java/com/android/server/devicestate/OverrideRequest.java @@ -18,6 +18,7 @@ package com.android.server.devicestate; import android.annotation.IntDef; import android.annotation.NonNull; +import android.hardware.devicestate.DeviceState; import android.hardware.devicestate.DeviceStateRequest; import android.os.IBinder; diff --git a/services/core/java/com/android/server/devicestate/OverrideRequestController.java b/services/core/java/com/android/server/devicestate/OverrideRequestController.java index f5f2fa8cabdc..6c3fd83d17a0 100644 --- a/services/core/java/com/android/server/devicestate/OverrideRequestController.java +++ b/services/core/java/com/android/server/devicestate/OverrideRequestController.java @@ -18,6 +18,7 @@ package com.android.server.devicestate; import android.annotation.IntDef; import android.annotation.NonNull; +import android.hardware.devicestate.DeviceState; import android.hardware.devicestate.DeviceStateRequest; import android.os.IBinder; import android.util.Slog; diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index b8a63cd7941c..96c0c8a9a7b8 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -3560,8 +3560,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_HAS_IME); mCurStatsToken = null; - if (!Flags.useHandwritingListenerForTooltype() - && lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) { + if (lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) { curMethod.updateEditorToolType(lastClickToolType); } mVisibilityApplier.performShowIme(windowToken, statsToken, 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/om/IdmapManager.java b/services/core/java/com/android/server/om/IdmapManager.java index 25a39cc8456f..86d05d92c95b 100644 --- a/services/core/java/com/android/server/om/IdmapManager.java +++ b/services/core/java/com/android/server/om/IdmapManager.java @@ -257,7 +257,7 @@ final class IdmapManager { private boolean matchesActorSignature(@NonNull AndroidPackage targetPackage, @NonNull AndroidPackage overlayPackage, int userId) { String targetOverlayableName = overlayPackage.getOverlayTargetOverlayableName(); - if (targetOverlayableName != null) { + if (targetOverlayableName != null && !mPackageManager.getNamedActors().isEmpty()) { try { OverlayableInfo overlayableInfo = mPackageManager.getOverlayableForTarget( targetPackage.getPackageName(), targetOverlayableName, userId); diff --git a/services/core/java/com/android/server/pm/AppDataHelper.java b/services/core/java/com/android/server/pm/AppDataHelper.java index 1dd790502486..18ba2cf1405e 100644 --- a/services/core/java/com/android/server/pm/AppDataHelper.java +++ b/services/core/java/com/android/server/pm/AppDataHelper.java @@ -503,21 +503,24 @@ 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); - } else if (!TextUtils.equals(volumeUuid, packageState.getVolumeUuid())) { + } + if (!TextUtils.equals(volumeUuid, packageState.getVolumeUuid())) { throw PackageManagerException.ofInternalError( "Package " + packageName + " found on unknown volume " + volumeUuid + "; expected volume " + packageState.getVolumeUuid(), PackageManagerException.INTERNAL_ERROR_STORAGE_INVALID_VOLUME_UNKNOWN); - } else if (!userState.isInstalled() && !userState.dataExists()) { + } + final PackageUserStateInternal userState = packageState.getUserStateOrDefault(userId); + if (!userState.isInstalled() && !userState.dataExists()) { throw PackageManagerException.ofInternalError( "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 + } + if (packageState.getPkg() != null && !shouldHaveAppStorage(packageState.getPkg())) { throw PackageManagerException.ofInternalError( "Package " + packageName + " shouldn't have storage", diff --git a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java index afcf5a094bd4..76952b30c1e9 100644 --- a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java +++ b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java @@ -29,6 +29,7 @@ import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; +import android.hardware.devicestate.DeviceState; import android.os.Environment; import android.os.PowerManager; import android.util.ArrayMap; @@ -40,7 +41,6 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import com.android.server.LocalServices; -import com.android.server.devicestate.DeviceState; import com.android.server.devicestate.DeviceStateProvider; import com.android.server.input.InputManagerInternal; import com.android.server.policy.devicestate.config.Conditions; 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 00036e48891f..af4da812d79e 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -842,7 +842,7 @@ public class BatteryStatsImpl extends BatteryStats { private int mBatteryChargeUah; private int mBatteryHealth; private int mBatteryTemperature; - private int mBatteryVoltageMv = -1; + private int mBatteryVoltageMv; @NonNull private final BatteryStatsHistory mHistory; diff --git a/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java b/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java index 0de73a5a30f6..b0dcf95b3f5d 100644 --- a/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java +++ b/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java @@ -30,7 +30,9 @@ import android.util.StatsEvent; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.FrameworkStatsLog; +import com.android.server.selinux.RateLimiter; +import java.time.Duration; import java.util.List; import java.util.Map; @@ -131,19 +133,36 @@ class AggregatedMobileDataStatsPuller { private final Handler mMobileDataStatsHandler; + private final RateLimiter mRateLimiter; + AggregatedMobileDataStatsPuller(NetworkStatsManager networkStatsManager) { + if (DEBUG) { + if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, + TAG + "-AggregatedMobileDataStatsPullerInit"); + } + } + + mRateLimiter = new RateLimiter(/* window= */ Duration.ofSeconds(1)); + mUidStats = new ArrayMap<>(); mUidPreviousState = new SparseIntArray(); mNetworkStatsManager = networkStatsManager; - if (mNetworkStatsManager != null) { - updateNetworkStats(mNetworkStatsManager); - } - HandlerThread mMobileDataStatsHandlerThread = new HandlerThread("MobileDataStatsHandler"); mMobileDataStatsHandlerThread.start(); mMobileDataStatsHandler = new Handler(mMobileDataStatsHandlerThread.getLooper()); + + if (mNetworkStatsManager != null) { + mMobileDataStatsHandler.post( + () -> { + updateNetworkStats(mNetworkStatsManager); + }); + } + if (DEBUG) { + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); + } } public void noteUidProcessState(int uid, int state, long unusedElapsedRealtime, @@ -180,18 +199,20 @@ class AggregatedMobileDataStatsPuller { } private void noteUidProcessStateImpl(int uid, int state) { - // noteUidProcessStateLocked can be called back to back several times while - // the updateNetworkStatsLocked loops over several stats for multiple uids - // and during the first call in a batch of proc state change event it can - // contain info for uid with unknown previous state yet which can happen due to a few - // reasons: - // - app was just started - // - app was started before the ActivityManagerService - // as result stats would be created with state == ActivityManager.PROCESS_STATE_UNKNOWN - if (mNetworkStatsManager != null) { - updateNetworkStats(mNetworkStatsManager); - } else { - Slog.w(TAG, "noteUidProcessStateLocked() can not get mNetworkStatsManager"); + if (mRateLimiter.tryAcquire()) { + // noteUidProcessStateImpl can be called back to back several times while + // the updateNetworkStats loops over several stats for multiple uids + // and during the first call in a batch of proc state change event it can + // contain info for uid with unknown previous state yet which can happen due to a few + // reasons: + // - app was just started + // - app was started before the ActivityManagerService + // as result stats would be created with state == ActivityManager.PROCESS_STATE_UNKNOWN + if (mNetworkStatsManager != null) { + updateNetworkStats(mNetworkStatsManager); + } else { + Slog.w(TAG, "noteUidProcessStateLocked() can not get mNetworkStatsManager"); + } } mUidPreviousState.put(uid, state); } diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java index 6a3cf43438fd..a3e28693cb21 100644 --- a/services/core/java/com/android/server/wm/DragDropController.java +++ b/services/core/java/com/android/server/wm/DragDropController.java @@ -16,6 +16,9 @@ package com.android.server.wm; +import static android.view.View.DRAG_FLAG_GLOBAL; +import static android.view.View.DRAG_FLAG_GLOBAL_SAME_APPLICATION; + import static com.android.input.flags.Flags.enablePointerChoreographer; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DRAG; import static com.android.server.wm.WindowManagerDebugConfig.SHOW_LIGHT_TRANSACTIONS; @@ -30,15 +33,20 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; +import android.os.RemoteException; import android.util.Slog; import android.view.Display; +import android.view.DragEvent; import android.view.IWindow; import android.view.InputDevice; import android.view.PointerIcon; import android.view.SurfaceControl; import android.view.View; import android.view.accessibility.AccessibilityManager; +import android.window.IUnhandledDragCallback; +import android.window.IUnhandledDragListener; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.wm.WindowManagerInternal.IDragDropCallback; import java.util.Objects; @@ -59,6 +67,7 @@ class DragDropController { static final int MSG_TEAR_DOWN_DRAG_AND_DROP_INPUT = 1; static final int MSG_ANIMATION_END = 2; static final int MSG_REMOVE_DRAG_SURFACE_TIMEOUT = 3; + static final int MSG_UNHANDLED_DROP_LISTENER_TIMEOUT = 4; /** * Drag state per operation. @@ -72,6 +81,21 @@ class DragDropController { private WindowManagerService mService; private final Handler mHandler; + // The unhandled drag listener for handling cross-window drags that end with no target window + private IUnhandledDragListener mUnhandledDragListener; + private final IBinder.DeathRecipient mUnhandledDragListenerDeathRecipient = + new IBinder.DeathRecipient() { + @Override + public void binderDied() { + synchronized (mService.mGlobalLock) { + if (hasPendingUnhandledDropCallback()) { + onUnhandledDropCallback(false /* consumedByListeners */); + } + setUnhandledDragListener(null); + } + } + }; + /** * Callback which is used to sync drag state with the vendor-specific code. */ @@ -83,10 +107,16 @@ class DragDropController { mHandler = new DragHandler(service, looper); } + @VisibleForTesting + Handler getHandler() { + return mHandler; + } + boolean dragDropActiveLocked() { return mDragState != null && !mDragState.isClosing(); } + @VisibleForTesting boolean dragSurfaceRelinquishedToDropTarget() { return mDragState != null && mDragState.mRelinquishDragSurfaceToDropTarget; } @@ -96,6 +126,32 @@ class DragDropController { mCallback.set(callback); } + /** + * Sets the listener for unhandled cross-window drags. + */ + public void setUnhandledDragListener(IUnhandledDragListener listener) { + if (mUnhandledDragListener != null && mUnhandledDragListener.asBinder() != null) { + mUnhandledDragListener.asBinder().unlinkToDeath( + mUnhandledDragListenerDeathRecipient, 0); + } + mUnhandledDragListener = listener; + if (listener != null && listener.asBinder() != null) { + try { + mUnhandledDragListener.asBinder().linkToDeath( + mUnhandledDragListenerDeathRecipient, 0); + } catch (RemoteException e) { + mUnhandledDragListener = null; + } + } + } + + /** + * Returns whether there is an unhandled drag listener set. + */ + boolean hasUnhandledDragListener() { + return mUnhandledDragListener != null; + } + void sendDragStartedIfNeededLocked(WindowState window) { mDragState.sendDragStartedIfNeededLocked(window); } @@ -247,6 +303,10 @@ class DragDropController { } } + /** + * This is called from the drop target window that received ACTION_DROP + * (see DragState#reportDropWindowLock()). + */ void reportDropResult(IWindow window, boolean consumed) { IBinder token = window.asBinder(); if (DEBUG_DRAG) { @@ -273,22 +333,89 @@ class DragDropController { // so be sure to halt the timeout even if the later WindowState // lookup fails. mHandler.removeMessages(MSG_DRAG_END_TIMEOUT, window.asBinder()); + WindowState callingWin = mService.windowForClientLocked(null, window, false); if (callingWin == null) { Slog.w(TAG_WM, "Bad result-reporting window " + window); return; // !!! TODO: throw here? } - mDragState.mDragResult = consumed; - mDragState.mRelinquishDragSurfaceToDropTarget = consumed - && mDragState.targetInterceptsGlobalDrag(callingWin); - mDragState.endDragLocked(); + // If the drop was not consumed by the target window, then check if it should be + // consumed by the system unhandled drag listener + if (!consumed && notifyUnhandledDrop(mDragState.mUnhandledDropEvent, + "window-drop")) { + // If the unhandled drag listener is notified, then defer ending the drag until + // the listener calls back + return; + } + + final boolean relinquishDragSurfaceToDropTarget = + consumed && mDragState.targetInterceptsGlobalDrag(callingWin); + mDragState.endDragLocked(consumed, relinquishDragSurfaceToDropTarget); } } finally { mCallback.get().postReportDropResult(); } } + /** + * Notifies the unhandled drag listener if needed. + * @return whether the listener was notified and subsequent drag completion should be deferred + * until the listener calls back + */ + boolean notifyUnhandledDrop(DragEvent dropEvent, String reason) { + final boolean isLocalDrag = + (mDragState.mFlags & (DRAG_FLAG_GLOBAL_SAME_APPLICATION | DRAG_FLAG_GLOBAL)) == 0; + if (!com.android.window.flags.Flags.delegateUnhandledDrags() + || mUnhandledDragListener == null + || isLocalDrag) { + // Skip if the flag is disabled, there is no unhandled-drag listener, or if this is a + // purely local drag + if (DEBUG_DRAG) Slog.d(TAG_WM, "Skipping unhandled listener " + + "(listener=" + mUnhandledDragListener + ", flags=" + mDragState.mFlags + ")"); + return false; + } + if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DROP to unhandled listener (" + reason + ")"); + try { + // Schedule timeout for the unhandled drag listener to call back + sendTimeoutMessage(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT, null, DRAG_TIMEOUT_MS); + mUnhandledDragListener.onUnhandledDrop(dropEvent, new IUnhandledDragCallback.Stub() { + @Override + public void notifyUnhandledDropComplete(boolean consumedByListener) { + if (DEBUG_DRAG) Slog.d(TAG_WM, "Unhandled listener finished handling DROP"); + synchronized (mService.mGlobalLock) { + onUnhandledDropCallback(consumedByListener); + } + } + }); + return true; + } catch (RemoteException e) { + Slog.e(TAG_WM, "Failed to call unhandled drag listener", e); + return false; + } + } + + /** + * Called when the unhandled drag listener has completed handling the drop + * (if it was notififed). + */ + @VisibleForTesting + void onUnhandledDropCallback(boolean consumedByListener) { + mHandler.removeMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT, null); + // If handled, then the listeners assume responsibility of cleaning up the drag surface + mDragState.mDragResult = consumedByListener; + mDragState.mRelinquishDragSurfaceToDropTarget = consumedByListener; + mDragState.closeLocked(); + } + + /** + * Returns whether we are currently waiting for the unhandled drag listener to callback after + * it was notified of an unhandled drag. + */ + boolean hasPendingUnhandledDropCallback() { + return mHandler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT); + } + void cancelDragAndDrop(IBinder dragToken, boolean skipAnimation) { if (DEBUG_DRAG) { Slog.d(TAG_WM, "cancelDragAndDrop"); @@ -436,8 +563,8 @@ class DragDropController { synchronized (mService.mGlobalLock) { // !!! TODO: ANR the drag-receiving app if (mDragState != null) { - mDragState.mDragResult = false; - mDragState.endDragLocked(); + mDragState.endDragLocked(false /* consumed */, + false /* relinquishDragSurfaceToDropTarget */); } } break; @@ -473,6 +600,13 @@ class DragDropController { } break; } + + case MSG_UNHANDLED_DROP_LISTENER_TIMEOUT: { + synchronized (mService.mGlobalLock) { + onUnhandledDropCallback(false /* consumedByListener */); + } + break; + } } } } diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java index d302f0641b58..76038b945288 100644 --- a/services/core/java/com/android/server/wm/DragState.java +++ b/services/core/java/com/android/server/wm/DragState.java @@ -147,6 +147,11 @@ class DragState { */ private boolean mIsClosing; + // Stores the last drop event which was reported to a valid drop target window, or null + // otherwise. This drop event will contain private info and should only be consumed by the + // unhandled drag listener. + DragEvent mUnhandledDropEvent; + DragState(WindowManagerService service, DragDropController controller, IBinder token, SurfaceControl surface, int flags, IBinder localWin) { mService = service; @@ -287,14 +292,54 @@ class DragState { mData = null; mThumbOffsetX = mThumbOffsetY = 0; mNotifiedWindows = null; + if (mUnhandledDropEvent != null) { + mUnhandledDropEvent.recycle(); + mUnhandledDropEvent = null; + } // Notifies the controller that the drag state is closed. mDragDropController.onDragStateClosedLocked(this); } /** + * Creates the drop event for this drag gesture. If `touchedWin` is null, then the drop event + * will be created for dispatching to the unhandled drag and the drag surface will be provided + * as a part of the dispatched event. + */ + private DragEvent createDropEvent(float x, float y, @Nullable WindowState touchedWin, + boolean includeDragSurface) { + if (touchedWin != null) { + final int targetUserId = UserHandle.getUserId(touchedWin.getOwningUid()); + final DragAndDropPermissionsHandler dragAndDropPermissions; + if ((mFlags & View.DRAG_FLAG_GLOBAL) != 0 && (mFlags & DRAG_FLAGS_URI_ACCESS) != 0 + && mData != null) { + dragAndDropPermissions = new DragAndDropPermissionsHandler(mService.mGlobalLock, + mData, + mUid, + touchedWin.getOwningPackage(), + mFlags & DRAG_FLAGS_URI_PERMISSIONS, + mSourceUserId, + targetUserId); + } else { + dragAndDropPermissions = null; + } + if (mSourceUserId != targetUserId) { + if (mData != null) { + mData.fixUris(mSourceUserId); + } + } + return obtainDragEvent(DragEvent.ACTION_DROP, x, y, mData, + targetInterceptsGlobalDrag(touchedWin), dragAndDropPermissions); + } else { + return obtainDragEvent(DragEvent.ACTION_DROP, x, y, mData, + includeDragSurface /* includeDragSurface */, null /* dragAndDropPermissions */); + } + } + + /** * Notify the drop target and tells it about the data. If the drop event is not sent to the - * target, invokes {@code endDragLocked} immediately. + * target, invokes {@code endDragLocked} after the unhandled drag listener gets a chance to + * handle the drop. */ boolean reportDropWindowLock(IBinder token, float x, float y) { if (mAnimator != null) { @@ -302,41 +347,27 @@ class DragState { } final WindowState touchedWin = mService.mInputToWindowMap.get(token); + final DragEvent unhandledDropEvent = createDropEvent(x, y, null /* touchedWin */, + true /* includePrivateInfo */); if (!isWindowNotified(touchedWin)) { - // "drop" outside a valid window -- no recipient to apply a - // timeout to, and we can send the drag-ended message immediately. - mDragResult = false; - endDragLocked(); + // Delegate to the unhandled drag listener as a first pass + if (mDragDropController.notifyUnhandledDrop(unhandledDropEvent, "unhandled-drop")) { + // The unhandled drag listener will call back to notify whether it has consumed + // the drag, so return here + return true; + } + + // "drop" outside a valid window -- no recipient to apply a timeout to, and we can send + // the drag-ended message immediately. + endDragLocked(false /* consumed */, false /* relinquishDragSurfaceToDropTarget */); if (DEBUG_DRAG) Slog.d(TAG_WM, "Drop outside a valid window " + touchedWin); return false; } if (DEBUG_DRAG) Slog.d(TAG_WM, "sending DROP to " + touchedWin); - final int targetUserId = UserHandle.getUserId(touchedWin.getOwningUid()); - - final DragAndDropPermissionsHandler dragAndDropPermissions; - if ((mFlags & View.DRAG_FLAG_GLOBAL) != 0 && (mFlags & DRAG_FLAGS_URI_ACCESS) != 0 - && mData != null) { - dragAndDropPermissions = new DragAndDropPermissionsHandler(mService.mGlobalLock, - mData, - mUid, - touchedWin.getOwningPackage(), - mFlags & DRAG_FLAGS_URI_PERMISSIONS, - mSourceUserId, - targetUserId); - } else { - dragAndDropPermissions = null; - } - if (mSourceUserId != targetUserId) { - if (mData != null) { - mData.fixUris(mSourceUserId); - } - } final IBinder clientToken = touchedWin.mClient.asBinder(); - final DragEvent event = obtainDragEvent(DragEvent.ACTION_DROP, x, y, - mData, targetInterceptsGlobalDrag(touchedWin), - dragAndDropPermissions); + final DragEvent event = createDropEvent(x, y, touchedWin, false /* includePrivateInfo */); try { touchedWin.mClient.dispatchDragEvent(event); @@ -345,7 +376,7 @@ class DragState { DragDropController.DRAG_TIMEOUT_MS); } catch (RemoteException e) { Slog.w(TAG_WM, "can't send drop notification to win " + touchedWin); - endDragLocked(); + endDragLocked(false /* consumed */, false /* relinquishDragSurfaceToDropTarget */); return false; } finally { if (MY_PID != touchedWin.mSession.mPid) { @@ -353,6 +384,7 @@ class DragState { } } mToken = clientToken; + mUnhandledDropEvent = unhandledDropEvent; return true; } @@ -478,6 +510,9 @@ class DragState { boolean containsAppExtras) { final boolean interceptsGlobalDrag = targetInterceptsGlobalDrag(newWin); if (mDragInProgress && isValidDropTarget(newWin, containsAppExtras, interceptsGlobalDrag)) { + if (DEBUG_DRAG) { + Slog.d(TAG_WM, "Sending DRAG_STARTED to new window " + newWin); + } // Only allow the extras to be dispatched to a global-intercepting drag target ClipData data = interceptsGlobalDrag ? mData.copyForTransferWithActivityInfo() : null; DragEvent event = obtainDragEvent(DragEvent.ACTION_DRAG_STARTED, @@ -523,14 +558,25 @@ class DragState { return false; } if (!targetWin.isPotentialDragTarget(interceptsGlobalDrag)) { + // Window should not be a target return false; } - if ((mFlags & View.DRAG_FLAG_GLOBAL) == 0 || !targetWindowSupportsGlobalDrag(targetWin)) { + final boolean isGlobalSameAppDrag = (mFlags & View.DRAG_FLAG_GLOBAL_SAME_APPLICATION) != 0; + final boolean isGlobalDrag = (mFlags & View.DRAG_FLAG_GLOBAL) != 0; + final boolean isAnyGlobalDrag = isGlobalDrag || isGlobalSameAppDrag; + if (!isAnyGlobalDrag || !targetWindowSupportsGlobalDrag(targetWin)) { // Drag is limited to the current window. if (!isLocalWindow) { return false; } } + if (isGlobalSameAppDrag) { + // Drag is limited to app windows from the same uid or windows that can intercept global + // drag + if (!interceptsGlobalDrag && mUid != targetWin.getUid()) { + return false; + } + } return interceptsGlobalDrag || mCrossProfileCopyAllowed @@ -547,7 +593,10 @@ class DragState { /** * @return whether the given window {@param targetWin} can intercept global drags. */ - public boolean targetInterceptsGlobalDrag(WindowState targetWin) { + public boolean targetInterceptsGlobalDrag(@Nullable WindowState targetWin) { + if (targetWin == null) { + return false; + } return (targetWin.mAttrs.privateFlags & PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP) != 0; } @@ -561,9 +610,6 @@ class DragState { if (isWindowNotified(newWin)) { return; } - if (DEBUG_DRAG) { - Slog.d(TAG_WM, "need to send DRAG_STARTED to new window " + newWin); - } sendDragStartedLocked(newWin, mCurrentX, mCurrentY, containsApplicationExtras(mDataDescription)); } @@ -578,7 +624,13 @@ class DragState { return false; } - void endDragLocked() { + /** + * Ends the current drag, animating the drag surface back to the source if the drop was not + * consumed by the receiving window. + */ + void endDragLocked(boolean dropConsumed, boolean relinquishDragSurfaceToDropTarget) { + mDragResult = dropConsumed; + mRelinquishDragSurfaceToDropTarget = relinquishDragSurfaceToDropTarget; if (mAnimator != null) { return; } diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 80894b201942..61fde5e31e8f 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -465,7 +465,9 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } } final InsetsSource source = new InsetsSource(id, provider.getType()); - source.setFrame(provider.getArbitraryRectangle()).updateSideHint(getBounds()); + source.setFrame(provider.getArbitraryRectangle()) + .updateSideHint(getBounds()) + .setBoundingRects(provider.getBoundingRects()); mLocalInsetsSources.put(id, source); mDisplayContent.getInsetsStateController().updateAboveInsetsState(true); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 4ea76e128584..de8d9f96453d 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -310,6 +310,7 @@ import android.window.IScreenRecordingCallback; import android.window.ISurfaceSyncGroupCompletedListener; import android.window.ITaskFpsCallback; import android.window.ITrustedPresentationListener; +import android.window.IUnhandledDragListener; import android.window.InputTransferToken; import android.window.ScreenCapture; import android.window.SystemPerformanceHinter; @@ -10026,4 +10027,16 @@ public class WindowManagerService extends IWindowManager.Stub void onProcessActivityVisibilityChanged(int uid, boolean visible) { mScreenRecordingCallbackController.onProcessActivityVisibilityChanged(uid, visible); } + + /** + * Sets the listener to be called back when a cross-window drag and drop operation is unhandled + * (ie. not handled by any window which can handle the drag). + */ + @Override + public void setUnhandledDragListener(IUnhandledDragListener listener) throws RemoteException { + mAtmService.enforceTaskPermission("setUnhandledDragListener"); + synchronized (mGlobalLock) { + mDragDropController.setUnhandledDragListener(listener); + } + } } 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/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java index 8b22718786e5..bc264a46f051 100644 --- a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java +++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java @@ -16,11 +16,12 @@ package com.android.server.policy; -import static com.android.server.devicestate.DeviceState.FLAG_CANCEL_OVERRIDE_REQUESTS; -import static com.android.server.devicestate.DeviceState.FLAG_CANCEL_WHEN_REQUESTER_NOT_ON_TOP; -import static com.android.server.devicestate.DeviceState.FLAG_EMULATED_ONLY; -import static com.android.server.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE; -import static com.android.server.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL; +import static android.hardware.devicestate.DeviceState.FLAG_CANCEL_OVERRIDE_REQUESTS; +import static android.hardware.devicestate.DeviceState.FLAG_CANCEL_WHEN_REQUESTER_NOT_ON_TOP; +import static android.hardware.devicestate.DeviceState.FLAG_EMULATED_ONLY; +import static android.hardware.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE; +import static android.hardware.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL; + import static com.android.server.policy.BookStyleStateTransitions.DEFAULT_STATE_TRANSITIONS; import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createConfig; import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createTentModeClosedState; @@ -36,7 +37,6 @@ import com.android.server.devicestate.DeviceStatePolicy; import com.android.server.devicestate.DeviceStateProvider; import com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration; import com.android.server.policy.feature.flags.FeatureFlags; -import com.android.server.policy.feature.flags.FeatureFlagsImpl; import java.io.PrintWriter; import java.util.function.Predicate; diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java index 021a667113e7..bf2619b52275 100644 --- a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java +++ b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java @@ -34,6 +34,7 @@ import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; +import android.hardware.devicestate.DeviceState; import android.hardware.display.DisplayManager; import android.os.Handler; import android.os.Looper; @@ -48,7 +49,6 @@ import android.view.Display; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; -import com.android.server.devicestate.DeviceState; import com.android.server.devicestate.DeviceStateProvider; import com.android.server.policy.feature.flags.FeatureFlags; import com.android.server.policy.feature.flags.FeatureFlagsImpl; diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java index 04cebab107ee..930f4a61a453 100644 --- a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java +++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java @@ -51,6 +51,7 @@ import android.content.Context; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorManager; +import android.hardware.devicestate.DeviceState; import android.hardware.display.DisplayManager; import android.hardware.input.InputSensorInfo; import android.os.Handler; @@ -58,7 +59,6 @@ import android.os.PowerManager; import android.testing.AndroidTestingRunner; import android.view.Display; -import com.android.server.devicestate.DeviceState; import com.android.server.devicestate.DeviceStateProvider.Listener; import com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration; import com.android.server.policy.feature.flags.FakeFeatureFlagsImpl; diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index c55d70927f8e..dbbfb039af78 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -48,7 +48,6 @@ import android.content.pm.PackageManagerInternal; import android.content.res.Configuration; import android.content.res.Resources.Theme; import android.credentials.CredentialManager; -import android.credentials.flags.Flags; import android.database.sqlite.SQLiteCompatibilityWalFlags; import android.database.sqlite.SQLiteGlobal; import android.graphics.GraphicsStatsService; @@ -464,6 +463,11 @@ public final class SystemServer implements Dumpable { private static final String DEVICE_LOCK_APEX_PATH = "/apex/com.android.devicelock/javalib/service-devicelock.jar"; + private static final String PROFILING_SERVICE_LIFECYCLE_CLASS = + "android.os.profiling.ProfilingService$Lifecycle"; + private static final String PROFILING_SERVICE_JAR_PATH = + "/apex/com.android.profiling/javalib/service-profiling.jar"; + private static final String TETHERING_CONNECTOR_CLASS = "android.net.ITetheringConnector"; private static final String PERSISTENT_DATA_BLOCK_PROP = "ro.frp.pst"; @@ -2774,6 +2778,14 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(ON_DEVICE_PERSONALIZATION_SYSTEM_SERVICE_CLASS); t.traceEnd(); + // Profiling + if (android.server.Flags.telemetryApisService()) { + t.traceBegin("StartProfilingCompanion"); + mSystemServiceManager.startServiceFromJar(PROFILING_SERVICE_LIFECYCLE_CLASS, + PROFILING_SERVICE_JAR_PATH); + t.traceEnd(); + } + if (safeMode) { mActivityManagerService.enterSafeMode(); } diff --git a/services/people/java/com/android/server/people/PeopleService.java b/services/people/java/com/android/server/people/PeopleService.java index 885ed355011b..b9f00d7979c2 100644 --- a/services/people/java/com/android/server/people/PeopleService.java +++ b/services/people/java/com/android/server/people/PeopleService.java @@ -38,6 +38,7 @@ import android.content.pm.ShortcutInfo; import android.os.Binder; import android.os.CancellationSignal; import android.os.IBinder; +import android.os.IRemoteCallback; import android.os.Process; import android.os.RemoteCallbackList; import android.os.RemoteException; @@ -474,6 +475,10 @@ public class PeopleService extends SystemService { getDataManager().restore(userId, payload); } + @Override + public void requestServiceFeatures(AppPredictionSessionId sessionId, + IRemoteCallback callback) {} + @VisibleForTesting SessionInfo getSessionInfo(AppPredictionSessionId sessionId) { return mSessions.get(sessionId); 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/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java index 395b3aac8fd5..d36b55345418 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 @@ -95,24 +95,24 @@ public class BatteryStatsHistoryIteratorTest { assertThat(item = iterator.next()).isNotNull(); assertHistoryItem(item, BatteryStats.HistoryItem.CMD_UPDATE, BatteryStats.HistoryItem.EVENT_NONE, - null, 0, 3_600_000, 90, 1_000_000); + null, 0, -1, 3_600_000, 90, 1_000_000); assertThat(item = iterator.next()).isNotNull(); assertHistoryItem(item, BatteryStats.HistoryItem.CMD_UPDATE, BatteryStats.HistoryItem.EVENT_NONE, - null, 0, 2_400_000, 80, 2_000_000); + null, 0, 3700, 2_400_000, 80, 2_000_000); assertThat(item = iterator.next()).isNotNull(); assertHistoryItem(item, BatteryStats.HistoryItem.CMD_UPDATE, BatteryStats.HistoryItem.EVENT_ALARM | BatteryStats.HistoryItem.EVENT_FLAG_START, - "foo", APP_UID, 2_400_000, 80, 3_000_000); + "foo", APP_UID, 3700, 2_400_000, 80, 3_000_000); assertThat(item = iterator.next()).isNotNull(); assertHistoryItem(item, BatteryStats.HistoryItem.CMD_UPDATE, BatteryStats.HistoryItem.EVENT_ALARM | BatteryStats.HistoryItem.EVENT_FLAG_FINISH, - "foo", APP_UID, 2_400_000, 80, 3_001_000); + "foo", APP_UID, 3700, 2_400_000, 80, 3_001_000); assertThat(iterator.hasNext()).isFalse(); assertThat(iterator.next()).isNull(); @@ -140,7 +140,7 @@ public class BatteryStatsHistoryIteratorTest { mMockClock.currentTime = 3000; mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, - 100, /* plugType */ 0, 90, 72, 3700, 3_600_000, 4_000_000, 0, 1_000_000, + 100, /* plugType */ 0, 90, 72, -1, 3_600_000, 4_000_000, 0, 1_000_000, 1_000_000, 1_000_000); mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, /* plugType */ 0, 80, 72, 3700, 2_400_000, 4_000_000, 0, 2_000_000, @@ -303,7 +303,7 @@ public class BatteryStatsHistoryIteratorTest { } private void assertHistoryItem(BatteryStats.HistoryItem item, int command, int eventCode, - String tag, int uid, int batteryChargeUah, int batteryLevel, + String tag, int uid, int voltageMv, int batteryChargeUah, int batteryLevel, long elapsedTimeMs) { assertThat(item.cmd).isEqualTo(command); assertThat(item.eventCode).isEqualTo(eventCode); @@ -313,6 +313,7 @@ public class BatteryStatsHistoryIteratorTest { assertThat(item.eventTag.string).isEqualTo(tag); assertThat(item.eventTag.uid).isEqualTo(uid); } + assertThat((int) item.batteryVoltage).isEqualTo(voltageMv); assertThat(item.batteryChargeUah).isEqualTo(batteryChargeUah); assertThat(item.batteryLevel).isEqualTo(batteryLevel); diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 26934d8e5d6e..4307ec5aa7e1 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -682,19 +682,19 @@ public class UserControllerTest { /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false); setUpAndStartUserInBackground(TEST_USER_ID); - assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID, /* delayedLocking= */ true, + assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID, /* allowDelayedLocking= */ true, /* keyEvictedCallback= */ null, /* expectLocking= */ true); setUpAndStartUserInBackground(TEST_USER_ID1); - assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* delayedLocking= */ true, + assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true, /* keyEvictedCallback= */ mKeyEvictedCallback, /* expectLocking= */ true); setUpAndStartUserInBackground(TEST_USER_ID2); - assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* delayedLocking= */ false, + assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* allowDelayedLocking= */ false, /* keyEvictedCallback= */ null, /* expectLocking= */ true); setUpAndStartUserInBackground(TEST_USER_ID3); - assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID3, /* delayedLocking= */ false, + assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID3, /* allowDelayedLocking= */ false, /* keyEvictedCallback= */ mKeyEvictedCallback, /* expectLocking= */ true); } @@ -739,21 +739,21 @@ public class UserControllerTest { mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ true); - // delayedLocking set and no KeyEvictedCallback, so it should not lock. + // allowDelayedLocking set and no KeyEvictedCallback, so it should not lock. setUpAndStartUserInBackground(TEST_USER_ID); - assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID, /* delayedLocking= */ true, + assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID, /* allowDelayedLocking= */ true, /* keyEvictedCallback= */ null, /* expectLocking= */ false); setUpAndStartUserInBackground(TEST_USER_ID1); - assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* delayedLocking= */ true, + assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true, /* keyEvictedCallback= */ mKeyEvictedCallback, /* expectLocking= */ true); setUpAndStartUserInBackground(TEST_USER_ID2); - assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* delayedLocking= */ false, + assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* allowDelayedLocking= */ false, /* keyEvictedCallback= */ null, /* expectLocking= */ true); setUpAndStartUserInBackground(TEST_USER_ID3); - assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID3, /* delayedLocking= */ false, + assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID3, /* allowDelayedLocking= */ false, /* keyEvictedCallback= */ mKeyEvictedCallback, /* expectLocking= */ true); } @@ -843,7 +843,7 @@ public class UserControllerTest { mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE); setUpAndStartProfileInBackground(TEST_USER_ID1, UserManager.USER_TYPE_PROFILE_PRIVATE); - assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* delayedLocking= */ true, + assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true, /* keyEvictedCallback */ null, /* expectLocking= */ false); } @@ -855,19 +855,20 @@ public class UserControllerTest { mSetFlagsRule.disableFlags( android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE); setUpAndStartProfileInBackground(TEST_USER_ID1, UserManager.USER_TYPE_PROFILE_PRIVATE); - assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* delayedLocking= */ true, + assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true, /* keyEvictedCallback */ null, /* expectLocking= */ true); mSetFlagsRule.disableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE); mSetFlagsRule.enableFlags( android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE); setUpAndStartProfileInBackground(TEST_USER_ID2, UserManager.USER_TYPE_PROFILE_PRIVATE); - assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* delayedLocking= */ true, + assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* allowDelayedLocking= */ true, /* keyEvictedCallback */ null, /* expectLocking= */ true); } + /** Delayed-locking users (as opposed to devices) have no limits on how many can be unlocked. */ @Test - public void testStopPrivateProfileWithDelayedLocking_maxRunningUsersBreached() + public void testStopPrivateProfileWithDelayedLocking_imperviousToNumberOfRunningUsers() throws Exception { mUserController.setInitialConfig(/* mUserSwitchUiEnabled */ true, /* maxRunningUsers= */ 1, /* delayUserDataLocking= */ false); @@ -875,10 +876,14 @@ public class UserControllerTest { android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE); setUpAndStartProfileInBackground(TEST_USER_ID1, UserManager.USER_TYPE_PROFILE_PRIVATE); setUpAndStartProfileInBackground(TEST_USER_ID2, UserManager.USER_TYPE_PROFILE_MANAGED); - assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* delayedLocking= */ true, - /* keyEvictedCallback */ null, /* expectLocking= */ true); + assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true, + /* keyEvictedCallback */ null, /* expectLocking= */ false); } + /** + * Tests that when a device/user (managed profile) does not permit delayed locking, then + * even if allowDelayedLocking is true, the user will still be locked. + */ @Test public void testStopManagedProfileWithDelayedLocking() throws Exception { mUserController.setInitialConfig(/* mUserSwitchUiEnabled */ true, @@ -886,7 +891,7 @@ public class UserControllerTest { mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE); setUpAndStartProfileInBackground(TEST_USER_ID1, UserManager.USER_TYPE_PROFILE_MANAGED); - assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* delayedLocking= */ true, + assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true, /* keyEvictedCallback */ null, /* expectLocking= */ true); } @@ -1087,29 +1092,29 @@ public class UserControllerTest { mUserStates.put(userId, mUserController.getStartedUserState(userId)); } - private void assertUserLockedOrUnlockedAfterStopping(int userId, boolean delayedLocking, + private void assertUserLockedOrUnlockedAfterStopping(int userId, boolean allowDelayedLocking, KeyEvictedCallback keyEvictedCallback, boolean expectLocking) throws Exception { - int r = mUserController.stopUser(userId, /* force= */ true, /* delayedLocking= */ - delayedLocking, null, keyEvictedCallback); + int r = mUserController.stopUser(userId, /* force= */ true, /* allowDelayedLocking= */ + allowDelayedLocking, null, keyEvictedCallback); assertThat(r).isEqualTo(ActivityManager.USER_OP_SUCCESS); - assertUserLockedOrUnlockedState(userId, delayedLocking, expectLocking); + assertUserLockedOrUnlockedState(userId, allowDelayedLocking, expectLocking); } private void assertProfileLockedOrUnlockedAfterStopping(int userId, boolean expectLocking) throws Exception { boolean profileStopped = mUserController.stopProfile(userId); assertThat(profileStopped).isTrue(); - assertUserLockedOrUnlockedState(userId, /* delayedLocking= */ false, expectLocking); + assertUserLockedOrUnlockedState(userId, /* allowDelayedLocking= */ false, expectLocking); } - private void assertUserLockedOrUnlockedState(int userId, boolean delayedLocking, + private void assertUserLockedOrUnlockedState(int userId, boolean allowDelayedLocking, boolean expectLocking) throws InterruptedException, RemoteException { // fake all interim steps UserState ussUser = mUserStates.get(userId); ussUser.setState(UserState.STATE_SHUTDOWN); // Passing delayedLocking invalidates incorrect internal data passing but currently there is // no easy way to get that information passed through lambda. - mUserController.finishUserStopped(ussUser, delayedLocking); + mUserController.finishUserStopped(ussUser, allowDelayedLocking); waitForHandlerToComplete(FgThread.getHandler(), HANDLER_WAIT_TIME_MS); verify(mInjector.mStorageManagerMock, times(expectLocking ? 1 : 0)) .lockCeStorage(userId); diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java index fa3936443f31..b7050771d2e4 100644 --- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java @@ -31,6 +31,7 @@ import static org.testng.Assert.assertNull; import static org.testng.Assert.assertThrows; import android.annotation.NonNull; +import android.hardware.devicestate.DeviceState; import android.hardware.devicestate.DeviceStateInfo; import android.hardware.devicestate.DeviceStateRequest; import android.hardware.devicestate.IDeviceStateManagerCallback; @@ -72,7 +73,8 @@ public final class DeviceStateManagerServiceTest { new DeviceState(0, "DEFAULT", 0 /* flags */); private static final DeviceState OTHER_DEVICE_STATE = new DeviceState(1, "OTHER", 0 /* flags */); - private static final DeviceState DEVICE_STATE_CANCEL_WHEN_REQUESTER_NOT_ON_TOP = + private static final DeviceState + DEVICE_STATE_CANCEL_WHEN_REQUESTER_NOT_ON_TOP = new DeviceState(2, "DEVICE_STATE_CANCEL_WHEN_REQUESTER_NOT_ON_TOP", DeviceState.FLAG_CANCEL_WHEN_REQUESTER_NOT_ON_TOP /* flags */); // A device state that is not reported as being supported for the default test provider. diff --git a/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java b/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java index b3d25f2eef25..cfdb5869e020 100644 --- a/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java @@ -25,6 +25,7 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNull; import android.annotation.Nullable; +import android.hardware.devicestate.DeviceState; import android.hardware.devicestate.DeviceStateRequest; import android.os.Binder; import android.platform.test.annotations.Presubmit; @@ -48,8 +49,10 @@ import java.util.Map; @RunWith(AndroidJUnit4.class) public final class OverrideRequestControllerTest { - private static final DeviceState TEST_DEVICE_STATE_ZERO = new DeviceState(0, "TEST_STATE", 0); - private static final DeviceState TEST_DEVICE_STATE_ONE = new DeviceState(1, "TEST_STATE", 0); + private static final DeviceState + TEST_DEVICE_STATE_ZERO = new DeviceState(0, "TEST_STATE", 0); + private static final DeviceState + TEST_DEVICE_STATE_ONE = new DeviceState(1, "TEST_STATE", 0); private TestStatusChangeListener mStatusListener; private OverrideRequestController mController; diff --git a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java index 7e40f96154d2..16909ab4511e 100644 --- a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java +++ b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java @@ -40,12 +40,12 @@ import android.content.Context; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorManager; +import android.hardware.devicestate.DeviceState; import android.os.PowerManager; import androidx.annotation.NonNull; import com.android.server.LocalServices; -import com.android.server.devicestate.DeviceState; import com.android.server.devicestate.DeviceStateProvider; import com.android.server.input.InputManagerInternal; 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..2f740eac626b 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -303,7 +303,6 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; @@ -2545,6 +2544,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 +2587,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 +3006,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 +3249,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 +5702,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 @@ -14313,7 +14367,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @Ignore("b/324348078") public void cancelNotificationsFromListener_rapidClear_old_cancelOne() throws RemoteException { mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED); @@ -14403,7 +14456,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @Ignore("b/324348078") public void cancelNotificationsFromListener_rapidClear_old_cancelAll() throws RemoteException { mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED); diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java index 1fb7cd8e6e1c..9e2b1eccc3b2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java @@ -32,14 +32,17 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; +import static com.android.server.wm.DragDropController.MSG_UNHANDLED_DROP_LISTENER_TIMEOUT; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.app.PendingIntent; @@ -49,9 +52,12 @@ import android.content.Intent; import android.content.pm.ShortcutServiceInternal; import android.graphics.PixelFormat; import android.os.Binder; +import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import android.os.Message; import android.os.Parcelable; +import android.os.RemoteException; import android.os.UserHandle; import android.platform.test.annotations.Presubmit; import android.view.DragEvent; @@ -61,6 +67,7 @@ import android.view.SurfaceSession; import android.view.View; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; +import android.window.IUnhandledDragListener; import androidx.test.filters.SmallTest; @@ -533,14 +540,98 @@ public class DragDropControllerTests extends WindowTestsBase { }); } + @Test + public void testUnhandledDragListenerNotCalledForNormalDrags() throws RemoteException { + assumeTrue(com.android.window.flags.Flags.delegateUnhandledDrags()); + + final IUnhandledDragListener listener = mock(IUnhandledDragListener.class); + doReturn(mock(Binder.class)).when(listener).asBinder(); + mTarget.setUnhandledDragListener(listener); + doDragAndDrop(0, ClipData.newPlainText("label", "Test"), 0, 0); + verify(listener, times(0)).onUnhandledDrop(any(), any()); + } + + @Test + public void testUnhandledDragListenerReceivesUnhandledDropOverWindow() { + assumeTrue(com.android.window.flags.Flags.delegateUnhandledDrags()); + + final IUnhandledDragListener listener = mock(IUnhandledDragListener.class); + doReturn(mock(Binder.class)).when(listener).asBinder(); + mTarget.setUnhandledDragListener(listener); + final int invalidXY = 100_000; + startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> { + // Notify the unhandled drag listener + mTarget.reportDropWindow(mWindow.mInputChannelToken, invalidXY, invalidXY); + mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); + mTarget.reportDropResult(mWindow.mClient, false); + mTarget.onUnhandledDropCallback(true); + mToken = null; + try { + verify(listener, times(1)).onUnhandledDrop(any(), any()); + } catch (RemoteException e) { + fail("Failed to verify unhandled drop: " + e); + } + }); + } + + @Test + public void testUnhandledDragListenerReceivesUnhandledDropOverNoValidWindow() { + assumeTrue(com.android.window.flags.Flags.delegateUnhandledDrags()); + + final IUnhandledDragListener listener = mock(IUnhandledDragListener.class); + doReturn(mock(Binder.class)).when(listener).asBinder(); + mTarget.setUnhandledDragListener(listener); + final int invalidXY = 100_000; + startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> { + // Notify the unhandled drag listener + mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); + mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); + mTarget.onUnhandledDropCallback(true); + mToken = null; + try { + verify(listener, times(1)).onUnhandledDrop(any(), any()); + } catch (RemoteException e) { + fail("Failed to verify unhandled drop: " + e); + } + }); + } + + @Test + public void testUnhandledDragListenerCallbackTimeout() { + assumeTrue(com.android.window.flags.Flags.delegateUnhandledDrags()); + + final IUnhandledDragListener listener = mock(IUnhandledDragListener.class); + doReturn(mock(Binder.class)).when(listener).asBinder(); + mTarget.setUnhandledDragListener(listener); + final int invalidXY = 100_000; + startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> { + // Notify the unhandled drag listener + mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); + mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); + + // Verify that the unhandled drop listener callback timeout has been scheduled + final Handler handler = mTarget.getHandler(); + assertTrue(handler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT)); + + // Force trigger the timeout and verify that it actually cleans up the drag & timeout + handler.handleMessage(Message.obtain(handler, MSG_UNHANDLED_DROP_LISTENER_TIMEOUT)); + assertFalse(handler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT)); + assertFalse(mTarget.dragDropActiveLocked()); + mToken = null; + }); + } + private void doDragAndDrop(int flags, ClipData data, float dropX, float dropY) { startDrag(flags, data, () -> { mTarget.reportDropWindow(mWindow.mInputChannelToken, dropX, dropY); - mTarget.handleMotionEvent(false, dropX, dropY); + mTarget.handleMotionEvent(false /* keepHandling */, dropX, dropY); mToken = mWindow.mClient.asBinder(); }); } + /** + * Starts a drag with the given parameters, calls Runnable `r` after drag is started. + */ private void startDrag(int flag, ClipData data, Runnable r) { final SurfaceSession appSession = new SurfaceSession(); try { diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index aa9c0c8457f5..03b695d170ad 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -48,6 +48,7 @@ import static com.android.server.wm.testing.Assert.assertThrows; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -901,7 +902,8 @@ public class WindowOrganizerTests extends WindowTestsBase { new Binder(), 0 /* index */, WindowInsets.Type.systemOverlays(), - new Rect(0, 0, 1080, 200)); + new Rect(0, 0, 1080, 200), + null /* boundingRects */); mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct); assertThat(navigationBarInsetsReceiverTask.mLocalInsetsSources @@ -910,6 +912,31 @@ public class WindowOrganizerTests extends WindowTestsBase { } @Test + public void testAddInsetsSource_withBoundingRects() { + final Task rootTask = createTask(mDisplayContent); + + final Task navigationBarInsetsReceiverTask = createTaskInRootTask(rootTask, 0); + navigationBarInsetsReceiverTask.getConfiguration().windowConfiguration.setBounds(new Rect( + 0, 200, 1080, 700)); + + final Rect[] boundingRects = new Rect[]{ + new Rect(0, 0, 10, 10), new Rect(100, 100, 200, 100) + }; + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.addInsetsSource( + navigationBarInsetsReceiverTask.mRemoteToken.toWindowContainerToken(), + new Binder(), + 0 /* index */, + WindowInsets.Type.systemOverlays(), + new Rect(0, 0, 1080, 200), + boundingRects); + mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct); + + assertArrayEquals(boundingRects, navigationBarInsetsReceiverTask.mLocalInsetsSources + .valueAt(0).getBoundingRects()); + } + + @Test public void testRemoveInsetsSource() { final Task rootTask = createTask(mDisplayContent); @@ -923,7 +950,8 @@ public class WindowOrganizerTests extends WindowTestsBase { owner, 0 /* index */, WindowInsets.Type.systemOverlays(), - new Rect(0, 0, 1080, 200)); + new Rect(0, 0, 1080, 200), + null /* boundingRects */); mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct); final WindowContainerTransaction wct2 = new WindowContainerTransaction(); 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/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java index 874c10c8ea83..a52614d5cda1 100644 --- a/telecomm/java/android/telecom/Call.java +++ b/telecomm/java/android/telecom/Call.java @@ -269,6 +269,27 @@ public final class Call { "android.telecom.extra.DIAGNOSTIC_MESSAGE"; /** + * Boolean indicating that the call is a verified business call. + * + * {@link Connection#setExtras(Bundle)} or {@link Connection#putExtras(Bundle)} + * should be used to notify Telecom this extra has been set. + */ + @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER) + public static final String EXTRA_IS_BUSINESS_CALL = + "android.telecom.extra.IS_BUSINESS_CALL"; + + /** + * String value indicating the asserted display name reported via + * ImsCallProfile#EXTRA_ASSERTED_DISPLAY_NAME. + * + * {@link Connection#setExtras(Bundle)} or {@link Connection#putExtras(Bundle)} + * should be used to notify Telecom this extra has been set. + */ + @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER) + public static final String EXTRA_ASSERTED_DISPLAY_NAME = + "android.telecom.extra.ASSERTED_DISPLAY_NAME"; + + /** * Reject reason used with {@link #reject(int)} to indicate that the user is rejecting this * call because they have declined to answer it. This typically means that they are unable * to answer the call at this time and would prefer it be sent to voicemail. diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index f7793f373916..697c8ecd96e7 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -9778,6 +9778,13 @@ public class CarrierConfigManager { public static final String KEY_SUPPORTS_CALL_COMPOSER_BOOL = "supports_call_composer_bool"; /** + * Indicates if the carrier supports a business call composer. + */ + @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_BUSINESS_CALL_COMPOSER) + public static final String KEY_SUPPORTS_BUSINESS_CALL_COMPOSER_BOOL = + "supports_business_call_composer_bool"; + + /** * Indicates the carrier server url that serves the call composer picture. */ public static final String KEY_CALL_COMPOSER_PICTURE_SERVER_URL_STRING = @@ -10861,6 +10868,7 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL, false); sDefaults.putString(KEY_DEFAULT_PREFERRED_APN_NAME_STRING, ""); sDefaults.putBoolean(KEY_SUPPORTS_CALL_COMPOSER_BOOL, false); + sDefaults.putBoolean(KEY_SUPPORTS_BUSINESS_CALL_COMPOSER_BOOL, false); sDefaults.putString(KEY_CALL_COMPOSER_PICTURE_SERVER_URL_STRING, ""); sDefaults.putBoolean(KEY_USE_ACS_FOR_RCS_BOOL, false); sDefaults.putBoolean(KEY_NETWORK_TEMP_NOT_METERED_SUPPORTED_BOOL, true); 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/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 61c7a420b604..041822bf4ee9 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -10629,20 +10629,27 @@ public class TelephonyManager { } /** - * Call composer status OFF from user setting. + * Call composer status <b>OFF</b> from user setting. */ public static final int CALL_COMPOSER_STATUS_OFF = 0; /** - * Call composer status ON from user setting. + * Call composer status <b>ON</b> from user setting. */ public static final int CALL_COMPOSER_STATUS_ON = 1; + /** + * Call composer status <b>Business Only</b> from user setting. + */ + @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_BUSINESS_CALL_COMPOSER) + public static final int CALL_COMPOSER_STATUS_BUSINESS_ONLY = 2; + /** @hide */ @IntDef(prefix = {"CALL_COMPOSER_STATUS_"}, value = { CALL_COMPOSER_STATUS_ON, CALL_COMPOSER_STATUS_OFF, + CALL_COMPOSER_STATUS_BUSINESS_ONLY }) @Retention(RetentionPolicy.SOURCE) public @interface CallComposerStatus {} @@ -10663,9 +10670,16 @@ public class TelephonyManager { @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) public void setCallComposerStatus(@CallComposerStatus int status) { - if (status > CALL_COMPOSER_STATUS_ON - || status < CALL_COMPOSER_STATUS_OFF) { - throw new IllegalArgumentException("requested status is invalid"); + if (com.android.server.telecom.flags.Flags.businessCallComposer()) { + if (status > CALL_COMPOSER_STATUS_BUSINESS_ONLY + || status < CALL_COMPOSER_STATUS_OFF) { + throw new IllegalArgumentException("requested status is invalid"); + } + } else { + if (status > CALL_COMPOSER_STATUS_ON + || status < CALL_COMPOSER_STATUS_OFF) { + throw new IllegalArgumentException("requested status is invalid"); + } } try { ITelephony telephony = getITelephony(); diff --git a/telephony/java/android/telephony/ims/ImsCallProfile.java b/telephony/java/android/telephony/ims/ImsCallProfile.java index d07edeb971ea..cebfe014a062 100644 --- a/telephony/java/android/telephony/ims/ImsCallProfile.java +++ b/telephony/java/android/telephony/ims/ImsCallProfile.java @@ -16,6 +16,7 @@ package android.telephony.ims; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -299,6 +300,16 @@ public final class ImsCallProfile implements Parcelable { "android.telephony.ims.extra.IS_BUSINESS_CALL"; /** + * The vendor IMS stack populates this {@code string} extra; it is used to hold the display name + * passed via the P-Asserted-Identity SIP header’s display-name field + * + * Reference: RFC3325 + */ + @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_BUSINESS_CALL_COMPOSER) + public static final String EXTRA_ASSERTED_DISPLAY_NAME = + "android.telephony.ims.extra.ASSERTED_DISPLAY_NAME"; + + /** * Values for EXTRA_OIR / EXTRA_CNAP */ /** diff --git a/telephony/java/android/telephony/ims/feature/MmTelFeature.java b/telephony/java/android/telephony/ims/feature/MmTelFeature.java index 746246c64e8c..9789082e1460 100644 --- a/telephony/java/android/telephony/ims/feature/MmTelFeature.java +++ b/telephony/java/android/telephony/ims/feature/MmTelFeature.java @@ -17,6 +17,7 @@ package android.telephony.ims.feature; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -59,6 +60,7 @@ import com.android.ims.internal.IImsEcbm; import com.android.ims.internal.IImsMultiEndpoint; import com.android.ims.internal.IImsUt; import com.android.internal.telephony.util.TelephonyUtils; +import com.android.server.telecom.flags.Flags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -513,7 +515,8 @@ public class MmTelFeature extends ImsFeature { CAPABILITY_TYPE_VIDEO, CAPABILITY_TYPE_UT, CAPABILITY_TYPE_SMS, - CAPABILITY_TYPE_CALL_COMPOSER + CAPABILITY_TYPE_CALL_COMPOSER, + CAPABILITY_TYPE_CALL_COMPOSER_BUSINESS_ONLY }) @Retention(RetentionPolicy.SOURCE) public @interface MmTelCapability {} @@ -550,11 +553,19 @@ public class MmTelFeature extends ImsFeature { */ public static final int CAPABILITY_TYPE_CALL_COMPOSER = 1 << 4; + + /** + * This MmTelFeature supports Business-only Call Composer + */ + @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER) + public static final int CAPABILITY_TYPE_CALL_COMPOSER_BUSINESS_ONLY = 1 << 5; + /** * This is used to check the upper range of MmTel capability * @hide */ - public static final int CAPABILITY_TYPE_MAX = CAPABILITY_TYPE_CALL_COMPOSER + 1; + public static final int CAPABILITY_TYPE_MAX = + CAPABILITY_TYPE_CALL_COMPOSER_BUSINESS_ONLY + 1; /** * @hide @@ -601,6 +612,8 @@ public class MmTelFeature extends ImsFeature { builder.append(isCapable(CAPABILITY_TYPE_SMS)); builder.append(" CALL_COMPOSER: "); builder.append(isCapable(CAPABILITY_TYPE_CALL_COMPOSER)); + builder.append(" BUSINESS_COMPOSER_ONLY: "); + builder.append(isCapable(CAPABILITY_TYPE_CALL_COMPOSER_BUSINESS_ONLY)); builder.append("]"); return builder.toString(); } diff --git a/tests/UsbManagerTests/Android.bp b/tests/UsbManagerTests/Android.bp index c02d8e96abb0..a16a7eafc8e8 100644 --- a/tests/UsbManagerTests/Android.bp +++ b/tests/UsbManagerTests/Android.bp @@ -29,12 +29,19 @@ 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", + "flag-junit", + "TestParameterInjector", + ], + jni_libs: [ + // Required for ExtendedMockito + "libdexmakerjvmtiagent", + "libmultiplejvmtiagentsinterferenceagent", + "libstaticjvmtiagent", ], - jni_libs: ["libdexmakerjvmtiagent"], certificate: "platform", platform_apis: true, test_suites: ["device-tests"], diff --git a/tests/UsbManagerTests/src/android/hardware/usb/UsbPortStatusTest.java b/tests/UsbManagerTests/src/android/hardware/usb/UsbPortStatusTest.java new file mode 100644 index 000000000000..dabfcae8e0fd --- /dev/null +++ b/tests/UsbManagerTests/src/android/hardware/usb/UsbPortStatusTest.java @@ -0,0 +1,99 @@ +/* + * 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.hardware.usb; + +import static android.hardware.usb.UsbPortStatus.CONTAMINANT_DETECTION_NOT_SUPPORTED; +import static android.hardware.usb.UsbPortStatus.CONTAMINANT_PROTECTION_NONE; +import static android.hardware.usb.UsbPortStatus.DATA_ROLE_DEVICE; +import static android.hardware.usb.UsbPortStatus.DATA_ROLE_HOST; +import static android.hardware.usb.UsbPortStatus.DATA_ROLE_NONE; +import static android.hardware.usb.UsbPortStatus.MODE_NONE; +import static android.hardware.usb.UsbPortStatus.POWER_ROLE_NONE; +import static android.hardware.usb.UsbPortStatus.POWER_ROLE_SINK; +import static android.hardware.usb.UsbPortStatus.POWER_ROLE_SOURCE; + +import static com.google.common.truth.Truth.assertThat; + +import android.hardware.usb.flags.Flags; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; + +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link android.hardware.usb.UsbPortStatus} */ +@RunWith(TestParameterInjector.class) +public class UsbPortStatusTest { + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_IS_PD_COMPLIANT_API) + public void testIsPdCompliant( + @TestParameter boolean isSinkDeviceRoleSupported, + @TestParameter boolean isSinkHostRoleSupported, + @TestParameter boolean isSourceDeviceRoleSupported, + @TestParameter boolean isSourceHostRoleSupported) { + int supportedRoleCombinations = getSupportedRoleCombinations( + isSinkDeviceRoleSupported, + isSinkHostRoleSupported, + isSourceDeviceRoleSupported, + isSinkHostRoleSupported); + UsbPortStatus usbPortStatus = new UsbPortStatus( + MODE_NONE, + POWER_ROLE_NONE, + DATA_ROLE_NONE, + supportedRoleCombinations, + CONTAMINANT_PROTECTION_NONE, + CONTAMINANT_DETECTION_NOT_SUPPORTED); + boolean expectedResult = isSinkDeviceRoleSupported + && isSinkHostRoleSupported + && isSourceDeviceRoleSupported + && isSourceHostRoleSupported; + + assertThat(usbPortStatus.isPdCompliant()).isEqualTo(expectedResult); + } + + private int getSupportedRoleCombinations( + boolean isSinkDeviceRoleSupported, + boolean isSinkHostRoleSupported, + boolean isSourceDeviceRoleSupported, + boolean isSourceHostRoleSupported) { + int result = UsbPort.combineRolesAsBit(POWER_ROLE_NONE, DATA_ROLE_NONE); + + if (isSinkDeviceRoleSupported) { + result |= UsbPort.combineRolesAsBit(POWER_ROLE_SINK, DATA_ROLE_DEVICE); + } + if (isSinkHostRoleSupported) { + result |= UsbPort.combineRolesAsBit(POWER_ROLE_SINK, DATA_ROLE_HOST); + } + if (isSourceDeviceRoleSupported) { + result |= UsbPort.combineRolesAsBit(POWER_ROLE_SOURCE, DATA_ROLE_DEVICE); + } + if (isSourceHostRoleSupported) { + result |= UsbPort.combineRolesAsBit(POWER_ROLE_SOURCE, DATA_ROLE_HOST); + } + + return result; + } +} 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/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index 60c25b75d2e9..be5c84c0353c 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -14,6 +14,8 @@ package android.testing; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; @@ -85,6 +87,57 @@ public class TestableLooper { setupQueue(looper); } + /** + * Wrap the given runnable so that it will run blocking on the Looper that will be set up for + * the given test. + * <p> + * This method is required to support any TestRule which needs to run setup and/or teardown code + * on the TestableLooper. Whether using {@link AndroidTestingRunner} or + * {@link TestWithLooperRule}, the TestRule's Statement evaluates on the test instrumentation + * thread, rather than the TestableLooper thread, so access to the TestableLooper is required. + * However, {@link #get(Object)} will return {@code null} both before and after the inner + * statement is evaluated: + * <ul> + * <li>Before the test {@link #get} returns {@code null} because while the TestableLooperHolder + * is accessible in sLoopers, it has not been initialized with an actual TestableLooper yet. + * This method's use of the internal LooperFrameworkMethod ensures that all setup and teardown + * of the TestableLooper happen as it would for all other wrapped code blocks. + * <li>After the test {@link #get} can return {@code null} because many tests call + * {@link #remove} in the teardown method. The fact that this method returns a runnable allows + * it to be called before the test (when the TestableLooperHolder is still in sLoopers), and + * then executed as teardown after the test. + * </ul> + * + * @param test the test instance (just like passed to {@link #get(Object)}) + * @param runnable the operation that should eventually be run on the TestableLooper + * @return a runnable that will block the thread on which it is called until the given runnable + * is finished. Will be {@code null} if there is no looper for the given test. + * @hide + */ + @Nullable + public static RunnableWithException wrapWithRunBlocking( + Object test, @NonNull RunnableWithException runnable) { + TestableLooperHolder looperHolder = sLoopers.get(test); + if (looperHolder == null) { + return null; + } + try { + FrameworkMethod base = new FrameworkMethod(runnable.getClass().getMethod("run")); + LooperFrameworkMethod wrapped = new LooperFrameworkMethod(base, looperHolder); + return () -> { + try { + wrapped.invokeExplosively(runnable); + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable e) { + throw new RuntimeException(e); + } + }; + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + public Looper getLooper() { return mLooper; } diff --git a/tools/hoststubgen/hoststubgen/Android.bp b/tools/hoststubgen/hoststubgen/Android.bp index 57bcc04a8aec..c6dd29ce7cc6 100644 --- a/tools/hoststubgen/hoststubgen/Android.bp +++ b/tools/hoststubgen/hoststubgen/Android.bp @@ -180,7 +180,6 @@ java_library { "framework-minus-apex.ravenwood", ], static_libs: [ - "core-xml-for-device", "hoststubgen-helper-libcore-runtime.ravenwood", ], } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt index 06eeb47c94ed..1089f82b6472 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt @@ -382,7 +382,7 @@ class HostStubGen(val options: HostStubGenOptions) { stubOutStream.putNextEntry(newEntry) convertClass(classInternalName, /*forImpl=*/false, bis, stubOutStream, filter, packageRedirector, enableChecker, classes, - errors, stats) + errors, null) stubOutStream.closeEntry() } } @@ -415,7 +415,7 @@ class HostStubGen(val options: HostStubGenOptions) { enableChecker: Boolean, classes: ClassNodes, errors: HostStubGenErrors, - stats: HostStubGenStats, + stats: HostStubGenStats?, ) { val cr = ClassReader(input) diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt index fe4072f6b3ee..50518e1ccd9c 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt @@ -17,6 +17,7 @@ package com.android.hoststubgen import com.android.hoststubgen.asm.toHumanReadableClassName import com.android.hoststubgen.filters.FilterPolicyWithReason +import org.objectweb.asm.Opcodes import java.io.PrintWriter open class HostStubGenStats { @@ -28,12 +29,26 @@ open class HostStubGenStats { private val stats = mutableMapOf<String, Stats>() - fun onVisitPolicyForMethod(fullClassName: String, policy: FilterPolicyWithReason) { + fun onVisitPolicyForMethod(fullClassName: String, methodName: String, descriptor: String, + policy: FilterPolicyWithReason, access: Int) { + // Ignore methods that aren't public + if ((access and Opcodes.ACC_PUBLIC) == 0) return + // Ignore methods that are abstract + if ((access and Opcodes.ACC_ABSTRACT) != 0) return + // Ignore methods where policy isn't relevant if (policy.isIgnoredForStats) return val packageName = resolvePackageName(fullClassName) val className = resolveClassName(fullClassName) + // Ignore methods for certain generated code + if (className.endsWith("Proto") + or className.endsWith("ProtoEnums") + or className.endsWith("LogTags") + or className.endsWith("StatsLog")) { + return + } + val packageStats = stats.getOrPut(packageName) { Stats() } val classStats = packageStats.children.getOrPut(className) { Stats() } 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 53eb5a8c2fdc..eb03f66b5afa 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt @@ -72,6 +72,6 @@ data class FilterPolicyWithReason ( || reason.contains("is-enum") || reason.contains("is-synthetic-method") || reason.contains("special-class") - || reason.contains("substitute-from") + || reason.contains("substitute-to") } } 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 c20aa8bc70ca..45e140c8e3ff 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt @@ -51,7 +51,7 @@ abstract class BaseAdapter ( */ data class Options ( val errors: HostStubGenErrors, - val stats: HostStubGenStats, + val stats: HostStubGenStats?, val enablePreTrace: Boolean, val enablePostTrace: Boolean, val enableNonStubMethodCallDetection: Boolean, @@ -178,6 +178,7 @@ abstract class BaseAdapter ( } val p = filter.getPolicyForMethod(currentClassName, name, descriptor) log.d("visitMethod: %s%s [%x] [%s] Policy: %s", name, descriptor, access, signature, p) + options.stats?.onVisitPolicyForMethod(currentClassName, name, descriptor, p, access) log.withIndent { // If it's a substitute-from method, then skip (== remove). 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 beca945a2819..416b78242899 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt @@ -141,11 +141,6 @@ 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 |