diff options
362 files changed, 9494 insertions, 2924 deletions
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index 7de67993bc62..60eb4ac61076 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -124,6 +124,15 @@ public class JobInfo implements Parcelable { @Overridable // Aid in testing public static final long ENFORCE_MINIMUM_TIME_WINDOWS = 311402873L; + /** + * Require that minimum latencies and override deadlines are nonnegative. + * + * @hide + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + public static final long REJECT_NEGATIVE_DELAYS_AND_DEADLINES = 323349338L; + /** @hide */ @IntDef(prefix = { "NETWORK_TYPE_" }, value = { NETWORK_TYPE_NONE, @@ -692,14 +701,14 @@ public class JobInfo implements Parcelable { * @see JobInfo.Builder#setMinimumLatency(long) */ public long getMinLatencyMillis() { - return minLatencyMillis; + return Math.max(0, minLatencyMillis); } /** * @see JobInfo.Builder#setOverrideDeadline(long) */ public long getMaxExecutionDelayMillis() { - return maxExecutionDelayMillis; + return Math.max(0, maxExecutionDelayMillis); } /** @@ -1869,6 +1878,13 @@ public class JobInfo implements Parcelable { * Because it doesn't make sense setting this property on a periodic job, doing so will * throw an {@link java.lang.IllegalArgumentException} when * {@link android.app.job.JobInfo.Builder#build()} is called. + * + * Negative latencies also don't make sense for a job and are indicative of an error, + * so starting in Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, + * setting a negative deadline will result in + * {@link android.app.job.JobInfo.Builder#build()} throwing an + * {@link java.lang.IllegalArgumentException}. + * * @param minLatencyMillis Milliseconds before which this job will not be considered for * execution. * @see JobInfo#getMinLatencyMillis() @@ -1892,6 +1908,13 @@ public class JobInfo implements Parcelable { * throw an {@link java.lang.IllegalArgumentException} when * {@link android.app.job.JobInfo.Builder#build()} is called. * + * <p> + * Negative deadlines also don't make sense for a job and are indicative of an error, + * so starting in Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, + * setting a negative deadline will result in + * {@link android.app.job.JobInfo.Builder#build()} throwing an + * {@link java.lang.IllegalArgumentException}. + * * <p class="note"> * Since a job will run once the deadline has passed regardless of the status of other * constraints, setting a deadline of 0 (or a {@link #setMinimumLatency(long) delay} equal @@ -2189,13 +2212,15 @@ public class JobInfo implements Parcelable { public JobInfo build() { return build(Compatibility.isChangeEnabled(DISALLOW_DEADLINES_FOR_PREFETCH_JOBS), Compatibility.isChangeEnabled(REJECT_NEGATIVE_NETWORK_ESTIMATES), - Compatibility.isChangeEnabled(ENFORCE_MINIMUM_TIME_WINDOWS)); + Compatibility.isChangeEnabled(ENFORCE_MINIMUM_TIME_WINDOWS), + Compatibility.isChangeEnabled(REJECT_NEGATIVE_DELAYS_AND_DEADLINES)); } /** @hide */ public JobInfo build(boolean disallowPrefetchDeadlines, boolean rejectNegativeNetworkEstimates, - boolean enforceMinimumTimeWindows) { + boolean enforceMinimumTimeWindows, + boolean rejectNegativeDelaysAndDeadlines) { // This check doesn't need to be inside enforceValidity. It's an unnecessary legacy // check that would ideally be phased out instead. if (mBackoffPolicySet && (mConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) { @@ -2205,7 +2230,7 @@ public class JobInfo implements Parcelable { } JobInfo jobInfo = new JobInfo(this); jobInfo.enforceValidity(disallowPrefetchDeadlines, rejectNegativeNetworkEstimates, - enforceMinimumTimeWindows); + enforceMinimumTimeWindows, rejectNegativeDelaysAndDeadlines); return jobInfo; } @@ -2225,7 +2250,8 @@ public class JobInfo implements Parcelable { */ public final void enforceValidity(boolean disallowPrefetchDeadlines, boolean rejectNegativeNetworkEstimates, - boolean enforceMinimumTimeWindows) { + boolean enforceMinimumTimeWindows, + boolean rejectNegativeDelaysAndDeadlines) { // Check that network estimates require network type and are reasonable values. if ((networkDownloadBytes > 0 || networkUploadBytes > 0 || minimumNetworkChunkBytes > 0) && networkRequest == null) { @@ -2259,6 +2285,17 @@ public class JobInfo implements Parcelable { throw new IllegalArgumentException("Minimum chunk size must be positive"); } + if (rejectNegativeDelaysAndDeadlines) { + if (minLatencyMillis < 0) { + throw new IllegalArgumentException( + "Minimum latency is negative: " + minLatencyMillis); + } + if (maxExecutionDelayMillis < 0) { + throw new IllegalArgumentException( + "Override deadline is negative: " + maxExecutionDelayMillis); + } + } + final boolean hasDeadline = maxExecutionDelayMillis != 0L; // Check that a deadline was not set on a periodic job. if (isPeriodic) { diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index a83c099b764d..f819f15b430f 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -4850,7 +4850,7 @@ public class JobSchedulerService extends com.android.server.SystemService Slog.w(TAG, "Uid " + uid + " set bias on its job"); return new JobInfo.Builder(job) .setBias(JobInfo.BIAS_DEFAULT) - .build(false, false, false); + .build(false, false, false, false); } } @@ -4874,7 +4874,9 @@ public class JobSchedulerService extends com.android.server.SystemService JobInfo.DISALLOW_DEADLINES_FOR_PREFETCH_JOBS, callingUid), rejectNegativeNetworkEstimates, CompatChanges.isChangeEnabled( - JobInfo.ENFORCE_MINIMUM_TIME_WINDOWS, callingUid)); + JobInfo.ENFORCE_MINIMUM_TIME_WINDOWS, callingUid), + CompatChanges.isChangeEnabled( + JobInfo.REJECT_NEGATIVE_DELAYS_AND_DEADLINES, callingUid)); if ((job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0) { getContext().enforceCallingOrSelfPermission( android.Manifest.permission.CONNECTIVITY_INTERNAL, TAG); diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java index 53b14d616ecc..d8934d8f83b8 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java @@ -1495,7 +1495,7 @@ public final class JobStore { // return value), the deadline is dropped. Periodic jobs require all constraints // to be met, so there's no issue with their deadlines. // The same logic applies for other target SDK-based validation checks. - builtJob = jobBuilder.build(false, false, false); + builtJob = jobBuilder.build(false, false, false, false); } catch (Exception e) { Slog.w(TAG, "Unable to build job from XML, ignoring: " + jobBuilder.summarize(), e); return null; diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index a0b9c5fb7a60..edd86e3454a5 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -652,7 +652,7 @@ public final class JobStatus { .build()); // Don't perform validation checks at this point since we've already passed the // initial validation check. - job = builder.build(false, false, false); + job = builder.build(false, false, false, false); } this.job = job; diff --git a/api/api.go b/api/api.go index c733f5b5bffd..e8858230ba4e 100644 --- a/api/api.go +++ b/api/api.go @@ -79,7 +79,45 @@ func registerBuildComponents(ctx android.RegistrationContext) { var PrepareForCombinedApisTest = android.FixtureRegisterWithContext(registerBuildComponents) +func (a *CombinedApis) apiFingerprintStubDeps() []string { + ret := []string{} + ret = append( + ret, + transformArray(a.properties.Bootclasspath, "", ".stubs")..., + ) + ret = append( + ret, + transformArray(a.properties.Bootclasspath, "", ".stubs.system")..., + ) + ret = append( + ret, + transformArray(a.properties.Bootclasspath, "", ".stubs.module_lib")..., + ) + ret = append( + ret, + transformArray(a.properties.System_server_classpath, "", ".stubs.system_server")..., + ) + return ret +} + +func (a *CombinedApis) DepsMutator(ctx android.BottomUpMutatorContext) { + ctx.AddDependency(ctx.Module(), nil, a.apiFingerprintStubDeps()...) +} + func (a *CombinedApis) GenerateAndroidBuildActions(ctx android.ModuleContext) { + ctx.WalkDeps(func(child, parent android.Module) bool { + if _, ok := child.(java.AndroidLibraryDependency); ok && child.Name() != "framework-res" { + // Stubs of BCP and SSCP libraries should not have any dependencies on apps + // This check ensures that we do not run into circular dependencies when UNBUNDLED_BUILD_TARGET_SDK_WITH_API_FINGERPRINT=true + ctx.ModuleErrorf( + "Module %s is not a valid dependency of the stub library %s\n."+ + "If this dependency has been added via `libs` of java_sdk_library, please move it to `impl_only_libs`\n", + child.Name(), parent.Name()) + return false // error detected + } + return true + }) + } type genruleProps struct { diff --git a/cmds/uinput/README.md b/cmds/uinput/README.md index b6e4e0de8cf7..f1775864aca0 100644 --- a/cmds/uinput/README.md +++ b/cmds/uinput/README.md @@ -154,7 +154,8 @@ will be unregistered. There is no explicit command for unregistering a device. #### `delay` -Add a delay to command processing +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. | 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 a78a46504684..bd61000186e5 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(uint16_t type, uint16_t code, int32_t value) { +void UinputDevice::injectEvent(std::chrono::microseconds timestamp, uint16_t type, uint16_t code, + int32_t value) { struct input_event event = {}; event.type = type; event.code = code; event.value = value; - timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - TIMESPEC_TO_TIMEVAL(&event.time, &ts); + event.time.tv_sec = timestamp.count() / 1'000'000; + event.time.tv_usec = timestamp.count() % 1'000'000; 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, jint type, jint code, - jint value) { +static void injectEvent(JNIEnv* /* env */, jclass /* clazz */, jlong ptr, jlong timestampMicros, + jint type, jint code, jint value) { uinput::UinputDevice* d = reinterpret_cast<uinput::UinputDevice*>(ptr); if (d != nullptr) { - d->injectEvent(static_cast<uint16_t>(type), static_cast<uint16_t>(code), - static_cast<int32_t>(value)); + d->injectEvent(std::chrono::microseconds(timestampMicros), 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", "(JIII)V", reinterpret_cast<void*>(injectEvent)}, + {"nativeInjectEvent", "(JJIII)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 9769a75bd9ef..72c8647ae743 100644 --- a/cmds/uinput/jni/com_android_commands_uinput_Device.h +++ b/cmds/uinput/jni/com_android_commands_uinput_Device.h @@ -14,13 +14,14 @@ * limitations under the License. */ -#include <memory> -#include <vector> - +#include <android-base/unique_fd.h> #include <jni.h> #include <linux/input.h> -#include <android-base/unique_fd.h> +#include <chrono> +#include <memory> +#include <vector> + #include "src/com/android/commands/uinput/InputAbsInfo.h" namespace android { @@ -53,7 +54,8 @@ public: virtual ~UinputDevice(); - void injectEvent(uint16_t type, uint16_t code, int32_t value); + void injectEvent(std::chrono::microseconds timestamp, 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 25d3a341bf6e..b452fc7094ba 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 mTimeToSend; + private long mTimeToSendNanos; static { System.loadLibrary("uinputcommand_jni"); @@ -65,7 +65,8 @@ 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, int type, int code, int value); + private static native void nativeInjectEvent(long ptr, long timestampMicros, 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); @@ -101,27 +102,54 @@ public class Device { } mHandler.obtainMessage(MSG_OPEN_UINPUT_DEVICE, args).sendToTarget(); - mTimeToSend = SystemClock.uptimeMillis(); + 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); } /** * 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) { + public void injectEvent(int[] events, long offsetMicros) { // if two messages are sent at identical time, they will be processed in order received - Message msg = mHandler.obtainMessage(MSG_INJECT_EVENT, events); - mHandler.sendMessageAtTime(msg, mTimeToSend); + SomeArgs args = SomeArgs.obtain(); + args.arg1 = events; + args.argl1 = offsetMicros; + args.argl2 = SystemClock.uptimeNanos(); + Message msg = mHandler.obtainMessage(MSG_INJECT_EVENT, args); + mHandler.sendMessageAtTime(msg, getTimeToSendMillis()); } /** - * Impose a delay to the device for execution. + * Delay subsequent device activity by the specified amount of time. * - * @param delay Time to delay in unit of milliseconds. + * <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. */ - public void addDelay(int delay) { - mTimeToSend = Math.max(SystemClock.uptimeMillis(), mTimeToSend) + delay; + public void addDelayNanos(long delayNanos) { + mTimeToSendNanos += delayNanos; } /** @@ -131,7 +159,8 @@ public class Device { * @param syncToken The token for this sync command. */ public void syncEvent(String syncToken) { - mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_SYNC_EVENT, syncToken), mTimeToSend); + mHandler.sendMessageAtTime( + mHandler.obtainMessage(MSG_SYNC_EVENT, syncToken), getTimeToSendMillis()); } /** @@ -140,7 +169,8 @@ public class Device { */ public void close() { Message msg = mHandler.obtainMessage(MSG_CLOSE_UINPUT_DEVICE); - mHandler.sendMessageAtTime(msg, Math.max(SystemClock.uptimeMillis(), mTimeToSend) + 1); + mHandler.sendMessageAtTime( + msg, Math.max(SystemClock.uptimeMillis(), getTimeToSendMillis()) + 1); try { synchronized (mCond) { mCond.wait(); @@ -151,6 +181,7 @@ public class Device { private class DeviceHandler extends Handler { private long mPtr; + private long mLastInjectTimestampMicros = -1; private int mBarrierToken; DeviceHandler(Looper looper) { @@ -160,7 +191,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 */, @@ -177,15 +208,43 @@ public class Device { } args.recycle(); break; - 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]); - } + } + 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 at which we scheduled this first + // batch, 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]); + } + args.recycle(); break; - case MSG_CLOSE_UINPUT_DEVICE: + } + case MSG_CLOSE_UINPUT_DEVICE: { if (mPtr != 0) { nativeCloseUinputDevice(mPtr); getLooper().quitSafely(); @@ -198,11 +257,14 @@ 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 7652f2403f6e..da991624e685 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_MILLIS = 500; + private static final int REGISTRATION_DELAY_NANOS = 500_000_000; 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.setDurationMillis(REGISTRATION_DELAY_MILLIS); + delayEb.setDurationNanos(REGISTRATION_DELAY_NANOS); mQueuedEvents.add(delayEb.build()); } @@ -175,7 +175,6 @@ 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(); @@ -192,21 +191,18 @@ public class EvemuParser implements EventParser { return eb.build(); } else { final long delayMicros = timeMicros - mLastEventTimeMicros; - // 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; + eb.setTimestampOffsetMicros(delayMicros); + if (delayMicros == 0) { 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 0f16a27aac1d..9e7ee0937efe 100644 --- a/cmds/uinput/src/com/android/commands/uinput/Event.java +++ b/cmds/uinput/src/com/android/commands/uinput/Event.java @@ -99,8 +99,9 @@ public class Event { private int mVersionId; private int mBusId; private int[] mInjections; + private long mTimestampOffsetMicros = -1; private SparseArray<int[]> mConfiguration; - private int mDurationMillis; + private long mDurationNanos; private int mFfEffectsMax = 0; private String mInputPort; private SparseArray<InputAbsInfo> mAbsInfo; @@ -139,19 +140,28 @@ 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 int getDurationMillis() { - return mDurationMillis; + public long getDurationNanos() { + return mDurationNanos; } public int getFfEffectsMax() { @@ -182,7 +192,7 @@ public class Event { + ", busId=" + mBusId + ", events=" + Arrays.toString(mInjections) + ", configuration=" + mConfiguration - + ", duration=" + mDurationMillis + "ms" + + ", duration=" + mDurationNanos + "ns" + ", ff_effects_max=" + mFfEffectsMax + ", port=" + mInputPort + "}"; @@ -211,6 +221,10 @@ 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. * @@ -237,8 +251,8 @@ public class Event { mEvent.mBusId = busId; } - public void setDurationMillis(int durationMillis) { - mEvent.mDurationMillis = durationMillis; + public void setDurationNanos(long durationNanos) { + mEvent.mDurationNanos = durationNanos; } public void setFfEffectsMax(int ffEffectsMax) { @@ -271,7 +285,7 @@ public class Event { } } case DELAY -> { - if (mEvent.mDurationMillis <= 0) { + if (mEvent.mDurationNanos <= 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 ed3ff33f7e52..6994f0cb0e4b 100644 --- a/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java +++ b/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java @@ -71,7 +71,8 @@ public class JsonStyleParser implements EventParser { case "configuration" -> eb.setConfiguration(readConfiguration()); case "ff_effects_max" -> eb.setFfEffectsMax(readInt()); case "abs_info" -> eb.setAbsInfo(readAbsInfoArray()); - case "duration" -> eb.setDurationMillis(readInt()); + // Duration is specified in milliseconds in the JSON-style format. + case "duration" -> eb.setDurationNanos(readInt() * 1_000_000L); 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 04df27987d58..760e981c8465 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()); - case DELAY -> d.addDelay(e.getDurationMillis()); + case INJECT -> d.injectEvent(e.getInjections(), e.getTimestampOffsetMicros()); + case DELAY -> d.addDelayNanos(e.getDurationNanos()); 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 a05cc676ddfa..5239fbc7e0a8 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,16 +183,22 @@ 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 durationMillis) { + private void assertDelayEvent(Event event, int durationNanos) { assertThat(event).isNotNull(); assertThat(event.getCommand()).isEqualTo(Event.Command.DELAY); - assertThat(event.getDurationMillis()).isEqualTo(durationMillis); + assertThat(event.getDurationNanos()).isEqualTo(durationNanos); } @Test @@ -207,7 +213,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); + assertInjectEvent(parser.getNextEvent(), 0x2, 0x0, 1, -1); assertInjectEvent(parser.getNextEvent(), 0x2, 0x1, -2); assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0); } @@ -228,17 +234,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); + assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1, -1); assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0); - assertDelayEvent(parser.getNextEvent(), 10); + assertDelayEvent(parser.getNextEvent(), 10_000_000); - assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 0); + assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 0, 10_000); assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0); - assertDelayEvent(parser.getNextEvent(), 1000); + assertDelayEvent(parser.getNextEvent(), 1_000_000_000); - assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1); + assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1, 1_000_000); assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0); } @@ -477,7 +483,7 @@ public class EvemuParserTest { assertThat(parser.getNextEvent().getCommand()).isEqualTo(Event.Command.DELAY); - assertInjectEvent(parser.getNextEvent(), 0x3, 0x39, 0); + assertInjectEvent(parser.getNextEvent(), 0x3, 0x39, 0, -1); assertInjectEvent(parser.getNextEvent(), 0x3, 0x35, 891); assertInjectEvent(parser.getNextEvent(), 0x3, 0x36, 333); assertInjectEvent(parser.getNextEvent(), 0x3, 0x3a, 56); @@ -490,8 +496,8 @@ public class EvemuParserTest { assertInjectEvent(parser.getNextEvent(), 0x3, 0x18, 56); assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0); - assertDelayEvent(parser.getNextEvent(), 6); + assertDelayEvent(parser.getNextEvent(), 6_080_000); - assertInjectEvent(parser.getNextEvent(), 0x3, 0x0035, 888); + assertInjectEvent(parser.getNextEvent(), 0x3, 0x0035, 888, 6_080); } } diff --git a/core/api/current.txt b/core/api/current.txt index c4cd575c86f2..7052fd1c08e9 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -140,6 +140,7 @@ package android { field public static final String MANAGE_DEVICE_POLICY_APPS_CONTROL = "android.permission.MANAGE_DEVICE_POLICY_APPS_CONTROL"; field public static final String MANAGE_DEVICE_POLICY_APP_RESTRICTIONS = "android.permission.MANAGE_DEVICE_POLICY_APP_RESTRICTIONS"; field public static final String MANAGE_DEVICE_POLICY_APP_USER_DATA = "android.permission.MANAGE_DEVICE_POLICY_APP_USER_DATA"; + field @FlaggedApi("android.app.admin.flags.assist_content_user_restriction_enabled") public static final String MANAGE_DEVICE_POLICY_ASSIST_CONTENT = "android.permission.MANAGE_DEVICE_POLICY_ASSIST_CONTENT"; field public static final String MANAGE_DEVICE_POLICY_AUDIO_OUTPUT = "android.permission.MANAGE_DEVICE_POLICY_AUDIO_OUTPUT"; field public static final String MANAGE_DEVICE_POLICY_AUTOFILL = "android.permission.MANAGE_DEVICE_POLICY_AUTOFILL"; field public static final String MANAGE_DEVICE_POLICY_BACKUP_SERVICE = "android.permission.MANAGE_DEVICE_POLICY_BACKUP_SERVICE"; @@ -165,6 +166,7 @@ package android { field public static final String MANAGE_DEVICE_POLICY_LOCK = "android.permission.MANAGE_DEVICE_POLICY_LOCK"; field public static final String MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS = "android.permission.MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS"; field public static final String MANAGE_DEVICE_POLICY_LOCK_TASK = "android.permission.MANAGE_DEVICE_POLICY_LOCK_TASK"; + field @FlaggedApi("android.app.admin.flags.esim_management_enabled") public static final String MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS = "android.permission.MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS"; field public static final String MANAGE_DEVICE_POLICY_METERED_DATA = "android.permission.MANAGE_DEVICE_POLICY_METERED_DATA"; field public static final String MANAGE_DEVICE_POLICY_MICROPHONE = "android.permission.MANAGE_DEVICE_POLICY_MICROPHONE"; field public static final String MANAGE_DEVICE_POLICY_MOBILE_NETWORK = "android.permission.MANAGE_DEVICE_POLICY_MOBILE_NETWORK"; @@ -653,6 +655,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 @@ -1602,6 +1605,7 @@ package android { field public static final int supportedTypes = 16844369; // 0x1010651 field public static final int supportsAssist = 16844016; // 0x10104f0 field public static final int supportsBatteryGameMode = 16844374; // 0x1010656 + field @FlaggedApi("android.view.inputmethod.connectionless_handwriting") public static final int supportsConnectionlessStylusHandwriting; field public static final int supportsInlineSuggestions = 16844301; // 0x101060d field public static final int supportsInlineSuggestionsWithTouchExploration = 16844397; // 0x101066d field public static final int supportsLaunchVoiceAssistFromKeyguard = 16844017; // 0x10104f1 @@ -2005,6 +2009,19 @@ package android { field public static final int system_control_highlight_light = 17170558; // 0x106007e field public static final int system_control_normal_dark = 17170600; // 0x10600a8 field public static final int system_control_normal_light = 17170557; // 0x106007d + field public static final int system_error_0; + field public static final int system_error_10; + field public static final int system_error_100; + field public static final int system_error_1000; + field public static final int system_error_200; + field public static final int system_error_300; + field public static final int system_error_400; + field public static final int system_error_50; + field public static final int system_error_500; + field public static final int system_error_600; + field public static final int system_error_700; + field public static final int system_error_800; + field public static final int system_error_900; field public static final int system_error_container_dark = 17170597; // 0x10600a5 field public static final int system_error_container_light = 17170554; // 0x106007a field public static final int system_error_dark = 17170595; // 0x10600a3 @@ -2054,6 +2071,7 @@ package android { field public static final int system_on_secondary_fixed_variant = 17170619; // 0x10600bb field public static final int system_on_secondary_light = 17170533; // 0x1060065 field public static final int system_on_surface_dark = 17170584; // 0x1060098 + field public static final int system_on_surface_disabled; field public static final int system_on_surface_light = 17170541; // 0x106006d field public static final int system_on_surface_variant_dark = 17170593; // 0x10600a1 field public static final int system_on_surface_variant_light = 17170550; // 0x1060076 @@ -2064,6 +2082,7 @@ package android { field public static final int system_on_tertiary_fixed_variant = 17170623; // 0x10600bf field public static final int system_on_tertiary_light = 17170537; // 0x1060069 field public static final int system_outline_dark = 17170594; // 0x10600a2 + field public static final int system_outline_disabled; field public static final int system_outline_light = 17170551; // 0x1060077 field public static final int system_outline_variant_dark = 17170625; // 0x10600c1 field public static final int system_outline_variant_light = 17170624; // 0x10600c0 @@ -2104,6 +2123,7 @@ package android { field public static final int system_surface_dark = 17170583; // 0x1060097 field public static final int system_surface_dim_dark = 17170591; // 0x106009f field public static final int system_surface_dim_light = 17170548; // 0x1060074 + field public static final int system_surface_disabled; field public static final int system_surface_light = 17170540; // 0x106006c field public static final int system_surface_variant_dark = 17170592; // 0x10600a0 field public static final int system_surface_variant_light = 17170549; // 0x1060075 @@ -2140,6 +2160,11 @@ package android { field public static final int notification_large_icon_width = 17104901; // 0x1050005 field public static final int system_app_widget_background_radius = 17104904; // 0x1050008 field public static final int system_app_widget_inner_radius = 17104905; // 0x1050009 + field public static final int system_corner_radius_large; + field public static final int system_corner_radius_medium; + field public static final int system_corner_radius_small; + field public static final int system_corner_radius_xlarge; + field public static final int system_corner_radius_xsmall; field public static final int thumbnail_height = 17104897; // 0x1050001 field public static final int thumbnail_width = 17104898; // 0x1050002 } @@ -8044,6 +8069,7 @@ package android.app.admin { method public CharSequence getStartUserSessionMessage(@NonNull android.content.ComponentName); method @Deprecated public boolean getStorageEncryption(@Nullable android.content.ComponentName); method public int getStorageEncryptionStatus(); + method @FlaggedApi("android.app.admin.flags.esim_management_enabled") @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS) public java.util.Set<java.lang.Integer> getSubscriptionsIds(); method @Nullable public android.app.admin.SystemUpdatePolicy getSystemUpdatePolicy(); method @Nullable public android.os.PersistableBundle getTransferOwnershipBundle(); method @Nullable public java.util.List<android.os.PersistableBundle> getTrustAgentConfiguration(@Nullable android.content.ComponentName, @NonNull android.content.ComponentName); @@ -10725,6 +10751,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 @@ -11315,8 +11342,10 @@ 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"; @@ -13040,6 +13069,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"; @@ -19197,7 +19227,7 @@ package android.hardware.camera2 { method public int getCameraAudioRestriction() throws android.hardware.camera2.CameraAccessException; method @NonNull public abstract String getId(); method @FlaggedApi("com.android.internal.camera.flags.feature_combination_query") @NonNull public android.hardware.camera2.CameraCharacteristics getSessionCharacteristics(@NonNull android.hardware.camera2.params.SessionConfiguration) throws android.hardware.camera2.CameraAccessException; - method @Deprecated public boolean isSessionConfigurationSupported(@NonNull android.hardware.camera2.params.SessionConfiguration) throws android.hardware.camera2.CameraAccessException; + method public boolean isSessionConfigurationSupported(@NonNull android.hardware.camera2.params.SessionConfiguration) throws android.hardware.camera2.CameraAccessException; method public void setCameraAudioRestriction(int) throws android.hardware.camera2.CameraAccessException; field public static final int AUDIO_RESTRICTION_NONE = 0; // 0x0 field public static final int AUDIO_RESTRICTION_VIBRATION = 1; // 0x1 @@ -19210,6 +19240,13 @@ package android.hardware.camera2 { field public static final int TEMPLATE_ZERO_SHUTTER_LAG = 5; // 0x5 } + @FlaggedApi("com.android.internal.camera.flags.camera_device_setup") public abstract static class CameraDevice.CameraDeviceSetup { + method @FlaggedApi("com.android.internal.camera.flags.camera_device_setup") @NonNull public abstract android.hardware.camera2.CaptureRequest.Builder createCaptureRequest(int) throws android.hardware.camera2.CameraAccessException; + method @FlaggedApi("com.android.internal.camera.flags.camera_device_setup") @NonNull public abstract String getId(); + method @FlaggedApi("com.android.internal.camera.flags.camera_device_setup") public abstract boolean isSessionConfigurationSupported(@NonNull android.hardware.camera2.params.SessionConfiguration) throws android.hardware.camera2.CameraAccessException; + method @FlaggedApi("com.android.internal.camera.flags.camera_device_setup") @RequiresPermission(android.Manifest.permission.CAMERA) public abstract void openCamera(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException; + } + public abstract static class CameraDevice.StateCallback { ctor public CameraDevice.StateCallback(); method public void onClosed(@NonNull android.hardware.camera2.CameraDevice); @@ -19278,14 +19315,14 @@ package android.hardware.camera2 { } public final class CameraManager { - method @FlaggedApi("com.android.internal.camera.flags.feature_combination_query") @NonNull @RequiresPermission(android.Manifest.permission.CAMERA) public android.hardware.camera2.CaptureRequest.Builder createCaptureRequest(@NonNull String, int) throws android.hardware.camera2.CameraAccessException; method @NonNull public android.hardware.camera2.CameraCharacteristics getCameraCharacteristics(@NonNull String) throws android.hardware.camera2.CameraAccessException; + method @FlaggedApi("com.android.internal.camera.flags.camera_device_setup") @NonNull public android.hardware.camera2.CameraDevice.CameraDeviceSetup getCameraDeviceSetup(@NonNull String) throws android.hardware.camera2.CameraAccessException; method @NonNull public android.hardware.camera2.CameraExtensionCharacteristics getCameraExtensionCharacteristics(@NonNull String) throws android.hardware.camera2.CameraAccessException; method @NonNull public String[] getCameraIdList() throws android.hardware.camera2.CameraAccessException; method @NonNull public java.util.Set<java.util.Set<java.lang.String>> getConcurrentCameraIds() throws android.hardware.camera2.CameraAccessException; method public int getTorchStrengthLevel(@NonNull String) throws android.hardware.camera2.CameraAccessException; + method @FlaggedApi("com.android.internal.camera.flags.camera_device_setup") public boolean isCameraDeviceSetupSupported(@NonNull String) throws android.hardware.camera2.CameraAccessException; method @RequiresPermission(android.Manifest.permission.CAMERA) public boolean isConcurrentSessionConfigurationSupported(@NonNull java.util.Map<java.lang.String,android.hardware.camera2.params.SessionConfiguration>) throws android.hardware.camera2.CameraAccessException; - method @FlaggedApi("com.android.internal.camera.flags.feature_combination_query") @RequiresPermission(android.Manifest.permission.CAMERA) public boolean isSessionConfigurationWithParametersSupported(@NonNull String, @NonNull android.hardware.camera2.params.SessionConfiguration) throws android.hardware.camera2.CameraAccessException; method @RequiresPermission(android.Manifest.permission.CAMERA) public void openCamera(@NonNull String, @NonNull android.hardware.camera2.CameraDevice.StateCallback, @Nullable android.os.Handler) throws android.hardware.camera2.CameraAccessException; method @RequiresPermission(android.Manifest.permission.CAMERA) public void openCamera(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException; method public void registerAvailabilityCallback(@NonNull android.hardware.camera2.CameraManager.AvailabilityCallback, @Nullable android.os.Handler); @@ -25885,8 +25922,11 @@ package android.media.metrics { @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public final class EditingEndedEvent extends android.media.metrics.Event implements android.os.Parcelable { method public int describeContents(); method public int getErrorCode(); + method @Nullable public String getExporterName(); + method public float getFinalProgressPercent(); method public int getFinalState(); method @NonNull public java.util.List<android.media.metrics.MediaItemInfo> getInputMediaItemInfos(); + method @Nullable public String getMuxerName(); method public long getOperationTypes(); method @Nullable public android.media.metrics.MediaItemInfo getOutputMediaItemInfo(); method public void writeToParcel(@NonNull android.os.Parcel, int); @@ -25921,6 +25961,7 @@ package android.media.metrics { 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 PROGRESS_PERCENT_UNKNOWN = -1; // 0xffffffff field public static final int TIME_SINCE_CREATED_UNKNOWN = -1; // 0xffffffff } @@ -25930,7 +25971,10 @@ package android.media.metrics { 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 setExporterName(@NonNull String); + method @NonNull public android.media.metrics.EditingEndedEvent.Builder setFinalProgressPercent(@FloatRange(from=0, to=100) float); method @NonNull public android.media.metrics.EditingEndedEvent.Builder setMetricsBundle(@NonNull android.os.Bundle); + method @NonNull public android.media.metrics.EditingEndedEvent.Builder setMuxerName(@NonNull String); 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); } @@ -33844,6 +33888,7 @@ package android.os { field public static final String DISALLOW_AIRPLANE_MODE = "no_airplane_mode"; field public static final String DISALLOW_AMBIENT_DISPLAY = "no_ambient_display"; field public static final String DISALLOW_APPS_CONTROL = "no_control_apps"; + field @FlaggedApi("android.app.admin.flags.assist_content_user_restriction_enabled") public static final String DISALLOW_ASSIST_CONTENT = "no_assist_content"; field public static final String DISALLOW_AUTOFILL = "no_autofill"; field public static final String DISALLOW_BLUETOOTH = "no_bluetooth"; field public static final String DISALLOW_BLUETOOTH_SHARING = "no_bluetooth_sharing"; @@ -40274,6 +40319,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(); @@ -46515,8 +46575,8 @@ package android.telephony.euicc { public class EuiccManager { method @NonNull public android.telephony.euicc.EuiccManager createForCardId(int); - method @RequiresPermission("android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS") public void deleteSubscription(int, android.app.PendingIntent); - method @RequiresPermission("android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS") public void downloadSubscription(android.telephony.euicc.DownloadableSubscription, boolean, android.app.PendingIntent); + method @RequiresPermission(anyOf={"android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS", android.Manifest.permission.MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS}) public void deleteSubscription(int, android.app.PendingIntent); + method @RequiresPermission(anyOf={"android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS", android.Manifest.permission.MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS}) public void downloadSubscription(android.telephony.euicc.DownloadableSubscription, boolean, android.app.PendingIntent); method @FlaggedApi("com.android.internal.telephony.flags.esim_available_memory") @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_STATE, "android.permission.READ_PRIVILEGED_PHONE_STATE", "carrier privileges"}) public long getAvailableMemoryInBytes(); method @Nullable public String getEid(); method @Nullable public android.telephony.euicc.EuiccInfo getEuiccInfo(); @@ -54206,8 +54266,10 @@ package android.view { method public void setSystemBarsAppearance(int, int); method public void setSystemBarsBehavior(int); method public void show(int); + field @FlaggedApi("android.view.flags.customizable_window_headers") public static final int APPEARANCE_LIGHT_CAPTION_BARS = 256; // 0x100 field public static final int APPEARANCE_LIGHT_NAVIGATION_BARS = 16; // 0x10 field public static final int APPEARANCE_LIGHT_STATUS_BARS = 8; // 0x8 + field @FlaggedApi("android.view.flags.customizable_window_headers") public static final int APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND = 128; // 0x80 field public static final int BEHAVIOR_DEFAULT = 1; // 0x1 field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1 field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0 @@ -55674,6 +55736,14 @@ package android.view.inputmethod { field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.CompletionInfo> CREATOR; } + @FlaggedApi("android.view.inputmethod.connectionless_handwriting") public interface ConnectionlessHandwritingCallback { + method public void onError(int); + method public void onResult(@NonNull CharSequence); + field public static final int CONNECTIONLESS_HANDWRITING_ERROR_NO_TEXT_RECOGNIZED = 0; // 0x0 + field public static final int CONNECTIONLESS_HANDWRITING_ERROR_OTHER = 2; // 0x2 + field public static final int CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED = 1; // 0x1 + } + public final class CorrectionInfo implements android.os.Parcelable { ctor public CorrectionInfo(int, CharSequence, CharSequence); method public int describeContents(); @@ -56085,6 +56155,7 @@ package android.view.inputmethod { method public android.graphics.drawable.Drawable loadIcon(android.content.pm.PackageManager); method public CharSequence loadLabel(android.content.pm.PackageManager); method public boolean shouldShowInInputMethodPicker(); + method @FlaggedApi("android.view.inputmethod.connectionless_handwriting") public boolean supportsConnectionlessStylusHandwriting(); method public boolean supportsStylusHandwriting(); method public boolean suppressesSpellChecker(); method public void writeToParcel(android.os.Parcel, int); @@ -56114,6 +56185,7 @@ package android.view.inputmethod { method public boolean isAcceptingText(); method public boolean isActive(android.view.View); method public boolean isActive(); + method @FlaggedApi("android.view.inputmethod.connectionless_handwriting") public boolean isConnectionlessStylusHandwritingAvailable(); method public boolean isFullscreenMode(); method public boolean isInputMethodSuppressingSpellChecker(); method public boolean isStylusHandwritingAvailable(); @@ -56134,6 +56206,9 @@ package android.view.inputmethod { method public boolean showSoftInput(android.view.View, int, android.os.ResultReceiver); method @Deprecated public void showSoftInputFromInputMethod(android.os.IBinder, int); method @Deprecated public void showStatusIcon(android.os.IBinder, String, @DrawableRes int); + method @FlaggedApi("android.view.inputmethod.connectionless_handwriting") public void startConnectionlessStylusHandwriting(@NonNull android.view.View, @Nullable android.view.inputmethod.CursorAnchorInfo, @NonNull java.util.concurrent.Executor, @NonNull android.view.inputmethod.ConnectionlessHandwritingCallback); + method @FlaggedApi("android.view.inputmethod.connectionless_handwriting") public void startConnectionlessStylusHandwritingForDelegation(@NonNull android.view.View, @Nullable android.view.inputmethod.CursorAnchorInfo, @NonNull java.util.concurrent.Executor, @NonNull android.view.inputmethod.ConnectionlessHandwritingCallback); + method @FlaggedApi("android.view.inputmethod.connectionless_handwriting") public void startConnectionlessStylusHandwritingForDelegation(@NonNull android.view.View, @Nullable android.view.inputmethod.CursorAnchorInfo, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.view.inputmethod.ConnectionlessHandwritingCallback); method public void startStylusHandwriting(@NonNull android.view.View); method @Deprecated public boolean switchToLastInputMethod(android.os.IBinder); method @Deprecated public boolean switchToNextInputMethod(android.os.IBinder, boolean); diff --git a/core/api/lint-baseline.txt b/core/api/lint-baseline.txt index e901f00d5f5f..b36b963f99c9 100644 --- a/core/api/lint-baseline.txt +++ b/core/api/lint-baseline.txt @@ -1093,6 +1093,66 @@ Todo: android.provider.ContactsContract.RawContacts#newEntityIterator(android.da Documentation mentions 'TODO' +UnflaggedApi: android.R.color#on_surface_disabled_material: + New API must be flagged with @FlaggedApi: field android.R.color.on_surface_disabled_material +UnflaggedApi: android.R.color#outline_disabled_material: + New API must be flagged with @FlaggedApi: field android.R.color.outline_disabled_material +UnflaggedApi: android.R.color#surface_disabled_material: + New API must be flagged with @FlaggedApi: field android.R.color.surface_disabled_material +UnflaggedApi: android.R.color#system_error_0: + New API must be flagged with @FlaggedApi: field android.R.color.system_error_0 +UnflaggedApi: android.R.color#system_error_10: + New API must be flagged with @FlaggedApi: field android.R.color.system_error_10 +UnflaggedApi: android.R.color#system_error_100: + New API must be flagged with @FlaggedApi: field android.R.color.system_error_100 +UnflaggedApi: android.R.color#system_error_1000: + New API must be flagged with @FlaggedApi: field android.R.color.system_error_1000 +UnflaggedApi: android.R.color#system_error_200: + New API must be flagged with @FlaggedApi: field android.R.color.system_error_200 +UnflaggedApi: android.R.color#system_error_300: + New API must be flagged with @FlaggedApi: field android.R.color.system_error_300 +UnflaggedApi: android.R.color#system_error_400: + New API must be flagged with @FlaggedApi: field android.R.color.system_error_400 +UnflaggedApi: android.R.color#system_error_50: + New API must be flagged with @FlaggedApi: field android.R.color.system_error_50 +UnflaggedApi: android.R.color#system_error_500: + New API must be flagged with @FlaggedApi: field android.R.color.system_error_500 +UnflaggedApi: android.R.color#system_error_600: + New API must be flagged with @FlaggedApi: field android.R.color.system_error_600 +UnflaggedApi: android.R.color#system_error_700: + New API must be flagged with @FlaggedApi: field android.R.color.system_error_700 +UnflaggedApi: android.R.color#system_error_800: + New API must be flagged with @FlaggedApi: field android.R.color.system_error_800 +UnflaggedApi: android.R.color#system_error_900: + New API must be flagged with @FlaggedApi: field android.R.color.system_error_900 +UnflaggedApi: android.R.color#system_on_surface_disabled: + New API must be flagged with @FlaggedApi: field android.R.color.system_on_surface_disabled +UnflaggedApi: android.R.color#system_on_surface_disabled_dark: + New API must be flagged with @FlaggedApi: field android.R.color.system_on_surface_disabled_dark +UnflaggedApi: android.R.color#system_on_surface_disabled_light: + New API must be flagged with @FlaggedApi: field android.R.color.system_on_surface_disabled_light +UnflaggedApi: android.R.color#system_outline_disabled: + New API must be flagged with @FlaggedApi: field android.R.color.system_outline_disabled +UnflaggedApi: android.R.color#system_outline_disabled_dark: + New API must be flagged with @FlaggedApi: field android.R.color.system_outline_disabled_dark +UnflaggedApi: android.R.color#system_outline_disabled_light: + New API must be flagged with @FlaggedApi: field android.R.color.system_outline_disabled_light +UnflaggedApi: android.R.color#system_surface_disabled: + New API must be flagged with @FlaggedApi: field android.R.color.system_surface_disabled +UnflaggedApi: android.R.color#system_surface_disabled_dark: + New API must be flagged with @FlaggedApi: field android.R.color.system_surface_disabled_dark +UnflaggedApi: android.R.color#system_surface_disabled_light: + New API must be flagged with @FlaggedApi: field android.R.color.system_surface_disabled_light +UnflaggedApi: android.R.dimen#system_corner_radius_large: + New API must be flagged with @FlaggedApi: field android.R.dimen.system_corner_radius_large +UnflaggedApi: android.R.dimen#system_corner_radius_medium: + New API must be flagged with @FlaggedApi: field android.R.dimen.system_corner_radius_medium +UnflaggedApi: android.R.dimen#system_corner_radius_small: + New API must be flagged with @FlaggedApi: field android.R.dimen.system_corner_radius_small +UnflaggedApi: android.R.dimen#system_corner_radius_xlarge: + New API must be flagged with @FlaggedApi: field android.R.dimen.system_corner_radius_xlarge +UnflaggedApi: android.R.dimen#system_corner_radius_xsmall: + New API must be flagged with @FlaggedApi: field android.R.dimen.system_corner_radius_xsmall UnflaggedApi: android.accessibilityservice.AccessibilityService#OVERLAY_RESULT_INTERNAL_ERROR: New API must be flagged with @FlaggedApi: field android.accessibilityservice.AccessibilityService.OVERLAY_RESULT_INTERNAL_ERROR UnflaggedApi: android.accessibilityservice.AccessibilityService#OVERLAY_RESULT_INVALID: diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index 1273da71b748..7ba78356af72 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -102,6 +102,7 @@ package android.content { method @NonNull public android.os.UserHandle getUser(); field public static final String PAC_PROXY_SERVICE = "pac_proxy"; field public static final String TEST_NETWORK_SERVICE = "test_network"; + field @FlaggedApi("android.webkit.update_service_ipc_wrapper") public static final String WEBVIEW_UPDATE_SERVICE = "webviewupdate"; } public class Intent implements java.lang.Cloneable android.os.Parcelable { @@ -412,6 +413,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); @@ -637,3 +651,34 @@ package android.view.accessibility { } +package android.webkit { + + @FlaggedApi("android.webkit.update_service_ipc_wrapper") public class WebViewBootstrapFrameworkInitializer { + method public static void registerServiceWrappers(); + } + + @FlaggedApi("android.webkit.update_service_ipc_wrapper") public final class WebViewProviderResponse implements android.os.Parcelable { + ctor public WebViewProviderResponse(@Nullable android.content.pm.PackageInfo, int); + method public int describeContents(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.webkit.WebViewProviderResponse> CREATOR; + field public static final int STATUS_FAILED_LISTING_WEBVIEW_PACKAGES = 4; // 0x4 + field public static final int STATUS_FAILED_WAITING_FOR_RELRO = 3; // 0x3 + field public static final int STATUS_SUCCESS = 0; // 0x0 + field @Nullable public final android.content.pm.PackageInfo packageInfo; + field public final int status; + } + + @FlaggedApi("android.webkit.update_service_ipc_wrapper") public final class WebViewUpdateManager { + method @Nullable @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public String changeProviderAndSetting(@NonNull String); + method @NonNull public android.webkit.WebViewProviderInfo[] getAllWebViewPackages(); + method @Nullable public android.content.pm.PackageInfo getCurrentWebViewPackage(); + method @Nullable public String getCurrentWebViewPackageName(); + method @FlaggedApi("android.webkit.update_service_v2") @NonNull public android.webkit.WebViewProviderInfo getDefaultWebViewPackage(); + method @Nullable public static android.webkit.WebViewUpdateManager getInstance(); + method @NonNull public android.webkit.WebViewProviderInfo[] getValidWebViewPackages(); + method @NonNull public android.webkit.WebViewProviderResponse waitForAndGetProvider(); + } + +} + diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 3d01ef656d8a..9e09931e5ba7 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -1040,13 +1040,20 @@ package android.app { method @Nullable public android.content.ComponentName getAllowedNotificationAssistant(); method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_NOTIFICATION_LISTENERS) public java.util.List<android.content.ComponentName> getEnabledNotificationListeners(); method public boolean isNotificationAssistantAccessGranted(@NonNull android.content.ComponentName); + method @FlaggedApi("android.service.notification.callstyle_callback_api") @RequiresPermission(allOf={android.Manifest.permission.INTERACT_ACROSS_USERS, android.Manifest.permission.ACCESS_NOTIFICATIONS}) public void registerCallNotificationEventListener(@NonNull String, @NonNull android.os.UserHandle, @NonNull java.util.concurrent.Executor, @NonNull android.app.NotificationManager.CallNotificationEventListener); method public void setNotificationAssistantAccessGranted(@Nullable android.content.ComponentName, boolean); method @RequiresPermission(android.Manifest.permission.MANAGE_NOTIFICATION_LISTENERS) public void setNotificationListenerAccessGranted(@NonNull android.content.ComponentName, boolean, boolean); + method @FlaggedApi("android.service.notification.callstyle_callback_api") @RequiresPermission(allOf={android.Manifest.permission.INTERACT_ACROSS_USERS, android.Manifest.permission.ACCESS_NOTIFICATIONS}) public void unregisterCallNotificationEventListener(@NonNull android.app.NotificationManager.CallNotificationEventListener); field @RequiresPermission(android.Manifest.permission.STATUS_BAR_SERVICE) public static final String ACTION_CLOSE_NOTIFICATION_HANDLER_PANEL = "android.app.action.CLOSE_NOTIFICATION_HANDLER_PANEL"; field @RequiresPermission(android.Manifest.permission.STATUS_BAR_SERVICE) public static final String ACTION_OPEN_NOTIFICATION_HANDLER_PANEL = "android.app.action.OPEN_NOTIFICATION_HANDLER_PANEL"; field @RequiresPermission(android.Manifest.permission.STATUS_BAR_SERVICE) public static final String ACTION_TOGGLE_NOTIFICATION_HANDLER_PANEL = "android.app.action.TOGGLE_NOTIFICATION_HANDLER_PANEL"; } + @FlaggedApi("android.service.notification.callstyle_callback_api") public static interface NotificationManager.CallNotificationEventListener { + method @FlaggedApi("android.service.notification.callstyle_callback_api") public void onCallNotificationPosted(@NonNull String, @NonNull android.os.UserHandle); + method @FlaggedApi("android.service.notification.callstyle_callback_api") public void onCallNotificationRemoved(@NonNull String, @NonNull android.os.UserHandle); + } + public final class RemoteLockscreenValidationResult implements android.os.Parcelable { method public int describeContents(); method public int getResultCode(); @@ -6644,6 +6651,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); @@ -13403,6 +13411,23 @@ package android.service.voice { field public static final int ERROR_CODE_UNKNOWN = 0; // 0x0 } + @FlaggedApi("android.service.voice.flags.allow_various_attention_types") public final class VisualQueryAttentionResult implements android.os.Parcelable { + method public int describeContents(); + method @IntRange(from=1, to=100) public int getEngagementLevel(); + method public int getInteractionIntention(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.service.voice.VisualQueryAttentionResult> CREATOR; + field public static final int INTERACTION_INTENTION_AUDIO_VISUAL = 0; // 0x0 + field public static final int INTERACTION_INTENTION_VISUAL_ACCESSIBILITY = 1; // 0x1 + } + + public static final class VisualQueryAttentionResult.Builder { + ctor public VisualQueryAttentionResult.Builder(); + method @NonNull public android.service.voice.VisualQueryAttentionResult build(); + method @NonNull public android.service.voice.VisualQueryAttentionResult.Builder setEngagementLevel(@IntRange(from=1, to=100) int); + method @NonNull public android.service.voice.VisualQueryAttentionResult.Builder setInteractionIntention(int); + } + @FlaggedApi("android.service.voice.flags.allow_complex_results_egress_from_vqds") public final class VisualQueryDetectedResult implements android.os.Parcelable { method public int describeContents(); method public static int getMaxSpeakerId(); @@ -13423,7 +13448,9 @@ package android.service.voice { ctor public VisualQueryDetectionService(); method public final void finishQuery() throws java.lang.IllegalStateException; method public final void gainedAttention(); + method @FlaggedApi("android.service.voice.flags.allow_various_attention_types") public final void gainedAttention(@NonNull android.service.voice.VisualQueryAttentionResult); method public final void lostAttention(); + method @FlaggedApi("android.service.voice.flags.allow_various_attention_types") public final void lostAttention(int); method @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent); method public void onStartDetection(); method public void onStopDetection(); @@ -13865,6 +13892,7 @@ package android.telecom { field public static final int CAPABILITY_EMERGENCY_VIDEO_CALLING = 512; // 0x200 field public static final int CAPABILITY_MULTI_USER = 32; // 0x20 field public static final String EXTRA_PLAY_CALL_RECORDING_TONE = "android.telecom.extra.PLAY_CALL_RECORDING_TONE"; + field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String EXTRA_SKIP_CALL_FILTERING = "android.telecom.extra.SKIP_CALL_FILTERING"; field public static final String EXTRA_SORT_ORDER = "android.telecom.extra.SORT_ORDER"; } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index dd79c4a40cba..f8a6af1a5c60 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -921,7 +921,33 @@ package android.companion { package android.companion.virtual { public final class VirtualDeviceManager { + method public int getAudioPlaybackSessionId(int); + method public int getAudioRecordingSessionId(int); + method public int getDeviceIdForDisplayId(int); + method public int getDevicePolicy(int, int); method @FlaggedApi("android.companion.virtual.flags.interactive_screen_mirror") public boolean isVirtualDeviceOwnedMirrorDisplay(int); + method public void playSoundEffect(int, int); + } + +} + +package android.companion.virtual.camera { + + @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCamera implements java.io.Closeable { + method @NonNull public String getId(); + } + +} + +package android.companion.virtual.sensor { + + public final class VirtualSensor implements android.os.Parcelable { + ctor public VirtualSensor(int, int, @NonNull String); + method public int getHandle(); + } + + public final class VirtualSensorConfig implements android.os.Parcelable { + method public int getFlags(); } } @@ -988,6 +1014,7 @@ package android.content { method public void setAutofillOptions(@Nullable android.content.AutofillOptions); method public void setContentCaptureOptions(@Nullable android.content.ContentCaptureOptions); method public void updateDeviceId(int); + method public abstract void updateDisplay(int); field public static final String ATTENTION_SERVICE = "attention"; field public static final String CONTENT_CAPTURE_MANAGER_SERVICE = "content_capture"; field public static final String DEVICE_IDLE_CONTROLLER = "deviceidle"; @@ -1001,6 +1028,7 @@ package android.content { public class ContextWrapper extends android.content.Context { method public int getDisplayId(); + method public void updateDisplay(int); } public class Intent implements java.lang.Cloneable android.os.Parcelable { @@ -2707,17 +2735,17 @@ package android.os.vibrator { package android.os.vibrator.persistence { - @FlaggedApi("android.os.vibrator.enable_vibration_serialization_apis") public class ParsedVibration { + public class ParsedVibration { method @NonNull public java.util.List<android.os.VibrationEffect> getVibrationEffects(); method @Nullable public android.os.VibrationEffect resolve(@NonNull android.os.Vibrator); } - @FlaggedApi("android.os.vibrator.enable_vibration_serialization_apis") public final class VibrationXmlParser { + public final class VibrationXmlParser { method @Nullable public static android.os.vibrator.persistence.ParsedVibration parseDocument(@NonNull java.io.Reader) throws java.io.IOException; method @Nullable public static android.os.VibrationEffect parseVibrationEffect(@NonNull java.io.Reader) throws java.io.IOException; } - @FlaggedApi("android.os.vibrator.enable_vibration_serialization_apis") public final class VibrationXmlSerializer { + public final class VibrationXmlSerializer { method public static void serialize(@NonNull android.os.VibrationEffect, @NonNull java.io.Writer) throws java.io.IOException, android.os.vibrator.persistence.VibrationXmlSerializer.SerializationFailedException; } @@ -3056,6 +3084,14 @@ package android.service.notification { method @Deprecated public boolean isBound(); } + @FlaggedApi("android.app.modes_api") public final class ZenDeviceEffects implements android.os.Parcelable { + method @NonNull public java.util.Set<java.lang.String> getExtraEffects(); + } + + @FlaggedApi("android.app.modes_api") public static final class ZenDeviceEffects.Builder { + method @NonNull public android.service.notification.ZenDeviceEffects.Builder setExtraEffects(@NonNull java.util.Set<java.lang.String>); + } + public final class ZenPolicy implements android.os.Parcelable { method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy overwrittenWith(@Nullable android.service.notification.ZenPolicy); } @@ -3537,6 +3573,7 @@ package android.view { public final class Display { method @RequiresPermission(android.Manifest.permission.MODIFY_USER_PREFERRED_DISPLAY_MODE) public void clearUserPreferredDisplayMode(); method @NonNull public android.view.Display.Mode getDefaultMode(); + method public int getRemoveMode(); method @NonNull public int[] getReportedHdrTypes(); method @NonNull public android.graphics.ColorSpace[] getSupportedWideColorGamut(); method @Nullable public android.view.Display.Mode getSystemPreferredDisplayMode(); @@ -3901,7 +3938,7 @@ package android.view.inputmethod { } public final class InputMethodInfo implements android.os.Parcelable { - ctor public InputMethodInfo(@NonNull String, @NonNull String, @NonNull CharSequence, @NonNull String, @NonNull String, boolean, @NonNull String); + ctor @FlaggedApi("android.view.inputmethod.connectionless_handwriting") public InputMethodInfo(@NonNull String, @NonNull String, @NonNull CharSequence, @NonNull String, @NonNull String, boolean, boolean, @NonNull String); ctor public InputMethodInfo(@NonNull String, @NonNull String, @NonNull CharSequence, @NonNull String, int); field public static final int COMPONENT_NAME_MAX_LENGTH = 1000; // 0x3e8 field public static final int MAX_IMES_PER_PACKAGE = 20; // 0x14 diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index de6a8487d8b1..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; @@ -8588,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/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java index d57a4e583a1a..f6373d690793 100644 --- a/core/java/android/app/AutomaticZenRule.java +++ b/core/java/android/app/AutomaticZenRule.java @@ -487,6 +487,9 @@ public final class AutomaticZenRule implements Parcelable { public void validate() { if (Flags.modesApi()) { checkValidType(mType); + if (mDeviceEffects != null) { + mDeviceEffects.validate(); + } } } diff --git a/core/java/android/app/ICallNotificationEventCallback.aidl b/core/java/android/app/ICallNotificationEventCallback.aidl new file mode 100644 index 000000000000..ba348294b462 --- /dev/null +++ b/core/java/android/app/ICallNotificationEventCallback.aidl @@ -0,0 +1,29 @@ +/* + * 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.app; + +import android.os.UserHandle; + +/** + * Callback to be called when a call notification is posted or removed + * + * @hide + */ +oneway interface ICallNotificationEventCallback { + void onCallNotificationPosted(String packageName, in UserHandle userHandle); + void onCallNotificationRemoved(String packageName, in UserHandle userHandle); +} diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 578105f9f99e..b5e355638ae8 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -24,6 +24,7 @@ import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationHistory; import android.app.NotificationManager; +import android.app.ICallNotificationEventCallback; import android.content.AttributionSource; import android.content.ComponentName; import android.content.Intent; @@ -247,4 +248,10 @@ interface INotificationManager @EnforcePermission("MANAGE_TOAST_RATE_LIMITING") void setToastRateLimitingEnabled(boolean enable); + + @EnforcePermission(allOf={"INTERACT_ACROSS_USERS", "ACCESS_NOTIFICATIONS"}) + void registerCallNotificationEventListener(String packageName, in UserHandle userHandle, in ICallNotificationEventCallback listener); + @EnforcePermission(allOf={"INTERACT_ACROSS_USERS", "ACCESS_NOTIFICATIONS"}) + void unregisterCallNotificationEventListener(String packageName, in UserHandle userHandle, in ICallNotificationEventCallback listener); + } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index aa9de814b4c5..d6e8ae3e5dff 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -3020,6 +3020,43 @@ public class Notification implements Parcelable } /** + * @hide + */ + public String loadHeaderAppName(Context context) { + CharSequence name = null; + // Check if there is a non-empty substitute app name and return that. + if (extras.containsKey(EXTRA_SUBSTITUTE_APP_NAME)) { + name = extras.getString(EXTRA_SUBSTITUTE_APP_NAME); + if (!TextUtils.isEmpty(name)) { + return name.toString(); + } + } + // If not, try getting the app info from extras. + if (context == null) { + return null; + } + final PackageManager pm = context.getPackageManager(); + if (TextUtils.isEmpty(name)) { + if (extras.containsKey(EXTRA_BUILDER_APPLICATION_INFO)) { + final ApplicationInfo info = extras.getParcelable(EXTRA_BUILDER_APPLICATION_INFO, + ApplicationInfo.class); + if (info != null) { + name = pm.getApplicationLabel(info); + } + } + } + // If that's still empty, use the one from the context directly. + if (TextUtils.isEmpty(name)) { + name = pm.getApplicationLabel(context.getApplicationInfo()); + } + // If there's still nothing, ¯\_(ツ)_/¯ + if (TextUtils.isEmpty(name)) { + return null; + } + return name.toString(); + } + + /** * Removes heavyweight parts of the Notification object for archival or for sending to * listeners when the full contents are not necessary. * @hide @@ -5769,34 +5806,7 @@ public class Notification implements Parcelable */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public String loadHeaderAppName() { - CharSequence name = null; - final PackageManager pm = mContext.getPackageManager(); - if (mN.extras.containsKey(EXTRA_SUBSTITUTE_APP_NAME)) { - // only system packages which lump together a bunch of unrelated stuff - // may substitute a different name to make the purpose of the - // notification more clear. the correct package label should always - // be accessible via SystemUI. - final String pkg = mContext.getPackageName(); - final String subName = mN.extras.getString(EXTRA_SUBSTITUTE_APP_NAME); - if (PackageManager.PERMISSION_GRANTED == pm.checkPermission( - android.Manifest.permission.SUBSTITUTE_NOTIFICATION_APP_NAME, pkg)) { - name = subName; - } else { - Log.w(TAG, "warning: pkg " - + pkg + " attempting to substitute app name '" + subName - + "' without holding perm " - + android.Manifest.permission.SUBSTITUTE_NOTIFICATION_APP_NAME); - } - } - if (TextUtils.isEmpty(name)) { - name = pm.getApplicationLabel(mContext.getApplicationInfo()); - } - if (TextUtils.isEmpty(name)) { - // still nothing? - return null; - } - - return String.valueOf(name); + return mN.loadHeaderAppName(mContext); } /** diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 8c886feedd0c..9dfb5b0dedbd 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -16,6 +16,7 @@ package android.app; +import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; @@ -69,6 +70,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.Executor; /** * Class to notify the user of events that happen. This is how you tell @@ -627,6 +629,9 @@ public class NotificationManager { */ public static int MAX_SERVICE_COMPONENT_NAME_LENGTH = 500; + private final Map<CallNotificationEventListener, CallNotificationEventCallbackStub> + mCallNotificationEventCallbacks = new HashMap<>(); + @UnsupportedAppUsage private static INotificationManager sService; @@ -2848,4 +2853,126 @@ public class NotificationManager { default: return defValue; } } + + /** + * Callback to receive updates when a call notification has been posted or removed + * @hide + */ + @SystemApi + @FlaggedApi(android.service.notification.Flags.FLAG_CALLSTYLE_CALLBACK_API) + public interface CallNotificationEventListener { + /** + * Called when a call notification was posted by a package this listener + * has registered for. + * @param packageName package name of the app that posted the removed notification + */ + @FlaggedApi(android.service.notification.Flags.FLAG_CALLSTYLE_CALLBACK_API) + void onCallNotificationPosted(@NonNull String packageName, @NonNull UserHandle userHandle); + + /** + * Called when a call notification was removed by a package this listener + * has registered for. + * @param packageName package name of the app that removed notification + */ + @FlaggedApi(android.service.notification.Flags.FLAG_CALLSTYLE_CALLBACK_API) + void onCallNotificationRemoved(@NonNull String packageName, @NonNull UserHandle userHandle); + } + + private static class CallNotificationEventCallbackStub extends + ICallNotificationEventCallback.Stub { + final String mPackageName; + final UserHandle mUserHandle; + final Executor mExecutor; + final CallNotificationEventListener mListener; + + CallNotificationEventCallbackStub(@NonNull String packageName, + @NonNull UserHandle userHandle, @NonNull @CallbackExecutor Executor executor, + @NonNull CallNotificationEventListener listener) { + mPackageName = packageName; + mUserHandle = userHandle; + mExecutor = executor; + mListener = listener; + } + + @FlaggedApi(android.service.notification.Flags.FLAG_CALLSTYLE_CALLBACK_API) + @Override + public void onCallNotificationPosted(String packageName, UserHandle userHandle) { + mExecutor.execute(() -> mListener.onCallNotificationPosted(packageName, userHandle)); + } + + @FlaggedApi(android.service.notification.Flags.FLAG_CALLSTYLE_CALLBACK_API) + @Override + public void onCallNotificationRemoved(String packageName, UserHandle userHandle) { + mExecutor.execute(() -> mListener.onCallNotificationRemoved(packageName, userHandle)); + } + } + + /** + * Register a listener to be notified when a call notification is posted or removed + * for a specific package and user. + * + * @param packageName Which package to monitor + * @param userHandle Which user to monitor + * @param executor Callback will run on this executor + * @param listener Listener to register + * @hide + */ + @SystemApi + @RequiresPermission(allOf = { + android.Manifest.permission.INTERACT_ACROSS_USERS, + android.Manifest.permission.ACCESS_NOTIFICATIONS}) + @FlaggedApi(android.service.notification.Flags.FLAG_CALLSTYLE_CALLBACK_API) + public void registerCallNotificationEventListener(@NonNull String packageName, + @NonNull UserHandle userHandle, @NonNull @CallbackExecutor Executor executor, + @NonNull CallNotificationEventListener listener) { + checkRequired("packageName", packageName); + checkRequired("userHandle", userHandle); + checkRequired("executor", executor); + checkRequired("listener", listener); + INotificationManager service = getService(); + try { + synchronized (mCallNotificationEventCallbacks) { + CallNotificationEventCallbackStub callbackStub = + new CallNotificationEventCallbackStub(packageName, userHandle, + executor, listener); + mCallNotificationEventCallbacks.put(listener, callbackStub); + + service.registerCallNotificationEventListener(packageName, userHandle, + callbackStub); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Unregister a listener that was previously + * registered with {@link #registerCallNotificationEventListener} + * + * @param listener Listener to unregister + * @hide + */ + @SystemApi + @FlaggedApi(android.service.notification.Flags.FLAG_CALLSTYLE_CALLBACK_API) + @RequiresPermission(allOf = { + android.Manifest.permission.INTERACT_ACROSS_USERS, + android.Manifest.permission.ACCESS_NOTIFICATIONS}) + public void unregisterCallNotificationEventListener( + @NonNull CallNotificationEventListener listener) { + checkRequired("listener", listener); + INotificationManager service = getService(); + try { + synchronized (mCallNotificationEventCallbacks) { + CallNotificationEventCallbackStub callbackStub = + mCallNotificationEventCallbacks.remove(listener); + if (callbackStub != null) { + service.unregisterCallNotificationEventListener(callbackStub.mPackageName, + callbackStub.mUserHandle, callbackStub); + } + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java index 625526047212..24a51573b48a 100644 --- a/core/java/android/app/ResourcesManager.java +++ b/core/java/android/app/ResourcesManager.java @@ -550,7 +550,7 @@ public class ResourcesManager { @UnsupportedAppUsage protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key, @Nullable ApkAssetsSupplier apkSupplier) { - final AssetManager.Builder builder = new AssetManager.Builder().setNoInit(); + final AssetManager.Builder builder = new AssetManager.Builder(); final ArrayList<ApkKey> apkKeys = extractApkKeys(key); for (int i = 0, n = apkKeys.size(); i < n; i++) { @@ -1555,7 +1555,7 @@ public class ResourcesManager { } else if(overlayPaths == null) { return ArrayUtils.cloneOrNull(resourceDirs); } else { - final var paths = new ArrayList<String>(overlayPaths.length + resourceDirs.length); + final ArrayList<String> paths = new ArrayList<>(); for (final String path : overlayPaths) { paths.add(path); } diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index ba9c8952892b..08c193f06286 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; @@ -250,6 +251,7 @@ import android.view.textservice.TextServicesManager; import android.view.translation.ITranslationManager; import android.view.translation.TranslationManager; import android.view.translation.UiTranslationManager; +import android.webkit.WebViewBootstrapFrameworkInitializer; import com.android.internal.R; import com.android.internal.app.IAppOpsService; @@ -1659,9 +1661,17 @@ public final class SystemServiceRegistry { OnDevicePersonalizationFrameworkInitializer.registerServiceWrappers(); DeviceLockFrameworkInitializer.registerServiceWrappers(); VirtualizationFrameworkInitializer.registerServiceWrappers(); + // This code is executed on zygote during preload, where only read-only + // flags can be used. Do not use mutable flags. if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()) { EnhancedConfirmationFrameworkInitializer.registerServiceWrappers(); } + if (android.server.Flags.telemetryApisService()) { + ProfilingFrameworkInitializer.registerServiceWrappers(); + } + if (android.webkit.Flags.updateServiceIpcWrapper()) { + WebViewBootstrapFrameworkInitializer.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/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index c649e622dfc5..9d50810425c6 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -51,6 +51,7 @@ import static android.Manifest.permission.QUERY_ADMIN_POLICY; import static android.Manifest.permission.REQUEST_PASSWORD_COMPLEXITY; import static android.Manifest.permission.SET_TIME; import static android.Manifest.permission.SET_TIME_ZONE; +import static android.app.admin.flags.Flags.FLAG_ESIM_MANAGEMENT_ENABLED; import static android.app.admin.flags.Flags.onboardingBugreportV2Enabled; import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM; import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1; @@ -17300,4 +17301,33 @@ public class DevicePolicyManager { public boolean isOnboardingBugreportV2FlagEnabled() { return onboardingBugreportV2Enabled(); } + + /** + * Returns the subscription ids of all subscriptions which was downloaded by the calling + * admin. + * + * <p> This returns only the subscriptions which were downloaded by the calling admin via + * {@link android.telephony.euicc.EuiccManager#downloadSubscription}. + * If a susbcription is returned by this method then in it subject to management controls + * and cannot be removed by users. + * + * <p> Callable by device owners and profile owners. + * + * @throws SecurityException if the caller is not authorized to call this method + * @return ids of all managed subscriptions currently downloaded by an admin on the device + */ + @FlaggedApi(FLAG_ESIM_MANAGEMENT_ENABLED) + @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS) + @NonNull + public Set<Integer> getSubscriptionsIds() { + throwIfParentInstance("getSubscriptionsIds"); + if (mService != null) { + try { + return intArrayToSet(mService.getSubscriptionIds(mContext.getPackageName())); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return new HashSet<>(); + } }
\ No newline at end of file diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index efcf5633515d..f72fdc069db5 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -613,4 +613,6 @@ interface IDevicePolicyManager { void setContentProtectionPolicy(in ComponentName who, String callerPackageName, int policy); int getContentProtectionPolicy(in ComponentName who, String callerPackageName); + + int[] getSubscriptionIds(String callerPackageName); } diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 3c98ef9cfb00..30cd1b72fd49 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -36,6 +36,48 @@ flag { } flag { + name: "dedicated_device_control_api_enabled" + namespace: "enterprise" + description: "(API) Allow the device management role holder to control which platform features are available on dedicated devices." + bug: "281964214" +} + +flag { + name: "permission_migration_for_zero_trust_api_enabled" + namespace: "enterprise" + description: "(API) Migrate existing APIs to permission based, and enable DMRH to call them to collect Zero Trust signals." + bug: "289520697" +} + +flag { + name: "permission_migration_for_zero_trust_impl_enabled" + namespace: "enterprise" + description: "(Implementation) Migrate existing APIs to permission based, and enable DMRH to call them to collect Zero Trust signals." + bug: "289520697" +} + +flag { + name: "device_theft_api_enabled" + namespace: "enterprise" + description: "Add new API for theft detection." + bug: "325073410" +} + +flag { + name: "device_theft_impl_enabled" + namespace: "enterprise" + description: "Implementing new API for theft detection." + bug: "325073410" +} + +flag { + name: "coexistence_migration_for_non_emm_management_enabled" + namespace: "enterprise" + description: "Migrate existing APIs to be coexistable, and enable DMRH to call them to support non-EMM device management." + bug: "289520697" +} + +flag { name: "security_log_v2_enabled" namespace: "enterprise" description: "Improve access to security logging in the context of Zero Trust." @@ -57,6 +99,13 @@ flag { } flag { + name: "assist_content_user_restriction_enabled" + namespace: "enterprise" + description: "Prevent work data leakage by sending assist content to privileged apps." + bug: "322975406" +} + +flag { name: "default_sms_personal_app_suspension_fix_enabled" namespace: "enterprise" description: "Exempt the default sms app of the context user for suspension when calling setPersonalAppsSuspended" diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index a16e94a534a9..3304475df89f 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -26,6 +26,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; +import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; @@ -333,6 +334,8 @@ public final class VirtualDeviceManager { * * @hide */ + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. + @TestApi public @VirtualDeviceParams.DevicePolicy int getDevicePolicy( int deviceId, @VirtualDeviceParams.PolicyType int policyType) { if (mService == null) { @@ -351,6 +354,8 @@ public final class VirtualDeviceManager { * * @hide */ + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. + @TestApi public int getDeviceIdForDisplayId(int displayId) { if (mService == null) { Log.w(TAG, "Failed to retrieve virtual devices; no virtual device manager service."); @@ -446,6 +451,8 @@ public final class VirtualDeviceManager { * * @hide */ + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. + @TestApi public int getAudioPlaybackSessionId(int deviceId) { if (mService == null) { return AUDIO_SESSION_ID_GENERATE; @@ -470,6 +477,8 @@ public final class VirtualDeviceManager { * * @hide */ + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. + @TestApi public int getAudioRecordingSessionId(int deviceId) { if (mService == null) { return AUDIO_SESSION_ID_GENERATE; @@ -491,6 +500,8 @@ public final class VirtualDeviceManager { * * @hide */ + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. + @TestApi public void playSoundEffect(int deviceId, @AudioManager.SystemSoundEffect int effectType) { if (mService == null) { Log.w(TAG, "Failed to dispatch sound effect; no virtual device manager service."); diff --git a/core/java/android/companion/virtual/camera/VirtualCamera.java b/core/java/android/companion/virtual/camera/VirtualCamera.java index 9d6c14b5b9a8..f7275894961c 100644 --- a/core/java/android/companion/virtual/camera/VirtualCamera.java +++ b/core/java/android/companion/virtual/camera/VirtualCamera.java @@ -18,7 +18,9 @@ package android.companion.virtual.camera; import android.annotation.FlaggedApi; import android.annotation.RequiresPermission; +import android.annotation.SuppressLint; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.companion.virtual.IVirtualDevice; import android.companion.virtual.VirtualDeviceManager; import android.companion.virtual.VirtualDeviceParams; @@ -84,6 +86,8 @@ public final class VirtualCamera implements Closeable { * Returns the id of this virtual camera instance. * @hide */ + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. + @TestApi @NonNull public String getId() { return mCameraId; diff --git a/core/java/android/companion/virtual/sensor/VirtualSensor.java b/core/java/android/companion/virtual/sensor/VirtualSensor.java index 14c799763a96..37e494bd8efe 100644 --- a/core/java/android/companion/virtual/sensor/VirtualSensor.java +++ b/core/java/android/companion/virtual/sensor/VirtualSensor.java @@ -18,7 +18,9 @@ package android.companion.virtual.sensor; import android.annotation.NonNull; import android.annotation.RequiresPermission; +import android.annotation.SuppressLint; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.companion.virtual.IVirtualDevice; import android.hardware.Sensor; import android.os.IBinder; @@ -54,6 +56,15 @@ public final class VirtualSensor implements Parcelable { mToken = token; } + /** + * @hide + */ + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. + @TestApi + public VirtualSensor(int handle, int type, @NonNull String name) { + this(handle, type, name, /*virtualDevice=*/null, /*token=*/null); + } + private VirtualSensor(Parcel parcel) { mHandle = parcel.readInt(); mType = parcel.readInt(); @@ -67,6 +78,8 @@ public final class VirtualSensor implements Parcelable { * * @hide */ + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. + @TestApi public int getHandle() { return mHandle; } diff --git a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java index 0dbe411a400e..21ad914bbc29 100644 --- a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java +++ b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java @@ -20,7 +20,9 @@ package android.companion.virtual.sensor; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.hardware.Sensor; import android.hardware.SensorDirectChannel; import android.os.Parcel; @@ -217,6 +219,8 @@ public final class VirtualSensorConfig implements Parcelable { * * @hide */ + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. + @TestApi public int getFlags() { return mFlags; } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index b8d754348211..9e192a06d404 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -6553,6 +6553,28 @@ 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"; + + /** + * Use with {@link #getSystemService(String)} to retrieve a {@link + * android.webkit.WebViewUpdateManager} for accessing the WebView update service. + * + * @see #getSystemService(String) + * @see android.webkit.WebViewUpdateManager + * @hide + */ + @FlaggedApi(android.webkit.Flags.FLAG_UPDATE_SERVICE_IPC_WRAPPER) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @SuppressLint("ServiceName") + public static final String WEBVIEW_UPDATE_SERVICE = "webviewupdate"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * @@ -7706,9 +7728,13 @@ public abstract class Context { } /** + * Updates the display association of this Context with the display with the given ID. + * * @hide */ @SuppressWarnings("HiddenAbstractMethod") + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. + @TestApi public abstract void updateDisplay(int displayId); /** diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 46232fb4b607..e8031a374310 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -56,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; @@ -73,6 +74,7 @@ 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; @@ -6066,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}. * diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java index d91358105b0b..e2907224d66e 100644 --- a/core/java/android/content/IntentFilter.java +++ b/core/java/android/content/IntentFilter.java @@ -677,7 +677,7 @@ public class IntentFilter implements Parcelable { * has at least one HTTP or HTTPS data URI pattern defined, and optionally * does not define any non-http/https data URI patterns. * - * This will check if if the Intent action is {@link android.content.Intent#ACTION_VIEW} and + * This will check if the Intent action is {@link android.content.Intent#ACTION_VIEW} and * the Intent category is {@link android.content.Intent#CATEGORY_BROWSABLE} and the Intent * data scheme is "http" or "https". * @@ -718,7 +718,7 @@ public class IntentFilter implements Parcelable { } // We get here if: - // 1) onlyWebSchemes and no non-web schemes were found, i.e success; or + // 1) onlyWebSchemes and no non-web schemes were found, i.e. success; or // 2) !onlyWebSchemes and no http/https schemes were found, i.e. failure. return onlyWebSchemes; } @@ -728,7 +728,7 @@ public class IntentFilter implements Parcelable { * * @return True if the filter needs to be automatically verified. False otherwise. * - * This will check if if the Intent action is {@link android.content.Intent#ACTION_VIEW} and + * This will check if the Intent action is {@link android.content.Intent#ACTION_VIEW} and * the Intent category is {@link android.content.Intent#CATEGORY_BROWSABLE} and the Intent * data scheme is "http" or "https". * diff --git a/core/java/android/content/pm/OWNERS b/core/java/android/content/pm/OWNERS index fb9560889507..c6f52202834b 100644 --- a/core/java/android/content/pm/OWNERS +++ b/core/java/android/content/pm/OWNERS @@ -3,11 +3,22 @@ file:/PACKAGE_MANAGER_OWNERS per-file PackageParser.java = set noparent -per-file PackageParser.java = chiuwinson@google.com,patb@google.com +per-file PackageParser.java = file:/PACKAGE_MANAGER_OWNERS + +# Bug component: 166829 = per-file *Capability* per-file *Capability* = file:/core/java/android/content/pm/SHORTCUT_OWNERS +# Bug component: 166829 = per-file *Shortcut* per-file *Shortcut* = file:/core/java/android/content/pm/SHORTCUT_OWNERS + +# Bug component: 860423 = per-file *Launcher* per-file *Launcher* = file:/core/java/android/content/pm/LAUNCHER_OWNERS + +# Bug component: 578329 = per-file *UserInfo* per-file UserInfo* = file:/MULTIUSER_OWNERS +# Bug component: 578329 = per-file *UserProperties* per-file *UserProperties* = file:/MULTIUSER_OWNERS +# Bug component: 578329 = per-file *multiuser* per-file *multiuser* = file:/MULTIUSER_OWNERS -per-file IBackgroundInstallControlService.aidl = file:/services/core/java/com/android/server/pm/BACKGROUND_INSTALL_OWNERS + +# Bug component: 1219020 = per-file *BackgroundInstallControl* +per-file *BackgroundInstallControl* = file:/services/core/java/com/android/server/pm/BACKGROUND_INSTALL_OWNERS 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/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 4b890faf527e..c7d93bfbb83d 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -135,3 +135,10 @@ flag { description: "Allow the use of a profileApiAvailability user property to exclude HIDDEN profiles in API results" bug: "316362775" } + +flag { + name: "enable_launcher_apps_hidden_profile_checks" + namespace: "profile_experiences" + description: "Enable extra check to limit access to hidden prfiles data in Launcher apps APIs." + bug: "321988638" +} diff --git a/core/java/android/content/pm/overlay/OverlayPaths.java b/core/java/android/content/pm/overlay/OverlayPaths.java index a4db733af013..bd74b0b9293c 100644 --- a/core/java/android/content/pm/overlay/OverlayPaths.java +++ b/core/java/android/content/pm/overlay/OverlayPaths.java @@ -49,6 +49,13 @@ public class OverlayPaths { public static class Builder { final OverlayPaths mPaths = new OverlayPaths(); + public Builder() {} + + public Builder(@NonNull OverlayPaths base) { + mPaths.mResourceDirs.addAll(base.getResourceDirs()); + mPaths.mOverlayPaths.addAll(base.getOverlayPaths()); + } + /** * Adds a non-APK path to the contents of {@link OverlayPaths#getOverlayPaths()}. */ diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index d259e9755a41..23b9d0b7c9a7 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -137,8 +137,6 @@ public final class AssetManager implements AutoCloseable { private ArrayList<ApkAssets> mUserApkAssets = new ArrayList<>(); private ArrayList<ResourcesLoader> mLoaders = new ArrayList<>(); - private boolean mNoInit = false; - public Builder addApkAssets(ApkAssets apkAssets) { mUserApkAssets.add(apkAssets); return this; @@ -149,11 +147,6 @@ public final class AssetManager implements AutoCloseable { return this; } - public Builder setNoInit() { - mNoInit = true; - return this; - } - public AssetManager build() { // Retrieving the system ApkAssets forces their creation as well. final ApkAssets[] systemApkAssets = getSystem().getApkAssets(); @@ -195,7 +188,7 @@ public final class AssetManager implements AutoCloseable { final AssetManager assetManager = new AssetManager(false /*sentinel*/); assetManager.mApkAssets = apkAssets; AssetManager.nativeSetApkAssets(assetManager.mObject, apkAssets, - false /*invalidateCaches*/, mNoInit /*preset*/); + false /*invalidateCaches*/); assetManager.mLoaders = mLoaders.isEmpty() ? null : mLoaders.toArray(new ResourcesLoader[0]); @@ -336,7 +329,7 @@ public final class AssetManager implements AutoCloseable { synchronized (this) { ensureOpenLocked(); mApkAssets = newApkAssets; - nativeSetApkAssets(mObject, mApkAssets, invalidateCaches, false); + nativeSetApkAssets(mObject, mApkAssets, invalidateCaches); if (invalidateCaches) { // Invalidate all caches. invalidateCachesLocked(-1); @@ -503,7 +496,7 @@ public final class AssetManager implements AutoCloseable { mApkAssets = Arrays.copyOf(mApkAssets, count + 1); mApkAssets[count] = assets; - nativeSetApkAssets(mObject, mApkAssets, true, false); + nativeSetApkAssets(mObject, mApkAssets, true); invalidateCachesLocked(-1); return count + 1; } @@ -1510,29 +1503,12 @@ public final class AssetManager implements AutoCloseable { int navigation, int screenWidth, int screenHeight, int smallestScreenWidthDp, int screenWidthDp, int screenHeightDp, int screenLayout, int uiMode, int colorMode, int grammaticalGender, int majorVersion) { - setConfigurationInternal(mcc, mnc, defaultLocale, locales, orientation, - touchscreen, density, keyboard, keyboardHidden, navigation, screenWidth, - screenHeight, smallestScreenWidthDp, screenWidthDp, screenHeightDp, - screenLayout, uiMode, colorMode, grammaticalGender, majorVersion, false); - } - - /** - * Change the configuration used when retrieving resources, and potentially force a refresh of - * the state. Not for use by applications. - * @hide - */ - void setConfigurationInternal(int mcc, int mnc, String defaultLocale, String[] locales, - int orientation, int touchscreen, int density, int keyboard, int keyboardHidden, - int navigation, int screenWidth, int screenHeight, int smallestScreenWidthDp, - int screenWidthDp, int screenHeightDp, int screenLayout, int uiMode, int colorMode, - int grammaticalGender, int majorVersion, boolean forceRefresh) { synchronized (this) { ensureValidLocked(); nativeSetConfiguration(mObject, mcc, mnc, defaultLocale, locales, orientation, touchscreen, density, keyboard, keyboardHidden, navigation, screenWidth, screenHeight, smallestScreenWidthDp, screenWidthDp, screenHeightDp, - screenLayout, uiMode, colorMode, grammaticalGender, majorVersion, - forceRefresh); + screenLayout, uiMode, colorMode, grammaticalGender, majorVersion); } } @@ -1617,13 +1593,13 @@ public final class AssetManager implements AutoCloseable { private static native long nativeCreate(); private static native void nativeDestroy(long ptr); private static native void nativeSetApkAssets(long ptr, @NonNull ApkAssets[] apkAssets, - boolean invalidateCaches, boolean preset); + boolean invalidateCaches); private static native void nativeSetConfiguration(long ptr, int mcc, int mnc, @Nullable String defaultLocale, @NonNull String[] locales, int orientation, int touchscreen, int density, int keyboard, int keyboardHidden, int navigation, int screenWidth, int screenHeight, int smallestScreenWidthDp, int screenWidthDp, int screenHeightDp, int screenLayout, int uiMode, int colorMode, int grammaticalGender, - int majorVersion, boolean forceRefresh); + int majorVersion); private static native @NonNull SparseArray<String> nativeGetAssignedPackageIdentifiers( long ptr, boolean includeOverlays, boolean includeLoaders); diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java index 079c2c1ab7c9..5e442b819774 100644 --- a/core/java/android/content/res/ResourcesImpl.java +++ b/core/java/android/content/res/ResourcesImpl.java @@ -200,7 +200,7 @@ public class ResourcesImpl { mMetrics.setToDefaults(); mDisplayAdjustments = displayAdjustments; mConfiguration.setToDefaults(); - updateConfigurationImpl(config, metrics, displayAdjustments.getCompatibilityInfo(), true); + updateConfiguration(config, metrics, displayAdjustments.getCompatibilityInfo()); } public DisplayAdjustments getDisplayAdjustments() { @@ -402,12 +402,7 @@ public class ResourcesImpl { } public void updateConfiguration(Configuration config, DisplayMetrics metrics, - CompatibilityInfo compat) { - updateConfigurationImpl(config, metrics, compat, false); - } - - private void updateConfigurationImpl(Configuration config, DisplayMetrics metrics, - CompatibilityInfo compat, boolean forceAssetsRefresh) { + CompatibilityInfo compat) { Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesImpl#updateConfiguration"); try { synchronized (mAccessLock) { @@ -533,7 +528,7 @@ public class ResourcesImpl { keyboardHidden = mConfiguration.keyboardHidden; } - mAssets.setConfigurationInternal(mConfiguration.mcc, mConfiguration.mnc, + mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc, defaultLocale, selectedLocales, mConfiguration.orientation, @@ -544,7 +539,7 @@ public class ResourcesImpl { mConfiguration.screenWidthDp, mConfiguration.screenHeightDp, mConfiguration.screenLayout, mConfiguration.uiMode, mConfiguration.colorMode, mConfiguration.getGrammaticalGender(), - Build.VERSION.RESOURCES_SDK_INT, forceAssetsRefresh); + Build.VERSION.RESOURCES_SDK_INT); if (DEBUG_CONFIG) { Slog.i(TAG, "**** Updating config of " + this + ": final config is " diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java index 3835c5201946..1d14169f8ade 100644 --- a/core/java/android/hardware/camera2/CameraDevice.java +++ b/core/java/android/hardware/camera2/CameraDevice.java @@ -16,10 +16,12 @@ package android.hardware.camera2; +import android.annotation.CallbackExecutor; 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.camera2.params.ExtensionSessionConfiguration; import android.hardware.camera2.params.InputConfiguration; @@ -35,6 +37,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; import java.util.Set; +import java.util.concurrent.Executor; /** * <p>The CameraDevice class is a representation of a single camera connected to an @@ -897,7 +900,7 @@ public abstract class CameraDevice implements AutoCloseable { * supported sizes. * Camera clients that register a Jpeg/R output within a stream combination that doesn't fit * in the mandatory stream table above can call - * {@link CameraManager#isSessionConfigurationWithParametersSupported} to ensure that this particular + * {@link #isSessionConfigurationSupported} to ensure that this particular * configuration is supported.</p> * * <h5>STREAM_USE_CASE capability additional guaranteed configurations</h5> @@ -970,7 +973,7 @@ public abstract class CameraDevice implements AutoCloseable { * * <p>Since the capabilities of camera devices vary greatly, a given camera device may support * target combinations with sizes outside of these guarantees, but this can only be tested for - * by calling {@link CameraManager#isSessionConfigurationWithParametersSupported} or attempting + * by calling {@link #isSessionConfigurationSupported} or attempting * to create a session with such targets.</p> * * <p>Exception on 176x144 (QCIF) resolution: @@ -1395,8 +1398,12 @@ public abstract class CameraDevice implements AutoCloseable { * {@link android.hardware.camera2.params.MandatoryStreamCombination} are better suited for this * purpose.</p> * - * <p>Note that session parameters will be ignored and calls to - * {@link SessionConfiguration#setSessionParameters} are not required.</p> + * <p><b>NOTE:</b> + * For apps targeting {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} and above, + * this method will ensure session parameters set through calls to + * {@link SessionConfiguration#setSessionParameters} are also supported if the Camera Device + * supports it. For apps targeting {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and + * below, session parameters will be ignored.</p> * * @return {@code true} if the given session configuration is supported by the camera device * {@code false} otherwise. @@ -1406,10 +1413,8 @@ public abstract class CameraDevice implements AutoCloseable { * @throws CameraAccessException if the camera device is no longer connected or has * encountered a fatal error * @throws IllegalStateException if the camera device has been closed - * @deprecated Please use {@link CameraManager#isSessionConfigurationWithParametersSupported} - * to check whether a SessionConfiguration is supported by the device. + * */ - @Deprecated public boolean isSessionConfigurationSupported( @NonNull SessionConfiguration sessionConfig) throws CameraAccessException { throw new UnsupportedOperationException("Subclasses must override this method"); @@ -1627,6 +1632,155 @@ public abstract class CameraDevice implements AutoCloseable { } /** + * CameraDeviceSetup is a limited representation of {@link CameraDevice} that can be used to + * query device specific information which would otherwise need a CameraDevice instance. + * This class can be constructed without calling {@link CameraManager#openCamera} and paying + * the latency cost of CameraDevice creation. Use {@link CameraManager#getCameraDeviceSetup} + * to get an instance of this class. + * + * <p>Can only be instantiated for camera devices for which + * {@link CameraManager#isCameraDeviceSetupSupported} returns true.</p> + * + * @see CameraManager#isCameraDeviceSetupSupported(String) + * @see CameraManager#getCameraDeviceSetup(String) + */ + @FlaggedApi(Flags.FLAG_CAMERA_DEVICE_SETUP) + public abstract static class CameraDeviceSetup { + /** + * Create a {@link CaptureRequest.Builder} for new capture requests, + * initialized with a template for target use case. + * + * <p>The settings are chosen to be the best options for the specific camera device, + * so it is not recommended to reuse the same request for a different camera device; + * create a builder specific for that device and template and override the + * settings as desired, instead.</p> + * + * <p>Supported if {@link CameraCharacteristics#INFO_SESSION_CONFIGURATION_QUERY_VERSION} + * is at least {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}. If less or equal to + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, this function throws an + * {@link UnsupportedOperationException}.</p> + * + * @param templateType An enumeration selecting the use case for this request. Not all + * template types are supported on every device. See the documentation + * for each template type for details. + * + * @return a builder for a capture request, initialized with default settings for that + * template, and no output streams + * + * @throws CameraAccessException if the querying the camera device failed or there has been + * a fatal error + * @throws IllegalArgumentException if the templateType is not supported by this device + */ + @NonNull + @FlaggedApi(Flags.FLAG_CAMERA_DEVICE_SETUP) + public abstract CaptureRequest.Builder createCaptureRequest( + @RequestTemplate int templateType) throws CameraAccessException; + + /** + * Checks whether a particular {@link SessionConfiguration} is supported by the camera + * device. + * + * <p>This method performs a runtime check of a given {@link SessionConfiguration}. The + * result confirms whether or not the {@code SessionConfiguration}, <b>including the + * parameters specified via {@link SessionConfiguration#setSessionParameters}</b>, can + * be used to create a camera capture session using + * {@link CameraDevice#createCaptureSession(SessionConfiguration)}.</p> + * + * <p>This method is supported if the + * {@link CameraCharacteristics#INFO_SESSION_CONFIGURATION_QUERY_VERSION} + * is at least {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}. If less or equal + * to {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, this function throws + * {@link UnsupportedOperationException}.</p> + * + * <p>Although this method is much faster than creating a new capture session, it can still + * take a few milliseconds per call. Applications should therefore not use this method to + * explore the entire space of supported session combinations.</p> + * + * <p>Instead, applications should use this method to query whether combinations of + * certain features are supported. {@link + * CameraCharacteristics#INFO_SESSION_CONFIGURATION_QUERY_VERSION} provides the list of + * feature combinations the camera device will reliably report.</p> + * + * <p><b>IMPORTANT:</b></p> + * <ul> + * <li>If a feature support can be queried via + * {@link CameraCharacteristics#SCALER_MANDATORY_STREAM_COMBINATIONS} or + * {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP}, applications should + * directly use it rather than calling this function as: (1) using + * {@code CameraCharacteristics} is more efficient, and (2) calling this function with on + * non-supported devices will throw a {@link UnsupportedOperationException}. + * + * <li>To minimize latency of {@link SessionConfiguration} creation, applications can + * use deferred surfaces for SurfaceView and SurfaceTexture to avoid waiting for UI + * creation before setting up the camera. For {@link android.media.MediaRecorder} and + * {@link android.media.MediaCodec} uses, applications can use {@code ImageReader} with + * {@link android.hardware.HardwareBuffer#USAGE_VIDEO_ENCODE}. The lightweight nature of + * {@code ImageReader} helps minimize the latency cost. + * </ul> + * + * @return {@code true} if the given session configuration is supported by the camera + * device, {@code false} otherwise. + * + * @throws CameraAccessException if the camera device is no longer connected or has + * encountered a fatal error + * @throws IllegalArgumentException if the session configuration is invalid + * + * @see CameraCharacteristics#INFO_SESSION_CONFIGURATION_QUERY_VERSION + * @see SessionConfiguration + * @see android.media.ImageReader + */ + @FlaggedApi(Flags.FLAG_CAMERA_DEVICE_SETUP) + public abstract boolean isSessionConfigurationSupported( + @NonNull SessionConfiguration config) throws CameraAccessException; + + /** + * Utility function to forward the call to + * {@link CameraManager#openCamera(String, Executor, StateCallback)}. This function simply + * calls {@code CameraManager.openCamera} for the cameraId for which this class was + * constructed. All semantics are consistent with {@code CameraManager.openCamera}. + * + * @param executor The executor which will be used when invoking the callback. + * @param callback The callback which is invoked once the camera is opened + * + * @throws CameraAccessException if the camera is disabled by device policy, + * has been disconnected, or is being used by a higher-priority camera API client. + * + * @throws IllegalArgumentException if cameraId, the callback or the executor was null, + * or the cameraId does not match any currently or previously available + * camera device. + * + * @throws SecurityException if the application does not have permission to + * access the camera + * + * @see CameraManager#openCamera(String, Executor, StateCallback) + */ + @FlaggedApi(Flags.FLAG_CAMERA_DEVICE_SETUP) + @RequiresPermission(android.Manifest.permission.CAMERA) + public abstract void openCamera(@NonNull @CallbackExecutor Executor executor, + @NonNull StateCallback callback) throws CameraAccessException; + + /** + * Get the ID of this camera device. + * + * <p>This matches the ID given to {@link CameraManager#getCameraDeviceSetup} to instantiate + * this object.</p> + * + * @return the ID for this camera device + * + * @see CameraManager#getCameraIdList + */ + @NonNull + @FlaggedApi(Flags.FLAG_CAMERA_DEVICE_SETUP) + public abstract String getId(); + + /** + * To be implemented by camera2 classes only. + * @hide + */ + public CameraDeviceSetup() {} + } + + /** * Set audio restriction mode when this CameraDevice is being used. * * <p>Some camera hardware (e.g. devices with optical image stabilization support) diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index bcce4b65be18..83b68a50d208 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -36,8 +36,9 @@ import android.hardware.CameraIdRemapping; import android.hardware.CameraStatus; import android.hardware.ICameraService; import android.hardware.ICameraServiceListener; -import android.hardware.camera2.CameraDevice.RequestTemplate; +import android.hardware.camera2.CameraDevice.StateCallback; import android.hardware.camera2.impl.CameraDeviceImpl; +import android.hardware.camera2.impl.CameraDeviceSetupImpl; import android.hardware.camera2.impl.CameraInjectionSessionImpl; import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.params.ExtensionSessionConfiguration; @@ -45,10 +46,10 @@ import android.hardware.camera2.params.SessionConfiguration; import android.hardware.camera2.params.StreamConfiguration; import android.hardware.camera2.utils.CameraIdAndSessionConfiguration; import android.hardware.camera2.utils.ConcurrentCameraIdCombination; +import android.hardware.camera2.utils.ExceptionUtils; import android.hardware.devicestate.DeviceStateManager; import android.hardware.display.DisplayManager; import android.os.Binder; -import android.os.DeadObjectException; import android.os.Handler; import android.os.HandlerExecutor; import android.os.HandlerThread; @@ -352,71 +353,6 @@ public final class CameraManager { } /** - * Checks whether a particular {@link SessionConfiguration} is supported by a camera device. - * - * <p>This method performs a runtime check of a given {@link SessionConfiguration}. The result - * confirms whether or not the session configuration, including the - * {@link SessionConfiguration#setSessionParameters specified session parameters}, can - * be successfully used to create a camera capture session using - * {@link CameraDevice#createCaptureSession( - * android.hardware.camera2.params.SessionConfiguration)}. - * </p> - * - * <p>Supported if the {@link CameraCharacteristics#INFO_SESSION_CONFIGURATION_QUERY_VERSION} - * is at least {@code android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM}. If less or equal to - * {@code android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE}, this function throws - * {@code UnsupportedOperationException}.</p> - * - * <p>Although this method is much faster than creating a new capture session, it is not - * trivial cost: the latency is less than 5 milliseconds in most cases. As a result, the - * app should not use this to explore the entire space of supported session combinations.</p> - * - * <p>Instead, the application should use this method to query whether the - * combination of certain features are supported. See {@link - * CameraCharacteristics#INFO_SESSION_CONFIGURATION_QUERY_VERSION} for the list of feature - * combinations the camera device will reliably report.</p> - * - * <p>IMPORTANT:</p> - * - * <ul> - * - * <li>If a feature support can be queried with {@code CameraCharacteristics}, - * the application must directly use {@code CameraCharacteristics} rather than - * calling this function. The reasons are: (1) using {@code CameraCharacteristics} is more - * efficient, and (2) calling this function with a non-supported feature will throw a {@code - * IllegalArgumentException}.</li> - * - * <li>To minimize latency for {@code SessionConfiguration} creation, the application should - * use deferred surfaces for SurfaceView and SurfaceTexture to avoid delays. Alternatively, - * the application can create {@code ImageReader} with {@code USAGE_COMPOSER_OVERLAY} and - * {@code USAGE_GPU_SAMPLED_IMAGE} usage respectively. For {@code MediaRecorder} and {@code - * MediaCodec}, the application can use {@code ImageReader} with {@code - * USAGE_VIDEO_ENCODE}. The lightweight nature of {@code ImageReader} helps minimize the - * latency cost.</li> - * - * </ul> - * - * - * @return {@code true} if the given session configuration is supported by the camera device - * {@code false} otherwise. - * @throws CameraAccessException if the camera device is no longer connected or has - * encountered a fatal error - * @throws IllegalArgumentException if the session configuration is invalid - * @throws UnsupportedOperationException if the query operation is not supported by the camera - * device - * - * @see CameraCharacteristics#INFO_SESSION_CONFIGURATION_QUERY_VERSION - */ - @RequiresPermission(android.Manifest.permission.CAMERA) - @FlaggedApi(Flags.FLAG_FEATURE_COMBINATION_QUERY) - public boolean isSessionConfigurationWithParametersSupported(@NonNull String cameraId, - @NonNull SessionConfiguration sessionConfig) throws CameraAccessException { - //TODO: b/298033056: restructure the OutputConfiguration API for better usability - return CameraManagerGlobal.get().isSessionConfigurationWithParametersSupported( - cameraId, sessionConfig); - } - - /** * Register a callback to be notified about camera device availability. * * <p>Registering the same callback again will replace the handler with the @@ -643,7 +579,7 @@ public final class CameraManager { ServiceSpecificException sse = new ServiceSpecificException( ICameraService.ERROR_DISCONNECTED, "Camera service is currently unavailable"); - throwAsPublicException(sse); + throw ExceptionUtils.throwAsPublicException(sse); } return multiResolutionStreamConfigurations; @@ -736,7 +672,7 @@ public final class CameraManager { characteristics = new CameraCharacteristics(info); } catch (ServiceSpecificException e) { - throwAsPublicException(e); + throw ExceptionUtils.throwAsPublicException(e); } catch (RemoteException e) { // Camera service died - act as if the camera was disconnected throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, @@ -785,6 +721,110 @@ public final class CameraManager { } /** + * Returns a {@link CameraDevice.CameraDeviceSetup} object for the given {@code cameraId}, + * which provides limited access to CameraDevice setup and query functionality without + * requiring an {@link #openCamera} call. The {@link CameraDevice} can later be obtained either + * by calling {@link #openCamera}, or {@link CameraDevice.CameraDeviceSetup#openCamera}. + * + * <p>Support for {@link CameraDevice.CameraDeviceSetup} for a given {@code cameraId} must be + * checked with {@link #isCameraDeviceSetupSupported}. If {@code isCameraDeviceSetupSupported} + * returns {@code false} for a {@code cameraId}, this method will throw an + * {@link UnsupportedOperationException}</p> + * + * @param cameraId The unique identifier of the camera device for which + * {@link CameraDevice.CameraDeviceSetup} object must be constructed. This + * identifier must be present in {@link #getCameraIdList()} + * + * @return {@link CameraDevice.CameraDeviceSetup} object corresponding to the provided + * {@code cameraId} + * + * @throws IllegalArgumentException If {@code cameraId} is null, or if {@code cameraId} does not + * match any device in {@link #getCameraIdList()}. + * @throws CameraAccessException if the camera device is not accessible + * @throws UnsupportedOperationException if {@link CameraDevice.CameraDeviceSetup} instance + * cannot be constructed for the given {@code cameraId}, i.e. + * {@link #isCameraDeviceSetupSupported} returns false. + * + * @see CameraDevice.CameraDeviceSetup + * @see #getCameraIdList() + * @see #openCamera + */ + @NonNull + @FlaggedApi(Flags.FLAG_CAMERA_DEVICE_SETUP) + public CameraDevice.CameraDeviceSetup getCameraDeviceSetup(@NonNull String cameraId) + throws CameraAccessException { + if (cameraId == null) { + throw new IllegalArgumentException("cameraId was null"); + } + + if (CameraManagerGlobal.sCameraServiceDisabled) { + throw new CameraAccessException(CameraAccessException.CAMERA_DISABLED, + "No cameras available on device"); + } + + if (!Arrays.asList(CameraManagerGlobal.get().getCameraIdList()).contains(cameraId)) { + throw new IllegalArgumentException( + "Camera ID '" + cameraId + "' not available on device."); + } + + if (!isCameraDeviceSetupSupported(cameraId)) { + throw new UnsupportedOperationException( + "CameraDeviceSetup is not supported for Camera ID: " + cameraId); + } + + return new CameraDeviceSetupImpl(cameraId, /*cameraManager=*/ this, + mContext.getApplicationInfo().targetSdkVersion); + } + + /** + * Checks a Camera Device's characteristics to ensure that a + * {@link CameraDevice.CameraDeviceSetup} instance can be constructed for a given + * {@code cameraId}. If this method returns false for a {@code cameraId}, calling + * {@link #getCameraDeviceSetup} for that {@code cameraId} will throw an + * {@link UnsupportedOperationException}. + * + * <p>{@link CameraDevice.CameraDeviceSetup} is supported for all devices that report + * {@link CameraCharacteristics#INFO_SESSION_CONFIGURATION_QUERY_VERSION} > + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}</p> + * + * @param cameraId The unique identifier of the camera device for which + * {@link CameraDevice.CameraDeviceSetup} support is being queried. This + * identifier must be present in {@link #getCameraIdList()}. + * + * @return {@code true} if {@link CameraDevice.CameraDeviceSetup} object can be constructed + * for the provided {@code cameraId}; {@code false} otherwise. + * + * @throws IllegalArgumentException If {@code cameraId} is null, or if {@code cameraId} does not + * match any device in {@link #getCameraIdList()}. + * @throws CameraAccessException if the camera device is not accessible + * + * @see CameraCharacteristics#INFO_SESSION_CONFIGURATION_QUERY_VERSION + * @see CameraDevice.CameraDeviceSetup + * @see #getCameraDeviceSetup(String) + * @see #getCameraIdList() + */ + @FlaggedApi(Flags.FLAG_CAMERA_DEVICE_SETUP) + public boolean isCameraDeviceSetupSupported(@NonNull String cameraId) + throws CameraAccessException { + if (cameraId == null) { + throw new IllegalArgumentException("Camera ID was null"); + } + + if (CameraManagerGlobal.sCameraServiceDisabled) { + throw new CameraAccessException(CameraAccessException.CAMERA_DISABLED, + "No cameras available on device"); + } + + if (!Arrays.asList(CameraManagerGlobal.get().getCameraIdList()).contains(cameraId)) { + throw new IllegalArgumentException( + "Camera ID '" + cameraId + "' not available on device."); + } + + CameraCharacteristics chars = getCameraCharacteristics(cameraId); + return CameraDeviceSetupImpl.isCameraDeviceSetupSupported(chars); + } + + /** * Helper for opening a connection to a camera with the given ID. * * @param cameraId The unique identifier of the camera device to open @@ -817,6 +857,11 @@ public final class CameraManager { synchronized (mLock) { ICameraDeviceUser cameraUser = null; + CameraDevice.CameraDeviceSetup cameraDeviceSetup = null; + if (Flags.cameraDeviceSetup() && isCameraDeviceSetupSupported(cameraId)) { + cameraDeviceSetup = getCameraDeviceSetup(cameraId); + } + android.hardware.camera2.impl.CameraDeviceImpl deviceImpl = new android.hardware.camera2.impl.CameraDeviceImpl( cameraId, @@ -825,8 +870,7 @@ public final class CameraManager { characteristics, physicalIdsToChars, mContext.getApplicationInfo().targetSdkVersion, - mContext); - + mContext, cameraDeviceSetup); ICameraDeviceCallbacks callbacks = deviceImpl.getCallbacks(); try { @@ -858,11 +902,11 @@ public final class CameraManager { e.errorCode == ICameraService.ERROR_DISCONNECTED || e.errorCode == ICameraService.ERROR_CAMERA_IN_USE) { // Per API docs, these failures call onError and throw - throwAsPublicException(e); + throw ExceptionUtils.throwAsPublicException(e); } } else { // Unexpected failure - rethrow - throwAsPublicException(e); + throw ExceptionUtils.throwAsPublicException(e); } } catch (RemoteException e) { // Camera service died - act as if it's a CAMERA_DISCONNECTED case @@ -870,7 +914,7 @@ public final class CameraManager { ICameraService.ERROR_DISCONNECTED, "Camera service is currently unavailable"); deviceImpl.setRemoteFailure(sse); - throwAsPublicException(sse); + throw ExceptionUtils.throwAsPublicException(sse); } // TODO: factor out callback to be non-nested, then move setter to constructor @@ -1310,48 +1354,6 @@ public final class CameraManager { } /** - * Create a {@link CaptureRequest.Builder} for new capture requests, - * initialized with template for a target use case. - * - * <p>The settings are chosen to be the best options for the specific camera device, - * so it is not recommended to reuse the same request for a different camera device; - * create a builder specific for that device and template and override the - * settings as desired, instead.</p> - * - * <p>Supported if the {@link CameraCharacteristics#INFO_SESSION_CONFIGURATION_QUERY_VERSION} - * is at least {@code android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM}. If less or equal to - * {@code android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE}, this function throws a - * {@code UnsupportedOperationException}. - * - * @param cameraId The camera ID to create capture request for. - * @param templateType An enumeration selecting the use case for this request. Not all template - * types are supported on every device. See the documentation for each template type for - * details. - * @return a builder for a capture request, initialized with default - * settings for that template, and no output streams - * - * @throws CameraAccessException if the camera device is no longer connected or has - * encountered a fatal error - * @throws IllegalArgumentException if the cameraId is not valid, or the templateType is - * not supported by this device. - * @throws UnsupportedOperationException if this method is not supported by the camera device, - * for example, if {@link CameraCharacteristics#INFO_SESSION_CONFIGURATION_QUERY_VERSION} - * is less than {@code android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM}. - */ - @NonNull - @RequiresPermission(android.Manifest.permission.CAMERA) - @FlaggedApi(Flags.FLAG_FEATURE_COMBINATION_QUERY) - public CaptureRequest.Builder createCaptureRequest(@NonNull String cameraId, - @RequestTemplate int templateType) throws CameraAccessException { - if (CameraManagerGlobal.sCameraServiceDisabled) { - throw new IllegalArgumentException("No camera available on device."); - } - - return CameraManagerGlobal.get().createCaptureRequest(cameraId, templateType, - mContext.getApplicationInfo().targetSdkVersion); - } - - /** * @hide */ public static boolean shouldOverrideToPortrait(@Nullable Context context) { @@ -1705,56 +1707,6 @@ public final class CameraManager { } /** - * Convert ServiceSpecificExceptions and Binder RemoteExceptions from camera binder interfaces - * into the correct public exceptions. - * - * @hide - */ - public static void throwAsPublicException(Throwable t) throws CameraAccessException { - if (t instanceof ServiceSpecificException) { - ServiceSpecificException e = (ServiceSpecificException) t; - int reason = CameraAccessException.CAMERA_ERROR; - switch(e.errorCode) { - case ICameraService.ERROR_DISCONNECTED: - reason = CameraAccessException.CAMERA_DISCONNECTED; - break; - case ICameraService.ERROR_DISABLED: - reason = CameraAccessException.CAMERA_DISABLED; - break; - case ICameraService.ERROR_CAMERA_IN_USE: - reason = CameraAccessException.CAMERA_IN_USE; - break; - case ICameraService.ERROR_MAX_CAMERAS_IN_USE: - reason = CameraAccessException.MAX_CAMERAS_IN_USE; - break; - case ICameraService.ERROR_DEPRECATED_HAL: - reason = CameraAccessException.CAMERA_DEPRECATED_HAL; - break; - case ICameraService.ERROR_ILLEGAL_ARGUMENT: - case ICameraService.ERROR_ALREADY_EXISTS: - throw new IllegalArgumentException(e.getMessage(), e); - case ICameraService.ERROR_PERMISSION_DENIED: - throw new SecurityException(e.getMessage(), e); - case ICameraService.ERROR_TIMED_OUT: - case ICameraService.ERROR_INVALID_OPERATION: - default: - reason = CameraAccessException.CAMERA_ERROR; - } - throw new CameraAccessException(reason, e.getMessage(), e); - } else if (t instanceof DeadObjectException) { - throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, - "Camera service has died unexpectedly", - t); - } else if (t instanceof RemoteException) { - throw new UnsupportedOperationException("An unknown RemoteException was thrown" + - " which should never happen.", t); - } else if (t instanceof RuntimeException) { - RuntimeException e = (RuntimeException) t; - throw e; - } - } - - /** * Queries the camera service if a cameraId is a hidden physical camera that belongs to a * logical camera device. * @@ -1829,13 +1781,13 @@ public final class CameraManager { internalCamId, externalCamId, cameraInjectionCallback); injectionSessionImpl.setRemoteInjectionSession(injectionSession); } catch (ServiceSpecificException e) { - throwAsPublicException(e); + throw ExceptionUtils.throwAsPublicException(e); } catch (RemoteException e) { // Camera service died - act as if it's a CAMERA_DISCONNECTED case ServiceSpecificException sse = new ServiceSpecificException( ICameraService.ERROR_DISCONNECTED, "Camera service is currently unavailable"); - throwAsPublicException(sse); + throw ExceptionUtils.throwAsPublicException(sse); } } } @@ -1875,6 +1827,23 @@ public final class CameraManager { } /** + * Returns the current CameraService instance connected to Global + * @hide + */ + public ICameraService getCameraService() { + return CameraManagerGlobal.get().getCameraService(); + } + + /** + * Returns true if cameraservice is currently disabled. If true, {@link #getCameraService()} + * will definitely return null. + * @hide + */ + public boolean isCameraServiceDisabled() { + return CameraManagerGlobal.sCameraServiceDisabled; + } + + /** * Reports {@link CameraExtensionSessionStats} to the {@link ICameraService} to be logged for * currently active session. Validation is done downstream. * @@ -2124,7 +2093,7 @@ public final class CameraManager { cameraService.remapCameraIds(cameraIdRemapping); mActiveCameraIdRemapping = cameraIdRemapping; } catch (ServiceSpecificException e) { - throwAsPublicException(e); + throw ExceptionUtils.throwAsPublicException(e); } catch (RemoteException e) { throw new CameraAccessException( CameraAccessException.CAMERA_DISCONNECTED, @@ -2148,7 +2117,7 @@ public final class CameraManager { try { cameraService.injectSessionParams(cameraId, sessionParams.getNativeMetadata()); } catch (ServiceSpecificException e) { - throwAsPublicException(e); + throw ExceptionUtils.throwAsPublicException(e); } catch (RemoteException e) { throw new CameraAccessException( CameraAccessException.CAMERA_DISCONNECTED, @@ -2391,35 +2360,13 @@ public final class CameraManager { return mCameraService.isConcurrentSessionConfigurationSupported( cameraIdsAndConfigs, targetSdkVersion); } catch (ServiceSpecificException e) { - throwAsPublicException(e); + throw ExceptionUtils.throwAsPublicException(e); } catch (RemoteException e) { // Camera service died - act as if the camera was disconnected throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, "Camera service is currently unavailable", e); } } - - return false; - } - - public boolean isSessionConfigurationWithParametersSupported( - @NonNull String cameraId, @NonNull SessionConfiguration sessionConfiguration) - throws CameraAccessException { - - synchronized (mLock) { - try { - return mCameraService.isSessionConfigurationWithParametersSupported( - cameraId, sessionConfiguration); - } catch (ServiceSpecificException e) { - throwAsPublicException(e); - } catch (RemoteException e) { - // Camera service died - act as if the camera was disconnected - throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, - "Camera service is currently unavailable", e); - } - } - - return false; } /** @@ -2462,7 +2409,7 @@ public final class CameraManager { try { cameraService.setTorchMode(cameraId, enabled, mTorchClientBinder); } catch(ServiceSpecificException e) { - throwAsPublicException(e); + throw ExceptionUtils.throwAsPublicException(e); } catch (RemoteException e) { throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, "Camera service is currently unavailable"); @@ -2488,7 +2435,7 @@ public final class CameraManager { cameraService.turnOnTorchWithStrengthLevel(cameraId, torchStrength, mTorchClientBinder); } catch(ServiceSpecificException e) { - throwAsPublicException(e); + throw ExceptionUtils.throwAsPublicException(e); } catch (RemoteException e) { throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, "Camera service is currently unavailable."); @@ -2512,7 +2459,7 @@ public final class CameraManager { try { torchStrength = cameraService.getTorchStrengthLevel(cameraId); } catch(ServiceSpecificException e) { - throwAsPublicException(e); + throw ExceptionUtils.throwAsPublicException(e); } catch (RemoteException e) { throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, "Camera service is currently unavailable."); @@ -2521,45 +2468,6 @@ public final class CameraManager { return torchStrength; } - public CaptureRequest.Builder createCaptureRequest(@NonNull String cameraId, - @RequestTemplate int templateType, int targetSdkVersion) - throws CameraAccessException { - CaptureRequest.Builder builder = null; - synchronized (mLock) { - if (cameraId == null) { - throw new IllegalArgumentException("cameraId was null"); - } - - ICameraService cameraService = getCameraService(); - if (cameraService == null) { - throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, - "Camera service is currently unavailable."); - } - - try { - CameraMetadataNative defaultRequest = - cameraService.createDefaultRequest(cameraId, templateType); - - CameraDeviceImpl.disableZslIfNeeded(defaultRequest, - targetSdkVersion, templateType); - - builder = new CaptureRequest.Builder(defaultRequest, /*reprocess*/false, - CameraCaptureSession.SESSION_ID_NONE, cameraId, - /*physicalCameraIdSet*/null); - } catch (ServiceSpecificException e) { - if (e.errorCode == ICameraService.ERROR_INVALID_OPERATION) { - throw new UnsupportedOperationException(e.getMessage()); - } - - throwAsPublicException(e); - } catch (RemoteException e) { - throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, - "Camera service is currently unavailable."); - } - } - return builder; - } - private void handleRecoverableSetupErrors(ServiceSpecificException e) { switch (e.errorCode) { case ICameraService.ERROR_DISCONNECTED: diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index f03876b985ec..98a44ee446b9 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -19,6 +19,10 @@ package android.hardware.camera2.impl; import static com.android.internal.util.function.pooled.PooledLambda.obtainRunnable; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.compat.CompatChanges; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; import android.content.Context; import android.graphics.ImageFormat; import android.hardware.ICameraService; @@ -59,6 +63,8 @@ import android.util.Size; import android.util.SparseArray; import android.view.Surface; +import com.android.internal.camera.flags.Flags; + import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Arrays; @@ -85,10 +91,23 @@ public class CameraDeviceImpl extends CameraDevice private static final int REQUEST_ID_NONE = -1; + /** + * Starting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, + * {@link #isSessionConfigurationSupported} also checks for compatibility of session parameters + * when supported by the HAL. This ChangeId guards enabling that functionality for apps + * that target {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above. + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) + static final long CHECK_PARAMS_IN_IS_SESSION_CONFIGURATION_SUPPORTED = 320741775; + // TODO: guard every function with if (!mRemoteDevice) check (if it was closed) private ICameraDeviceUserWrapper mRemoteDevice; private boolean mRemoteDeviceInit = false; + // CameraDeviceSetup object to delegate some of the newer calls to. + @Nullable private final CameraDeviceSetup mCameraDeviceSetup; + // Lock to synchronize cross-thread access to device public interface final Object mInterfaceLock = new Object(); // access from this class and Session only! private final CameraDeviceCallbacks mCallbacks = new CameraDeviceCallbacks(); @@ -275,7 +294,8 @@ public class CameraDeviceImpl extends CameraDevice CameraCharacteristics characteristics, Map<String, CameraCharacteristics> physicalIdsToChars, int appTargetSdkVersion, - Context ctx) { + Context ctx, + @Nullable CameraDevice.CameraDeviceSetup cameraDeviceSetup) { if (cameraId == null || callback == null || executor == null || characteristics == null) { throw new IllegalArgumentException("Null argument given"); } @@ -286,6 +306,7 @@ public class CameraDeviceImpl extends CameraDevice mPhysicalIdsToChars = physicalIdsToChars; mAppTargetSdkVersion = appTargetSdkVersion; mContext = ctx; + mCameraDeviceSetup = cameraDeviceSetup; final int MAX_TAG_LEN = 23; String tag = String.format("CameraDevice-JV-%s", mCameraId); @@ -781,7 +802,11 @@ public class CameraDeviceImpl extends CameraDevice UnsupportedOperationException, IllegalArgumentException { synchronized (mInterfaceLock) { checkIfCameraClosedOrInError(); - + if (CompatChanges.isChangeEnabled(CHECK_PARAMS_IN_IS_SESSION_CONFIGURATION_SUPPORTED) + && Flags.cameraDeviceSetup() + && mCameraDeviceSetup != null) { + return mCameraDeviceSetup.isSessionConfigurationSupported(sessionConfig); + } return mRemoteDevice.isSessionConfigurationSupported(sessionConfig); } } diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java new file mode 100644 index 000000000000..fa2f519fee0d --- /dev/null +++ b/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java @@ -0,0 +1,162 @@ +/* + * 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.camera2.impl; + +import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; +import android.hardware.ICameraService; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.SessionConfiguration; +import android.hardware.camera2.utils.ExceptionUtils; +import android.os.Build; +import android.os.RemoteException; +import android.os.ServiceSpecificException; + +import androidx.annotation.NonNull; + +import com.android.internal.camera.flags.Flags; + +import java.util.concurrent.Executor; + +@FlaggedApi(Flags.FLAG_CAMERA_DEVICE_SETUP) +public class CameraDeviceSetupImpl extends CameraDevice.CameraDeviceSetup { + private final String mCameraId; + private final CameraManager mCameraManager; + private final int mTargetSdkVersion; + + private final Object mInterfaceLock = new Object(); + + public CameraDeviceSetupImpl(@NonNull String cameraId, @NonNull CameraManager cameraManager, + int targetSdkVersion) { + mCameraId = cameraId; + mCameraManager = cameraManager; + mTargetSdkVersion = targetSdkVersion; + } + + @NonNull + @Override + public CaptureRequest.Builder createCaptureRequest(int templateType) + throws CameraAccessException { + synchronized (mInterfaceLock) { + if (mCameraManager.isCameraServiceDisabled()) { + throw new IllegalArgumentException("No cameras available on device"); + } + + ICameraService cameraService = mCameraManager.getCameraService(); + if (cameraService == null) { + throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, + "Camera service is currently unavailable."); + } + + try { + CameraMetadataNative defaultRequest = cameraService.createDefaultRequest(mCameraId, + templateType); + CameraDeviceImpl.disableZslIfNeeded(defaultRequest, mTargetSdkVersion, + templateType); + + return new CaptureRequest.Builder( + defaultRequest, /*reprocess=*/ false, + CameraCaptureSession.SESSION_ID_NONE, mCameraId, + /*physicalCameraIdSet=*/ null); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); + } + } + } + + @Override + public boolean isSessionConfigurationSupported(@NonNull SessionConfiguration config) + throws CameraAccessException { + // TODO(b/298033056): restructure the OutputConfiguration API for better usability + synchronized (mInterfaceLock) { + if (mCameraManager.isCameraServiceDisabled()) { + throw new IllegalArgumentException("No cameras available on device"); + } + + ICameraService cameraService = mCameraManager.getCameraService(); + if (cameraService == null) { + throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, + "Camera service is currently unavailable."); + } + + try { + return cameraService.isSessionConfigurationWithParametersSupported( + mCameraId, config); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); + } + } + } + + @Override + public void openCamera(@NonNull @CallbackExecutor Executor executor, + @NonNull CameraDevice.StateCallback callback) throws CameraAccessException { + mCameraManager.openCamera(mCameraId, executor, callback); + } + + @NonNull + @Override + public String getId() { + return mCameraId; + } + + @Override + public int hashCode() { + return mCameraId.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof CameraDeviceSetupImpl other) { + return mCameraId.equals(other.mCameraId); + } + return false; + } + + @Override + public String toString() { + return "CameraDeviceSetup(cameraId='" + mCameraId + "')"; + } + + /** + * Returns true if HAL supports calls to {@code isSessionConfigurationWithParametersSupported}; + * false otherwise. + * <p> + * Suppressing AndroidFrameworkCompatChange because we are querying HAL support here + * and HAL's return value happens to follow the same scheme as SDK version. + * AndroidFrameworkCompatChange incorrectly flags this as an SDK version check. + * @hide + */ + @SuppressWarnings("AndroidFrameworkCompatChange") + public static boolean isCameraDeviceSetupSupported(CameraCharacteristics chars) { + if (!Flags.featureCombinationQuery()) { + return false; + } + + Integer queryVersion = chars.get( + CameraCharacteristics.INFO_SESSION_CONFIGURATION_QUERY_VERSION); + return queryVersion != null && queryVersion > Build.VERSION_CODES.UPSIDE_DOWN_CAKE; + } +} diff --git a/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java b/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java index 2129260b0ae8..241268d866a4 100644 --- a/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java +++ b/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java @@ -18,14 +18,13 @@ package android.hardware.camera2.impl; import android.hardware.ICameraService; import android.hardware.camera2.CameraAccessException; -import android.hardware.camera2.CameraManager; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.ICameraDeviceCallbacks; import android.hardware.camera2.ICameraDeviceUser; import android.hardware.camera2.ICameraOfflineSession; -import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; +import android.hardware.camera2.utils.ExceptionUtils; import android.hardware.camera2.utils.SubmitInfo; import android.os.IBinder; import android.os.RemoteException; @@ -69,9 +68,10 @@ public class ICameraDeviceUserWrapper { throws CameraAccessException { try { return mRemoteDevice.submitRequest(request, streaming); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } @@ -79,27 +79,30 @@ public class ICameraDeviceUserWrapper { throws CameraAccessException { try { return mRemoteDevice.submitRequestList(requestList, streaming); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } public long cancelRequest(int requestId) throws CameraAccessException { try { return mRemoteDevice.cancelRequest(requestId); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } public void beginConfigure() throws CameraAccessException { try { mRemoteDevice.beginConfigure(); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } @@ -108,18 +111,20 @@ public class ICameraDeviceUserWrapper { try { return mRemoteDevice.endConfigure(operatingMode, (sessionParams == null) ? new CameraMetadataNative() : sessionParams, startTimeMs); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } public void deleteStream(int streamId) throws CameraAccessException { try { mRemoteDevice.deleteStream(streamId); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } @@ -127,9 +132,10 @@ public class ICameraDeviceUserWrapper { throws CameraAccessException { try { return mRemoteDevice.createStream(outputConfiguration); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } @@ -137,45 +143,50 @@ public class ICameraDeviceUserWrapper { throws CameraAccessException { try { return mRemoteDevice.createInputStream(width, height, format, isMultiResolution); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } public Surface getInputSurface() throws CameraAccessException { try { return mRemoteDevice.getInputSurface(); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } public CameraMetadataNative createDefaultRequest(int templateId) throws CameraAccessException { try { return mRemoteDevice.createDefaultRequest(templateId); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } public CameraMetadataNative getCameraInfo() throws CameraAccessException { try { return mRemoteDevice.getCameraInfo(); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } public void waitUntilIdle() throws CameraAccessException { try { mRemoteDevice.waitUntilIdle(); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } @@ -191,10 +202,9 @@ public class ICameraDeviceUserWrapper { throw new IllegalArgumentException("Invalid session configuration"); } - throw e; - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } @@ -213,46 +223,49 @@ public class ICameraDeviceUserWrapper { throw new IllegalArgumentException("Invalid session configuration"); } - throw e; - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } public long flush() throws CameraAccessException { try { return mRemoteDevice.flush(); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } public void prepare(int streamId) throws CameraAccessException { try { mRemoteDevice.prepare(streamId); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } public void tearDown(int streamId) throws CameraAccessException { try { mRemoteDevice.tearDown(streamId); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } public void prepare2(int maxCount, int streamId) throws CameraAccessException { try { mRemoteDevice.prepare2(maxCount, streamId); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } @@ -260,9 +273,10 @@ public class ICameraDeviceUserWrapper { throws CameraAccessException { try { mRemoteDevice.updateOutputConfiguration(streamId, config); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } @@ -270,9 +284,10 @@ public class ICameraDeviceUserWrapper { int[] offlineOutputIds) throws CameraAccessException { try { return mRemoteDevice.switchToOffline(cbs, offlineOutputIds); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } @@ -280,27 +295,30 @@ public class ICameraDeviceUserWrapper { throws CameraAccessException { try { mRemoteDevice.finalizeOutputConfigurations(streamId, deferredConfig); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } public void setCameraAudioRestriction(int mode) throws CameraAccessException { try { mRemoteDevice.setCameraAudioRestriction(mode); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } public int getGlobalAudioRestriction() throws CameraAccessException { try { return mRemoteDevice.getGlobalAudioRestriction(); - } catch (Throwable t) { - CameraManager.throwAsPublicException(t); - throw new UnsupportedOperationException("Unexpected exception", t); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); } } diff --git a/core/java/android/hardware/camera2/utils/ExceptionUtils.java b/core/java/android/hardware/camera2/utils/ExceptionUtils.java new file mode 100644 index 000000000000..bfa96f26fa18 --- /dev/null +++ b/core/java/android/hardware/camera2/utils/ExceptionUtils.java @@ -0,0 +1,110 @@ +/* + * 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.camera2.utils; + +import android.hardware.ICameraService; +import android.hardware.camera2.CameraAccessException; +import android.os.DeadObjectException; +import android.os.RemoteException; +import android.os.ServiceSpecificException; + +/** + * @hide + */ +public class ExceptionUtils { + /** + * Converts and throws {@link ServiceSpecificException} from camera binder interfaces as + * {@link CameraAccessException}, {@link IllegalArgumentException}, or {@link SecurityException} + * based on {@link ServiceSpecificException#errorCode} + * <p> + * Usage: {@code throw ExceptionUtils.throwAsPublicException(e)} + * <p> + * Notice the preceding `throw` before calling this method. The throw is essentially + * useless but lets the compiler know that execution will terminate at that statement + * preventing false "missing return statement" errors. + * <p> + * The return type is set to the only checked exception this method throws to ensure + * that the caller knows exactly which checked exception to declare/handle. + * + * @hide + */ + public static CameraAccessException throwAsPublicException(ServiceSpecificException e) + throws CameraAccessException { + int reason; + switch(e.errorCode) { + case ICameraService.ERROR_DISCONNECTED: + reason = CameraAccessException.CAMERA_DISCONNECTED; + break; + case ICameraService.ERROR_DISABLED: + reason = CameraAccessException.CAMERA_DISABLED; + break; + case ICameraService.ERROR_CAMERA_IN_USE: + reason = CameraAccessException.CAMERA_IN_USE; + break; + case ICameraService.ERROR_MAX_CAMERAS_IN_USE: + reason = CameraAccessException.MAX_CAMERAS_IN_USE; + break; + case ICameraService.ERROR_DEPRECATED_HAL: + reason = CameraAccessException.CAMERA_DEPRECATED_HAL; + break; + case ICameraService.ERROR_ILLEGAL_ARGUMENT: + case ICameraService.ERROR_ALREADY_EXISTS: + throw new IllegalArgumentException(e.getMessage(), e); + case ICameraService.ERROR_PERMISSION_DENIED: + throw new SecurityException(e.getMessage(), e); + case ICameraService.ERROR_TIMED_OUT: + case ICameraService.ERROR_INVALID_OPERATION: + default: + reason = CameraAccessException.CAMERA_ERROR; + } + + throw new CameraAccessException(reason, e.getMessage(), e); + } + + /** + * Converts and throws Binder {@link DeadObjectException} and {@link RemoteException} from + * camera binder interfaces as {@link CameraAccessException} or + * {@link UnsupportedOperationException} + * <p> + * Usage: {@code throw ExceptionUtils.throwAsPublicException(e)} + * <p> + * Notice the preceding `throw` before calling this method. The throw is essentially + * useless but lets the compiler know that execution will terminate at that statement + * preventing false "missing return statement" errors. + * <p> + * The return type is set to the only checked exception this method throws to ensure + * that the caller knows exactly which checked exception to declare/handle. + * + * @hide + */ + public static CameraAccessException throwAsPublicException(RemoteException e) + throws CameraAccessException { + if (e instanceof DeadObjectException) { + throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, + "Camera service has died unexpectedly", e); + } + + throw new UnsupportedOperationException("An unknown RemoteException was thrown" + + " which should never happen.", e); + } + + /** + * Static methods only. Do not initialize. + * @hide + */ + private ExceptionUtils() {} +} 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 de32423eab2e..89576ed62afe 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -1955,6 +1955,26 @@ public class UserManager { "no_sim_globally"; /** + * This user restriction specifies if assist content is disallowed from being sent to + * a privileged app such as the Assistant app. Assist content includes screenshots and + * information about an app, such as package name. + * + * <p>This restriction can only be set by a device owner or a profile owner. When it is set + * by a device owner, it disables the assist contextual data on the entire device. When it is + * set by a profile owner, it disables assist content on the profile. + * + * <p>Default is <code>false</code>. + * + * <p>Key for user restrictions. + * <p>Type: Boolean + * @see DevicePolicyManager#addUserRestriction(ComponentName, String) + * @see DevicePolicyManager#clearUserRestriction(ComponentName, String) + * @see #getUserRestrictions() + */ + @FlaggedApi(android.app.admin.flags.Flags.FLAG_ASSIST_CONTENT_USER_RESTRICTION_ENABLED) + public static final String DISALLOW_ASSIST_CONTENT = "no_assist_content"; + + /** * List of key values that can be passed into the various user restriction related methods * in {@link UserManager} & {@link DevicePolicyManager}. * Note: This is slightly different from the real set of user restrictions listed in {@link @@ -2042,6 +2062,7 @@ public class UserManager { DISALLOW_NEAR_FIELD_COMMUNICATION_RADIO, DISALLOW_THREAD_NETWORK, DISALLOW_SIM_GLOBALLY, + DISALLOW_ASSIST_CONTENT, }) @Retention(RetentionPolicy.SOURCE) public @interface UserRestrictionKey {} diff --git a/core/java/android/os/storage/OWNERS b/core/java/android/os/storage/OWNERS index 6941857e8fef..2cb16d337bba 100644 --- a/core/java/android/os/storage/OWNERS +++ b/core/java/android/os/storage/OWNERS @@ -10,7 +10,6 @@ gargshivam@google.com krishang@google.com riyaghai@google.com sahanas@google.com -sergeynv@google.com shikhamalhotra@google.com shubhisaxena@google.com tylersaunders@google.com diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig index ea9375ef323c..d485eca7375b 100644 --- a/core/java/android/os/vibrator/flags.aconfig +++ b/core/java/android/os/vibrator/flags.aconfig @@ -16,13 +16,6 @@ flag { flag { namespace: "haptics" - name: "enable_vibration_serialization_apis" - description: "Enables the APIs for vibration serialization/deserialization." - bug: "245129509" -} - -flag { - namespace: "haptics" name: "haptic_feedback_vibration_oem_customization_enabled" description: "Enables OEMs/devices to customize vibrations for haptic feedback" # Make read only. This is because the flag is used only once, and this could happen before diff --git a/core/java/android/os/vibrator/persistence/ParsedVibration.java b/core/java/android/os/vibrator/persistence/ParsedVibration.java index 3d1deea57f14..a16d21eb87a3 100644 --- a/core/java/android/os/vibrator/persistence/ParsedVibration.java +++ b/core/java/android/os/vibrator/persistence/ParsedVibration.java @@ -16,9 +16,9 @@ package android.os.vibrator.persistence; -import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.annotation.TestApi; import android.os.VibrationEffect; import android.os.Vibrator; @@ -35,8 +35,8 @@ import java.util.List; * * @hide */ -@FlaggedApi(android.os.vibrator.Flags.FLAG_ENABLE_VIBRATION_SERIALIZATION_APIS) @TestApi +@SuppressLint("UnflaggedApi") // @TestApi without associated feature. public class ParsedVibration { private final List<VibrationEffect> mEffects; diff --git a/core/java/android/os/vibrator/persistence/VibrationXmlParser.java b/core/java/android/os/vibrator/persistence/VibrationXmlParser.java index 3d711a7b69d5..7202d9a19c30 100644 --- a/core/java/android/os/vibrator/persistence/VibrationXmlParser.java +++ b/core/java/android/os/vibrator/persistence/VibrationXmlParser.java @@ -16,10 +16,10 @@ package android.os.vibrator.persistence; -import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.annotation.TestApi; import android.os.VibrationEffect; import android.util.Slog; @@ -116,8 +116,8 @@ import java.util.List; * * @hide */ -@FlaggedApi(android.os.vibrator.Flags.FLAG_ENABLE_VIBRATION_SERIALIZATION_APIS) @TestApi +@SuppressLint("UnflaggedApi") // @TestApi without associated feature. public final class VibrationXmlParser { private static final String TAG = "VibrationXmlParser"; diff --git a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java index 28804544edaf..2065d5d0a5a7 100644 --- a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java +++ b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java @@ -16,9 +16,9 @@ package android.os.vibrator.persistence; -import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.SuppressLint; import android.annotation.TestApi; import android.os.CombinedVibration; import android.os.VibrationEffect; @@ -43,8 +43,8 @@ import java.lang.annotation.RetentionPolicy; * * @hide */ -@FlaggedApi(android.os.vibrator.Flags.FLAG_ENABLE_VIBRATION_SERIALIZATION_APIS) @TestApi +@SuppressLint("UnflaggedApi") // @TestApi without associated feature. public final class VibrationXmlSerializer { /** diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index 8c7050176506..9d7fb7018d52 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -94,3 +94,11 @@ flag { description: "Enable signature permission allowlist" bug: "308573169" } + +flag { + name: "device_aware_permissions_enabled" + is_fixed_read_only: true + namespace: "permissions" + description: "When the flag is off no permissions can be device aware" + bug: "274852670" +}
\ No newline at end of file diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index b026ce94b3fd..ecf193739627 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -11061,6 +11061,15 @@ public final class Settings { public static final String SEARCH_LONG_PRESS_HOME_ENABLED = "search_long_press_home_enabled"; + + /** + * Whether or not the accessibility data streaming is enbled for the + * {@link VisualQueryDetectedResult#setAccessibilityDetectionData}. + * @hide + */ + public static final String VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED = + "visual_query_accessibility_detection_enabled"; + /** * Control whether Night display is currently activated. * @hide @@ -12286,6 +12295,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/provider/Telephony.java b/core/java/android/provider/Telephony.java index 658cec86f7d5..88bd87e6c27c 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -4956,6 +4956,26 @@ public final class Telephony { */ public static final String COLUMN_SERVICE_CAPABILITIES = "service_capabilities"; + /** + * TelephonyProvider column name for satellite entitlement status. The value of this column + * is set based on entitlement query result for satellite configuration. + * By default, it's disabled. + * + * @hide + */ + public static final String COLUMN_SATELLITE_ENTITLEMENT_STATUS = + "satellite_entitlement_status"; + + /** + * TelephonyProvider column name for satellite entitlement plmns. The value of this + * column is set based on entitlement query result for satellite configuration. + * By default, it's empty. + * + * @hide + */ + public static final String COLUMN_SATELLITE_ENTITLEMENT_PLMNS = + "satellite_entitlement_plmns"; + /** All columns in {@link SimInfo} table. */ private static final List<String> ALL_COLUMNS = List.of( COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID, @@ -5029,7 +5049,9 @@ public final class Telephony { COLUMN_SATELLITE_ATTACH_ENABLED_FOR_CARRIER, COLUMN_IS_NTN, COLUMN_SERVICE_CAPABILITIES, - COLUMN_TRANSFER_STATUS + COLUMN_TRANSFER_STATUS, + COLUMN_SATELLITE_ENTITLEMENT_STATUS, + COLUMN_SATELLITE_ENTITLEMENT_PLMNS ); /** 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/notification/ZenDeviceEffects.java b/core/java/android/service/notification/ZenDeviceEffects.java index 90049e6a934a..22b1be08e1db 100644 --- a/core/java/android/service/notification/ZenDeviceEffects.java +++ b/core/java/android/service/notification/ZenDeviceEffects.java @@ -20,6 +20,7 @@ import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.app.Flags; import android.os.Parcel; import android.os.Parcelable; @@ -27,7 +28,10 @@ import android.os.Parcelable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; /** * Represents the set of device effects (affecting display and device behavior in general) that @@ -51,6 +55,7 @@ public final class ZenDeviceEffects implements Parcelable { FIELD_DISABLE_TOUCH, FIELD_MINIMIZE_RADIO_USAGE, FIELD_MAXIMIZE_DOZE, + FIELD_EXTRA_EFFECTS }) @Retention(RetentionPolicy.SOURCE) public @interface ModifiableField {} @@ -95,6 +100,12 @@ public final class ZenDeviceEffects implements Parcelable { * @hide */ public static final int FIELD_MAXIMIZE_DOZE = 1 << 9; + /** + * @hide + */ + public static final int FIELD_EXTRA_EFFECTS = 1 << 10; + + private static final int MAX_EFFECTS_LENGTH = 2_000; // characters private final boolean mGrayscale; private final boolean mSuppressAmbientDisplay; @@ -107,11 +118,12 @@ public final class ZenDeviceEffects implements Parcelable { private final boolean mDisableTouch; private final boolean mMinimizeRadioUsage; private final boolean mMaximizeDoze; + private final Set<String> mExtraEffects; private ZenDeviceEffects(boolean grayscale, boolean suppressAmbientDisplay, boolean dimWallpaper, boolean nightMode, boolean disableAutoBrightness, boolean disableTapToWake, boolean disableTiltToWake, boolean disableTouch, - boolean minimizeRadioUsage, boolean maximizeDoze) { + boolean minimizeRadioUsage, boolean maximizeDoze, Set<String> extraEffects) { mGrayscale = grayscale; mSuppressAmbientDisplay = suppressAmbientDisplay; mDimWallpaper = dimWallpaper; @@ -122,6 +134,21 @@ public final class ZenDeviceEffects implements Parcelable { mDisableTouch = disableTouch; mMinimizeRadioUsage = minimizeRadioUsage; mMaximizeDoze = maximizeDoze; + mExtraEffects = Collections.unmodifiableSet(extraEffects); + } + + /** @hide */ + @FlaggedApi(Flags.FLAG_MODES_API) + public void validate() { + int extraEffectsLength = 0; + for (String extraEffect : mExtraEffects) { + extraEffectsLength += extraEffect.length(); + } + if (extraEffectsLength > MAX_EFFECTS_LENGTH) { + throw new IllegalArgumentException( + "Total size of extra effects must be at most " + MAX_EFFECTS_LENGTH + + " characters"); + } } @Override @@ -138,19 +165,20 @@ public final class ZenDeviceEffects implements Parcelable { && this.mDisableTiltToWake == that.mDisableTiltToWake && this.mDisableTouch == that.mDisableTouch && this.mMinimizeRadioUsage == that.mMinimizeRadioUsage - && this.mMaximizeDoze == that.mMaximizeDoze; + && this.mMaximizeDoze == that.mMaximizeDoze + && Objects.equals(this.mExtraEffects, that.mExtraEffects); } @Override public int hashCode() { return Objects.hash(mGrayscale, mSuppressAmbientDisplay, mDimWallpaper, mNightMode, mDisableAutoBrightness, mDisableTapToWake, mDisableTiltToWake, mDisableTouch, - mMinimizeRadioUsage, mMaximizeDoze); + mMinimizeRadioUsage, mMaximizeDoze, mExtraEffects); } @Override public String toString() { - ArrayList<String> effects = new ArrayList<>(10); + ArrayList<String> effects = new ArrayList<>(11); if (mGrayscale) effects.add("grayscale"); if (mSuppressAmbientDisplay) effects.add("suppressAmbientDisplay"); if (mDimWallpaper) effects.add("dimWallpaper"); @@ -161,6 +189,9 @@ public final class ZenDeviceEffects implements Parcelable { if (mDisableTouch) effects.add("disableTouch"); if (mMinimizeRadioUsage) effects.add("minimizeRadioUsage"); if (mMaximizeDoze) effects.add("maximizeDoze"); + if (mExtraEffects.size() > 0) { + effects.add("extraEffects=[" + String.join(",", mExtraEffects) + "]"); + } return "[" + String.join(", ", effects) + "]"; } @@ -197,6 +228,9 @@ public final class ZenDeviceEffects implements Parcelable { if ((bitmask & FIELD_MAXIMIZE_DOZE) != 0) { modified.add("FIELD_MAXIMIZE_DOZE"); } + if ((bitmask & FIELD_EXTRA_EFFECTS) != 0) { + modified.add("FIELD_EXTRA_EFFECTS"); + } return "{" + String.join(",", modified) + "}"; } @@ -270,7 +304,7 @@ public final class ZenDeviceEffects implements Parcelable { } /** - * Whether Doze should be enhanced (e.g. with more aggresive activation, or less frequent + * Whether Doze should be enhanced (e.g. with more aggressive activation, or less frequent * maintenance windows) while the rule is active. * @hide */ @@ -279,13 +313,26 @@ public final class ZenDeviceEffects implements Parcelable { } /** + * (Immutable) set of extra effects to be applied while the rule is active. Extra effects are + * not used in AOSP, but OEMs may add support for them by providing a custom + * {@link DeviceEffectsApplier}. + * @hide + */ + @TestApi + @NonNull + public Set<String> getExtraEffects() { + return mExtraEffects; + } + + /** * Whether any of the effects are set up. * @hide */ public boolean hasEffects() { return mGrayscale || mSuppressAmbientDisplay || mDimWallpaper || mNightMode || mDisableAutoBrightness || mDisableTapToWake || mDisableTiltToWake - || mDisableTouch || mMinimizeRadioUsage || mMaximizeDoze; + || mDisableTouch || mMinimizeRadioUsage || mMaximizeDoze + || mExtraEffects.size() > 0; } /** {@link Parcelable.Creator} that instantiates {@link ZenDeviceEffects} objects. */ @@ -296,7 +343,8 @@ public final class ZenDeviceEffects implements Parcelable { return new ZenDeviceEffects(in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), - in.readBoolean()); + in.readBoolean(), + Set.of(in.readArray(String.class.getClassLoader(), String.class))); } @Override @@ -322,6 +370,7 @@ public final class ZenDeviceEffects implements Parcelable { dest.writeBoolean(mDisableTouch); dest.writeBoolean(mMinimizeRadioUsage); dest.writeBoolean(mMaximizeDoze); + dest.writeArray(mExtraEffects.toArray(new String[0])); } /** Builder class for {@link ZenDeviceEffects} objects. */ @@ -338,6 +387,7 @@ public final class ZenDeviceEffects implements Parcelable { private boolean mDisableTouch; private boolean mMinimizeRadioUsage; private boolean mMaximizeDoze; + private final HashSet<String> mExtraEffects = new HashSet<>(); /** * Instantiates a new {@link ZenPolicy.Builder} with all effects set to default (disabled). @@ -360,6 +410,7 @@ public final class ZenDeviceEffects implements Parcelable { mDisableTouch = zenDeviceEffects.shouldDisableTouch(); mMinimizeRadioUsage = zenDeviceEffects.shouldMinimizeRadioUsage(); mMaximizeDoze = zenDeviceEffects.shouldMaximizeDoze(); + mExtraEffects.addAll(zenDeviceEffects.getExtraEffects()); } /** @@ -450,7 +501,7 @@ public final class ZenDeviceEffects implements Parcelable { } /** - * Sets whether Doze should be enhanced (e.g. with more aggresive activation, or less + * Sets whether Doze should be enhanced (e.g. with more aggressive activation, or less * frequent maintenance windows) while the rule is active. * @hide */ @@ -461,6 +512,54 @@ public final class ZenDeviceEffects implements Parcelable { } /** + * Sets the extra effects to be applied while the rule is active. Extra effects are not + * used in AOSP, but OEMs may add support for them by providing a custom + * {@link DeviceEffectsApplier}. + * + * @apiNote The total size of the extra effects (concatenation of strings) is limited. + * + * @hide + */ + @TestApi + @NonNull + public Builder setExtraEffects(@NonNull Set<String> extraEffects) { + Objects.requireNonNull(extraEffects); + mExtraEffects.clear(); + mExtraEffects.addAll(extraEffects); + return this; + } + + /** + * Adds the supplied extra effects to the set to be applied while the rule is active. + * Extra effects are not used in AOSP, but OEMs may add support for them by providing a + * custom {@link DeviceEffectsApplier}. + * + * @apiNote The total size of the extra effects (concatenation of strings) is limited. + * + * @hide + */ + @NonNull + public Builder addExtraEffects(@NonNull Set<String> extraEffects) { + mExtraEffects.addAll(Objects.requireNonNull(extraEffects)); + return this; + } + + /** + * Adds the supplied extra effect to the set to be applied while the rule is active. + * Extra effects are not used in AOSP, but OEMs may add support for them by providing a + * custom {@link DeviceEffectsApplier}. + * + * @apiNote The total size of the extra effects (concatenation of strings) is limited. + * + * @hide + */ + @NonNull + public Builder addExtraEffect(@NonNull String extraEffect) { + mExtraEffects.add(Objects.requireNonNull(extraEffect)); + return this; + } + + /** * Applies the effects that are {@code true} on the supplied {@link ZenDeviceEffects} to * this builder (essentially logically-ORing the effect set). * @hide @@ -478,6 +577,7 @@ public final class ZenDeviceEffects implements Parcelable { if (effects.shouldDisableTouch()) setShouldDisableTouch(true); if (effects.shouldMinimizeRadioUsage()) setShouldMinimizeRadioUsage(true); if (effects.shouldMaximizeDoze()) setShouldMaximizeDoze(true); + addExtraEffects(effects.getExtraEffects()); return this; } @@ -487,7 +587,7 @@ public final class ZenDeviceEffects implements Parcelable { return new ZenDeviceEffects(mGrayscale, mSuppressAmbientDisplay, mDimWallpaper, mNightMode, mDisableAutoBrightness, mDisableTapToWake, mDisableTiltToWake, mDisableTouch, mMinimizeRadioUsage, - mMaximizeDoze); + mMaximizeDoze, mExtraEffects); } } } diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index d4a5356d99c6..f169ecd1a4e5 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -27,6 +27,7 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OF import android.annotation.FlaggedApi; import android.annotation.IntDef; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.AlarmManager; @@ -65,17 +66,21 @@ import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.TimeZone; import java.util.UUID; +import java.util.regex.Pattern; /** * Persisted configuration for zen mode. @@ -272,8 +277,13 @@ public class ZenModeConfig implements Parcelable { private static final String DEVICE_EFFECT_DISABLE_TOUCH = "zdeDisableTouch"; private static final String DEVICE_EFFECT_MINIMIZE_RADIO_USAGE = "zdeMinimizeRadioUsage"; private static final String DEVICE_EFFECT_MAXIMIZE_DOZE = "zdeMaximizeDoze"; + private static final String DEVICE_EFFECT_EXTRAS = "zdeExtraEffects"; private static final String DEVICE_EFFECT_USER_MODIFIED_FIELDS = "zdeUserModifiedFields"; + private static final String ITEM_SEPARATOR = ","; + private static final String ITEM_SEPARATOR_ESCAPE = "\\"; + private static final Pattern ITEM_SPLITTER_REGEX = Pattern.compile("(?<!\\\\),"); + @UnsupportedAppUsage public boolean allowAlarms = DEFAULT_ALLOW_ALARMS; public boolean allowMedia = DEFAULT_ALLOW_MEDIA; @@ -1099,6 +1109,7 @@ public class ZenModeConfig implements Parcelable { .setShouldMinimizeRadioUsage( safeBoolean(parser, DEVICE_EFFECT_MINIMIZE_RADIO_USAGE, false)) .setShouldMaximizeDoze(safeBoolean(parser, DEVICE_EFFECT_MAXIMIZE_DOZE, false)) + .setExtraEffects(safeStringSet(parser, DEVICE_EFFECT_EXTRAS)) .build(); return deviceEffects.hasEffects() ? deviceEffects : null; @@ -1123,6 +1134,7 @@ public class ZenModeConfig implements Parcelable { writeBooleanIfTrue(out, DEVICE_EFFECT_MINIMIZE_RADIO_USAGE, deviceEffects.shouldMinimizeRadioUsage()); writeBooleanIfTrue(out, DEVICE_EFFECT_MAXIMIZE_DOZE, deviceEffects.shouldMaximizeDoze()); + writeStringSet(out, DEVICE_EFFECT_EXTRAS, deviceEffects.getExtraEffects()); } private static void writeBooleanIfTrue(TypedXmlSerializer out, String att, boolean value) @@ -1132,6 +1144,26 @@ public class ZenModeConfig implements Parcelable { } } + private static void writeStringSet(TypedXmlSerializer out, String att, Set<String> values) + throws IOException { + if (values.isEmpty()) { + return; + } + // We escape each item by replacing "\" by "\\" and "," by "\,". Then we concatenate with + // "," as separator. Reading performs the same operations in the opposite order. + List<String> escapedItems = new ArrayList<>(); + for (String item : values) { + escapedItems.add( + item + .replace(ITEM_SEPARATOR_ESCAPE, + ITEM_SEPARATOR_ESCAPE + ITEM_SEPARATOR_ESCAPE) + .replace(ITEM_SEPARATOR, + ITEM_SEPARATOR_ESCAPE + ITEM_SEPARATOR)); + } + String serialized = String.join(ITEM_SEPARATOR, escapedItems); + out.attribute(null, att, serialized); + } + public static boolean isValidHour(int val) { return val >= 0 && val < 24; } @@ -1182,6 +1214,26 @@ public class ZenModeConfig implements Parcelable { return tryParseLong(val, defValue); } + @NonNull + private static Set<String> safeStringSet(TypedXmlPullParser parser, String att) { + Set<String> values = new HashSet<>(); + + String serialized = parser.getAttributeValue(null, att); + if (!TextUtils.isEmpty(serialized)) { + // We split on every "," that is *not preceded* by the escape character "\". + // Then we reverse the escaping done on each individual item. + String[] escapedItems = ITEM_SPLITTER_REGEX.split(serialized); + for (String escapedItem : escapedItems) { + values.add(escapedItem + .replace(ITEM_SEPARATOR_ESCAPE + ITEM_SEPARATOR_ESCAPE, + ITEM_SEPARATOR_ESCAPE) + .replace(ITEM_SEPARATOR_ESCAPE + ITEM_SEPARATOR, + ITEM_SEPARATOR)); + } + } + return values; + } + @Override public int describeContents() { return 0; diff --git a/core/java/android/service/voice/IDetectorSessionVisualQueryDetectionCallback.aidl b/core/java/android/service/voice/IDetectorSessionVisualQueryDetectionCallback.aidl index 4f6eb88e34e9..c2036a44181b 100644 --- a/core/java/android/service/voice/IDetectorSessionVisualQueryDetectionCallback.aidl +++ b/core/java/android/service/voice/IDetectorSessionVisualQueryDetectionCallback.aidl @@ -16,6 +16,7 @@ package android.service.voice; +import android.service.voice.VisualQueryAttentionResult; import android.service.voice.VisualQueryDetectedResult; /** @@ -31,12 +32,12 @@ oneway interface IDetectorSessionVisualQueryDetectionCallback { /** * Called when the user attention is gained and intent to show the assistant icon in SysUI. */ - void onAttentionGained(); + void onAttentionGained(in VisualQueryAttentionResult attentionResult); /** * Called when the user attention is lost and intent to hide the assistant icon in SysUI. */ - void onAttentionLost(); + void onAttentionLost(int interactionIntention); /** * Called when the detected query is streamed. diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneModel.kt b/core/java/android/service/voice/VisualQueryAttentionResult.aidl index f3d549f03868..38c8f07df76a 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneModel.kt +++ b/core/java/android/service/voice/VisualQueryAttentionResult.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * 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. @@ -14,14 +14,6 @@ * limitations under the License. */ -package com.android.systemui.scene.shared.model +package android.service.voice; -/** Models a scene. */ -data class SceneModel( - - /** The key of the scene. */ - val key: SceneKey, - - /** An optional name for the transition that led to this scene being the current scene. */ - val transitionName: String? = null, -) +parcelable VisualQueryAttentionResult;
\ No newline at end of file diff --git a/core/java/android/service/voice/VisualQueryAttentionResult.java b/core/java/android/service/voice/VisualQueryAttentionResult.java new file mode 100644 index 000000000000..690990b46ed6 --- /dev/null +++ b/core/java/android/service/voice/VisualQueryAttentionResult.java @@ -0,0 +1,365 @@ +/* + * 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.voice; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.service.voice.flags.Flags; + +import com.android.internal.util.DataClass; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Represents a result supporting the visual query attention. + * + * @hide + */ +@DataClass( + genConstructor = false, + genBuilder = true, + genEqualsHashCode = true, + genHiddenConstDefs = true, + genParcelable = true, + genToString = true +) +@SystemApi +@FlaggedApi(Flags.FLAG_ALLOW_VARIOUS_ATTENTION_TYPES) +public final class VisualQueryAttentionResult implements Parcelable { + + /** Intention type to allow the system to listen to audio-visual query interactions. */ + public static final int INTERACTION_INTENTION_AUDIO_VISUAL = 0; + + /** Intention type to allow the system to listen to visual accessibility query interactions. */ + public static final int INTERACTION_INTENTION_VISUAL_ACCESSIBILITY = 1; + + /** + * Intention of interaction associated with the attention result that the device should listen + * to after the attention signal is gained. + */ + private final @InteractionIntention int mInteractionIntention; + + private static @InteractionIntention int defaultInteractionIntention() { + return INTERACTION_INTENTION_AUDIO_VISUAL; + } + + /** + * Integer value denoting the level of user engagement of the attention. System will + * also use this to adjust the intensity of UI indicators. + * + * The value can be between 1 and 100 (inclusive). The default value is set to be 100 which is + * defined as a complete engagement, which leads to the same UI result as the legacy + * {@link VisualQueryDetectionService#gainedAttention()}. + * + * Different values of engagement level corresponds to various SysUI effects. Within the same + * interaction intention, higher value of engagement level will lead to stronger visual + * presentation of the device attention UI. + */ + @IntRange(from = 1, to = 100) + private final int mEngagementLevel; + + private static int defaultEngagementLevel() { + return 100; + } + + /** + * Provides an instance of {@link Builder} with state corresponding to this instance. + * + * @hide + */ + public Builder buildUpon() { + return new Builder() + .setInteractionIntention(mInteractionIntention) + .setEngagementLevel(mEngagementLevel); + } + + + + + // Code below generated by codegen v1.0.23. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/voice/VisualQueryAttentionResult.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + /** @hide */ + @IntDef(prefix = "INTERACTION_INTENTION_", value = { + INTERACTION_INTENTION_AUDIO_VISUAL, + INTERACTION_INTENTION_VISUAL_ACCESSIBILITY + }) + @Retention(RetentionPolicy.SOURCE) + @DataClass.Generated.Member + public @interface InteractionIntention {} + + /** @hide */ + @DataClass.Generated.Member + public static String interactionIntentionToString(@InteractionIntention int value) { + switch (value) { + case INTERACTION_INTENTION_AUDIO_VISUAL: + return "INTERACTION_INTENTION_AUDIO_VISUAL"; + case INTERACTION_INTENTION_VISUAL_ACCESSIBILITY: + return "INTERACTION_INTENTION_VISUAL_ACCESSIBILITY"; + default: return Integer.toHexString(value); + } + } + + @DataClass.Generated.Member + /* package-private */ VisualQueryAttentionResult( + @InteractionIntention int interactionIntention, + @IntRange(from = 1, to = 100) int engagementLevel) { + this.mInteractionIntention = interactionIntention; + + if (!(mInteractionIntention == INTERACTION_INTENTION_AUDIO_VISUAL) + && !(mInteractionIntention == INTERACTION_INTENTION_VISUAL_ACCESSIBILITY)) { + throw new java.lang.IllegalArgumentException( + "interactionIntention was " + mInteractionIntention + " but must be one of: " + + "INTERACTION_INTENTION_AUDIO_VISUAL(" + INTERACTION_INTENTION_AUDIO_VISUAL + "), " + + "INTERACTION_INTENTION_VISUAL_ACCESSIBILITY(" + INTERACTION_INTENTION_VISUAL_ACCESSIBILITY + ")"); + } + + this.mEngagementLevel = engagementLevel; + com.android.internal.util.AnnotationValidations.validate( + IntRange.class, null, mEngagementLevel, + "from", 1, + "to", 100); + + // onConstructed(); // You can define this method to get a callback + } + + /** + * Intention of interaction associated with the attention result that the device should listen + * to after the attention signal is gained. + */ + @DataClass.Generated.Member + public @InteractionIntention int getInteractionIntention() { + return mInteractionIntention; + } + + /** + * Integer value denoting the level of user engagement of the attention. System will + * also use this to adjust the intensity of UI indicators. + * + * The value can be between 1 and 100 (inclusive). The default value is set to be 100 which is + * defined as a complete engagement, which leads to the same UI result as the legacy + * {@link VisualQueryDetectionService#gainedAttention()}. + * + * Different values of engagement level corresponds to various SysUI effects. Within the same + * interaction intention, higher value of engagement level will lead to stronger visual + * presentation of the device attention UI. + */ + @DataClass.Generated.Member + public @IntRange(from = 1, to = 100) int getEngagementLevel() { + return mEngagementLevel; + } + + @Override + @DataClass.Generated.Member + public String toString() { + // You can override field toString logic by defining methods like: + // String fieldNameToString() { ... } + + return "VisualQueryAttentionResult { " + + "interactionIntention = " + interactionIntentionToString(mInteractionIntention) + ", " + + "engagementLevel = " + mEngagementLevel + + " }"; + } + + @Override + @DataClass.Generated.Member + public boolean equals(@Nullable Object o) { + // You can override field equality logic by defining either of the methods like: + // boolean fieldNameEquals(VisualQueryAttentionResult other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + VisualQueryAttentionResult that = (VisualQueryAttentionResult) o; + //noinspection PointlessBooleanExpression + return true + && mInteractionIntention == that.mInteractionIntention + && mEngagementLevel == that.mEngagementLevel; + } + + @Override + @DataClass.Generated.Member + public int hashCode() { + // You can override field hashCode logic by defining methods like: + // int fieldNameHashCode() { ... } + + int _hash = 1; + _hash = 31 * _hash + mInteractionIntention; + _hash = 31 * _hash + mEngagementLevel; + return _hash; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + dest.writeInt(mInteractionIntention); + dest.writeInt(mEngagementLevel); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ VisualQueryAttentionResult(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + int interactionIntention = in.readInt(); + int engagementLevel = in.readInt(); + + this.mInteractionIntention = interactionIntention; + + if (!(mInteractionIntention == INTERACTION_INTENTION_AUDIO_VISUAL) + && !(mInteractionIntention == INTERACTION_INTENTION_VISUAL_ACCESSIBILITY)) { + throw new java.lang.IllegalArgumentException( + "interactionIntention was " + mInteractionIntention + " but must be one of: " + + "INTERACTION_INTENTION_AUDIO_VISUAL(" + INTERACTION_INTENTION_AUDIO_VISUAL + "), " + + "INTERACTION_INTENTION_VISUAL_ACCESSIBILITY(" + INTERACTION_INTENTION_VISUAL_ACCESSIBILITY + ")"); + } + + this.mEngagementLevel = engagementLevel; + com.android.internal.util.AnnotationValidations.validate( + IntRange.class, null, mEngagementLevel, + "from", 1, + "to", 100); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<VisualQueryAttentionResult> CREATOR + = new Parcelable.Creator<VisualQueryAttentionResult>() { + @Override + public VisualQueryAttentionResult[] newArray(int size) { + return new VisualQueryAttentionResult[size]; + } + + @Override + public VisualQueryAttentionResult createFromParcel(@NonNull Parcel in) { + return new VisualQueryAttentionResult(in); + } + }; + + /** + * A builder for {@link VisualQueryAttentionResult} + */ + @SuppressWarnings("WeakerAccess") + @DataClass.Generated.Member + public static final class Builder { + + private @InteractionIntention int mInteractionIntention; + private @IntRange(from = 1, to = 100) int mEngagementLevel; + + private long mBuilderFieldsSet = 0L; + + public Builder() { + } + + /** + * Intention of interaction associated with the attention result that the device should listen + * to after the attention signal is gained. + */ + @DataClass.Generated.Member + public @NonNull Builder setInteractionIntention(@InteractionIntention int value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x1; + mInteractionIntention = value; + return this; + } + + /** + * Integer value denoting the level of user engagement of the attention. System will + * also use this to adjust the intensity of UI indicators. + * + * The value can be between 1 and 100 (inclusive). The default value is set to be 100 which is + * defined as a complete engagement, which leads to the same UI result as the legacy + * {@link VisualQueryDetectionService#gainedAttention()}. + * + * Different values of engagement level corresponds to various SysUI effects. Within the same + * interaction intention, higher value of engagement level will lead to stronger visual + * presentation of the device attention UI. + */ + @DataClass.Generated.Member + public @NonNull Builder setEngagementLevel(@IntRange(from = 1, to = 100) int value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x2; + mEngagementLevel = value; + return this; + } + + /** Builds the instance. This builder should not be touched after calling this! */ + public @NonNull VisualQueryAttentionResult build() { + checkNotUsed(); + mBuilderFieldsSet |= 0x4; // Mark builder used + + if ((mBuilderFieldsSet & 0x1) == 0) { + mInteractionIntention = defaultInteractionIntention(); + } + if ((mBuilderFieldsSet & 0x2) == 0) { + mEngagementLevel = defaultEngagementLevel(); + } + VisualQueryAttentionResult o = new VisualQueryAttentionResult( + mInteractionIntention, + mEngagementLevel); + return o; + } + + private void checkNotUsed() { + if ((mBuilderFieldsSet & 0x4) != 0) { + throw new IllegalStateException( + "This Builder should not be reused. Use a new Builder instance instead"); + } + } + } + + @DataClass.Generated( + time = 1707773691880L, + codegenVersion = "1.0.23", + sourceFile = "frameworks/base/core/java/android/service/voice/VisualQueryAttentionResult.java", + inputSignatures = "public static final int INTERACTION_INTENTION_AUDIO_VISUAL\npublic static final int INTERACTION_INTENTION_VISUAL_ACCESSIBILITY\nprivate final @android.service.voice.VisualQueryAttentionResult.InteractionIntention int mInteractionIntention\nprivate final @android.annotation.IntRange int mEngagementLevel\nprivate static @android.service.voice.VisualQueryAttentionResult.InteractionIntention int defaultInteractionIntention()\nprivate static int defaultEngagementLevel()\npublic android.service.voice.VisualQueryAttentionResult.Builder buildUpon()\nclass VisualQueryAttentionResult extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=true, genEqualsHashCode=true, genHiddenConstDefs=true, genParcelable=true, genToString=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/core/java/android/service/voice/VisualQueryDetectedResult.java b/core/java/android/service/voice/VisualQueryDetectedResult.java index 13cdfdebc86a..322148acdca5 100644 --- a/core/java/android/service/voice/VisualQueryDetectedResult.java +++ b/core/java/android/service/voice/VisualQueryDetectedResult.java @@ -82,8 +82,6 @@ public final class VisualQueryDetectedResult implements Parcelable { } - - // Code below generated by codegen v1.0.23. // // DO NOT MODIFY! diff --git a/core/java/android/service/voice/VisualQueryDetectionService.java b/core/java/android/service/voice/VisualQueryDetectionService.java index b60c77502247..887b5751ffc8 100644 --- a/core/java/android/service/voice/VisualQueryDetectionService.java +++ b/core/java/android/service/voice/VisualQueryDetectionService.java @@ -262,23 +262,82 @@ public abstract class VisualQueryDetectionService extends Service public void onStopDetection() { } + // TODO(b/324341724): Properly deprecate this API. /** - * Informs the system that the user attention is gained so queries can be streamed. + * Informs the system that the attention is gained for the interaction intention + * {@link VisualQueryAttentionResult#INTERACTION_INTENTION_AUDIO_VISUAL} with + * engagement level equals to the maximum value possible so queries can be streamed. + * + * Usage of this method is not recommended, please use + * {@link VisualQueryDetectionService#gainedAttention(VisualQueryAttentionResult)} instead. + * */ public final void gainedAttention() { + if (Flags.allowVariousAttentionTypes()) { + gainedAttention(new VisualQueryAttentionResult.Builder().build()); + } else { + try { + mRemoteCallback.onAttentionGained(null); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Puts the device into an attention state that will listen to certain interaction intention + * based on the {@link VisualQueryAttentionResult} provided. + * + * Different type and levels of engagement will lead to corresponding UI icons showing. See + * {@link VisualQueryAttentionResult#setInteractionIntention(int)} for details. + * + * Exactly one {@link VisualQueryAttentionResult} can be set at a time with this method at + * the moment. Multiple attention results will be supported to set the device into with this + * API before {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} is finalized. + * + * Latest call will override the {@link VisualQueryAttentionResult} of previous calls. Queries + * streamed are independent of the attention interactionIntention. + * + * @param attentionResult Attention result of type {@link VisualQueryAttentionResult}. + */ + @FlaggedApi(Flags.FLAG_ALLOW_VARIOUS_ATTENTION_TYPES) + public final void gainedAttention(@NonNull VisualQueryAttentionResult attentionResult) { try { - mRemoteCallback.onAttentionGained(); + mRemoteCallback.onAttentionGained(attentionResult); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** - * Informs the system that the user attention is lost to stop streaming. + * Informs the system that all attention has lost to stop streaming. */ public final void lostAttention() { + if (Flags.allowVariousAttentionTypes()) { + lostAttention(VisualQueryAttentionResult.INTERACTION_INTENTION_AUDIO_VISUAL); + lostAttention(VisualQueryAttentionResult.INTERACTION_INTENTION_VISUAL_ACCESSIBILITY); + } else { + try { + mRemoteCallback.onAttentionLost(0); // placeholder + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * This will cancel the corresponding attention if the provided interaction intention is the + * same as which of the object called with + * {@link VisualQueryDetectionService#gainedAttention(VisualQueryAttentionResult)}. + * + * @param interactionIntention Interaction intention, one of + * {@link VisualQueryAttentionResult#InteractionIntention}. + */ + @FlaggedApi(Flags.FLAG_ALLOW_VARIOUS_ATTENTION_TYPES) + public final void lostAttention( + @VisualQueryAttentionResult.InteractionIntention int interactionIntention) { try { - mRemoteCallback.onAttentionLost(); + mRemoteCallback.onAttentionLost(interactionIntention); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index f37c4c2a060c..f68fcab94952 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -110,3 +110,12 @@ flag { description: "A flag for replacing AndroidBidi with android.icu.text.Bidi." bug: "317144801" } + +flag { + name: "lazy_variation_instance" + namespace: "text" + description: "A flag for enabling lazy variation instance creation." + # Make read only, as it could be used before the Settings provider is initialized. + is_fixed_read_only: true + bug: "324676775" +} diff --git a/core/java/android/tracing/OWNERS b/core/java/android/tracing/OWNERS index 2ebe2e9e2761..f67844de44ea 100644 --- a/core/java/android/tracing/OWNERS +++ b/core/java/android/tracing/OWNERS @@ -1,6 +1,4 @@ carmenjackson@google.com kevinjeon@google.com -pablogamito@google.com -natanieljr@google.com -keanmariotti@google.com +include platform/development:/tools/winscope/OWNERS include platform/external/perfetto:/OWNERS diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index 0006139d2cb0..1f2b4faacc10 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -1211,6 +1211,8 @@ public final class Display { * @see #REMOVE_MODE_DESTROY_CONTENT */ // TODO (b/114338689): Remove the method and use IWindowManager#getRemoveContentMode + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. + @TestApi public int getRemoveMode() { return mDisplayInfo.removeMode; } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 3dfd68af5aaa..dc0b1a77de80 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -6394,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; } } diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java index cc2cd7982841..b7542dcd55e2 100644 --- a/core/java/android/view/WindowInsetsController.java +++ b/core/java/android/view/WindowInsetsController.java @@ -16,6 +16,7 @@ package android.view; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -26,6 +27,7 @@ import android.os.CancellationSignal; import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetsType; import android.view.animation.Interpolator; +import android.view.flags.Flags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -78,6 +80,20 @@ public interface WindowInsetsController { int APPEARANCE_SEMI_TRANSPARENT_NAVIGATION_BARS = 1 << 6; /** + * Makes the caption bar transparent. + */ + @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS) + int APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND = 1 << 7; + + /** + * When {@link WindowInsetsController#APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND} is set, + * changes the foreground color of the caption bars so that the items on the bar can be read + * clearly on light backgrounds. + */ + @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS) + int APPEARANCE_LIGHT_CAPTION_BARS = 1 << 8; + + /** * Determines the appearance of system bars. * @hide */ @@ -85,7 +101,8 @@ public interface WindowInsetsController { @IntDef(flag = true, value = {APPEARANCE_OPAQUE_STATUS_BARS, APPEARANCE_OPAQUE_NAVIGATION_BARS, APPEARANCE_LOW_PROFILE_BARS, APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_NAVIGATION_BARS, APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS, - APPEARANCE_SEMI_TRANSPARENT_NAVIGATION_BARS}) + APPEARANCE_SEMI_TRANSPARENT_NAVIGATION_BARS, + APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND, APPEARANCE_LIGHT_CAPTION_BARS}) @interface Appearance { } diff --git a/core/java/android/view/inputmethod/ConnectionlessHandwritingCallback.java b/core/java/android/view/inputmethod/ConnectionlessHandwritingCallback.java new file mode 100644 index 000000000000..d985732840fd --- /dev/null +++ b/core/java/android/view/inputmethod/ConnectionlessHandwritingCallback.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.inputmethod; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.view.View; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.Executor; + +/** + * Interface to receive the result of starting a connectionless stylus handwriting session using + * one of {@link InputMethodManager#startConnectionlessStylusHandwriting(View, CursorAnchorInfo, + * Executor,ConnectionlessHandwritingCallback)}, {@link + * InputMethodManager#startConnectionlessStylusHandwritingForDelegation(View, CursorAnchorInfo, + * Executor, ConnectionlessHandwritingCallback)}, or {@link + * InputMethodManager#startConnectionlessStylusHandwritingForDelegation(View, CursorAnchorInfo, + * String, Executor, ConnectionlessHandwritingCallback)}. + */ +@FlaggedApi(Flags.FLAG_CONNECTIONLESS_HANDWRITING) +public interface ConnectionlessHandwritingCallback { + + /** @hide */ + @IntDef(prefix = {"CONNECTIONLESS_HANDWRITING_ERROR_"}, value = { + CONNECTIONLESS_HANDWRITING_ERROR_NO_TEXT_RECOGNIZED, + CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED, + CONNECTIONLESS_HANDWRITING_ERROR_OTHER + }) + @Retention(RetentionPolicy.SOURCE) + @interface ConnectionlessHandwritingError { + } + + /** + * Error code indicating that the connectionless handwriting session started and completed + * but no text was recognized. + */ + int CONNECTIONLESS_HANDWRITING_ERROR_NO_TEXT_RECOGNIZED = 0; + + /** + * Error code indicating that the connectionless handwriting session was not started as the + * current IME does not support it. + */ + int CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED = 1; + + /** + * Error code for any other reason that the connectionless handwriting session did not complete + * successfully. Either the session could not start, or the session started but did not complete + * successfully. + */ + int CONNECTIONLESS_HANDWRITING_ERROR_OTHER = 2; + + /** + * Callback when the connectionless handwriting session completed successfully and + * recognized text. + */ + void onResult(@NonNull CharSequence text); + + /** Callback when the connectionless handwriting session did not complete successfully. */ + void onError(@ConnectionlessHandwritingError int errorCode); +} diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java index 89da04128bc8..dc5e0e5c62aa 100644 --- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java +++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java @@ -34,6 +34,7 @@ import android.view.WindowManager; import android.window.ImeOnBackInvokedDispatcher; import com.android.internal.inputmethod.DirectBootAwareness; +import com.android.internal.inputmethod.IConnectionlessHandwritingCallback; import com.android.internal.inputmethod.IImeTracker; import com.android.internal.inputmethod.IInputMethodClient; import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; @@ -492,6 +493,27 @@ final class IInputMethodManagerGlobalInvoker { } @AnyThread + static boolean startConnectionlessStylusHandwriting( + @NonNull IInputMethodClient client, + @UserIdInt int userId, + @Nullable CursorAnchorInfo cursorAnchorInfo, + @Nullable String delegatePackageName, + @Nullable String delegatorPackageName, + @NonNull IConnectionlessHandwritingCallback callback) { + final IInputMethodManager service = getService(); + if (service == null) { + return false; + } + try { + service.startConnectionlessStylusHandwriting(client, userId, cursorAnchorInfo, + delegatePackageName, delegatorPackageName, callback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + return true; + } + + @AnyThread static void prepareStylusHandwritingDelegation( @NonNull IInputMethodClient client, @UserIdInt int userId, @@ -530,13 +552,14 @@ final class IInputMethodManagerGlobalInvoker { @AnyThread @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true) - static boolean isStylusHandwritingAvailableAsUser(@UserIdInt int userId) { + static boolean isStylusHandwritingAvailableAsUser( + @UserIdInt int userId, boolean connectionless) { final IInputMethodManager service = getService(); if (service == null) { return false; } try { - return service.isStylusHandwritingAvailableAsUser(userId); + return service.isStylusHandwritingAvailableAsUser(userId, connectionless); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java index b60efc1ee508..7c9678f11e0e 100644 --- a/core/java/android/view/inputmethod/InputMethodInfo.java +++ b/core/java/android/view/inputmethod/InputMethodInfo.java @@ -204,6 +204,9 @@ public final class InputMethodInfo implements Parcelable { */ private final boolean mSupportsStylusHandwriting; + /** The flag whether this IME supports connectionless stylus handwriting sessions. */ + private final boolean mSupportsConnectionlessStylusHandwriting; + /** * The stylus handwriting setting activity's name, used by the system settings to * launch the stylus handwriting specific setting activity of this input method. @@ -330,6 +333,9 @@ public final class InputMethodInfo implements Parcelable { com.android.internal.R.styleable.InputMethod_configChanges, 0); mSupportsStylusHandwriting = sa.getBoolean( com.android.internal.R.styleable.InputMethod_supportsStylusHandwriting, false); + mSupportsConnectionlessStylusHandwriting = sa.getBoolean( + com.android.internal.R.styleable + .InputMethod_supportsConnectionlessStylusHandwriting, false); stylusHandwritingSettingsActivity = sa.getString( com.android.internal.R.styleable.InputMethod_stylusHandwritingSettingsActivity); sa.recycle(); @@ -442,6 +448,7 @@ public final class InputMethodInfo implements Parcelable { mSubtypes = source.mSubtypes; mHandledConfigChanges = source.mHandledConfigChanges; mSupportsStylusHandwriting = source.mSupportsStylusHandwriting; + mSupportsConnectionlessStylusHandwriting = source.mSupportsConnectionlessStylusHandwriting; mForceDefault = source.mForceDefault; mStylusHandwritingSettingsActivityAttr = source.mStylusHandwritingSettingsActivityAttr; } @@ -463,6 +470,7 @@ public final class InputMethodInfo implements Parcelable { mSubtypes = new InputMethodSubtypeArray(source); mHandledConfigChanges = source.readInt(); mSupportsStylusHandwriting = source.readBoolean(); + mSupportsConnectionlessStylusHandwriting = source.readBoolean(); mStylusHandwritingSettingsActivityAttr = source.readString8(); mForceDefault = false; } @@ -479,6 +487,7 @@ public final class InputMethodInfo implements Parcelable { false /* inlineSuggestionsEnabled */, false /* isVrOnly */, false /* isVirtualDeviceOnly */, 0 /* handledConfigChanges */, false /* supportsStylusHandwriting */, + false /* supportConnectionlessStylusHandwriting */, null /* stylusHandwritingSettingsActivityAttr */, false /* inlineSuggestionsEnabled */); } @@ -488,9 +497,11 @@ public final class InputMethodInfo implements Parcelable { * @hide */ @TestApi + @FlaggedApi(Flags.FLAG_CONNECTIONLESS_HANDWRITING) public InputMethodInfo(@NonNull String packageName, @NonNull String className, @NonNull CharSequence label, @NonNull String settingsActivity, @NonNull String languageSettingsActivity, boolean supportStylusHandwriting, + boolean supportConnectionlessStylusHandwriting, @NonNull String stylusHandwritingSettingsActivityAttr) { this(buildFakeResolveInfo(packageName, className, label), false /* isAuxIme */, settingsActivity, languageSettingsActivity, null /* subtypes */, @@ -498,8 +509,8 @@ public final class InputMethodInfo implements Parcelable { true /* supportsSwitchingToNextInputMethod */, false /* inlineSuggestionsEnabled */, false /* isVrOnly */, false /* isVirtualDeviceOnly */, 0 /* handledConfigChanges */, - supportStylusHandwriting, stylusHandwritingSettingsActivityAttr, - false /* inlineSuggestionsEnabled */); + supportStylusHandwriting, supportConnectionlessStylusHandwriting, + stylusHandwritingSettingsActivityAttr, false /* inlineSuggestionsEnabled */); } /** @@ -517,6 +528,7 @@ public final class InputMethodInfo implements Parcelable { false /* inlineSuggestionsEnabled */, false /* isVrOnly */, false /* isVirtualDeviceOnly */, handledConfigChanges, false /* supportsStylusHandwriting */, + false /* supportConnectionlessStylusHandwriting */, null /* stylusHandwritingSettingsActivityAttr */, false /* inlineSuggestionsEnabled */); } @@ -533,6 +545,7 @@ public final class InputMethodInfo implements Parcelable { true /* supportsSwitchingToNextInputMethod */, false /* inlineSuggestionsEnabled */, false /* isVrOnly */, false /* isVirtualDeviceOnly */, 0 /* handledconfigChanges */, false /* supportsStylusHandwriting */, + false /* supportConnectionlessStylusHandwriting */, null /* stylusHandwritingSettingsActivityAttr */, false /* inlineSuggestionsEnabled */); } @@ -549,6 +562,7 @@ public final class InputMethodInfo implements Parcelable { supportsSwitchingToNextInputMethod, false /* inlineSuggestionsEnabled */, isVrOnly, false /* isVirtualDeviceOnly */, 0 /* handledConfigChanges */, false /* supportsStylusHandwriting */, + false /* supportConnectionlessStylusHandwriting */, null /* stylusHandwritingSettingsActivityAttr */, false /* inlineSuggestionsEnabled */); } @@ -562,7 +576,8 @@ public final class InputMethodInfo implements Parcelable { int isDefaultResId, boolean forceDefault, boolean supportsSwitchingToNextInputMethod, boolean inlineSuggestionsEnabled, boolean isVrOnly, boolean isVirtualDeviceOnly, int handledConfigChanges, - boolean supportsStylusHandwriting, String stylusHandwritingSettingsActivityAttr, + boolean supportsStylusHandwriting, boolean supportsConnectionlessStylusHandwriting, + String stylusHandwritingSettingsActivityAttr, boolean supportsInlineSuggestionsWithTouchExploration) { final ServiceInfo si = ri.serviceInfo; mService = ri; @@ -583,6 +598,7 @@ public final class InputMethodInfo implements Parcelable { mIsVirtualDeviceOnly = isVirtualDeviceOnly; mHandledConfigChanges = handledConfigChanges; mSupportsStylusHandwriting = supportsStylusHandwriting; + mSupportsConnectionlessStylusHandwriting = supportsConnectionlessStylusHandwriting; mStylusHandwritingSettingsActivityAttr = stylusHandwritingSettingsActivityAttr; } @@ -763,6 +779,16 @@ public final class InputMethodInfo implements Parcelable { } /** + * Returns whether the IME supports connectionless stylus handwriting sessions. + * + * @attr ref android.R.styleable#InputMethod_supportsConnectionlessStylusHandwriting + */ + @FlaggedApi(Flags.FLAG_CONNECTIONLESS_HANDWRITING) + public boolean supportsConnectionlessStylusHandwriting() { + return mSupportsConnectionlessStylusHandwriting; + } + + /** * Returns {@link Intent} for stylus handwriting settings activity with * {@link Intent#getAction() Intent action} {@link #ACTION_STYLUS_HANDWRITING_SETTINGS} * if IME {@link #supportsStylusHandwriting() supports stylus handwriting}, else @@ -828,6 +854,8 @@ public final class InputMethodInfo implements Parcelable { + " mSuppressesSpellChecker=" + mSuppressesSpellChecker + " mShowInInputMethodPicker=" + mShowInInputMethodPicker + " mSupportsStylusHandwriting=" + mSupportsStylusHandwriting + + " mSupportsConnectionlessStylusHandwriting=" + + mSupportsConnectionlessStylusHandwriting + " mStylusHandwritingSettingsActivityAttr=" + mStylusHandwritingSettingsActivityAttr); pw.println(prefix + "mIsDefaultResId=0x" @@ -947,6 +975,7 @@ public final class InputMethodInfo implements Parcelable { mSubtypes.writeToParcel(dest); dest.writeInt(mHandledConfigChanges); dest.writeBoolean(mSupportsStylusHandwriting); + dest.writeBoolean(mSupportsConnectionlessStylusHandwriting); dest.writeString8(mStylusHandwritingSettingsActivityAttr); } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 3b07f27dc95a..f4b09df35705 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -38,6 +38,7 @@ import static android.view.inputmethod.InputMethodManagerProto.SERVED_VIEW; import static com.android.internal.inputmethod.StartInputReason.BOUND_TO_IMMS; import android.Manifest; +import android.annotation.CallbackExecutor; import android.annotation.DisplayContext; import android.annotation.DrawableRes; import android.annotation.DurationMillisLong; @@ -108,6 +109,7 @@ import android.window.WindowOnBackInvokedDispatcher; import com.android.internal.annotations.GuardedBy; import com.android.internal.inputmethod.DirectBootAwareness; +import com.android.internal.inputmethod.IConnectionlessHandwritingCallback; import com.android.internal.inputmethod.IInputMethodClient; import com.android.internal.inputmethod.IInputMethodSession; import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; @@ -134,6 +136,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; @@ -566,8 +569,15 @@ public final class InputMethodManager { @GuardedBy("mH") private PropertyInvalidatedCache<Integer, Boolean> mStylusHandwritingAvailableCache; + /** Cached value for {@link #isConnectionlessStylusHandwritingAvailable} for userId. */ + @GuardedBy("mH") + private PropertyInvalidatedCache<Integer, Boolean> + mConnectionlessStylusHandwritingAvailableCache; + private static final String CACHE_KEY_STYLUS_HANDWRITING_PROPERTY = "cache_key.system_server.stylus_handwriting"; + private static final String CACHE_KEY_CONNECTIONLESS_STYLUS_HANDWRITING_PROPERTY = + "cache_key.system_server.connectionless_stylus_handwriting"; @GuardedBy("mH") private int mCursorSelStart; @@ -691,6 +701,17 @@ public final class InputMethodManager { PropertyInvalidatedCache.invalidateCache(CACHE_KEY_STYLUS_HANDWRITING_PROPERTY); } + /** + * Calling this will invalidate the local connectionless stylus handwriting availability cache, + * which forces the next query in any process to recompute the cache. + * + * @hide + */ + public static void invalidateLocalConnectionlessStylusHandwritingAvailabilityCaches() { + PropertyInvalidatedCache.invalidateCache( + CACHE_KEY_CONNECTIONLESS_STYLUS_HANDWRITING_PROPERTY); + } + private static boolean isAutofillUIShowing(View servedView) { AutofillManager afm = servedView.getContext().getSystemService(AutofillManager.class); return afm != null && afm.isAutofillUiShowing(); @@ -1584,7 +1605,7 @@ public final class InputMethodManager { @Override public Boolean recompute(Integer userId) { return IInputMethodManagerGlobalInvoker.isStylusHandwritingAvailableAsUser( - userId); + userId, /* connectionless= */ false); } }; } @@ -1594,6 +1615,30 @@ public final class InputMethodManager { } /** + * Returns {@code true} if the currently selected IME supports connectionless stylus handwriting + * sessions and is enabled. + */ + @FlaggedApi(Flags.FLAG_CONNECTIONLESS_HANDWRITING) + public boolean isConnectionlessStylusHandwritingAvailable() { + if (ActivityThread.currentApplication() == null) { + return false; + } + synchronized (mH) { + if (mConnectionlessStylusHandwritingAvailableCache == null) { + mConnectionlessStylusHandwritingAvailableCache = new PropertyInvalidatedCache<>( + /* maxEntries= */ 4, CACHE_KEY_CONNECTIONLESS_STYLUS_HANDWRITING_PROPERTY) { + @Override + public Boolean recompute(@NonNull Integer userId) { + return IInputMethodManagerGlobalInvoker.isStylusHandwritingAvailableAsUser( + userId, /* connectionless= */ true); + } + }; + } + return mConnectionlessStylusHandwritingAvailableCache.query(UserHandle.myUserId()); + } + } + + /** * Returns the list of installed input methods for the specified user. * * <p>{@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} is required when and only when @@ -2433,6 +2478,127 @@ public final class InputMethodManager { } /** + * Starts a connectionless stylus handwriting session. A connectionless session differs from a + * regular stylus handwriting session in that the IME does not use an input connection to + * communicate with a text editor. Instead, the IME directly returns recognised handwritten text + * via a callback. + * + * <p>The {code cursorAnchorInfo} may be used by the IME to improve the handwriting recognition + * accuracy and user experience of the handwriting session. Usually connectionless handwriting + * is used for a view which appears like a text editor but does not really support text editing. + * For best results, the {code cursorAnchorInfo} should be populated as it would be for a real + * text editor (for example, the insertion marker location can be set to where the user would + * expect it to be, even if there is no visible cursor). + * + * @param view the view receiving stylus events + * @param cursorAnchorInfo positional information about the view receiving stylus events + * @param callbackExecutor the executor to run the callback on + * @param callback the callback to receive the result + */ + @FlaggedApi(Flags.FLAG_CONNECTIONLESS_HANDWRITING) + public void startConnectionlessStylusHandwriting(@NonNull View view, + @Nullable CursorAnchorInfo cursorAnchorInfo, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull ConnectionlessHandwritingCallback callback) { + startConnectionlessStylusHandwritingInternal( + view, cursorAnchorInfo, null, null, callbackExecutor, callback); + } + + /** + * Starts a connectionless stylus handwriting session (see {@link + * #startConnectionlessStylusHandwriting}) and additionally enables the recognised handwritten + * text to be later committed to a text editor using {@link + * #acceptStylusHandwritingDelegation(View)}. + * + * <p>After a connectionless session started using this method completes successfully, a text + * editor view, called the delegate view, may call {@link + * #acceptStylusHandwritingDelegation(View)} which will request the IME to commit the recognised + * handwritten text from the connectionless session to the delegate view. + * + * <p>The delegate view must belong to the same package as the delegator view for the delegation + * to succeed. If the delegate view belongs to a different package, use {@link + * #startConnectionlessStylusHandwritingForDelegation(View, CursorAnchorInfo, String, Executor, + * ConnectionlessHandwritingCallback)} instead. + * + * @param delegatorView the view receiving stylus events + * @param cursorAnchorInfo positional information about the view receiving stylus events + * @param callbackExecutor the executor to run the callback on + * @param callback the callback to receive the result + */ + @FlaggedApi(Flags.FLAG_CONNECTIONLESS_HANDWRITING) + public void startConnectionlessStylusHandwritingForDelegation(@NonNull View delegatorView, + @Nullable CursorAnchorInfo cursorAnchorInfo, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull ConnectionlessHandwritingCallback callback) { + String delegatorPackageName = delegatorView.getContext().getOpPackageName(); + startConnectionlessStylusHandwritingInternal(delegatorView, cursorAnchorInfo, + delegatorPackageName, delegatorPackageName, callbackExecutor, callback); + } + + /** + * Starts a connectionless stylus handwriting session (see {@link + * #startConnectionlessStylusHandwriting}) and additionally enables the recognised handwritten + * text to be later committed to a text editor using {@link + * #acceptStylusHandwritingDelegation(View, String)}. + * + * <p>After a connectionless session started using this method completes successfully, a text + * editor view, called the delegate view, may call {@link + * #acceptStylusHandwritingDelegation(View, String)} which will request the IME to commit the + * recognised handwritten text from the connectionless session to the delegate view. + * + * <p>The delegate view must belong to {@code delegatePackageName} for the delegation to + * succeed. + * + * @param delegatorView the view receiving stylus events + * @param cursorAnchorInfo positional information about the view receiving stylus events + * @param delegatePackageName name of the package containing the delegate view which will accept + * the delegation + * @param callbackExecutor the executor to run the callback on + * @param callback the callback to receive the result + */ + @FlaggedApi(Flags.FLAG_CONNECTIONLESS_HANDWRITING) + public void startConnectionlessStylusHandwritingForDelegation(@NonNull View delegatorView, + @Nullable CursorAnchorInfo cursorAnchorInfo, + @NonNull String delegatePackageName, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull ConnectionlessHandwritingCallback callback) { + Objects.requireNonNull(delegatePackageName); + String delegatorPackageName = delegatorView.getContext().getOpPackageName(); + startConnectionlessStylusHandwritingInternal(delegatorView, cursorAnchorInfo, + delegatorPackageName, delegatePackageName, callbackExecutor, callback); + } + + private void startConnectionlessStylusHandwritingInternal(@NonNull View view, + @Nullable CursorAnchorInfo cursorAnchorInfo, + @Nullable String delegatorPackageName, + @Nullable String delegatePackageName, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull ConnectionlessHandwritingCallback callback) { + Objects.requireNonNull(view); + Objects.requireNonNull(callbackExecutor); + Objects.requireNonNull(callback); + // Re-dispatch if there is a context mismatch. + final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); + if (fallbackImm != null) { + fallbackImm.startConnectionlessStylusHandwritingInternal(view, cursorAnchorInfo, + delegatorPackageName, delegatePackageName, callbackExecutor, callback); + } + + checkFocus(); + synchronized (mH) { + if (view.getViewRootImpl() != mCurRootView) { + Log.w(TAG, "Ignoring startConnectionlessStylusHandwriting: " + + "View's window does not have focus."); + return; + } + IInputMethodManagerGlobalInvoker.startConnectionlessStylusHandwriting( + mClient, UserHandle.myUserId(), cursorAnchorInfo, + delegatePackageName, delegatorPackageName, + new ConnectionlessHandwritingCallbackProxy(callbackExecutor, callback)); + } + } + + /** * Prepares delegation of starting stylus handwriting session to a different editor in same * or different window than the view on which initial handwriting stroke was detected. * @@ -2511,12 +2677,18 @@ public final class InputMethodManager { * {@link #acceptStylusHandwritingDelegation(View, String)} instead.</p> * * @param delegateView delegate view capable of receiving input via {@link InputConnection} - * on which {@link #startStylusHandwriting(View)} will be called. * @return {@code true} if view belongs to same application package as used in - * {@link #prepareStylusHandwritingDelegation(View)} and handwriting session can start. - * @see #acceptStylusHandwritingDelegation(View, String) + * {@link #prepareStylusHandwritingDelegation(View)} and delegation is accepted * @see #prepareStylusHandwritingDelegation(View) + * @see #acceptStylusHandwritingDelegation(View, String) */ + // TODO(b/300979854): Once connectionless APIs are finalised, update documentation to add: + // <p>Otherwise, if the delegator view previously started delegation using {@link + // #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, CursorAnchorInfo)}, + // requests the IME to commit the recognised handwritten text from the connectionless session to + // the delegate view. + // @see #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, + // CursorAnchorInfo) public boolean acceptStylusHandwritingDelegation(@NonNull View delegateView) { return startStylusHandwritingInternal( delegateView, delegateView.getContext().getOpPackageName(), @@ -2533,13 +2705,19 @@ public final class InputMethodManager { * {@link #acceptStylusHandwritingDelegation(View)} instead.</p> * * @param delegateView delegate view capable of receiving input via {@link InputConnection} - * on which {@link #startStylusHandwriting(View)} will be called. * @param delegatorPackageName package name of the delegator that handled initial stylus stroke. - * @return {@code true} if view belongs to allowed delegate package declared in - * {@link #prepareStylusHandwritingDelegation(View, String)} and handwriting session can start. + * @return {@code true} if view belongs to allowed delegate package declared in {@link + * #prepareStylusHandwritingDelegation(View, String)} and delegation is accepted * @see #prepareStylusHandwritingDelegation(View, String) * @see #acceptStylusHandwritingDelegation(View) */ + // TODO(b/300979854): Once connectionless APIs are finalised, update documentation to add: + // <p>Otherwise, if the delegator view previously started delegation using {@link + // #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, CursorAnchorInfo, + // String)}, requests the IME to commit the recognised handwritten text from the connectionless + // session to the delegate view. + // @see #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, + // CursorAnchorInfo, String) public boolean acceptStylusHandwritingDelegation( @NonNull View delegateView, @NonNull String delegatorPackageName) { Objects.requireNonNull(delegatorPackageName); @@ -2556,15 +2734,21 @@ public final class InputMethodManager { * <p>Note: If delegator and delegate are in the same application package, use {@link * #acceptStylusHandwritingDelegation(View)} instead. * - * @param delegateView delegate view capable of receiving input via {@link InputConnection} on - * which {@link #startStylusHandwriting(View)} will be called. + * @param delegateView delegate view capable of receiving input via {@link InputConnection} * @param delegatorPackageName package name of the delegator that handled initial stylus stroke. * @param flags {@link #HANDWRITING_DELEGATE_FLAG_HOME_DELEGATOR_ALLOWED} or {@code 0} * @return {@code true} if view belongs to allowed delegate package declared in {@link - * #prepareStylusHandwritingDelegation(View, String)} and handwriting session can start. + * #prepareStylusHandwritingDelegation(View, String)} and delegation is accepted * @see #prepareStylusHandwritingDelegation(View, String) * @see #acceptStylusHandwritingDelegation(View) */ + // TODO(b/300979854): Once connectionless APIs are finalised, update documentation to add: + // <p>Otherwise, if the delegator view previously started delegation using {@link + // #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, CursorAnchorInfo, + // String)}, requests the IME to commit the recognised handwritten text from the connectionless + // session to the delegate view. + // @see #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, + // CursorAnchorInfo, String) @FlaggedApi(FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR) public boolean acceptStylusHandwritingDelegation( @NonNull View delegateView, @NonNull String delegatorPackageName, @@ -4315,6 +4499,73 @@ public final class InputMethodManager { public void onFinishedInputEvent(Object token, boolean handled); } + private static class ConnectionlessHandwritingCallbackProxy + extends IConnectionlessHandwritingCallback.Stub { + private final Object mLock = new Object(); + + @Nullable + @GuardedBy("mLock") + private Executor mExecutor; + + @Nullable + @GuardedBy("mLock") + private ConnectionlessHandwritingCallback mCallback; + + ConnectionlessHandwritingCallbackProxy( + @NonNull Executor executor, @NonNull ConnectionlessHandwritingCallback callback) { + mExecutor = executor; + mCallback = callback; + } + + @Override + public void onResult(CharSequence text) { + Executor executor; + ConnectionlessHandwritingCallback callback; + synchronized (mLock) { + if (mExecutor == null || mCallback == null) { + return; + } + executor = mExecutor; + callback = mCallback; + mExecutor = null; + mCallback = null; + } + final long identity = Binder.clearCallingIdentity(); + try { + if (TextUtils.isEmpty(text)) { + executor.execute(() -> callback.onError( + ConnectionlessHandwritingCallback + .CONNECTIONLESS_HANDWRITING_ERROR_NO_TEXT_RECOGNIZED)); + } else { + executor.execute(() -> callback.onResult(text)); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void onError(int errorCode) { + Executor executor; + ConnectionlessHandwritingCallback callback; + synchronized (mLock) { + if (mExecutor == null || mCallback == null) { + return; + } + executor = mExecutor; + callback = mCallback; + mExecutor = null; + mCallback = null; + } + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> callback.onError(errorCode)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + private final class ImeInputEventSender extends InputEventSender { public ImeInputEventSender(InputChannel inputChannel, Looper looper) { super(inputChannel, looper); diff --git a/core/java/android/webkit/IWebViewUpdateService.aidl b/core/java/android/webkit/IWebViewUpdateService.aidl index c6bd20cec07d..aeb746cbbe5e 100644 --- a/core/java/android/webkit/IWebViewUpdateService.aidl +++ b/core/java/android/webkit/IWebViewUpdateService.aidl @@ -45,6 +45,7 @@ interface IWebViewUpdateService { * it would then try to update the provider to such a package while in reality the update * service would switch to another one. */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)") String changeProviderAndSetting(String newProvider); /** diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index f5b81b027134..f336b5d8a727 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -3087,14 +3087,22 @@ public class WebView extends AbsoluteLayout return webviewPackage; } - IWebViewUpdateService service = WebViewFactory.getUpdateService(); - if (service == null) { - return null; - } - try { - return service.getCurrentWebViewPackage(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (Flags.updateServiceIpcWrapper()) { + WebViewUpdateManager manager = WebViewUpdateManager.getInstance(); + if (manager == null) { + return null; + } + return manager.getCurrentWebViewPackage(); + } else { + IWebViewUpdateService service = WebViewFactory.getUpdateService(); + if (service == null) { + return null; + } + try { + return service.getCurrentWebViewPackage(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } diff --git a/core/java/android/webkit/WebViewBootstrapFrameworkInitializer.java b/core/java/android/webkit/WebViewBootstrapFrameworkInitializer.java new file mode 100644 index 000000000000..9b15ab391032 --- /dev/null +++ b/core/java/android/webkit/WebViewBootstrapFrameworkInitializer.java @@ -0,0 +1,45 @@ +/* + * 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.webkit; + +import android.annotation.FlaggedApi; +import android.annotation.SystemApi; +import android.app.SystemServiceRegistry; +import android.content.Context; + +/** + * Class for performing registration for webviewupdate service. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_UPDATE_SERVICE_IPC_WRAPPER) +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) +public class WebViewBootstrapFrameworkInitializer { + private WebViewBootstrapFrameworkInitializer() {} + + /** + * Called by {@link SystemServiceRegistry}'s static initializer and registers webviewupdate + * service to {@link Context}, so that {@link Context#getSystemService} can return them. + * + * @throws IllegalStateException if this is called from anywhere besides + * {@link SystemServiceRegistry} + */ + public static void registerServiceWrappers() { + SystemServiceRegistry.registerForeverStaticService(Context.WEBVIEW_UPDATE_SERVICE, + WebViewUpdateManager.class, + (b) -> new WebViewUpdateManager(IWebViewUpdateService.Stub.asInterface(b))); + } +} diff --git a/core/java/android/webkit/WebViewDelegate.java b/core/java/android/webkit/WebViewDelegate.java index 8e89541647c3..3fc0a305c8e6 100644 --- a/core/java/android/webkit/WebViewDelegate.java +++ b/core/java/android/webkit/WebViewDelegate.java @@ -16,8 +16,6 @@ package android.webkit; -import static android.webkit.Flags.updateServiceV2; - import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; @@ -207,7 +205,12 @@ public final class WebViewDelegate { * Returns whether WebView should run in multiprocess mode. */ public boolean isMultiProcessEnabled() { - if (updateServiceV2()) { + if (Flags.updateServiceV2()) { + return true; + } else if (Flags.updateServiceIpcWrapper()) { + // We don't want to support this method in the new wrapper because updateServiceV2 is + // intended to ship in the same release (or sooner). It's only possible to disable it + // with an obscure adb command, so just return true here too. return true; } try { diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java index 53b047a17f6d..c748a57dce74 100644 --- a/core/java/android/webkit/WebViewFactory.java +++ b/core/java/android/webkit/WebViewFactory.java @@ -285,10 +285,16 @@ public final class WebViewFactory { return LIBLOAD_WRONG_PACKAGE_NAME; } + Application initialApplication = AppGlobals.getInitialApplication(); WebViewProviderResponse response = null; try { - response = getUpdateService().waitForAndGetProvider(); - } catch (RemoteException e) { + if (Flags.updateServiceIpcWrapper()) { + response = initialApplication.getSystemService(WebViewUpdateManager.class) + .waitForAndGetProvider(); + } else { + response = getUpdateService().waitForAndGetProvider(); + } + } catch (Exception e) { Log.e(LOGTAG, "error waiting for relro creation", e); return LIBLOAD_FAILED_WAITING_FOR_WEBVIEW_REASON_UNKNOWN; } @@ -302,7 +308,7 @@ public final class WebViewFactory { return LIBLOAD_WRONG_PACKAGE_NAME; } - PackageManager packageManager = AppGlobals.getInitialApplication().getPackageManager(); + PackageManager packageManager = initialApplication.getPackageManager(); String libraryFileName; try { PackageInfo packageInfo = packageManager.getPackageInfo(packageName, @@ -436,7 +442,12 @@ public final class WebViewFactory { Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewUpdateService.waitForAndGetProvider()"); try { - response = getUpdateService().waitForAndGetProvider(); + if (Flags.updateServiceIpcWrapper()) { + response = initialApplication.getSystemService(WebViewUpdateManager.class) + .waitForAndGetProvider(); + } else { + response = getUpdateService().waitForAndGetProvider(); + } } finally { Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW); } diff --git a/core/java/android/webkit/WebViewLibraryLoader.java b/core/java/android/webkit/WebViewLibraryLoader.java index 91412d7e8631..a68a5778de82 100644 --- a/core/java/android/webkit/WebViewLibraryLoader.java +++ b/core/java/android/webkit/WebViewLibraryLoader.java @@ -24,7 +24,6 @@ import android.content.Context; import android.content.pm.PackageInfo; import android.os.Build; import android.os.Process; -import android.os.RemoteException; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; @@ -87,8 +86,12 @@ public class WebViewLibraryLoader { } finally { // We must do our best to always notify the update service, even if something fails. try { - WebViewFactory.getUpdateServiceUnchecked().notifyRelroCreationCompleted(); - } catch (RemoteException e) { + if (Flags.updateServiceIpcWrapper()) { + WebViewUpdateManager.getInstance().notifyRelroCreationCompleted(); + } else { + WebViewFactory.getUpdateServiceUnchecked().notifyRelroCreationCompleted(); + } + } catch (Exception e) { Log.e(LOGTAG, "error notifying update service", e); } @@ -114,8 +117,12 @@ public class WebViewLibraryLoader { public void run() { try { Log.e(LOGTAG, "relro file creator for " + abi + " crashed. Proceeding without"); - WebViewFactory.getUpdateService().notifyRelroCreationCompleted(); - } catch (RemoteException e) { + if (Flags.updateServiceIpcWrapper()) { + WebViewUpdateManager.getInstance().notifyRelroCreationCompleted(); + } else { + WebViewFactory.getUpdateService().notifyRelroCreationCompleted(); + } + } catch (Exception e) { Log.e(LOGTAG, "Cannot reach WebViewUpdateService. " + e.getMessage()); } } diff --git a/core/java/android/webkit/WebViewProviderResponse.java b/core/java/android/webkit/WebViewProviderResponse.java index 02e48ddb6638..84e34a34f7f7 100644 --- a/core/java/android/webkit/WebViewProviderResponse.java +++ b/core/java/android/webkit/WebViewProviderResponse.java @@ -16,17 +16,42 @@ package android.webkit; +import android.annotation.FlaggedApi; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.PackageInfo; import android.os.Parcel; import android.os.Parcelable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** @hide */ +@FlaggedApi(Flags.FLAG_UPDATE_SERVICE_IPC_WRAPPER) +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) public final class WebViewProviderResponse implements Parcelable { - public WebViewProviderResponse(PackageInfo packageInfo, int status) { + @IntDef( + prefix = {"STATUS_"}, + value = { + STATUS_SUCCESS, + STATUS_FAILED_WAITING_FOR_RELRO, + STATUS_FAILED_LISTING_WEBVIEW_PACKAGES, + }) + @Retention(RetentionPolicy.SOURCE) + private @interface WebViewProviderStatus {} + + public static final int STATUS_SUCCESS = WebViewFactory.LIBLOAD_SUCCESS; + public static final int STATUS_FAILED_WAITING_FOR_RELRO = + WebViewFactory.LIBLOAD_FAILED_WAITING_FOR_RELRO; + public static final int STATUS_FAILED_LISTING_WEBVIEW_PACKAGES = + WebViewFactory.LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES; + + public WebViewProviderResponse( + @Nullable PackageInfo packageInfo, @WebViewProviderStatus int status) { this.packageInfo = packageInfo; this.status = status; } @@ -54,13 +79,11 @@ public final class WebViewProviderResponse implements Parcelable { } @Override - public void writeToParcel(Parcel out, int flags) { + public void writeToParcel(@NonNull Parcel out, int flags) { out.writeTypedObject(packageInfo, flags); out.writeInt(status); } - @UnsupportedAppUsage - @Nullable - public final PackageInfo packageInfo; - public final int status; + @UnsupportedAppUsage public final @Nullable PackageInfo packageInfo; + public final @WebViewProviderStatus int status; } diff --git a/core/java/android/webkit/WebViewUpdateManager.java b/core/java/android/webkit/WebViewUpdateManager.java new file mode 100644 index 000000000000..8ada598d8a76 --- /dev/null +++ b/core/java/android/webkit/WebViewUpdateManager.java @@ -0,0 +1,170 @@ +/* + * 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.webkit; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.app.SystemServiceRegistry; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.os.RemoteException; + +/** @hide */ +@FlaggedApi(Flags.FLAG_UPDATE_SERVICE_IPC_WRAPPER) +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) +public final class WebViewUpdateManager { + private final IWebViewUpdateService mService; + + /** @hide */ + public WebViewUpdateManager(@NonNull IWebViewUpdateService service) { + mService = service; + } + + /** + * Get the singleton instance of the manager. + * + * This exists for the benefit of callsites without a {@link Context}; prefer + * {@link Context#getSystemService(Class)} otherwise. + */ + @SuppressLint("ManagerLookup") // service opts in to getSystemServiceWithNoContext() + public static @Nullable WebViewUpdateManager getInstance() { + return (WebViewUpdateManager) SystemServiceRegistry.getSystemServiceWithNoContext( + Context.WEBVIEW_UPDATE_SERVICE); + } + + /** + * Block until system-level WebView preparations are complete. + * + * This also makes the current WebView provider package visible to the caller. + * + * @return the status of WebView preparation and the current provider package. + */ + public @NonNull WebViewProviderResponse waitForAndGetProvider() { + try { + return mService.waitForAndGetProvider(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get the package that is the system's current WebView implementation. + * + * @return the package, or null if no valid implementation is present. + */ + public @Nullable PackageInfo getCurrentWebViewPackage() { + try { + return mService.getCurrentWebViewPackage(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get the complete list of supported WebView providers for this device. + * + * This includes all configured providers, regardless of whether they are currently available + * or valid. + */ + @SuppressLint({"ParcelableList", "ArrayReturn"}) + public @NonNull WebViewProviderInfo[] getAllWebViewPackages() { + try { + return mService.getAllWebViewPackages(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get the list of currently-valid WebView providers for this device. + * + * This only includes providers that are currently present on the device and meet the validity + * criteria (signature, version, etc), but does not check if the provider is installed and + * enabled for all users. + */ + @SuppressLint({"ParcelableList", "ArrayReturn"}) + public @NonNull WebViewProviderInfo[] getValidWebViewPackages() { + try { + return mService.getValidWebViewPackages(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get the package name of the current WebView implementation. + * + * @return the package name, or null if no valid implementation is present. + */ + public @Nullable String getCurrentWebViewPackageName() { + try { + return mService.getCurrentWebViewPackageName(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Ask the system to switch to a specific WebView implementation if possible. + * + * This choice will be stored persistently. + * + * @param newProvider the package name to use, or null to reset to default. + * @return the package name which is now in use, which may not be the + * requested one if it was not usable. + */ + @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + public @Nullable String changeProviderAndSetting(@NonNull String newProvider) { + try { + return mService.changeProviderAndSetting(newProvider); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Used by the relro file creator to notify the service that it's done. + * @hide + */ + void notifyRelroCreationCompleted() { + try { + mService.notifyRelroCreationCompleted(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get the WebView provider which will be used if no explicit choice has been made. + * + * The default provider is not guaranteed to be currently valid/usable. + * + * @return the default WebView provider. + */ + @FlaggedApi(Flags.FLAG_UPDATE_SERVICE_V2) + public @NonNull WebViewProviderInfo getDefaultWebViewPackage() { + try { + return mService.getDefaultWebViewPackage(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/core/java/android/webkit/WebViewUpdateService.java b/core/java/android/webkit/WebViewUpdateService.java index 9152b438618f..6f53ddeafef1 100644 --- a/core/java/android/webkit/WebViewUpdateService.java +++ b/core/java/android/webkit/WebViewUpdateService.java @@ -33,14 +33,22 @@ public final class WebViewUpdateService { * Fetch all packages that could potentially implement WebView. */ public static WebViewProviderInfo[] getAllWebViewPackages() { - IWebViewUpdateService service = getUpdateService(); - if (service == null) { - return new WebViewProviderInfo[0]; - } - try { - return service.getAllWebViewPackages(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (Flags.updateServiceIpcWrapper()) { + WebViewUpdateManager manager = WebViewUpdateManager.getInstance(); + if (manager == null) { + return new WebViewProviderInfo[0]; + } + return manager.getAllWebViewPackages(); + } else { + IWebViewUpdateService service = getUpdateService(); + if (service == null) { + return new WebViewProviderInfo[0]; + } + try { + return service.getAllWebViewPackages(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } @@ -48,14 +56,22 @@ public final class WebViewUpdateService { * Fetch all packages that could potentially implement WebView and are currently valid. */ public static WebViewProviderInfo[] getValidWebViewPackages() { - IWebViewUpdateService service = getUpdateService(); - if (service == null) { - return new WebViewProviderInfo[0]; - } - try { - return service.getValidWebViewPackages(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (Flags.updateServiceIpcWrapper()) { + WebViewUpdateManager manager = WebViewUpdateManager.getInstance(); + if (manager == null) { + return new WebViewProviderInfo[0]; + } + return manager.getValidWebViewPackages(); + } else { + IWebViewUpdateService service = getUpdateService(); + if (service == null) { + return new WebViewProviderInfo[0]; + } + try { + return service.getValidWebViewPackages(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } @@ -63,14 +79,22 @@ public final class WebViewUpdateService { * Used by DevelopmentSetting to get the name of the WebView provider currently in use. */ public static String getCurrentWebViewPackageName() { - IWebViewUpdateService service = getUpdateService(); - if (service == null) { - return null; - } - try { - return service.getCurrentWebViewPackageName(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (Flags.updateServiceIpcWrapper()) { + WebViewUpdateManager manager = WebViewUpdateManager.getInstance(); + if (manager == null) { + return null; + } + return manager.getCurrentWebViewPackageName(); + } else { + IWebViewUpdateService service = getUpdateService(); + if (service == null) { + return null; + } + try { + return service.getCurrentWebViewPackageName(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } diff --git a/core/java/android/window/BackProgressAnimator.java b/core/java/android/window/BackProgressAnimator.java index e7280d0e7867..40e28cbbbd05 100644 --- a/core/java/android/window/BackProgressAnimator.java +++ b/core/java/android/window/BackProgressAnimator.java @@ -17,6 +17,7 @@ package android.window; import android.annotation.NonNull; +import android.annotation.Nullable; import android.util.FloatProperty; import com.android.internal.dynamicanimation.animation.DynamicAnimation; @@ -44,6 +45,14 @@ public class BackProgressAnimator { private float mProgress = 0; private BackMotionEvent mLastBackEvent; private boolean mBackAnimationInProgress = false; + @Nullable + private Runnable mBackCancelledFinishRunnable; + private final DynamicAnimation.OnAnimationEndListener mOnAnimationEndListener = + (animation, canceled, value, velocity) -> { + invokeBackCancelledRunnable(); + reset(); + }; + private void setProgress(float progress) { mProgress = progress; @@ -116,6 +125,11 @@ public class BackProgressAnimator { * Resets the back progress animation. This should be called when back is invoked or cancelled. */ public void reset() { + if (mBackCancelledFinishRunnable != null) { + // Ensure that last progress value that apps see is 0 + updateProgressValue(0); + invokeBackCancelledRunnable(); + } mSpring.animateToFinalPosition(0); if (mSpring.canSkipToEnd()) { mSpring.skipToEnd(); @@ -136,17 +150,8 @@ public class BackProgressAnimator { * @param finishCallback the callback to be invoked when the progress is reach to 0. */ public void onBackCancelled(@NonNull Runnable finishCallback) { - final DynamicAnimation.OnAnimationEndListener listener = - new DynamicAnimation.OnAnimationEndListener() { - @Override - public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value, - float velocity) { - mSpring.removeEndListener(this); - finishCallback.run(); - reset(); - } - }; - mSpring.addEndListener(listener); + mBackCancelledFinishRunnable = finishCallback; + mSpring.addEndListener(mOnAnimationEndListener); mSpring.animateToFinalPosition(0); } @@ -164,4 +169,10 @@ public class BackProgressAnimator { progress / SCALE_FACTOR, mLastBackEvent.getSwipeEdge())); } -} + private void invokeBackCancelledRunnable() { + mSpring.removeEndListener(mOnAnimationEndListener); + mBackCancelledFinishRunnable.run(); + mBackCancelledFinishRunnable = null; + } + +}
\ No newline at end of file diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index 5c911f4a632a..45d7767380a1 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -371,11 +371,11 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { } final OnBackAnimationCallback callback = getBackAnimationCallback(); if (callback != null) { + mProgressAnimator.reset(); callback.onBackStarted(new BackEvent( backEvent.getTouchX(), backEvent.getTouchY(), backEvent.getProgress(), backEvent.getSwipeEdge())); - mProgressAnimator.onBackStarted(backEvent, event -> - callback.onBackProgressed(event)); + mProgressAnimator.onBackStarted(backEvent, callback::onBackProgressed); } }); } diff --git a/core/java/com/android/internal/app/IVisualQueryDetectionAttentionListener.aidl b/core/java/com/android/internal/app/IVisualQueryDetectionAttentionListener.aidl index 3e48da72c131..eeaa3efa1014 100644 --- a/core/java/com/android/internal/app/IVisualQueryDetectionAttentionListener.aidl +++ b/core/java/com/android/internal/app/IVisualQueryDetectionAttentionListener.aidl @@ -16,6 +16,8 @@ package com.android.internal.app; +import android.service.voice.VisualQueryAttentionResult; + /** * Allows sysui to notify users the assistant is ready to take a query without notifying the * assistant app. @@ -24,10 +26,10 @@ oneway interface IVisualQueryDetectionAttentionListener { /** * Called when attention signal is sent. */ - void onAttentionGained(); + void onAttentionGained(in VisualQueryAttentionResult attentionResult); /** - * Called when a attention signal is lost. + * Called when a attention signal is lost for a certain interaction intention. */ - void onAttentionLost(); + void onAttentionLost(int interactionIntention); }
\ No newline at end of file diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/FakeSceneModule.kt b/core/java/com/android/internal/inputmethod/IConnectionlessHandwritingCallback.aidl index 5d22a6e10a26..e564599e046f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/FakeSceneModule.kt +++ b/core/java/com/android/internal/inputmethod/IConnectionlessHandwritingCallback.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * 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. @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.scene -import com.android.systemui.scene.shared.flag.FakeSceneContainerFlagsModule -import com.android.systemui.scene.shared.model.FakeSceneContainerConfigModule -import dagger.Module +package com.android.internal.inputmethod; -@Module(includes = [FakeSceneContainerConfigModule::class, FakeSceneContainerFlagsModule::class]) -object FakeSceneModule +/** Binder interface to receive a result from a connectionless stylus handwriting session. */ +oneway interface IConnectionlessHandwritingCallback { + void onResult(in CharSequence text); + void onError(int errorCode); +} 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/protolog/OWNERS b/core/java/com/android/internal/protolog/OWNERS new file mode 100644 index 000000000000..18cf2be9f7df --- /dev/null +++ b/core/java/com/android/internal/protolog/OWNERS @@ -0,0 +1,3 @@ +# ProtoLog owners +# Bug component: 1157642 +include platform/development:/tools/winscope/OWNERS diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 595bf3b8eb97..e95127be8543 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -17,12 +17,14 @@ package com.android.internal.view; import android.os.ResultReceiver; +import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.ImeTracker; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import android.view.inputmethod.EditorInfo; import android.window.ImeOnBackInvokedDispatcher; +import com.android.internal.inputmethod.IConnectionlessHandwritingCallback; import com.android.internal.inputmethod.IImeTracker; import com.android.internal.inputmethod.IInputMethodClient; import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; @@ -144,6 +146,9 @@ interface IInputMethodManager { /** Start Stylus handwriting session **/ void startStylusHandwriting(in IInputMethodClient client); + oneway void startConnectionlessStylusHandwriting(in IInputMethodClient client, int userId, + in CursorAnchorInfo cursorAnchorInfo, in String delegatePackageName, + in String delegatorPackageName, in IConnectionlessHandwritingCallback callback); /** Prepares delegation of starting stylus handwriting session to a different editor **/ void prepareStylusHandwritingDelegation(in IInputMethodClient client, @@ -158,7 +163,7 @@ interface IInputMethodManager { /** Returns {@code true} if currently selected IME supports Stylus handwriting. */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)") - boolean isStylusHandwritingAvailableAsUser(int userId); + boolean isStylusHandwritingAvailableAsUser(int userId, boolean connectionless); /** add virtual stylus id for test Stylus handwriting session **/ @EnforcePermission("TEST_INPUT_METHOD") diff --git a/core/jni/LayoutlibLoader.cpp b/core/jni/LayoutlibLoader.cpp index 200ddefc3bbc..01e9f437a79f 100644 --- a/core/jni/LayoutlibLoader.cpp +++ b/core/jni/LayoutlibLoader.cpp @@ -416,11 +416,17 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) { env->NewStringUTF("icu.data.path"), env->NewStringUTF("")); const char* path = env->GetStringUTFChars(stringPath, 0); - bool icuInitialized = init_icu(path); - env->ReleaseStringUTFChars(stringPath, path); - if (!icuInitialized) { - return JNI_ERR; + + if (strcmp(path, "**n/a**") != 0) { + bool icuInitialized = init_icu(path); + if (!icuInitialized) { + fprintf(stderr, "Failed to initialize ICU\n"); + return JNI_ERR; + } + } else { + fprintf(stderr, "Skip initializing ICU\n"); } + env->ReleaseStringUTFChars(stringPath, path); jstring useJniProperty = (jstring)env->CallStaticObjectMethod(system, getPropertyMethod, @@ -449,12 +455,18 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) { // Use English locale for number format to ensure correct parsing of floats when using strtof setlocale(LC_NUMERIC, "en_US.UTF-8"); - auto keyboardPathsString = + auto keyboardPathsJString = (jstring)env->CallStaticObjectMethod(system, getPropertyMethod, env->NewStringUTF("keyboard_paths"), env->NewStringUTF("")); - vector<string> keyboardPaths = parseCsv(env, keyboardPathsString); - init_keyboard(env, keyboardPaths); + const char* keyboardPathsString = env->GetStringUTFChars(keyboardPathsJString, 0); + if (strcmp(keyboardPathsString, "**n/a**") != 0) { + vector<string> keyboardPaths = parseCsv(env, keyboardPathsJString); + init_keyboard(env, keyboardPaths); + } else { + fprintf(stderr, "Skip initializing keyboard\n"); + } + env->ReleaseStringUTFChars(keyboardPathsJString, keyboardPathsString); return JNI_VERSION_1_6; } diff --git a/core/jni/android_os_VintfObject.cpp b/core/jni/android_os_VintfObject.cpp index ce4a33735c6d..8dc9d0aa578e 100644 --- a/core/jni/android_os_VintfObject.cpp +++ b/core/jni/android_os_VintfObject.cpp @@ -39,6 +39,7 @@ using vintf::CompatibilityMatrix; using vintf::HalManifest; using vintf::Level; using vintf::SchemaType; +using vintf::SepolicyVersion; using vintf::to_string; using vintf::toXml; using vintf::Version; @@ -139,7 +140,7 @@ static jstring android_os_VintfObject_getPlatformSepolicyVersion(JNIEnv* env, jc return nullptr; } - Version latest; + SepolicyVersion latest; for (const auto& range : versions) { latest = std::max(latest, range.maxVer()); } diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp index 3d0ab4ef0981..3ee15ab734b9 100644 --- a/core/jni/android_util_AssetManager.cpp +++ b/core/jni/android_util_AssetManager.cpp @@ -314,8 +314,7 @@ static void NativeDestroy(JNIEnv* /*env*/, jclass /*clazz*/, jlong ptr) { } static void NativeSetApkAssets(JNIEnv* env, jclass /*clazz*/, jlong ptr, - jobjectArray apk_assets_array, jboolean invalidate_caches, - jboolean preset) { + jobjectArray apk_assets_array, jboolean invalidate_caches) { ATRACE_NAME("AssetManager::SetApkAssets"); const jsize apk_assets_len = env->GetArrayLength(apk_assets_array); @@ -344,11 +343,7 @@ static void NativeSetApkAssets(JNIEnv* env, jclass /*clazz*/, jlong ptr, } auto assetmanager = LockAndStartAssetManager(ptr); - if (preset) { - assetmanager->PresetApkAssets(apk_assets); - } else { - assetmanager->SetApkAssets(apk_assets, invalidate_caches); - } + assetmanager->SetApkAssets(apk_assets, invalidate_caches); } static void NativeSetConfiguration(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint mcc, jint mnc, @@ -358,7 +353,7 @@ static void NativeSetConfiguration(JNIEnv* env, jclass /*clazz*/, jlong ptr, jin jint screen_height, jint smallest_screen_width_dp, jint screen_width_dp, jint screen_height_dp, jint screen_layout, jint ui_mode, jint color_mode, jint grammatical_gender, - jint major_version, jboolean force_refresh) { + jint major_version) { ATRACE_NAME("AssetManager::SetConfiguration"); const jsize locale_count = (locales == NULL) ? 0 : env->GetArrayLength(locales); @@ -418,7 +413,7 @@ static void NativeSetConfiguration(JNIEnv* env, jclass /*clazz*/, jlong ptr, jin } auto assetmanager = LockAndStartAssetManager(ptr); - assetmanager->SetConfigurations(std::move(configs), force_refresh != JNI_FALSE); + assetmanager->SetConfigurations(configs); assetmanager->SetDefaultLocale(default_locale_int); } @@ -1527,8 +1522,8 @@ static const JNINativeMethod gAssetManagerMethods[] = { // AssetManager setup methods. {"nativeCreate", "()J", (void*)NativeCreate}, {"nativeDestroy", "(J)V", (void*)NativeDestroy}, - {"nativeSetApkAssets", "(J[Landroid/content/res/ApkAssets;ZZ)V", (void*)NativeSetApkAssets}, - {"nativeSetConfiguration", "(JIILjava/lang/String;[Ljava/lang/String;IIIIIIIIIIIIIIIIZ)V", + {"nativeSetApkAssets", "(J[Landroid/content/res/ApkAssets;Z)V", (void*)NativeSetApkAssets}, + {"nativeSetConfiguration", "(JIILjava/lang/String;[Ljava/lang/String;IIIIIIIIIIIIIIII)V", (void*)NativeSetConfiguration}, {"nativeGetAssignedPackageIdentifiers", "(JZZ)Landroid/util/SparseArray;", (void*)NativeGetAssignedPackageIdentifiers}, diff --git a/core/proto/OWNERS b/core/proto/OWNERS index c65794e01ceb..b900fa60ff70 100644 --- a/core/proto/OWNERS +++ b/core/proto/OWNERS @@ -22,7 +22,6 @@ per-file android/content/intent.proto = file:/PACKAGE_MANAGER_OWNERS per-file android/hardware/location/context_hub_info.proto = file:/services/core/java/com/android/server/location/contexthub/OWNERS # Biometrics -jaggies@google.com jbolinger@google.com # Launcher diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index c62e53646aa7..4fc9b40465ee 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -144,6 +144,7 @@ message SecureSettingsProto { optional SettingProto long_press_home_enabled = 11 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto search_press_hold_nav_handle_enabled = 12 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto search_long_press_home_enabled = 13 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto visual_query_accessibility_detection_enabled = 14 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Assist assist = 7; diff --git a/core/res/Android.bp b/core/res/Android.bp index 34c404520a2b..277824cbb4c8 100644 --- a/core/res/Android.bp +++ b/core/res/Android.bp @@ -154,6 +154,14 @@ android_app { }, generate_product_characteristics_rro: true, + + flags_packages: [ + "android.content.pm.flags-aconfig", + "android.provider.flags-aconfig", + "camera_platform_flags", + "com.android.net.flags-aconfig", + "com.android.window.flags.window-aconfig", + ], } java_genrule { diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index a2ce212a5a53..04367e7d0534 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -835,6 +835,7 @@ <!-- Added in V --> <protected-broadcast android:name="android.intent.action.PROFILE_AVAILABLE" /> <protected-broadcast android:name="android.intent.action.PROFILE_UNAVAILABLE" /> + <protected-broadcast android:name="android.app.action.CONSOLIDATED_NOTIFICATION_POLICY_CHANGED" /> <!-- ====================================================================== --> <!-- RUNTIME PERMISSIONS --> @@ -892,7 +893,8 @@ android:permissionGroup="android.permission-group.UNDEFINED" android:label="@string/permlab_writeVerificationStateE2eeContactKeys" android:description="@string/permdesc_writeVerificationStateE2eeContactKeys" - android:protectionLevel="signature|privileged" /> + android:protectionLevel="signature|privileged" + android:featureFlag="android.provider.user_keys" /> <!-- Allows an application to set default account for new contacts. <p> This permission is only granted to system applications fulfilling the Contacts app role. @@ -1728,7 +1730,8 @@ android:permissionGroup="android.permission-group.UNDEFINED" android:label="@string/permlab_cameraHeadlessSystemUser" android:description="@string/permdesc_cameraHeadlessSystemUser" - android:protectionLevel="signature" /> + android:protectionLevel="signature" + android:featureFlag="com.android.internal.camera.flags.camera_hsum_permission" /> <!-- ====================================================================== --> <!-- Permissions for accessing the device sensors --> @@ -2321,7 +2324,8 @@ @hide This should only be used by system apps. --> <permission android:name="android.permission.REGISTER_NSD_OFFLOAD_ENGINE" - android:protectionLevel="signature" /> + android:protectionLevel="signature" + android:featureFlag="com.android.net.flags.register_nsd_offload_engine" /> <!-- ======================================= --> <!-- Permissions for short range, peripheral networks --> @@ -2390,7 +2394,8 @@ them from running without explicit user action. --> <permission android:name="android.permission.QUARANTINE_APPS" - android:protectionLevel="signature|verifier" /> + android:protectionLevel="signature|verifier" + android:featureFlag="android.content.pm.quarantined_enabled" /> <!-- Allows applications to discover and pair bluetooth devices. <p>Protection level: normal @@ -2650,7 +2655,8 @@ @FlaggedApi("com.android.window.flags.screen_recording_callbacks") --> <permission android:name="android.permission.DETECT_SCREEN_RECORDING" - android:protectionLevel="normal" /> + android:protectionLevel="normal" + android:featureFlag="com.android.window.flags.screen_recording_callbacks" /> <!-- ======================================== --> <!-- Permissions for factory reset protection --> @@ -3617,6 +3623,13 @@ <permission android:name="android.permission.MANAGE_DEVICE_POLICY_THREAD_NETWORK" android:protectionLevel="internal|role" /> + <!-- Allows an application to set policy related to sending assist content to a + privileged app such as the Assistant app. + @FlaggedApi("android.app.admin.flags.assist_content_user_restriction_enabled") + --> + <permission android:name="android.permission.MANAGE_DEVICE_POLICY_ASSIST_CONTENT" + android:protectionLevel="internal|role" /> + <!-- Allows an application to set policy related to windows. <p>{@link Manifest.permission#MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL} is required to call APIs protected by this permission on users different to the calling user. @@ -3799,6 +3812,13 @@ <permission android:name="android.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION" android:protectionLevel="internal|role" /> + <!-- Allows an application to set policy related to subscriptions downloaded by an admin. + <p>{@link Manifest.permission#MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL} is required to call + APIs protected by this permission on users different to the calling user. + @FlaggedApi("android.app.admin.flags.esim_management_enabled") --> + <permission android:name="android.permission.MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS" + android:protectionLevel="internal|role" /> + <!-- Allows an application to set device policies outside the current user that are critical for securing data within the current user. <p>Holding this permission allows the use of other held MANAGE_DEVICE_POLICY_* @@ -5922,7 +5942,7 @@ <permission android:name="android.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS" android:protectionLevel="signature|privileged" /> - <!-- Allows an application to collect usage infomation about brightness slider changes. + <!-- Allows an application to collect usage information about brightness slider changes. <p>Not for use by third-party applications.</p> @hide @SystemApi diff --git a/core/res/OWNERS b/core/res/OWNERS index 332ad2a82d38..6924248b33cf 100644 --- a/core/res/OWNERS +++ b/core/res/OWNERS @@ -8,7 +8,6 @@ dupin@google.com hackbod@android.com hackbod@google.com ilyamaty@google.com -jaggies@google.com jbolinger@google.com jsharkey@android.com jsharkey@google.com diff --git a/core/res/res/color/system_on_surface_disabled.xml b/core/res/res/color/system_on_surface_disabled.xml new file mode 100644 index 000000000000..aba87f543c44 --- /dev/null +++ b/core/res/res/color/system_on_surface_disabled.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2023 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="?attr/materialColorOnSurface" + android:alpha="?attr/disabledAlpha" /> +</selector> diff --git a/core/res/res/color/system_outline_disabled.xml b/core/res/res/color/system_outline_disabled.xml new file mode 100644 index 000000000000..0a67ce3bf186 --- /dev/null +++ b/core/res/res/color/system_outline_disabled.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2023 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="?attr/materialColorOutline" + android:alpha="?attr/disabledAlpha" /> +</selector> diff --git a/core/res/res/color/system_surface_disabled.xml b/core/res/res/color/system_surface_disabled.xml new file mode 100644 index 000000000000..2d7fe7d727be --- /dev/null +++ b/core/res/res/color/system_surface_disabled.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2023 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="?attr/materialColorSurface" + android:alpha="?attr/disabledAlpha" /> +</selector> diff --git a/core/res/res/drawable/ic_private_profile_badge.xml b/core/res/res/drawable/ic_private_profile_badge.xml index 28c0f8aba6f1..b042c39aea8b 100644 --- a/core/res/res/drawable/ic_private_profile_badge.xml +++ b/core/res/res/drawable/ic_private_profile_badge.xml @@ -20,6 +20,6 @@ android:viewportWidth="24" android:viewportHeight="24"> <path - android:pathData="M10.5,15H13.5L12.925,11.775C13.258,11.608 13.517,11.367 13.7,11.05C13.9,10.733 14,10.383 14,10C14,9.45 13.8,8.983 13.4,8.6C13.017,8.2 12.55,8 12,8C11.45,8 10.975,8.2 10.575,8.6C10.192,8.983 10,9.45 10,10C10,10.383 10.092,10.733 10.275,11.05C10.475,11.367 10.742,11.608 11.075,11.775L10.5,15ZM12,22C9.683,21.417 7.767,20.092 6.25,18.025C4.75,15.942 4,13.633 4,11.1V5L12,2L20,5V11.1C20,13.633 19.242,15.942 17.725,18.025C16.225,20.092 14.317,21.417 12,22ZM12,19.9C13.733,19.35 15.167,18.25 16.3,16.6C17.433,14.95 18,13.117 18,11.1V6.375L12,4.125L6,6.375V11.1C6,13.117 6.567,14.95 7.7,16.6C8.833,18.25 10.267,19.35 12,19.9Z" - android:fillColor="@android:color/system_accent1_900"/> + android:pathData="M5,3H19C20.1,3 21,3.9 21,5V19C21,20.1 20.1,21 19,21H5C3.9,21 3,20.1 3,19V5C3,3.9 3.9,3 5,3ZM13.5,15.501L12.93,12.271C13.57,11.941 14,11.271 14,10.501C14,9.401 13.1,8.501 12,8.501C10.9,8.501 10,9.401 10,10.501C10,11.271 10.43,11.941 11.07,12.271L10.5,15.501H13.5Z" + android:fillColor="#3C4043"/> </vector>
\ No newline at end of file diff --git a/core/res/res/drawable/ic_private_profile_icon_badge.xml b/core/res/res/drawable/ic_private_profile_icon_badge.xml index 5cb6a9dc118f..5f1f1b72b5ee 100644 --- a/core/res/res/drawable/ic_private_profile_icon_badge.xml +++ b/core/res/res/drawable/ic_private_profile_icon_badge.xml @@ -25,7 +25,7 @@ android:translateX="42" android:translateY="42"> <path - android:pathData="M10.5,15H13.5L12.925,11.775C13.258,11.608 13.517,11.367 13.7,11.05C13.9,10.733 14,10.383 14,10C14,9.45 13.8,8.983 13.4,8.6C13.017,8.2 12.55,8 12,8C11.45,8 10.975,8.2 10.575,8.6C10.192,8.983 10,9.45 10,10C10,10.383 10.092,10.733 10.275,11.05C10.475,11.367 10.742,11.608 11.075,11.775L10.5,15ZM12,22C9.683,21.417 7.767,20.092 6.25,18.025C4.75,15.942 4,13.633 4,11.1V5L12,2L20,5V11.1C20,13.633 19.242,15.942 17.725,18.025C16.225,20.092 14.317,21.417 12,22ZM12,19.9C13.733,19.35 15.167,18.25 16.3,16.6C17.433,14.95 18,13.117 18,11.1V6.375L12,4.125L6,6.375V11.1C6,13.117 6.567,14.95 7.7,16.6C8.833,18.25 10.267,19.35 12,19.9Z" - android:fillColor="@android:color/system_accent1_900"/> + android:pathData="M5,3H19C20.1,3 21,3.9 21,5V19C21,20.1 20.1,21 19,21H5C3.9,21 3,20.1 3,19V5C3,3.9 3.9,3 5,3ZM13.5,15.501L12.93,12.271C13.57,11.941 14,11.271 14,10.501C14,9.401 13.1,8.501 12,8.501C10.9,8.501 10,9.401 10,10.501C10,11.271 10.43,11.941 11.07,12.271L10.5,15.501H13.5Z" + android:fillColor="#3C4043"/> </group> </vector>
\ No newline at end of file diff --git a/core/res/res/drawable/stat_sys_private_profile_status.xml b/core/res/res/drawable/stat_sys_private_profile_status.xml index 98cc88d86ef3..429070efa8d8 100644 --- a/core/res/res/drawable/stat_sys_private_profile_status.xml +++ b/core/res/res/drawable/stat_sys_private_profile_status.xml @@ -21,5 +21,5 @@ android:viewportHeight="24"> <path android:fillColor="@android:color/white" - android:pathData="M10.5,15H13.5L12.925,11.775C13.258,11.608 13.517,11.367 13.7,11.05C13.9,10.733 14,10.383 14,10C14,9.45 13.8,8.983 13.4,8.6C13.017,8.2 12.55,8 12,8C11.45,8 10.975,8.2 10.575,8.6C10.192,8.983 10,9.45 10,10C10,10.383 10.092,10.733 10.275,11.05C10.475,11.367 10.742,11.608 11.075,11.775L10.5,15ZM12,22C9.683,21.417 7.767,20.092 6.25,18.025C4.75,15.942 4,13.633 4,11.1V5L12,2L20,5V11.1C20,13.633 19.242,15.942 17.725,18.025C16.225,20.092 14.317,21.417 12,22ZM12,19.9C13.733,19.35 15.167,18.25 16.3,16.6C17.433,14.95 18,13.117 18,11.1V6.375L12,4.125L6,6.375V11.1C6,13.117 6.567,14.95 7.7,16.6C8.833,18.25 10.267,19.35 12,19.9Z"/> + android:pathData="M5,3H19C20.1,3 21,3.9 21,5V19C21,20.1 20.1,21 19,21H5C3.9,21 3,20.1 3,19V5C3,3.9 3.9,3 5,3ZM13.5,15.501L12.93,12.271C13.57,11.941 14,11.271 14,10.501C14,9.401 13.1,8.501 12,8.501C10.9,8.501 10,9.401 10,10.501C10,11.271 10.43,11.941 11.07,12.271L10.5,15.501H13.5Z"/> </vector>
\ No newline at end of file diff --git a/core/res/res/layout/app_perms_summary.xml b/core/res/res/layout/app_perms_summary.xml index b8d93aca755b..509b98829883 100644 --- a/core/res/res/layout/app_perms_summary.xml +++ b/core/res/res/layout/app_perms_summary.xml @@ -14,7 +14,7 @@ limitations under the License. --> -<!-- Describes permission item consisting of a group name and the list of permisisons under the group --> +<!-- Describes permission item consisting of a group name and the list of permissions under the group --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" diff --git a/core/res/res/values-watch/colors.xml b/core/res/res/values-watch/colors.xml new file mode 100644 index 000000000000..0b00bd8ea686 --- /dev/null +++ b/core/res/res/values-watch/colors.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2014 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. +--> + +<!-- Watch specific system colors. --> +<resources> + <color name="system_error_light">#B3261E</color> + <color name="system_on_error_light">#FFFFFF</color> + <color name="system_error_container_light">#F9DEDC</color> + <color name="system_on_error_container_light">#410E0B</color> + + <color name="system_error_dark">#EC928E</color> + <color name="system_on_error_dark">#410E0B</color> + <color name="system_error_container_dark">#F2B8B5</color> + <color name="system_on_error_container_dark">#601410</color> +</resources> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 41bc8251694f..4ee03deab6c2 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -1344,6 +1344,8 @@ <!-- A color that passes accessibility guidelines for text/iconography when drawn on top of tertiary. @hide --> <attr name="materialColorTertiary" format="color"/> + <!-- The error color for the app, intended to draw attention to error conditions. @hide --> + <attr name="materialColorError" format="color"/> </declare-styleable> <!-- **************************************************************** --> @@ -3641,6 +3643,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. --> @@ -3966,6 +3980,26 @@ {@link android.inputmethodservice.InputMethodService#onFinishInput()}. --> <attr name="supportsStylusHandwriting" format="boolean" /> + <!-- Specifies whether the IME supports connectionless stylus handwriting sessions. A + connectionless session differs from a regular session in that the IME does not use an + input connection to communicate with a text editor. Instead, the IME directly returns + recognised handwritten text via an {@link + android.inputmethodservice.InputMethodService} handwriting lifecycle API. + + <p>If the IME supports connectionless sessions, apps or framework may start a + connectionless session when a stylus motion event sequence begins. {@link + android.inputmethodservice.InputMethodService#onStartConnectionlessStylusHandwriting} + is called. If the IME is ready for stylus input, it should return {code true} to start + the basic mode session. As in the regular session, the IME will receive stylus motion + events to the stylus handwriting window and should render ink to a view in this window. + When the user has stopped handwriting, the IME should end the session and deliver the + result by calling {@link + android.inputmethodservice.InputMethodService#finishConnectionlessStylusHandwriting}. + + The default value is {code false}. If {code true}, {@link + android.R.attr#supportsStylusHandwriting} should also be {code true}. + --> + <attr name="supportsConnectionlessStylusHandwriting" format="boolean" /> <!-- Class name of an activity that allows the user to modify the stylus handwriting settings for this service --> <attr name="stylusHandwritingSettingsActivity" format="string" /> diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml index 53a6270ed0e4..b879c9794f39 100644 --- a/core/res/res/values/colors.xml +++ b/core/res/res/values/colors.xml @@ -438,7 +438,47 @@ This value can be overlaid at runtime by OverlayManager RROs. --> <color name="system_neutral2_1000">#000000</color> - <!-- Colors used in Android system, from Material design system. + <!-- Lightest shade of the error color used by the system. White. + This value can be overlaid at runtime by OverlayManager RROs. --> + <color name="system_error_0">#ffffff</color> + <!-- Shade of the error system color at 99% perceptual luminance (L* in L*a*b* color space). + This value can be overlaid at runtime by OverlayManager RROs. --> + <color name="system_error_10">#FFFBF9</color> + <!-- Shade of the error system color at 95% perceptual luminance (L* in L*a*b* color space). + This value can be overlaid at runtime by OverlayManager RROs. --> + <color name="system_error_50">#FCEEEE</color> + <!-- Shade of the error system color at 90% perceptual luminance (L* in L*a*b* color space). + This value can be overlaid at runtime by OverlayManager RROs. --> + <color name="system_error_100">#F9DEDC</color> + <!-- Shade of the error system color at 80% perceptual luminance (L* in L*a*b* color space). + This value can be overlaid at runtime by OverlayManager RROs. --> + <color name="system_error_200">#F2B8B5</color> + <!-- Shade of the error system color at 70% perceptual luminance (L* in L*a*b* color space). + This value can be overlaid at runtime by OverlayManager RROs. --> + <color name="system_error_300">#EC928E</color> + <!-- Shade of the error system color at 60% perceptual luminance (L* in L*a*b* color space). + This value can be overlaid at runtime by OverlayManager RROs. --> + <color name="system_error_400">#E46962</color> + <!-- Shade of the error system color at 49% perceptual luminance (L* in L*a*b* color space). + This value can be overlaid at runtime by OverlayManager RROs. --> + <color name="system_error_500">#DC362E</color> + <!-- Shade of the error system color at 40% perceptual luminance (L* in L*a*b* color space). + This value can be overlaid at runtime by OverlayManager RROs. --> + <color name="system_error_600">#B3261E</color> + <!-- Shade of the error system color at 30% perceptual luminance (L* in L*a*b* color space). + This value can be overlaid at runtime by OverlayManager RROs. --> + <color name="system_error_700">#8C1D18</color> + <!-- Shade of the error system color at 20% perceptual luminance (L* in L*a*b* color space). + This value can be overlaid at runtime by OverlayManager RROs. --> + <color name="system_error_800">#601410</color> + <!-- Shade of the error system color at 10% perceptual luminance (L* in L*a*b* color space). + This value can be overlaid at runtime by OverlayManager RROs. --> + <color name="system_error_900">#410E0B</color> + <!-- Darkest shade of the error color used by the system. Black. + This value can be overlaid at runtime by OverlayManager RROs. --> + <color name="system_error_1000">#000000</color> + + <!-- Colors used in Android system, from design system. These values can be overlaid at runtime by OverlayManager RROs. --> <color name="system_primary_container_light">#D8E2FF</color> <color name="system_on_primary_container_light">#001A41</color> diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index 78ce2d9ca28f..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" /> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 0acccee998a8..291a5936330a 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -65,7 +65,7 @@ <!-- Width of the navigation bar when it is placed vertically on the screen --> <dimen name="navigation_bar_width">48dp</dimen> <!-- Height of the bottom taskbar not including decorations like rounded corners. --> - <dimen name="taskbar_frame_height">60dp</dimen> + <dimen name="taskbar_frame_height">56dp</dimen> <!-- How much we expand the touchable region of the status bar below the notch to catch touches that just start below the notch. --> <dimen name="display_cutout_touchable_region_size">12dp</dimen> diff --git a/core/res/res/values/dimens_material.xml b/core/res/res/values/dimens_material.xml index 972fe7ed91de..fa15c3fa4cff 100644 --- a/core/res/res/values/dimens_material.xml +++ b/core/res/res/values/dimens_material.xml @@ -204,4 +204,11 @@ <dimen name="progress_bar_size_small">16dip</dimen> <dimen name="progress_bar_size_medium">48dp</dimen> <dimen name="progress_bar_size_large">76dp</dimen> + + <!-- System corner radius baseline sizes. Used by Material styling of rounded corner shapes--> + <dimen name="system_corner_radius_xsmall">4dp</dimen> + <dimen name="system_corner_radius_small">8dp</dimen> + <dimen name="system_corner_radius_medium">16dp</dimen> + <dimen name="system_corner_radius_large">26dp</dimen> + <dimen name="system_corner_radius_xlarge">36dp</dimen> </resources> diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index 5ee555543387..dcb6bb0cd743 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -155,6 +155,10 @@ <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" /> + <!-- @FlaggedApi("android.view.inputmethod.connectionless_handwriting") --> + <public name="supportsConnectionlessStylusHandwriting" /> </staging-public-group> <staging-public-group type="id" first-id="0x01bc0000"> @@ -171,9 +175,31 @@ </staging-public-group> <staging-public-group type="dimen" first-id="0x01b90000"> + <!-- System corner radius baseline sizes. Used by Material styling of rounded corner shapes--> + <public name="system_corner_radius_xsmall" /> + <public name="system_corner_radius_small" /> + <public name="system_corner_radius_medium" /> + <public name="system_corner_radius_large" /> + <public name="system_corner_radius_xlarge" /> </staging-public-group> <staging-public-group type="color" first-id="0x01b80000"> + <public name="system_surface_disabled"/> + <public name="system_on_surface_disabled"/> + <public name="system_outline_disabled"/> + <public name="system_error_0"/> + <public name="system_error_10"/> + <public name="system_error_50"/> + <public name="system_error_100"/> + <public name="system_error_200"/> + <public name="system_error_300"/> + <public name="system_error_400"/> + <public name="system_error_500"/> + <public name="system_error_600"/> + <public name="system_error_700"/> + <public name="system_error_800"/> + <public name="system_error_900"/> + <public name="system_error_1000"/> </staging-public-group> <staging-public-group type="array" first-id="0x01b70000"> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 3d19c8587f71..7c290b1ba3e1 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5244,6 +5244,7 @@ <java-symbol name="materialColorPrimary" type="attr"/> <java-symbol name="materialColorSecondary" type="attr"/> <java-symbol name="materialColorTertiary" type="attr"/> + <java-symbol name="materialColorError" type="attr"/> <java-symbol type="attr" name="actionModeUndoDrawable" /> <java-symbol type="attr" name="actionModeRedoDrawable" /> @@ -5355,4 +5356,11 @@ <java-symbol type="drawable" name="ic_satellite_alt_24px" /> <java-symbol type="bool" name="config_watchlistUseFileHashesCache" /> + + <!-- System corner radius baseline sizes. Used by Material styling of rounded corner shapes--> + <java-symbol type="dimen" name="system_corner_radius_xsmall" /> + <java-symbol type="dimen" name="system_corner_radius_small" /> + <java-symbol type="dimen" name="system_corner_radius_medium" /> + <java-symbol type="dimen" name="system_corner_radius_large" /> + <java-symbol type="dimen" name="system_corner_radius_xlarge" /> </resources> diff --git a/core/res/res/values/themes_device_defaults.xml b/core/res/res/values/themes_device_defaults.xml index 84f1d6e3071a..ee191449ac35 100644 --- a/core/res/res/values/themes_device_defaults.xml +++ b/core/res/res/values/themes_device_defaults.xml @@ -283,6 +283,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <style name="Theme.DeviceDefault" parent="Theme.DeviceDefaultBase" /> @@ -378,6 +379,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <!-- Variant of {@link #Theme_DeviceDefault} with no action bar and no status bar. This theme @@ -472,6 +474,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <!-- Variant of {@link #Theme_DeviceDefault} with no action bar and no status bar and @@ -568,6 +571,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <!-- Variant of {@link #Theme_DeviceDefault} that has no title bar and translucent @@ -663,6 +667,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <!-- DeviceDefault theme for dialog windows and activities. This changes the window to be @@ -766,6 +771,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <!-- Variant of {@link #Theme_DeviceDefault_Dialog} that has a nice minimum width for a @@ -860,6 +866,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <!-- Variant of {@link #Theme_DeviceDefault_Dialog} without an action bar --> @@ -953,6 +960,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <!-- Variant of {@link #Theme_DeviceDefault_Dialog_NoActionBar} that has a nice minimum width @@ -1047,6 +1055,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <!-- Variant of Theme.DeviceDefault.Dialog that has a fixed size. --> @@ -1157,6 +1166,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <!-- DeviceDefault theme for a window without an action bar that will be displayed either @@ -1252,6 +1262,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <!-- DeviceDefault theme for a presentation window on a secondary display. --> @@ -1345,6 +1356,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <!-- DeviceDefault theme for panel windows. This removes all extraneous window @@ -1440,6 +1452,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <!-- DeviceDefault theme for windows that want to have the user's selected wallpaper appear @@ -1534,6 +1547,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <!-- DeviceDefault theme for windows that want to have the user's selected wallpaper appear @@ -1628,6 +1642,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <!-- DeviceDefault style for input methods, which is used by the @@ -1722,6 +1737,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- DeviceDefault style for input methods, which is used by the @@ -1816,6 +1832,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <style name="Theme.DeviceDefault.Dialog.Alert" parent="Theme.Material.Dialog.Alert"> @@ -1910,6 +1927,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <!-- Theme for the dialog shown when an app crashes or ANRs. --> @@ -2009,6 +2027,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <style name="Theme.DeviceDefault.Dialog.NoFrame" parent="Theme.Material.Dialog.NoFrame"> @@ -2101,6 +2120,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <!-- Variant of {@link #Theme_DeviceDefault} with a light-colored style --> @@ -2331,6 +2351,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- Variant of the DeviceDefault (light) theme that has a solid (opaque) action bar with an @@ -2425,6 +2446,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- Variant of {@link #Theme_DeviceDefault_Light} with no action bar --> @@ -2518,6 +2540,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- Variant of {@link #Theme_DeviceDefault_Light} with no action bar and no status bar. @@ -2612,6 +2635,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- Variant of {@link #Theme_DeviceDefault_Light} with no action bar and no status bar @@ -2708,6 +2732,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- Variant of {@link #Theme_DeviceDefault_Light} that has no title bar and translucent @@ -2803,6 +2828,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- DeviceDefault light theme for dialog windows and activities. This changes the window to be @@ -2904,6 +2930,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- Variant of {@link #Theme_DeviceDefault_Light_Dialog} that has a nice minimum width for a @@ -3001,6 +3028,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- Variant of {@link #Theme_DeviceDefault_Light_Dialog} without an action bar --> @@ -3097,6 +3125,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- Variant of {@link #Theme_DeviceDefault_Light_Dialog_NoActionBar} that has a nice minimum @@ -3194,6 +3223,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- Variant of Theme.DeviceDefault.Dialog that has a fixed size. --> @@ -3272,6 +3302,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- Variant of Theme.DeviceDefault.Dialog.NoActionBar that has a fixed size. --> @@ -3350,6 +3381,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- DeviceDefault light theme for a window that will be displayed either full-screen on smaller @@ -3447,6 +3479,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- DeviceDefault light theme for a window without an action bar that will be displayed either @@ -3545,6 +3578,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- DeviceDefault light theme for a presentation window on a secondary display. --> @@ -3641,6 +3675,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- DeviceDefault light theme for panel windows. This removes all extraneous window @@ -3736,6 +3771,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <style name="Theme.DeviceDefault.Light.Dialog.Alert" parent="Theme.Material.Light.Dialog.Alert"> @@ -3830,6 +3866,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <style name="Theme.DeviceDefault.Dialog.Alert.DayNight" parent="Theme.DeviceDefault.Light.Dialog.Alert" /> @@ -3924,6 +3961,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <style name="Theme.DeviceDefault.Light.Voice" parent="Theme.Material.Light.Voice"> @@ -4016,6 +4054,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- DeviceDefault theme for a window that should look like the Settings app. --> @@ -4116,6 +4155,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <style name="Theme.DeviceDefault.SystemUI" parent="Theme.DeviceDefault.Light"> @@ -4197,6 +4237,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <style name="Theme.DeviceDefault.SystemUI.Dialog" parent="Theme.DeviceDefault.Light.Dialog"> @@ -4270,6 +4311,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- Variant of {@link #Theme_DeviceDefault_Settings_Dark} with no action bar --> @@ -4364,6 +4406,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <style name="Theme.DeviceDefault.Settings.DialogBase" parent="Theme.Material.Light.BaseDialog"> @@ -4442,6 +4485,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <style name="Theme.DeviceDefault.Settings.Dialog" parent="Theme.DeviceDefault.Settings.DialogBase"> @@ -4560,6 +4604,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <style name="Theme.DeviceDefault.Settings.Dialog.Alert" parent="Theme.Material.Settings.Dialog.Alert"> @@ -4656,6 +4701,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <style name="Theme.DeviceDefault.Settings.Dialog.NoActionBar" parent="Theme.DeviceDefault.Light.Dialog.NoActionBar" /> @@ -4778,6 +4824,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <style name="ThemeOverlay.DeviceDefault.Accent.Light"> @@ -4830,6 +4877,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <!-- Theme overlay that replaces colorAccent with the colorAccent from {@link #Theme_DeviceDefault_DayNight}. --> @@ -4886,6 +4934,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <style name="Theme.DeviceDefault.Light.Dialog.Alert.UserSwitchingDialog" parent="Theme.DeviceDefault.NoActionBar.Fullscreen"> @@ -4938,6 +4987,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_light</item> <item name="materialColorSecondary">@color/system_secondary_light</item> <item name="materialColorTertiary">@color/system_tertiary_light</item> + <item name="materialColorError">@color/system_error_light</item> </style> <style name="Theme.DeviceDefault.Notification" parent="@style/Theme.Material.Notification"> @@ -5001,6 +5051,7 @@ easier. <item name="materialColorPrimary">@color/system_primary_dark</item> <item name="materialColorSecondary">@color/system_secondary_dark</item> <item name="materialColorTertiary">@color/system_tertiary_dark</item> + <item name="materialColorError">@color/system_error_dark</item> </style> <style name="Theme.DeviceDefault.AutofillHalfScreenDialogList" parent="Theme.DeviceDefault.DayNight"> <item name="colorListDivider">@color/list_divider_opacity_device_default_light</item> diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java index 5aace81696cf..e4cf7ac7e7cd 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java @@ -286,7 +286,7 @@ public final class TunerAdapterTest { int scanStatus = mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ false); verify(mTunerMock).seek(/* directionDown= */ true, /* skipSubChannel= */ false); - assertWithMessage("Status for scaning") + assertWithMessage("Status for scanning") .that(scanStatus).isEqualTo(RadioManager.STATUS_OK); verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO); } diff --git a/core/tests/ConnectivityManagerTest/src/com/android/connectivitymanagertest/ConnectivityManagerStressTestRunner.java b/core/tests/ConnectivityManagerTest/src/com/android/connectivitymanagertest/ConnectivityManagerStressTestRunner.java index 0806fa0b9879..db95d7af5d52 100644 --- a/core/tests/ConnectivityManagerTest/src/com/android/connectivitymanagertest/ConnectivityManagerStressTestRunner.java +++ b/core/tests/ConnectivityManagerTest/src/com/android/connectivitymanagertest/ConnectivityManagerStressTestRunner.java @@ -99,7 +99,7 @@ public class ConnectivityManagerStressTestRunner extends InstrumentationTestRunn } } - public int getSoftApInterations() { + public int getSoftApIterations() { return mSoftApIterations; } diff --git a/data/etc/com.android.launcher3.xml b/data/etc/com.android.launcher3.xml index 5616d1d3c189..47e2e3844bb9 100644 --- a/data/etc/com.android.launcher3.xml +++ b/data/etc/com.android.launcher3.xml @@ -26,5 +26,6 @@ <permission name="android.permission.STATUS_BAR"/> <permission name="android.permission.STOP_APP_SWITCHES"/> <permission name="android.permission.ACCESS_SHORTCUTS"/> + <permission name="android.permission.ACCESS_HIDDEN_PROFILES_FULL"/> </privapp-permissions> </permissions> diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index fdb520835810..8e2c415f6383 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -316,6 +316,8 @@ applications that come with the platform <permission name="android.permission.SET_LOW_POWER_STANDBY_PORTS" /> <permission name="android.permission.MANAGE_ROLLBACKS"/> <permission name="android.permission.MANAGE_USB"/> + <!-- Permission required to test Launcher Apps APIs for hidden profiles --> + <permission name="android.permission.ACCESS_HIDDEN_PROFILES_FULL" /> <!-- Needed for tests only --> <permission name="android.permission.MANAGE_CLOUDSEARCH" /> <permission name="android.permission.MANAGE_WALLPAPER_EFFECTS_GENERATION" /> diff --git a/graphics/java/android/graphics/BitmapFactory.java b/graphics/java/android/graphics/BitmapFactory.java index 1da8e189d768..d915b746e0cc 100644 --- a/graphics/java/android/graphics/BitmapFactory.java +++ b/graphics/java/android/graphics/BitmapFactory.java @@ -25,10 +25,13 @@ import android.content.res.AssetManager; import android.content.res.Resources; import android.os.Build; import android.os.Trace; +import android.system.OsConstants; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; +import libcore.io.IoBridge; + import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.IOException; @@ -523,19 +526,19 @@ public class BitmapFactory { public static Bitmap decodeFile(String pathName, Options opts) { validate(opts); Bitmap bm = null; - InputStream stream = null; + FileDescriptor fd = null; try { - stream = new FileInputStream(pathName); - bm = decodeStream(stream, null, opts); + fd = IoBridge.open(pathName, OsConstants.O_RDONLY); + bm = decodeFileDescriptor(fd, null, opts); } catch (Exception e) { /* do nothing. If the exception happened on open, bm will be null. */ - Log.e("BitmapFactory", "Unable to decode stream: " + e); + Log.e("BitmapFactory", "Unable to decode file: " + e); } finally { - if (stream != null) { + if (fd != null) { try { - stream.close(); + IoBridge.closeAndSignalBlockedThreads(fd); } catch (IOException e) { // do nothing here } diff --git a/graphics/java/android/view/PixelCopy.java b/graphics/java/android/view/PixelCopy.java index e6de5978ceb0..9159a00152dd 100644 --- a/graphics/java/android/view/PixelCopy.java +++ b/graphics/java/android/view/PixelCopy.java @@ -96,7 +96,7 @@ public final class PixelCopy { * * The contents of the source will be scaled to fit exactly inside the bitmap. * The pixel format of the source buffer will be converted, as part of the copy, - * to fit the the bitmap's {@link Bitmap.Config}. The most recently queued buffer + * to fit the bitmap's {@link Bitmap.Config}. The most recently queued buffer * in the SurfaceView's Surface will be used as the source of the copy. * * @param source The source from which to copy @@ -117,7 +117,7 @@ public final class PixelCopy { * * The contents of the source will be scaled to fit exactly inside the bitmap. * The pixel format of the source buffer will be converted, as part of the copy, - * to fit the the bitmap's {@link Bitmap.Config}. The most recently queued buffer + * to fit the bitmap's {@link Bitmap.Config}. The most recently queued buffer * in the SurfaceView's Surface will be used as the source of the copy. * * @param source The source from which to copy @@ -143,7 +143,7 @@ public final class PixelCopy { * * The contents of the source will be scaled to fit exactly inside the bitmap. * The pixel format of the source buffer will be converted, as part of the copy, - * to fit the the bitmap's {@link Bitmap.Config}. The most recently queued buffer + * to fit the bitmap's {@link Bitmap.Config}. The most recently queued buffer * in the Surface will be used as the source of the copy. * * @param source The source from which to copy @@ -164,7 +164,7 @@ public final class PixelCopy { * * The contents of the source rect will be scaled to fit exactly inside the bitmap. * The pixel format of the source buffer will be converted, as part of the copy, - * to fit the the bitmap's {@link Bitmap.Config}. The most recently queued buffer + * to fit the bitmap's {@link Bitmap.Config}. The most recently queued buffer * in the Surface will be used as the source of the copy. * * @param source The source from which to copy @@ -201,7 +201,7 @@ public final class PixelCopy { * * The contents of the source will be scaled to fit exactly inside the bitmap. * The pixel format of the source buffer will be converted, as part of the copy, - * to fit the the bitmap's {@link Bitmap.Config}. The most recently queued buffer + * to fit the bitmap's {@link Bitmap.Config}. The most recently queued buffer * in the Window's Surface will be used as the source of the copy. * * Note: This is limited to being able to copy from Window's with a non-null @@ -231,7 +231,7 @@ public final class PixelCopy { * * The contents of the source rect will be scaled to fit exactly inside the bitmap. * The pixel format of the source buffer will be converted, as part of the copy, - * to fit the the bitmap's {@link Bitmap.Config}. The most recently queued buffer + * to fit the bitmap's {@link Bitmap.Config}. The most recently queued buffer * in the Window's Surface will be used as the source of the copy. * * Note: This is limited to being able to copy from Window's with a non-null diff --git a/keystore/aaid/aidl/android/security/keystore/IKeyAttestationApplicationIdProvider.aidl b/keystore/aaid/aidl/android/security/keystore/IKeyAttestationApplicationIdProvider.aidl index c360cb8f281a..cfc5980e009a 100644 --- a/keystore/aaid/aidl/android/security/keystore/IKeyAttestationApplicationIdProvider.aidl +++ b/keystore/aaid/aidl/android/security/keystore/IKeyAttestationApplicationIdProvider.aidl @@ -20,8 +20,14 @@ import android.security.keystore.KeyAttestationApplicationId; /** @hide */ interface IKeyAttestationApplicationIdProvider { + const int ERROR_GET_ATTESTATION_APPLICATION_ID_FAILED = 1; + /** * Provides information describing the possible applications identified by a UID. + * + * In case of not getting package ids from uid return + * {@link #ERROR_GET_ATTESTATION_APPLICATION_ID_FAILED} to the caller. + * * @hide */ KeyAttestationApplicationId getKeyAttestationApplicationId(int uid); diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index a12fa5fe26ff..310300d2f32a 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -19,6 +19,7 @@ package { // to get the below license kinds: // SPDX-license-identifier-Apache-2.0 default_applicable_licenses: ["frameworks_base_license"], + default_team: "trendy_team_multitasking_windowing", } // Begin ProtoLog diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 28e709845e88..a13de9f9743e 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -266,6 +266,10 @@ <dimen name="bubble_bar_manage_menu_item_height">52dp</dimen> <!-- Size of the icons in the bubble bar manage menu. --> <dimen name="bubble_bar_manage_menu_item_icon_size">20dp</dimen> + <!-- Corner radius for expanded view when bubble bar is used --> + <dimen name="bubble_bar_expanded_view_corner_radius">16dp</dimen> + <!-- Corner radius for expanded view while it is being dragged --> + <dimen name="bubble_bar_expanded_view_corner_radius_dragged">28dp</dimen> <!-- Bottom and end margin for compat buttons. --> <dimen name="compat_button_margin">24dp</dimen> @@ -435,6 +439,9 @@ Text varies in size, we will calculate that width separately. --> <dimen name="desktop_mode_app_details_width_minus_text">62dp</dimen> + <!-- 22dp padding + 24dp app icon + 16dp expand button + 86dp text (max) --> + <dimen name="desktop_mode_app_details_max_width">148dp</dimen> + <!-- The width of the maximize menu in desktop mode. --> <dimen name="desktop_mode_maximize_menu_width">287dp</dimen> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index 35c1e8c2a047..b23fd5269eae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -441,43 +441,42 @@ public class BubbleStackView extends FrameLayout /** Magnet listener that handles animating and dismissing individual dragged-out bubbles. */ private final MagnetizedObject.MagnetListener mIndividualBubbleMagnetListener = new MagnetizedObject.MagnetListener() { + @Override - public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { - if (mExpandedAnimationController.getDraggedOutBubble() == null) { - return; + public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject draggedObject) { + if (draggedObject.getUnderlyingObject() instanceof View view) { + animateDismissBubble(view, true); } - animateDismissBubble( - mExpandedAnimationController.getDraggedOutBubble(), true); } @Override public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject draggedObject, float velX, float velY, boolean wasFlungOut) { - if (mExpandedAnimationController.getDraggedOutBubble() == null) { - return; - } - animateDismissBubble( - mExpandedAnimationController.getDraggedOutBubble(), false); + if (draggedObject.getUnderlyingObject() instanceof View view) { + animateDismissBubble(view, false); - if (wasFlungOut) { - mExpandedAnimationController.snapBubbleBack( - mExpandedAnimationController.getDraggedOutBubble(), velX, velY); - mDismissView.hide(); - } else { - mExpandedAnimationController.onUnstuckFromTarget(); + if (wasFlungOut) { + mExpandedAnimationController.snapBubbleBack(view, velX, velY); + mDismissView.hide(); + } else { + mExpandedAnimationController.onUnstuckFromTarget(); + } } } @Override - public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { - if (mExpandedAnimationController.getDraggedOutBubble() == null) { - return; + public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject<?> draggedObject) { + if (draggedObject.getUnderlyingObject() instanceof View view) { + mExpandedAnimationController.dismissDraggedOutBubble( + view /* bubble */, + mDismissView.getHeight() /* translationYBy */, + () -> dismissBubbleIfExists( + mBubbleData.getBubbleWithView(view)) /* after */); } - mExpandedAnimationController.dismissDraggedOutBubble( - mExpandedAnimationController.getDraggedOutBubble() /* bubble */, - mDismissView.getHeight() /* translationYBy */, - BubbleStackView.this::dismissMagnetizedObject /* after */); mDismissView.hide(); } }; @@ -487,12 +486,14 @@ public class BubbleStackView extends FrameLayout new MagnetizedObject.MagnetListener() { @Override public void onStuckToTarget( - @NonNull MagnetizedObject.MagneticTarget target) { + @NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject<?> draggedObject) { animateDismissBubble(mBubbleContainer, true); } @Override public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject<?> draggedObject, float velX, float velY, boolean wasFlungOut) { animateDismissBubble(mBubbleContainer, false); if (wasFlungOut) { @@ -505,14 +506,14 @@ public class BubbleStackView extends FrameLayout } @Override - public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { + public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject<?> draggedObject) { mStackAnimationController.animateStackDismissal( mDismissView.getHeight() /* translationYBy */, () -> { + mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE); resetDismissAnimator(); - dismissMagnetizedObject(); - } - ); + } /*after */); mDismissView.hide(); } }; @@ -2759,19 +2760,6 @@ public class BubbleStackView extends FrameLayout return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event); } - /** - * Dismisses the magnetized object - either an individual bubble, if we're expanded, or the - * stack, if we're collapsed. - */ - private void dismissMagnetizedObject() { - if (mIsExpanded) { - final View draggedOutBubbleView = (View) mMagnetizedObject.getUnderlyingObject(); - dismissBubbleIfExists(mBubbleData.getBubbleWithView(draggedOutBubbleView)); - } else { - mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE); - } - } - private void dismissBubbleIfExists(@Nullable BubbleViewProvider bubble) { if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { if (mIsExpanded && mBubbleData.getBubbles().size() > 1 diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java index 84a616f1e1a0..4e995bc7c92e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java @@ -15,17 +15,28 @@ */ package com.android.wm.shell.bubbles.bar; +import static android.view.View.SCALE_X; +import static android.view.View.SCALE_Y; +import static android.view.View.TRANSLATION_X; +import static android.view.View.TRANSLATION_Y; import static android.view.View.VISIBLE; +import static android.view.View.X; +import static android.view.View.Y; + +import static com.android.wm.shell.animation.Interpolators.EMPHASIZED; +import static com.android.wm.shell.animation.Interpolators.EMPHASIZED_DECELERATE; +import static com.android.wm.shell.bubbles.bar.BubbleBarExpandedView.CORNER_RADIUS; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Point; import android.graphics.Rect; import android.util.Log; import android.util.Size; -import android.view.View; import android.widget.FrameLayout; import androidx.annotation.Nullable; @@ -48,15 +59,16 @@ public class BubbleBarAnimationHelper { private static final float EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT = 0.1f; private static final float EXPANDED_VIEW_ANIMATE_OUT_SCALE_AMOUNT = .75f; private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150; - private static final int EXPANDED_VIEW_SNAP_TO_DISMISS_DURATION = 100; - private static final int EXPANDED_VIEW_ANIMATE_POSITION_DURATION = 300; + private static final int EXPANDED_VIEW_SNAP_TO_DISMISS_DURATION = 400; + private static final int EXPANDED_VIEW_ANIMATE_TO_REST_DURATION = 400; private static final int EXPANDED_VIEW_DISMISS_DURATION = 250; - private static final int EXPANDED_VIEW_DRAG_ANIMATION_DURATION = 150; + private static final int EXPANDED_VIEW_DRAG_ANIMATION_DURATION = 400; /** * Additional scale applied to expanded view when it is positioned inside a magnetic target. */ - private static final float EXPANDED_VIEW_IN_TARGET_SCALE = 0.6f; - private static final float EXPANDED_VIEW_DRAG_SCALE = 0.5f; + private static final float EXPANDED_VIEW_IN_TARGET_SCALE = 0.2f; + private static final float EXPANDED_VIEW_DRAG_SCALE = 0.4f; + private static final float DISMISS_VIEW_SCALE = 1.25f; /** Spring config for the expanded view scale-in animation. */ private final PhysicsAnimator.SpringConfig mScaleInSpringConfig = @@ -72,6 +84,9 @@ public class BubbleBarAnimationHelper { /** Animator for animating the expanded view's alpha (including the TaskView inside it). */ private final ValueAnimator mExpandedViewAlphaAnimator = ValueAnimator.ofFloat(0f, 1f); + @Nullable + private Animator mRunningDragAnimator; + private final Context mContext; private final BubbleBarLayerView mLayerView; private final BubblePositioner mPositioner; @@ -232,14 +247,18 @@ public class BubbleBarAnimationHelper { Log.w(TAG, "Trying to animate start drag without a bubble"); return; } - bbev.setPivotX(bbev.getWidth() / 2f); - bbev.setPivotY(0f); - bbev.animate() - .scaleX(EXPANDED_VIEW_DRAG_SCALE) - .scaleY(EXPANDED_VIEW_DRAG_SCALE) - .setInterpolator(Interpolators.EMPHASIZED) - .setDuration(EXPANDED_VIEW_DRAG_ANIMATION_DURATION) - .start(); + setDragPivot(bbev); + AnimatorSet animatorSet = new AnimatorSet(); + // Corner radius gets scaled, apply the reverse scale to ensure we have the desired radius + final float cornerRadius = bbev.getDraggedCornerRadius() / EXPANDED_VIEW_DRAG_SCALE; + animatorSet.playTogether( + ObjectAnimator.ofFloat(bbev, SCALE_X, EXPANDED_VIEW_DRAG_SCALE), + ObjectAnimator.ofFloat(bbev, SCALE_Y, EXPANDED_VIEW_DRAG_SCALE), + ObjectAnimator.ofFloat(bbev, CORNER_RADIUS, cornerRadius) + ); + animatorSet.setDuration(EXPANDED_VIEW_DRAG_ANIMATION_DURATION).setInterpolator(EMPHASIZED); + animatorSet.addListener(new DragAnimatorListenerAdapter(bbev)); + startNewDragAnimation(animatorSet); } /** @@ -258,6 +277,7 @@ public class BubbleBarAnimationHelper { int[] location = bbev.getLocationOnScreen(); int diffFromBottom = mPositioner.getScreenRect().bottom - location[1]; + cancelAnimations(); bbev.animate() // 2x distance from bottom so the view flies out .translationYBy(diffFromBottom * 2) @@ -276,19 +296,24 @@ public class BubbleBarAnimationHelper { return; } Point restPoint = getExpandedViewRestPosition(getExpandedViewSize()); - bbev.animate() - .x(restPoint.x) - .y(restPoint.y) - .scaleX(1f) - .scaleY(1f) - .setDuration(EXPANDED_VIEW_ANIMATE_POSITION_DURATION) - .setInterpolator(Interpolators.EMPHASIZED_DECELERATE) - .withStartAction(() -> bbev.setAnimating(true)) - .withEndAction(() -> { - bbev.setAnimating(false); - bbev.resetPivot(); - }) - .start(); + + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether( + ObjectAnimator.ofFloat(bbev, X, restPoint.x), + ObjectAnimator.ofFloat(bbev, Y, restPoint.y), + ObjectAnimator.ofFloat(bbev, SCALE_X, 1f), + ObjectAnimator.ofFloat(bbev, SCALE_Y, 1f), + ObjectAnimator.ofFloat(bbev, CORNER_RADIUS, bbev.getRestingCornerRadius()) + ); + animatorSet.setDuration(EXPANDED_VIEW_ANIMATE_TO_REST_DURATION).setInterpolator(EMPHASIZED); + animatorSet.addListener(new DragAnimatorListenerAdapter(bbev) { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + bbev.resetPivot(); + } + }); + startNewDragAnimation(animatorSet); } /** @@ -304,17 +329,7 @@ public class BubbleBarAnimationHelper { return; } - // Calculate scale of expanded view so it fits inside the magnetic target - float bbevMaxSide = Math.max(bbev.getWidth(), bbev.getHeight()); - View targetView = target.getTargetView(); - float targetMaxSide = Math.max(targetView.getWidth(), targetView.getHeight()); - // Reduce target size to have some padding between the target and expanded view - targetMaxSide *= EXPANDED_VIEW_IN_TARGET_SCALE; - float scaleInTarget = targetMaxSide / bbevMaxSide; - - // Scale around the top center of the expanded view. Same as when dragging. - bbev.setPivotX(bbev.getWidth() / 2f); - bbev.setPivotY(0); + setDragPivot(bbev); // When the view animates into the target, it is scaled down with the pivot at center top. // Find the point on the view that would be the center of the view at its final scale. @@ -330,13 +345,13 @@ public class BubbleBarAnimationHelper { // Get scaled width of the view and adjust mTmpLocation so that point on x-axis is at the // center of the view at its current size. float currentWidth = bbev.getWidth() * bbev.getScaleX(); - mTmpLocation[0] += currentWidth / 2; + mTmpLocation[0] += (int) (currentWidth / 2f); // Since pivotY is at the top of the view, at final scale, top coordinate of the view // remains the same. // Get height of the view at final scale and adjust mTmpLocation so that point on y-axis is // moved down by half of the height at final scale. - float targetHeight = bbev.getHeight() * scaleInTarget; - mTmpLocation[1] += targetHeight / 2; + float targetHeight = bbev.getHeight() * EXPANDED_VIEW_IN_TARGET_SCALE; + mTmpLocation[1] += (int) (targetHeight / 2f); // mTmpLocation is now set to the point on the view that will be the center of the view once // scale is applied. @@ -344,41 +359,61 @@ public class BubbleBarAnimationHelper { float xDiff = target.getCenterOnScreen().x - mTmpLocation[0]; float yDiff = target.getCenterOnScreen().y - mTmpLocation[1]; - bbev.animate() - .translationX(bbev.getTranslationX() + xDiff) - .translationY(bbev.getTranslationY() + yDiff) - .scaleX(scaleInTarget) - .scaleY(scaleInTarget) - .setDuration(EXPANDED_VIEW_SNAP_TO_DISMISS_DURATION) - .setInterpolator(Interpolators.EMPHASIZED) - .withStartAction(() -> bbev.setAnimating(true)) - .withEndAction(() -> { - bbev.setAnimating(false); - if (endRunnable != null) { - endRunnable.run(); - } - }) - .start(); + // Corner radius gets scaled, apply the reverse scale to ensure we have the desired radius + final float cornerRadius = bbev.getDraggedCornerRadius() / EXPANDED_VIEW_IN_TARGET_SCALE; + + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether( + // Move expanded view to the center of dismiss view + ObjectAnimator.ofFloat(bbev, TRANSLATION_X, bbev.getTranslationX() + xDiff), + ObjectAnimator.ofFloat(bbev, TRANSLATION_Y, bbev.getTranslationY() + yDiff), + // Scale expanded view down + ObjectAnimator.ofFloat(bbev, SCALE_X, EXPANDED_VIEW_IN_TARGET_SCALE), + ObjectAnimator.ofFloat(bbev, SCALE_Y, EXPANDED_VIEW_IN_TARGET_SCALE), + // Update corner radius for expanded view + ObjectAnimator.ofFloat(bbev, CORNER_RADIUS, cornerRadius), + // Scale dismiss view up + ObjectAnimator.ofFloat(target.getTargetView(), SCALE_X, DISMISS_VIEW_SCALE), + ObjectAnimator.ofFloat(target.getTargetView(), SCALE_Y, DISMISS_VIEW_SCALE) + ); + animatorSet.setDuration(EXPANDED_VIEW_SNAP_TO_DISMISS_DURATION).setInterpolator( + EMPHASIZED_DECELERATE); + animatorSet.addListener(new DragAnimatorListenerAdapter(bbev) { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (endRunnable != null) { + endRunnable.run(); + } + } + }); + startNewDragAnimation(animatorSet); } /** * Animate currently expanded view when it is released from dismiss view */ - public void animateUnstuckFromDismissView() { - BubbleBarExpandedView expandedView = getExpandedView(); - if (expandedView == null) { + public void animateUnstuckFromDismissView(MagneticTarget target) { + BubbleBarExpandedView bbev = getExpandedView(); + if (bbev == null) { Log.w(TAG, "Trying to unsnap the expanded view from dismiss without a bubble"); return; } - expandedView - .animate() - .scaleX(EXPANDED_VIEW_DRAG_SCALE) - .scaleY(EXPANDED_VIEW_DRAG_SCALE) - .setDuration(EXPANDED_VIEW_SNAP_TO_DISMISS_DURATION) - .setInterpolator(Interpolators.EMPHASIZED) - .withStartAction(() -> expandedView.setAnimating(true)) - .withEndAction(() -> expandedView.setAnimating(false)) - .start(); + setDragPivot(bbev); + // Corner radius gets scaled, apply the reverse scale to ensure we have the desired radius + final float cornerRadius = bbev.getDraggedCornerRadius() / EXPANDED_VIEW_DRAG_SCALE; + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether( + ObjectAnimator.ofFloat(bbev, SCALE_X, EXPANDED_VIEW_DRAG_SCALE), + ObjectAnimator.ofFloat(bbev, SCALE_Y, EXPANDED_VIEW_DRAG_SCALE), + ObjectAnimator.ofFloat(bbev, CORNER_RADIUS, cornerRadius), + ObjectAnimator.ofFloat(target.getTargetView(), SCALE_X, 1f), + ObjectAnimator.ofFloat(target.getTargetView(), SCALE_Y, 1f) + ); + animatorSet.setDuration(EXPANDED_VIEW_SNAP_TO_DISMISS_DURATION).setInterpolator( + EMPHASIZED_DECELERATE); + animatorSet.addListener(new DragAnimatorListenerAdapter(bbev)); + startNewDragAnimation(animatorSet); } /** @@ -391,6 +426,10 @@ public class BubbleBarAnimationHelper { if (bbev != null) { bbev.animate().cancel(); } + if (mRunningDragAnimator != null) { + mRunningDragAnimator.cancel(); + mRunningDragAnimator = null; + } } private @Nullable BubbleBarExpandedView getExpandedView() { @@ -438,4 +477,35 @@ public class BubbleBarAnimationHelper { final int height = mPositioner.getExpandedViewHeightForBubbleBar(isOverflowExpanded); return new Size(width, height); } + + private void startNewDragAnimation(Animator animator) { + cancelAnimations(); + mRunningDragAnimator = animator; + animator.start(); + } + + private static void setDragPivot(BubbleBarExpandedView bbev) { + bbev.setPivotX(bbev.getWidth() / 2f); + bbev.setPivotY(0f); + } + + private class DragAnimatorListenerAdapter extends AnimatorListenerAdapter { + + private final BubbleBarExpandedView mBubbleBarExpandedView; + + DragAnimatorListenerAdapter(BubbleBarExpandedView bbev) { + mBubbleBarExpandedView = bbev; + } + + @Override + public void onAnimationStart(Animator animation) { + mBubbleBarExpandedView.setAnimating(true); + } + + @Override + public void onAnimationEnd(Animator animation) { + mBubbleBarExpandedView.setAnimating(false); + mRunningDragAnimator = null; + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index ebb8e3e2d207..eddd43f263d9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -27,13 +27,13 @@ import android.graphics.Insets; import android.graphics.Outline; import android.graphics.Rect; import android.util.AttributeSet; +import android.util.FloatProperty; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.widget.FrameLayout; -import com.android.internal.policy.ScreenDecorationsUtils; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleExpandedViewManager; @@ -61,6 +61,23 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView void onBackPressed(); } + /** + * A property wrapper around corner radius for the expanded view, handled by + * {@link #setCornerRadius(float)} and {@link #getCornerRadius()} methods. + */ + public static final FloatProperty<BubbleBarExpandedView> CORNER_RADIUS = new FloatProperty<>( + "cornerRadius") { + @Override + public void setValue(BubbleBarExpandedView bbev, float radius) { + bbev.setCornerRadius(radius); + } + + @Override + public Float get(BubbleBarExpandedView bbev) { + return bbev.getCornerRadius(); + } + }; + private static final String TAG = BubbleBarExpandedView.class.getSimpleName(); private static final int INVALID_TASK_ID = -1; @@ -78,7 +95,12 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView private int mCaptionHeight; private int mBackgroundColor; - private float mCornerRadius = 0f; + /** Corner radius used when view is resting */ + private float mRestingCornerRadius = 0f; + /** Corner radius applied while dragging */ + private float mDraggedCornerRadius = 0f; + /** Current corner radius */ + private float mCurrentCornerRadius = 0f; /** * Whether we want the {@code TaskView}'s content to be visible (alpha = 1f). If @@ -118,7 +140,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView setOutlineProvider(new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { - outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius); + outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCurrentCornerRadius); } }); } @@ -155,7 +177,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT); addView(mTaskView, lp); mTaskView.setEnableSurfaceClipping(true); - mTaskView.setCornerRadius(mCornerRadius); + mTaskView.setCornerRadius(mCurrentCornerRadius); mTaskView.setVisibility(VISIBLE); // Handle view needs to draw on top of task view. @@ -198,22 +220,24 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView // TODO (b/275087636): call this when theme/config changes /** Updates the view based on the current theme. */ public void applyThemeAttrs() { - boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows( - mContext.getResources()); + mRestingCornerRadius = getResources().getDimensionPixelSize( + R.dimen.bubble_bar_expanded_view_corner_radius + ); + mDraggedCornerRadius = getResources().getDimensionPixelSize( + R.dimen.bubble_bar_expanded_view_corner_radius_dragged + ); + + mCurrentCornerRadius = mRestingCornerRadius; + final TypedArray ta = mContext.obtainStyledAttributes(new int[]{ - android.R.attr.dialogCornerRadius, android.R.attr.colorBackgroundFloating}); - mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0; - mCornerRadius = mCornerRadius / 2f; - mBackgroundColor = ta.getColor(1, Color.WHITE); - + mBackgroundColor = ta.getColor(0, Color.WHITE); ta.recycle(); - mCaptionHeight = getResources().getDimensionPixelSize( R.dimen.bubble_bar_expanded_view_caption_height); if (mTaskView != null) { - mTaskView.setCornerRadius(mCornerRadius); + mTaskView.setCornerRadius(mCurrentCornerRadius); updateHandleColor(true /* animated */); } } @@ -396,4 +420,30 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView public boolean isAnimating() { return mIsAnimating; } + + /** @return corner radius that should be applied while view is in rest */ + public float getRestingCornerRadius() { + return mRestingCornerRadius; + } + + /** @return corner radius that should be applied while view is being dragged */ + public float getDraggedCornerRadius() { + return mDraggedCornerRadius; + } + + /** @return current corner radius */ + public float getCornerRadius() { + return mCurrentCornerRadius; + } + + /** Update corner radius */ + public void setCornerRadius(float cornerRadius) { + if (mCurrentCornerRadius != cornerRadius) { + mCurrentCornerRadius = cornerRadius; + if (mTaskView != null) { + mTaskView.setCornerRadius(cornerRadius); + } + invalidateOutline(); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt index 5e634a23955a..7d37d6068dfb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt @@ -126,21 +126,28 @@ class BubbleBarExpandedViewDragController( } private inner class MagnetListener : MagnetizedObject.MagnetListener { - override fun onStuckToTarget(target: MagnetizedObject.MagneticTarget) { + override fun onStuckToTarget( + target: MagnetizedObject.MagneticTarget, + draggedObject: MagnetizedObject<*> + ) { isStuckToDismiss = true } override fun onUnstuckFromTarget( - target: MagnetizedObject.MagneticTarget, - velX: Float, - velY: Float, - wasFlungOut: Boolean + target: MagnetizedObject.MagneticTarget, + draggedObject: MagnetizedObject<*>, + velX: Float, + velY: Float, + wasFlungOut: Boolean ) { isStuckToDismiss = false - animationHelper.animateUnstuckFromDismissView() + animationHelper.animateUnstuckFromDismissView(target) } - override fun onReleasedInTarget(target: MagnetizedObject.MagneticTarget) { + override fun onReleasedInTarget( + target: MagnetizedObject.MagneticTarget, + draggedObject: MagnetizedObject<*> + ) { onDismissed() dismissView.hide() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt index 7c931df35d7b..11e477716eb0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt @@ -91,8 +91,9 @@ abstract class MagnetizedObject<T : Any>( * to [onUnstuckFromTarget] or [onReleasedInTarget]. * * @param target The target that the object is now stuck to. + * @param draggedObject The object that is stuck to the target. */ - fun onStuckToTarget(target: MagneticTarget) + fun onStuckToTarget(target: MagneticTarget, draggedObject: MagnetizedObject<*>) /** * Called when the object is no longer stuck to a target. This means that either touch @@ -110,6 +111,7 @@ abstract class MagnetizedObject<T : Any>( * and [maybeConsumeMotionEvent] is now returning false. * * @param target The target that this object was just unstuck from. + * @param draggedObject The object being unstuck from the target. * @param velX The X velocity of the touch gesture when it exited the magnetic field. * @param velY The Y velocity of the touch gesture when it exited the magnetic field. * @param wasFlungOut Whether the object was unstuck via a fling gesture. This means that @@ -119,6 +121,7 @@ abstract class MagnetizedObject<T : Any>( */ fun onUnstuckFromTarget( target: MagneticTarget, + draggedObject: MagnetizedObject<*>, velX: Float, velY: Float, wasFlungOut: Boolean @@ -129,8 +132,9 @@ abstract class MagnetizedObject<T : Any>( * velocity to reach it. * * @param target The target that the object was released in. + * @param draggedObject The object released in the target. */ - fun onReleasedInTarget(target: MagneticTarget) + fun onReleasedInTarget(target: MagneticTarget, draggedObject: MagnetizedObject<*>) } private val animator: PhysicsAnimator<T> = PhysicsAnimator.getInstance(underlyingObject) @@ -386,7 +390,7 @@ abstract class MagnetizedObject<T : Any>( // animate sticking to the magnet. targetObjectIsStuckTo = targetObjectIsInMagneticFieldOf cancelAnimations() - magnetListener.onStuckToTarget(targetObjectIsInMagneticFieldOf!!) + magnetListener.onStuckToTarget(targetObjectIsInMagneticFieldOf!!, this) animateStuckToTarget(targetObjectIsInMagneticFieldOf, velX, velY, false, null) vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK) @@ -397,7 +401,8 @@ abstract class MagnetizedObject<T : Any>( // move the object out of the target using its own movement logic. cancelAnimations() magnetListener.onUnstuckFromTarget( - targetObjectIsStuckTo!!, velocityTracker.xVelocity, velocityTracker.yVelocity, + targetObjectIsStuckTo!!, this, + velocityTracker.xVelocity, velocityTracker.yVelocity, wasFlungOut = false) targetObjectIsStuckTo = null @@ -420,10 +425,11 @@ abstract class MagnetizedObject<T : Any>( // the upward direction, tell the listener so the object can be animated out of // the target. magnetListener.onUnstuckFromTarget( - targetObjectIsStuckTo!!, velX, velY, wasFlungOut = true) + targetObjectIsStuckTo!!, this, + velX, velY, wasFlungOut = true) } else { // If the object is stuck and not flung away, it was released inside the target. - magnetListener.onReleasedInTarget(targetObjectIsStuckTo!!) + magnetListener.onReleasedInTarget(targetObjectIsStuckTo!!, this) vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK) } @@ -440,11 +446,11 @@ abstract class MagnetizedObject<T : Any>( if (flungToTarget != null) { // If this is a fling-to-target, animate the object to the magnet and then release // it. - magnetListener.onStuckToTarget(flungToTarget) + magnetListener.onStuckToTarget(flungToTarget, this) targetObjectIsStuckTo = flungToTarget animateStuckToTarget(flungToTarget, velX, velY, true) { - magnetListener.onReleasedInTarget(flungToTarget) + magnetListener.onReleasedInTarget(flungToTarget, this) targetObjectIsStuckTo = null vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK) } 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 ba08b096e82b..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,18 +137,18 @@ 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.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); @@ -157,12 +158,12 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition 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/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index a80241e0ac5c..f2bdcae31956 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -16,6 +16,8 @@ package com.android.wm.shell.freeform; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; + import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM; import android.app.ActivityManager.RunningTaskInfo; @@ -151,6 +153,9 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, @Override public void onFocusTaskChanged(RunningTaskInfo taskInfo) { + if (taskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM) { + return; + } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Focus Changed: #%d focused=%b", taskInfo.taskId, taskInfo.isFocused); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java index 4e75847b6bc0..f92938989637 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java @@ -131,7 +131,8 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen }); mMagnetizedPip.setMagnetListener(new MagnetizedObject.MagnetListener() { @Override - public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { + public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject<?> draggedObject) { // Show the dismiss target, in case the initial touch event occurred within // the magnetic field radius. if (mEnableDismissDragToEdge) { @@ -141,6 +142,7 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen @Override public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject<?> draggedObject, float velX, float velY, boolean wasFlungOut) { if (wasFlungOut) { mMotionHelper.flingToSnapTarget(velX, velY, null /* endAction */); @@ -151,7 +153,8 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen } @Override - public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { + public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject<?> draggedObject) { if (mEnableDismissDragToEdge) { mMainExecutor.executeDelayed(() -> { mMotionHelper.notifyDismissalPending(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 97d3457aaa38..dffcc6df79d8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -19,6 +19,7 @@ package com.android.wm.shell.recents; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; +import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED; import static android.view.WindowManager.TRANSIT_SLEEP; @@ -591,7 +592,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { cancel("transit_sleep"); return; } - if (mKeyguardLocked || (info.getFlags() & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0) { + if (mKeyguardLocked || (info.getFlags() & KEYGUARD_VISIBILITY_TRANSIT_FLAGS) != 0) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.merge: keyguard is locked", mInstanceId); // We will not accept new changes if we are swiping over the keyguard. 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/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 3f0a28118597..185365b2a501 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -299,12 +299,34 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin ActivityManager.RunningTaskInfo taskInfo, boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { + final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode()); relayoutParams.reset(); relayoutParams.mRunningTaskInfo = taskInfo; - relayoutParams.mLayoutResId = - getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode()); + relayoutParams.mLayoutResId = captionLayoutId; relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode()); relayoutParams.mCaptionWidthId = getCaptionWidthId(relayoutParams.mLayoutResId); + + // The "app controls" type caption bar should report the occluding elements as bounding + // rects to the insets system so that apps can draw in the empty space left in the center. + if (captionLayoutId == R.layout.desktop_mode_app_controls_window_decor) { + // The "app chip" section of the caption bar, it's aligned to the left and its width + // varies depending on the length of the app name, but we'll report its max width for + // now. + // TODO(b/316387515): consider reporting the true width after it's been laid out. + final RelayoutParams.OccludingCaptionElement appChipElement = + new RelayoutParams.OccludingCaptionElement(); + appChipElement.mWidthResId = R.dimen.desktop_mode_app_details_max_width; + appChipElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.START; + relayoutParams.mOccludingCaptionElements.add(appChipElement); + // The "controls" section of the caption bar (maximize, close btns). These are aligned + // to the right of the caption bar and have a fixed width. + // TODO(b/316387515): add additional padding for an exclusive drag-move region. + final RelayoutParams.OccludingCaptionElement controlsElement = + new RelayoutParams.OccludingCaptionElement(); + controlsElement.mWidthResId = R.dimen.desktop_mode_right_edge_buttons_width; + controlsElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.END; + relayoutParams.mOccludingCaptionElements.add(controlsElement); + } if (DesktopModeStatus.useWindowShadow(/* isFocusedWindow= */ taskInfo.isFocused)) { relayoutParams.mShadowRadiusId = taskInfo.isFocused ? R.dimen.freeform_decor_shadow_focused_thickness 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 35d59401ac1a..dc65855646ea 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 @@ -50,7 +50,10 @@ import android.window.WindowContainerTransaction; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.desktopmode.DesktopModeStatus; +import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams.OccludingCaptionElement; +import java.util.ArrayList; +import java.util.List; import java.util.function.Supplier; /** @@ -293,13 +296,36 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused); // Caption insets - mCaptionInsetsRect.set(taskBounds); if (mIsCaptionVisible) { - mCaptionInsetsRect.bottom = - mCaptionInsetsRect.top + outResult.mCaptionHeight; + // Caption inset is the full width of the task with the |captionHeight| and + // positioned at the top of the task bounds, also in absolute coordinates. + // So just reuse the task bounds and adjust the bottom coordinate. + mCaptionInsetsRect.set(taskBounds); + mCaptionInsetsRect.bottom = mCaptionInsetsRect.top + outResult.mCaptionHeight; + + // Caption bounding rectangles: these are optional, and are used to present finer + // insets than traditional |Insets| to apps about where their content is occluded. + // These are also in absolute coordinates. + final Rect[] boundingRects; + final int numOfElements = params.mOccludingCaptionElements.size(); + if (numOfElements == 0) { + boundingRects = null; + } else { + boundingRects = new Rect[numOfElements]; + for (int i = 0; i < numOfElements; i++) { + final OccludingCaptionElement element = + params.mOccludingCaptionElements.get(i); + final int elementWidthPx = + resources.getDimensionPixelSize(element.mWidthResId); + boundingRects[i] = + calculateBoundingRect(element, elementWidthPx, mCaptionInsetsRect); + } + } + + // Add this caption as an inset source. wct.addInsetsSource(mTaskInfo.token, mOwner, 0 /* index */, WindowInsets.Type.captionBar(), mCaptionInsetsRect, - null /* boundingRects */); + boundingRects); wct.addInsetsSource(mTaskInfo.token, mOwner, 0 /* index */, WindowInsets.Type.mandatorySystemGestures(), mCaptionInsetsRect, null /* boundingRects */); @@ -378,6 +404,20 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } } + private Rect calculateBoundingRect(@NonNull OccludingCaptionElement element, + int elementWidthPx, @NonNull Rect captionRect) { + switch (element.mAlignment) { + case START -> { + return new Rect(0, 0, elementWidthPx, captionRect.height()); + } + case END -> { + return new Rect(captionRect.width() - elementWidthPx, 0, + captionRect.width(), captionRect.height()); + } + } + throw new IllegalArgumentException("Unexpected alignment " + element.mAlignment); + } + /** * Checks if task has entered/exited immersive mode and requires a change in caption visibility. */ @@ -555,8 +595,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> int mLayoutResId; int mCaptionHeightId; int mCaptionWidthId; - int mShadowRadiusId; + final List<OccludingCaptionElement> mOccludingCaptionElements = new ArrayList<>(); + int mShadowRadiusId; int mCornerRadius; Configuration mWindowDecorConfig; @@ -568,14 +609,28 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mLayoutResId = Resources.ID_NULL; mCaptionHeightId = Resources.ID_NULL; mCaptionWidthId = Resources.ID_NULL; - mShadowRadiusId = Resources.ID_NULL; + mOccludingCaptionElements.clear(); + mShadowRadiusId = Resources.ID_NULL; mCornerRadius = 0; mApplyStartTransactionOnDraw = false; mSetTaskPositionAndCrop = false; mWindowDecorConfig = null; } + + /** + * Describes elements within the caption bar that could occlude app content, and should be + * sent as bounding rectangles to the insets system. + */ + static class OccludingCaptionElement { + int mWidthResId; + Alignment mAlignment; + + enum Alignment { + START, END + } + } } static class RelayoutResult<T extends View & TaskFocusStateConsumer> { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt index 144373f3550e..2309c54b6591 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt @@ -8,6 +8,8 @@ import android.graphics.Bitmap import android.graphics.Color import android.view.View import android.view.View.OnLongClickListener +import android.view.WindowInsetsController.APPEARANCE_LIGHT_CAPTION_BARS +import android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView @@ -79,6 +81,9 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( @ColorInt private fun getCaptionBackgroundColor(taskInfo: RunningTaskInfo): Int { + if (isTransparentBackgroundRequested(taskInfo)) { + return Color.TRANSPARENT + } val materialColorAttr: Int = if (isDarkMode()) { if (!taskInfo.isFocused) { @@ -102,6 +107,10 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( @ColorInt private fun getAppNameAndButtonColor(taskInfo: RunningTaskInfo): Int { val materialColorAttr = when { + isTransparentBackgroundRequested(taskInfo) && + isLightCaptionBar(taskInfo) -> materialColorOnSecondaryContainer + isTransparentBackgroundRequested(taskInfo) && + !isLightCaptionBar(taskInfo) -> materialColorOnSurface isDarkMode() -> materialColorOnSurface else -> materialColorOnSecondaryContainer } @@ -132,6 +141,16 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( Configuration.UI_MODE_NIGHT_YES } + private fun isTransparentBackgroundRequested(taskInfo: RunningTaskInfo): Boolean { + val appearance = taskInfo.taskDescription?.statusBarAppearance ?: 0 + return (appearance and APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND) != 0 + } + + private fun isLightCaptionBar(taskInfo: RunningTaskInfo): Boolean { + val appearance = taskInfo.taskDescription?.statusBarAppearance ?: 0 + return (appearance and APPEARANCE_LIGHT_CAPTION_BARS) != 0 + } + companion object { private const val TAG = "DesktopModeAppControlsWindowDecorationViewHolder" private const val DARK_THEME_UNFOCUSED_OPACITY = 140 // 55% diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp index aadadd604d3e..8c4711603904 100644 --- a/libs/WindowManager/Shell/tests/unittest/Android.bp +++ b/libs/WindowManager/Shell/tests/unittest/Android.bp @@ -19,6 +19,7 @@ package { // to get the below license kinds: // SPDX-license-identifier-Apache-2.0 default_applicable_licenses: ["frameworks_base_license"], + default_team: "trendy_team_multitasking_windowing", } android_test { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java index 91503b1c3619..7e26577e96d4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java @@ -18,6 +18,7 @@ package com.android.wm.shell.back; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import android.os.Handler; import android.os.Looper; @@ -28,6 +29,7 @@ import android.window.BackMotionEvent; import android.window.BackProgressAnimator; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Before; import org.junit.Test; @@ -102,6 +104,36 @@ public class BackProgressAnimatorTest { assertEquals(mReceivedBackEvent.getProgress(), mTargetProgress, 0 /* delta */); } + @Test + public void testResetCallsCancelCallbackImmediately() throws InterruptedException { + // Give the animator some progress. + final BackMotionEvent backEvent = backMotionEventFrom(100, mTargetProgress); + mMainThreadHandler.post( + () -> mProgressAnimator.onBackProgressed(backEvent)); + mTargetProgressCalled.await(1, TimeUnit.SECONDS); + assertNotNull(mReceivedBackEvent); + + mTargetProgress = 0; + mReceivedBackEvent = null; + mTargetProgressCalled = new CountDownLatch(1); + + CountDownLatch cancelCallbackCalled = new CountDownLatch(1); + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> mProgressAnimator.onBackCancelled(cancelCallbackCalled::countDown)); + + // verify onBackProgressed and onBackCancelled not yet called + assertNull(mReceivedBackEvent); + assertEquals(1, cancelCallbackCalled.getCount()); + + // call reset + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> mProgressAnimator.reset()); + + // verify that back event with progress 0 is sent and cancel callback is invoked + assertNotNull(mReceivedBackEvent); + assertEquals(mReceivedBackEvent.getProgress(), mTargetProgress, 0 /* delta */); + assertEquals(0, cancelCallbackCalled.getCount()); + } + private void onGestureProgress(BackEvent backEvent) { if (mTargetProgress == backEvent.getProgress()) { mReceivedBackEvent = backEvent; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt index a9f054ec2b2f..a4fb3504f31d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt @@ -201,9 +201,11 @@ class MagnetizedObjectTest : ShellTestCase() { getMotionEvent(x = 200, y = 200)) // You can't become unstuck if you were never stuck in the first place. - verify(magnetListener, never()).onStuckToTarget(magneticTarget) + verify(magnetListener, never()).onStuckToTarget(magneticTarget, + magnetizedObject) verify(magnetListener, never()).onUnstuckFromTarget( - eq(magneticTarget), ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(), + eq(magneticTarget), eq(magnetizedObject), + ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(), eq(false)) // Move into and then around inside the magnetic field. @@ -213,9 +215,10 @@ class MagnetizedObjectTest : ShellTestCase() { getMotionEvent(x = targetCenterX + 100, y = targetCenterY + 100)) // We should only have received one call to onStuckToTarget and none to unstuck. - verify(magnetListener, times(1)).onStuckToTarget(magneticTarget) + verify(magnetListener, times(1)).onStuckToTarget(magneticTarget, magnetizedObject) verify(magnetListener, never()).onUnstuckFromTarget( - eq(magneticTarget), ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(), + eq(magneticTarget), eq(magnetizedObject), + ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(), eq(false)) // Move out of the field and then release. @@ -226,7 +229,8 @@ class MagnetizedObjectTest : ShellTestCase() { // We should have received one unstuck call and no more stuck calls. We also should never // have received an onReleasedInTarget call. verify(magnetListener, times(1)).onUnstuckFromTarget( - eq(magneticTarget), ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(), + eq(magneticTarget), eq(magnetizedObject), + ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(), eq(false)) verifyNoMoreInteractions(magnetListener) } @@ -242,8 +246,8 @@ class MagnetizedObjectTest : ShellTestCase() { getMotionEvent( x = targetCenterX, y = targetCenterY)) - verify(magnetListener, times(1)).onStuckToTarget(magneticTarget) - verify(magnetListener, never()).onReleasedInTarget(magneticTarget) + verify(magnetListener, times(1)).onStuckToTarget(magneticTarget, magnetizedObject) + verify(magnetListener, never()).onReleasedInTarget(magneticTarget, magnetizedObject) // Move back out. dispatchMotionEvents( @@ -252,9 +256,11 @@ class MagnetizedObjectTest : ShellTestCase() { y = targetCenterY - magneticFieldRadius)) verify(magnetListener, times(1)).onUnstuckFromTarget( - eq(magneticTarget), ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(), + eq(magneticTarget), + eq(magnetizedObject), + ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(), eq(false)) - verify(magnetListener, never()).onReleasedInTarget(magneticTarget) + verify(magnetListener, never()).onReleasedInTarget(magneticTarget, magnetizedObject) // Move in again and release in the magnetic field. dispatchMotionEvents( @@ -264,8 +270,8 @@ class MagnetizedObjectTest : ShellTestCase() { getMotionEvent( x = targetCenterX, y = targetCenterY, action = MotionEvent.ACTION_UP)) - verify(magnetListener, times(2)).onStuckToTarget(magneticTarget) - verify(magnetListener).onReleasedInTarget(magneticTarget) + verify(magnetListener, times(2)).onStuckToTarget(magneticTarget, magnetizedObject) + verify(magnetListener).onReleasedInTarget(magneticTarget, magnetizedObject) verifyNoMoreInteractions(magnetListener) } @@ -288,7 +294,7 @@ class MagnetizedObjectTest : ShellTestCase() { action = MotionEvent.ACTION_UP)) // Nevertheless it should have ended up stuck to the target. - verify(magnetListener, times(1)).onStuckToTarget(magneticTarget) + verify(magnetListener, times(1)).onStuckToTarget(magneticTarget, magnetizedObject) } @Test @@ -366,7 +372,7 @@ class MagnetizedObjectTest : ShellTestCase() { getMotionEvent(x = 100, y = 900)) // Verify that we received an onStuck for the second target, and no others. - verify(magnetListener).onStuckToTarget(secondMagneticTarget) + verify(magnetListener).onStuckToTarget(secondMagneticTarget, magnetizedObject) verifyNoMoreInteractions(magnetListener) // Drag into the original target. @@ -376,8 +382,9 @@ class MagnetizedObjectTest : ShellTestCase() { // We should have unstuck from the second one and stuck into the original one. verify(magnetListener).onUnstuckFromTarget( - eq(secondMagneticTarget), anyFloat(), anyFloat(), eq(false)) - verify(magnetListener).onStuckToTarget(magneticTarget) + eq(secondMagneticTarget), eq(magnetizedObject), + anyFloat(), anyFloat(), eq(false)) + verify(magnetListener).onStuckToTarget(magneticTarget, magnetizedObject) verifyNoMoreInteractions(magnetListener) } @@ -394,7 +401,7 @@ class MagnetizedObjectTest : ShellTestCase() { getMotionEvent(x = 100, y = 650, action = MotionEvent.ACTION_UP)) // Verify that we received an onStuck for the second target. - verify(magnetListener).onStuckToTarget(secondMagneticTarget) + verify(magnetListener).onStuckToTarget(secondMagneticTarget, magnetizedObject) // Fling towards the first target. dispatchMotionEvents( @@ -403,7 +410,7 @@ class MagnetizedObjectTest : ShellTestCase() { getMotionEvent(x = 500, y = 650, action = MotionEvent.ACTION_UP)) // Verify that we received onStuck for the original target. - verify(magnetListener).onStuckToTarget(magneticTarget) + verify(magnetListener).onStuckToTarget(magneticTarget, magnetizedObject) } @Test @@ -413,10 +420,10 @@ class MagnetizedObjectTest : ShellTestCase() { dispatchMotionEvents(getMotionEvent(x = targetCenterX, y = targetCenterY)) // Moved into the target location, but it should be shifted due to screen offset. // Should not get stuck. - verify(magnetListener, never()).onStuckToTarget(magneticTarget) + verify(magnetListener, never()).onStuckToTarget(magneticTarget, magnetizedObject) dispatchMotionEvents(getMotionEvent(x = targetCenterX, y = targetCenterY + 500)) - verify(magnetListener).onStuckToTarget(magneticTarget) + verify(magnetListener).onStuckToTarget(magneticTarget, magnetizedObject) dispatchMotionEvents( getMotionEvent( @@ -426,7 +433,7 @@ class MagnetizedObjectTest : ShellTestCase() { ) ) - verify(magnetListener).onReleasedInTarget(magneticTarget) + verify(magnetListener).onReleasedInTarget(magneticTarget, magnetizedObject) verifyNoMoreInteractions(magnetListener) } @@ -437,14 +444,15 @@ class MagnetizedObjectTest : ShellTestCase() { dispatchMotionEvents(getMotionEvent(x = targetCenterX, y = adjustedTargetCenter)) dispatchMotionEvents(getMotionEvent(x = 0, y = 0)) - verify(magnetListener).onStuckToTarget(magneticTarget) + verify(magnetListener).onStuckToTarget(magneticTarget, magnetizedObject) verify(magnetListener) - .onUnstuckFromTarget(eq(magneticTarget), anyFloat(), anyFloat(), anyBoolean()) + .onUnstuckFromTarget(eq(magneticTarget), eq(magnetizedObject), + anyFloat(), anyFloat(), anyBoolean()) // Offset if removed, we should now get stuck at the target location magneticTarget.screenVerticalOffset = 0 dispatchMotionEvents(getMotionEvent(x = targetCenterX, y = targetCenterY)) - verify(magnetListener, times(2)).onStuckToTarget(magneticTarget) + verify(magnetListener, times(2)).onStuckToTarget(magneticTarget, magnetizedObject) } @Test @@ -466,7 +474,7 @@ class MagnetizedObjectTest : ShellTestCase() { ) // Nevertheless it should have ended up stuck to the target. - verify(magnetListener, times(1)).onStuckToTarget(magneticTarget) + verify(magnetListener, times(1)).onStuckToTarget(magneticTarget, magnetizedObject) } private fun getSecondMagneticTarget(): MagnetizedObject.MagneticTarget { 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/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java new file mode 100644 index 000000000000..71eea4bb59b1 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2020 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.freeform; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestRunningTaskInfoBuilder; +import com.android.wm.shell.desktopmode.DesktopModeStatus; +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.windowdecor.WindowDecorViewModel; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.quality.Strictness; + +import java.util.Optional; + +/** + * Tests for {@link FreeformTaskListener} + * Build/Install/Run: + * atest WMShellUnitTests:FreeformTaskListenerTests + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class FreeformTaskListenerTests extends ShellTestCase { + + @Mock + private ShellTaskOrganizer mTaskOrganizer; + @Mock + private ShellInit mShellInit; + @Mock + private WindowDecorViewModel mWindowDecorViewModel; + @Mock + private DesktopModeTaskRepository mDesktopModeTaskRepository; + private FreeformTaskListener mFreeformTaskListener; + private StaticMockitoSession mMockitoSession; + + @Before + public void setup() { + mMockitoSession = mockitoSession().initMocks(this) + .strictness(Strictness.LENIENT).mockStatic(DesktopModeStatus.class).startMocking(); + when(DesktopModeStatus.isEnabled()).thenReturn(true); + mFreeformTaskListener = new FreeformTaskListener( + mShellInit, + mTaskOrganizer, + Optional.of(mDesktopModeTaskRepository), + mWindowDecorViewModel); + } + + @Test + public void testFocusTaskChanged_freeformTaskIsAddedToRepo() { + ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder() + .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + task.isFocused = true; + + mFreeformTaskListener.onFocusTaskChanged(task); + + verify(mDesktopModeTaskRepository).addOrMoveFreeformTaskToTop(task.taskId); + } + + @Test + public void testFocusTaskChanged_fullscreenTaskIsNotAddedToRepo() { + ActivityManager.RunningTaskInfo fullscreenTask = new TestRunningTaskInfoBuilder() + .setWindowingMode(WINDOWING_MODE_FULLSCREEN).build(); + fullscreenTask.isFocused = true; + + mFreeformTaskListener.onFocusTaskChanged(fullscreenTask); + + verify(mDesktopModeTaskRepository, never()) + .addOrMoveFreeformTaskToTop(fullscreenTask.taskId); + } + + @After + public void tearDown() { + mMockitoSession.finishMocking(); + } +} diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index 46f636e2ae7f..8748dab581bb 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -117,10 +117,6 @@ bool AssetManager2::SetApkAssets(ApkAssetsList apk_assets, bool invalidate_cache return true; } -void AssetManager2::PresetApkAssets(ApkAssetsList apk_assets) { - BuildDynamicRefTable(apk_assets); -} - bool AssetManager2::SetApkAssets(std::initializer_list<ApkAssetsPtr> apk_assets, bool invalidate_caches) { return SetApkAssets(ApkAssetsList(apk_assets.begin(), apk_assets.size()), invalidate_caches); @@ -436,18 +432,13 @@ bool AssetManager2::ContainsAllocatedTable() const { return false; } -void AssetManager2::SetConfigurations(std::vector<ResTable_config> configurations, - bool force_refresh) { +void AssetManager2::SetConfigurations(std::vector<ResTable_config> configurations) { int diff = 0; - if (force_refresh) { + if (configurations_.size() != configurations.size()) { diff = -1; } else { - if (configurations_.size() != configurations.size()) { - diff = -1; - } else { - for (int i = 0; i < configurations_.size(); i++) { - diff |= configurations_[i].diff(configurations[i]); - } + for (int i = 0; i < configurations_.size(); i++) { + diff |= configurations_[i].diff(configurations[i]); } } configurations_ = std::move(configurations); @@ -784,7 +775,8 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry( bool has_locale = false; if (result->config.locale == 0) { if (default_locale_ != 0) { - ResTable_config conf = {.locale = default_locale_}; + ResTable_config conf; + conf.locale = default_locale_; // Since we know conf has a locale and only a locale, match will tell us if that locale // matches has_locale = conf.match(config); diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h index 17a8ba6c03bd..d9ff35b49e0a 100644 --- a/libs/androidfw/include/androidfw/AssetManager2.h +++ b/libs/androidfw/include/androidfw/AssetManager2.h @@ -124,9 +124,6 @@ class AssetManager2 { // new resource IDs. bool SetApkAssets(ApkAssetsList apk_assets, bool invalidate_caches = true); bool SetApkAssets(std::initializer_list<ApkAssetsPtr> apk_assets, bool invalidate_caches = true); - // This one is an optimization - it skips all calculations for applying the currently set - // configuration, expecting a configuration update later with a forced refresh. - void PresetApkAssets(ApkAssetsList apk_assets); const ApkAssetsPtr& GetApkAssets(ApkAssetsCookie cookie) const; int GetApkAssetsCount() const { @@ -159,7 +156,7 @@ class AssetManager2 { // Sets/resets the configuration for this AssetManager. This will cause all // caches that are related to the configuration change to be invalidated. - void SetConfigurations(std::vector<ResTable_config> configurations, bool force_refresh = false); + void SetConfigurations(std::vector<ResTable_config> configurations); inline const std::vector<ResTable_config>& GetConfigurations() const { return configurations_; diff --git a/media/java/android/media/AudioDevicePort.java b/media/java/android/media/AudioDevicePort.java index 2de8eefc4e78..ab5c54b5cd7e 100644 --- a/media/java/android/media/AudioDevicePort.java +++ b/media/java/android/media/AudioDevicePort.java @@ -33,7 +33,7 @@ import java.util.List; * device at the boundary of the audio system. * In addition to base audio port attributes, the device descriptor contains: * - the device type (e.g AudioManager.DEVICE_OUT_SPEAKER) - * - the device address (e.g MAC adddress for AD2P sink). + * - the device address (e.g MAC address for AD2P sink). * @see AudioPort * @hide */ diff --git a/media/java/android/media/AudioHalVersionInfo.java b/media/java/android/media/AudioHalVersionInfo.java index 4cdfc515fe9a..2b6f72eabeec 100644 --- a/media/java/android/media/AudioHalVersionInfo.java +++ b/media/java/android/media/AudioHalVersionInfo.java @@ -79,6 +79,9 @@ public final class AudioHalVersionInfo implements Parcelable, Comparable<AudioHa /** * List of all valid Audio HAL versions. This list need to be in sync with sAudioHALVersions * defined in frameworks/av/media/libaudiohal/FactoryHal.cpp. + * + * Note: update {@link android.media.audio.cts.AudioHalVersionInfoTest} CTS accordingly if + * there is a change to supported versions. */ public static final @NonNull List<AudioHalVersionInfo> VERSIONS = List.of(AIDL_1_0, HIDL_7_1, HIDL_7_0, HIDL_6_0, HIDL_5_0); diff --git a/media/java/android/media/OWNERS b/media/java/android/media/OWNERS index 49890c150971..8cc42e0bd9b5 100644 --- a/media/java/android/media/OWNERS +++ b/media/java/android/media/OWNERS @@ -11,3 +11,8 @@ per-file *Image* = file:/graphics/java/android/graphics/OWNERS per-file ExifInterface.java,ExifInterfaceUtils.java,IMediaHTTPConnection.aidl,IMediaHTTPService.aidl,JetPlayer.java,MediaDataSource.java,MediaExtractor.java,MediaHTTPConnection.java,MediaHTTPService.java,MediaPlayer.java=set noparent per-file ExifInterface.java,ExifInterfaceUtils.java,IMediaHTTPConnection.aidl,IMediaHTTPService.aidl,JetPlayer.java,MediaDataSource.java,MediaExtractor.java,MediaHTTPConnection.java,MediaHTTPService.java,MediaPlayer.java=file:platform/frameworks/av:/media/janitors/media_solutions_OWNERS + +# Haptics team also works on Ringtone +per-file *Ringtone* = file:/services/core/java/com/android/server/vibrator/OWNERS + +per-file flags/projection.aconfig = file:projection/OWNERS diff --git a/media/java/android/media/metrics/EditingEndedEvent.java b/media/java/android/media/metrics/EditingEndedEvent.java index f1c5c9d0c748..9b3477f4bd69 100644 --- a/media/java/android/media/metrics/EditingEndedEvent.java +++ b/media/java/android/media/metrics/EditingEndedEvent.java @@ -18,6 +18,7 @@ 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; @@ -60,6 +61,8 @@ public final class EditingEndedEvent extends Event implements Parcelable { private final @FinalState int mFinalState; + private final float mFinalProgressPercent; + // The special value 0 is reserved for the field being unspecified in the proto. /** Special value representing that no error occurred. */ @@ -155,10 +158,15 @@ public final class EditingEndedEvent extends Event implements Parcelable { /** Special value for unknown {@linkplain #getTimeSinceCreatedMillis() time since creation}. */ public static final int TIME_SINCE_CREATED_UNKNOWN = -1; + /** Special value for unknown {@linkplain #getFinalProgressPercent() final progress}. */ + public static final int PROGRESS_PERCENT_UNKNOWN = -1; + private final @ErrorCode int mErrorCode; @SuppressWarnings("HidingField") // Hiding field from superclass as for playback events. private final long mTimeSinceCreatedMillis; + @Nullable private final String mExporterName; + @Nullable private final String mMuxerName; private final ArrayList<MediaItemInfo> mInputMediaItemInfos; @Nullable private final MediaItemInfo mOutputMediaItemInfo; @@ -207,15 +215,21 @@ public final class EditingEndedEvent extends Event implements Parcelable { private EditingEndedEvent( @FinalState int finalState, + float finalProgressPercent, @ErrorCode int errorCode, long timeSinceCreatedMillis, + @Nullable String exporterName, + @Nullable String muxerName, ArrayList<MediaItemInfo> inputMediaItemInfos, @Nullable MediaItemInfo outputMediaItemInfo, @OperationType long operationTypes, @NonNull Bundle extras) { mFinalState = finalState; + mFinalProgressPercent = finalProgressPercent; mErrorCode = errorCode; mTimeSinceCreatedMillis = timeSinceCreatedMillis; + mExporterName = exporterName; + mMuxerName = muxerName; mInputMediaItemInfos = inputMediaItemInfos; mOutputMediaItemInfo = outputMediaItemInfo; mOperationTypes = operationTypes; @@ -228,6 +242,14 @@ public final class EditingEndedEvent extends Event implements Parcelable { return mFinalState; } + /** + * Returns the progress of the editing operation in percent at the moment that it ended, or + * {@link #PROGRESS_PERCENT_UNKNOWN} if unknown. + */ + public float getFinalProgressPercent() { + return mFinalProgressPercent; + } + /** Returns the error code for a {@linkplain #FINAL_STATE_ERROR failed} editing session. */ @ErrorCode public int getErrorCode() { @@ -249,6 +271,24 @@ public final class EditingEndedEvent extends Event implements Parcelable { return mTimeSinceCreatedMillis; } + /** + * Returns the name of the library implementing the exporting operation, or {@code null} if + * unknown. + */ + @Nullable + public String getExporterName() { + return mExporterName; + } + + /** + * Returns the name of the library implementing the media muxing operation, or {@code null} if + * unknown. + */ + @Nullable + public String getMuxerName() { + return mMuxerName; + } + /** Gets information about the input media items, or an empty list if unspecified. */ @NonNull public List<MediaItemInfo> getInputMediaItemInfos() { @@ -284,12 +324,21 @@ public final class EditingEndedEvent extends Event implements Parcelable { + "finalState = " + mFinalState + ", " + + "finalProgressPercent = " + + mFinalProgressPercent + + ", " + "errorCode = " + mErrorCode + ", " + "timeSinceCreatedMillis = " + mTimeSinceCreatedMillis + ", " + + "exporterName = " + + mExporterName + + ", " + + "muxerName = " + + mMuxerName + + ", " + "inputMediaItemInfos = " + mInputMediaItemInfos + ", " @@ -307,29 +356,38 @@ public final class EditingEndedEvent extends Event implements Parcelable { if (o == null || getClass() != o.getClass()) return false; EditingEndedEvent that = (EditingEndedEvent) o; return mFinalState == that.mFinalState + && mFinalProgressPercent == that.mFinalProgressPercent && mErrorCode == that.mErrorCode && Objects.equals(mInputMediaItemInfos, that.mInputMediaItemInfos) && Objects.equals(mOutputMediaItemInfo, that.mOutputMediaItemInfo) && mOperationTypes == that.mOperationTypes - && mTimeSinceCreatedMillis == that.mTimeSinceCreatedMillis; + && mTimeSinceCreatedMillis == that.mTimeSinceCreatedMillis + && Objects.equals(mExporterName, that.mExporterName) + && Objects.equals(mMuxerName, that.mMuxerName); } @Override public int hashCode() { return Objects.hash( mFinalState, + mFinalProgressPercent, mErrorCode, mInputMediaItemInfos, mOutputMediaItemInfo, mOperationTypes, - mTimeSinceCreatedMillis); + mTimeSinceCreatedMillis, + mExporterName, + mMuxerName); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mFinalState); + dest.writeFloat(mFinalProgressPercent); dest.writeInt(mErrorCode); dest.writeLong(mTimeSinceCreatedMillis); + dest.writeString(mExporterName); + dest.writeString(mMuxerName); dest.writeTypedList(mInputMediaItemInfos); dest.writeTypedObject(mOutputMediaItemInfo, /* parcelableFlags= */ 0); dest.writeLong(mOperationTypes); @@ -343,8 +401,11 @@ public final class EditingEndedEvent extends Event implements Parcelable { private EditingEndedEvent(@NonNull Parcel in) { mFinalState = in.readInt(); + mFinalProgressPercent = in.readFloat(); mErrorCode = in.readInt(); mTimeSinceCreatedMillis = in.readLong(); + mExporterName = in.readString(); + mMuxerName = in.readString(); mInputMediaItemInfos = new ArrayList<>(); in.readTypedList(mInputMediaItemInfos, MediaItemInfo.CREATOR); mOutputMediaItemInfo = in.readTypedObject(MediaItemInfo.CREATOR); @@ -370,8 +431,11 @@ public final class EditingEndedEvent extends Event implements Parcelable { public static final class Builder { private final @FinalState int mFinalState; private final ArrayList<MediaItemInfo> mInputMediaItemInfos; + private float mFinalProgressPercent; private @ErrorCode int mErrorCode; private long mTimeSinceCreatedMillis; + @Nullable private String mExporterName; + @Nullable private String mMuxerName; @Nullable private MediaItemInfo mOutputMediaItemInfo; private @OperationType long mOperationTypes; private Bundle mMetricsBundle; @@ -383,6 +447,7 @@ public final class EditingEndedEvent extends Event implements Parcelable { */ public Builder(@FinalState int finalState) { mFinalState = finalState; + mFinalProgressPercent = PROGRESS_PERCENT_UNKNOWN; mErrorCode = ERROR_CODE_NONE; mTimeSinceCreatedMillis = TIME_SINCE_CREATED_UNKNOWN; mInputMediaItemInfos = new ArrayList<>(); @@ -390,6 +455,19 @@ public final class EditingEndedEvent extends Event implements Parcelable { } /** + * Sets the progress of the editing operation in percent at the moment that it ended. + * + * @param finalProgressPercent The progress of the editing operation in percent at the + * moment that it ended. + * @see #getFinalProgressPercent() + */ + public @NonNull Builder setFinalProgressPercent( + @FloatRange(from = 0, to = 100) float finalProgressPercent) { + mFinalProgressPercent = finalProgressPercent; + return this; + } + + /** * Sets the elapsed time since creating the editing session, in milliseconds. * * @param timeSinceCreatedMillis The elapsed time since creating the editing session, in @@ -402,6 +480,30 @@ public final class EditingEndedEvent extends Event implements Parcelable { return this; } + /** + * The name of the library implementing the exporting operation. For example, a Maven + * artifact ID like "androidx.media3.media3-transformer:1.3.0-beta01". + * + * @param exporterName The name of the library implementing the export operation. + * @see #getExporterName() + */ + public @NonNull Builder setExporterName(@NonNull String exporterName) { + mExporterName = Objects.requireNonNull(exporterName); + return this; + } + + /** + * The name of the library implementing the media muxing operation. For example, a Maven + * artifact ID like "androidx.media3.media3-muxer:1.3.0-beta01". + * + * @param muxerName The name of the library implementing the media muxing operation. + * @see #getMuxerName() + */ + public @NonNull Builder setMuxerName(@NonNull String muxerName) { + mMuxerName = Objects.requireNonNull(muxerName); + return this; + } + /** Sets the error code for a {@linkplain #FINAL_STATE_ERROR failed} editing session. */ public @NonNull Builder setErrorCode(@ErrorCode int value) { mErrorCode = value; @@ -444,8 +546,11 @@ public final class EditingEndedEvent extends Event implements Parcelable { public @NonNull EditingEndedEvent build() { return new EditingEndedEvent( mFinalState, + mFinalProgressPercent, mErrorCode, mTimeSinceCreatedMillis, + mExporterName, + mMuxerName, mInputMediaItemInfos, mOutputMediaItemInfo, mOperationTypes, 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/SettingsLib/res/values/arrays.xml b/packages/SettingsLib/res/values/arrays.xml index 3252aad6262f..1b29e8367a78 100644 --- a/packages/SettingsLib/res/values/arrays.xml +++ b/packages/SettingsLib/res/values/arrays.xml @@ -526,7 +526,7 @@ </string-array> <!-- USB configuration values for Developer Settings. - These are lists of USB functions passed to the USB Manager to change USB configuraton. + These are lists of USB functions passed to the USB Manager to change USB configuration. This can be overridden by devices with additional USB configurations. Do not translate. --> <string-array name="usb_configuration_values" translatable="false"> diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java index 58e0a89d5387..1150ac10c063 100644 --- a/packages/SettingsLib/src/com/android/settingslib/Utils.java +++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java @@ -50,6 +50,7 @@ import android.util.Log; import android.webkit.IWebViewUpdateService; import android.webkit.WebViewFactory; import android.webkit.WebViewProviderInfo; +import android.webkit.WebViewUpdateManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -495,16 +496,26 @@ public class Utils { return sDefaultWebViewPackageName; } - try { - IWebViewUpdateService service = WebViewFactory.getUpdateService(); - if (service != null) { - WebViewProviderInfo provider = service.getDefaultWebViewPackage(); - if (provider != null) { - sDefaultWebViewPackageName = provider.packageName; + WebViewProviderInfo provider = null; + + if (android.webkit.Flags.updateServiceIpcWrapper()) { + WebViewUpdateManager manager = WebViewUpdateManager.getInstance(); + if (manager != null) { + provider = manager.getDefaultWebViewPackage(); + } + } else { + try { + IWebViewUpdateService service = WebViewFactory.getUpdateService(); + if (service != null) { + provider = service.getDefaultWebViewPackage(); } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException when trying to fetch default WebView package Name", e); } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException when trying to fetch default WebView package Name", e); + } + + if (provider != null) { + sDefaultWebViewPackageName = provider.packageName; } return sDefaultWebViewPackageName; } diff --git a/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/model/ZenMode.kt b/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/model/ZenMode.kt new file mode 100644 index 000000000000..a696f8c9c9c9 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/model/ZenMode.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.settingslib.statusbar.notification.data.model + +import android.provider.Settings.Global + +/** Validating wrapper for [android.app.NotificationManager.getZenMode] values. */ +@JvmInline +value class ZenMode(val zenMode: Int) { + + init { + require(zenMode in supportedModes) { "Unsupported zenMode=$zenMode" } + } + + private companion object { + + val supportedModes = + listOf( + Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, + Global.ZEN_MODE_NO_INTERRUPTIONS, + Global.ZEN_MODE_ALARMS, + Global.ZEN_MODE_OFF, + ) + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/FakeNotificationsSoundPolicyRepository.kt b/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/FakeNotificationsSoundPolicyRepository.kt new file mode 100644 index 000000000000..60983070b1cf --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/FakeNotificationsSoundPolicyRepository.kt @@ -0,0 +1,42 @@ +/* + * 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.settingslib.statusbar.notification.data.repository + +import android.app.NotificationManager +import com.android.settingslib.statusbar.notification.data.model.ZenMode +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeNotificationsSoundPolicyRepository : NotificationsSoundPolicyRepository { + + private val mutableNotificationPolicy = MutableStateFlow<NotificationManager.Policy?>(null) + override val notificationPolicy: StateFlow<NotificationManager.Policy?> + get() = mutableNotificationPolicy.asStateFlow() + + private val mutableZenMode = MutableStateFlow<ZenMode?>(null) + override val zenMode: StateFlow<ZenMode?> + get() = mutableZenMode.asStateFlow() + + fun updateNotificationPolicy(policy: NotificationManager.Policy?) { + mutableNotificationPolicy.value = policy + } + + fun updateZenMode(zenMode: ZenMode?) { + mutableZenMode.value = zenMode + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/NotificationsSoundPolicyRepository.kt b/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/NotificationsSoundPolicyRepository.kt new file mode 100644 index 000000000000..0fb8c3f8bc6d --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/NotificationsSoundPolicyRepository.kt @@ -0,0 +1,95 @@ +/* + * 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.settingslib.statusbar.notification.data.repository + +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import com.android.settingslib.statusbar.notification.data.model.ZenMode +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** Provides state of volume policy and restrictions imposed by notifications. */ +interface NotificationsSoundPolicyRepository { + + /** @see NotificationManager.getNotificationPolicy */ + val notificationPolicy: StateFlow<NotificationManager.Policy?> + + /** @see NotificationManager.getZenMode */ + val zenMode: StateFlow<ZenMode?> +} + +class NotificationsSoundPolicyRepositoryImpl( + private val context: Context, + private val notificationManager: NotificationManager, + scope: CoroutineScope, + backgroundCoroutineContext: CoroutineContext, +) : NotificationsSoundPolicyRepository { + + private val notificationBroadcasts = + callbackFlow { + val receiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + intent?.action?.let { action -> launch { send(action) } } + } + } + + context.registerReceiver( + receiver, + IntentFilter().apply { + addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED) + addAction(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED) + } + ) + + awaitClose { context.unregisterReceiver(receiver) } + } + .shareIn( + started = SharingStarted.WhileSubscribed(), + scope = scope, + ) + + override val notificationPolicy: StateFlow<NotificationManager.Policy?> = + notificationBroadcasts + .filter { NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED == it } + .map { notificationManager.consolidatedNotificationPolicy } + .onStart { emit(notificationManager.consolidatedNotificationPolicy) } + .flowOn(backgroundCoroutineContext) + .stateIn(scope, SharingStarted.WhileSubscribed(), null) + + override val zenMode: StateFlow<ZenMode?> = + notificationBroadcasts + .filter { NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED == it } + .map { ZenMode(notificationManager.zenMode) } + .onStart { emit(ZenMode(notificationManager.zenMode)) } + .flowOn(backgroundCoroutineContext) + .stateIn(scope, SharingStarted.WhileSubscribed(), null) +} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/statusbar/notification/data/repository/NotificationsSoundPolicyRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/statusbar/notification/data/repository/NotificationsSoundPolicyRepositoryTest.kt new file mode 100644 index 000000000000..dfc4c0a847c0 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/statusbar/notification/data/repository/NotificationsSoundPolicyRepositoryTest.kt @@ -0,0 +1,128 @@ +/* + * 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.settingslib.statusbar.notification.data.repository + +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.provider.Settings.Global +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.statusbar.notification.data.model.ZenMode +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +@SmallTest +class NotificationsSoundPolicyRepositoryTest { + + @Mock private lateinit var context: Context + @Mock private lateinit var notificationManager: NotificationManager + @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver> + + private lateinit var underTest: NotificationsSoundPolicyRepository + + private val testScope: TestScope = TestScope() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + underTest = + NotificationsSoundPolicyRepositoryImpl( + context, + notificationManager, + testScope.backgroundScope, + testScope.testScheduler, + ) + } + + @Test + fun policyChanges_repositoryEmits() { + testScope.runTest { + val values = mutableListOf<NotificationManager.Policy?>() + `when`(notificationManager.notificationPolicy).thenReturn(testPolicy1) + underTest.notificationPolicy.onEach { values.add(it) }.launchIn(backgroundScope) + runCurrent() + + `when`(notificationManager.notificationPolicy).thenReturn(testPolicy2) + triggerIntent(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED) + runCurrent() + + assertThat(values) + .containsExactlyElementsIn(listOf(null, testPolicy1, testPolicy2)) + .inOrder() + } + } + + @Test + fun zenModeChanges_repositoryEmits() { + testScope.runTest { + val values = mutableListOf<ZenMode?>() + `when`(notificationManager.zenMode).thenReturn(Global.ZEN_MODE_OFF) + underTest.zenMode.onEach { values.add(it) }.launchIn(backgroundScope) + runCurrent() + + `when`(notificationManager.zenMode).thenReturn(Global.ZEN_MODE_ALARMS) + triggerIntent(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED) + runCurrent() + + assertThat(values) + .containsExactlyElementsIn( + listOf(null, ZenMode(Global.ZEN_MODE_OFF), ZenMode(Global.ZEN_MODE_ALARMS)) + ) + .inOrder() + } + } + + private fun triggerIntent(action: String) { + verify(context).registerReceiver(receiverCaptor.capture(), any()) + receiverCaptor.value.onReceive(context, Intent(action)) + } + + private companion object { + val testPolicy1 = + NotificationManager.Policy( + /* priorityCategories = */ 1, + /* priorityCallSenders =*/ 1, + /* priorityMessageSenders = */ 1, + ) + val testPolicy2 = + NotificationManager.Policy( + /* priorityCategories = */ 2, + /* priorityCallSenders =*/ 2, + /* priorityMessageSenders = */ 2, + ) + } +} diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 8ae117e4143c..e424797c664e 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, @@ -264,6 +265,7 @@ public class SecureSettings { Settings.Secure.EVEN_DIMMER_ACTIVATED, Settings.Secure.EVEN_DIMMER_MIN_NITS, Settings.Secure.STYLUS_POINTER_ICON_ENABLED, - Settings.Secure.CAMERA_EXTENSIONS_FALLBACK + Settings.Secure.CAMERA_EXTENSIONS_FALLBACK, + Settings.Secure.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED }; } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 5adae37533ff..a32eeadc2982 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -207,6 +207,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.ASSIST_GESTURE_WAKE_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ASSIST_TOUCH_GESTURE_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ASSIST_LONG_PRESS_HOME_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SEARCH_LONG_PRESS_HOME_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.VR_DISPLAY_MODE, new DiscreteValueValidator(new String[] {"0", "1"})); @@ -397,6 +398,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/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 612badd9e363..d27ff175ca25 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -1951,6 +1951,9 @@ class SettingsProtoDumpUtil { dumpSetting(s, p, Settings.Secure.SEARCH_LONG_PRESS_HOME_ENABLED, SecureSettingsProto.Assist.SEARCH_LONG_PRESS_HOME_ENABLED); + dumpSetting(s, p, + Settings.Secure.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED, + SecureSettingsProto.Assist.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED); p.end(assistToken); final long assistHandlesToken = p.start(SecureSettingsProto.ASSIST_HANDLES); diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index c086baa7c8db..9e5e7a2474cb 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -250,6 +250,8 @@ <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" /> <uses-permission android:name="android.permission.MOUNT_FORMAT_FILESYSTEMS" /> <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" /> + <!-- Permission required to test LauncherApps APIs for hidden profiles --> + <uses-permission android:name="android.permission.ACCESS_HIDDEN_PROFILES_FULL" /> <!-- Shell only holds android.permission.NETWORK_SCAN in order to to enable CTS testing --> <uses-permission android:name="android.permission.NETWORK_SCAN" /> <uses-permission android:name="android.permission.REGISTER_CALL_PROVIDER" /> diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS index 967a36b38090..78cacec6d4db 100644 --- a/packages/SystemUI/OWNERS +++ b/packages/SystemUI/OWNERS @@ -39,7 +39,6 @@ hwwang@google.com hyunyoungs@google.com ikateryna@google.com iyz@google.com -jaggies@google.com jamesoleary@google.com jbolinger@google.com jdemeulenaere@google.com diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 56576f1f0c03..c2072ac8770d 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -420,3 +420,23 @@ flag { } } +flag { + name: "get_connected_device_name_unsynchronized" + namespace: "systemui" + description: "Decide whether to fetch the connected bluetooth device name outside a synchronized block." + bug: "323995015" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "update_user_switcher_background" + namespace: "systemui" + description: "Decide whether to update user switcher in background thread." + bug: "322745650" + metadata { + purpose: PURPOSE_BUGFIX + } +} + diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt new file mode 100644 index 000000000000..b8c4fae975af --- /dev/null +++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt @@ -0,0 +1,321 @@ +/* + * 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(ExperimentalMaterial3Api::class) + +package com.android.compose + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.android.compose.modifiers.padding +import com.android.compose.theme.LocalAndroidColorScheme + +/** Indicator corner radius used when the user drags the [PlatformSlider]. */ +private val DefaultPlatformSliderDraggingCornerRadius = 8.dp + +/** + * Platform slider implementation that displays a slider with an [icon] and a [label] at the start. + * + * @param onValueChangeFinished is called when the slider settles on a [value]. This callback + * shouldn't be used to react to value changes. Use [onValueChange] instead + * @param interactionSource - the [MutableInteractionSource] representing the stream of Interactions + * for this slider. You can create and pass in your own remembered instance to observe + * Interactions and customize the appearance / behavior of this slider in different states. + * @param colors - slider color scheme. + * @param draggingCornersRadius - radius of the slider indicator when the user drags it + * @param icon - icon at the start of the slider. Icon is limited to a square space at the start of + * the slider + * @param label - control shown next to the icon. + */ +@Composable +fun PlatformSlider( + value: Float, + onValueChange: (Float) -> Unit, + modifier: Modifier = Modifier, + onValueChangeFinished: (() -> Unit)? = null, + valueRange: ClosedFloatingPointRange<Float> = 0f..1f, + enabled: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + colors: PlatformSliderColors = + if (isSystemInDarkTheme()) darkThemePlatformSliderColors() + else lightThemePlatformSliderColors(), + draggingCornersRadius: Dp = DefaultPlatformSliderDraggingCornerRadius, + icon: (@Composable (isDragging: Boolean) -> Unit)? = null, + label: (@Composable (isDragging: Boolean) -> Unit)? = null, +) { + val sliderHeight: Dp = 64.dp + val iconWidth: Dp = sliderHeight + var isDragging by remember { mutableStateOf(false) } + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is DragInteraction.Start -> { + isDragging = true + } + is DragInteraction.Cancel, + is DragInteraction.Stop -> { + isDragging = false + } + } + } + } + val paddingStart by + animateDpAsState( + targetValue = + if ((!isDragging && value == 0f) || icon == null) { + 16.dp + } else { + 0.dp + }, + label = "LabelIconSpacingAnimation" + ) + + Box(modifier = modifier.height(sliderHeight)) { + Slider( + modifier = Modifier.fillMaxSize(), + value = value, + onValueChange = onValueChange, + valueRange = valueRange, + onValueChangeFinished = onValueChangeFinished, + interactionSource = interactionSource, + track = { + Track( + sliderState = it, + enabled = enabled, + colors = colors, + iconWidth = iconWidth, + draggingCornersRadius = draggingCornersRadius, + sliderHeight = sliderHeight, + isDragging = isDragging, + modifier = Modifier, + ) + }, + thumb = { Spacer(Modifier.width(iconWidth).height(sliderHeight)) }, + ) + + if (icon != null || label != null) { + Row(modifier = Modifier.fillMaxSize()) { + icon?.let { iconComposable -> + Box( + modifier = Modifier.fillMaxHeight().aspectRatio(1f), + contentAlignment = Alignment.Center, + ) { + iconComposable(isDragging) + } + } + + label?.let { labelComposable -> + Box( + modifier = + Modifier.fillMaxHeight() + .weight(1f) + .padding(start = { paddingStart.roundToPx() }), + contentAlignment = Alignment.CenterStart, + ) { + labelComposable(isDragging) + } + } + } + } + } +} + +@Composable +private fun Track( + sliderState: SliderState, + enabled: Boolean, + colors: PlatformSliderColors, + iconWidth: Dp, + draggingCornersRadius: Dp, + sliderHeight: Dp, + isDragging: Boolean, + modifier: Modifier = Modifier, +) { + val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl + val iconWidthPx: Float + val halfIconWidthPx: Float + val targetIndicatorRadiusPx: Float + val halfSliderHeightPx: Float + with(LocalDensity.current) { + halfSliderHeightPx = sliderHeight.toPx() / 2 + iconWidthPx = iconWidth.toPx() + halfIconWidthPx = iconWidthPx / 2 + targetIndicatorRadiusPx = + if (isDragging) draggingCornersRadius.toPx() else halfSliderHeightPx + } + + val indicatorRadiusPx: Float by + animateFloatAsState( + targetValue = targetIndicatorRadiusPx, + label = "PlatformSliderCornersAnimation", + ) + + val trackColor = colors.getTrackColor(enabled) + val indicatorColor = colors.getIndicatorColor(enabled) + val trackCornerRadius = CornerRadius(halfSliderHeightPx, halfSliderHeightPx) + val indicatorCornerRadius = CornerRadius(indicatorRadiusPx, indicatorRadiusPx) + Canvas(modifier.fillMaxSize()) { + val trackPath = Path() + trackPath.addRoundRect( + RoundRect( + left = -halfIconWidthPx, + top = 0f, + right = size.width + halfIconWidthPx, + bottom = size.height, + cornerRadius = trackCornerRadius, + ) + ) + drawPath(path = trackPath, color = trackColor) + + clipPath(trackPath) { + val indicatorPath = Path() + if (isRtl) { + indicatorPath.addRoundRect( + RoundRect( + left = + size.width - + size.width * sliderState.coercedNormalizedValue - + halfIconWidthPx, + top = 0f, + right = size.width + iconWidthPx, + bottom = size.height, + topLeftCornerRadius = indicatorCornerRadius, + topRightCornerRadius = trackCornerRadius, + bottomRightCornerRadius = trackCornerRadius, + bottomLeftCornerRadius = indicatorCornerRadius, + ) + ) + } else { + indicatorPath.addRoundRect( + RoundRect( + left = -halfIconWidthPx, + top = 0f, + right = size.width * sliderState.coercedNormalizedValue + halfIconWidthPx, + bottom = size.height, + topLeftCornerRadius = trackCornerRadius, + topRightCornerRadius = indicatorCornerRadius, + bottomRightCornerRadius = indicatorCornerRadius, + bottomLeftCornerRadius = trackCornerRadius, + ) + ) + } + drawPath(path = indicatorPath, color = indicatorColor) + } + } +} + +/** [SliderState.value] normalized using [SliderState.valueRange]. The result belongs to [0, 1] */ +private val SliderState.coercedNormalizedValue: Float + get() { + val dif = valueRange.endInclusive - valueRange.start + return if (dif == 0f) { + 0f + } else { + val coercedValue = value.coerceIn(valueRange.start, valueRange.endInclusive) + (coercedValue - valueRange.start) / dif + } + } + +/** + * [PlatformSlider] color scheme. + * + * @param trackColor fills the track of the slider. This is a "background" of the slider + * @param indicatorColor fills the slider from the start to the value + * @param iconColor is the default icon color + * @param labelColor is the default icon color + * @param disabledTrackColor is the [trackColor] when the PlatformSlider#enabled == false + * @param disabledIndicatorColor is the [indicatorColor] when the PlatformSlider#enabled == false + * @param disabledIconColor is the [iconColor] when the PlatformSlider#enabled == false + * @param disabledLabelColor is the [labelColor] when the PlatformSlider#enabled == false + */ +data class PlatformSliderColors( + val trackColor: Color, + val indicatorColor: Color, + val iconColor: Color, + val labelColor: Color, + val disabledTrackColor: Color, + val disabledIndicatorColor: Color, + val disabledIconColor: Color, + val disabledLabelColor: Color, +) + +/** [PlatformSliderColors] for the light theme */ +@Composable +private fun lightThemePlatformSliderColors() = + PlatformSliderColors( + trackColor = MaterialTheme.colorScheme.tertiaryContainer, + indicatorColor = LocalAndroidColorScheme.current.tertiaryFixedDim, + iconColor = MaterialTheme.colorScheme.onTertiaryContainer, + labelColor = MaterialTheme.colorScheme.onTertiaryContainer, + disabledTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest, + disabledIndicatorColor = MaterialTheme.colorScheme.surfaceContainerHighest, + disabledIconColor = MaterialTheme.colorScheme.outline, + disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant, + ) + +/** [PlatformSliderColors] for the dark theme */ +@Composable +private fun darkThemePlatformSliderColors() = + PlatformSliderColors( + trackColor = MaterialTheme.colorScheme.onTertiary, + indicatorColor = LocalAndroidColorScheme.current.onTertiaryFixedVariant, + iconColor = MaterialTheme.colorScheme.onTertiaryContainer, + labelColor = MaterialTheme.colorScheme.onTertiaryContainer, + disabledTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest, + disabledIndicatorColor = MaterialTheme.colorScheme.surfaceContainerHighest, + disabledIconColor = MaterialTheme.colorScheme.outline, + disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant, + ) + +private fun PlatformSliderColors.getTrackColor(isEnabled: Boolean): Color = + if (isEnabled) trackColor else disabledTrackColor + +private fun PlatformSliderColors.getIndicatorColor(isEnabled: Boolean): Color = + if (isEnabled) indicatorColor else disabledIndicatorColor diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt index 36ab46b4e616..e6424345da39 100644 --- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -87,6 +87,7 @@ object ComposeFacade : BaseComposeFacade { viewModel: SceneContainerViewModel, windowInsets: StateFlow<WindowInsets?>, sceneByKey: Map<SceneKey, Scene>, + dataSourceDelegator: SceneDataSourceBinder, ): View { throwComposeUnavailableError() } diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt index 5b6aa09e620e..a1bbc7d7bfb8 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -53,6 +53,7 @@ import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.compose.FooterActions import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.scene.shared.model.Scene +import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.composable.ComposableScene import com.android.systemui.scene.ui.composable.SceneContainer @@ -127,6 +128,7 @@ object ComposeFacade : BaseComposeFacade { viewModel: SceneContainerViewModel, windowInsets: StateFlow<WindowInsets?>, sceneByKey: Map<SceneKey, Scene>, + dataSourceDelegator: SceneDataSourceDelegator, ): View { return ComposeView(context).apply { setContent { @@ -139,6 +141,7 @@ object ComposeFacade : BaseComposeFacade { viewModel = viewModel, sceneByKey = sceneByKey.mapValues { (_, scene) -> scene as ComposableScene }, + dataSourceDelegator = dataSourceDelegator, ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt index 428bc39c7632..0469cbe519ea 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt @@ -29,8 +29,8 @@ import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.scene.shared.model.Direction import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.scene.shared.model.UserAction +import com.android.systemui.scene.shared.model.UserActionResult import com.android.systemui.scene.ui.composable.ComposableScene import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -54,11 +54,11 @@ constructor( ) : ComposableScene { override val key = SceneKey.Bouncer - override val destinationScenes: StateFlow<Map<UserAction, SceneModel>> = + override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = MutableStateFlow( mapOf( - UserAction.Back to SceneModel(SceneKey.Lockscreen), - UserAction.Swipe(Direction.DOWN) to SceneModel(SceneKey.Lockscreen), + UserAction.Back to UserActionResult(SceneKey.Lockscreen), + UserAction.Swipe(Direction.DOWN) to UserActionResult(SceneKey.Lockscreen), ) ) .asStateFlow() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt index f3bef7bf47da..11a38f92c234 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt @@ -23,8 +23,8 @@ import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.scene.shared.model.Direction import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.scene.shared.model.UserAction +import com.android.systemui.scene.shared.model.UserActionResult import com.android.systemui.scene.ui.composable.ComposableScene import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -40,10 +40,10 @@ constructor( ) : ComposableScene { override val key = SceneKey.Communal - override val destinationScenes: StateFlow<Map<UserAction, SceneModel>> = - MutableStateFlow<Map<UserAction, SceneModel>>( + override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = + MutableStateFlow<Map<UserAction, UserActionResult>>( mapOf( - UserAction.Swipe(Direction.RIGHT) to SceneModel(SceneKey.Lockscreen), + UserAction.Swipe(Direction.RIGHT) to UserActionResult(SceneKey.Lockscreen), ) ) .asStateFlow() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt index 378a1e4858e8..7b21d091d451 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt @@ -26,8 +26,8 @@ import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel 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.SceneModel import com.android.systemui.scene.shared.model.UserAction +import com.android.systemui.scene.shared.model.UserActionResult import com.android.systemui.scene.ui.composable.ComposableScene import dagger.Lazy import javax.inject.Inject @@ -49,7 +49,7 @@ constructor( ) : ComposableScene { override val key = SceneKey.Lockscreen - override val destinationScenes: StateFlow<Map<UserAction, SceneModel>> = + override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = combine(viewModel.upDestinationSceneKey, viewModel.leftDestinationSceneKey, ::Pair) .map { (upKey, leftKey) -> destinationScenes(up = upKey, left = leftKey) } .stateIn( @@ -75,13 +75,13 @@ constructor( private fun destinationScenes( up: SceneKey?, left: SceneKey?, - ): Map<UserAction, SceneModel> { + ): Map<UserAction, UserActionResult> { return buildMap { - up?.let { this[UserAction.Swipe(Direction.UP)] = SceneModel(up) } - left?.let { this[UserAction.Swipe(Direction.LEFT)] = SceneModel(left) } + up?.let { this[UserAction.Swipe(Direction.UP)] = UserActionResult(up) } + left?.let { this[UserAction.Swipe(Direction.LEFT)] = UserActionResult(left) } this[UserAction.Swipe(fromEdge = Edge.TOP, direction = Direction.DOWN)] = - SceneModel(SceneKey.QuickSettings) - this[UserAction.Swipe(direction = Direction.DOWN)] = SceneModel(SceneKey.Shade) + UserActionResult(SceneKey.QuickSettings) + this[UserAction.Swipe(direction = Direction.DOWN)] = UserActionResult(SceneKey.Shade) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index 46554a41891d..d36345a310d3 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -63,7 +63,7 @@ import com.android.systemui.qs.footer.ui.compose.FooterActions import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.composable.ComposableScene -import com.android.systemui.scene.ui.composable.toTransitionSceneKey +import com.android.systemui.scene.ui.composable.asComposeAware import com.android.systemui.shade.ui.composable.CollapsedShadeHeader import com.android.systemui.shade.ui.composable.ExpandedShadeHeader import com.android.systemui.shade.ui.composable.Shade @@ -138,7 +138,7 @@ private fun SceneScope.QuickSettingsScene( when (val state = layoutState.transitionState) { is TransitionState.Idle -> true is TransitionState.Transition -> { - state.fromScene == SceneKey.QuickSettings.toTransitionSceneKey() + state.fromScene == SceneKey.QuickSettings.asComposeAware() } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt index 736ee1f235c6..f90f29d8b9dd 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt @@ -25,9 +25,8 @@ import com.android.systemui.dagger.SysUISingleton 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.SceneModel import com.android.systemui.scene.shared.model.UserAction -import com.android.systemui.shade.ui.composable.Shade +import com.android.systemui.scene.shared.model.UserActionResult import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -46,15 +45,16 @@ constructor( ) : ComposableScene { override val key = SceneKey.Gone - override val destinationScenes: StateFlow<Map<UserAction, SceneModel>> = - MutableStateFlow<Map<UserAction, SceneModel>>( + override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = + MutableStateFlow<Map<UserAction, UserActionResult>>( mapOf( UserAction.Swipe( pointerCount = 2, fromEdge = Edge.TOP, direction = Direction.DOWN, - ) to SceneModel(SceneKey.QuickSettings), - UserAction.Swipe(direction = Direction.DOWN) to SceneModel(SceneKey.Shade), + ) to UserActionResult(SceneKey.QuickSettings), + UserAction.Swipe(direction = Direction.DOWN) to + UserActionResult(SceneKey.Shade), ) ) .asStateFlow() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt index da1b417ae190..5006beb01fb4 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -25,6 +25,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -32,24 +34,14 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.motionEventSpy import androidx.compose.ui.input.pointer.pointerInput -import com.android.compose.animation.scene.Back -import com.android.compose.animation.scene.Edge as SceneTransitionEdge -import com.android.compose.animation.scene.ObservableTransitionState as SceneTransitionObservableTransitionState -import com.android.compose.animation.scene.SceneKey as SceneTransitionSceneKey +import com.android.compose.animation.scene.MutableSceneTransitionLayoutState import com.android.compose.animation.scene.SceneTransitionLayout -import com.android.compose.animation.scene.Swipe -import com.android.compose.animation.scene.SwipeDirection -import com.android.compose.animation.scene.UserAction as SceneTransitionUserAction -import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.observableTransitionState -import com.android.compose.animation.scene.updateSceneTransitionLayoutState import com.android.systemui.ribbon.ui.composable.BottomRightCornerRibbon -import com.android.systemui.scene.shared.model.Direction -import com.android.systemui.scene.shared.model.Edge -import com.android.systemui.scene.shared.model.ObservableTransitionState +import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.scene.shared.model.UserAction +import com.android.systemui.scene.shared.model.UserActionResult import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import kotlinx.coroutines.flow.map @@ -75,22 +67,31 @@ import kotlinx.coroutines.flow.map fun SceneContainer( viewModel: SceneContainerViewModel, sceneByKey: Map<SceneKey, ComposableScene>, + dataSourceDelegator: SceneDataSourceDelegator, modifier: Modifier = Modifier, ) { - val currentSceneModel: SceneModel by viewModel.currentScene.collectAsState() - val currentSceneKey = currentSceneModel.key + val coroutineScope = rememberCoroutineScope() + val currentSceneKey: SceneKey by viewModel.currentScene.collectAsState() val currentScene = checkNotNull(sceneByKey[currentSceneKey]) - val currentDestinations: Map<UserAction, SceneModel> by + val currentDestinations: Map<UserAction, UserActionResult> by currentScene.destinationScenes.collectAsState() - val state = - updateSceneTransitionLayoutState( - currentSceneKey.toTransitionSceneKey(), - onChangeScene = viewModel::onSceneChanged, + val state: MutableSceneTransitionLayoutState = remember { + MutableSceneTransitionLayoutState( + initialScene = currentSceneKey.asComposeAware(), transitions = SceneContainerTransitions, ) + } + + DisposableEffect(state) { + val dataSource = SceneTransitionLayoutDataSource(state, coroutineScope) + dataSourceDelegator.setDelegate(dataSource) + onDispose { dataSourceDelegator.setDelegate(null) } + } DisposableEffect(viewModel, state) { - viewModel.setTransitionState(state.observableTransitionState().map { it.toModel() }) + viewModel.setTransitionState( + state.observableTransitionState().map { it.asComposeUnaware() } + ) onDispose { viewModel.setTransitionState(null) } } @@ -114,22 +115,22 @@ fun SceneContainer( ) { sceneByKey.forEach { (sceneKey, composableScene) -> scene( - key = sceneKey.toTransitionSceneKey(), + key = sceneKey.asComposeAware(), userActions = if (sceneKey == currentSceneKey) { currentDestinations } else { composableScene.destinationScenes.value } - .map { (userAction, destinationSceneModel) -> - toTransitionModels(userAction, destinationSceneModel) + .map { (userAction, userActionResult) -> + userAction.asComposeAware() to userActionResult.asComposeAware() } .toMap(), ) { with(composableScene) { this@scene.Content( modifier = - Modifier.element(sceneKey.toTransitionSceneKey().rootElementKey) + Modifier.element(sceneKey.asComposeAware().rootElementKey) .fillMaxSize(), ) } @@ -148,62 +149,3 @@ fun SceneContainer( ) } } - -// TODO(b/293899074): remove this once we can use the one from SceneTransitionLayout. -private fun SceneTransitionObservableTransitionState.toModel(): ObservableTransitionState { - return when (this) { - is SceneTransitionObservableTransitionState.Idle -> - ObservableTransitionState.Idle(scene.toModel().key) - is SceneTransitionObservableTransitionState.Transition -> - ObservableTransitionState.Transition( - fromScene = fromScene.toModel().key, - toScene = toScene.toModel().key, - progress = progress, - isInitiatedByUserInput = isInitiatedByUserInput, - isUserInputOngoing = isUserInputOngoing, - ) - } -} - -// TODO(b/293899074): remove this once we can use the one from SceneTransitionLayout. -private fun toTransitionModels( - userAction: UserAction, - sceneModel: SceneModel, -): Pair<SceneTransitionUserAction, UserActionResult> { - return userAction.toTransitionUserAction() to sceneModel.key.toTransitionSceneKey() -} - -// TODO(b/293899074): remove this once we can use the one from SceneTransitionLayout. -private fun SceneTransitionSceneKey.toModel(): SceneModel { - return SceneModel(key = identity as SceneKey) -} - -// TODO(b/293899074): remove this once we can use the one from SceneTransitionLayout. -private fun UserAction.toTransitionUserAction(): SceneTransitionUserAction { - return when (this) { - is UserAction.Swipe -> - Swipe( - pointerCount = pointerCount, - fromSource = - when (this.fromEdge) { - null -> null - Edge.LEFT -> SceneTransitionEdge.Left - Edge.TOP -> SceneTransitionEdge.Top - Edge.RIGHT -> SceneTransitionEdge.Right - Edge.BOTTOM -> SceneTransitionEdge.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 - } -} - -private fun SceneContainerViewModel.onSceneChanged(sceneKey: SceneTransitionSceneKey) { - onSceneChanged(sceneKey.toModel()) -} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt index 2848245c42b1..61f81209ad7e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt @@ -1,6 +1,8 @@ package com.android.systemui.scene.ui.composable import com.android.compose.animation.scene.transitions +import com.android.systemui.scene.shared.model.TransitionKeys.CollapseShadeInstantly +import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse import com.android.systemui.scene.ui.composable.transitions.bouncerToGoneTransition import com.android.systemui.scene.ui.composable.transitions.goneToQuickSettingsTransition import com.android.systemui.scene.ui.composable.transitions.goneToShadeTransition @@ -26,10 +28,38 @@ import com.android.systemui.scene.ui.composable.transitions.shadeToQuickSettings val SceneContainerTransitions = transitions { from(Bouncer, to = Gone) { bouncerToGoneTransition() } from(Gone, to = Shade) { goneToShadeTransition() } + from( + Gone, + to = Shade, + key = CollapseShadeInstantly.asComposeAware(), + ) { + goneToShadeTransition(durationScale = 0.0) + } + from( + Gone, + to = Shade, + key = SlightlyFasterShadeCollapse.asComposeAware(), + ) { + goneToShadeTransition(durationScale = 0.9) + } from(Gone, to = QuickSettings) { goneToQuickSettingsTransition() } from(Lockscreen, to = Bouncer) { lockscreenToBouncerTransition() } from(Lockscreen, to = Communal) { lockscreenToCommunalTransition() } from(Lockscreen, to = Shade) { lockscreenToShadeTransition() } + from( + Lockscreen, + to = Shade, + key = CollapseShadeInstantly.asComposeAware(), + ) { + lockscreenToShadeTransition(durationScale = 0.0) + } + from( + Lockscreen, + to = Shade, + key = SlightlyFasterShadeCollapse.asComposeAware(), + ) { + lockscreenToShadeTransition(durationScale = 0.9) + } from(Lockscreen, to = QuickSettings) { lockscreenToQuickSettingsTransition() } from(Lockscreen, to = Gone) { lockscreenToGoneTransition() } from(Shade, to = QuickSettings) { shadeToQuickSettingsTransition() } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt index 0c66701de61c..5a9add1ad587 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt @@ -1,16 +1,10 @@ package com.android.systemui.scene.ui.composable -import com.android.compose.animation.scene.SceneKey as SceneTransitionSceneKey import com.android.systemui.scene.shared.model.SceneKey -val Lockscreen = SceneKey.Lockscreen.toTransitionSceneKey() -val Bouncer = SceneKey.Bouncer.toTransitionSceneKey() -val Shade = SceneKey.Shade.toTransitionSceneKey() -val QuickSettings = SceneKey.QuickSettings.toTransitionSceneKey() -val Gone = SceneKey.Gone.toTransitionSceneKey() -val Communal = SceneKey.Communal.toTransitionSceneKey() - -// TODO(b/293899074): Remove this file once we can use the scene keys from SceneTransitionLayout. -fun SceneKey.toTransitionSceneKey(): SceneTransitionSceneKey { - return SceneTransitionSceneKey(debugName = toString(), identity = this) -} +val Lockscreen = SceneKey.Lockscreen.asComposeAware() +val Bouncer = SceneKey.Bouncer.asComposeAware() +val Shade = SceneKey.Shade.asComposeAware() +val QuickSettings = SceneKey.QuickSettings.asComposeAware() +val Gone = SceneKey.Gone.asComposeAware() +val Communal = SceneKey.Communal.asComposeAware() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt index 1223ace6957b..6f115d88dbe2 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt @@ -6,11 +6,16 @@ import com.android.compose.animation.scene.TransitionBuilder import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.shade.ui.composable.ShadeHeader +import kotlin.time.Duration.Companion.milliseconds -fun TransitionBuilder.goneToShadeTransition() { - spec = tween(durationMillis = 500) +fun TransitionBuilder.goneToShadeTransition( + durationScale: Double = 1.0, +) { + spec = tween(durationMillis = DefaultDuration.times(durationScale).inWholeMilliseconds.toInt()) fractionRange(start = .58f) { fade(ShadeHeader.Elements.CollapsedContent) } translate(QuickSettings.Elements.Content, y = -ShadeHeader.Dimensions.CollapsedHeight * .66f) translate(Notifications.Elements.NotificationScrim, Edge.Top, false) } + +private val DefaultDuration = 500.milliseconds diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt index 2d5cf5c752bf..e71f99669df2 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt @@ -6,9 +6,12 @@ import com.android.compose.animation.scene.TransitionBuilder import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.shade.ui.composable.Shade +import kotlin.time.Duration.Companion.milliseconds -fun TransitionBuilder.lockscreenToShadeTransition() { - spec = tween(durationMillis = 500) +fun TransitionBuilder.lockscreenToShadeTransition( + durationScale: Double = 1.0, +) { + spec = tween(durationMillis = DefaultDuration.times(durationScale).inWholeMilliseconds.toInt()) fractionRange(end = 0.5f) { fade(Shade.Elements.BackgroundScrim) @@ -20,3 +23,5 @@ fun TransitionBuilder.lockscreenToShadeTransition() { } fractionRange(start = 0.5f) { fade(Notifications.Elements.NotificationScrim) } } + +private val DefaultDuration = 500.milliseconds diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index cac35cb9369f..25df3e49b618 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -54,8 +54,8 @@ import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Direction import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.scene.shared.model.UserAction +import com.android.systemui.scene.shared.model.UserActionResult import com.android.systemui.scene.ui.composable.ComposableScene import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel import com.android.systemui.statusbar.phone.StatusBarIconController @@ -108,7 +108,7 @@ constructor( ) : ComposableScene { override val key = SceneKey.Shade - override val destinationScenes: StateFlow<Map<UserAction, SceneModel>> = + override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = viewModel.upDestinationSceneKey .map { sceneKey -> destinationScenes(up = sceneKey) } .stateIn( @@ -139,10 +139,10 @@ constructor( private fun destinationScenes( up: SceneKey, - ): Map<UserAction, SceneModel> { + ): Map<UserAction, UserActionResult> { return mapOf( - UserAction.Swipe(Direction.UP) to SceneModel(up), - UserAction.Swipe(Direction.DOWN) to SceneModel(SceneKey.QuickSettings), + UserAction.Swipe(Direction.UP) to UserActionResult(up), + UserAction.Swipe(Direction.DOWN) to UserActionResult(SceneKey.QuickSettings), ) } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/util/ThreadAssert.kt b/packages/SystemUI/customization/src/com/android/systemui/util/ThreadAssert.kt new file mode 100644 index 000000000000..ccbf4ef68680 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/util/ThreadAssert.kt @@ -0,0 +1,23 @@ +/* + * 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.util + +/** Injectable helper providing thread assertions. */ +class ThreadAssert() { + fun isMainThread() = Assert.isMainThread() + fun isNotMainThread() = Assert.isNotMainThread() +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index e8a43ac0ad6c..38dc24ed2f5f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -64,9 +64,10 @@ import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags +import com.android.systemui.scene.shared.model.FakeSceneDataSource import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.DevicePostureController @@ -170,6 +171,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { private lateinit var deviceEntryInteractor: DeviceEntryInteractor @Mock private lateinit var primaryBouncerInteractor: Lazy<PrimaryBouncerInteractor> private lateinit var sceneTransitionStateFlow: MutableStateFlow<ObservableTransitionState> + private lateinit var fakeSceneDataSource: FakeSceneDataSource private lateinit var underTest: KeyguardSecurityContainerController @@ -246,6 +248,8 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { sceneInteractor.setTransitionState(sceneTransitionStateFlow) deviceEntryInteractor = kosmos.deviceEntryInteractor + fakeSceneDataSource = kosmos.fakeSceneDataSource + underTest = KeyguardSecurityContainerController( view, @@ -810,7 +814,8 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { // is // not enough to trigger a dismissal of the keyguard. underTest.onViewAttached() - sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer, null), "reason") + fakeSceneDataSource.pause() + sceneInteractor.changeScene(SceneKey.Bouncer, "reason") sceneTransitionStateFlow.value = ObservableTransitionState.Transition( SceneKey.Lockscreen, @@ -820,7 +825,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { isUserInputOngoing = flowOf(false), ) runCurrent() - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer, null), "reason") + fakeSceneDataSource.unpause(expectedScene = SceneKey.Bouncer) sceneTransitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Bouncer) runCurrent() verify(viewMediatorCallback, never()).keyguardDone(anyInt()) @@ -829,7 +834,8 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { // keyguard. kosmos.fakeDeviceEntryRepository.setUnlocked(true) runCurrent() - sceneInteractor.changeScene(SceneModel(SceneKey.Gone, null), "reason") + fakeSceneDataSource.pause() + sceneInteractor.changeScene(SceneKey.Gone, "reason") sceneTransitionStateFlow.value = ObservableTransitionState.Transition( SceneKey.Bouncer, @@ -839,7 +845,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { isUserInputOngoing = flowOf(false), ) runCurrent() - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone, null), "reason") + fakeSceneDataSource.unpause(expectedScene = SceneKey.Gone) sceneTransitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Gone) runCurrent() verify(viewMediatorCallback).keyguardDone(anyInt()) @@ -847,7 +853,8 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { // While listening, moving back to the bouncer scene does not dismiss the keyguard // again. clearInvocations(viewMediatorCallback) - sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer, null), "reason") + fakeSceneDataSource.pause() + sceneInteractor.changeScene(SceneKey.Bouncer, "reason") sceneTransitionStateFlow.value = ObservableTransitionState.Transition( SceneKey.Gone, @@ -857,7 +864,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { isUserInputOngoing = flowOf(false), ) runCurrent() - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer, null), "reason") + fakeSceneDataSource.unpause(expectedScene = SceneKey.Bouncer) sceneTransitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Bouncer) runCurrent() verify(viewMediatorCallback, never()).keyguardDone(anyInt()) @@ -866,7 +873,8 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { // scene // does not dismiss the keyguard while we're not listening. underTest.onViewDetached() - sceneInteractor.changeScene(SceneModel(SceneKey.Gone, null), "reason") + fakeSceneDataSource.pause() + sceneInteractor.changeScene(SceneKey.Gone, "reason") sceneTransitionStateFlow.value = ObservableTransitionState.Transition( SceneKey.Bouncer, @@ -876,13 +884,14 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { isUserInputOngoing = flowOf(false), ) runCurrent() - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone, null), "reason") + fakeSceneDataSource.unpause(expectedScene = SceneKey.Gone) sceneTransitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Gone) runCurrent() verify(viewMediatorCallback, never()).keyguardDone(anyInt()) // While not listening, moving to the lockscreen does not dismiss the keyguard. - sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen, null), "reason") + fakeSceneDataSource.pause() + sceneInteractor.changeScene(SceneKey.Lockscreen, "reason") sceneTransitionStateFlow.value = ObservableTransitionState.Transition( SceneKey.Gone, @@ -892,7 +901,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { isUserInputOngoing = flowOf(false), ) runCurrent() - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen, null), "reason") + fakeSceneDataSource.unpause(expectedScene = SceneKey.Lockscreen) sceneTransitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Lockscreen) runCurrent() verify(viewMediatorCallback, never()).keyguardDone(anyInt()) @@ -900,7 +909,8 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { // Reattaching the view starts listening again so moving from the bouncer scene to the // gone scene now does dismiss the keyguard again, this time from lockscreen. underTest.onViewAttached() - sceneInteractor.changeScene(SceneModel(SceneKey.Gone, null), "reason") + fakeSceneDataSource.pause() + sceneInteractor.changeScene(SceneKey.Gone, "reason") sceneTransitionStateFlow.value = ObservableTransitionState.Transition( SceneKey.Lockscreen, @@ -910,7 +920,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { isUserInputOngoing = flowOf(false), ) runCurrent() - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone, null), "reason") + fakeSceneDataSource.unpause(expectedScene = SceneKey.Gone) sceneTransitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Gone) runCurrent() verify(viewMediatorCallback).keyguardDone(anyInt()) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt index fbb5415402db..ad29e68f1bbf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt @@ -34,7 +34,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.testKosmos import com.android.systemui.user.data.model.SelectedUserModel import com.android.systemui.user.data.model.SelectionStatus @@ -87,14 +86,14 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onShown() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) + val currentScene by collectLastValue(sceneInteractor.currentScene) val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) lockDeviceAndOpenPasswordBouncer() assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD) assertThat(password).isEmpty() - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + assertThat(currentScene).isEqualTo(SceneKey.Bouncer) assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Password) } @@ -117,7 +116,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onPasswordInputChanged() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) + val currentScene by collectLastValue(sceneInteractor.currentScene) val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) lockDeviceAndOpenPasswordBouncer() @@ -126,7 +125,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { assertThat(message?.text).isEmpty() assertThat(password).isEqualTo("password") - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + assertThat(currentScene).isEqualTo(SceneKey.Bouncer) } @Test @@ -201,7 +200,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onShown_againAfterSceneChange_resetsPassword() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) + val currentScene by collectLastValue(sceneInteractor.currentScene) val password by collectLastValue(underTest.password) lockDeviceAndOpenPasswordBouncer() @@ -217,7 +216,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { // Ensure the previously-entered password is not shown. assertThat(password).isEmpty() - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + assertThat(currentScene).isEqualTo(SceneKey.Bouncer) } @Test @@ -330,16 +329,15 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { } private fun TestScope.switchToScene(toScene: SceneKey) { - val currentScene by collectLastValue(sceneInteractor.desiredScene) - val bouncerShown = currentScene?.key != SceneKey.Bouncer && toScene == SceneKey.Bouncer - val bouncerHidden = currentScene?.key == SceneKey.Bouncer && toScene != SceneKey.Bouncer - sceneInteractor.changeScene(SceneModel(toScene), "reason") - sceneInteractor.onSceneChanged(SceneModel(toScene), "reason") + val currentScene by collectLastValue(sceneInteractor.currentScene) + val bouncerShown = currentScene != SceneKey.Bouncer && toScene == SceneKey.Bouncer + val bouncerHidden = currentScene == SceneKey.Bouncer && toScene != SceneKey.Bouncer + sceneInteractor.changeScene(toScene, "reason") if (bouncerShown) underTest.onShown() if (bouncerHidden) underTest.onHidden() runCurrent() - assertThat(currentScene).isEqualTo(SceneModel(toScene)) + assertThat(currentScene).isEqualTo(toScene) } private fun TestScope.lockDeviceAndOpenPasswordBouncer() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt index 725bdbd43445..32de1f2a892c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt @@ -32,7 +32,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage @@ -78,7 +77,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() { @Test fun onShown() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) + val currentScene by collectLastValue(sceneInteractor.currentScene) val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) @@ -87,14 +86,14 @@ class PatternBouncerViewModelTest : SysuiTestCase() { assertThat(message?.text).isEqualTo(ENTER_YOUR_PATTERN) assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + assertThat(currentScene).isEqualTo(SceneKey.Bouncer) assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Pattern) } @Test fun onDragStart() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) + val currentScene by collectLastValue(sceneInteractor.currentScene) val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) @@ -105,7 +104,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() { assertThat(message?.text).isEmpty() assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + assertThat(currentScene).isEqualTo(SceneKey.Bouncer) } @Test @@ -147,7 +146,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() { @Test fun onDragEnd_whenWrong() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) + val currentScene by collectLastValue(sceneInteractor.currentScene) val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) @@ -160,7 +159,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() { assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() assertThat(message?.text).isEqualTo(WRONG_PATTERN) - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + assertThat(currentScene).isEqualTo(SceneKey.Bouncer) } @Test @@ -369,16 +368,15 @@ class PatternBouncerViewModelTest : SysuiTestCase() { } private fun TestScope.switchToScene(toScene: SceneKey) { - val currentScene by collectLastValue(sceneInteractor.desiredScene) - val bouncerShown = currentScene?.key != SceneKey.Bouncer && toScene == SceneKey.Bouncer - val bouncerHidden = currentScene?.key == SceneKey.Bouncer && toScene != SceneKey.Bouncer - sceneInteractor.changeScene(SceneModel(toScene), "reason") - sceneInteractor.onSceneChanged(SceneModel(toScene), "reason") + val currentScene by collectLastValue(sceneInteractor.currentScene) + val bouncerShown = currentScene != SceneKey.Bouncer && toScene == SceneKey.Bouncer + val bouncerHidden = currentScene == SceneKey.Bouncer && toScene != SceneKey.Bouncer + sceneInteractor.changeScene(toScene, "reason") if (bouncerShown) underTest.onShown() if (bouncerHidden) underTest.onHidden() runCurrent() - assertThat(currentScene).isEqualTo(SceneModel(toScene)) + assertThat(currentScene).isEqualTo(toScene) } private fun TestScope.lockDeviceAndOpenPatternBouncer() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt index 06e12586d384..ccf7094e2bf7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -32,7 +32,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -63,12 +62,12 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun setUp() { underTest = PinBouncerViewModel( - applicationContext = context, - viewModelScope = testScope.backgroundScope, - interactor = bouncerInteractor, - isInputEnabled = MutableStateFlow(true).asStateFlow(), - simBouncerInteractor = kosmos.simBouncerInteractor, - authenticationMethod = AuthenticationMethodModel.Pin, + applicationContext = context, + viewModelScope = testScope.backgroundScope, + interactor = bouncerInteractor, + isInputEnabled = MutableStateFlow(true).asStateFlow(), + simBouncerInteractor = kosmos.simBouncerInteractor, + authenticationMethod = AuthenticationMethodModel.Pin, ) overrideResource(R.string.keyguard_enter_your_pin, ENTER_YOUR_PIN) @@ -182,7 +181,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onBackspaceButtonLongPressed() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) + val currentScene by collectLastValue(sceneInteractor.currentScene) val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() @@ -197,7 +196,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { assertThat(message?.text).isEmpty() assertThat(pin).isEmpty() - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + assertThat(currentScene).isEqualTo(SceneKey.Bouncer) } @Test @@ -216,7 +215,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onAuthenticateButtonClicked_whenWrong() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) + val currentScene by collectLastValue(sceneInteractor.currentScene) val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() @@ -231,7 +230,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { assertThat(pin).isEmpty() assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN) - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + assertThat(currentScene).isEqualTo(SceneKey.Bouncer) } @Test @@ -276,7 +275,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onAutoConfirm_whenWrong() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) + val currentScene by collectLastValue(sceneInteractor.currentScene) val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) @@ -291,7 +290,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { assertThat(pin).isEmpty() assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN) - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + assertThat(currentScene).isEqualTo(SceneKey.Bouncer) } @Test @@ -389,16 +388,15 @@ class PinBouncerViewModelTest : SysuiTestCase() { } private fun TestScope.switchToScene(toScene: SceneKey) { - val currentScene by collectLastValue(sceneInteractor.desiredScene) - val bouncerShown = currentScene?.key != SceneKey.Bouncer && toScene == SceneKey.Bouncer - val bouncerHidden = currentScene?.key == SceneKey.Bouncer && toScene != SceneKey.Bouncer - sceneInteractor.changeScene(SceneModel(toScene), "reason") - sceneInteractor.onSceneChanged(SceneModel(toScene), "reason") + val currentScene by collectLastValue(sceneInteractor.currentScene) + val bouncerShown = currentScene != SceneKey.Bouncer && toScene == SceneKey.Bouncer + val bouncerHidden = currentScene == SceneKey.Bouncer && toScene != SceneKey.Bouncer + sceneInteractor.changeScene(toScene, "reason") if (bouncerShown) underTest.onShown() if (bouncerHidden) underTest.onHidden() runCurrent() - assertThat(currentScene).isEqualTo(SceneModel(toScene)) + assertThat(currentScene).isEqualTo(toScene) } private fun TestScope.lockDeviceAndOpenPinBouncer() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt index b4e2eab22c88..5b20ae5131b2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt @@ -26,7 +26,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.scene.data.repository.sceneContainerRepository import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.flowOf @@ -80,7 +79,7 @@ class CommunalRepositoryImplTest : SysuiTestCase() { testScope.runTest { underTest = createRepositoryImpl(true) - sceneContainerRepository.setDesiredScene(SceneModel(key = SceneKey.Communal)) + sceneContainerRepository.changeScene(SceneKey.Communal) val isCommunalHubShowing by collectLastValue(underTest.isCommunalHubShowing) assertThat(isCommunalHubShowing).isTrue() @@ -91,7 +90,7 @@ class CommunalRepositoryImplTest : SysuiTestCase() { testScope.runTest { underTest = createRepositoryImpl(true) - sceneContainerRepository.setDesiredScene(SceneModel(key = SceneKey.Lockscreen)) + sceneContainerRepository.changeScene(SceneKey.Lockscreen) val isCommunalHubShowing by collectLastValue(underTest.isCommunalHubShowing) assertThat(isCommunalHubShowing).isFalse() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractorTest.kt new file mode 100644 index 000000000000..2e9ee5ca2851 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractorTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.deviceentry.domain.interactor + +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.keyguard.data.repository.biometricSettingsRepository +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class DeviceEntryBiometricSettingsInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val biometricSettingsRepository = kosmos.biometricSettingsRepository + private val underTest = kosmos.deviceEntryBiometricSettingsInteractor + + @Test + fun isCoex_true() = runTest { + val isCoex by collectLastValue(underTest.fingerprintAndFaceEnrolledAndEnabled) + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + assertThat(isCoex).isTrue() + } + + @Test + fun isCoex_faceOnly() = runTest { + val isCoex by collectLastValue(underTest.fingerprintAndFaceEnrolledAndEnabled) + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + assertThat(isCoex).isFalse() + } + + @Test + fun isCoex_fingerprintOnly() = runTest { + val isCoex by collectLastValue(underTest.fingerprintAndFaceEnrolledAndEnabled) + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + assertThat(isCoex).isFalse() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt index 05b589126b08..98719dd32e5a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt @@ -32,7 +32,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -311,9 +310,9 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun showOrUnlockDevice_notLocked_switchesToGoneScene() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) + val currentScene by collectLastValue(sceneInteractor.currentScene) switchToScene(SceneKey.Lockscreen) - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + assertThat(currentScene).isEqualTo(SceneKey.Lockscreen) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pin @@ -323,15 +322,15 @@ class DeviceEntryInteractorTest : SysuiTestCase() { underTest.attemptDeviceEntry() - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + assertThat(currentScene).isEqualTo(SceneKey.Gone) } @Test fun showOrUnlockDevice_authMethodNotSecure_switchesToGoneScene() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) + val currentScene by collectLastValue(sceneInteractor.currentScene) switchToScene(SceneKey.Lockscreen) - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + assertThat(currentScene).isEqualTo(SceneKey.Lockscreen) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.None @@ -340,15 +339,15 @@ class DeviceEntryInteractorTest : SysuiTestCase() { underTest.attemptDeviceEntry() - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + assertThat(currentScene).isEqualTo(SceneKey.Gone) } @Test fun showOrUnlockDevice_authMethodSwipe_switchesToGoneScene() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) + val currentScene by collectLastValue(sceneInteractor.currentScene) switchToScene(SceneKey.Lockscreen) - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + assertThat(currentScene).isEqualTo(SceneKey.Lockscreen) kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( @@ -358,7 +357,7 @@ class DeviceEntryInteractorTest : SysuiTestCase() { underTest.attemptDeviceEntry() - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + assertThat(currentScene).isEqualTo(SceneKey.Gone) } @Test @@ -384,6 +383,6 @@ class DeviceEntryInteractorTest : SysuiTestCase() { } private fun switchToScene(sceneKey: SceneKey) { - sceneInteractor.changeScene(SceneModel(sceneKey), "reason") + sceneInteractor.changeScene(sceneKey, "reason") } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt index 72617238cbb1..3455050d8542 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt @@ -36,7 +36,6 @@ import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.kosmos.testScope import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository @@ -66,7 +65,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { ) kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) kosmos.fakeDeviceEntryRepository.setUnlocked(true) - sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason") + sceneInteractor.changeScene(SceneKey.Lockscreen, "reason") assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone) } @@ -79,7 +78,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { AuthenticationMethodModel.Pin ) kosmos.fakeDeviceEntryRepository.setUnlocked(false) - sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason") + sceneInteractor.changeScene(SceneKey.Lockscreen, "reason") assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Bouncer) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt index 6fc5be1e376d..15cf83c50de3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt @@ -53,7 +53,7 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository val primaryBouncerInteractor = kosmos.mockPrimaryBouncerInteractor - val sysuiStatusBarStateController = kosmos.sysuiStatusBarStateController + private val sysuiStatusBarStateController = kosmos.sysuiStatusBarStateController val underTest by lazy { kosmos.primaryBouncerToGoneTransitionViewModel } @Before diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt index 51f8b11ab72d..d47da3e47d2f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt @@ -29,8 +29,8 @@ import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Direction import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.scene.shared.model.UserAction +import com.android.systemui.scene.shared.model.UserActionResult import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository @@ -120,8 +120,8 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { assertThat(destinations) .isEqualTo( mapOf( - UserAction.Back to SceneModel(SceneKey.Shade), - UserAction.Swipe(Direction.UP) to SceneModel(SceneKey.Shade), + UserAction.Back to UserActionResult(SceneKey.Shade), + UserAction.Swipe(Direction.UP) to UserActionResult(SceneKey.Shade), ) ) } @@ -135,7 +135,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { assertThat(destinations) .isEqualTo( mapOf( - UserAction.Back to SceneModel(SceneKey.QuickSettings), + UserAction.Back to UserActionResult(SceneKey.QuickSettings), ) ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 006f429ab98a..7c30c7eb7f50 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -62,7 +62,7 @@ import com.android.systemui.scene.domain.startable.SceneContainerStartable import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.settings.FakeDisplayTracker import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel @@ -196,6 +196,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private lateinit var emergencyAffordanceManager: EmergencyAffordanceManager private lateinit var telecomManager: TelecomManager + private val fakeSceneDataSource = kosmos.fakeSceneDataSource @Before fun setUp() { @@ -270,7 +271,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { startable.start() assertWithMessage("Initial scene key mismatch!") - .that(sceneContainerViewModel.currentScene.value.key) + .that(sceneContainerViewModel.currentScene.value) .isEqualTo(sceneContainerConfig.initialSceneKey) assertWithMessage("Initial scene container visibility mismatch!") .that(sceneContainerViewModel.isVisible.value) @@ -285,11 +286,12 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { testScope.runTest { emulateUserDrivenTransition(SceneKey.Bouncer) + fakeSceneDataSource.pause() enterPin() - assertCurrentScene(SceneKey.Gone) - emulateUiSceneTransition( + emulatePendingTransitionProgress( expectedVisible = false, ) + assertCurrentScene(SceneKey.Gone) } @Test @@ -302,11 +304,12 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { to = upDestinationSceneKey, ) + fakeSceneDataSource.pause() enterPin() - assertCurrentScene(SceneKey.Gone) - emulateUiSceneTransition( + emulatePendingTransitionProgress( expectedVisible = false, ) + assertCurrentScene(SceneKey.Gone) } @Test @@ -451,10 +454,11 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { to = upDestinationSceneKey, ) + fakeSceneDataSource.pause() dismissIme() + emulatePendingTransitionProgress() assertCurrentScene(SceneKey.Lockscreen) - emulateUiSceneTransition() } @Test @@ -507,8 +511,9 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { @Test fun goesToGone_whenSimUnlocked_whileDeviceUnlocked() = testScope.runTest { + fakeSceneDataSource.pause() introduceLockedSim() - emulateUiSceneTransition(expectedVisible = true) + emulatePendingTransitionProgress(expectedVisible = true) enterSimPin(authMethodAfterSimUnlock = AuthenticationMethodModel.None) assertCurrentScene(SceneKey.Gone) } @@ -516,8 +521,9 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { @Test fun showLockscreen_whenSimUnlocked_whileDeviceLocked() = testScope.runTest { + fakeSceneDataSource.pause() introduceLockedSim() - emulateUiSceneTransition(expectedVisible = true) + emulatePendingTransitionProgress(expectedVisible = true) enterSimPin(authMethodAfterSimUnlock = AuthenticationMethodModel.Pin) assertCurrentScene(SceneKey.Lockscreen) } @@ -545,7 +551,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private fun TestScope.assertCurrentScene(expected: SceneKey) { runCurrent() assertWithMessage("Current scene mismatch!") - .that(sceneContainerViewModel.currentScene.value.key) + .that(sceneContainerViewModel.currentScene.value) .isEqualTo(expected) } @@ -592,35 +598,32 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { } /** - * Emulates a complete transition in the UI from whatever the current scene is in the UI to - * whatever the current scene should be, based on the value in - * [SceneContainerViewModel.onSceneChanged]. - * - * This should post a series of values into [transitionState] to emulate a gradual scene - * transition and culminate with a call to [SceneContainerViewModel.onSceneChanged]. + * Emulates a gradual transition to the currently pending scene that's sitting in the + * [fakeSceneDataSource]. This emits a series of progress updates to the [transitionState] and + * finishes by committing the pending scene as the current scene. * - * The method asserts that a transition is actually required. E.g. it will fail if the current - * scene in [transitionState] is already caught up with the scene in - * [SceneContainerViewModel.currentScene]. - * - * @param expectedVisible Whether [SceneContainerViewModel.isVisible] should be set at the end - * of the UI transition. + * In order to use this, the [fakeSceneDataSource] must be paused before this method is called. */ - private fun TestScope.emulateUiSceneTransition( + private fun TestScope.emulatePendingTransitionProgress( expectedVisible: Boolean = true, ) { - val to = sceneContainerViewModel.currentScene.value + assertWithMessage("The FakeSceneDataSource has to be paused for this to do anything.") + .that(fakeSceneDataSource.isPaused) + .isTrue() + + val to = fakeSceneDataSource.pendingScene ?: return val from = getCurrentSceneInUi() - assertWithMessage("Cannot transition to ${to.key} as the UI is already on that scene!") - .that(to.key) - .isNotEqualTo(from) + + if (to == from) { + return + } // Begin to transition. val progressFlow = MutableStateFlow(0f) transitionState.value = ObservableTransitionState.Transition( fromScene = getCurrentSceneInUi(), - toScene = to.key, + toScene = to, progress = progressFlow, isInitiatedByUserInput = false, isUserInputOngoing = flowOf(false), @@ -634,17 +637,18 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { } // End the transition and report the change. - transitionState.value = ObservableTransitionState.Idle(to.key) + transitionState.value = ObservableTransitionState.Idle(to) - sceneContainerViewModel.onSceneChanged(to) + fakeSceneDataSource.unpause(force = true) runCurrent() - assertWithMessage("Visibility mismatch after scene transition from $from to ${to.key}!") + assertWithMessage("Visibility mismatch after scene transition from $from to $to!") .that(sceneContainerViewModel.isVisible.value) .isEqualTo(expectedVisible) + assertThat(sceneContainerViewModel.currentScene.value).isEqualTo(to) bouncerSceneJob = - if (to.key == SceneKey.Bouncer) { + if (to == SceneKey.Bouncer) { testScope.backgroundScope.launch { bouncerViewModel.authMethodViewModel.collect { // Do nothing. Need this to turn this otherwise cold flow, hot. @@ -662,7 +666,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { * causes a scene change to the [to] scene. * * This also includes the emulation of the resulting UI transition that culminates with the UI - * catching up with the requested scene change (see [emulateUiSceneTransition]). + * catching up with the requested scene change (see [emulatePendingTransitionProgress]). * * @param to The scene to transition to. */ @@ -671,10 +675,10 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { ) { checkNotNull(to) - sceneInteractor.changeScene(SceneModel(to), "reason") - assertThat(sceneContainerViewModel.currentScene.value.key).isEqualTo(to) + fakeSceneDataSource.pause() + sceneInteractor.changeScene(to, "reason") - emulateUiSceneTransition( + emulatePendingTransitionProgress( expectedVisible = to != SceneKey.Gone, ) } @@ -703,11 +707,12 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { .isFalse() emulateUserDrivenTransition(SceneKey.Bouncer) + fakeSceneDataSource.pause() enterPin() // This repository state is not changed by the AuthInteractor, it relies on // KeyguardStateController. kosmos.fakeDeviceEntryRepository.setUnlocked(true) - emulateUiSceneTransition( + emulatePendingTransitionProgress( expectedVisible = false, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt index 2ad872ca6023..1da3bc1aeda8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt @@ -28,7 +28,6 @@ import com.android.systemui.scene.sceneKeys import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -63,21 +62,21 @@ class SceneContainerRepositoryTest : SysuiTestCase() { } @Test - fun desiredScene() = + fun currentScene() = testScope.runTest { val underTest = kosmos.sceneContainerRepository - val currentScene by collectLastValue(underTest.desiredScene) - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + val currentScene by collectLastValue(underTest.currentScene) + assertThat(currentScene).isEqualTo(SceneKey.Lockscreen) - underTest.setDesiredScene(SceneModel(SceneKey.Shade)) - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Shade)) + underTest.changeScene(SceneKey.Shade) + assertThat(currentScene).isEqualTo(SceneKey.Shade) } @Test(expected = IllegalStateException::class) - fun setDesiredScene_noSuchSceneInContainer_throws() { + fun changeScene_noSuchSceneInContainer_throws() { kosmos.sceneKeys = listOf(SceneKey.QuickSettings, SceneKey.Lockscreen) val underTest = kosmos.sceneContainerRepository - underTest.setDesiredScene(SceneModel(SceneKey.Shade)) + underTest.changeScene(SceneKey.Shade) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index 942fbc229e8a..4b9ebdc295c6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -29,7 +29,7 @@ import com.android.systemui.scene.sceneKeys import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow @@ -46,6 +46,7 @@ class SceneInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope + private val fakeSceneDataSource = kosmos.fakeSceneDataSource private lateinit var underTest: SceneInteractor @@ -63,24 +64,24 @@ class SceneInteractorTest : SysuiTestCase() { @Test fun changeScene() = testScope.runTest { - val desiredScene by collectLastValue(underTest.desiredScene) - assertThat(desiredScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + val currentScene by collectLastValue(underTest.currentScene) + assertThat(currentScene).isEqualTo(SceneKey.Lockscreen) - underTest.changeScene(SceneModel(SceneKey.Shade), "reason") - assertThat(desiredScene).isEqualTo(SceneModel(SceneKey.Shade)) + underTest.changeScene(SceneKey.Shade, "reason") + assertThat(currentScene).isEqualTo(SceneKey.Shade) } @Test fun changeScene_toGoneWhenUnl_doesNotThrow() = testScope.runTest { - val desiredScene by collectLastValue(underTest.desiredScene) - assertThat(desiredScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + val currentScene by collectLastValue(underTest.currentScene) + assertThat(currentScene).isEqualTo(SceneKey.Lockscreen) kosmos.fakeDeviceEntryRepository.setUnlocked(true) runCurrent() - underTest.changeScene(SceneModel(SceneKey.Gone), "reason") - assertThat(desiredScene).isEqualTo(SceneModel(SceneKey.Gone)) + underTest.changeScene(SceneKey.Gone, "reason") + assertThat(currentScene).isEqualTo(SceneKey.Gone) } @Test(expected = IllegalStateException::class) @@ -88,17 +89,18 @@ class SceneInteractorTest : SysuiTestCase() { testScope.runTest { kosmos.fakeDeviceEntryRepository.setUnlocked(false) - underTest.changeScene(SceneModel(SceneKey.Gone), "reason") + underTest.changeScene(SceneKey.Gone, "reason") } @Test - fun onSceneChanged() = + fun sceneChanged_inDataSource() = testScope.runTest { - val desiredScene by collectLastValue(underTest.desiredScene) - assertThat(desiredScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + val currentScene by collectLastValue(underTest.currentScene) + assertThat(currentScene).isEqualTo(SceneKey.Lockscreen) - underTest.onSceneChanged(SceneModel(SceneKey.Shade), "reason") - assertThat(desiredScene).isEqualTo(SceneModel(SceneKey.Shade)) + fakeSceneDataSource.changeScene(SceneKey.Shade) + + assertThat(currentScene).isEqualTo(SceneKey.Shade) } @Test @@ -142,20 +144,20 @@ class SceneInteractorTest : SysuiTestCase() { testScope.runTest { val transitionState = MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Idle(underTest.desiredScene.value.key) + ObservableTransitionState.Idle(underTest.currentScene.value) ) underTest.setTransitionState(transitionState) val transitionTo by collectLastValue(underTest.transitioningTo) assertThat(transitionTo).isNull() - underTest.changeScene(SceneModel(SceneKey.Shade), "reason") + underTest.changeScene(SceneKey.Shade, "reason") assertThat(transitionTo).isNull() val progress = MutableStateFlow(0f) transitionState.value = ObservableTransitionState.Transition( - fromScene = underTest.desiredScene.value.key, + fromScene = underTest.currentScene.value, toScene = SceneKey.Shade, progress = progress, isInitiatedByUserInput = false, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 34c5173436c8..ffea84b70d07 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class) +@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalCoroutinesApi::class) package com.android.systemui.scene.domain.startable @@ -47,7 +47,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository @@ -59,7 +59,6 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -95,6 +94,7 @@ class SceneContainerStartableTest : SysuiTestCase() { private val sysUiState: SysUiState = mock() private val falsingCollector: FalsingCollector = mock() private val powerInteractor = PowerInteractorFactory.create().powerInteractor + private val fakeSceneDataSource = kosmos.fakeSceneDataSource private lateinit var underTest: SceneContainerStartable @@ -115,8 +115,8 @@ class SceneContainerStartableTest : SysuiTestCase() { falsingCollector = falsingCollector, powerInteractor = powerInteractor, bouncerInteractor = bouncerInteractor, - simBouncerInteractor = dagger.Lazy { kosmos.simBouncerInteractor }, - authenticationInteractor = dagger.Lazy { authenticationInteractor }, + simBouncerInteractor = { kosmos.simBouncerInteractor }, + authenticationInteractor = { authenticationInteractor }, windowController = windowController, deviceProvisioningInteractor = kosmos.deviceProvisioningInteractor, centralSurfaces = centralSurfaces, @@ -126,8 +126,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun hydrateVisibility() = testScope.runTest { - val currentDesiredSceneKey by - collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val currentDesiredSceneKey by collectLastValue(sceneInteractor.currentScene) val isVisible by collectLastValue(sceneInteractor.isVisible) val transitionStateFlow = prepareState( @@ -140,7 +139,8 @@ class SceneContainerStartableTest : SysuiTestCase() { underTest.start() assertThat(isVisible).isFalse() - sceneInteractor.changeScene(SceneModel(SceneKey.Shade), "reason") + fakeSceneDataSource.pause() + sceneInteractor.changeScene(SceneKey.Shade, "reason") transitionStateFlow.value = ObservableTransitionState.Transition( fromScene = SceneKey.Gone, @@ -150,11 +150,12 @@ class SceneContainerStartableTest : SysuiTestCase() { isUserInputOngoing = flowOf(false), ) assertThat(isVisible).isTrue() - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Shade), "reason") + fakeSceneDataSource.unpause(expectedScene = SceneKey.Shade) transitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Shade) assertThat(isVisible).isTrue() - sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason") + fakeSceneDataSource.pause() + sceneInteractor.changeScene(SceneKey.Gone, "reason") transitionStateFlow.value = ObservableTransitionState.Transition( fromScene = SceneKey.Shade, @@ -164,7 +165,7 @@ class SceneContainerStartableTest : SysuiTestCase() { isUserInputOngoing = flowOf(false), ) assertThat(isVisible).isTrue() - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone), "reason") + fakeSceneDataSource.unpause(expectedScene = SceneKey.Gone) transitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Gone) assertThat(isVisible).isFalse() } @@ -196,7 +197,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun startsInLockscreenScene() = testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) prepareState() underTest.start() @@ -208,7 +209,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun switchToLockscreenWhenDeviceLocks() = testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) prepareState( isDeviceUnlocked = true, initialSceneKey = SceneKey.Gone, @@ -224,7 +225,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun switchFromBouncerToGoneWhenDeviceUnlocked() = testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) prepareState( isDeviceUnlocked = false, initialSceneKey = SceneKey.Bouncer, @@ -240,7 +241,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun switchFromLockscreenToGoneWhenDeviceUnlocksWithBypassOn() = testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) prepareState( isBypassEnabled = true, initialSceneKey = SceneKey.Lockscreen, @@ -256,7 +257,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun stayOnLockscreenWhenDeviceUnlocksWithBypassOff() = testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) prepareState( isBypassEnabled = false, initialSceneKey = SceneKey.Lockscreen, @@ -274,7 +275,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun stayOnCurrentSceneWhenDeviceIsUnlockedAndUserIsNotOnLockscreen() = testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val transitionStateFlowValue = prepareState( isBypassEnabled = true, @@ -284,7 +285,7 @@ class SceneContainerStartableTest : SysuiTestCase() { underTest.start() runCurrent() - sceneInteractor.changeScene(SceneModel(SceneKey.Shade), "switch to shade") + sceneInteractor.changeScene(SceneKey.Shade, "switch to shade") transitionStateFlowValue.value = ObservableTransitionState.Idle(SceneKey.Shade) assertThat(currentSceneKey).isEqualTo(SceneKey.Shade) @@ -297,7 +298,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun switchToGoneWhenDeviceIsUnlockedAndUserIsOnBouncerWithBypassDisabled() = testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) prepareState( isBypassEnabled = false, initialSceneKey = SceneKey.Bouncer, @@ -315,7 +316,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun switchToLockscreenWhenDeviceSleepsLocked() = testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) prepareState( isDeviceUnlocked = false, initialSceneKey = SceneKey.Shade, @@ -347,11 +348,12 @@ class SceneContainerStartableTest : SysuiTestCase() { kosmos.fakeDeviceEntryRepository.setUnlocked(true) runCurrent() } - sceneInteractor.changeScene(SceneModel(sceneKey), "reason") + fakeSceneDataSource.pause() + sceneInteractor.changeScene(sceneKey, "reason") runCurrent() verify(sysUiState, times(index)).commitUpdate(Display.DEFAULT_DISPLAY) - sceneInteractor.onSceneChanged(SceneModel(sceneKey), "reason") + fakeSceneDataSource.unpause(expectedScene = sceneKey) runCurrent() verify(sysUiState, times(index)).commitUpdate(Display.DEFAULT_DISPLAY) @@ -364,7 +366,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun switchToGoneWhenDeviceStartsToWakeUp_authMethodNone() = testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) prepareState( initialSceneKey = SceneKey.Lockscreen, authenticationMethod = AuthenticationMethodModel.None, @@ -380,7 +382,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun stayOnLockscreenWhenDeviceStartsToWakeUp_authMethodSwipe() = testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) prepareState( initialSceneKey = SceneKey.Lockscreen, authenticationMethod = AuthenticationMethodModel.None, @@ -396,7 +398,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun doesNotSwitchToGoneWhenDeviceStartsToWakeUp_authMethodSecure() = testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) prepareState( initialSceneKey = SceneKey.Lockscreen, authenticationMethod = AuthenticationMethodModel.Pin, @@ -411,7 +413,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun switchToGoneWhenDeviceStartsToWakeUp_authMethodSecure_deviceUnlocked() = testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) prepareState( initialSceneKey = SceneKey.Lockscreen, authenticationMethod = AuthenticationMethodModel.Pin, @@ -450,7 +452,7 @@ class SceneContainerStartableTest : SysuiTestCase() { SceneKey.Bouncer, ) .forEach { sceneKey -> - sceneInteractor.changeScene(SceneModel(sceneKey), "reason") + sceneInteractor.changeScene(sceneKey, "reason") runCurrent() verify(falsingCollector, never()).onSuccessfulUnlock() } @@ -458,7 +460,7 @@ class SceneContainerStartableTest : SysuiTestCase() { // Changing to the Gone scene should report a successful unlock. kosmos.fakeDeviceEntryRepository.setUnlocked(true) runCurrent() - sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason") + sceneInteractor.changeScene(SceneKey.Gone, "reason") runCurrent() verify(falsingCollector).onSuccessfulUnlock() @@ -471,13 +473,13 @@ class SceneContainerStartableTest : SysuiTestCase() { SceneKey.Gone, ) .forEach { sceneKey -> - sceneInteractor.changeScene(SceneModel(sceneKey), "reason") + sceneInteractor.changeScene(sceneKey, "reason") runCurrent() verify(falsingCollector, times(1)).onSuccessfulUnlock() } // Changing to the Lockscreen scene shouldn't report a successful unlock. - sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason") + sceneInteractor.changeScene(SceneKey.Lockscreen, "reason") runCurrent() verify(falsingCollector, times(1)).onSuccessfulUnlock() @@ -490,13 +492,13 @@ class SceneContainerStartableTest : SysuiTestCase() { SceneKey.Bouncer, ) .forEach { sceneKey -> - sceneInteractor.changeScene(SceneModel(sceneKey), "reason") + sceneInteractor.changeScene(sceneKey, "reason") runCurrent() verify(falsingCollector, times(1)).onSuccessfulUnlock() } // Changing to the Gone scene should report a second successful unlock. - sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason") + sceneInteractor.changeScene(SceneKey.Gone, "reason") runCurrent() verify(falsingCollector, times(2)).onSuccessfulUnlock() } @@ -525,7 +527,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun bouncerImeHidden_shouldTransitionBackToLockscreen() = testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) prepareState( initialSceneKey = SceneKey.Lockscreen, authenticationMethod = AuthenticationMethodModel.Password, @@ -647,13 +649,13 @@ class SceneContainerStartableTest : SysuiTestCase() { runCurrent() verify(falsingCollector).onBouncerHidden() - sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason") + sceneInteractor.changeScene(SceneKey.Bouncer, "reason") runCurrent() verify(falsingCollector).onBouncerShown() kosmos.fakeDeviceEntryRepository.setUnlocked(true) runCurrent() - sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason") + sceneInteractor.changeScene(SceneKey.Gone, "reason") runCurrent() verify(falsingCollector, times(2)).onBouncerHidden() } @@ -661,7 +663,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun switchesToBouncer_whenSimBecomesLocked() = testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) prepareState( initialSceneKey = SceneKey.Lockscreen, @@ -681,7 +683,7 @@ class SceneContainerStartableTest : SysuiTestCase() { fun switchesToLockscreen_whenSimBecomesUnlocked() = testScope.runTest { kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = true - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) prepareState( initialSceneKey = SceneKey.Bouncer, @@ -700,7 +702,7 @@ class SceneContainerStartableTest : SysuiTestCase() { fun switchesToGone_whenSimBecomesUnlocked_ifDeviceUnlockedAndLockscreenDisabled() = testScope.runTest { kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = true - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) prepareState( initialSceneKey = SceneKey.Lockscreen, @@ -719,8 +721,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun hydrateWindowFocus() = testScope.runTest { - val currentDesiredSceneKey by - collectLastValue(sceneInteractor.desiredScene.map { it.key }) + val currentDesiredSceneKey by collectLastValue(sceneInteractor.currentScene) val transitionStateFlow = prepareState( isDeviceUnlocked = true, @@ -733,7 +734,8 @@ class SceneContainerStartableTest : SysuiTestCase() { runCurrent() verify(windowController, times(1)).setNotificationShadeFocusable(false) - sceneInteractor.changeScene(SceneModel(SceneKey.Shade), "reason") + fakeSceneDataSource.pause() + sceneInteractor.changeScene(SceneKey.Shade, "reason") transitionStateFlow.value = ObservableTransitionState.Transition( fromScene = SceneKey.Gone, @@ -745,12 +747,13 @@ class SceneContainerStartableTest : SysuiTestCase() { runCurrent() verify(windowController, times(1)).setNotificationShadeFocusable(false) - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Shade), "reason") + fakeSceneDataSource.unpause(expectedScene = SceneKey.Shade) transitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Shade) runCurrent() verify(windowController, times(1)).setNotificationShadeFocusable(true) - sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason") + fakeSceneDataSource.pause() + sceneInteractor.changeScene(SceneKey.Gone, "reason") transitionStateFlow.value = ObservableTransitionState.Transition( fromScene = SceneKey.Shade, @@ -762,7 +765,7 @@ class SceneContainerStartableTest : SysuiTestCase() { runCurrent() verify(windowController, times(1)).setNotificationShadeFocusable(true) - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone), "reason") + fakeSceneDataSource.unpause(expectedScene = SceneKey.Gone) transitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Gone) runCurrent() verify(windowController, times(2)).setNotificationShadeFocusable(false) @@ -965,8 +968,8 @@ class SceneContainerStartableTest : SysuiTestCase() { verifyDuringTransition: (() -> Unit)? = null, verifyAfterTransition: (() -> Unit)? = null, ) { - val fromScene = sceneInteractor.desiredScene.value.key - sceneInteractor.changeScene(SceneModel(toScene), "reason") + val fromScene = sceneInteractor.currentScene.value + sceneInteractor.changeScene(toScene, "reason") runCurrent() verifyBeforeTransition?.invoke() @@ -1020,8 +1023,7 @@ class SceneContainerStartableTest : SysuiTestCase() { sceneInteractor.setTransitionState(transitionStateFlow) initialSceneKey?.let { transitionStateFlow.value = ObservableTransitionState.Idle(it) - sceneInteractor.changeScene(SceneModel(it), "reason") - sceneInteractor.onSceneChanged(SceneModel(it), "reason") + sceneInteractor.changeScene(it, "reason") } authenticationMethod?.let { kosmos.fakeAuthenticationRepository.setAuthenticationMethod(authenticationMethod) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt index a16ce4325dcf..6c78317f61e5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt @@ -23,11 +23,12 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.domain.interactor.falsingInteractor import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.sceneKeys import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -41,7 +42,10 @@ import org.junit.runner.RunWith class SceneContainerViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() + private val testScope by lazy { kosmos.testScope } private val interactor by lazy { kosmos.sceneInteractor } + private val fakeSceneDataSource = kosmos.fakeSceneDataSource + private lateinit var underTest: SceneContainerViewModel @Before @@ -55,16 +59,17 @@ class SceneContainerViewModelTest : SysuiTestCase() { } @Test - fun isVisible() = runTest { - val isVisible by collectLastValue(underTest.isVisible) - assertThat(isVisible).isTrue() + fun isVisible() = + testScope.runTest { + val isVisible by collectLastValue(underTest.isVisible) + assertThat(isVisible).isTrue() - interactor.setVisible(false, "reason") - assertThat(isVisible).isFalse() + interactor.setVisible(false, "reason") + assertThat(isVisible).isFalse() - interactor.setVisible(true, "reason") - assertThat(isVisible).isTrue() - } + interactor.setVisible(true, "reason") + assertThat(isVisible).isTrue() + } @Test fun allSceneKeys() { @@ -72,12 +77,13 @@ class SceneContainerViewModelTest : SysuiTestCase() { } @Test - fun sceneTransition() = runTest { - val currentScene by collectLastValue(underTest.currentScene) - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + fun sceneTransition() = + testScope.runTest { + val currentScene by collectLastValue(underTest.currentScene) + assertThat(currentScene).isEqualTo(SceneKey.Lockscreen) - underTest.onSceneChanged(SceneModel(SceneKey.Shade)) + fakeSceneDataSource.changeScene(SceneKey.Shade) - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Shade)) - } + assertThat(currentScene).isEqualTo(SceneKey.Shade) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt index 51b834207cfd..ec424b05fc06 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt @@ -30,7 +30,6 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.CommandQueue @@ -88,7 +87,7 @@ class ShadeControllerSceneImplTest : SysuiTestCase() { runCurrent() // THEN the shade remains collapsed and the post-collapse action ran - assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Gone) + assertThat(sceneInteractor.currentScene.value).isEqualTo(SceneKey.Gone) verify(testRunnable, times(1)).run() } @@ -106,7 +105,7 @@ class ShadeControllerSceneImplTest : SysuiTestCase() { runCurrent() // THEN the shade remains expanded and the post-collapse action did not run - assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Shade) + assertThat(sceneInteractor.currentScene.value).isEqualTo(SceneKey.Shade) assertThat(shadeInteractor.isAnyFullyExpanded.value).isTrue() verify(testRunnable, never()).run() } @@ -123,7 +122,7 @@ class ShadeControllerSceneImplTest : SysuiTestCase() { runCurrent() // THEN the shade collapses back to lockscreen and the post-collapse action ran - assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Lockscreen) + assertThat(sceneInteractor.currentScene.value).isEqualTo(SceneKey.Lockscreen) } @Test @@ -138,7 +137,7 @@ class ShadeControllerSceneImplTest : SysuiTestCase() { runCurrent() // THEN the shade collapses back to lockscreen and the post-collapse action ran - assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Gone) + assertThat(sceneInteractor.currentScene.value).isEqualTo(SceneKey.Gone) } @Test @@ -172,7 +171,7 @@ class ShadeControllerSceneImplTest : SysuiTestCase() { } private fun setScene(key: SceneKey) { - sceneInteractor.changeScene(SceneModel(key), "test") + sceneInteractor.changeScene(key, "test") sceneInteractor.setTransitionState( MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key)) ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImplTest.kt index d3c6598296a6..ec4da0405b6d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImplTest.kt @@ -26,7 +26,6 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.shared.recents.utilities.Utilities import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -60,7 +59,7 @@ class ShadeBackActionInteractorImplTest : SysuiTestCase() { setScene(SceneKey.Shade) underTest.animateCollapseQs(true) runCurrent() - assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Shade) + assertThat(sceneInteractor.currentScene.value).isEqualTo(SceneKey.Shade) } @Test @@ -70,7 +69,7 @@ class ShadeBackActionInteractorImplTest : SysuiTestCase() { setScene(SceneKey.QuickSettings) underTest.animateCollapseQs(true) runCurrent() - assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Gone) + assertThat(sceneInteractor.currentScene.value).isEqualTo(SceneKey.Gone) } @Test @@ -80,7 +79,7 @@ class ShadeBackActionInteractorImplTest : SysuiTestCase() { setScene(SceneKey.QuickSettings) underTest.animateCollapseQs(true) runCurrent() - assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Lockscreen) + assertThat(sceneInteractor.currentScene.value).isEqualTo(SceneKey.Lockscreen) } @Test @@ -89,7 +88,7 @@ class ShadeBackActionInteractorImplTest : SysuiTestCase() { setScene(SceneKey.QuickSettings) underTest.animateCollapseQs(false) runCurrent() - assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Shade) + assertThat(sceneInteractor.currentScene.value).isEqualTo(SceneKey.Shade) } private fun enterDevice() { @@ -99,7 +98,7 @@ class ShadeBackActionInteractorImplTest : SysuiTestCase() { } private fun setScene(key: SceneKey) { - sceneInteractor.changeScene(SceneModel(key), "test") + sceneInteractor.changeScene(key, "test") sceneInteractor.setTransitionState( MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key)) ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt index f1f5dc378e0a..799e8f054d51 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -31,7 +31,6 @@ import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor @@ -146,8 +145,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.None ) - sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason") - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen), "reason") + sceneInteractor.changeScene(SceneKey.Lockscreen, "reason") assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Lockscreen) } @@ -162,8 +160,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { AuthenticationMethodModel.None ) runCurrent() - sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason") - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone), "reason") + sceneInteractor.changeScene(SceneKey.Gone, "reason") assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone) } @@ -171,7 +168,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { @Test fun onContentClicked_deviceUnlocked_switchesToGone() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) + val currentScene by collectLastValue(sceneInteractor.currentScene) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pin ) @@ -180,13 +177,13 @@ class ShadeSceneViewModelTest : SysuiTestCase() { underTest.onContentClicked() - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + assertThat(currentScene).isEqualTo(SceneKey.Gone) } @Test fun onContentClicked_deviceLockedSecurely_switchesToBouncer() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.desiredScene) + val currentScene by collectLastValue(sceneInteractor.currentScene) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pin ) @@ -195,7 +192,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { underTest.onContentClicked() - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + assertThat(currentScene).isEqualTo(SceneKey.Bouncer) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt index 4d7d5d3fa664..efd8f000df41 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt @@ -30,7 +30,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationStackAppearanceViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.testKosmos @@ -59,6 +59,7 @@ class NotificationStackAppearanceIntegrationTest : SysuiTestCase() { private val placeholderViewModel by lazy { kosmos.notificationsPlaceholderViewModel } private val appearanceViewModel by lazy { kosmos.notificationStackAppearanceViewModel } private val sceneInteractor by lazy { kosmos.sceneInteractor } + private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource } @Test fun updateBounds() = @@ -97,7 +98,8 @@ class NotificationStackAppearanceIntegrationTest : SysuiTestCase() { val expandFraction by collectLastValue(appearanceViewModel.expandFraction) assertThat(expandFraction).isEqualTo(0f) - sceneInteractor.changeScene(SceneModel(SceneKey.Shade), "reason") + fakeSceneDataSource.pause() + sceneInteractor.changeScene(SceneKey.Shade, "reason") val transitionProgress = MutableStateFlow(0f) transitionState.value = ObservableTransitionState.Transition( @@ -115,7 +117,7 @@ class NotificationStackAppearanceIntegrationTest : SysuiTestCase() { assertThat(expandFraction).isWithin(0.01f).of(progress) } - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Shade), "reason") + fakeSceneDataSource.unpause(expectedScene = SceneKey.Shade) assertThat(expandFraction).isWithin(0.01f).of(1f) } @@ -142,7 +144,8 @@ class NotificationStackAppearanceIntegrationTest : SysuiTestCase() { val expandFraction by collectLastValue(appearanceViewModel.expandFraction) assertThat(expandFraction).isEqualTo(1f) - sceneInteractor.changeScene(SceneModel(SceneKey.QuickSettings), "reason") + fakeSceneDataSource.pause() + sceneInteractor.changeScene(SceneKey.QuickSettings, "reason") val transitionProgress = MutableStateFlow(0f) transitionState.value = ObservableTransitionState.Transition( @@ -160,7 +163,7 @@ class NotificationStackAppearanceIntegrationTest : SysuiTestCase() { assertThat(expandFraction).isEqualTo(1f) } - sceneInteractor.onSceneChanged(SceneModel(SceneKey.QuickSettings), "reason") + fakeSceneDataSource.unpause(expectedScene = SceneKey.QuickSettings) assertThat(expandFraction).isEqualTo(1f) } } diff --git a/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml b/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml index 50d560728fb7..41fb57a6ebb5 100644 --- a/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml +++ b/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml @@ -32,4 +32,24 @@ android:importantForAccessibility="no" sysui:ignoreRightInset="true" /> + <!-- Keyguard messages --> + <LinearLayout + android:id="@+id/alternate_bouncer_message_area_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:layout_marginTop="@dimen/status_bar_height" + android:layout_gravity="top|center_horizontal" + android:gravity="center_horizontal"> + <com.android.keyguard.AuthKeyguardMessageArea + android:id="@+id/alternate_bouncer_message_area" + style="@style/Keyguard.TextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/keyguard_lock_padding" + android:gravity="center" + android:singleLine="true" + android:ellipsize="marquee" + android:focusable="true"/> + </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 2ab0813300e3..71ae0d716429 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -228,6 +228,7 @@ <item type="id" name="ambient_indication_container" /> <item type="id" name="status_view_media_container" /> <item type="id" name="smart_space_barrier_bottom" /> + <item type="id" name="small_clock_guideline_top" /> <item type="id" name="weather_clock_date_and_icons_barrier_bottom" /> <!-- Privacy dialog --> diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java index 975a6020430d..ad6609a41125 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java @@ -100,7 +100,7 @@ class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.It new AccessibilityNodeInfoCompat.AccessibilityActionCompat( R.id.action_edit, res.getString( - R.string.accessibility_floating_button_action_remove_menu)); + R.string.accessibility_floating_button_action_edit)); info.addAction(edit); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java index 035ccbd19f59..27f9106fde7c 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java @@ -41,6 +41,8 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.android.internal.accessibility.dialog.AccessibilityTarget; +import com.android.internal.annotations.VisibleForTesting; +import com.android.modules.expresslog.Counter; import com.android.systemui.Flags; import com.android.systemui.util.settings.SecureSettings; @@ -436,6 +438,20 @@ class MenuView extends FrameLayout implements mContext.startActivity(getIntentForEditScreen()); } + void incrementTexMetricForAllTargets(String metric) { + if (!Flags.floatingMenuDragToEdit()) { + return; + } + for (AccessibilityTarget target : mTargetFeatures) { + incrementTexMetric(metric, target.getUid()); + } + } + + @VisibleForTesting + void incrementTexMetric(String metric, int uid) { + Counter.logIncrementWithUid(metric, uid); + } + Intent getIntentForEditScreen() { List<String> targets = new SettingsStringUtil.ColonDelimitedSet.OfStrings( mSecureSettings.getStringForUser( diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java index a883c009269c..6d4baf4771b8 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java @@ -92,6 +92,20 @@ class MenuViewLayer extends FrameLayout implements MenuView.OnMoveToTuckedListener { private static final int SHOW_MESSAGE_DELAY_MS = 3000; + /** + * Counter indicating the FAB was dragged to the Dismiss action button. + * + * <p>Defined in frameworks/proto_logging/stats/express/catalog/accessibility.cfg. + */ + static final String TEX_METRIC_DISMISS = "accessibility.value_fab_shortcut_action_dismiss"; + + /** + * Counter indicating the FAB was dragged to the Edit action button. + * + * <p>Defined in frameworks/proto_logging/stats/express/catalog/accessibility.cfg. + */ + static final String TEX_METRIC_EDIT = "accessibility.value_fab_shortcut_action_edit"; + private final WindowManager mWindowManager; private final MenuView mMenuView; private final MenuListViewTouchHandler mMenuListViewTouchHandler; @@ -229,20 +243,23 @@ class MenuViewLayer extends FrameLayout implements } mDragToInteractAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() { @Override - public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { + public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject<?> draggedObject) { mDragToInteractAnimationController.animateInteractMenu( target.getTargetView().getId(), /* scaleUp= */ true); } @Override public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject<?> draggedObject, float velocityX, float velocityY, boolean wasFlungOut) { mDragToInteractAnimationController.animateInteractMenu( target.getTargetView().getId(), /* scaleUp= */ false); } @Override - public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { + public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject<?> draggedObject) { dispatchAccessibilityAction(target.getTargetView().getId()); } }); @@ -457,9 +474,11 @@ class MenuViewLayer extends FrameLayout implements } else { hideMenuAndShowMessage(); } + mMenuView.incrementTexMetricForAllTargets(TEX_METRIC_DISMISS); } else if (id == R.id.action_edit && Flags.floatingMenuDragToEdit()) { mMenuView.gotoEditScreen(); + mMenuView.incrementTexMetricForAllTargets(TEX_METRIC_EDIT); } mDismissView.hide(); mDragToInteractView.hide(); diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java index 685ea81fe40d..74ea58caf8b6 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java @@ -21,6 +21,7 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; +import android.service.voice.VisualQueryAttentionResult; import android.service.voice.VoiceInteractionSession; import android.util.Log; @@ -157,12 +158,14 @@ public class AssistManager { private final IVisualQueryDetectionAttentionListener mVisualQueryDetectionAttentionListener = new IVisualQueryDetectionAttentionListener.Stub() { @Override - public void onAttentionGained() { + public void onAttentionGained(VisualQueryAttentionResult attentionResult) { + // TODO (b/319132184): Implemented this with different types. handleVisualAttentionChanged(true); } @Override - public void onAttentionLost() { + public void onAttentionLost(int interactionIntention) { + //TODO (b/319132184): Implemented this with different types. handleVisualAttentionChanged(false); } }; @@ -472,6 +475,7 @@ public class AssistManager { }); } + // TODO (b/319132184): Implemented this with different types. private void handleVisualAttentionChanged(boolean attentionGained) { final StatusBarManager statusBarManager = mContext.getSystemService(StatusBarManager.class); if (statusBarManager != null) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDeferral.kt b/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDeferral.kt index b015f7078a83..8c68eac84963 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDeferral.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDeferral.kt @@ -20,23 +20,37 @@ import android.content.res.Resources import com.android.keyguard.logging.BiometricMessageDeferralLogger import com.android.keyguard.logging.FaceMessageDeferralLogger import com.android.systemui.Dumpable -import com.android.systemui.res.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.res.R import java.io.PrintWriter import java.util.Objects import javax.inject.Inject +@SysUISingleton +class FaceHelpMessageDeferralFactory +@Inject +constructor( + @Main private val resources: Resources, + private val logBuffer: FaceMessageDeferralLogger, + private val dumpManager: DumpManager +) { + fun create(): FaceHelpMessageDeferral { + return FaceHelpMessageDeferral( + resources = resources, + logBuffer = logBuffer, + dumpManager = dumpManager, + ) + } +} + /** * Provides whether a face acquired help message should be shown immediately when its received or * should be shown when face auth times out. See [updateMessage] and [getDeferredMessage]. */ -@SysUISingleton -class FaceHelpMessageDeferral -@Inject -constructor( - @Main resources: Resources, +class FaceHelpMessageDeferral( + resources: Resources, logBuffer: FaceMessageDeferralLogger, dumpManager: DumpManager ) : diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt index e6939f06b642..ff9cdbd7dc24 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt @@ -39,6 +39,8 @@ constructor( configurationInteractor: ConfigurationInteractor, displayStateInteractor: DisplayStateInteractor, ) { + val isUdfps: Flow<Boolean> = repository.sensorType.map { it.isUdfps() } + /** * Devices with multiple physical displays use unique display ids to determine which sensor is * on the active physical display. This value represents a unique physical display id. diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt index ef4554cfdd7c..8197145f9646 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt @@ -92,6 +92,7 @@ private const val SYS_BOOT_REASON_PROP = "sys.boot.reason.last" private const val REBOOT_MAINLINE_UPDATE = "reboot,mainline_update" private const val TAG = "BouncerMessageInteractor" +/** Handles business logic for the primary bouncer message area. */ @SysUISingleton class BouncerMessageInteractor @Inject diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt index 4a06585f5a70..9e68ff88622c 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt @@ -102,7 +102,7 @@ constructor( override val isCommunalHubShowing: Flow<Boolean> = if (sceneContainerFlags.isEnabled()) { - sceneContainerRepository.desiredScene.map { scene -> scene.key == SceneKey.Communal } + sceneContainerRepository.currentScene.map { sceneKey -> sceneKey == SceneKey.Communal } } else { desiredScene.map { sceneKey -> sceneKey == CommunalSceneKey.Communal } } diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt index 4e23ecd9c937..8178adef49b2 100644 --- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt +++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt @@ -33,6 +33,7 @@ import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.scene.shared.model.Scene +import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel @@ -95,6 +96,7 @@ interface BaseComposeFacade { viewModel: SceneContainerViewModel, windowInsets: StateFlow<WindowInsets?>, sceneByKey: Map<SceneKey, Scene>, + dataSourceDelegator: SceneDataSourceDelegator, ): View /** Creates sticky key indicator content presenting provided [viewModel] */ diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 1720de880cbd..e8931770b15e 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -88,6 +88,8 @@ import com.android.systemui.qs.footer.dagger.FooterActionsModule; import com.android.systemui.recents.Recents; import com.android.systemui.recordissue.RecordIssueModule; import com.android.systemui.retail.dagger.RetailModeModule; +import com.android.systemui.scene.shared.model.SceneDataSource; +import com.android.systemui.scene.shared.model.SceneDataSourceDelegator; import com.android.systemui.scene.ui.view.WindowRootViewComponent; import com.android.systemui.screenrecord.ScreenRecordModule; import com.android.systemui.screenshot.dagger.ScreenshotModule; @@ -393,4 +395,7 @@ public abstract class SystemUIModule { @IntoMap @ClassKey(SystemUISecondaryUserService.class) abstract Service bindsSystemUISecondaryUserService(SystemUISecondaryUserService service); + + @Binds + abstract SceneDataSource bindSceneDataSource(SceneDataSourceDelegator delegator); } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractor.kt new file mode 100644 index 000000000000..55d2bfcc8911 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractor.kt @@ -0,0 +1,174 @@ +/* + * 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.deviceentry.domain.interactor + +import android.content.res.Resources +import com.android.systemui.biometrics.domain.interactor.FingerprintPropertyInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.FaceMessage +import com.android.systemui.deviceentry.shared.model.FaceTimeoutMessage +import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage +import com.android.systemui.deviceentry.shared.model.FingerprintMessage +import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus +import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus +import com.android.systemui.res.R +import com.android.systemui.util.kotlin.sample +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge + +/** + * BiometricMessage business logic. Filters biometric error/acquired/fail/success events for + * authentication events that should never surface a message to the user at the current device + * state. + */ +@ExperimentalCoroutinesApi +@SysUISingleton +class BiometricMessageInteractor +@Inject +constructor( + @Main private val resources: Resources, + fingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, + fingerprintPropertyInteractor: FingerprintPropertyInteractor, + faceAuthInteractor: DeviceEntryFaceAuthInteractor, + biometricSettingsInteractor: DeviceEntryBiometricSettingsInteractor, +) { + private val faceHelp: Flow<HelpFaceAuthenticationStatus> = + faceAuthInteractor.authenticationStatus.filterIsInstance<HelpFaceAuthenticationStatus>() + private val faceError: Flow<ErrorFaceAuthenticationStatus> = + faceAuthInteractor.authenticationStatus.filterIsInstance<ErrorFaceAuthenticationStatus>() + private val faceFailure: Flow<FailedFaceAuthenticationStatus> = + faceAuthInteractor.authenticationStatus.filterIsInstance<FailedFaceAuthenticationStatus>() + + /** + * The acquisition message ids to show message when both fingerprint and face are enrolled and + * enabled for device entry. + */ + private val coExFaceAcquisitionMsgIdsToShow: Set<Int> = + resources.getIntArray(R.array.config_face_help_msgs_when_fingerprint_enrolled).toSet() + + private fun ErrorFingerprintAuthenticationStatus.shouldSuppressError(): Boolean { + return isCancellationError() || isPowerPressedError() + } + + private fun ErrorFaceAuthenticationStatus.shouldSuppressError(): Boolean { + return isCancellationError() || isUnableToProcessError() + } + + private val fingerprintErrorMessage: Flow<FingerprintMessage> = + fingerprintAuthInteractor.fingerprintError + .filterNot { it.shouldSuppressError() } + .sample(biometricSettingsInteractor.fingerprintAuthCurrentlyAllowed, ::Pair) + .filter { (errorStatus, fingerprintAuthAllowed) -> + fingerprintAuthAllowed || errorStatus.isLockoutError() + } + .map { (errorStatus, _) -> + when { + errorStatus.isLockoutError() -> FingerprintLockoutMessage(errorStatus.msg) + else -> FingerprintMessage(errorStatus.msg) + } + } + + private val fingerprintHelpMessage: Flow<FingerprintMessage> = + fingerprintAuthInteractor.fingerprintHelp + .sample(biometricSettingsInteractor.fingerprintAuthCurrentlyAllowed, ::Pair) + .filter { (_, fingerprintAuthAllowed) -> fingerprintAuthAllowed } + .map { (helpStatus, _) -> + FingerprintMessage( + helpStatus.msg, + ) + } + + private val fingerprintFailMessage: Flow<FingerprintMessage> = + fingerprintPropertyInteractor.isUdfps.flatMapLatest { isUdfps -> + fingerprintAuthInteractor.fingerprintFailure + .sample(biometricSettingsInteractor.fingerprintAuthCurrentlyAllowed) + .filter { fingerprintAuthAllowed -> fingerprintAuthAllowed } + .map { + FingerprintMessage( + if (isUdfps) { + resources.getString( + com.android.internal.R.string.fingerprint_udfps_error_not_match + ) + } else { + resources.getString( + com.android.internal.R.string.fingerprint_error_not_match + ) + }, + ) + } + } + + val fingerprintMessage: Flow<FingerprintMessage> = + merge( + fingerprintErrorMessage, + fingerprintFailMessage, + fingerprintHelpMessage, + ) + + private val faceHelpMessage: Flow<FaceMessage> = + biometricSettingsInteractor.fingerprintAndFaceEnrolledAndEnabled + .flatMapLatest { fingerprintAndFaceEnrolledAndEnabled -> + if (fingerprintAndFaceEnrolledAndEnabled) { + faceHelp.filter { faceAuthHelpStatus -> + coExFaceAcquisitionMsgIdsToShow.contains(faceAuthHelpStatus.msgId) + } + } else { + faceHelp + } + } + .sample(biometricSettingsInteractor.faceAuthCurrentlyAllowed, ::Pair) + .filter { (_, faceAuthCurrentlyAllowed) -> faceAuthCurrentlyAllowed } + .map { (status, _) -> FaceMessage(status.msg) } + + private val faceFailureMessage: Flow<FaceMessage> = + faceFailure + .sample(biometricSettingsInteractor.faceAuthCurrentlyAllowed) + .filter { faceAuthCurrentlyAllowed -> faceAuthCurrentlyAllowed } + .map { FaceMessage(resources.getString(R.string.keyguard_face_failed)) } + + private val faceErrorMessage: Flow<FaceMessage> = + faceError + .filterNot { it.shouldSuppressError() } + .sample(biometricSettingsInteractor.faceAuthCurrentlyAllowed, ::Pair) + .filter { (errorStatus, faceAuthCurrentlyAllowed) -> + faceAuthCurrentlyAllowed || errorStatus.isLockoutError() + } + .map { (status, _) -> + when { + status.isTimeoutError() -> FaceTimeoutMessage(status.msg) + else -> FaceMessage(status.msg) + } + } + + // TODO(b/317215391): support showing face acquired messages on timeout + face lockout errors + val faceMessage: Flow<FaceMessage> = + merge( + faceHelpMessage, + faceFailureMessage, + faceErrorMessage, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractor.kt new file mode 100644 index 000000000000..4515fcb545b3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractor.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.deviceentry.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +/** Encapsulates business logic for device entry biometric settings. */ +@ExperimentalCoroutinesApi +@SysUISingleton +class DeviceEntryBiometricSettingsInteractor +@Inject +constructor( + repository: BiometricSettingsRepository, +) { + val fingerprintAuthCurrentlyAllowed: Flow<Boolean> = + repository.isFingerprintAuthCurrentlyAllowed + val faceAuthCurrentlyAllowed: Flow<Boolean> = repository.isFaceAuthCurrentlyAllowed + + /** Whether both fingerprint and face are enrolled and enabled for device entry. */ + val fingerprintAndFaceEnrolledAndEnabled: Flow<Boolean> = + combine( + repository.isFingerprintEnrolledAndEnabled, + repository.isFaceAuthEnrolledAndEnabled, + ) { fpEnabled, faceEnabled -> + fpEnabled && faceEnabled + } +} diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt index 2461c26a56ca..a5f6f7c77a38 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt @@ -18,8 +18,10 @@ package com.android.systemui.deviceentry.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterIsInstance @@ -30,9 +32,6 @@ class DeviceEntryFingerprintAuthInteractor constructor( repository: DeviceEntryFingerprintAuthRepository, ) { - val fingerprintFailure: Flow<FailFingerprintAuthenticationStatus> = - repository.authenticationStatus.filterIsInstance<FailFingerprintAuthenticationStatus>() - /** Whether fingerprint authentication is currently running or not */ val isRunning: Flow<Boolean> = repository.isRunning @@ -41,4 +40,11 @@ constructor( repository.authenticationStatus val isLockedOut: Flow<Boolean> = repository.isLockedOut + + val fingerprintFailure: Flow<FailFingerprintAuthenticationStatus> = + repository.authenticationStatus.filterIsInstance<FailFingerprintAuthenticationStatus>() + val fingerprintError: Flow<ErrorFingerprintAuthenticationStatus> = + repository.authenticationStatus.filterIsInstance<ErrorFingerprintAuthenticationStatus>() + val fingerprintHelp: Flow<HelpFingerprintAuthenticationStatus> = + repository.authenticationStatus.filterIsInstance<HelpFingerprintAuthenticationStatus>() } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt index 73389cb1bdce..21fd87c1c241 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt @@ -26,7 +26,6 @@ import com.android.systemui.keyguard.data.repository.TrustRepository import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -80,8 +79,7 @@ constructor( * Note: This does not imply that the lockscreen is visible or not. */ val isDeviceEntered: StateFlow<Boolean> = - sceneInteractor.desiredScene - .map { it.key } + sceneInteractor.currentScene .filter { currentScene -> currentScene == SceneKey.Gone || currentScene == SceneKey.Lockscreen } @@ -150,12 +148,12 @@ constructor( applicationScope.launch { if (isAuthenticationRequired()) { sceneInteractor.changeScene( - scene = SceneModel(SceneKey.Bouncer), + toScene = SceneKey.Bouncer, loggingReason = "request to unlock device while authentication required", ) } else { sceneInteractor.changeScene( - scene = SceneModel(SceneKey.Gone), + toScene = SceneKey.Gone, loggingReason = "request to unlock device while authentication isn't required", ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt index 63823385dc21..79455ebb624c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * 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. @@ -11,10 +11,10 @@ * 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 + * limitations under the License. */ -package com.android.systemui.keyguard.domain.interactor +package com.android.systemui.deviceentry.domain.interactor import android.content.Context import android.content.Intent @@ -22,7 +22,10 @@ import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.deviceentry.shared.model.BiometricMessage +import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.plugins.ActivityStarter @@ -40,7 +43,6 @@ import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch /** Business logic for handling authentication events when an app is occluding the lockscreen. */ @@ -77,16 +79,15 @@ constructor( private val fingerprintLockoutEvents: Flow<Unit> = fingerprintAuthRepository.authenticationStatus .ifKeyguardOccludedByApp() - .filter { it is ErrorFingerprintAuthenticationStatus && it.isLockoutMessage() } + .filter { it is ErrorFingerprintAuthenticationStatus && it.isLockoutError() } .map {} // maps FingerprintAuthenticationStatus => Unit val message: Flow<BiometricMessage?> = - merge( - biometricMessageInteractor.fingerprintErrorMessage.filterNot { - it.isFingerprintLockoutMessage() - }, - biometricMessageInteractor.fingerprintFailMessage, - biometricMessageInteractor.fingerprintHelpMessage, - ) + biometricMessageInteractor.fingerprintMessage + .filterNot { fingerprintMessage -> + // On lockout, the device will show the bouncer. Let's not show the message + // before the transition or else it'll look flickery. + fingerprintMessage is FingerprintLockoutMessage + } .ifKeyguardOccludedByApp(/* elseFlow */ flowOf(null)) init { diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/shared/model/BiometricMessageModels.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/shared/model/BiometricMessageModels.kt new file mode 100644 index 000000000000..118215c6ba15 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/shared/model/BiometricMessageModels.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.deviceentry.shared.model + +/** + * BiometricMessage provided by + * [com.android.systemui.deviceentry.domain.interactor.BiometricMessageInteractor] + */ +sealed class BiometricMessage( + val message: String?, +) + +/** Face biometric message */ +open class FaceMessage(faceMessage: String?) : BiometricMessage(faceMessage) + +data class FaceTimeoutMessage( + private val faceTimeoutMessage: String?, +) : FaceMessage(faceTimeoutMessage) + +/** Fingerprint biometric message */ +open class FingerprintMessage(fingerprintMessage: String?) : BiometricMessage(fingerprintMessage) + +data class FingerprintLockoutMessage( + private val fingerprintLockoutMessage: String?, +) : FingerprintMessage(fingerprintLockoutMessage) diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/shared/model/FaceAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/shared/model/FaceAuthenticationModels.kt index f006b3484033..5f1667a70e5a 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/shared/model/FaceAuthenticationModels.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/shared/model/FaceAuthenticationModels.kt @@ -71,11 +71,16 @@ data class ErrorFaceAuthenticationStatus( */ fun isCancellationError() = msgId == FaceManager.FACE_ERROR_CANCELED + fun isUnableToProcessError() = msgId == FaceManager.FACE_ERROR_UNABLE_TO_PROCESS + /** Method that checks if [msgId] is a hardware error. */ fun isHardwareError() = msgId == FaceManager.FACE_ERROR_HW_UNAVAILABLE || msgId == FaceManager.FACE_ERROR_UNABLE_TO_PROCESS + /** Method that checks if [msgId] is a timeout error. */ + fun isTimeoutError() = msgId == FaceManager.FACE_ERROR_TIMEOUT + companion object { /** * Error message that is created when cancel confirmation is not received from FaceManager diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index abe49eefda99..86b99ecac66c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -100,6 +100,7 @@ constructor( private val keyguardClockViewModel: KeyguardClockViewModel, private val lockscreenContentViewModel: LockscreenContentViewModel, private val lockscreenSceneBlueprintsLazy: Lazy<Set<LockscreenSceneBlueprint>>, + private val keyguardBlueprintViewBinder: KeyguardBlueprintViewBinder, ) : CoreStartable { private var rootViewHandle: DisposableHandle? = null @@ -143,7 +144,7 @@ constructor( cs.connect(composeView.id, BOTTOM, PARENT_ID, BOTTOM) keyguardRootView.addView(composeView) } else { - KeyguardBlueprintViewBinder.bind( + keyguardBlueprintViewBinder.bind( keyguardRootView, keyguardBlueprintViewModel, keyguardClockViewModel, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 0b227fa7f5a6..968c3e3a6792 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -73,6 +73,7 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; import com.android.systemui.util.DeviceConfigProxy; +import com.android.systemui.util.ThreadAssert; import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.util.settings.SystemSettings; @@ -223,6 +224,13 @@ public interface KeyguardModule { return new KeyguardQuickAffordancesMetricsLoggerImpl(); } + /** */ + @Provides + @SysUISingleton + static ThreadAssert providesThreadAssert() { + return new ThreadAssert(); + } + /** Binds {@link KeyguardUpdateMonitor} as a {@link CoreStartable}. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt index 938183025336..0659c7c7d79c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt @@ -17,14 +17,16 @@ package com.android.systemui.keyguard.data.repository +import android.os.Handler import android.util.Log import com.android.systemui.common.ui.data.repository.ConfigurationRepository import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.shared.model.KeyguardBlueprint import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT import com.android.systemui.keyguard.ui.view.layout.blueprints.KeyguardBlueprintModule -import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType -import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType.DefaultTransition +import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config +import com.android.systemui.util.ThreadAssert import java.io.PrintWriter import java.util.TreeMap import javax.inject.Inject @@ -49,16 +51,17 @@ class KeyguardBlueprintRepository constructor( configurationRepository: ConfigurationRepository, blueprints: Set<@JvmSuppressWildcards KeyguardBlueprint>, + @Main val handler: Handler, + val assert: ThreadAssert, ) { // This is TreeMap so that we can order the blueprints and assign numerical values to the // blueprints in the adb tool. private val blueprintIdMap: TreeMap<String, KeyguardBlueprint> = TreeMap<String, KeyguardBlueprint>().apply { putAll(blueprints.associateBy { it.id }) } val blueprint: MutableStateFlow<KeyguardBlueprint> = MutableStateFlow(blueprintIdMap[DEFAULT]!!) - val refreshBluePrint: MutableSharedFlow<Unit> = MutableSharedFlow(extraBufferCapacity = 1) - val refreshBlueprintTransition: MutableSharedFlow<IntraBlueprintTransitionType> = - MutableSharedFlow(extraBufferCapacity = 1) + val refreshTransition = MutableSharedFlow<Config>(extraBufferCapacity = 1) val configurationChange: Flow<Unit> = configurationRepository.onAnyConfigurationChange + private var targetTransitionConfig: Config? = null /** * Emits the blueprint value to the collectors. @@ -105,14 +108,32 @@ constructor( blueprint?.let { this.blueprint.value = it } } - /** Re-emits the last emitted blueprint value if possible. */ - fun refreshBlueprint() { - refreshBlueprintWithTransition(DefaultTransition) - } + /** + * Re-emits the last emitted blueprint value if possible. This is delayed until next frame to + * dedupe requests and determine the correct transition to execute. + */ + fun refreshBlueprint(config: Config = Config.DEFAULT) { + fun scheduleCallback() { + // We use a handler here instead of a CoroutineDipsatcher because the one provided by + // @Main CoroutineDispatcher is currently Dispatchers.Main.immediate, which doesn't + // delay the callback, and instead runs it imemdiately. + handler.post { + assert.isMainThread() + targetTransitionConfig?.let { + val success = refreshTransition.tryEmit(it) + if (!success) { + Log.e(TAG, "refreshBlueprint: Failed to emit blueprint refresh: $it") + } + } + targetTransitionConfig = null + } + } - fun refreshBlueprintWithTransition(type: IntraBlueprintTransitionType = DefaultTransition) { - refreshBluePrint.tryEmit(Unit) - refreshBlueprintTransition.tryEmit(type) + assert.isMainThread() + if ((targetTransitionConfig?.type?.priority ?: Int.MIN_VALUE) < config.type.priority) { + if (targetTransitionConfig == null) scheduleCallback() + targetTransitionConfig = config + } } /** Prints all available blueprints to the PrintWriter. */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractor.kt deleted file mode 100644 index 508f71a2c4a5..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractor.kt +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.keyguard.domain.interactor - -import android.content.res.Resources -import android.hardware.biometrics.BiometricSourceType -import android.hardware.biometrics.BiometricSourceType.FINGERPRINT -import android.hardware.fingerprint.FingerprintManager -import com.android.keyguard.KeyguardUpdateMonitor -import com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED -import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository -import com.android.systemui.biometrics.shared.model.FingerprintSensorType -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository -import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus -import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus -import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus -import com.android.systemui.keyguard.util.IndicationHelper -import javax.inject.Inject -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterNot -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map - -/** - * BiometricMessage business logic. Filters biometric error/acquired/fail/success events for - * authentication events that should never surface a message to the user at the current device - * state. - */ -@ExperimentalCoroutinesApi -@SysUISingleton -class BiometricMessageInteractor -@Inject -constructor( - @Main private val resources: Resources, - private val fingerprintAuthRepository: DeviceEntryFingerprintAuthRepository, - private val fingerprintPropertyRepository: FingerprintPropertyRepository, - private val indicationHelper: IndicationHelper, - private val keyguardUpdateMonitor: KeyguardUpdateMonitor, -) { - val fingerprintErrorMessage: Flow<BiometricMessage> = - fingerprintAuthRepository.authenticationStatus - .filter { - it is ErrorFingerprintAuthenticationStatus && - !indicationHelper.shouldSuppressErrorMsg(FINGERPRINT, it.msgId) - } - .map { - val errorStatus = it as ErrorFingerprintAuthenticationStatus - BiometricMessage( - FINGERPRINT, - BiometricMessageType.ERROR, - errorStatus.msgId, - errorStatus.msg, - ) - } - - val fingerprintHelpMessage: Flow<BiometricMessage> = - fingerprintAuthRepository.authenticationStatus - .filter { it is HelpFingerprintAuthenticationStatus } - .filterNot { isPrimaryAuthRequired() } - .map { - val helpStatus = it as HelpFingerprintAuthenticationStatus - BiometricMessage( - FINGERPRINT, - BiometricMessageType.HELP, - helpStatus.msgId, - helpStatus.msg, - ) - } - - val fingerprintFailMessage: Flow<BiometricMessage> = - isUdfps().flatMapLatest { isUdfps -> - fingerprintAuthRepository.authenticationStatus - .filter { it is FailFingerprintAuthenticationStatus } - .filterNot { isPrimaryAuthRequired() } - .map { - BiometricMessage( - FINGERPRINT, - BiometricMessageType.FAIL, - BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED, - if (isUdfps) { - resources.getString( - com.android.internal.R.string.fingerprint_udfps_error_not_match - ) - } else { - resources.getString( - com.android.internal.R.string.fingerprint_error_not_match - ) - }, - ) - } - } - - private fun isUdfps() = - fingerprintPropertyRepository.sensorType.map { - it == FingerprintSensorType.UDFPS_OPTICAL || - it == FingerprintSensorType.UDFPS_ULTRASONIC - } - - private fun isPrimaryAuthRequired(): Boolean { - // Only checking if unlocking with Biometric is allowed (no matter strong or non-strong - // as long as primary auth, i.e. PIN/pattern/password, is required), so it's ok to - // pass true for isStrongBiometric to isUnlockingWithBiometricAllowed() to bypass the - // check of whether non-strong biometric is allowed since strong biometrics can still be - // used. - return !keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(true /* isStrongBiometric */) - } -} - -data class BiometricMessage( - val source: BiometricSourceType, - val type: BiometricMessageType, - val id: Int, - val message: String?, -) { - fun isFingerprintLockoutMessage(): Boolean { - return source == FINGERPRINT && - type == BiometricMessageType.ERROR && - (id == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT || - id == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT) - } -} - -enum class BiometricMessageType { - HELP, - ERROR, - FAIL, -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt index 566e006598a5..56d64a298bc0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt @@ -24,13 +24,12 @@ import com.android.systemui.keyguard.data.repository.KeyguardBlueprintRepository import com.android.systemui.keyguard.shared.model.KeyguardBlueprint import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint -import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType -import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType.DefaultTransition +import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config +import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type import com.android.systemui.statusbar.policy.SplitShadeStateController import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch @@ -44,20 +43,14 @@ constructor( private val splitShadeStateController: SplitShadeStateController, ) { - /** - * The current blueprint for the lockscreen. - * - * This flow can also emit the same blueprint value if refreshBlueprint is emitted. - */ + /** The current blueprint for the lockscreen. */ val blueprint: Flow<KeyguardBlueprint> = keyguardBlueprintRepository.blueprint - val blueprintWithTransition = - combine( - keyguardBlueprintRepository.refreshBluePrint, - keyguardBlueprintRepository.refreshBlueprintTransition - ) { _, source -> - source - } + /** + * Triggered when the blueprint isn't changed, but the ConstraintSet should be rebuilt and + * optionally a transition should be fired to move to the rebuilt ConstraintSet. + */ + val refreshTransition = keyguardBlueprintRepository.refreshTransition init { applicationScope.launch { @@ -105,14 +98,11 @@ constructor( return keyguardBlueprintRepository.applyBlueprint(blueprintId) } - /** Re-emits the blueprint value to the collectors. */ - fun refreshBlueprint() { - keyguardBlueprintRepository.refreshBlueprint() - } + /** Emits a value to refresh the blueprint with the appropriate transition. */ + fun refreshBlueprint(type: Type = Type.NoTransition) = refreshBlueprint(Config(type)) - fun refreshBlueprintWithTransition(type: IntraBlueprintTransitionType = DefaultTransition) { - keyguardBlueprintRepository.refreshBlueprintWithTransition(type) - } + /** Emits a value to refresh the blueprint with the appropriate transition. */ + fun refreshBlueprint(config: Config) = keyguardBlueprintRepository.refreshBlueprint(config) fun getCurrentBlueprint(): KeyguardBlueprint { return keyguardBlueprintRepository.blueprint.value diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt index 474de77f09ab..d8b7b4a6a3d4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.shared.model +import android.hardware.biometrics.BiometricFingerprintConstants import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START import android.hardware.fingerprint.FingerprintManager @@ -61,8 +62,14 @@ data class ErrorFingerprintAuthenticationStatus( // present to break equality check if the same error occurs repeatedly. val createdAt: Long = elapsedRealtime(), ) : FingerprintAuthenticationStatus() { - fun isLockoutMessage(): Boolean { - return msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT || + fun isCancellationError(): Boolean = + msgId == BiometricFingerprintConstants.FINGERPRINT_ERROR_CANCELED || + msgId == BiometricFingerprintConstants.FINGERPRINT_ERROR_USER_CANCELED + + fun isPowerPressedError(): Boolean = + msgId == BiometricFingerprintConstants.BIOMETRIC_ERROR_POWER_PRESSED + + fun isLockoutError(): Boolean = + msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT || msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT - } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerMessageAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerMessageAreaViewBinder.kt new file mode 100644 index 000000000000..9186dde9a9bf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerMessageAreaViewBinder.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.binder + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.keyguard.AuthKeyguardMessageArea +import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerMessageAreaViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import kotlinx.coroutines.ExperimentalCoroutinesApi + +/** Binds the alternate bouncer message view to its view-model. */ +@ExperimentalCoroutinesApi +object AlternateBouncerMessageAreaViewBinder { + + /** Binds the view to the view-model, continuing to update the former based on the latter. */ + @JvmStatic + fun bind( + view: AuthKeyguardMessageArea, + viewModel: AlternateBouncerMessageAreaViewModel, + ) { + view.setIsVisible(true) + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.message.collect { biometricMsg -> + if (biometricMsg == null) { + view.setMessage("", true) + } else { + view.setMessage(biometricMsg.message, true) + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt index b2a35495c693..d40021007a2b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt @@ -62,6 +62,11 @@ object AlternateBouncerViewBinder { alternateBouncerDependencies.udfpsAccessibilityOverlayViewModel, ) + AlternateBouncerMessageAreaViewBinder.bind( + view = view.requireViewById(R.id.alternate_bouncer_message_area), + viewModel = alternateBouncerDependencies.messageAreaViewModel, + ) + val scrim = view.requireViewById(R.id.alternate_bouncer_scrim) as ScrimView val viewModel = alternateBouncerDependencies.viewModel val swipeUpAnywhereGestureHandler = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt index 404046b00b66..6e70368476ed 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt @@ -17,7 +17,9 @@ package com.android.systemui.keyguard.ui.binder +import android.os.Handler import android.os.Trace +import android.transition.Transition import android.transition.TransitionManager import android.util.Log import androidx.constraintlayout.widget.ConstraintLayout @@ -25,98 +27,168 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.Flags.keyguardBottomAreaRefactor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.BaseBlueprintTransition import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition -import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType +import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.lifecycle.repeatWhenAttached +import javax.inject.Inject +import kotlin.math.max import kotlinx.coroutines.launch -class KeyguardBlueprintViewBinder { - companion object { - private const val TAG = "KeyguardBlueprintViewBinder" - - fun bind( - constraintLayout: ConstraintLayout, - viewModel: KeyguardBlueprintViewModel, - clockViewModel: KeyguardClockViewModel - ) { - constraintLayout.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - viewModel.blueprint.collect { blueprint -> - val prevBluePrint = viewModel.currentBluePrint - Trace.beginSection("KeyguardBlueprint#applyBlueprint") - Log.d(TAG, "applying blueprint: $blueprint") - TransitionManager.endTransitions(constraintLayout) - - val cs = - ConstraintSet().apply { - clone(constraintLayout) - val emptyLayout = ConstraintSet.Layout() - knownIds.forEach { - getConstraint(it).layout.copyFrom(emptyLayout) - } - blueprint.applyConstraints(this) - } - - // Apply transition. +private const val TAG = "KeyguardBlueprintViewBinder" +private const val DEBUG = true + +@SysUISingleton +class KeyguardBlueprintViewBinder +@Inject +constructor( + @Main private val handler: Handler, +) { + private var runningPriority = -1 + private val runningTransitions = mutableSetOf<Transition>() + private val isTransitionRunning: Boolean + get() = runningTransitions.size > 0 + private val transitionListener = + object : Transition.TransitionListener { + override fun onTransitionCancel(transition: Transition) { + if (DEBUG) Log.e(TAG, "onTransitionCancel: ${transition::class.simpleName}") + runningTransitions.remove(transition) + } + + override fun onTransitionEnd(transition: Transition) { + if (DEBUG) Log.e(TAG, "onTransitionEnd: ${transition::class.simpleName}") + runningTransitions.remove(transition) + } + + override fun onTransitionPause(transition: Transition) { + if (DEBUG) Log.i(TAG, "onTransitionPause: ${transition::class.simpleName}") + runningTransitions.remove(transition) + } + + override fun onTransitionResume(transition: Transition) { + if (DEBUG) Log.i(TAG, "onTransitionResume: ${transition::class.simpleName}") + runningTransitions.add(transition) + } + + override fun onTransitionStart(transition: Transition) { + if (DEBUG) Log.i(TAG, "onTransitionStart: ${transition::class.simpleName}") + runningTransitions.add(transition) + } + } + + fun bind( + constraintLayout: ConstraintLayout, + viewModel: KeyguardBlueprintViewModel, + clockViewModel: KeyguardClockViewModel, + ) { + constraintLayout.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.CREATED) { + launch { + viewModel.blueprint.collect { blueprint -> + Trace.beginSection("KeyguardBlueprintViewBinder#applyBlueprint") + val prevBluePrint = viewModel.currentBluePrint + + val cs = + ConstraintSet().apply { + clone(constraintLayout) + val emptyLayout = ConstraintSet.Layout() + knownIds.forEach { getConstraint(it).layout.copyFrom(emptyLayout) } + blueprint.applyConstraints(this) + } + + var transition = if ( !keyguardBottomAreaRefactor() && prevBluePrint != null && prevBluePrint != blueprint ) { - TransitionManager.beginDelayedTransition( - constraintLayout, - BaseBlueprintTransition(clockViewModel) - .addTransition( - IntraBlueprintTransition( - IntraBlueprintTransitionType.NoTransition, - clockViewModel - ) - ) - ) - } else { - TransitionManager.beginDelayedTransition( - constraintLayout, - IntraBlueprintTransition( - IntraBlueprintTransitionType.NoTransition, - clockViewModel + BaseBlueprintTransition(clockViewModel) + .addTransition( + IntraBlueprintTransition(Config.DEFAULT, clockViewModel) ) - ) + } else { + IntraBlueprintTransition(Config.DEFAULT, clockViewModel) } - // Add and remove views of sections that are not contained by the - // other. + runTransition(constraintLayout, transition, Config.DEFAULT) { + // Add and remove views of sections that are not contained by the other. blueprint.replaceViews(prevBluePrint, constraintLayout) cs.applyTo(constraintLayout) - - viewModel.currentBluePrint = blueprint - Trace.endSection() } - } - launch { - viewModel.blueprintWithTransition.collect { source -> - TransitionManager.endTransitions(constraintLayout) + viewModel.currentBluePrint = blueprint + Trace.endSection() + } + } - val cs = - ConstraintSet().apply { - clone(constraintLayout) - viewModel.currentBluePrint?.applyConstraints(this) - } + launch { + viewModel.refreshTransition.collect { transition -> + Trace.beginSection("KeyguardBlueprintViewBinder#refreshTransition") + val cs = + ConstraintSet().apply { + clone(constraintLayout) + viewModel.currentBluePrint?.applyConstraints(this) + } - TransitionManager.beginDelayedTransition( - constraintLayout, - IntraBlueprintTransition(source, clockViewModel) - ) + runTransition( + constraintLayout, + IntraBlueprintTransition(transition, clockViewModel), + transition, + ) { cs.applyTo(constraintLayout) - Trace.endSection() } + Trace.endSection() } } } } } + + private fun runTransition( + constraintLayout: ConstraintLayout, + transition: Transition, + config: Config, + apply: () -> Unit, + ) { + val currentPriority = if (isTransitionRunning) runningPriority else -1 + if (config.checkPriority && config.type.priority < currentPriority) { + if (DEBUG) { + Log.w( + TAG, + "runTransition: skipping ${transition::class.simpleName}: " + + "currentPriority=$currentPriority; config=$config" + ) + } + apply() + return + } + + if (DEBUG) { + Log.i( + TAG, + "runTransition: running ${transition::class.simpleName}: " + + "currentPriority=$currentPriority; config=$config" + ) + } + + // beginDelayedTransition makes a copy, so we temporarially add the uncopied transition to + // the running set until the copy is started by the handler. + runningTransitions.add(transition) + transition.addListener(transitionListener) + runningPriority = max(currentPriority, config.type.priority) + + handler.post { + if (config.terminatePrevious) { + TransitionManager.endTransitions(constraintLayout) + } + + TransitionManager.beginDelayedTransition(constraintLayout, transition) + runningTransitions.remove(transition) + apply() + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt index 62a6e8b45285..01596ed2e3ef 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt @@ -30,7 +30,7 @@ import com.android.keyguard.KeyguardClockSwitch.SMALL import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor -import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType +import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.lifecycle.repeatWhenAttached @@ -39,6 +39,8 @@ import com.android.systemui.shared.clocks.DEFAULT_CLOCK_ID import kotlinx.coroutines.launch object KeyguardClockViewBinder { + private val TAG = KeyguardClockViewBinder::class.simpleName!! + @JvmStatic fun bind( clockSection: ClockSection, @@ -68,9 +70,7 @@ object KeyguardClockViewBinder { if (!migrateClocksToBlueprint()) return@launch viewModel.clockSize.collect { updateBurnInLayer(keyguardRootView, viewModel) - blueprintInteractor.refreshBlueprintWithTransition( - IntraBlueprintTransitionType.ClockSize - ) + blueprintInteractor.refreshBlueprint(Type.ClockSize) } } launch { @@ -83,13 +83,9 @@ object KeyguardClockViewBinder { it.largeClock.config.hasCustomPositionUpdatedAnimation && it.config.id == DEFAULT_CLOCK_ID ) { - blueprintInteractor.refreshBlueprintWithTransition( - IntraBlueprintTransitionType.DefaultClockStepping - ) + blueprintInteractor.refreshBlueprint(Type.DefaultClockStepping) } else { - blueprintInteractor.refreshBlueprintWithTransition( - IntraBlueprintTransitionType.DefaultTransition - ) + blueprintInteractor.refreshBlueprint(Type.DefaultTransition) } } } @@ -102,9 +98,7 @@ object KeyguardClockViewBinder { if ( viewModel.useLargeClock && it.config.id == "DIGITAL_CLOCK_WEATHER" ) { - blueprintInteractor.refreshBlueprintWithTransition( - IntraBlueprintTransitionType.DefaultTransition - ) + blueprintInteractor.refreshBlueprint(Type.DefaultTransition) } } } @@ -112,6 +106,7 @@ object KeyguardClockViewBinder { } } } + @VisibleForTesting fun updateBurnInLayer( keyguardRootView: ConstraintLayout, @@ -171,6 +166,7 @@ object KeyguardClockViewBinder { } } } + fun applyConstraints( clockSection: ClockSection, rootView: ConstraintLayout, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt index 08a2b9cb8968..b77f0c5a1e60 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt @@ -23,6 +23,8 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor +import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config +import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.lifecycle.repeatWhenAttached @@ -49,7 +51,13 @@ object KeyguardSmartspaceViewBinder { clockViewModel, smartspaceViewModel ) - blueprintInteractor.refreshBlueprintWithTransition() + blueprintInteractor.refreshBlueprint( + Config( + Type.SmartspaceVisibility, + checkPriority = false, + terminatePrevious = false, + ) + ) } } @@ -57,7 +65,13 @@ object KeyguardSmartspaceViewBinder { if (!migrateClocksToBlueprint()) return@launch smartspaceViewModel.bcSmartspaceVisibility.collect { updateBCSmartspaceInBurnInLayer(keyguardRootView, clockViewModel) - blueprintInteractor.refreshBlueprintWithTransition() + blueprintInteractor.refreshBlueprint( + Config( + Type.SmartspaceVisibility, + checkPriority = false, + terminatePrevious = false, + ) + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt index 524aa1a95d2d..a7075d97459e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt @@ -21,25 +21,42 @@ import com.android.systemui.keyguard.ui.view.layout.sections.transitions.ClockSi import com.android.systemui.keyguard.ui.view.layout.sections.transitions.DefaultClockSteppingTransition import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel -enum class IntraBlueprintTransitionType { - ClockSize, - ClockCenter, - DefaultClockStepping, - DefaultTransition, - AodNotifIconsTransition, - // When transition between blueprint, we don't need any duration or interpolator but we need - // all elements go to correct state - NoTransition, -} - class IntraBlueprintTransition( - type: IntraBlueprintTransitionType, - clockViewModel: KeyguardClockViewModel + config: IntraBlueprintTransition.Config, + clockViewModel: KeyguardClockViewModel, ) : TransitionSet() { + + enum class Type( + val priority: Int, + ) { + ClockSize(100), + ClockCenter(99), + DefaultClockStepping(98), + AodNotifIconsTransition(97), + SmartspaceVisibility(2), + DefaultTransition(1), + // When transition between blueprint, we don't need any duration or interpolator but we need + // all elements go to correct state + NoTransition(0), + } + + data class Config( + val type: Type, + val checkPriority: Boolean = true, + val terminatePrevious: Boolean = true, + ) { + companion object { + val DEFAULT = Config(Type.NoTransition) + } + } + init { ordering = ORDERING_TOGETHER - if (type == IntraBlueprintTransitionType.DefaultClockStepping) - addTransition(clockViewModel.clock?.let { DefaultClockSteppingTransition(it) }) - addTransition(ClockSizeTransition(type, clockViewModel)) + when (config.type) { + Type.NoTransition -> {} + Type.DefaultClockStepping -> + addTransition(clockViewModel.clock?.let { DefaultClockSteppingTransition(it) }) + else -> addTransition(ClockSizeTransition(config, clockViewModel)) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index 631b3427dae9..54a7ca47d4e7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -24,7 +24,7 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.END -import androidx.constraintlayout.widget.ConstraintSet.INVISIBLE +import androidx.constraintlayout.widget.ConstraintSet.GONE import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP @@ -52,11 +52,6 @@ internal fun ConstraintSet.setVisibility( visibility: Int, ) = views.forEach { view -> this.setVisibility(view.id, visibility) } -internal fun ConstraintSet.setAlpha( - views: Iterable<View>, - alpha: Float, -) = views.forEach { view -> this.setAlpha(view.id, alpha) } - open class ClockSection @Inject constructor( @@ -105,7 +100,7 @@ constructor( // Add constraint between elements in clock and clock container return constraintSet.apply { setVisibility(getTargetClockFace(clock).views, VISIBLE) - setVisibility(getNonTargetClockFace(clock).views, INVISIBLE) + setVisibility(getNonTargetClockFace(clock).views, GONE) if (!keyguardClockViewModel.useLargeClock) { connect(sharedR.id.bc_smartspace_view, TOP, sharedR.id.date_smartspace_view, BOTTOM) } @@ -150,6 +145,7 @@ constructor( } } } + open fun applyDefaultConstraints(constraints: ConstraintSet) { val guideline = if (keyguardClockViewModel.clockShouldBeCentered.value) PARENT_ID @@ -168,8 +164,8 @@ constructor( largeClockTopMargin += getDimen(ENHANCED_SMARTSPACE_HEIGHT) connect(R.id.lockscreen_clock_view_large, TOP, PARENT_ID, TOP, largeClockTopMargin) - constrainHeight(R.id.lockscreen_clock_view_large, WRAP_CONTENT) constrainWidth(R.id.lockscreen_clock_view_large, WRAP_CONTENT) + constrainHeight(R.id.lockscreen_clock_view_large, WRAP_CONTENT) constrainWidth(R.id.lockscreen_clock_view, WRAP_CONTENT) constrainHeight( R.id.lockscreen_clock_view, @@ -190,11 +186,10 @@ constructor( context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) + Utils.getStatusBarHeaderHeightKeyguard(context) } - if (keyguardClockViewModel.useLargeClock) { - smallClockTopMargin -= - context.resources.getDimensionPixelSize(customizationR.dimen.small_clock_height) - } - connect(R.id.lockscreen_clock_view, TOP, PARENT_ID, TOP, smallClockTopMargin) + + create(R.id.small_clock_guideline_top, ConstraintSet.HORIZONTAL_GUIDELINE) + setGuidelineBegin(R.id.small_clock_guideline_top, smallClockTopMargin) + connect(R.id.lockscreen_clock_view, TOP, R.id.small_clock_guideline_top, BOTTOM) } constrainWeatherClockDateIconsBarrier(constraints) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt index 2f99719df36c..8255bcc87400 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt @@ -53,14 +53,14 @@ constructor( private var dateView: View? = null private var smartspaceVisibilityListener: OnGlobalLayoutListener? = null + private var pastVisibility: Int = -1 override fun addViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { - return - } + if (!migrateClocksToBlueprint()) return smartspaceView = smartspaceController.buildAndConnectView(constraintLayout) weatherView = smartspaceController.buildAndConnectWeatherView(constraintLayout) dateView = smartspaceController.buildAndConnectDateView(constraintLayout) + pastVisibility = smartspaceView?.visibility ?: View.GONE if (keyguardSmartspaceViewModel.isSmartspaceEnabled) { constraintLayout.addView(smartspaceView) if (keyguardSmartspaceViewModel.isDateWeatherDecoupled) { @@ -69,26 +69,20 @@ constructor( } } keyguardUnlockAnimationController.lockscreenSmartspace = smartspaceView - smartspaceVisibilityListener = - object : OnGlobalLayoutListener { - var pastVisibility = GONE - override fun onGlobalLayout() { - smartspaceView?.let { - val newVisibility = it.visibility - if (pastVisibility != newVisibility) { - keyguardSmartspaceInteractor.setBcSmartspaceVisibility(newVisibility) - pastVisibility = newVisibility - } - } + smartspaceVisibilityListener = OnGlobalLayoutListener { + smartspaceView?.let { + val newVisibility = it.visibility + if (pastVisibility != newVisibility) { + keyguardSmartspaceInteractor.setBcSmartspaceVisibility(newVisibility) + pastVisibility = newVisibility } } + } smartspaceView?.viewTreeObserver?.addOnGlobalLayoutListener(smartspaceVisibilityListener) } override fun bindData(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { - return - } + if (!migrateClocksToBlueprint()) return KeyguardSmartspaceViewBinder.bind( constraintLayout, keyguardClockViewModel, @@ -98,9 +92,7 @@ constructor( } override fun applyConstraints(constraintSet: ConstraintSet) { - if (!migrateClocksToBlueprint()) { - return - } + if (!migrateClocksToBlueprint()) return val horizontalPaddingStart = context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) + context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal) @@ -196,9 +188,7 @@ constructor( } override fun removeViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { - return - } + if (!migrateClocksToBlueprint()) return listOf(smartspaceView, dateView, weatherView).forEach { it?.let { if (it.parent == constraintLayout) { @@ -211,6 +201,9 @@ constructor( } private fun updateVisibility(constraintSet: ConstraintSet) { + // This may update the visibility of the smartspace views + smartspaceController.requestSmartspaceUpdate() + constraintSet.apply { setVisibility( sharedR.id.weather_smartspace_view, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt index 99565b104a00..64cbb3229a57 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt @@ -17,128 +17,192 @@ package com.android.systemui.keyguard.ui.view.layout.sections.transitions import android.animation.Animator -import android.animation.ObjectAnimator -import android.transition.ChangeBounds +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.graphics.Rect +import android.transition.Transition import android.transition.TransitionSet import android.transition.TransitionValues -import android.transition.Visibility +import android.util.Log import android.view.View import android.view.ViewGroup +import android.view.ViewTreeObserver.OnPreDrawListener import com.android.app.animation.Interpolators -import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType +import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition +import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.res.R import com.android.systemui.shared.R as sharedR +import kotlin.math.abs -const val CLOCK_OUT_MILLIS = 133L -const val CLOCK_IN_MILLIS = 167L -val CLOCK_IN_INTERPOLATOR = Interpolators.LINEAR_OUT_SLOW_IN -const val CLOCK_IN_START_DELAY_MILLIS = 133L -val CLOCK_OUT_INTERPOLATOR = Interpolators.LINEAR +internal fun View.setRect(rect: Rect) = + this.setLeftTopRightBottom(rect.left, rect.top, rect.right, rect.bottom) class ClockSizeTransition( - val type: IntraBlueprintTransitionType, - clockViewModel: KeyguardClockViewModel + config: IntraBlueprintTransition.Config, + clockViewModel: KeyguardClockViewModel, ) : TransitionSet() { init { ordering = ORDERING_TOGETHER - addTransition(ClockOutTransition(clockViewModel, type)) - addTransition(ClockInTransition(clockViewModel, type)) - addTransition(SmartspaceChangeBounds(clockViewModel, type)) - addTransition(ClockInChangeBounds(clockViewModel, type)) - addTransition(ClockOutChangeBounds(clockViewModel, type)) + if (config.type != Type.SmartspaceVisibility) { + addTransition(ClockFaceOutTransition(config, clockViewModel)) + addTransition(ClockFaceInTransition(config, clockViewModel)) + } + addTransition(SmartspaceMoveTransition(config, clockViewModel)) } - class ClockInTransition(viewModel: KeyguardClockViewModel, type: IntraBlueprintTransitionType) : - Visibility() { - init { - mode = MODE_IN - if (type != IntraBlueprintTransitionType.NoTransition) { - duration = CLOCK_IN_MILLIS - startDelay = CLOCK_IN_START_DELAY_MILLIS - interpolator = Interpolators.LINEAR_OUT_SLOW_IN - } else { - duration = 0 - startDelay = 0 - } + open class VisibilityBoundsTransition() : Transition() { + var captureSmartspace: Boolean = false - addTarget(sharedR.id.bc_smartspace_view) - addTarget(sharedR.id.date_smartspace_view) - addTarget(sharedR.id.weather_smartspace_view) - if (viewModel.useLargeClock) { - viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } } - } else { - addTarget(R.id.lockscreen_clock_view) - } + override fun captureEndValues(transition: TransitionValues) = captureValues(transition) + override fun captureStartValues(transition: TransitionValues) = captureValues(transition) + override fun getTransitionProperties(): Array<String> = TRANSITION_PROPERTIES + open fun mutateBounds( + view: View, + fromVis: Int, + toVis: Int, + fromBounds: Rect, + toBounds: Rect, + fromSSBounds: Rect?, + toSSBounds: Rect? + ) {} + + private fun captureValues(transition: TransitionValues) { + val view = transition.view + transition.values[PROP_VISIBILITY] = view.visibility + transition.values[PROP_ALPHA] = view.alpha + transition.values[PROP_BOUNDS] = Rect(view.left, view.top, view.right, view.bottom) + + if (!captureSmartspace) return + val ss = (view.parent as View).findViewById<View>(sharedR.id.bc_smartspace_view) + if (ss == null) return + transition.values[SMARTSPACE_BOUNDS] = Rect(ss.left, ss.top, ss.right, ss.bottom) } - override fun onAppear( - sceneRoot: ViewGroup?, - view: View, + override fun createAnimator( + sceenRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues? - ): Animator { - return ObjectAnimator.ofFloat(view, "alpha", 1f).also { - it.duration = duration - it.startDelay = startDelay - it.interpolator = interpolator - it.addUpdateListener { view.alpha = it.animatedValue as Float } - it.start() + ): Animator? { + if (startValues == null || endValues == null) return null + + val fromView = startValues.view + var fromVis = startValues.values[PROP_VISIBILITY] as Int + var fromIsVis = fromVis == View.VISIBLE + var fromAlpha = startValues.values[PROP_ALPHA] as Float + val fromBounds = startValues.values[PROP_BOUNDS] as Rect + val fromSSBounds = + if (captureSmartspace) startValues.values[SMARTSPACE_BOUNDS] as Rect else null + + val toView = endValues.view + val toVis = endValues.values[PROP_VISIBILITY] as Int + val toBounds = endValues.values[PROP_BOUNDS] as Rect + val toSSBounds = + if (captureSmartspace) endValues.values[SMARTSPACE_BOUNDS] as Rect else null + val toIsVis = toVis == View.VISIBLE + val toAlpha = if (toIsVis) 1f else 0f + + // Align starting visibility and alpha + if (!fromIsVis) fromAlpha = 0f + else if (fromAlpha <= 0f) { + fromIsVis = false + fromVis = View.INVISIBLE } - } - } - class ClockOutTransition( - viewModel: KeyguardClockViewModel, - type: IntraBlueprintTransitionType - ) : Visibility() { - init { - mode = MODE_OUT - if (type != IntraBlueprintTransitionType.NoTransition) { - duration = CLOCK_OUT_MILLIS - interpolator = CLOCK_OUT_INTERPOLATOR - } else { - duration = 0 + mutateBounds(toView, fromVis, toVis, fromBounds, toBounds, fromSSBounds, toSSBounds) + if (fromIsVis == toIsVis && fromBounds.equals(toBounds)) { + if (DEBUG) { + Log.w( + TAG, + "Skipping no-op transition: $toView; " + + "vis: $fromVis -> $toVis; " + + "alpha: $fromAlpha -> $toAlpha; " + + "bounds: $fromBounds -> $toBounds; " + ) + } + return null } - addTarget(sharedR.id.bc_smartspace_view) - addTarget(sharedR.id.date_smartspace_view) - addTarget(sharedR.id.weather_smartspace_view) - if (viewModel.useLargeClock) { - addTarget(R.id.lockscreen_clock_view) - } else { - viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } } + val sendToBack = fromIsVis && !toIsVis + fun lerp(start: Int, end: Int, fract: Float): Int = + (start * (1f - fract) + end * fract).toInt() + fun computeBounds(fract: Float): Rect = + Rect( + lerp(fromBounds.left, toBounds.left, fract), + lerp(fromBounds.top, toBounds.top, fract), + lerp(fromBounds.right, toBounds.right, fract), + lerp(fromBounds.bottom, toBounds.bottom, fract) + ) + + fun assignAnimValues(src: String, alpha: Float, fract: Float, vis: Int? = null) { + val bounds = computeBounds(fract) + if (DEBUG) Log.i(TAG, "$src: $toView; alpha=$alpha; vis=$vis; bounds=$bounds;") + toView.setVisibility(vis ?: View.VISIBLE) + toView.setAlpha(alpha) + toView.setRect(bounds) } - } - override fun onDisappear( - sceneRoot: ViewGroup?, - view: View, - startValues: TransitionValues?, - endValues: TransitionValues? - ): Animator { - return ObjectAnimator.ofFloat(view, "alpha", 0f).also { - it.duration = duration - it.interpolator = interpolator - it.addUpdateListener { view.alpha = it.animatedValue as Float } - it.start() + if (DEBUG) { + Log.i( + TAG, + "transitioning: $toView; " + + "vis: $fromVis -> $toVis; " + + "alpha: $fromAlpha -> $toAlpha; " + + "bounds: $fromBounds -> $toBounds; " + ) + } + + return ValueAnimator.ofFloat(fromAlpha, toAlpha).also { anim -> + // We enforce the animation parameters on the target view every frame using a + // predraw listener. This is suboptimal but prevents issues with layout passes + // overwriting the animation for individual frames. + val predrawCallback = OnPreDrawListener { + assignAnimValues("predraw", anim.animatedValue as Float, anim.animatedFraction) + return@OnPreDrawListener true + } + + anim.duration = duration + anim.startDelay = startDelay + anim.interpolator = interpolator + anim.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(anim: Animator) { + assignAnimValues("start", fromAlpha, 0f) + } + + override fun onAnimationEnd(anim: Animator) { + assignAnimValues("end", toAlpha, 1f, toVis) + if (sendToBack) toView.translationZ = 0f + toView.viewTreeObserver.removeOnPreDrawListener(predrawCallback) + } + } + ) + toView.viewTreeObserver.addOnPreDrawListener(predrawCallback) } } + + companion object { + private const val PROP_VISIBILITY = "ClockSizeTransition:Visibility" + private const val PROP_ALPHA = "ClockSizeTransition:Alpha" + private const val PROP_BOUNDS = "ClockSizeTransition:Bounds" + private const val SMARTSPACE_BOUNDS = "ClockSizeTransition:SSBounds" + private val TRANSITION_PROPERTIES = + arrayOf(PROP_VISIBILITY, PROP_ALPHA, PROP_BOUNDS, SMARTSPACE_BOUNDS) + + private val DEBUG = true + private val TAG = ClockFaceInTransition::class.simpleName!! + } } - class ClockInChangeBounds( - viewModel: KeyguardClockViewModel, - type: IntraBlueprintTransitionType - ) : ChangeBounds() { + class ClockFaceInTransition( + config: IntraBlueprintTransition.Config, + val viewModel: KeyguardClockViewModel, + ) : VisibilityBoundsTransition() { init { - if (type != IntraBlueprintTransitionType.NoTransition) { - duration = CLOCK_IN_MILLIS - startDelay = CLOCK_IN_START_DELAY_MILLIS - interpolator = CLOCK_IN_INTERPOLATOR - } else { - duration = 0 - startDelay = 0 - } + duration = CLOCK_IN_MILLIS + startDelay = CLOCK_IN_START_DELAY_MILLIS + interpolator = CLOCK_IN_INTERPOLATOR + captureSmartspace = !viewModel.useLargeClock if (viewModel.useLargeClock) { viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } } @@ -146,46 +210,115 @@ class ClockSizeTransition( addTarget(R.id.lockscreen_clock_view) } } - } - class ClockOutChangeBounds( - viewModel: KeyguardClockViewModel, - type: IntraBlueprintTransitionType - ) : ChangeBounds() { - init { - if (type != IntraBlueprintTransitionType.NoTransition) { - duration = CLOCK_OUT_MILLIS - interpolator = CLOCK_OUT_INTERPOLATOR + override fun mutateBounds( + view: View, + fromVis: Int, + toVis: Int, + fromBounds: Rect, + toBounds: Rect, + fromSSBounds: Rect?, + toSSBounds: Rect? + ) { + fromBounds.left = toBounds.left + fromBounds.right = toBounds.right + if (viewModel.useLargeClock) { + // Large clock shouldn't move + fromBounds.top = toBounds.top + fromBounds.bottom = toBounds.bottom + } else if (toSSBounds != null && fromSSBounds != null) { + // Instead of moving the small clock the full distance, we compute the distance + // smartspace will move. We then scale this to match the duration of this animation + // so that the small clock moves at the same speed as smartspace. + val ssTranslation = + abs((toSSBounds.top - fromSSBounds.top) * SMALL_CLOCK_IN_MOVE_SCALE).toInt() + fromBounds.top = toBounds.top - ssTranslation + fromBounds.bottom = toBounds.bottom - ssTranslation } else { - duration = 0 + Log.e(TAG, "mutateBounds: smallClock received no smartspace bounds") } + } + + companion object { + const val CLOCK_IN_MILLIS = 167L + const val CLOCK_IN_START_DELAY_MILLIS = 133L + val CLOCK_IN_INTERPOLATOR = Interpolators.LINEAR_OUT_SLOW_IN + const val SMALL_CLOCK_IN_MOVE_SCALE = + CLOCK_IN_MILLIS / SmartspaceMoveTransition.STATUS_AREA_MOVE_DOWN_MILLIS.toFloat() + private val TAG = ClockFaceInTransition::class.simpleName!! + } + } + + class ClockFaceOutTransition( + config: IntraBlueprintTransition.Config, + val viewModel: KeyguardClockViewModel, + ) : VisibilityBoundsTransition() { + init { + duration = CLOCK_OUT_MILLIS + interpolator = CLOCK_OUT_INTERPOLATOR + captureSmartspace = viewModel.useLargeClock + if (viewModel.useLargeClock) { addTarget(R.id.lockscreen_clock_view) } else { viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } } } } + + override fun mutateBounds( + view: View, + fromVis: Int, + toVis: Int, + fromBounds: Rect, + toBounds: Rect, + fromSSBounds: Rect?, + toSSBounds: Rect? + ) { + toBounds.left = fromBounds.left + toBounds.right = fromBounds.right + if (!viewModel.useLargeClock) { + // Large clock shouldn't move + toBounds.top = fromBounds.top + toBounds.bottom = fromBounds.bottom + } else if (toSSBounds != null && fromSSBounds != null) { + // Instead of moving the small clock the full distance, we compute the distance + // smartspace will move. We then scale this to match the duration of this animation + // so that the small clock moves at the same speed as smartspace. + val ssTranslation = + abs((toSSBounds.top - fromSSBounds.top) * SMALL_CLOCK_OUT_MOVE_SCALE).toInt() + toBounds.top = fromBounds.top - ssTranslation + toBounds.bottom = fromBounds.bottom - ssTranslation + } else { + Log.w(TAG, "mutateBounds: smallClock received no smartspace bounds") + } + } + + companion object { + const val CLOCK_OUT_MILLIS = 133L + val CLOCK_OUT_INTERPOLATOR = Interpolators.LINEAR + const val SMALL_CLOCK_OUT_MOVE_SCALE = + CLOCK_OUT_MILLIS / SmartspaceMoveTransition.STATUS_AREA_MOVE_UP_MILLIS.toFloat() + private val TAG = ClockFaceOutTransition::class.simpleName!! + } } - class SmartspaceChangeBounds( + // TODO: Might need a mechanism to update this one while in-progress + class SmartspaceMoveTransition( + val config: IntraBlueprintTransition.Config, viewModel: KeyguardClockViewModel, - val type: IntraBlueprintTransitionType = IntraBlueprintTransitionType.DefaultTransition - ) : ChangeBounds() { + ) : VisibilityBoundsTransition() { init { - if (type != IntraBlueprintTransitionType.NoTransition) { - duration = - if (viewModel.useLargeClock) { - STATUS_AREA_MOVE_UP_MILLIS - } else { - STATUS_AREA_MOVE_DOWN_MILLIS - } - interpolator = Interpolators.EMPHASIZED - } else { - duration = 0 - } + duration = + if (viewModel.useLargeClock) STATUS_AREA_MOVE_UP_MILLIS + else STATUS_AREA_MOVE_DOWN_MILLIS + interpolator = Interpolators.EMPHASIZED addTarget(sharedR.id.date_smartspace_view) addTarget(sharedR.id.weather_smartspace_view) addTarget(sharedR.id.bc_smartspace_view) + + // Notifications normally and media on split shade needs to be moved + addTarget(R.id.aod_notification_icon_container) + addTarget(R.id.status_view_media_container) } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt index c35dad71a584..60ab40c0a16b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt @@ -24,12 +24,15 @@ import android.view.ViewGroup import com.android.app.animation.Interpolators import com.android.systemui.plugins.clocks.ClockController -class DefaultClockSteppingTransition(private val clock: ClockController) : Transition() { +class DefaultClockSteppingTransition( + private val clock: ClockController, +) : Transition() { init { interpolator = Interpolators.LINEAR duration = KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION_MS addTarget(clock.largeClock.view) } + private fun captureValues(transitionValues: TransitionValues) { transitionValues.values[PROP_BOUNDS_LEFT] = transitionValues.view.left val locationInWindowTmp = IntArray(2) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerDependencies.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerDependencies.kt index 6846886875c9..065c20ac52e8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerDependencies.kt @@ -36,4 +36,5 @@ constructor( val udfpsIconViewModel: AlternateBouncerUdfpsIconViewModel, val udfpsAccessibilityOverlayViewModel: Lazy<AlternateBouncerUdfpsAccessibilityOverlayViewModel>, + val messageAreaViewModel: AlternateBouncerMessageAreaViewModel, ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerMessageAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerMessageAreaViewModel.kt new file mode 100644 index 000000000000..49c64bdcdd23 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerMessageAreaViewModel.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor +import com.android.systemui.deviceentry.domain.interactor.BiometricMessageInteractor +import com.android.systemui.deviceentry.shared.model.BiometricMessage +import com.android.systemui.deviceentry.shared.model.FaceMessage +import com.android.systemui.deviceentry.shared.model.FaceTimeoutMessage +import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage +import com.android.systemui.deviceentry.shared.model.FingerprintMessage +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.merge + +/** View model for the alternate bouncer message area. */ +@ExperimentalCoroutinesApi +class AlternateBouncerMessageAreaViewModel +@Inject +constructor( + biometricMessageInteractor: BiometricMessageInteractor, + alternateBouncerInteractor: AlternateBouncerInteractor, +) { + + private val faceHelp: Flow<FaceMessage> = + biometricMessageInteractor.faceMessage.filterNot { faceMessage -> + faceMessage !is FaceTimeoutMessage + } + private val fingerprintMessages: Flow<FingerprintMessage> = + biometricMessageInteractor.fingerprintMessage.filterNot { fingerprintMessage -> + // On lockout, the device will show the bouncer. Let's not show the message + // before the transition or else it'll look flickery. + fingerprintMessage is FingerprintLockoutMessage + } + + val message: Flow<BiometricMessage?> = + alternateBouncerInteractor.isVisible.flatMapLatest { isVisible -> + if (isVisible) { + merge( + faceHelp, + fingerprintMessages, + ) + } else { + flowOf(null) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt index a3d54532411c..c9cf0c31a8fd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt @@ -34,9 +34,11 @@ import com.android.systemui.util.kotlin.sample import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine 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 @@ -175,19 +177,33 @@ constructor( flowOf(BurnInOffsets(x = 0, y = 0, progress = 0f)) } } + + private val isUnlocked: Flow<Boolean> = + deviceEntryInteractor.isUnlocked.flatMapLatest { isUnlocked -> + if (!isUnlocked) { + flowOf(false) + } else { + flow { + // delay in case device ends up transitioning away from the lock screen; + // we don't want to animate to the unlocked icon and just let the + // icon fade with the transition to GONE + delay(UNLOCKED_DELAY_MS) + emit(true) + } + } + } + val iconType: Flow<DeviceEntryIconView.IconType> = combine( deviceEntryUdfpsInteractor.isListeningForUdfps, - deviceEntryInteractor.isUnlocked, + keyguardInteractor.isKeyguardDismissible, ) { isListeningForUdfps, isUnlocked -> - if (isUnlocked) { + if (isListeningForUdfps) { + DeviceEntryIconView.IconType.FINGERPRINT + } else if (isUnlocked) { DeviceEntryIconView.IconType.UNLOCK } else { - if (isListeningForUdfps) { - DeviceEntryIconView.IconType.FINGERPRINT - } else { - DeviceEntryIconView.IconType.LOCK - } + DeviceEntryIconView.IconType.LOCK } } val isLongPressEnabled: Flow<Boolean> = @@ -229,6 +245,10 @@ constructor( DeviceEntryIconView.AccessibilityHintType.NONE } } + + companion object { + const val UNLOCKED_DELAY_MS = 50L + } } data class BurnInOffsets( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt index d22856b1bec2..edd331882896 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt @@ -23,8 +23,10 @@ import javax.inject.Inject class KeyguardBlueprintViewModel @Inject -constructor(keyguardBlueprintInteractor: KeyguardBlueprintInteractor) { +constructor( + keyguardBlueprintInteractor: KeyguardBlueprintInteractor, +) { var currentBluePrint: KeyguardBlueprint? = null val blueprint = keyguardBlueprintInteractor.blueprint - val blueprintWithTransition = keyguardBlueprintInteractor.blueprintWithTransition + val refreshTransition = keyguardBlueprintInteractor.refreshTransition } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludingAppDeviceEntryMessageViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludingAppDeviceEntryMessageViewModel.kt index 3a162d7f14a6..846bcbfd38d9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludingAppDeviceEntryMessageViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludingAppDeviceEntryMessageViewModel.kt @@ -18,8 +18,8 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.domain.interactor.BiometricMessage -import com.android.systemui.keyguard.domain.interactor.OccludingAppDeviceEntryInteractor +import com.android.systemui.deviceentry.domain.interactor.OccludingAppDeviceEntryInteractor +import com.android.systemui.deviceentry.shared.model.BiometricMessage import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt index 8a900ece2750..17454a97f5d2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt @@ -23,8 +23,8 @@ import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.scene.shared.model.Direction import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.scene.shared.model.UserAction +import com.android.systemui.scene.shared.model.UserActionResult import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import java.util.concurrent.atomic.AtomicBoolean @@ -45,11 +45,13 @@ constructor( val destinationScenes = qsSceneAdapter.isCustomizing.map { customizing -> if (customizing) { - mapOf<UserAction, SceneModel>(UserAction.Back to SceneModel(SceneKey.QuickSettings)) + mapOf<UserAction, UserActionResult>( + UserAction.Back to UserActionResult(SceneKey.QuickSettings) + ) } else { mapOf( - UserAction.Back to SceneModel(SceneKey.Shade), - UserAction.Swipe(Direction.UP) to SceneModel(SceneKey.Shade), + UserAction.Back to UserActionResult(SceneKey.Shade), + UserAction.Swipe(Direction.UP) to UserActionResult(SceneKey.Shade), ) } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt index 350fa38f2052..a3021946713f 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt @@ -21,8 +21,9 @@ package com.android.systemui.scene.data.repository import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneContainerConfig +import com.android.systemui.scene.shared.model.SceneDataSource import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.TransitionKey import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -41,9 +42,9 @@ class SceneContainerRepository constructor( @Application applicationScope: CoroutineScope, private val config: SceneContainerConfig, + private val dataSource: SceneDataSource, ) { - private val _desiredScene = MutableStateFlow(SceneModel(config.initialSceneKey)) - val desiredScene: StateFlow<SceneModel> = _desiredScene.asStateFlow() + val currentScene: StateFlow<SceneKey> = dataSource.currentScene private val _isVisible = MutableStateFlow(true) val isVisible: StateFlow<Boolean> = _isVisible.asStateFlow() @@ -69,16 +70,22 @@ constructor( return config.sceneKeys } - fun setDesiredScene(scene: SceneModel) { - check(allSceneKeys().contains(scene.key)) { + fun changeScene( + toScene: SceneKey, + transitionKey: TransitionKey? = null, + ) { + check(allSceneKeys().contains(toScene)) { """ - Cannot set the desired scene key to "${scene.key}". The configuration does not + Cannot set the desired scene key to "$toScene". The configuration does not contain a scene with that key. """ .trimIndent() } - _desiredScene.value = scene + dataSource.changeScene( + toScene = toScene, + transitionKey = transitionKey, + ) } /** Sets whether the container is visible. */ diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index b9e9fe7684e9..494c86c7b8c8 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -24,7 +24,8 @@ import com.android.systemui.scene.data.repository.SceneContainerRepository import com.android.systemui.scene.shared.logger.SceneLogger import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.TransitionKey +import com.android.systemui.util.kotlin.pairwiseBy import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -55,34 +56,25 @@ constructor( ) { /** - * The currently *desired* scene. + * The current scene. * - * **Important:** this value will _commonly be different_ from what is being rendered in the UI, - * by design. - * - * There are two intended sources for this value: - * 1. Programmatic requests to transition to another scene (calls to [changeScene]). - * 2. Reports from the UI about completing a transition to another scene (calls to - * [onSceneChanged]). - * - * Both the sources above cause the value of this flow to change; however, they cause mismatches - * in different ways. - * - * **Updates from programmatic transitions** - * - * When an external bit of code asks the framework to switch to another scene, the value here - * will update immediately. Downstream, the UI will detect this change and initiate the - * transition animation. As the transition animation progresses, a threshold will be reached, at - * which point the UI and the state here will match each other. - * - * **Updates from the UI** - * - * When the user interacts with the UI, the UI runs a transition animation that tracks the user - * pointer (for example, the user's finger). During this time, the state value here and what the - * UI shows will likely not match. Once/if a threshold is met, the UI reports it and commits the - * change, making the value here match the UI again. + * Note that during a transition between scenes, more than one scene might be rendered but only + * one is considered the committed/current scene. */ - val desiredScene: StateFlow<SceneModel> = repository.desiredScene + val currentScene: StateFlow<SceneKey> = + repository.currentScene + .pairwiseBy(initialValue = repository.currentScene.value) { from, to -> + logger.logSceneChangeCommitted( + from = from, + to = to, + ) + to + } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = repository.currentScene.value, + ) /** * The current state of the transition. @@ -146,14 +138,32 @@ constructor( /** * Requests a scene change to the given scene. * - * The change is animated. Therefore, while the value in [desiredScene] will update immediately, - * it will be some time before the UI will switch to the desired scene. The scene change - * requested is remembered here but served by the UI layer, which will start a transition - * animation. Once enough of the transition has occurred, the system will come into agreement - * between the [desiredScene] and the UI. + * The change is animated. Therefore, it will be some time before the UI will switch to the + * desired scene. Once enough of the transition has occurred, the [currentScene] will become + * [toScene] (unless the transition is canceled by user action or another call to this method). */ - fun changeScene(scene: SceneModel, loggingReason: String) { - updateDesiredScene(scene, loggingReason, logger::logSceneChangeRequested) + fun changeScene( + toScene: SceneKey, + loggingReason: String, + transitionKey: TransitionKey? = null, + ) { + check(toScene != SceneKey.Gone || deviceUnlockedInteractor.isDeviceUnlocked.value) { + "Cannot change to the Gone scene while the device is locked. Logging reason for scene" + + " change was: $loggingReason" + } + + val currentSceneKey = currentScene.value + if (currentSceneKey == toScene) { + return + } + + logger.logSceneChangeRequested( + from = currentSceneKey, + to = toScene, + reason = loggingReason, + ) + + repository.changeScene(toScene, transitionKey) } /** Sets the visibility of the container. */ @@ -184,39 +194,4 @@ constructor( fun onUserInput() { powerInteractor.onUserTouch() } - - /** - * Notifies that the UI has transitioned sufficiently to the given scene. - * - * *Not intended for external use!* - * - * Once a transition between one scene and another passes a threshold, the UI invokes this - * method to report it, updating the value in [desiredScene] to match what the UI shows. - */ - fun onSceneChanged(scene: SceneModel, loggingReason: String) { - updateDesiredScene(scene, loggingReason, logger::logSceneChangeCommitted) - } - - private fun updateDesiredScene( - scene: SceneModel, - loggingReason: String, - log: (from: SceneKey, to: SceneKey, loggingReason: String) -> Unit, - ) { - check(scene.key != SceneKey.Gone || deviceUnlockedInteractor.isDeviceUnlocked.value) { - "Cannot change to the Gone scene while the device is locked. Logging reason for scene" + - " change was: $loggingReason" - } - - val currentSceneKey = desiredScene.value.key - if (currentSceneKey == scene.key) { - return - } - - log( - /* from= */ currentSceneKey, - /* to= */ scene.key, - /* loggingReason= */ loggingReason, - ) - repository.setDesiredScene(scene) - } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index dcd87c0fe845..605a5d9b6772 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -40,7 +40,6 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.scene.shared.logger.SceneLogger import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.notification.stack.shared.flexiNotifsEnabled import com.android.systemui.statusbar.phone.CentralSurfaces @@ -164,9 +163,9 @@ constructor( applicationScope.launch { // TODO (b/308001302): Move this to a bouncer specific interactor. bouncerInteractor.onImeHiddenByUser.collectLatest { - if (sceneInteractor.desiredScene.value.key == SceneKey.Bouncer) { + if (sceneInteractor.currentScene.value == SceneKey.Bouncer) { sceneInteractor.changeScene( - scene = SceneModel(SceneKey.Lockscreen), + toScene = SceneKey.Lockscreen, loggingReason = "IME hidden", ) } @@ -353,8 +352,8 @@ constructor( } applicationScope.launch { - sceneInteractor.desiredScene - .map { it.key == SceneKey.Bouncer } + sceneInteractor.currentScene + .map { it == SceneKey.Bouncer } .distinctUntilChanged() .collect { switchedToBouncerScene -> if (switchedToBouncerScene) { @@ -422,7 +421,7 @@ constructor( private fun switchToScene(targetSceneKey: SceneKey, loggingReason: String) { sceneInteractor.changeScene( - scene = SceneModel(targetSceneKey), + toScene = targetSceneKey, loggingReason = loggingReason, ) } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt index c2c2e04990c2..d59fcff34796 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt @@ -62,7 +62,6 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: fun logSceneChangeCommitted( from: SceneKey, to: SceneKey, - reason: String, ) { logBuffer.log( tag = TAG, @@ -70,9 +69,8 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: messageInitializer = { str1 = from.toString() str2 = to.toString() - str3 = reason }, - messagePrinter = { "Scene change committed: $str1 → $str2, reason: $str3" }, + messagePrinter = { "Scene change committed: $str1 → $str2" }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scene.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scene.kt index 2e45353634fe..05056c133ca3 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scene.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scene.kt @@ -32,7 +32,7 @@ interface Scene { val key: SceneKey /** - * The mapping between [UserAction] and destination [SceneModel]s. + * The mapping between [UserAction] and destination [UserActionResult]s. * * When the scene framework detects a user action, if the current scene has a map entry for that * user action, the framework starts a transition to the scene in the map. @@ -40,7 +40,7 @@ interface Scene { * Once the [Scene] becomes the current one, the scene framework will read this property and set * up a collector to watch for new mapping values. If every map entry provided by the scene, the * framework will set up user input handling for its [UserAction] and, if such a user action is - * detected, initiate a transition to the specified [SceneModel]. + * detected, initiate a transition to the specified [UserActionResult]. * * Note that reading from this method does _not_ mean that any user action has occurred. * Instead, the property is read before any user action/gesture is detected so that the @@ -51,7 +51,7 @@ interface Scene { * type is not currently active in the scene and should be ignored by the framework, while the * current scene is this one. */ - val destinationScenes: StateFlow<Map<UserAction, SceneModel>> + val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> } /** Enumerates all scene framework supported user actions. */ diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt new file mode 100644 index 000000000000..926878c1870e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt @@ -0,0 +1,34 @@ +/* + * 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 + +/** + * Defines all known named transitions. + * + * These are the subset of transitions that can be referenced by key when asking for a scene change. + */ +object TransitionKeys { + + /** Reference to a scene transition that can collapse the shade scene instantly. */ + val CollapseShadeInstantly = TransitionKey("CollapseShadeInstantly") + + /** + * Reference to a scene transition that can collapse the shade scene slightly faster than a + * normal collapse would. + */ + val SlightlyFasterShadeCollapse = TransitionKey("SlightlyFasterShadeCollapse") +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt index c88a04c24044..67dc0cc6e03b 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt @@ -7,6 +7,7 @@ import android.view.WindowInsets import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneContainerConfig +import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import kotlinx.coroutines.flow.MutableStateFlow @@ -33,6 +34,7 @@ class SceneWindowRootView( flags: SceneContainerFlags, scenes: Set<Scene>, layoutInsetController: LayoutInsetsController, + sceneDataSourceDelegator: SceneDataSourceDelegator, ) { this.viewModel = viewModel setLayoutInsetsController(layoutInsetController) @@ -46,7 +48,8 @@ class SceneWindowRootView( scenes = scenes, onVisibilityChangedInternal = { isVisible -> super.setVisibility(if (isVisible) View.VISIBLE else View.INVISIBLE) - } + }, + dataSourceDelegator = sceneDataSourceDelegator, ) } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt index 2b978b2375d9..45b6f65d5d67 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt @@ -34,6 +34,7 @@ import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneContainerConfig +import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.statusbar.notification.stack.shared.flexiNotifsEnabled @@ -54,6 +55,7 @@ object SceneWindowRootViewBinder { flags: SceneContainerFlags, scenes: Set<Scene>, onVisibilityChangedInternal: (isVisible: Boolean) -> Unit, + dataSourceDelegator: SceneDataSourceDelegator, ) { val unsortedSceneByKey: Map<SceneKey, Scene> = scenes.associateBy { scene -> scene.key } val sortedSceneByKey: Map<SceneKey, Scene> = buildMap { @@ -90,6 +92,7 @@ object SceneWindowRootViewBinder { viewModel = viewModel, windowInsets = windowInsets, sceneByKey = sortedSceneByKey, + dataSourceDelegator = dataSourceDelegator, ) .also { it.id = R.id.scene_container_root_composable } ) diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt index 24316601830a..5d290cefcd55 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt @@ -22,7 +22,6 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -44,19 +43,11 @@ constructor( val allSceneKeys: List<SceneKey> = sceneInteractor.allSceneKeys() /** The scene that should be rendered. */ - val currentScene: StateFlow<SceneModel> = sceneInteractor.desiredScene + val currentScene: StateFlow<SceneKey> = sceneInteractor.currentScene /** Whether the container is visible. */ val isVisible: StateFlow<Boolean> = sceneInteractor.isVisible - /** Notifies that the UI has transitioned sufficiently to the given scene. */ - fun onSceneChanged(scene: SceneModel) { - sceneInteractor.onSceneChanged( - scene = scene, - loggingReason = SCENE_TRANSITION_LOGGING_REASON, - ) - } - /** * Binds the given flow so the system remembers it. * diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt index 4e8b4039cc79..7cb3be7f159d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt @@ -26,7 +26,8 @@ import com.android.systemui.log.LogBuffer import com.android.systemui.log.dagger.ShadeTouchLog import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.TransitionKeys.CollapseShadeInstantly +import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse import com.android.systemui.shade.ShadeController.ShadeVisibilityListener import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.CommandQueue @@ -97,8 +98,9 @@ constructor( override fun instantCollapseShade() { // TODO(b/315921512) add support for instant transition sceneInteractor.changeScene( - SceneModel(getCollapseDestinationScene(), "instant"), - "hide shade" + getCollapseDestinationScene(), + "hide shade", + CollapseShadeInstantly, ) } @@ -119,10 +121,7 @@ constructor( // release focus immediately to kick off focus change transition notificationShadeWindowController.setNotificationShadeFocusable(false) notificationStackScrollLayout.cancelExpandHelper() - sceneInteractor.changeScene( - SceneModel(SceneKey.Shade, null), - "ShadeController.animateExpandShade" - ) + sceneInteractor.changeScene(SceneKey.Shade, "ShadeController.animateExpandShade") if (delayed) { scope.launch { delay(125) @@ -136,8 +135,9 @@ constructor( private fun animateCollapseShadeInternal() { sceneInteractor.changeScene( - SceneModel(getCollapseDestinationScene(), "ShadeController.animateCollapseShade"), - "ShadeController.animateCollapseShade" + getCollapseDestinationScene(), + "ShadeController.animateCollapseShade", + SlightlyFasterShadeCollapse, ) } @@ -183,17 +183,11 @@ constructor( } override fun expandToNotifications() { - sceneInteractor.changeScene( - SceneModel(SceneKey.Shade, null), - "ShadeController.animateExpandShade" - ) + sceneInteractor.changeScene(SceneKey.Shade, "ShadeController.animateExpandShade") } override fun expandToQs() { - sceneInteractor.changeScene( - SceneModel(SceneKey.QuickSettings, null), - "ShadeController.animateExpandQs" - ) + sceneInteractor.changeScene(SceneKey.QuickSettings, "ShadeController.animateExpandQs") } override fun setVisibilityListener(listener: ShadeVisibilityListener) { @@ -243,7 +237,7 @@ constructor( } override fun isExpandedVisible(): Boolean { - return sceneInteractor.desiredScene.value.key != SceneKey.Gone + return sceneInteractor.currentScene.value != SceneKey.Gone } override fun onStatusBarTouch(event: MotionEvent) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt index f40be4bbb678..2cb9f9acca26 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt @@ -35,6 +35,7 @@ import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneContainerConfig +import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.ui.view.SceneWindowRootView import com.android.systemui.scene.ui.view.WindowRootView import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel @@ -72,6 +73,7 @@ abstract class ShadeViewProviderModule { flagsProvider: Provider<SceneContainerFlags>, scenesProvider: Provider<Set<@JvmSuppressWildcards Scene>>, layoutInsetController: NotificationInsetsController, + sceneDataSourceDelegator: Provider<SceneDataSourceDelegator>, ): WindowRootView { return if (sceneContainerFlags.isEnabled()) { val sceneWindowRootView = @@ -84,6 +86,7 @@ abstract class ShadeViewProviderModule { flags = flagsProvider.get(), scenes = scenesProvider.get(), layoutInsetController = layoutInsetController, + sceneDataSourceDelegator = sceneDataSourceDelegator.get(), ) sceneWindowRootView } else { diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt index 9bbe1bd28fee..a2e25983e68f 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt @@ -19,7 +19,6 @@ package com.android.systemui.shade.domain.interactor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -44,7 +43,7 @@ constructor( } else { SceneKey.Shade } - sceneInteractor.changeScene(SceneModel(key), "animateCollapseQs") + sceneInteractor.changeScene(key, "animateCollapseQs") } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 04d9b0cbd428..14230ba43f59 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -87,6 +87,7 @@ import com.android.settingslib.Utils; import com.android.settingslib.fuelgauge.BatteryStatus; import com.android.systemui.biometrics.AuthController; import com.android.systemui.biometrics.FaceHelpMessageDeferral; +import com.android.systemui.biometrics.FaceHelpMessageDeferralFactory; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -270,7 +271,7 @@ public class KeyguardIndicationController { ScreenLifecycle screenLifecycle, KeyguardBypassController keyguardBypassController, AccessibilityManager accessibilityManager, - FaceHelpMessageDeferral faceHelpMessageDeferral, + FaceHelpMessageDeferralFactory faceHelpMessageDeferral, KeyguardLogger keyguardLogger, AlternateBouncerInteractor alternateBouncerInteractor, AlarmManager alarmManager, @@ -308,7 +309,7 @@ public class KeyguardIndicationController { mIndicationHelper = indicationHelper; mKeyguardInteractor = keyguardInteractor; - mFaceAcquiredMessageDeferral = faceHelpMessageDeferral; + mFaceAcquiredMessageDeferral = faceHelpMessageDeferral.create(); mCoExFaceAcquisitionMsgIdsToShow = new HashSet<>(); int[] msgIds = context.getResources().getIntArray( com.android.systemui.res.R.array.config_face_help_msgs_when_fingerprint_enrolled); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 4d373353d7a3..0e0f15237185 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -766,7 +766,6 @@ public class NotificationShelf extends ActivatableNotificationView { } } else if (viewEnd >= shelfClipStart - && view.isInShelf() && (mAmbientState.isShadeExpanded() || (!view.isPinned() && !view.isHeadsUpAnimatingAway()))) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationContentDescription.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationContentDescription.kt index e7012ea51caf..17fc5c60f74f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationContentDescription.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationContentDescription.kt @@ -20,35 +20,14 @@ package com.android.systemui.statusbar.notification import android.app.Notification import android.content.Context -import android.content.pm.ApplicationInfo import android.text.TextUtils -import android.util.Log import androidx.annotation.MainThread import com.android.systemui.res.R -/** - * Returns accessibility content description for a given notification. - * - * NOTE: This is a relatively slow call. - */ +/** Returns accessibility content description for a given notification. */ @MainThread fun contentDescForNotification(c: Context, n: Notification?): CharSequence { - var appName = "" - try { - val builder = Notification.Builder.recoverBuilder(c, n) - appName = builder.loadHeaderAppName() - } catch (e: RuntimeException) { - Log.e("ContentDescription", "Unable to recover builder", e) - // Trying to get the app name from the app info instead. - val appInfo = - n?.extras?.getParcelable( - Notification.EXTRA_BUILDER_APPLICATION_INFO, - ApplicationInfo::class.java - ) - if (appInfo != null) { - appName = appInfo.loadLabel(c.packageManager).toString() - } - } + val appName = n?.loadHeaderAppName(c) ?: "" val title = n?.extras?.getCharSequence(Notification.EXTRA_TITLE) val text = n?.extras?.getCharSequence(Notification.EXTRA_TEXT) val ticker = n?.tickerText diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt index f096dd6abb0e..6e4b32761a7b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt @@ -142,7 +142,7 @@ constructor( private fun getAllNotificationsOnMainThread() = runBlocking(mainDispatcher) { - traceSection("NML#getNotifications") { notificationPipeline.allNotifs } + traceSection("NML#getNotifications") { notificationPipeline.allNotifs.toList() } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java index c4d266ed2f17..ec8e5d730c36 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java @@ -720,6 +720,9 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro mInShelf = inShelf; } + /** + * @return true if the view is currently fully in the notification shelf. + */ public boolean isInShelf() { return mInShelf; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 15fde0ed49ee..634de7a17ef7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -826,16 +826,11 @@ public class StackScrollAlgorithm { } } if (row.isPinned()) { - if (NotificationsImprovedHunAnimation.isEnabled()) { - // Make sure row yTranslation is at the HUN yTranslation, - // which accounts for AmbientState.stackTopMargin in split-shade. - childState.setYTranslation(headsUpTranslation); - } else { - // Make sure row yTranslation is at maximum the HUN yTranslation, - // which accounts for AmbientState.stackTopMargin in split-shade. - childState.setYTranslation( - Math.max(childState.getYTranslation(), headsUpTranslation)); - } + // Make sure row yTranslation is at at least the HUN yTranslation, + // which accounts for AmbientState.stackTopMargin in split-shade. + // Once we start opening the shade, we keep the previously calculated translation. + childState.setYTranslation( + Math.max(childState.getYTranslation(), headsUpTranslation)); childState.height = Math.max(row.getIntrinsicHeight(), childState.height); childState.hidden = false; ExpandableViewState topState = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java index 433e5c77e128..ab62ed65716e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java @@ -470,15 +470,8 @@ public class StackStateAnimator { mHeadsUpAppearChildren.add(changingView); mTmpState.copyFrom(changingView.getViewState()); - if (event.headsUpFromBottom) { - // start from the bottom of the screen - mTmpState.setYTranslation( - mHeadsUpAppearHeightBottom + mHeadsUpAppearStartAboveScreen); - } else { - // start from the top of the screen - mTmpState.setYTranslation( - -mStackTopMargin - mHeadsUpAppearStartAboveScreen); - } + // translate the HUN in from the top, or the bottom of the screen + mTmpState.setYTranslation(getHeadsUpYTranslationStart(event.headsUpFromBottom)); // set the height and the initial position mTmpState.applyToView(changingView); mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y, @@ -522,12 +515,20 @@ public class StackStateAnimator { || event.animationType == ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK) { mHeadsUpDisappearChildren.add(changingView); Runnable endRunnable = null; + mTmpState.copyFrom(changingView.getViewState()); if (changingView.getParent() == null) { // This notification was actually removed, so we need to add it // transiently mHostLayout.addTransientView(changingView, 0); changingView.setTransientContainer(mHostLayout); - mTmpState.initFrom(changingView); + if (NotificationsImprovedHunAnimation.isEnabled()) { + // StackScrollAlgorithm cannot find this view because it has been removed + // from the NSSL. To correctly translate the view to the top or bottom of + // the screen (where it animated from), we need to update its translation. + mTmpState.setYTranslation( + getHeadsUpYTranslationStart(event.headsUpFromBottom) + ); + } endRunnable = changingView::removeFromTransientContainer; } @@ -575,16 +576,19 @@ public class StackStateAnimator { changingView.setInRemovalAnimation(true); }; } - if (NotificationsImprovedHunAnimation.isEnabled()) { - mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y, - Interpolators.FAST_OUT_SLOW_IN_REVERSE); - } long removeAnimationDelay = changingView.performRemoveAnimation( ANIMATION_DURATION_HEADS_UP_DISAPPEAR, 0, 0.0f, true /* isHeadsUpAppear */, startAnimation, postAnimation, getGlobalAnimationFinishedListener()); mAnimationProperties.delay += removeAnimationDelay; + if (NotificationsImprovedHunAnimation.isEnabled()) { + mAnimationProperties.duration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR; + mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y, + Interpolators.FAST_OUT_SLOW_IN_REVERSE); + mAnimationProperties.getAnimationFilter().animateY = true; + mTmpState.animateTo(changingView, mAnimationProperties); + } } else if (endRunnable != null) { endRunnable.run(); } @@ -595,6 +599,15 @@ public class StackStateAnimator { return needsCustomAnimation; } + private float getHeadsUpYTranslationStart(boolean headsUpFromBottom) { + if (headsUpFromBottom) { + // start from the bottom of the screen + return mHeadsUpAppearHeightBottom + mHeadsUpAppearStartAboveScreen; + } + // start from the top of the screen + return -mStackTopMargin - mHeadsUpAppearStartAboveScreen; + } + public void animateOverScrollToAmount(float targetAmount, final boolean onTop, final boolean isRubberbanded) { final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index 2206be5e614b..38b37183b97f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -20,6 +20,7 @@ import static android.app.StatusBarManager.DISABLE2_SYSTEM_ICONS; import static android.app.StatusBarManager.DISABLE_SYSTEM_INFO; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; +import static com.android.systemui.Flags.updateUserSwitcherBackground; import android.content.res.Configuration; import android.content.res.Resources; @@ -43,6 +44,7 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.keyguard.logging.KeyguardLogger; import com.android.systemui.battery.BatteryMeterViewController; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; @@ -122,6 +124,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat private final SecureSettings mSecureSettings; private final CommandQueue mCommandQueue; private final Executor mMainExecutor; + private final Executor mBackgroundExecutor; private final Object mLock = new Object(); private final KeyguardLogger mLogger; @@ -296,6 +299,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat SecureSettings secureSettings, CommandQueue commandQueue, @Main Executor mainExecutor, + @Background Executor backgroundExecutor, KeyguardLogger logger, NotificationMediaManager notificationMediaManager, StatusOverlayHoverListenerFactory statusOverlayHoverListenerFactory @@ -323,6 +327,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat mSecureSettings = secureSettings; mCommandQueue = commandQueue; mMainExecutor = mainExecutor; + mBackgroundExecutor = backgroundExecutor; mLogger = logger; mFirstBypassAttempt = mKeyguardBypassController.getBypassEnabled(); @@ -607,8 +612,18 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat * Updates visibility of the user switcher button based on {@link android.os.UserManager} state. */ private void updateUserSwitcher() { - mView.setUserSwitcherEnabled(mUserManager.isUserSwitcherEnabled(getResources().getBoolean( - R.bool.qs_show_user_switcher_for_single_user))); + if (updateUserSwitcherBackground()) { + mBackgroundExecutor.execute(() -> { + final boolean isUserSwitcherEnabled = mUserManager.isUserSwitcherEnabled( + getResources().getBoolean(R.bool.qs_show_user_switcher_for_single_user)); + mMainExecutor.execute(() -> { + mView.setUserSwitcherEnabled(isUserSwitcherEnabled); + }); + }); + } else { + mView.setUserSwitcherEnabled(mUserManager.isUserSwitcherEnabled( + getResources().getBoolean(R.bool.qs_show_user_switcher_for_single_user))); + } } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 7f7eb04f9970..9d70f4221e3a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -439,8 +439,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } mBypassController = bypassController; mNotificationContainer = notificationContainer; - mKeyguardMessageAreaController = mKeyguardMessageAreaFactory.create( - centralSurfaces.getKeyguardMessageArea()); + if (!DeviceEntryUdfpsRefactor.isEnabled()) { + mKeyguardMessageAreaController = mKeyguardMessageAreaFactory.create( + centralSurfaces.getKeyguardMessageArea()); + } + mCentralSurfacesRegistered = true; registerListeners(); @@ -863,6 +866,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb final boolean isShowingAlternateBouncer = mAlternateBouncerInteractor.isVisibleState(); if (mKeyguardMessageAreaController != null) { + DeviceEntryUdfpsRefactor.assertInLegacyMode(); mKeyguardMessageAreaController.setIsVisible(isShowingAlternateBouncer); mKeyguardMessageAreaController.setMessage(""); } @@ -1447,6 +1451,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb public void setKeyguardMessage(String message, ColorStateList colorState) { if (mAlternateBouncerInteractor.isVisibleState()) { if (mKeyguardMessageAreaController != null) { + DeviceEntryUdfpsRefactor.assertInLegacyMode(); mKeyguardMessageAreaController.setMessage(message); } } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java index fc2f6e958b32..c089092c9b86 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java @@ -35,6 +35,7 @@ import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfile; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; +import com.android.systemui.Flags; import com.android.systemui.bluetooth.BluetoothLogger; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; @@ -240,9 +241,21 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa @WorkerThread @Override public String getConnectedDeviceName() { - synchronized (mConnectedDevices) { - if (mConnectedDevices.size() == 1) { - return mConnectedDevices.get(0).getName(); + if (Flags.getConnectedDeviceNameUnsynchronized()) { + CachedBluetoothDevice connectedDevice = null; + // Calling the getName() API for CachedBluetoothDevice outside the synchronized block + // so that the main thread is not blocked. + synchronized (mConnectedDevices) { + if (mConnectedDevices.size() == 1) { + connectedDevice = mConnectedDevices.get(0); + } + } + return connectedDevice != null ? connectedDevice.getName() : null; + } else { + synchronized (mConnectedDevices) { + if (mConnectedDevices.size() == 1) { + return mConnectedDevices.get(0).getName(); + } } } return null; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/tv/OWNERS new file mode 100644 index 000000000000..43450c4418aa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/OWNERS @@ -0,0 +1,5 @@ +# Android TV Core Framework +rgl@google.com +valiiftime@google.com +galinap@google.com +robhor@google.com
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt index 1af5c46cd14b..67d6a7f5d735 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt @@ -16,11 +16,14 @@ package com.android.systemui.volume.dagger +import android.app.NotificationManager import android.content.Context import android.media.AudioManager import com.android.settingslib.media.data.repository.SpatializerRepository import com.android.settingslib.media.data.repository.SpatializerRepositoryImpl import com.android.settingslib.media.domain.interactor.SpatializerInteractor +import com.android.settingslib.statusbar.notification.data.repository.NotificationsSoundPolicyRepository +import com.android.settingslib.statusbar.notification.data.repository.NotificationsSoundPolicyRepositoryImpl import com.android.settingslib.volume.data.repository.AudioRepository import com.android.settingslib.volume.data.repository.AudioRepositoryImpl import com.android.settingslib.volume.domain.interactor.AudioModeInteractor @@ -68,5 +71,19 @@ interface AudioModule { @Provides fun provideSpatializerInetractor(repository: SpatializerRepository): SpatializerInteractor = SpatializerInteractor(repository) + + @Provides + fun provideNotificationsSoundPolicyRepository( + context: Context, + notificationManager: NotificationManager, + @Background coroutineContext: CoroutineContext, + @Application coroutineScope: CoroutineScope, + ): NotificationsSoundPolicyRepository = + NotificationsSoundPolicyRepositoryImpl( + context, + notificationManager, + coroutineScope, + coroutineContext, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt index 1c17fc34a34a..bb4a2c405b31 100644 --- a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt +++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt @@ -77,17 +77,17 @@ constructor( controller.setSuggestionCardIds(storeLocations.toSet()) } - private val binder: IWalletContextualLocationsService.Stub - = object : IWalletContextualLocationsService.Stub() { - override fun addWalletCardsUpdatedListener(listener: IWalletCardsUpdatedListener) { - addWalletCardsUpdatedListenerInternal(listener) - } - override fun onWalletContextualLocationsStateUpdated(storeLocations: List<String>) { - onWalletContextualLocationsStateUpdatedInternal(storeLocations) + private val binder: IWalletContextualLocationsService.Stub = + object : IWalletContextualLocationsService.Stub() { + override fun addWalletCardsUpdatedListener(listener: IWalletCardsUpdatedListener) { + addWalletCardsUpdatedListenerInternal(listener) + } + override fun onWalletContextualLocationsStateUpdated(storeLocations: List<String>) { + onWalletContextualLocationsStateUpdatedInternal(storeLocations) + } } - } companion object { private const val TAG = "WalletContextualLocationsService" } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java index 90878169c201..abc95bc23f87 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java @@ -82,18 +82,21 @@ public class DragToInteractAnimationControllerTest extends SysuiTestCase { mDragToInteractAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() { @Override - public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { + public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject<?> draggedObject) { } @Override public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, - float velX, float velY, boolean wasFlungOut) { + @NonNull MagnetizedObject<?> draggedObject, float velX, float velY, + boolean wasFlungOut) { } @Override - public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { + public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject<?> draggedObject) { } }); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java index 4a1bdbcc9b48..ce4db8febb6c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java @@ -31,10 +31,12 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -69,6 +71,7 @@ import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.SpringAnimation; import androidx.test.filters.SmallTest; +import com.android.internal.accessibility.dialog.AccessibilityTarget; import com.android.internal.messages.nano.SystemMessageProto; import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; @@ -116,6 +119,7 @@ public class MenuViewLayerTest extends SysuiTestCase { private String mLastAccessibilityButtonTargets; private String mLastEnabledAccessibilityServices; private WindowMetrics mWindowMetrics; + private MenuViewModel mMenuViewModel; private MenuView mMenuView; private MenuAnimationController mMenuAnimationController; @@ -148,15 +152,17 @@ public class MenuViewLayerTest extends SysuiTestCase { new WindowMetrics(mDisplayBounds, fakeDisplayInsets(), /* density = */ 0.0f)); doReturn(mWindowMetrics).when(mStubWindowManager).getCurrentWindowMetrics(); - MenuViewModel menuViewModel = new MenuViewModel( + mMenuViewModel = new MenuViewModel( mSpyContext, mStubAccessibilityManager, mSecureSettings); MenuViewAppearance menuViewAppearance = new MenuViewAppearance( mSpyContext, mStubWindowManager); mMenuView = spy( - new MenuView(mSpyContext, menuViewModel, menuViewAppearance, mSecureSettings)); + new MenuView(mSpyContext, mMenuViewModel, menuViewAppearance, mSecureSettings)); + // Ensure tests don't actually update metrics. + doNothing().when(mMenuView).incrementTexMetric(any(), anyInt()); mMenuViewLayer = spy(new MenuViewLayer(mSpyContext, mStubWindowManager, - mStubAccessibilityManager, menuViewModel, menuViewAppearance, mMenuView, + mStubAccessibilityManager, mMenuViewModel, menuViewAppearance, mMenuView, mFloatingMenu, mSecureSettings)); mMenuView = (MenuView) mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW); mMenuAnimationController = mMenuView.getMenuAnimationController(); @@ -382,6 +388,47 @@ public class MenuViewLayerTest extends SysuiTestCase { verify(mFloatingMenu).hide(); } + @Test + @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void onDismissAction_incrementsTexMetricDismiss() { + int uid1 = 1234, uid2 = 5678; + mMenuViewModel.onTargetFeaturesChanged( + List.of(new TestAccessibilityTarget(mSpyContext, uid1), + new TestAccessibilityTarget(mSpyContext, uid2))); + + mMenuViewLayer.dispatchAccessibilityAction(R.id.action_remove_menu); + + ArgumentCaptor<Integer> uidCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mMenuView, times(2)).incrementTexMetric(eq(MenuViewLayer.TEX_METRIC_DISMISS), + uidCaptor.capture()); + assertThat(uidCaptor.getAllValues()).containsExactly(uid1, uid2); + } + + @Test + @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void onEditAction_incrementsTexMetricEdit() { + int uid1 = 1234, uid2 = 5678; + mMenuViewModel.onTargetFeaturesChanged( + List.of(new TestAccessibilityTarget(mSpyContext, uid1), + new TestAccessibilityTarget(mSpyContext, uid2))); + + mMenuViewLayer.dispatchAccessibilityAction(R.id.action_edit); + + ArgumentCaptor<Integer> uidCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mMenuView, times(2)).incrementTexMetric(eq(MenuViewLayer.TEX_METRIC_EDIT), + uidCaptor.capture()); + assertThat(uidCaptor.getAllValues()).containsExactly(uid1, uid2); + } + + /** Simplified AccessibilityTarget for testing MenuViewLayer. */ + private static class TestAccessibilityTarget extends AccessibilityTarget { + TestAccessibilityTarget(Context context, int uid) { + // Set fields unused by tests to defaults that allow test compilation. + super(context, AccessibilityManager.ACCESSIBILITY_BUTTON, 0, false, + TEST_SELECT_TO_SPEAK_COMPONENT_NAME.flattenToString(), uid, null, null, null); + } + } + private void setupEnabledAccessibilityServiceList() { Settings.Secure.putString(mSpyContext.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, @@ -455,6 +502,6 @@ public class MenuViewLayerTest extends SysuiTestCase { View view = mock(View.class); when(view.getId()).thenReturn(id); magnetListener.onReleasedInTarget( - new MagnetizedObject.MagneticTarget(view, 200)); + new MagnetizedObject.MagneticTarget(view, 200), mock(MagnetizedObject.class)); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorTest.kt new file mode 100644 index 000000000000..a53f8d43f323 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorTest.kt @@ -0,0 +1,352 @@ +/* + * 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.deviceentry.domain.interactor + +import android.content.res.mainResources +import android.hardware.face.FaceManager +import android.hardware.fingerprint.FingerprintManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import com.android.systemui.biometrics.shared.model.SensorStrength +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.FaceTimeoutMessage +import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage +import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus +import com.android.systemui.keyguard.data.repository.biometricSettingsRepository +import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class BiometricMessageInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val underTest = kosmos.biometricMessageInteractor + + private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository + private val fingerprintAuthRepository = kosmos.deviceEntryFingerprintAuthRepository + private val faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository + private val biometricSettingsRepository = kosmos.biometricSettingsRepository + + @Test + fun fingerprintErrorMessage() = + testScope.runTest { + val fingerprintErrorMessage by collectLastValue(underTest.fingerprintMessage) + + // GIVEN fingerprint is allowed + biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + + // WHEN authentication status error is FINGERPRINT_ERROR_HW_UNAVAILABLE + fingerprintAuthRepository.setAuthenticationStatus( + ErrorFingerprintAuthenticationStatus( + msgId = FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE, + msg = "test" + ) + ) + + // THEN fingerprintErrorMessage is updated + assertThat(fingerprintErrorMessage?.message).isEqualTo("test") + } + + @Test + fun fingerprintLockoutErrorMessage() = + testScope.runTest { + val fingerprintErrorMessage by collectLastValue(underTest.fingerprintMessage) + + // GIVEN fingerprint is allowed + biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + + // WHEN authentication status error is FINGERPRINT_ERROR_HW_UNAVAILABLE + fingerprintAuthRepository.setAuthenticationStatus( + ErrorFingerprintAuthenticationStatus( + msgId = FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, + msg = "lockout" + ) + ) + + // THEN fingerprintError is updated + assertThat(fingerprintErrorMessage).isInstanceOf(FingerprintLockoutMessage::class.java) + assertThat(fingerprintErrorMessage?.message).isEqualTo("lockout") + } + + @Test + fun fingerprintErrorMessage_suppressedError() = + testScope.runTest { + val fingerprintErrorMessage by collectLastValue(underTest.fingerprintMessage) + + // WHEN authentication status error is FINGERPRINT_ERROR_HW_UNAVAILABLE + fingerprintAuthRepository.setAuthenticationStatus( + ErrorFingerprintAuthenticationStatus( + msgId = FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE, + msg = "test" + ) + ) + + // THEN fingerprintErrorMessage isn't update - it's still null + assertThat(fingerprintErrorMessage).isNull() + } + + @Test + fun fingerprintHelpMessage() = + testScope.runTest { + val fingerprintHelpMessage by collectLastValue(underTest.fingerprintMessage) + + // GIVEN fingerprint is allowed + biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + + // WHEN authentication status help is FINGERPRINT_ACQUIRED_IMAGER_DIRTY + fingerprintAuthRepository.setAuthenticationStatus( + HelpFingerprintAuthenticationStatus( + msgId = FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY, + msg = "test" + ) + ) + + // THEN fingerprintHelpMessage is updated + assertThat(fingerprintHelpMessage?.message).isEqualTo("test") + } + + @Test + fun fingerprintHelpMessage_primaryAuthRequired() = + testScope.runTest { + val fingerprintHelpMessage by collectLastValue(underTest.fingerprintMessage) + + // GIVEN fingerprint cannot currently be used + biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(false) + + // WHEN authentication status help is FINGERPRINT_ACQUIRED_IMAGER_DIRTY + fingerprintAuthRepository.setAuthenticationStatus( + HelpFingerprintAuthenticationStatus( + msgId = FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY, + msg = "test" + ) + ) + + // THEN fingerprintHelpMessage isn't update - it's still null + assertThat(fingerprintHelpMessage).isNull() + } + + @Test + fun fingerprintFailMessage_nonUdfps() = + testScope.runTest { + val fingerprintFailMessage by collectLastValue(underTest.fingerprintMessage) + + // GIVEN rear fingerprint (not UDFPS) + fingerprintPropertyRepository.setProperties( + 0, + SensorStrength.STRONG, + FingerprintSensorType.REAR, + mapOf() + ) + + // GIVEN fingerprint is allowed + biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + + // WHEN authentication status fail + fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus) + + // THEN fingerprintFailMessage is updated + assertThat(fingerprintFailMessage?.message) + .isEqualTo( + kosmos.mainResources.getString( + com.android.internal.R.string.fingerprint_error_not_match + ) + ) + } + + @Test + fun fingerprintFailMessage_udfps() = + testScope.runTest { + val fingerprintFailMessage by collectLastValue(underTest.fingerprintMessage) + + // GIVEN UDFPS + fingerprintPropertyRepository.setProperties( + 0, + SensorStrength.STRONG, + FingerprintSensorType.UDFPS_OPTICAL, + mapOf() + ) + + // GIVEN fingerprint is allowed + biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + + // WHEN authentication status fail + fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus) + + // THEN fingerprintFailMessage is updated to udfps message + assertThat(fingerprintFailMessage?.message) + .isEqualTo( + kosmos.mainResources.getString( + com.android.internal.R.string.fingerprint_udfps_error_not_match + ) + ) + } + + @Test + fun faceFailedMessage_primaryAuthRequired() = + testScope.runTest { + val faceFailedMessage by collectLastValue(underTest.faceMessage) + + // GIVEN face is not allowed + biometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(false) + + // WHEN authentication status fail + faceAuthRepository.setAuthenticationStatus(FailedFaceAuthenticationStatus()) + + // THEN fingerprintFailedMessage doesn't update - it's still null + assertThat(faceFailedMessage).isNull() + } + + @Test + fun faceFailedMessage_faceOnly() = + testScope.runTest { + val faceFailedMessage by collectLastValue(underTest.faceMessage) + + // GIVEN face is allowed + biometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(true) + + // GIVEN face only enrolled + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + + // WHEN authentication status fail + faceAuthRepository.setAuthenticationStatus(FailedFaceAuthenticationStatus()) + + // THEN fingerprintFailedMessage is updated + assertThat(faceFailedMessage).isNotNull() + } + + @Test + fun faceHelpMessage_faceOnly() = + testScope.runTest { + val faceHelpMessage by collectLastValue(underTest.faceMessage) + + // GIVEN face is allowed + biometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(true) + + // GIVEN face only enrolled + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + + // WHEN authentication status help + faceAuthRepository.setAuthenticationStatus( + HelpFaceAuthenticationStatus( + msg = "Move left", + msgId = FaceManager.FACE_ACQUIRED_TOO_RIGHT, + ) + ) + + // THEN fingerprintFailedMessage is updated + assertThat(faceHelpMessage).isNotNull() + } + + @Test + fun faceHelpMessage_coEx() = + testScope.runTest { + val faceHelpMessage by collectLastValue(underTest.faceMessage) + + // GIVEN face and fingerprint are allowed + biometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(true) + biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + + // GIVEN face only enrolled + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + + // WHEN authentication status help + faceAuthRepository.setAuthenticationStatus( + HelpFaceAuthenticationStatus( + msg = "Move left", + msgId = FaceManager.FACE_ACQUIRED_TOO_RIGHT, + ) + ) + + // THEN fingerprintFailedMessage is NOT updated + assertThat(faceHelpMessage).isNull() + } + + @Test + fun faceErrorMessage_suppressedError() = + testScope.runTest { + val faceErrorMessage by collectLastValue(underTest.faceMessage) + + // WHEN authentication status error is FACE_ERROR_HW_UNAVAILABLE + faceAuthRepository.setAuthenticationStatus( + ErrorFaceAuthenticationStatus( + msgId = FaceManager.FACE_ERROR_HW_UNAVAILABLE, + msg = "test" + ) + ) + + // THEN faceErrorMessage isn't updated - it's still null since it was suppressed + assertThat(faceErrorMessage).isNull() + } + + @Test + fun faceErrorMessage() = + testScope.runTest { + val faceErrorMessage by collectLastValue(underTest.faceMessage) + + // WHEN authentication status error is FACE_ERROR_HW_UNAVAILABLE + faceAuthRepository.setAuthenticationStatus( + ErrorFaceAuthenticationStatus( + msgId = FaceManager.FACE_ERROR_HW_UNAVAILABLE, + msg = "test" + ) + ) + + // GIVEN face is allowed + biometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(true) + + // THEN faceErrorMessage is updated + assertThat(faceErrorMessage?.message).isEqualTo("test") + } + + @Test + fun faceTimeoutErrorMessage() = + testScope.runTest { + val faceErrorMessage by collectLastValue(underTest.faceMessage) + + // WHEN authentication status error is FACE_ERROR_HW_UNAVAILABLE + faceAuthRepository.setAuthenticationStatus( + ErrorFaceAuthenticationStatus(msgId = FaceManager.FACE_ERROR_TIMEOUT, msg = "test") + ) + + // GIVEN face is allowed + biometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(true) + + // THEN faceErrorMessage is updated + assertThat(faceErrorMessage).isInstanceOf(FaceTimeoutMessage::class.java) + assertThat(faceErrorMessage?.message).isEqualTo("test") + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt index bdf0e06ce410..43c860cd302f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * 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. diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt new file mode 100644 index 000000000000..f36f0320b14d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt @@ -0,0 +1,259 @@ +/* + * 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.deviceentry.domain.interactor + +import android.content.Intent +import android.content.mockedContext +import android.hardware.fingerprint.FingerprintManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.biometricSettingsRepository +import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.ActivityStarter.OnDismissAction +import com.android.systemui.plugins.activityStarter +import com.android.systemui.power.data.repository.fakePowerRepository +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.any +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.eq +import org.mockito.ArgumentMatchers.isNull +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val underTest = kosmos.occludingAppDeviceEntryInteractor + + private val fingerprintAuthRepository = kosmos.deviceEntryFingerprintAuthRepository + private val keyguardRepository = kosmos.fakeKeyguardRepository + private val bouncerRepository = kosmos.keyguardBouncerRepository + private val powerRepository = kosmos.fakePowerRepository + private val biometricSettingsRepository = kosmos.biometricSettingsRepository + private val mockedContext = kosmos.mockedContext + private val mockedActivityStarter = kosmos.activityStarter + + @Test + fun fingerprintSuccess_goToHomeScreen() = + testScope.runTest { + givenOnOccludingApp(true) + fingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + runCurrent() + verifyGoToHomeScreen() + } + + @Test + fun fingerprintSuccess_notInteractive_doesNotGoToHomeScreen() = + testScope.runTest { + givenOnOccludingApp(true) + powerRepository.setInteractive(false) + fingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + runCurrent() + verifyNeverGoToHomeScreen() + } + + @Test + fun fingerprintSuccess_dreaming_doesNotGoToHomeScreen() = + testScope.runTest { + givenOnOccludingApp(true) + keyguardRepository.setDreaming(true) + fingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + runCurrent() + verifyNeverGoToHomeScreen() + } + + @Test + fun fingerprintSuccess_notOnOccludingApp_doesNotGoToHomeScreen() = + testScope.runTest { + givenOnOccludingApp(false) + fingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + runCurrent() + verifyNeverGoToHomeScreen() + } + + @Test + fun lockout_goToHomeScreenOnDismissAction() = + testScope.runTest { + givenOnOccludingApp(true) + fingerprintAuthRepository.setAuthenticationStatus( + ErrorFingerprintAuthenticationStatus( + FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, + "lockoutTest" + ) + ) + runCurrent() + verifyGoToHomeScreenOnDismiss() + } + + @Test + fun lockout_notOnOccludingApp_neverGoToHomeScreen() = + testScope.runTest { + givenOnOccludingApp(false) + fingerprintAuthRepository.setAuthenticationStatus( + ErrorFingerprintAuthenticationStatus( + FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, + "lockoutTest" + ) + ) + runCurrent() + verifyNeverGoToHomeScreen() + } + + @Test + fun message_fpFailOnOccludingApp_thenNotOnOccludingApp() = + testScope.runTest { + val message by collectLastValue(underTest.message) + + givenOnOccludingApp(true) + givenFingerprintAllowed(true) + runCurrent() + // WHEN a fp failure come in + fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus) + + // GIVEN fingerprint shouldn't run + givenOnOccludingApp(false) + runCurrent() + // WHEN another fp failure arrives + fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus) + + // THEN message set to null + assertThat(message).isNull() + } + + @Test + fun message_fpErrorHelpFailOnOccludingApp() = + testScope.runTest { + val message by collectLastValue(underTest.message) + + givenOnOccludingApp(true) + givenFingerprintAllowed(true) + runCurrent() + + // ERROR message + fingerprintAuthRepository.setAuthenticationStatus( + ErrorFingerprintAuthenticationStatus( + FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE, + "testError", + ) + ) + assertThat(message?.message).isEqualTo("testError") + + // HELP message + fingerprintAuthRepository.setAuthenticationStatus( + HelpFingerprintAuthenticationStatus( + FingerprintManager.FINGERPRINT_ACQUIRED_PARTIAL, + "testHelp", + ) + ) + assertThat(message?.message).isEqualTo("testHelp") + + // FAIL message + fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus) + assertThat(message?.message).isNotEqualTo("testHelp") + } + + @Test + fun message_fpError_lockoutFilteredOut() = + testScope.runTest { + val message by collectLastValue(underTest.message) + + givenOnOccludingApp(true) + givenFingerprintAllowed(true) + runCurrent() + + // permanent lockout error message + fingerprintAuthRepository.setAuthenticationStatus( + ErrorFingerprintAuthenticationStatus( + FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT, + "testPermanentLockoutMessageFiltered", + ) + ) + assertThat(message).isNull() + + // temporary lockout error message + fingerprintAuthRepository.setAuthenticationStatus( + ErrorFingerprintAuthenticationStatus( + FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, + "testLockoutMessageFiltered", + ) + ) + assertThat(message).isNull() + } + + private fun givenOnOccludingApp(isOnOccludingApp: Boolean) { + powerRepository.setInteractive(true) + keyguardRepository.setKeyguardOccluded(isOnOccludingApp) + keyguardRepository.setKeyguardShowing(isOnOccludingApp) + keyguardRepository.setDreaming(false) + bouncerRepository.setPrimaryShow(!isOnOccludingApp) + bouncerRepository.setAlternateVisible(!isOnOccludingApp) + } + + private fun givenFingerprintAllowed(allowed: Boolean) { + biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(allowed) + } + + private fun verifyGoToHomeScreen() { + val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) + verify(mockedContext).startActivity(intentCaptor.capture()) + + assertThat(intentCaptor.value.hasCategory(Intent.CATEGORY_HOME)).isTrue() + assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_MAIN) + } + + private fun verifyNeverGoToHomeScreen() { + verify(mockedContext, never()).startActivity(any()) + verify(mockedActivityStarter, never()) + .dismissKeyguardThenExecute(any(OnDismissAction::class.java), isNull(), eq(false)) + } + + private fun verifyGoToHomeScreenOnDismiss() { + val onDimissActionCaptor = ArgumentCaptor.forClass(OnDismissAction::class.java) + verify(mockedActivityStarter) + .dismissKeyguardThenExecute(onDimissActionCaptor.capture(), isNull(), eq(false)) + onDimissActionCaptor.value.onDismiss() + + verifyGoToHomeScreen() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt index e158e47b3cfa..e97edcbdc68a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt @@ -27,6 +27,7 @@ import com.android.systemui.deviceentry.data.ui.viewmodel.deviceEntryUdfpsAccess import com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState @@ -51,6 +52,7 @@ class UdfpsAccessibilityOverlayViewModelTest : SysuiTestCase() { } private val deviceEntryIconTransition = kosmos.fakeDeviceEntryIconViewModelTransition private val testScope = kosmos.testScope + private val keyguardRepository = kosmos.fakeKeyguardRepository private val accessibilityRepository = kosmos.fakeAccessibilityRepository private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository @@ -101,13 +103,11 @@ class UdfpsAccessibilityOverlayViewModelTest : SysuiTestCase() { ) assertThat(visible).isFalse() } - - @Test - fun deviceUnlocked_overlayNotVisible() = + fun fpNotRunning_overlayNotVisible() = testScope.runTest { val visible by collectLastValue(underTest.visible) setupVisibleStateOnLockscreen() - deviceEntryRepository.setUnlocked(true) + deviceEntryFingerprintAuthRepository.setIsRunning(false) assertThat(visible).isFalse() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt index f2bd817af44d..37836a56fdd5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt @@ -19,12 +19,18 @@ package com.android.systemui.keyguard.data.repository +import android.os.fakeExecutorHandler import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.ConfigurationRepository +import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.util.ThreadAssert import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -45,17 +51,23 @@ class KeyguardBlueprintRepositoryTest : SysuiTestCase() { private lateinit var underTest: KeyguardBlueprintRepository @Mock lateinit var configurationRepository: ConfigurationRepository @Mock lateinit var defaultLockscreenBlueprint: DefaultKeyguardBlueprint + @Mock lateinit var threadAssert: ThreadAssert private val testScope = TestScope(StandardTestDispatcher()) + private val kosmos: Kosmos = testKosmos() @Before fun setup() { MockitoAnnotations.initMocks(this) - whenever(defaultLockscreenBlueprint.id).thenReturn(DEFAULT) - underTest = - KeyguardBlueprintRepository( - configurationRepository, - setOf(defaultLockscreenBlueprint), - ) + with(kosmos) { + whenever(defaultLockscreenBlueprint.id).thenReturn(DEFAULT) + underTest = + KeyguardBlueprintRepository( + configurationRepository, + setOf(defaultLockscreenBlueprint), + fakeExecutorHandler, + threadAssert, + ) + } } @Test @@ -88,13 +100,17 @@ class KeyguardBlueprintRepositoryTest : SysuiTestCase() { @Test fun testTransitionToSameBlueprint_refreshesBlueprint() = - testScope.runTest { - val refreshBlueprint by collectLastValue(underTest.refreshBluePrint) - runCurrent() + with(kosmos) { + testScope.runTest { + val transition by collectLastValue(underTest.refreshTransition) + fakeExecutor.runAllReady() + runCurrent() - underTest.applyBlueprint(defaultLockscreenBlueprint) - runCurrent() + underTest.applyBlueprint(defaultLockscreenBlueprint) + fakeExecutor.runAllReady() + runCurrent() - assertThat(refreshBlueprint).isNotNull() + assertThat(transition).isNotNull() + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractorTest.kt deleted file mode 100644 index 3389fa9a48af..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractorTest.kt +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.keyguard.domain.interactor - -import android.hardware.biometrics.BiometricSourceType.FINGERPRINT -import android.hardware.fingerprint.FingerprintManager -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.keyguard.KeyguardUpdateMonitor -import com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED -import com.android.systemui.SysuiTestCase -import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository -import com.android.systemui.biometrics.shared.model.FingerprintSensorType -import com.android.systemui.biometrics.shared.model.SensorStrength -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository -import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus -import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus -import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus -import com.android.systemui.keyguard.util.IndicationHelper -import com.android.systemui.util.mockito.whenever -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyBoolean -import org.mockito.Mock -import org.mockito.MockitoAnnotations - -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -@RunWith(AndroidJUnit4::class) -class BiometricMessageInteractorTest : SysuiTestCase() { - - private lateinit var underTest: BiometricMessageInteractor - private lateinit var testScope: TestScope - private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository - private lateinit var fingerprintAuthRepository: FakeDeviceEntryFingerprintAuthRepository - - @Mock private lateinit var indicationHelper: IndicationHelper - @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - testScope = TestScope() - fingerprintPropertyRepository = FakeFingerprintPropertyRepository() - fingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository() - underTest = - BiometricMessageInteractor( - mContext.resources, - fingerprintAuthRepository, - fingerprintPropertyRepository, - indicationHelper, - keyguardUpdateMonitor, - ) - } - - @Test - fun fingerprintErrorMessage() = - testScope.runTest { - val fingerprintErrorMessage by collectLastValue(underTest.fingerprintErrorMessage) - - // GIVEN FINGERPRINT_ERROR_HW_UNAVAILABLE should NOT be suppressed - whenever( - indicationHelper.shouldSuppressErrorMsg( - FINGERPRINT, - FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE - ) - ) - .thenReturn(false) - - // WHEN authentication status error is FINGERPRINT_ERROR_HW_UNAVAILABLE - fingerprintAuthRepository.setAuthenticationStatus( - ErrorFingerprintAuthenticationStatus( - msgId = FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE, - msg = "test" - ) - ) - - // THEN fingerprintErrorMessage is updated - assertThat(fingerprintErrorMessage?.source).isEqualTo(FINGERPRINT) - assertThat(fingerprintErrorMessage?.type).isEqualTo(BiometricMessageType.ERROR) - assertThat(fingerprintErrorMessage?.id) - .isEqualTo(FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE) - assertThat(fingerprintErrorMessage?.message).isEqualTo("test") - } - - @Test - fun fingerprintErrorMessage_suppressedError() = - testScope.runTest { - val fingerprintErrorMessage by collectLastValue(underTest.fingerprintErrorMessage) - - // GIVEN FINGERPRINT_ERROR_HW_UNAVAILABLE should be suppressed - whenever( - indicationHelper.shouldSuppressErrorMsg( - FINGERPRINT, - FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE - ) - ) - .thenReturn(true) - - // WHEN authentication status error is FINGERPRINT_ERROR_HW_UNAVAILABLE - fingerprintAuthRepository.setAuthenticationStatus( - ErrorFingerprintAuthenticationStatus( - msgId = FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE, - msg = "test" - ) - ) - - // THEN fingerprintErrorMessage isn't update - it's still null - assertThat(fingerprintErrorMessage).isNull() - } - - @Test - fun fingerprintHelpMessage() = - testScope.runTest { - val fingerprintHelpMessage by collectLastValue(underTest.fingerprintHelpMessage) - - // GIVEN primary auth is NOT required - whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) - .thenReturn(true) - - // WHEN authentication status help is FINGERPRINT_ACQUIRED_IMAGER_DIRTY - fingerprintAuthRepository.setAuthenticationStatus( - HelpFingerprintAuthenticationStatus( - msgId = FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY, - msg = "test" - ) - ) - - // THEN fingerprintHelpMessage is updated - assertThat(fingerprintHelpMessage?.source).isEqualTo(FINGERPRINT) - assertThat(fingerprintHelpMessage?.type).isEqualTo(BiometricMessageType.HELP) - assertThat(fingerprintHelpMessage?.id) - .isEqualTo(FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY) - assertThat(fingerprintHelpMessage?.message).isEqualTo("test") - } - - @Test - fun fingerprintHelpMessage_primaryAuthRequired() = - testScope.runTest { - val fingerprintHelpMessage by collectLastValue(underTest.fingerprintHelpMessage) - - // GIVEN primary auth is required - whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) - .thenReturn(false) - - // WHEN authentication status help is FINGERPRINT_ACQUIRED_IMAGER_DIRTY - fingerprintAuthRepository.setAuthenticationStatus( - HelpFingerprintAuthenticationStatus( - msgId = FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY, - msg = "test" - ) - ) - - // THEN fingerprintHelpMessage isn't update - it's still null - assertThat(fingerprintHelpMessage).isNull() - } - - @Test - fun fingerprintFailMessage_nonUdfps() = - testScope.runTest { - val fingerprintFailMessage by collectLastValue(underTest.fingerprintFailMessage) - - // GIVEN primary auth is NOT required - whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) - .thenReturn(true) - - // GIVEN rear fingerprint (not UDFPS) - fingerprintPropertyRepository.setProperties( - 0, - SensorStrength.STRONG, - FingerprintSensorType.REAR, - mapOf() - ) - - // WHEN authentication status fail - fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus) - - // THEN fingerprintFailMessage is updated - assertThat(fingerprintFailMessage?.source).isEqualTo(FINGERPRINT) - assertThat(fingerprintFailMessage?.type).isEqualTo(BiometricMessageType.FAIL) - assertThat(fingerprintFailMessage?.id) - .isEqualTo(BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED) - assertThat(fingerprintFailMessage?.message) - .isEqualTo( - mContext.resources.getString( - com.android.internal.R.string.fingerprint_error_not_match - ) - ) - } - - @Test - fun fingerprintFailMessage_udfps() = - testScope.runTest { - val fingerprintFailMessage by collectLastValue(underTest.fingerprintFailMessage) - - // GIVEN primary auth is NOT required - whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) - .thenReturn(true) - - // GIVEN UDFPS - fingerprintPropertyRepository.setProperties( - 0, - SensorStrength.STRONG, - FingerprintSensorType.UDFPS_OPTICAL, - mapOf() - ) - - // WHEN authentication status fail - fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus) - - // THEN fingerprintFailMessage is updated to udfps message - assertThat(fingerprintFailMessage?.source).isEqualTo(FINGERPRINT) - assertThat(fingerprintFailMessage?.type).isEqualTo(BiometricMessageType.FAIL) - assertThat(fingerprintFailMessage?.id) - .isEqualTo(BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED) - assertThat(fingerprintFailMessage?.message) - .isEqualTo( - mContext.resources.getString( - com.android.internal.R.string.fingerprint_udfps_error_not_match - ) - ) - } - - @Test - fun fingerprintFailedMessage_primaryAuthRequired() = - testScope.runTest { - val fingerprintFailedMessage by collectLastValue(underTest.fingerprintFailMessage) - - // GIVEN primary auth is required - whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) - .thenReturn(false) - - // WHEN authentication status fail - fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus) - - // THEN fingerprintFailedMessage isn't update - it's still null - assertThat(fingerprintFailedMessage).isNull() - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt index 8b16da2a1f78..9df00d3682a4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt @@ -23,7 +23,9 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.data.repository.KeyguardBlueprintRepository import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint -import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType +import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition +import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config +import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever @@ -47,8 +49,7 @@ class KeyguardBlueprintInteractorTest : SysuiTestCase() { private lateinit var underTest: KeyguardBlueprintInteractor private lateinit var testScope: TestScope - val refreshBluePrint: MutableSharedFlow<Unit> = MutableSharedFlow(extraBufferCapacity = 1) - val refreshBlueprintTransition: MutableSharedFlow<IntraBlueprintTransitionType> = + val refreshTransition: MutableSharedFlow<IntraBlueprintTransition.Config> = MutableSharedFlow(extraBufferCapacity = 1) @Mock private lateinit var splitShadeStateController: SplitShadeStateController @@ -59,9 +60,7 @@ class KeyguardBlueprintInteractorTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) testScope = TestScope(StandardTestDispatcher()) whenever(keyguardBlueprintRepository.configurationChange).thenReturn(configurationFlow) - whenever(keyguardBlueprintRepository.refreshBluePrint).thenReturn(refreshBluePrint) - whenever(keyguardBlueprintRepository.refreshBlueprintTransition) - .thenReturn(refreshBlueprintTransition) + whenever(keyguardBlueprintRepository.refreshTransition).thenReturn(refreshTransition) underTest = KeyguardBlueprintInteractor( @@ -116,8 +115,8 @@ class KeyguardBlueprintInteractorTest : SysuiTestCase() { @Test fun testRefreshBlueprintWithTransition() { - underTest.refreshBlueprintWithTransition(IntraBlueprintTransitionType.DefaultTransition) + underTest.refreshBlueprint(Type.DefaultTransition) verify(keyguardBlueprintRepository) - .refreshBlueprintWithTransition(IntraBlueprintTransitionType.DefaultTransition) + .refreshBlueprint(Config(Type.DefaultTransition, true, true)) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt deleted file mode 100644 index 7358b9d5d2d1..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt +++ /dev/null @@ -1,371 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.keyguard.domain.interactor - -import android.content.Context -import android.content.Intent -import android.hardware.biometrics.BiometricSourceType -import android.hardware.fingerprint.FingerprintManager -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.keyguard.KeyguardUpdateMonitor -import com.android.systemui.SysuiTestCase -import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository -import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository -import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor -import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor -import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository -import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.data.repository.FakeTrustRepository -import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus -import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus -import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus -import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus -import com.android.systemui.keyguard.util.IndicationHelper -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.plugins.ActivityStarter.OnDismissAction -import com.android.systemui.power.data.repository.FakePowerRepository -import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.user.domain.interactor.SelectedUserInteractor -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers.anyBoolean -import org.mockito.ArgumentMatchers.eq -import org.mockito.ArgumentMatchers.isNull -import org.mockito.Mock -import org.mockito.Mockito.never -import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations - -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -@RunWith(AndroidJUnit4::class) -class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { - - private lateinit var underTest: OccludingAppDeviceEntryInteractor - private lateinit var testScope: TestScope - private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository - private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository - private lateinit var fingerprintAuthRepository: FakeDeviceEntryFingerprintAuthRepository - private lateinit var keyguardRepository: FakeKeyguardRepository - private lateinit var bouncerRepository: FakeKeyguardBouncerRepository - private lateinit var configurationRepository: FakeConfigurationRepository - private lateinit var featureFlags: FakeFeatureFlags - private lateinit var trustRepository: FakeTrustRepository - private lateinit var powerRepository: FakePowerRepository - private lateinit var powerInteractor: PowerInteractor - - @Mock private lateinit var indicationHelper: IndicationHelper - @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor - @Mock private lateinit var mockedContext: Context - @Mock private lateinit var activityStarter: ActivityStarter - @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - testScope = TestScope() - biometricSettingsRepository = FakeBiometricSettingsRepository() - fingerprintPropertyRepository = FakeFingerprintPropertyRepository() - fingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository() - keyguardRepository = FakeKeyguardRepository() - bouncerRepository = FakeKeyguardBouncerRepository() - configurationRepository = FakeConfigurationRepository() - featureFlags = FakeFeatureFlags() - trustRepository = FakeTrustRepository() - powerRepository = FakePowerRepository() - powerInteractor = - PowerInteractor( - powerRepository, - falsingCollector = mock(), - screenOffAnimationController = mock(), - statusBarStateController = mock(), - ) - - underTest = - OccludingAppDeviceEntryInteractor( - BiometricMessageInteractor( - mContext.resources, - fingerprintAuthRepository, - fingerprintPropertyRepository, - indicationHelper, - keyguardUpdateMonitor, - ), - fingerprintAuthRepository, - KeyguardInteractorFactory.create( - featureFlags = featureFlags, - repository = keyguardRepository, - bouncerRepository = bouncerRepository, - configurationRepository = configurationRepository, - sceneInteractor = - mock { whenever(transitioningTo).thenReturn(MutableStateFlow(null)) }, - powerInteractor = powerInteractor, - ) - .keyguardInteractor, - PrimaryBouncerInteractor( - bouncerRepository, - primaryBouncerView = mock(), - mainHandler = mock(), - keyguardStateController = mock(), - keyguardSecurityModel = mock(), - primaryBouncerCallbackInteractor = mock(), - falsingCollector = mock(), - dismissCallbackRegistry = mock(), - context, - keyguardUpdateMonitor, - trustRepository, - testScope.backgroundScope, - mSelectedUserInteractor, - deviceEntryFaceAuthInteractor = mock(), - ), - AlternateBouncerInteractor( - statusBarStateController = mock(), - keyguardStateController = mock(), - bouncerRepository, - FakeFingerprintPropertyRepository(), - biometricSettingsRepository, - FakeSystemClock(), - keyguardUpdateMonitor, - scope = testScope.backgroundScope, - ), - testScope.backgroundScope, - mockedContext, - activityStarter, - powerInteractor, - ) - } - - @Test - fun fingerprintSuccess_goToHomeScreen() = - testScope.runTest { - givenOnOccludingApp(true) - fingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) - runCurrent() - verifyGoToHomeScreen() - } - - @Test - fun fingerprintSuccess_notInteractive_doesNotGoToHomeScreen() = - testScope.runTest { - givenOnOccludingApp(true) - powerRepository.setInteractive(false) - fingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) - runCurrent() - verifyNeverGoToHomeScreen() - } - - @Test - fun fingerprintSuccess_dreaming_doesNotGoToHomeScreen() = - testScope.runTest { - givenOnOccludingApp(true) - keyguardRepository.setDreaming(true) - fingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) - runCurrent() - verifyNeverGoToHomeScreen() - } - - @Test - fun fingerprintSuccess_notOnOccludingApp_doesNotGoToHomeScreen() = - testScope.runTest { - givenOnOccludingApp(false) - fingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) - runCurrent() - verifyNeverGoToHomeScreen() - } - - @Test - fun lockout_goToHomeScreenOnDismissAction() = - testScope.runTest { - givenOnOccludingApp(true) - fingerprintAuthRepository.setAuthenticationStatus( - ErrorFingerprintAuthenticationStatus( - FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, - "lockoutTest" - ) - ) - runCurrent() - verifyGoToHomeScreenOnDismiss() - } - - @Test - fun lockout_notOnOccludingApp_neverGoToHomeScreen() = - testScope.runTest { - givenOnOccludingApp(false) - fingerprintAuthRepository.setAuthenticationStatus( - ErrorFingerprintAuthenticationStatus( - FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, - "lockoutTest" - ) - ) - runCurrent() - verifyNeverGoToHomeScreen() - } - - @Test - fun message_fpFailOnOccludingApp_thenNotOnOccludingApp() = - testScope.runTest { - val message by collectLastValue(underTest.message) - - givenOnOccludingApp(true) - givenPrimaryAuthRequired(false) - runCurrent() - // WHEN a fp failure come in - fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus) - // THEN message set to failure - assertThat(message?.type).isEqualTo(BiometricMessageType.FAIL) - - // GIVEN fingerprint shouldn't run - givenOnOccludingApp(false) - runCurrent() - // WHEN another fp failure arrives - fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus) - - // THEN message set to null - assertThat(message).isNull() - } - - @Test - fun message_fpErrorHelpFailOnOccludingApp() = - testScope.runTest { - val message by collectLastValue(underTest.message) - - givenOnOccludingApp(true) - givenPrimaryAuthRequired(false) - runCurrent() - - // ERROR message - fingerprintAuthRepository.setAuthenticationStatus( - ErrorFingerprintAuthenticationStatus( - FingerprintManager.FINGERPRINT_ERROR_CANCELED, - "testError", - ) - ) - assertThat(message?.source).isEqualTo(BiometricSourceType.FINGERPRINT) - assertThat(message?.id).isEqualTo(FingerprintManager.FINGERPRINT_ERROR_CANCELED) - assertThat(message?.message).isEqualTo("testError") - assertThat(message?.type).isEqualTo(BiometricMessageType.ERROR) - - // HELP message - fingerprintAuthRepository.setAuthenticationStatus( - HelpFingerprintAuthenticationStatus( - FingerprintManager.FINGERPRINT_ACQUIRED_PARTIAL, - "testHelp", - ) - ) - assertThat(message?.source).isEqualTo(BiometricSourceType.FINGERPRINT) - assertThat(message?.id).isEqualTo(FingerprintManager.FINGERPRINT_ACQUIRED_PARTIAL) - assertThat(message?.message).isEqualTo("testHelp") - assertThat(message?.type).isEqualTo(BiometricMessageType.HELP) - - // FAIL message - fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus) - assertThat(message?.source).isEqualTo(BiometricSourceType.FINGERPRINT) - assertThat(message?.id) - .isEqualTo(KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED) - assertThat(message?.type).isEqualTo(BiometricMessageType.FAIL) - } - - @Test - fun message_fpError_lockoutFilteredOut() = - testScope.runTest { - val message by collectLastValue(underTest.message) - - givenOnOccludingApp(true) - givenPrimaryAuthRequired(false) - runCurrent() - - // permanent lockout error message - fingerprintAuthRepository.setAuthenticationStatus( - ErrorFingerprintAuthenticationStatus( - FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT, - "testPermanentLockoutMessageFiltered", - ) - ) - assertThat(message).isNull() - - // temporary lockout error message - fingerprintAuthRepository.setAuthenticationStatus( - ErrorFingerprintAuthenticationStatus( - FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, - "testLockoutMessageFiltered", - ) - ) - assertThat(message).isNull() - } - - private fun givenOnOccludingApp(isOnOccludingApp: Boolean) { - powerRepository.setInteractive(true) - keyguardRepository.setKeyguardOccluded(isOnOccludingApp) - keyguardRepository.setKeyguardShowing(isOnOccludingApp) - keyguardRepository.setDreaming(false) - bouncerRepository.setPrimaryShow(!isOnOccludingApp) - bouncerRepository.setAlternateVisible(!isOnOccludingApp) - } - - private fun givenPrimaryAuthRequired(required: Boolean) { - whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) - .thenReturn(!required) - } - - private fun verifyGoToHomeScreen() { - val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) - verify(mockedContext).startActivity(intentCaptor.capture()) - - assertThat(intentCaptor.value.hasCategory(Intent.CATEGORY_HOME)).isTrue() - assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_MAIN) - } - - private fun verifyNeverGoToHomeScreen() { - verify(mockedContext, never()).startActivity(any()) - verify(activityStarter, never()) - .dismissKeyguardThenExecute(any(OnDismissAction::class.java), isNull(), eq(false)) - } - - private fun verifyGoToHomeScreenOnDismiss() { - val onDimissActionCaptor = ArgumentCaptor.forClass(OnDismissAction::class.java) - verify(activityStarter) - .dismissKeyguardThenExecute(onDimissActionCaptor.capture(), isNull(), eq(false)) - onDimissActionCaptor.value.onDismiss() - - verifyGoToHomeScreen() - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt index 2da74b099eda..08d44c1855e2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt @@ -138,10 +138,10 @@ class ClockSectionTest : SysuiTestCase() { underTest.applyDefaultConstraints(cs) val expectedLargeClockTopMargin = LARGE_CLOCK_TOP - assetLargeClockTop(cs, expectedLargeClockTopMargin) + assertLargeClockTop(cs, expectedLargeClockTopMargin) - val expectedSmallClockTopMargin = SMALL_CLOCK_TOP_SPLIT_SHADE - CLOCK_FADE_TRANSLATION_Y - assetSmallClockTop(cs, expectedSmallClockTopMargin) + val expectedSmallClockTopMargin = SMALL_CLOCK_TOP_SPLIT_SHADE + assertSmallClockTop(cs, expectedSmallClockTopMargin) } @Test @@ -152,10 +152,10 @@ class ClockSectionTest : SysuiTestCase() { underTest.applyDefaultConstraints(cs) val expectedLargeClockTopMargin = LARGE_CLOCK_TOP - assetLargeClockTop(cs, expectedLargeClockTopMargin) + assertLargeClockTop(cs, expectedLargeClockTopMargin) - val expectedSmallClockTopMargin = SMALL_CLOCK_TOP_NON_SPLIT_SHADE - CLOCK_FADE_TRANSLATION_Y - assetSmallClockTop(cs, expectedSmallClockTopMargin) + val expectedSmallClockTopMargin = SMALL_CLOCK_TOP_NON_SPLIT_SHADE + assertSmallClockTop(cs, expectedSmallClockTopMargin) } @Test @@ -166,10 +166,10 @@ class ClockSectionTest : SysuiTestCase() { underTest.applyDefaultConstraints(cs) val expectedLargeClockTopMargin = LARGE_CLOCK_TOP - assetLargeClockTop(cs, expectedLargeClockTopMargin) + assertLargeClockTop(cs, expectedLargeClockTopMargin) val expectedSmallClockTopMargin = SMALL_CLOCK_TOP_SPLIT_SHADE - assetSmallClockTop(cs, expectedSmallClockTopMargin) + assertSmallClockTop(cs, expectedSmallClockTopMargin) } @Test @@ -179,10 +179,10 @@ class ClockSectionTest : SysuiTestCase() { val cs = ConstraintSet() underTest.applyDefaultConstraints(cs) val expectedLargeClockTopMargin = LARGE_CLOCK_TOP - assetLargeClockTop(cs, expectedLargeClockTopMargin) + assertLargeClockTop(cs, expectedLargeClockTopMargin) val expectedSmallClockTopMargin = SMALL_CLOCK_TOP_NON_SPLIT_SHADE - assetSmallClockTop(cs, expectedSmallClockTopMargin) + assertSmallClockTop(cs, expectedSmallClockTopMargin) } @Test @@ -228,16 +228,22 @@ class ClockSectionTest : SysuiTestCase() { .thenReturn(isInSplitShade) } - private fun assetLargeClockTop(cs: ConstraintSet, expectedLargeClockTopMargin: Int) { + private fun assertLargeClockTop(cs: ConstraintSet, expectedLargeClockTopMargin: Int) { val largeClockConstraint = cs.getConstraint(R.id.lockscreen_clock_view_large) assertThat(largeClockConstraint.layout.topToTop).isEqualTo(ConstraintSet.PARENT_ID) assertThat(largeClockConstraint.layout.topMargin).isEqualTo(expectedLargeClockTopMargin) } - private fun assetSmallClockTop(cs: ConstraintSet, expectedSmallClockTopMargin: Int) { + private fun assertSmallClockTop(cs: ConstraintSet, expectedSmallClockTopMargin: Int) { + val smallClockGuidelineConstraint = cs.getConstraint(R.id.small_clock_guideline_top) + assertThat(smallClockGuidelineConstraint.layout.topToTop).isEqualTo(-1) + assertThat(smallClockGuidelineConstraint.layout.guideBegin) + .isEqualTo(expectedSmallClockTopMargin) + val smallClockConstraint = cs.getConstraint(R.id.lockscreen_clock_view) - assertThat(smallClockConstraint.layout.topToTop).isEqualTo(ConstraintSet.PARENT_ID) - assertThat(smallClockConstraint.layout.topMargin).isEqualTo(expectedSmallClockTopMargin) + assertThat(smallClockConstraint.layout.topToBottom) + .isEqualTo(R.id.small_clock_guideline_top) + assertThat(smallClockConstraint.layout.topMargin).isEqualTo(0) } companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java index 8a531fd9c842..bfb18c88bf9b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java @@ -20,7 +20,10 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.wm.shell.Flags.enableTaskbarNavbarUnification; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -47,7 +50,6 @@ import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.systemui.Dependency; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; import com.android.systemui.model.SysUiState; import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.settings.FakeDisplayTracker; @@ -140,6 +142,8 @@ public class NavigationBarControllerImplTest extends SysuiTestCase { @Test public void testCreateNavigationBarsIncludeDefaultTrue() { + assumeFalse(enableTaskbarNavbarUnification()); + // Large screens may be using taskbar and the logic is different mNavigationBarController.mIsLargeScreen = false; doNothing().when(mNavigationBarController).createNavigationBar(any(), any(), any()); @@ -295,11 +299,22 @@ public class NavigationBarControllerImplTest extends SysuiTestCase { } @Test - public void testConfigurationChange_taskbarInitialized() { + public void testConfigurationChangeUnfolding_taskbarInitialized() { Configuration configuration = mContext.getResources().getConfiguration(); when(Utilities.isLargeScreen(any())).thenReturn(true); when(mTaskbarDelegate.isInitialized()).thenReturn(true); mNavigationBarController.onConfigChanged(configuration); verify(mTaskbarDelegate, times(1)).onConfigurationChanged(configuration); } + + @Test + public void testConfigurationChangeFolding_taskbarInitialized() { + assumeTrue(enableTaskbarNavbarUnification()); + + Configuration configuration = mContext.getResources().getConfiguration(); + when(Utilities.isLargeScreen(any())).thenReturn(false); + when(mTaskbarDelegate.isInitialized()).thenReturn(true); + mNavigationBarController.onConfigChanged(configuration); + verify(mTaskbarDelegate, times(1)).onConfigurationChanged(configuration); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index 8a22f4cf3fd3..17e4e0fb33cb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -178,7 +178,8 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { mTestScope.getBackgroundScope(), new SceneContainerRepository( mTestScope.getBackgroundScope(), - mKosmos.getFakeSceneContainerConfig()), + mKosmos.getFakeSceneContainerConfig(), + mKosmos.getSceneDataSource()), powerInteractor, mock(SceneLogger.class), mKosmos.getDeviceUnlockedInteractor()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java index f582402b95a4..2f765d540b6b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java @@ -206,7 +206,8 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { mTestScope.getBackgroundScope(), new SceneContainerRepository( mTestScope.getBackgroundScope(), - mKosmos.getFakeSceneContainerConfig()), + mKosmos.getFakeSceneContainerConfig(), + mKosmos.getSceneDataSource()), powerInteractor, mock(SceneLogger.class), mKosmos.getDeviceUnlockedInteractor()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerBaseTest.java index 459040abd566..2bd0d7963593 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerBaseTest.java @@ -58,6 +58,7 @@ import com.android.keyguard.logging.KeyguardLogger; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; import com.android.systemui.biometrics.FaceHelpMessageDeferral; +import com.android.systemui.biometrics.FaceHelpMessageDeferralFactory; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -136,6 +137,8 @@ public class KeyguardIndicationControllerBaseTest extends SysuiTestCase { @Mock protected AccessibilityManager mAccessibilityManager; @Mock + protected FaceHelpMessageDeferralFactory mFaceHelpMessageDeferralFactory; + @Mock protected FaceHelpMessageDeferral mFaceHelpMessageDeferral; @Mock protected AlternateBouncerInteractor mAlternateBouncerInteractor; @@ -224,6 +227,8 @@ public class KeyguardIndicationControllerBaseTest extends SysuiTestCase { .thenReturn(mDisclosureWithOrganization); when(mUserTracker.getUserId()).thenReturn(mCurrentUserId); + when(mFaceHelpMessageDeferralFactory.create()).thenReturn(mFaceHelpMessageDeferral); + mIndicationHelper = new IndicationHelper(mKeyguardUpdateMonitor); mWakeLock = new WakeLockFake(); @@ -257,7 +262,7 @@ public class KeyguardIndicationControllerBaseTest extends SysuiTestCase { mUserManager, mExecutor, mExecutor, mFalsingManager, mAuthController, mLockPatternUtils, mScreenLifecycle, mKeyguardBypassController, mAccessibilityManager, - mFaceHelpMessageDeferral, mock(KeyguardLogger.class), + mFaceHelpMessageDeferralFactory, mock(KeyguardLogger.class), mAlternateBouncerInteractor, mAlarmManager, mUserTracker, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt index 446b9d0bd434..dbf7b6ce145e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt @@ -57,7 +57,6 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.settings.UserContextProvider import com.android.systemui.shade.shadeControllerSceneImpl import com.android.systemui.statusbar.NotificationEntryHelper @@ -607,7 +606,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { } else { SceneKey.Bouncer } - sceneInteractor.changeScene(SceneModel(key), "test") + sceneInteractor.changeScene(key, "test") sceneInteractor.setTransitionState( MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key)) ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt index d4300f08fbac..b938029cc6b8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt @@ -226,7 +226,7 @@ open class NotificationShelfTest : SysuiTestCase() { whenever(expandableView.minHeight).thenReturn(25) whenever(expandableView.shelfTransformationTarget).thenReturn(null) // use translationY - whenever(expandableView.isInShelf).thenReturn(true) + whenever(expandableView.isInShelf).thenReturn(false) whenever(ambientState.isOnKeyguard).thenReturn(true) whenever(ambientState.isExpansionChanging).thenReturn(false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt index f266f039958f..91a9da399ee8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt @@ -85,6 +85,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { private val bigGap = px(R.dimen.notification_section_divider_height) private val smallGap = px(R.dimen.notification_section_divider_height_lockscreen) + private val scrimPadding = px(R.dimen.notification_side_paddings) @Before fun setUp() { @@ -119,6 +120,18 @@ class StackScrollAlgorithmTest : SysuiTestCase() { } @Test + fun resetViewStates_defaultHunWhenShadeIsOpening_yTranslationIsInset() { + whenever(notificationRow.isPinned).thenReturn(true) + whenever(notificationRow.isHeadsUp).thenReturn(true) + + // scroll the panel over the HUN inset + ambientState.stackY = stackScrollAlgorithm.mHeadsUpInset + bigGap + + // the HUN translation should be the panel scroll position + the scrim padding + resetViewStates_hunYTranslationIs(ambientState.stackY + scrimPadding) + } + + @Test @DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) fun resetViewStates_hunAnimatingAway_yTranslationIsInset() { whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt index 414d3181c282..4f0f91a7ee56 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt @@ -18,8 +18,10 @@ package com.android.systemui.statusbar.notification.stack import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.AnimatorTestRule import com.android.systemui.res.R import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation @@ -31,10 +33,12 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Mockito.any +import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.description import org.mockito.Mockito.eq import org.mockito.Mockito.verify @@ -45,8 +49,11 @@ private const val HEADS_UP_ABOVE_SCREEN = 80 @SmallTest @RunWith(AndroidTestingRunner::class) +@RunWithLooper class StackStateAnimatorTest : SysuiTestCase() { + @get:Rule val animatorTestRule = AnimatorTestRule(this) + private lateinit var stackStateAnimator: StackStateAnimator private val stackScroller: NotificationStackScrollLayout = mock() private val view: ExpandableView = mock() @@ -112,13 +119,16 @@ class StackStateAnimatorTest : SysuiTestCase() { } @Test + @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME) fun startAnimationForEvents_startsHeadsUpDisappearAnim() { + val disappearDuration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR.toLong() val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR) + clearInvocations(view) stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0) verify(view) .performRemoveAnimation( - /* duration= */ eq(ANIMATION_DURATION_HEADS_UP_DISAPPEAR.toLong()), + /* duration= */ eq(disappearDuration), /* delay= */ eq(0L), /* translationDirection= */ eq(0f), /* isHeadsUpAnimation= */ eq(true), @@ -127,9 +137,12 @@ class StackStateAnimatorTest : SysuiTestCase() { /* animationListener= */ any() ) + animatorTestRule.advanceTimeBy(disappearDuration) // move to the end of SSA animations runnableCaptor.value.run() // execute the end runnable - verify(view, description("should be called at the end of the animation")) + verify(view, description("should be translated to the heads up appear start")) + .translationY = -stackStateAnimator.mHeadsUpAppearStartAboveScreen + verify(view, description("should be called at the end of the disappear animation")) .removeFromTransientContainer() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java index 2d120cd9b13f..76c9740f77dc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java @@ -147,6 +147,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { private KeyguardStatusBarView mKeyguardStatusBarView; private KeyguardStatusBarViewController mController; private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); + private final FakeExecutor mBackgroundExecutor = new FakeExecutor(new FakeSystemClock()); private final TestScope mTestScope = TestScopeProvider.getTestScope(); private final FakeKeyguardRepository mKeyguardRepository = new FakeKeyguardRepository(); private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); @@ -214,6 +215,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { mSecureSettings, mCommandQueue, mFakeExecutor, + mBackgroundExecutor, mLogger, mNotificationMediaManager, mStatusOverlayHoverListenerFactory diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyLightsOutNotifControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyLightsOutNotifControllerTest.java index bde2243db4e6..f91064b49e95 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyLightsOutNotifControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyLightsOutNotifControllerTest.java @@ -25,6 +25,7 @@ import static junit.framework.Assert.assertTrue; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.platform.test.annotations.DisableFlags; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; import android.view.Display; @@ -40,6 +41,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.notification.collection.NotifLiveData; import com.android.systemui.statusbar.notification.collection.NotifLiveDataStore; +import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor; import org.junit.Before; import org.junit.Test; @@ -54,6 +56,7 @@ import java.util.Objects; @SmallTest @RunWith(AndroidTestingRunner.class) @RunWithLooper +@DisableFlags(NotificationsLiveDataStoreRefactor.FLAG_NAME) public class LegacyLightsOutNotifControllerTest extends SysuiTestCase { private static final int LIGHTS_ON = 0; private static final int LIGHTS_OUT = APPEARANCE_LOW_PROFILE_BARS; diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualLocationsServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualLocationsServiceTest.kt index af1d7881195e..e6b4d069ac98 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualLocationsServiceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualLocationsServiceTest.kt @@ -12,12 +12,9 @@ import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.TestCoroutineScope -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -41,11 +38,12 @@ class WalletContextualLocationsServiceTest : SysuiTestCase() { private lateinit var underTest: WalletContextualLocationsService private lateinit var testScope: TestScope private var listenerRegisteredCount: Int = 0 - private val listener: IWalletCardsUpdatedListener.Stub = object : IWalletCardsUpdatedListener.Stub() { - override fun registerNewWalletCards(cards: List<WalletCard?>) { - listenerRegisteredCount++ + private val listener: IWalletCardsUpdatedListener.Stub = + object : IWalletCardsUpdatedListener.Stub() { + override fun registerNewWalletCards(cards: List<WalletCard?>) { + listenerRegisteredCount++ + } } - } @Before @kotlinx.coroutines.ExperimentalCoroutinesApi @@ -60,50 +58,56 @@ class WalletContextualLocationsServiceTest : SysuiTestCase() { featureFlags.set(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS, true) listenerRegisteredCount = 0 - underTest = WalletContextualLocationsService(controller, featureFlags, testScope.backgroundScope) + underTest = + WalletContextualLocationsService(controller, featureFlags, testScope.backgroundScope) } @Test @kotlinx.coroutines.ExperimentalCoroutinesApi - fun addListener() = testScope.runTest { - underTest.addWalletCardsUpdatedListenerInternal(listener) - assertThat(listenerRegisteredCount).isEqualTo(1) - } + fun addListener() = + testScope.runTest { + underTest.addWalletCardsUpdatedListenerInternal(listener) + assertThat(listenerRegisteredCount).isEqualTo(1) + } @Test @kotlinx.coroutines.ExperimentalCoroutinesApi - fun addStoreLocations() = testScope.runTest { - underTest.onWalletContextualLocationsStateUpdatedInternal(ArrayList<String>()) - verify(controller, times(1)).setSuggestionCardIds(anySet()) - } + fun addStoreLocations() = + testScope.runTest { + underTest.onWalletContextualLocationsStateUpdatedInternal(ArrayList<String>()) + verify(controller, times(1)).setSuggestionCardIds(anySet()) + } @Test @kotlinx.coroutines.ExperimentalCoroutinesApi - fun updateListenerAndLocationsState() = testScope.runTest { - // binds to the service and adds a listener - val underTestStub = getInterface - underTestStub.addWalletCardsUpdatedListener(listener) - assertThat(listenerRegisteredCount).isEqualTo(1) + fun updateListenerAndLocationsState() = + testScope.runTest { + // binds to the service and adds a listener + val underTestStub = getInterface + underTestStub.addWalletCardsUpdatedListener(listener) + assertThat(listenerRegisteredCount).isEqualTo(1) - // sends a list of card IDs to the controller - underTestStub.onWalletContextualLocationsStateUpdated(ArrayList<String>()) - verify(controller, times(1)).setSuggestionCardIds(anySet()) + // sends a list of card IDs to the controller + underTestStub.onWalletContextualLocationsStateUpdated(ArrayList<String>()) + verify(controller, times(1)).setSuggestionCardIds(anySet()) - // adds another listener - fakeWalletCards.update{ updatedFakeWalletCards } - runCurrent() - assertThat(listenerRegisteredCount).isEqualTo(2) + // adds another listener + fakeWalletCards.update { updatedFakeWalletCards } + runCurrent() + assertThat(listenerRegisteredCount).isEqualTo(2) - // sends another list of card IDs to the controller - underTestStub.onWalletContextualLocationsStateUpdated(ArrayList<String>()) - verify(controller, times(2)).setSuggestionCardIds(anySet()) - } + // sends another list of card IDs to the controller + underTestStub.onWalletContextualLocationsStateUpdated(ArrayList<String>()) + verify(controller, times(2)).setSuggestionCardIds(anySet()) + } private val fakeWalletCards: MutableStateFlow<List<WalletCard>> get() { val intent = Intent(getContext(), WalletContextualLocationsService::class.java) - val pi: PendingIntent = PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE) - val icon: Icon = Icon.createWithBitmap(Bitmap.createBitmap(70, 50, Bitmap.Config.ARGB_8888)) + val pi: PendingIntent = + PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE) + val icon: Icon = + Icon.createWithBitmap(Bitmap.createBitmap(70, 50, Bitmap.Config.ARGB_8888)) val walletCards: ArrayList<WalletCard> = ArrayList<WalletCard>() walletCards.add(WalletCard.Builder("card1", icon, "card", pi).build()) walletCards.add(WalletCard.Builder("card2", icon, "card", pi).build()) @@ -113,8 +117,10 @@ class WalletContextualLocationsServiceTest : SysuiTestCase() { private val updatedFakeWalletCards: List<WalletCard> get() { val intent = Intent(getContext(), WalletContextualLocationsService::class.java) - val pi: PendingIntent = PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE) - val icon: Icon = Icon.createWithBitmap(Bitmap.createBitmap(70, 50, Bitmap.Config.ARGB_8888)) + val pi: PendingIntent = + PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE) + val icon: Icon = + Icon.createWithBitmap(Bitmap.createBitmap(70, 50, Bitmap.Config.ARGB_8888)) val walletCards: ArrayList<WalletCard> = ArrayList<WalletCard>() walletCards.add(WalletCard.Builder("card3", icon, "card", pi).build()) return walletCards @@ -125,4 +131,4 @@ class WalletContextualLocationsServiceTest : SysuiTestCase() { val intent = Intent() return IWalletContextualLocationsService.Stub.asInterface(underTest.onBind(intent)) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 8d933dcdb9d6..97bd96d605ef 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -413,7 +413,8 @@ public class BubblesTest extends SysuiTestCase { mTestScope.getBackgroundScope(), new SceneContainerRepository( mTestScope.getBackgroundScope(), - mKosmos.getFakeSceneContainerConfig()), + mKosmos.getFakeSceneContainerConfig(), + mKosmos.getSceneDataSource()), powerInteractor, mock(SceneLogger.class), mKosmos.getDeviceUnlockedInteractor()); diff --git a/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt b/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt index 5e254bf4f879..f6ddfccfa532 100644 --- a/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt +++ b/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt @@ -18,5 +18,7 @@ package android.content import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testCase +import com.android.systemui.util.mockito.mock var Kosmos.applicationContext: Context by Kosmos.Fixture { testCase.context } +val Kosmos.mockedContext: Context by Kosmos.Fixture { mock<Context>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/FakeSystemUiModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/FakeSystemUiModule.kt index dc5fd953239d..6dd8d07b356b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/FakeSystemUiModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/FakeSystemUiModule.kt @@ -19,7 +19,6 @@ import com.android.systemui.classifier.FakeClassifierModule import com.android.systemui.data.FakeSystemUiDataLayerModule import com.android.systemui.flags.FakeFeatureFlagsClassicModule import com.android.systemui.log.FakeUiEventLoggerModule -import com.android.systemui.scene.FakeSceneModule import com.android.systemui.settings.FakeSettingsModule import com.android.systemui.statusbar.policy.FakeConfigurationControllerModule import com.android.systemui.statusbar.policy.FakeSplitShadeStateControllerModule @@ -34,7 +33,6 @@ import dagger.Module FakeConfigurationControllerModule::class, FakeExecutorModule::class, FakeFeatureFlagsClassicModule::class, - FakeSceneModule::class, FakeSettingsModule::class, FakeSplitShadeStateControllerModule::class, FakeSystemClockModule::class, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt index 3724291571d7..69b769eb2321 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt @@ -27,7 +27,10 @@ import com.android.systemui.coroutines.collectValues import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.scene.SceneContainerFrameworkModule import com.android.systemui.scene.shared.flag.SceneContainerFlags +import com.android.systemui.scene.shared.model.SceneDataSource +import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.shade.domain.interactor.BaseShadeInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl @@ -52,6 +55,7 @@ import kotlinx.coroutines.test.runTest TestMocksModule::class, CoroutineTestScopeModule::class, FakeSystemUiModule::class, + SceneContainerFrameworkModule::class, ] ) interface SysUITestModule { @@ -63,6 +67,7 @@ interface SysUITestModule { @Binds @Main fun bindMainResources(resources: Resources): Resources @Binds fun bindBroadcastDispatcher(fake: FakeBroadcastDispatcher): BroadcastDispatcher @Binds @SysUISingleton fun bindsShadeInteractor(sii: ShadeInteractorImpl): ShadeInteractor + @Binds fun bindSceneDataSource(delegator: SceneDataSourceDelegator): SceneDataSource companion object { @Provides diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt index 3f55f42b8d85..b8c880b3892f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt @@ -42,6 +42,7 @@ import com.android.systemui.model.SysUiState import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.scene.shared.logger.SceneLogger import com.android.systemui.shared.system.ActivityManagerWrapper import com.android.systemui.statusbar.LockscreenShadeTransitionController import com.android.systemui.statusbar.NotificationListener @@ -121,12 +122,13 @@ data class TestMocksModule( @get:Provides val systemUIDialogManager: SystemUIDialogManager = mock(), @get:Provides val deviceEntryIconTransitions: Set<DeviceEntryIconTransition> = emptySet(), @get:Provides val communalInteractor: CommunalInteractor = mock(), + @get:Provides val sceneLogger: SceneLogger = mock(), // log buffers @get:[Provides BroadcastDispatcherLog] val broadcastDispatcherLogger: LogBuffer = mock(), @get:[Provides SceneFrameworkLog] - val sceneLogger: LogBuffer = mock(), + val sceneLogBuffer: LogBuffer = mock(), @get:[Provides BiometricLog] val biometricLogger: LogBuffer = mock(), @get:Provides val lsShadeTransitionLogger: LSShadeTransitionLogger = mock(), diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt index 244ef8d81ebd..3c6135388971 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt @@ -34,7 +34,8 @@ import com.android.systemui.util.concurrency.mockExecutorHandler import com.android.systemui.util.mockito.mock var Kosmos.mockPrimaryBouncerInteractor by Kosmos.Fixture { mock<PrimaryBouncerInteractor>() } -var Kosmos.primaryBouncerInteractor by + +val Kosmos.primaryBouncerInteractor by Kosmos.Fixture { PrimaryBouncerInteractor( repository = keyguardBouncerRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorKosmos.kt new file mode 100644 index 000000000000..77f48db60f41 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorKosmos.kt @@ -0,0 +1,34 @@ +/* + * 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.deviceentry.domain.interactor + +import android.content.res.mainResources +import com.android.systemui.biometrics.domain.interactor.fingerprintPropertyInteractor +import com.android.systemui.kosmos.Kosmos +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +val Kosmos.biometricMessageInteractor by + Kosmos.Fixture { + BiometricMessageInteractor( + resources = mainResources, + fingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor, + fingerprintPropertyInteractor = fingerprintPropertyInteractor, + faceAuthInteractor = deviceEntryFaceAuthInteractor, + biometricSettingsInteractor = deviceEntryBiometricSettingsInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractorKosmos.kt index 8811b8db1cc0..4fcf43a2a055 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractorKosmos.kt @@ -13,25 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.scene.shared.model -import dagger.Module -import dagger.Provides +package com.android.systemui.deviceentry.domain.interactor -@Module -data class FakeSceneContainerConfigModule( - @get:Provides - val sceneContainerConfig: SceneContainerConfig = - SceneContainerConfig( - sceneKeys = - listOf( - SceneKey.QuickSettings, - SceneKey.Shade, - SceneKey.Lockscreen, - SceneKey.Bouncer, - SceneKey.Gone, - SceneKey.Communal, - ), - initialSceneKey = SceneKey.Lockscreen, - ), -) +import com.android.systemui.keyguard.data.repository.biometricSettingsRepository +import com.android.systemui.kosmos.Kosmos +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +val Kosmos.deviceEntryBiometricSettingsInteractor by + Kosmos.Fixture { + DeviceEntryBiometricSettingsInteractor( + repository = biometricSettingsRepository, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt new file mode 100644 index 000000000000..c3f677e8d0d3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt @@ -0,0 +1,44 @@ +/* + * 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.deviceentry.domain.interactor + +import android.content.mockedContext +import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor +import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor +import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.plugins.activityStarter +import com.android.systemui.power.domain.interactor.powerInteractor +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +val Kosmos.occludingAppDeviceEntryInteractor by + Kosmos.Fixture { + OccludingAppDeviceEntryInteractor( + biometricMessageInteractor = biometricMessageInteractor, + fingerprintAuthRepository = deviceEntryFingerprintAuthRepository, + keyguardInteractor = keyguardInteractor, + primaryBouncerInteractor = primaryBouncerInteractor, + alternateBouncerInteractor = alternateBouncerInteractor, + scope = applicationCoroutineScope, + context = mockedContext, + activityStarter = activityStarter, + powerInteractor = powerInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt index 19cd9502862f..8452963ddba1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt @@ -16,17 +16,22 @@ package com.android.systemui.keyguard.data.repository +import android.os.fakeExecutorHandler import com.android.systemui.common.ui.data.repository.configurationRepository import com.android.systemui.keyguard.shared.model.KeyguardBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.ThreadAssert +import com.android.systemui.util.mockito.mock val Kosmos.keyguardBlueprintRepository by Kosmos.Fixture { KeyguardBlueprintRepository( configurationRepository = configurationRepository, blueprints = setOf(defaultBlueprint), + handler = fakeExecutorHandler, + assert = mock<ThreadAssert>(), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index 10305f7d50c4..f6b32800263a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -43,6 +43,7 @@ import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.sceneContainerConfig import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags +import com.android.systemui.scene.shared.model.sceneDataSource import com.android.systemui.statusbar.phone.screenOffAnimationController import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository @@ -90,6 +91,7 @@ class KosmosJavaAdapter( val fromPrimaryBouncerTransitionInteractor by lazy { kosmos.fromPrimaryBouncerTransitionInteractor } + val sceneDataSource by lazy { kosmos.sceneDataSource } init { kosmos.applicationContext = testCase.context diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryKosmos.kt index e19941cfbaa0..8734609c6c69 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryKosmos.kt @@ -17,8 +17,15 @@ package com.android.systemui.scene.data.repository import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.scene.sceneContainerConfig +import com.android.systemui.scene.shared.model.sceneDataSource -val Kosmos.sceneContainerRepository by - Kosmos.Fixture { SceneContainerRepository(applicationCoroutineScope, sceneContainerConfig) } +val Kosmos.sceneContainerRepository by Fixture { + SceneContainerRepository( + applicationScope = applicationCoroutineScope, + config = sceneContainerConfig, + dataSource = sceneDataSource, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/NotificationsDataKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/NotificationsDataKosmos.kt new file mode 100644 index 000000000000..a61f7ececc69 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/NotificationsDataKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.data + +import com.android.settingslib.statusbar.notification.data.repository.FakeNotificationsSoundPolicyRepository +import com.android.systemui.kosmos.Kosmos + +val Kosmos.notificationsSoundPolicyRepository by + Kosmos.Fixture { FakeNotificationsSoundPolicyRepository() } diff --git a/ravenwood/bivalenttest/Android.bp b/ravenwood/bivalenttest/Android.bp index acf85331601e..a6b6ed934ce7 100644 --- a/ravenwood/bivalenttest/Android.bp +++ b/ravenwood/bivalenttest/Android.bp @@ -7,6 +7,30 @@ package { default_applicable_licenses: ["frameworks_base_license"], } +cc_library_shared { + name: "libravenwoodbivalenttest_jni", + host_supported: true, + + cflags: [ + "-Wall", + "-Werror", + "-Wno-unused-parameter", + "-Wthread-safety", + ], + + srcs: [ + "jni/*.cpp", + ], + + shared_libs: [ + "libbase", + "liblog", + "libnativehelper", + "libutils", + "libcutils", + ], +} + android_ravenwood_test { name: "RavenwoodBivalentTest", @@ -18,6 +42,9 @@ android_ravenwood_test { srcs: [ "test/**/*.java", ], + jni_libs: [ + "libravenwoodbivalenttest_jni", + ], sdk_version: "test_current", auto_gen_config: true, } @@ -38,6 +65,9 @@ android_test { "ravenwood-junit", ], + jni_libs: [ + "libravenwoodbivalenttest_jni", + ], test_suites: [ "device-tests", ], diff --git a/ravenwood/bivalenttest/jni/ravenwood_core_test_jni.cpp b/ravenwood/bivalenttest/jni/ravenwood_core_test_jni.cpp new file mode 100644 index 000000000000..5e66b29b370a --- /dev/null +++ b/ravenwood/bivalenttest/jni/ravenwood_core_test_jni.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <nativehelper/JNIHelp.h> +#include "jni.h" +#include "utils/Log.h" +#include "utils/misc.h" + +static jint add(JNIEnv* env, jclass clazz, jint a, jint b) { + return a + b; +} + +static const JNINativeMethod sMethods[] = +{ + { "add", "(II)I", (void*)add }, +}; + +extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) +{ + JNIEnv* env = NULL; + jint result = -1; + + if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { + ALOGE("GetEnv failed!"); + return result; + } + ALOG_ASSERT(env, "Could not retrieve the env!"); + + ALOGI("%s: JNI_OnLoad", __FILE__); + + int res = jniRegisterNativeMethods(env, + "com/android/platform/test/ravenwood/bivalenttest/RavenwoodJniTest", + sMethods, NELEM(sMethods)); + if (res < 0) { + return res; + } + + return JNI_VERSION_1_4; +} diff --git a/ravenwood/bivalenttest/test/com/android/platform/test/ravenwood/bivalenttest/RavenwoodJniTest.java b/ravenwood/bivalenttest/test/com/android/platform/test/ravenwood/bivalenttest/RavenwoodJniTest.java new file mode 100644 index 000000000000..3b106da74ed4 --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/platform/test/ravenwood/bivalenttest/RavenwoodJniTest.java @@ -0,0 +1,44 @@ +/* + * 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 static junit.framework.Assert.assertEquals; + +import android.platform.test.ravenwood.RavenwoodRule; +import android.platform.test.ravenwood.RavenwoodUtils; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public final class RavenwoodJniTest { + static { + RavenwoodUtils.loadJniLibrary("ravenwoodbivalenttest_jni"); + } + + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder().build(); + + private static native int add(int a, int b); + + @Test + public void testNativeMethod() { + assertEquals(5, add(2, 3)); + } +} diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java new file mode 100644 index 000000000000..b736a7662bd4 --- /dev/null +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.platform.test.ravenwood; + +import java.io.File; +import java.io.PrintStream; +import java.util.Arrays; + +/** + * Utilities for writing (bivalent) ravenwood tests. + */ +public class RavenwoodUtils { + private RavenwoodUtils() { + } + + /** + * Load a JNI library respecting {@code java.library.path} + * (which reflects {@code LD_LIBRARY_PATH}). + * + * <p>{@code libname} must be the library filename without: + * - directory + * - "lib" prefix + * - and the ".so" extension + * + * <p>For example, in order to load "libmyjni.so", then pass "myjni". + * + * <p>This is basically the same thing as Java's {@link System#loadLibrary(String)}, + * but this API works slightly different on ART and on the desktop Java, namely + * the desktop Java version uses a different entry point method name + * {@code JNI_OnLoad_libname()} (note the included "libname") + * while ART always seems to use {@code JNI_OnLoad()}. + * + * <p>This method provides the same behavior on both the device side and on Ravenwood -- + * it uses {@code JNI_OnLoad()} as the entry point name on both. + */ + public static void loadJniLibrary(String libname) { + if (RavenwoodRule.isOnRavenwood()) { + loadLibraryOnRavenwood(libname); + } else { + // Just delegate to the loadLibrary(). + System.loadLibrary(libname); + } + } + + private static void loadLibraryOnRavenwood(String libname) { + var path = System.getProperty("java.library.path"); + var filename = "lib" + libname + ".so"; + + System.out.println("Looking for library " + libname + ".so in java.library.path:" + path); + + try { + if (path == null) { + throw new UnsatisfiedLinkError("Cannot load library " + libname + "." + + " Property java.library.path not set!"); + } + for (var dir : path.split(":")) { + var file = new File(dir + "/" + filename); + if (file.exists()) { + System.load(file.getAbsolutePath()); + return; + } + } + throw new UnsatisfiedLinkError("Library " + libname + " no found in " + + "java.library.path: " + path); + } catch (Exception e) { + dumpFiles(System.out); + throw e; + } + } + + private static void dumpFiles(PrintStream out) { + try { + var path = System.getProperty("java.library.path"); + out.println("# java.library.path=" + path); + + for (var dir : path.split(":")) { + listFiles(out, new File(dir), ""); + + var gparent = new File((new File(dir)).getAbsolutePath() + "../../..") + .getCanonicalFile(); + if (gparent.getName().contains("testcases")) { + // Special case: if we found this directory, dump its contents too. + listFiles(out, gparent, ""); + } + } + } catch (Throwable th) { + out.println("Error: " + th.toString()); + th.printStackTrace(out); + } + } + + private static void listFiles(PrintStream out, File dir, String prefix) { + if (!dir.isDirectory()) { + out.println(prefix + dir.getAbsolutePath() + " is not a directory!"); + return; + } + out.println(prefix + ":" + dir.getAbsolutePath() + "/"); + // First, list the files. + for (var file : Arrays.stream(dir.listFiles()).sorted().toList()) { + out.println(prefix + " " + file.getName() + "" + (file.isDirectory() ? "/" : "")); + } + + // Then recurse. + if (dir.getAbsolutePath().startsWith("/usr") || dir.getAbsolutePath().startsWith("/lib")) { + // There would be too many files, so don't recurse. + return; + } + for (var file : Arrays.stream(dir.listFiles()).sorted().toList()) { + if (file.isDirectory()) { + listFiles(out, file, prefix + " "); + } + } + } +} diff --git a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING index 5a548fdca12e..82ab0980a22b 100644 --- a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING +++ b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING @@ -33,6 +33,14 @@ ] }, { + "name": "CtsVirtualDevicesCameraTestCases", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + }, + { "name": "CtsHardwareTestCases", "options": [ { @@ -54,11 +62,33 @@ "exclude-annotation": "android.support.test.filters.FlakyTest" } ] + } + ], + "postsubmit": [ + { + "name": "CtsMediaAudioTestCases", + "options": [ + { + "include-filter": "android.media.audio.cts.AudioFocusWithVdmTest" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] }, { - "name": "CtsVirtualDevicesCameraTestCases", + "name": "CtsPermissionTestCases", "options": [ { + "include-filter": "android.permissionmultidevice.cts.DeviceAwarePermissionGrantTest" + }, + { + "include-filter": "android.permission.cts.DevicePermissionsTest" + }, + { + "include-filter": "android.permission.cts.PermissionUpdateListenerTest" + }, + { "exclude-annotation": "androidx.test.filters.FlakyTest" } ] 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/biometrics/OWNERS b/services/core/java/com/android/server/biometrics/OWNERS index 1bf2aeffdf0c..4703efbcd7be 100644 --- a/services/core/java/com/android/server/biometrics/OWNERS +++ b/services/core/java/com/android/server/biometrics/OWNERS @@ -2,7 +2,6 @@ set noparent graciecheng@google.com ilyamaty@google.com -jaggies@google.com jbolinger@google.com jeffpu@google.com joshmccloskey@google.com diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java index c8c0482f5a9d..a100fe06c407 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java @@ -77,6 +77,7 @@ final class InputMethodBindingController { @GuardedBy("ImfLock.class") private int mCurSeq; @GuardedBy("ImfLock.class") private boolean mVisibleBound; @GuardedBy("ImfLock.class") private boolean mSupportsStylusHw; + @GuardedBy("ImfLock.class") private boolean mSupportsConnectionlessStylusHw; @Nullable private CountDownLatch mLatchForTesting; @@ -243,10 +244,17 @@ final class InputMethodBindingController { /** * Returns {@code true} if current IME supports Stylus Handwriting. */ + @GuardedBy("ImfLock.class") boolean supportsStylusHandwriting() { return mSupportsStylusHw; } + /** Returns whether the current IME supports connectionless stylus handwriting sessions. */ + @GuardedBy("ImfLock.class") + boolean supportsConnectionlessStylusHandwriting() { + return mSupportsConnectionlessStylusHw; + } + /** * Used to bring IME service up to visible adjustment while it is being shown. */ @@ -298,6 +306,15 @@ final class InputMethodBindingController { if (supportsStylusHwChanged) { InputMethodManager.invalidateLocalStylusHandwritingAvailabilityCaches(); } + boolean supportsConnectionlessStylusHwChanged = + mSupportsConnectionlessStylusHw + != info.supportsConnectionlessStylusHandwriting(); + if (supportsConnectionlessStylusHwChanged) { + mSupportsConnectionlessStylusHw = + info.supportsConnectionlessStylusHandwriting(); + InputMethodManager + .invalidateLocalConnectionlessStylusHandwritingAvailabilityCaches(); + } mService.initializeImeLocked(mCurMethod, mCurToken); mService.scheduleNotifyImeUidToAudioService(mCurMethodUid); mService.reRequestCurrentClientSessionLocked(); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 96c0c8a9a7b8..5db18ad7a07f 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -123,6 +123,7 @@ import android.view.WindowManager; import android.view.WindowManager.DisplayImePolicy; import android.view.WindowManager.LayoutParams; import android.view.WindowManager.LayoutParams.SoftInputModeFlags; +import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; @@ -145,6 +146,7 @@ import com.android.internal.content.PackageMonitor; import com.android.internal.infra.AndroidFuture; import com.android.internal.inputmethod.DirectBootAwareness; import com.android.internal.inputmethod.IAccessibilityInputMethodSession; +import com.android.internal.inputmethod.IConnectionlessHandwritingCallback; import com.android.internal.inputmethod.IImeTracker; import com.android.internal.inputmethod.IInlineSuggestionsRequestCallback; import com.android.internal.inputmethod.IInputContentUriToken; @@ -204,6 +206,7 @@ import java.util.OptionalInt; import java.util.WeakHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -1980,7 +1983,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @Override - public boolean isStylusHandwritingAvailableAsUser(@UserIdInt int userId) { + public boolean isStylusHandwritingAvailableAsUser( + @UserIdInt int userId, boolean connectionless) { if (UserHandle.getCallingUserId() != userId) { mContext.enforceCallingOrSelfPermission( Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); @@ -1993,14 +1997,17 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // Check if selected IME of current user supports handwriting. if (userId == mSettings.getUserId()) { - return mBindingController.supportsStylusHandwriting(); + return mBindingController.supportsStylusHandwriting() + && (!connectionless + || mBindingController.supportsConnectionlessStylusHandwriting()); } //TODO(b/197848765): This can be optimized by caching multi-user methodMaps/methodList. //TODO(b/210039666): use cache. final InputMethodSettings settings = queryMethodMapForUser(userId); final InputMethodInfo imi = settings.getMethodMap().get( settings.getSelectedInputMethod()); - return imi != null && imi.supportsStylusHandwriting(); + return imi != null && imi.supportsStylusHandwriting() + && (!connectionless || imi.supportsConnectionlessStylusHandwriting()); } } @@ -3376,6 +3383,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub startStylusHandwriting(client, false /* usesDelegation */); } + @BinderThread + @Override + public void startConnectionlessStylusHandwriting(IInputMethodClient client, int userId, + @Nullable CursorAnchorInfo cursorAnchorInfo, @Nullable String delegatePackageName, + @Nullable String delegatorPackageName, + @NonNull IConnectionlessHandwritingCallback callback) { + // TODO(b/300979854) + } + private void startStylusHandwriting(IInputMethodClient client, boolean usesDelegation) { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.startStylusHandwriting"); try { @@ -5988,7 +6004,23 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @BinderThread private void dumpAsProtoNoCheck(FileDescriptor fd) { final ProtoOutputStream proto = new ProtoOutputStream(fd); + // Dump in the format of an ImeTracing trace with a single entry. + final long magicNumber = + ((long) InputMethodManagerServiceTraceFileProto.MAGIC_NUMBER_H << 32) + | InputMethodManagerServiceTraceFileProto.MAGIC_NUMBER_L; + final long timeOffsetNs = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()) + - SystemClock.elapsedRealtimeNanos(); + proto.write(InputMethodManagerServiceTraceFileProto.MAGIC_NUMBER, + magicNumber); + proto.write(InputMethodManagerServiceTraceFileProto.REAL_TO_ELAPSED_TIME_OFFSET_NANOS, + timeOffsetNs); + final long token = proto.start(InputMethodManagerServiceTraceFileProto.ENTRY); + proto.write(InputMethodManagerServiceTraceProto.ELAPSED_REALTIME_NANOS, + SystemClock.elapsedRealtimeNanos()); + proto.write(InputMethodManagerServiceTraceProto.WHERE, + "InputMethodManagerService.mPriorityDumper#dumpAsProtoNoCheck"); dumpDebug(proto, InputMethodManagerServiceTraceProto.INPUT_METHOD_MANAGER_SERVICE); + proto.end(token); proto.flush(); } }; diff --git a/services/core/java/com/android/server/locksettings/OWNERS b/services/core/java/com/android/server/locksettings/OWNERS index 5d4986307835..48da270180e6 100644 --- a/services/core/java/com/android/server/locksettings/OWNERS +++ b/services/core/java/com/android/server/locksettings/OWNERS @@ -1,4 +1,3 @@ # Bug component: 1333694 ebiggers@google.com -jaggies@google.com rubinxu@google.com diff --git a/services/core/java/com/android/server/media/MediaSession2Record.java b/services/core/java/com/android/server/media/MediaSession2Record.java index 393e7efcce6e..34bb219a9162 100644 --- a/services/core/java/com/android/server/media/MediaSession2Record.java +++ b/services/core/java/com/android/server/media/MediaSession2Record.java @@ -55,8 +55,15 @@ public class MediaSession2Record implements MediaSessionRecordImpl { @GuardedBy("mLock") private boolean mIsClosed; - public MediaSession2Record(Session2Token sessionToken, MediaSessionService service, - Looper handlerLooper, int policies) { + private final int mPid; + private final ForegroundServiceDelegationOptions mForegroundServiceDelegationOptions; + + public MediaSession2Record( + Session2Token sessionToken, + MediaSessionService service, + Looper handlerLooper, + int pid, + int policies) { // The lock is required to prevent `Controller2Callback` from using partially initialized // `MediaSession2Record.this`. synchronized (mLock) { @@ -66,7 +73,27 @@ public class MediaSession2Record implements MediaSessionRecordImpl { mController = new MediaController2.Builder(service.getContext(), sessionToken) .setControllerCallback(mHandlerExecutor, new Controller2Callback()) .build(); + mPid = pid; mPolicies = policies; + mForegroundServiceDelegationOptions = + new ForegroundServiceDelegationOptions.Builder() + .setClientPid(mPid) + .setClientUid(getUid()) + .setClientPackageName(getPackageName()) + .setClientAppThread(null) + .setSticky(false) + .setClientInstanceName( + "MediaSessionFgsDelegate_" + + getUid() + + "_" + + mPid + + "_" + + getPackageName()) + .setForegroundServiceTypes(0) + .setDelegationService( + ForegroundServiceDelegationOptions + .DELEGATION_SERVICE_MEDIA_PLAYBACK) + .build(); } } @@ -91,8 +118,7 @@ public class MediaSession2Record implements MediaSessionRecordImpl { @Override public ForegroundServiceDelegationOptions getForegroundServiceDelegationOptions() { - // TODO: Implement when MediaSession2 knows about its owner pid. - return null; + return mForegroundServiceDelegationOptions; } @Override diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index 7fabdf293b39..8cb5cef38ec6 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -171,12 +171,27 @@ public class MediaSessionService extends SystemService implements Monitor { private final MediaCommunicationManager.SessionCallback mSession2TokenCallback = new MediaCommunicationManager.SessionCallback() { @Override + // TODO (b/324266224): Deprecate this method once other overload is published. public void onSession2TokenCreated(Session2Token token) { + addSession(token, Process.INVALID_PID); + } + + @Override + public void onSession2TokenCreated(Session2Token token, int pid) { + addSession(token, pid); + } + + private void addSession(Session2Token token, int pid) { if (DEBUG) { Log.d(TAG, "Session2 is created " + token); } - MediaSession2Record record = new MediaSession2Record(token, - MediaSessionService.this, mRecordThread.getLooper(), 0); + MediaSession2Record record = + new MediaSession2Record( + token, + MediaSessionService.this, + mRecordThread.getLooper(), + pid, + /* policies= */ 0); synchronized (mLock) { FullUserRecord user = getFullUserRecordLocked(record.getUserId()); if (user != null) { @@ -583,7 +598,8 @@ public class MediaSessionService extends SystemService implements Monitor { } ForegroundServiceDelegationOptions foregroundServiceDelegationOptions = record.getForegroundServiceDelegationOptions(); - if (foregroundServiceDelegationOptions == null) { + if (foregroundServiceDelegationOptions == null + || foregroundServiceDelegationOptions.mClientPid == Process.INVALID_PID) { // This record doesn't support FGS delegation. In practice, this is MediaSession2. return; } 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 2cd8fe0ac43e..f60f55c11935 100644 --- a/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java +++ b/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java @@ -47,6 +47,7 @@ import com.android.server.SystemService; import java.security.SecureRandom; import java.util.Arrays; import java.util.List; +import java.util.regex.Pattern; /** * System service manages media metrics. @@ -79,6 +80,10 @@ public final class MediaMetricsManagerService extends SystemService { 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 Pattern PATTERN_KNOWN_EDITING_LIBRARY_NAMES = + Pattern.compile( + "androidx\\.media3:media3-(transformer|muxer):" + + "[\\d.]+(-(alpha|beta|rc)\\d\\d)?"); 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/"; @@ -415,8 +420,11 @@ public final class MediaMetricsManagerService extends SystemService { .setAtomId(798) .writeString(sessionId) .writeInt(event.getFinalState()) + .writeFloat(event.getFinalProgressPercent()) .writeInt(event.getErrorCode()) .writeLong(event.getTimeSinceCreatedMillis()) + .writeString(getFilteredLibraryName(event.getExporterName())) + .writeString(getFilteredLibraryName(event.getMuxerName())) .writeInt(getThroughputFps(event)) .writeInt(event.getInputMediaItemInfos().size()) .writeInt(inputMediaItemInfo.getSourceType()) @@ -629,6 +637,16 @@ public final class MediaMetricsManagerService extends SystemService { } } + private static String getFilteredLibraryName(String libraryName) { + if (TextUtils.isEmpty(libraryName)) { + return ""; + } + if (!PATTERN_KNOWN_EDITING_LIBRARY_NAMES.matcher(libraryName).matches()) { + return ""; + } + return libraryName; + } + private static int getThroughputFps(EditingEndedEvent event) { MediaItemInfo outputMediaItemInfo = event.getOutputMediaItemInfo(); if (outputMediaItemInfo == null) { diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 3507d2d56cbd..ea4e67a17e50 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -95,9 +95,11 @@ import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_NORMAL; import static android.os.PowerWhitelistManager.REASON_NOTIFICATION_SERVICE; import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED; +import static android.os.UserHandle.USER_ALL; import static android.os.UserHandle.USER_NULL; import static android.os.UserHandle.USER_SYSTEM; import static android.service.notification.Flags.redactSensitiveNotificationsFromUntrustedListeners; +import static android.service.notification.Flags.callstyleCallbackApi; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING; @@ -160,6 +162,7 @@ import android.Manifest; import android.Manifest.permission; import android.annotation.DurationMillisLong; import android.annotation.ElapsedRealtimeLong; +import android.annotation.EnforcePermission; import android.annotation.FlaggedApi; import android.annotation.MainThread; import android.annotation.NonNull; @@ -176,6 +179,7 @@ import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.AutomaticZenRule; import android.app.IActivityManager; +import android.app.ICallNotificationEventCallback; import android.app.INotificationManager; import android.app.ITransientNotification; import android.app.ITransientNotificationCallback; @@ -255,6 +259,7 @@ import android.os.ParcelFileDescriptor; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.Process; +import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; @@ -729,6 +734,9 @@ public class NotificationManagerService extends SystemService { private NotificationUsageStats mUsageStats; private boolean mLockScreenAllowSecureNotifications = true; boolean mSystemExemptFromDismissal = false; + final ArrayMap<String, ArrayMap<Integer, + RemoteCallbackList<ICallNotificationEventCallback>>> + mCallNotificationEventCallbacks = new ArrayMap<>(); private static final int MY_UID = Process.myUid(); private static final int MY_PID = Process.myPid(); @@ -4888,6 +4896,94 @@ public class NotificationManagerService extends SystemService { } /** + * Register a listener to be notified when a call notification is posted or removed + * for a specific package and user. + * @param packageName Which package to monitor + * @param userHandle Which user to monitor + * @param listener Listener to register + */ + @Override + @EnforcePermission(allOf = { + android.Manifest.permission.INTERACT_ACROSS_USERS, + android.Manifest.permission.ACCESS_NOTIFICATIONS}) + public void registerCallNotificationEventListener(String packageName, UserHandle userHandle, + ICallNotificationEventCallback listener) { + registerCallNotificationEventListener_enforcePermission(); + + final int userId = userHandle.getIdentifier() != UserHandle.USER_CURRENT + ? userHandle.getIdentifier() : mAmi.getCurrentUserId(); + + synchronized (mCallNotificationEventCallbacks) { + ArrayMap<Integer, RemoteCallbackList<ICallNotificationEventCallback>> + callbacksForPackage = + mCallNotificationEventCallbacks.getOrDefault(packageName, new ArrayMap<>()); + RemoteCallbackList<ICallNotificationEventCallback> callbackList = + callbacksForPackage.getOrDefault(userId, new RemoteCallbackList<>()); + + if (callbackList.register(listener)) { + callbacksForPackage.put(userId, callbackList); + mCallNotificationEventCallbacks.put(packageName, callbacksForPackage); + } else { + Log.e(TAG, + "registerCallNotificationEventListener failed to register listener: " + + packageName + " " + userHandle + " " + listener); + return; + } + } + + synchronized (mNotificationLock) { + for (NotificationRecord r : mNotificationList) { + if (r.getNotification().isStyle(Notification.CallStyle.class) + && notificationMatchesUserId(r, userId, false) + && r.getSbn().getPackageName().equals(packageName)) { + try { + listener.onCallNotificationPosted(packageName, r.getUser()); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + } + } + } + + /** + * Unregister a listener that was previously + * registered with {@link #registerCallNotificationEventListener} + * + * @param packageName Which package to stop monitoring + * @param userHandle Which user to stop monitoring + * @param listener Listener to unregister + */ + @Override + @EnforcePermission(allOf = { + android.Manifest.permission.INTERACT_ACROSS_USERS, + android.Manifest.permission.ACCESS_NOTIFICATIONS}) + public void unregisterCallNotificationEventListener(String packageName, + UserHandle userHandle, ICallNotificationEventCallback listener) { + unregisterCallNotificationEventListener_enforcePermission(); + synchronized (mCallNotificationEventCallbacks) { + final int userId = userHandle.getIdentifier() != UserHandle.USER_CURRENT + ? userHandle.getIdentifier() : mAmi.getCurrentUserId(); + + ArrayMap<Integer, RemoteCallbackList<ICallNotificationEventCallback>> + callbacksForPackage = mCallNotificationEventCallbacks.get(packageName); + if (callbacksForPackage == null) { + return; + } + RemoteCallbackList<ICallNotificationEventCallback> callbackList = + callbacksForPackage.get(userId); + if (callbackList == null) { + return; + } + if (!callbackList.unregister(listener)) { + Log.e(TAG, + "unregisterCallNotificationEventListener listener not found for: " + + packageName + " " + userHandle + " " + listener); + } + } + } + + /** * Register a listener binder directly with the notification manager. * * Only works with system callers. Apps should extend @@ -6309,6 +6405,10 @@ public class NotificationManagerService extends SystemService { Objects.requireNonNull(user); verifyPrivilegedListener(token, user, false); + + final NotificationChannel originalChannel = mPreferencesHelper.getNotificationChannel( + pkg, getUidForPackageAndUser(pkg, user), channel.getId(), true); + verifyPrivilegedListenerUriPermission(Binder.getCallingUid(), channel, originalChannel); updateNotificationChannelInt(pkg, getUidForPackageAndUser(pkg, user), channel, true); } @@ -6406,6 +6506,24 @@ public class NotificationManagerService extends SystemService { } } + private void verifyPrivilegedListenerUriPermission(int sourceUid, + @NonNull NotificationChannel updateChannel, + @Nullable NotificationChannel originalChannel) { + // Check that the NLS has the required permissions to access the channel + final Uri soundUri = updateChannel.getSound(); + final Uri originalSoundUri = + (originalChannel != null) ? originalChannel.getSound() : null; + if (soundUri != null && !Objects.equals(originalSoundUri, soundUri)) { + Binder.withCleanCallingIdentity(() -> { + mUgmInternal.checkGrantUriPermission(sourceUid, null, + ContentProvider.getUriWithoutUserId(soundUri), + Intent.FLAG_GRANT_READ_URI_PERMISSION, + ContentProvider.getUserIdFromUri(soundUri, + UserHandle.getUserId(sourceUid))); + }); + } + } + private int getUidForPackageAndUser(String pkg, UserHandle user) throws RemoteException { int uid = INVALID_UID; final long identity = Binder.clearCallingIdentity(); @@ -8676,6 +8794,11 @@ public class NotificationManagerService extends SystemService { } }); } + + if (callstyleCallbackApi()) { + notifyCallNotificationEventListenerOnRemoved(r); + } + // ATTENTION: in a future release we will bail out here // so that we do not play sounds, show lights, etc. for invalid // notifications @@ -10033,6 +10156,9 @@ public class NotificationManagerService extends SystemService { mGroupHelper.onNotificationRemoved(r.getSbn()); } }); + if (callstyleCallbackApi()) { + notifyCallNotificationEventListenerOnRemoved(r); + } } if (Flags.refactorAttentionHelper()) { @@ -11707,6 +11833,10 @@ public class NotificationManagerService extends SystemService { mNotificationRecordLogger.logNotificationPosted(report); } }); + + if (callstyleCallbackApi()) { + notifyCallNotificationEventListenerOnPosted(r); + } } @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR) @@ -12763,6 +12893,91 @@ public class NotificationManagerService extends SystemService { } } + @GuardedBy("mNotificationLock") + private void broadcastToCallNotificationEventCallbacks( + final RemoteCallbackList<ICallNotificationEventCallback> callbackList, + final NotificationRecord r, + boolean isPosted) { + if (callbackList != null) { + int numCallbacks = callbackList.beginBroadcast(); + try { + for (int i = 0; i < numCallbacks; i++) { + if (isPosted) { + callbackList.getBroadcastItem(i) + .onCallNotificationPosted(r.getSbn().getPackageName(), r.getUser()); + } else { + callbackList.getBroadcastItem(i) + .onCallNotificationRemoved(r.getSbn().getPackageName(), + r.getUser()); + } + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } + callbackList.finishBroadcast(); + } + } + + @GuardedBy("mNotificationLock") + void notifyCallNotificationEventListenerOnPosted(final NotificationRecord r) { + if (!r.getNotification().isStyle(Notification.CallStyle.class)) { + return; + } + + synchronized (mCallNotificationEventCallbacks) { + ArrayMap<Integer, RemoteCallbackList<ICallNotificationEventCallback>> + callbacksForPackage = + mCallNotificationEventCallbacks.get(r.getSbn().getPackageName()); + if (callbacksForPackage == null) { + return; + } + + if (!r.getUser().equals(UserHandle.ALL)) { + broadcastToCallNotificationEventCallbacks( + callbacksForPackage.get(r.getUser().getIdentifier()), r, true); + // Also notify the listeners registered for USER_ALL + broadcastToCallNotificationEventCallbacks(callbacksForPackage.get(USER_ALL), r, + true); + } else { + // Notify listeners registered for any userId + for (RemoteCallbackList<ICallNotificationEventCallback> callbackList + : callbacksForPackage.values()) { + broadcastToCallNotificationEventCallbacks(callbackList, r, true); + } + } + } + } + + @GuardedBy("mNotificationLock") + void notifyCallNotificationEventListenerOnRemoved(final NotificationRecord r) { + if (!r.getNotification().isStyle(Notification.CallStyle.class)) { + return; + } + + synchronized (mCallNotificationEventCallbacks) { + ArrayMap<Integer, RemoteCallbackList<ICallNotificationEventCallback>> + callbacksForPackage = + mCallNotificationEventCallbacks.get(r.getSbn().getPackageName()); + if (callbacksForPackage == null) { + return; + } + + if (!r.getUser().equals(UserHandle.ALL)) { + broadcastToCallNotificationEventCallbacks( + callbacksForPackage.get(r.getUser().getIdentifier()), r, false); + // Also notify the listeners registered for USER_ALL + broadcastToCallNotificationEventCallbacks(callbacksForPackage.get(USER_ALL), r, + false); + } else { + // Notify listeners registered for any userId + for (RemoteCallbackList<ICallNotificationEventCallback> callbackList + : callbacksForPackage.values()) { + broadcastToCallNotificationEventCallbacks(callbackList, r, false); + } + } + } + } + // TODO (b/194833441): remove when we've fully migrated to a permission class RoleObserver implements OnRoleHoldersChangedListener { // Role name : user id : list of approved packages diff --git a/services/core/java/com/android/server/notification/ZenModeEventLogger.java b/services/core/java/com/android/server/notification/ZenModeEventLogger.java index a90efe6f54f8..b9a267f37ff9 100644 --- a/services/core/java/com/android/server/notification/ZenModeEventLogger.java +++ b/services/core/java/com/android/server/notification/ZenModeEventLogger.java @@ -26,6 +26,7 @@ import static android.service.notification.NotificationServiceProto.RULE_TYPE_UN import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.app.Flags; import android.app.NotificationManager; import android.content.pm.PackageManager; @@ -33,6 +34,7 @@ import android.os.Process; import android.service.notification.DNDPolicyProto; import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeConfig.ConfigChangeOrigin; +import android.service.notification.ZenModeConfig.ZenRule; import android.service.notification.ZenModeDiff; import android.service.notification.ZenPolicy; import android.util.ArrayMap; @@ -46,6 +48,9 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.util.FrameworkStatsLog; import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Objects; /** @@ -58,6 +63,9 @@ class ZenModeEventLogger { // Placeholder int for unknown zen mode, to distinguish from "off". static final int ZEN_MODE_UNKNOWN = -1; + // Special rule type for manual rule. Keep in sync with ActiveRuleType in dnd_enums.proto. + protected static final int ACTIVE_RULE_TYPE_MANUAL = 999; + // Object for tracking config changes and policy changes associated with an overall zen // mode change. ZenModeEventLogger.ZenStateChanges mChangeState = new ZenModeEventLogger.ZenStateChanges(); @@ -192,7 +200,8 @@ class ZenModeEventLogger { /* bool user_action = 6 */ mChangeState.getIsUserAction(), /* int32 package_uid = 7 */ mChangeState.getPackageUid(), /* DNDPolicyProto current_policy = 8 */ mChangeState.getDNDPolicyProto(), - /* bool are_channels_bypassing = 9 */ mChangeState.getAreChannelsBypassing()); + /* bool are_channels_bypassing = 9 */ mChangeState.getAreChannelsBypassing(), + /* ActiveRuleType active_rule_types = 10 */ mChangeState.getActiveRuleTypes()); } /** @@ -371,35 +380,45 @@ class ZenModeEventLogger { } /** - * Get the number of active rules represented in a zen mode config. Because this is based - * on a config, this does not take into account the zen mode at the time of the config, - * which means callers need to take the zen mode into account for whether the rules are - * actually active. + * Get a list of the active rules in the provided config. This is a helper function for + * other methods that then use this information to get the number and type of active + * rules available. */ - int numActiveRulesInConfig(ZenModeConfig config) { - // If the config is null, return early + @SuppressLint("WrongConstant") // special case for log-only type on manual rule + @NonNull List<ZenRule> activeRulesList(ZenModeConfig config) { + ArrayList<ZenRule> rules = new ArrayList<>(); if (config == null) { - return 0; + return rules; } - int rules = 0; - // Loop through the config and check: - // - does a manual rule exist? (if it's non-null, it's active) - // - how many automatic rules are active, as defined by isAutomaticActive()? if (config.manualRule != null) { - rules++; + // If the manual rule is non-null, then it's active. We make a copy and set the rule + // type so that the correct value gets logged. + ZenRule rule = config.manualRule.copy(); + rule.type = ACTIVE_RULE_TYPE_MANUAL; + rules.add(rule); } if (config.automaticRules != null) { for (ZenModeConfig.ZenRule rule : config.automaticRules.values()) { if (rule != null && rule.isAutomaticActive()) { - rules++; + rules.add(rule); } } } return rules; } + /** + * Get the number of active rules represented in a zen mode config. Because this is based + * on a config, this does not take into account the zen mode at the time of the config, + * which means callers need to take the zen mode into account for whether the rules are + * actually active. + */ + int numActiveRulesInConfig(ZenModeConfig config) { + return activeRulesList(config).size(); + } + // Determine the number of (automatic & manual) rules active after the change takes place. int getNumRulesActive() { if (!Flags.modesApi()) { @@ -412,6 +431,34 @@ class ZenModeEventLogger { } /** + * Return a list of the types of each of the active rules in the configuration. + * Only available when {@code MODES_API} is active; otherwise returns an empty list. + */ + int[] getActiveRuleTypes() { + if (!Flags.modesApi() || mNewZenMode == ZEN_MODE_OFF) { + return new int[0]; + } + + ArrayList<Integer> activeTypes = new ArrayList<>(); + List<ZenRule> activeRules = activeRulesList(mNewConfig); + if (activeRules.size() == 0) { + return new int[0]; + } + + for (ZenRule rule : activeRules) { + activeTypes.add(rule.type); + } + + // Sort the list of active types to have a consistent order in the atom + Collections.sort(activeTypes); + int[] out = new int[activeTypes.size()]; + for (int i = 0; i < activeTypes.size(); i++) { + out[i] = activeTypes.get(i); + } + return out; + } + + /** * Return our best guess as to whether the changes observed are due to a user action. * Note that this (before {@code MODES_API}) won't be 100% accurate as we can't necessarily * distinguish between a system uid call indicating "user interacted with Settings" vs "a diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 1c20b2df4283..54de1976edb6 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -16,6 +16,7 @@ package com.android.server.notification; +import static android.app.AutomaticZenRule.TYPE_UNKNOWN; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ACTIVATED; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_DEACTIVATED; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_DISABLED; @@ -1264,7 +1265,7 @@ public class ZenModeHelper { : new ZenDeviceEffects.Builder().build(); if (isFromApp) { - // Don't allow apps to toggle hidden effects. + // Don't allow apps to toggle hidden (non-public-API) effects. newEffects = new ZenDeviceEffects.Builder(newEffects) .setShouldDisableAutoBrightness(oldEffects.shouldDisableAutoBrightness()) .setShouldDisableTapToWake(oldEffects.shouldDisableTapToWake()) @@ -1272,6 +1273,7 @@ public class ZenModeHelper { .setShouldDisableTouch(oldEffects.shouldDisableTouch()) .setShouldMinimizeRadioUsage(oldEffects.shouldMinimizeRadioUsage()) .setShouldMaximizeDoze(oldEffects.shouldMaximizeDoze()) + .setExtraEffects(oldEffects.getExtraEffects()) .build(); } @@ -1311,6 +1313,9 @@ public class ZenModeHelper { if (oldEffects.shouldMaximizeDoze() != newEffects.shouldMaximizeDoze()) { userModifiedFields |= ZenDeviceEffects.FIELD_MAXIMIZE_DOZE; } + if (!Objects.equals(oldEffects.getExtraEffects(), newEffects.getExtraEffects())) { + userModifiedFields |= ZenDeviceEffects.FIELD_EXTRA_EFFECTS; + } zenRule.zenDeviceEffectsUserModifiedFields = userModifiedFields; } } @@ -2166,7 +2171,8 @@ public class ZenModeHelper { /* optional DNDPolicyProto policy = 7 */ config.toZenPolicy().toProto(), /* optional int32 rule_modified_fields = 8 */ 0, /* optional int32 policy_modified_fields = 9 */ 0, - /* optional int32 device_effects_modified_fields = 10 */ 0)); + /* optional int32 device_effects_modified_fields = 10 */ 0, + /* optional ActiveRuleType rule_type = 11 */ TYPE_UNKNOWN)); if (config.manualRule != null) { ruleToProtoLocked(user, config.manualRule, true, events); } @@ -2192,8 +2198,10 @@ public class ZenModeHelper { pkg = rule.enabler; } + int ruleType = rule.type; if (isManualRule) { id = ZenModeConfig.MANUAL_RULE_ID; + ruleType = ZenModeEventLogger.ACTIVE_RULE_TYPE_MANUAL; } SysUiStatsEvent.Builder data; @@ -2212,7 +2220,8 @@ public class ZenModeHelper { /* optional int32 rule_modified_fields = 8 */ rule.userModifiedFields, /* optional int32 policy_modified_fields = 9 */ rule.zenPolicyUserModifiedFields, /* optional int32 device_effects_modified_fields = 10 */ - rule.zenDeviceEffectsUserModifiedFields)); + rule.zenDeviceEffectsUserModifiedFields, + /* optional ActiveRuleType rule_type = 11 */ ruleType)); } private int getPackageUid(String pkg, int user) { diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java index b9464d96a019..872952299055 100644 --- a/services/core/java/com/android/server/om/OverlayManagerService.java +++ b/services/core/java/com/android/server/om/OverlayManagerService.java @@ -362,7 +362,7 @@ public final class OverlayManagerService extends SystemService { defaultPackages.add(packageName); } } - return defaultPackages.toArray(new String[defaultPackages.size()]); + return defaultPackages.toArray(new String[0]); } private final class OverlayManagerPackageMonitor extends PackageMonitor { @@ -1160,7 +1160,7 @@ public final class OverlayManagerService extends SystemService { // state may lead to contradictions within OMS. Better then to lag // behind until all pending intents have been processed. private final ArrayMap<String, PackageStateUsers> mCache = new ArrayMap<>(); - private final Set<Integer> mInitializedUsers = new ArraySet<>(); + private final ArraySet<Integer> mInitializedUsers = new ArraySet<>(); PackageManagerHelperImpl(Context context) { mContext = context; @@ -1176,8 +1176,7 @@ public final class OverlayManagerService extends SystemService { */ @NonNull public ArrayMap<String, PackageState> initializeForUser(final int userId) { - if (!mInitializedUsers.contains(userId)) { - mInitializedUsers.add(userId); + if (mInitializedUsers.add(userId)) { mPackageManagerInternal.forEachPackageState((packageState -> { if (packageState.getPkg() != null && packageState.getUserStateOrDefault(userId).isInstalled()) { @@ -1545,8 +1544,7 @@ public final class OverlayManagerService extends SystemService { final OverlayPaths frameworkOverlays = mImpl.getEnabledOverlayPaths("android", userId, false); for (final String targetPackageName : targetPackageNames) { - final OverlayPaths.Builder list = new OverlayPaths.Builder(); - list.addAll(frameworkOverlays); + final var list = new OverlayPaths.Builder(frameworkOverlays); if (!"android".equals(targetPackageName)) { list.addAll(mImpl.getEnabledOverlayPaths(targetPackageName, userId, true)); } @@ -1558,17 +1556,21 @@ public final class OverlayManagerService extends SystemService { final HashSet<String> invalidPackages = new HashSet<>(); pm.setEnabledOverlayPackages(userId, pendingChanges, updatedPackages, invalidPackages); - for (final String targetPackageName : targetPackageNames) { - if (DEBUG) { - Slog.d(TAG, "-> Updating overlay: target=" + targetPackageName + " overlays=[" - + pendingChanges.get(targetPackageName) - + "] userId=" + userId); - } + if (DEBUG || !invalidPackages.isEmpty()) { + for (final String targetPackageName : targetPackageNames) { + if (DEBUG) { + Slog.d(TAG, + "-> Updating overlay: target=" + targetPackageName + " overlays=[" + + pendingChanges.get(targetPackageName) + + "] userId=" + userId); + } - if (invalidPackages.contains(targetPackageName)) { - Slog.e(TAG, TextUtils.formatSimple( - "Failed to change enabled overlays for %s user %d", targetPackageName, - userId)); + if (invalidPackages.contains(targetPackageName)) { + Slog.e(TAG, TextUtils.formatSimple( + "Failed to change enabled overlays for %s user %d", + targetPackageName, + userId)); + } } } return new ArrayList<>(updatedPackages); diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java index 972c78db9460..c1b6ccc7e25c 100644 --- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java +++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java @@ -772,24 +772,20 @@ final class OverlayManagerServiceImpl { OverlayPaths getEnabledOverlayPaths(@NonNull final String targetPackageName, final int userId, boolean includeImmutableOverlays) { - final List<OverlayInfo> overlays = mSettings.getOverlaysForTarget(targetPackageName, - userId); - final OverlayPaths.Builder paths = new OverlayPaths.Builder(); - final int n = overlays.size(); - for (int i = 0; i < n; i++) { - final OverlayInfo oi = overlays.get(i); + final var paths = new OverlayPaths.Builder(); + mSettings.forEachMatching(userId, null, targetPackageName, oi -> { if (!oi.isEnabled()) { - continue; + return; } if (!includeImmutableOverlays && !oi.isMutable) { - continue; + return; } if (oi.isFabricated()) { paths.addNonApkPath(oi.baseCodePath); } else { paths.addApkPath(oi.baseCodePath); } - } + }); return paths.build(); } diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java index eae614ac9e77..b8b49f3eed2f 100644 --- a/services/core/java/com/android/server/om/OverlayManagerSettings.java +++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java @@ -47,6 +47,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Stream; @@ -182,6 +183,23 @@ final class OverlayManagerSettings { return CollectionUtils.map(items, SettingsItem::getOverlayInfo); } + void forEachMatching(int userId, String overlayName, String targetPackageName, + @NonNull Consumer<OverlayInfo> consumer) { + for (int i = 0, n = mItems.size(); i < n; i++) { + final SettingsItem item = mItems.get(i); + if (item.getUserId() != userId) { + continue; + } + if (overlayName != null && !item.mOverlay.getPackageName().equals(overlayName)) { + continue; + } + if (targetPackageName != null && !item.mTargetPackageName.equals(targetPackageName)) { + continue; + } + consumer.accept(item.getOverlayInfo()); + } + } + ArrayMap<String, List<OverlayInfo>> getOverlaysForUser(final int userId) { final List<SettingsItem> items = selectWhereUser(userId); diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index db5acc2eda09..a1dac0456ff9 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -4191,7 +4191,7 @@ final class InstallPackageHelper { + "; old: " + pkgSetting.getPathString() + " @ " + pkgSetting.getVersionCode() + "; new: " + parsedPackage.getPath() + " @ " - + parsedPackage.getPath()); + + parsedPackage.getLongVersionCode()); } } diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 984a6299cc81..295528e14ca7 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -38,6 +38,7 @@ import static android.content.pm.LauncherApps.FLAG_CACHE_PEOPLE_TILE_SHORTCUTS; import static com.android.server.pm.PackageArchiver.isArchivingEnabled; +import android.Manifest; import android.annotation.AppIdInt; import android.annotation.NonNull; import android.annotation.Nullable; @@ -52,6 +53,7 @@ import android.app.IApplicationThread; import android.app.PendingIntent; import android.app.admin.DevicePolicyCache; import android.app.admin.DevicePolicyManager; +import android.app.role.RoleManager; import android.app.usage.UsageStatsManagerInternal; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; @@ -84,7 +86,9 @@ import android.content.pm.ShortcutQueryWrapper; import android.content.pm.ShortcutServiceInternal; import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener; import android.content.pm.UserInfo; +import android.content.pm.UserProperties; import android.graphics.Rect; +import android.multiuser.Flags; import android.net.Uri; import android.os.Binder; import android.os.Bundle; @@ -211,6 +215,7 @@ public class LauncherAppsService extends SystemService { private final Context mContext; private final UserManager mUm; + private final RoleManager mRoleManager; private final IPackageManager mIPM; private final UserManagerInternal mUserManagerInternal; private final UsageStatsManagerInternal mUsageStatsManagerInternal; @@ -247,6 +252,7 @@ public class LauncherAppsService extends SystemService { mContext = context; mIPM = AppGlobals.getPackageManager(); mUm = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + mRoleManager = mContext.getSystemService(RoleManager.class); mUserManagerInternal = Objects.requireNonNull( LocalServices.getService(UserManagerInternal.class)); mUsageStatsManagerInternal = Objects.requireNonNull( @@ -451,7 +457,6 @@ public class LauncherAppsService extends SystemService { private boolean canAccessProfile(int callingUid, int callingUserId, int callingPid, int targetUserId, String message) { - if (targetUserId == callingUserId) return true; if (injectHasInteractAcrossUsersFullPermission(callingPid, callingUid)) { return true; @@ -465,6 +470,14 @@ public class LauncherAppsService extends SystemService { + targetUserId + " from " + callingUserId + " not allowed"); return false; } + + if (areHiddenApisChecksEnabled() + && mUm.getUserProperties(UserHandle.of(targetUserId)) + .getProfileApiVisibility() + == UserProperties.PROFILE_API_VISIBILITY_HIDDEN + && !canAccessHiddenProfileInjected(callingUid, callingPid)) { + return false; + } } finally { injectRestoreCallingIdentity(ident); } @@ -473,10 +486,43 @@ public class LauncherAppsService extends SystemService { message, true); } + boolean areHiddenApisChecksEnabled() { + return android.os.Flags.allowPrivateProfile() + && Flags.enableLauncherAppsHiddenProfileChecks() + && Flags.enablePermissionToAccessHiddenProfiles(); + } + private void verifyCallingPackage(String callingPackage) { verifyCallingPackage(callingPackage, injectBinderCallingUid()); } + boolean canAccessHiddenProfileInjected(int callingUid, int callingPid) { + AndroidPackage callingPackage = mPackageManagerInternal.getPackage(callingUid); + if (callingPackage == null) { + return false; + } + + if (!mRoleManager + .getRoleHoldersAsUser( + RoleManager.ROLE_HOME, UserHandle.getUserHandleForUid(callingUid)) + .contains(callingPackage.getPackageName())) { + return false; + } + + if (mContext.checkPermission( + Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL, callingPid, callingUid) + == PackageManager.PERMISSION_GRANTED) { + return true; + } + + // TODO(b/321988638): add option to disable with a flag + return mContext.checkPermission( + android.Manifest.permission.ACCESS_HIDDEN_PROFILES, + callingPid, + callingUid) + == PackageManager.PERMISSION_GRANTED; + } + @VisibleForTesting // We override it in unit tests void verifyCallingPackage(String callingPackage, int callerUid) { int packageUid = -1; @@ -1566,11 +1612,6 @@ public class LauncherAppsService extends SystemService { @Override public @Nullable LauncherUserInfo getLauncherUserInfo(@NonNull UserHandle user) { - // Only system launchers, which have access to recents should have access to this API. - // TODO(b/303803157): Add the new permission check if we decide to have one. - if (!mActivityTaskManagerInternal.isCallerRecents(Binder.getCallingUid())) { - throw new SecurityException("Caller is not the recents app"); - } if (!canAccessProfile(user.getIdentifier(), "Can't access LauncherUserInfo for another user")) { return null; @@ -1585,11 +1626,6 @@ public class LauncherAppsService extends SystemService { @Override public List<String> getPreInstalledSystemPackages(UserHandle user) { - // Only system launchers, which have access to recents should have access to this API. - // TODO(b/303803157): Update access control for this API to default Launcher app. - if (!mActivityTaskManagerInternal.isCallerRecents(Binder.getCallingUid())) { - throw new SecurityException("Caller is not the recents app"); - } if (!canAccessProfile(user.getIdentifier(), "Can't access preinstalled packages for another user")) { return null; @@ -1610,11 +1646,6 @@ public class LauncherAppsService extends SystemService { @Override public @Nullable IntentSender getAppMarketActivityIntent(@NonNull String callingPackage, @Nullable String packageName, @NonNull UserHandle user) { - // Only system launchers, which have access to recents should have access to this API. - // TODO(b/303803157): Update access control for this API to default Launcher app. - if (!mActivityTaskManagerInternal.isCallerRecents(Binder.getCallingUid())) { - throw new SecurityException("Caller is not the recents app"); - } if (!canAccessProfile(user.getIdentifier(), "Can't access AppMarketActivity for another user")) { return null; diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 796edde7d9bd..f222fe9add0f 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1466,7 +1466,7 @@ public class UserManagerService extends IUserManager.Stub { if (userType != null && !userType.equals(profile.userType)) { continue; } - if (excludeHidden && isProfileHidden(userId)) { + if (excludeHidden && isProfileHidden(profile.id)) { continue; } result.add(profile.id); diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java index c94c49d68e75..a9d2858fd36e 100644 --- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java +++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java @@ -154,7 +154,8 @@ public class UserRestrictionsUtils { UserManager.DISALLOW_ULTRA_WIDEBAND_RADIO, UserManager.DISALLOW_CONFIG_DEFAULT_APPS, UserManager.DISALLOW_NEAR_FIELD_COMMUNICATION_RADIO, - UserManager.DISALLOW_SIM_GLOBALLY + UserManager.DISALLOW_SIM_GLOBALLY, + UserManager.DISALLOW_ASSIST_CONTENT }); public static final Set<String> DEPRECATED_USER_RESTRICTIONS = Sets.newArraySet( @@ -230,7 +231,8 @@ public class UserRestrictionsUtils { UserManager.DISALLOW_RUN_IN_BACKGROUND, UserManager.DISALLOW_UNMUTE_MICROPHONE, UserManager.DISALLOW_UNMUTE_DEVICE, - UserManager.DISALLOW_CAMERA + UserManager.DISALLOW_CAMERA, + UserManager.DISALLOW_ASSIST_CONTENT ); /** diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 25e4116a4507..bc260184e487 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -4057,15 +4057,17 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_SYSRQ: if (down && repeatCount == 0) { interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/); + return true; } - return true; + break; case KeyEvent.KEYCODE_ESCAPE: if (down && KeyEvent.metaStateHasNoModifiers(metaState) && repeatCount == 0) { mContext.closeSystemDialogs(); + return true; } - return true; + break; case KeyEvent.KEYCODE_STEM_PRIMARY: handleUnhandledSystemKey(event); sendSystemKeyToStatusBarAsync(event); 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/security/KeyAttestationApplicationIdProviderService.java b/services/core/java/com/android/server/security/KeyAttestationApplicationIdProviderService.java index d5bc91278aa8..b69ccb348fa7 100644 --- a/services/core/java/com/android/server/security/KeyAttestationApplicationIdProviderService.java +++ b/services/core/java/com/android/server/security/KeyAttestationApplicationIdProviderService.java @@ -14,7 +14,6 @@ * limitations under the License. */ - package com.android.server.security; import android.content.Context; @@ -23,6 +22,7 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Binder; import android.os.RemoteException; +import android.os.ServiceSpecificException; import android.os.UserHandle; import android.security.keystore.IKeyAttestationApplicationIdProvider; import android.security.keystore.KeyAttestationApplicationId; @@ -57,7 +57,10 @@ public class KeyAttestationApplicationIdProviderService try { String[] packageNames = mPackageManager.getPackagesForUid(uid); if (packageNames == null) { - throw new RemoteException("No packages for uid"); + throw new ServiceSpecificException( + IKeyAttestationApplicationIdProvider + .ERROR_GET_ATTESTATION_APPLICATION_ID_FAILED, + "No package for uid: " + uid); } int userId = UserHandle.getUserId(uid); keyAttestationPackageInfos = new KeyAttestationPackageInfo[packageNames.length]; diff --git a/services/core/java/com/android/server/selinux/OWNERS b/services/core/java/com/android/server/selinux/OWNERS new file mode 100644 index 000000000000..6ca4da2fd740 --- /dev/null +++ b/services/core/java/com/android/server/selinux/OWNERS @@ -0,0 +1,3 @@ +# Bug component: 1117393 + +sandrom@google.com 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/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java index f4fb1a108663..1bc635bd9ca3 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java @@ -17,6 +17,7 @@ package com.android.server.webkit; import android.annotation.Nullable; import android.content.Context; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.Signature; @@ -31,8 +32,12 @@ import android.webkit.WebViewFactory; import android.webkit.WebViewProviderInfo; import android.webkit.WebViewProviderResponse; +import com.android.server.LocalServices; +import com.android.server.PinnerService; + import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -83,6 +88,8 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { private static final int VALIDITY_INCORRECT_SIGNATURE = 3; private static final int VALIDITY_NO_LIBRARY_FLAG = 4; + private static final String PIN_GROUP = "webview"; + private final SystemInterface mSystemInterface; private final Context mContext; @@ -349,6 +356,39 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { return newPackage; } + private void pinWebviewIfRequired(ApplicationInfo appInfo) { + PinnerService pinnerService = LocalServices.getService(PinnerService.class); + if (pinnerService == null) { + // This happens in unit tests which do not have services. + return; + } + int webviewPinQuota = pinnerService.getWebviewPinQuota(); + if (webviewPinQuota <= 0) { + return; + } + + pinnerService.unpinGroup(PIN_GROUP); + + ArrayList<String> apksToPin = new ArrayList<>(); + boolean pinSharedFirst = appInfo.metaData.getBoolean("PIN_SHARED_LIBS_FIRST", true); + for (String sharedLib : appInfo.sharedLibraryFiles) { + apksToPin.add(sharedLib); + } + apksToPin.add(appInfo.sourceDir); + if (!pinSharedFirst) { + // We want to prioritize pinning of the native library that is most likely used by apps + // which in some build flavors live in the main apk and as a shared library for others. + Collections.reverse(apksToPin); + } + for (String apk : apksToPin) { + if (webviewPinQuota <= 0) { + break; + } + int bytesPinned = pinnerService.pinFile(apk, webviewPinQuota, appInfo, PIN_GROUP); + webviewPinQuota -= bytesPinned; + } + } + /** * This is called when we change WebView provider, either when the current provider is * updated or a new provider is chosen / takes precedence. @@ -357,6 +397,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { synchronized (mLock) { mAnyWebViewInstalled = true; if (mNumRelroCreationsStarted == mNumRelroCreationsFinished) { + pinWebviewIfRequired(newPackage.applicationInfo); mCurrentWebViewPackage = newPackage; // The relro creations might 'finish' (not start at all) before diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 640c9dc21a53..dcc68a7e3190 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -3522,10 +3522,15 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { if (displayContent == null) { return false; } - hasRestrictedWindow = displayContent.forAllWindows(windowState -> { - return windowState.isOnScreen() && UserManager.isUserTypePrivateProfile( - getUserManager().getProfileType(windowState.mShowUserId)); - }, true /* traverseTopToBottom */); + final long callingIdentity = Binder.clearCallingIdentity(); + try { + hasRestrictedWindow = displayContent.forAllWindows(windowState -> { + return windowState.isOnScreen() && UserManager.isUserTypePrivateProfile( + getUserManager().getProfileType(windowState.mShowUserId)); + }, true /* traverseTopToBottom */); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } } return DevicePolicyCache.getInstance().isScreenCaptureAllowed(userId) && !hasRestrictedWindow; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 85eac29599f5..0036f498972e 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -48,6 +48,7 @@ import static android.Manifest.permission.MANAGE_DEVICE_POLICY_LOCATION; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_LOCK; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_LOCK_TASK; +import static android.Manifest.permission.MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_MICROPHONE; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_MOBILE_NETWORK; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_MODIFY_USERS; @@ -76,6 +77,7 @@ import static android.Manifest.permission.MANAGE_DEVICE_POLICY_THREAD_NETWORK; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_TIME; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_USB_DATA_SIGNALLING; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_USB_FILE_TRANSFER; +import static android.Manifest.permission.MANAGE_DEVICE_POLICY_ASSIST_CONTENT; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_VPN; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_WALLPAPER; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_WIFI; @@ -229,6 +231,7 @@ 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.app.admin.flags.Flags.assistContentUserRestrictionEnabled; import static android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE; import static android.content.Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; @@ -446,6 +449,7 @@ import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.ParcelableKeyGenParameterSpec; import android.stats.devicepolicy.DevicePolicyEnums; import android.telecom.TelecomManager; +import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.telephony.data.ApnSetting; @@ -13372,6 +13376,11 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { UserManager.DISALLOW_THREAD_NETWORK, new String[]{MANAGE_DEVICE_POLICY_THREAD_NETWORK}); } + if (assistContentUserRestrictionEnabled()) { + USER_RESTRICTION_PERMISSIONS.put( + UserManager.DISALLOW_ASSIST_CONTENT, + new String[]{MANAGE_DEVICE_POLICY_ASSIST_CONTENT}); + } USER_RESTRICTION_PERMISSIONS.put( UserManager.DISALLOW_ULTRA_WIDEBAND_RADIO, new String[]{MANAGE_DEVICE_POLICY_NEARBY_COMMUNICATION}); USER_RESTRICTION_PERMISSIONS.put( @@ -22330,6 +22339,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_CAMERA, MANAGE_DEVICE_POLICY_CERTIFICATES, MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE, + MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES, MANAGE_DEVICE_POLICY_DEFAULT_SMS, MANAGE_DEVICE_POLICY_DISPLAY, @@ -22343,6 +22353,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_LOCK, MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS, MANAGE_DEVICE_POLICY_LOCK_TASK, + MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS, MANAGE_DEVICE_POLICY_MICROPHONE, MANAGE_DEVICE_POLICY_MOBILE_NETWORK, MANAGE_DEVICE_POLICY_MODIFY_USERS, @@ -22413,6 +22424,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_CALLS, MANAGE_DEVICE_POLICY_CAMERA, MANAGE_DEVICE_POLICY_CERTIFICATES, + MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES, MANAGE_DEVICE_POLICY_DISPLAY, MANAGE_DEVICE_POLICY_FACTORY_RESET, @@ -22423,7 +22435,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_LOCATION, MANAGE_DEVICE_POLICY_LOCK, MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS, - MANAGE_DEVICE_POLICY_CERTIFICATES, + MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS, MANAGE_DEVICE_POLICY_NEARBY_COMMUNICATION, MANAGE_DEVICE_POLICY_ORGANIZATION_IDENTITY, MANAGE_DEVICE_POLICY_PACKAGE_STATE, @@ -23228,38 +23240,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } - private EnforcingAdmin enforceCanCallContentProtectionLocked( - ComponentName who, String callerPackageName) { - CallerIdentity caller = getCallerIdentity(who, callerPackageName); - final int userId = caller.getUserId(); - - EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( - who, - MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, - caller.getPackageName(), - userId - ); - if ((isDeviceOwner(caller) || isProfileOwner(caller)) - && !canDPCManagedUserUseLockTaskLocked(userId)) { - throw new SecurityException( - "User " + userId + " is not allowed to use content protection"); - } - return enforcingAdmin; - } - - private void enforceCanQueryContentProtectionLocked( - ComponentName who, String callerPackageName) { - CallerIdentity caller = getCallerIdentity(who, callerPackageName); - final int userId = caller.getUserId(); - - enforceCanQuery(MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, caller.getPackageName(), userId); - if ((isDeviceOwner(caller) || isProfileOwner(caller)) - && !canDPCManagedUserUseLockTaskLocked(userId)) { - throw new SecurityException( - "User " + userId + " is not allowed to use content protection"); - } - } - @Override public void setContentProtectionPolicy( ComponentName who, String callerPackageName, @ContentProtectionPolicy int policy) @@ -23269,24 +23249,21 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } CallerIdentity caller = getCallerIdentity(who, callerPackageName); + int userId = caller.getUserId(); checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_SET_CONTENT_PROTECTION_POLICY); - - EnforcingAdmin enforcingAdmin; - synchronized (getLockObject()) { - enforcingAdmin = enforceCanCallContentProtectionLocked(who, caller.getPackageName()); - } + EnforcingAdmin enforcingAdmin = + enforcePermissionAndGetEnforcingAdmin( + who, MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, callerPackageName, userId); if (policy == CONTENT_PROTECTION_DISABLED) { mDevicePolicyEngine.removeLocalPolicy( - PolicyDefinition.CONTENT_PROTECTION, - enforcingAdmin, - caller.getUserId()); + PolicyDefinition.CONTENT_PROTECTION, enforcingAdmin, userId); } else { mDevicePolicyEngine.setLocalPolicy( PolicyDefinition.CONTENT_PROTECTION, enforcingAdmin, new IntegerPolicyValue(policy), - caller.getUserId()); + userId); } } @@ -23298,13 +23275,11 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } CallerIdentity caller = getCallerIdentity(who, callerPackageName); - final int userHandle = caller.getUserId(); + int userId = caller.getUserId(); + enforceCanQuery(MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, callerPackageName, userId); - synchronized (getLockObject()) { - enforceCanQueryContentProtectionLocked(who, caller.getPackageName()); - } - Integer policy = mDevicePolicyEngine.getResolvedPolicy( - PolicyDefinition.CONTENT_PROTECTION, userHandle); + Integer policy = + mDevicePolicyEngine.getResolvedPolicy(PolicyDefinition.CONTENT_PROTECTION, userId); if (policy == null) { return CONTENT_PROTECTION_DISABLED; } else { @@ -24012,4 +23987,32 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { Slogf.d(LOG_TAG, "Unable to get stacktrace"); } } + + @Override + public int[] getSubscriptionIds(String callerPackageName) { + final CallerIdentity caller = getCallerIdentity(callerPackageName); + enforceCanQuery( + MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS, + caller.getPackageName(), + caller.getUserId()); + return getSubscriptionIdsInternal(callerPackageName).toArray(); + } + + private IntArray getSubscriptionIdsInternal(String callerPackageName) { + SubscriptionManager subscriptionManager = + mContext.getSystemService(SubscriptionManager.class); + return mInjector.binderWithCleanCallingIdentity(() -> { + IntArray adminOwnedSubscriptions = new IntArray(); + List<SubscriptionInfo> subs = subscriptionManager.getAvailableSubscriptionInfoList(); + int subCount = (subs != null) ? subs.size() : 0; + for (int i = 0; i < subCount; i++) { + SubscriptionInfo sub = subs.get(i); + if (sub.getGroupOwner() + .equals(callerPackageName)) { + adminOwnedSubscriptions.add(sub.getSubscriptionId()); + } + } + return adminOwnedSubscriptions; + }); + } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java index 5996e422a2d4..b09908e608ba 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java @@ -487,6 +487,7 @@ final class PolicyDefinition<V> { USER_RESTRICTION_FLAGS.put( UserManager.DISALLOW_SIM_GLOBALLY, POLICY_FLAG_GLOBAL_ONLY_POLICY); + USER_RESTRICTION_FLAGS.put(UserManager.DISALLOW_ASSIST_CONTENT, /* flags= */ 0); for (String key : USER_RESTRICTION_FLAGS.keySet()) { createAndAddUserRestrictionPolicyDefinition(key, USER_RESTRICTION_FLAGS.get(key)); 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/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt index 2f5c1092e67b..67f66de71d39 100644 --- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt +++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt @@ -2958,7 +2958,7 @@ class PermissionService(private val service: AccessCheckingService) : /** These permissions are supported for virtual devices. */ // TODO: b/298661870 - Use new API to get the list of device aware permissions. val DEVICE_AWARE_PERMISSIONS = - if (Flags.deviceAwarePermissionApisEnabled()) { + if (Flags.deviceAwarePermissionsEnabled()) { setOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) } else { emptySet<String>() diff --git a/services/tests/mockingservicestests/src/com/android/server/OWNERS b/services/tests/mockingservicestests/src/com/android/server/OWNERS index c0f0ce047da6..b363f545760e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/OWNERS +++ b/services/tests/mockingservicestests/src/com/android/server/OWNERS @@ -1,3 +1,4 @@ per-file *Alarm* = file:/apex/jobscheduler/OWNERS per-file *AppStateTracker* = file:/apex/jobscheduler/OWNERS per-file *DeviceIdleController* = file:/apex/jobscheduler/OWNERS +per-file SensitiveContentProtectionManagerServiceTest.java = file:/core/java/android/permission/OWNERS diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java index 656bc71eebca..7bbcd50cdf1f 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java @@ -722,6 +722,17 @@ public final class UserManagerServiceTest { Mockito.verify(mKeyguardManager, never()).addKeyguardLockedStateListener(any(), any()); } + @Test + public void testGetProfileIdsExcludingHidden() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_HIDING_PROFILES); + UserInfo privateProfileUser = + mUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile", + USER_TYPE_PROFILE_PRIVATE, 0, 0, null); + for (int id : mUms.getProfileIdsExcludingHidden(0, true)) { + assertThat(id).isNotEqualTo(privateProfileUser.id); + } + } + /** * Returns true if the user's XML file has Default restrictions * @param userId Id of the user. diff --git a/services/tests/mockingservicestests/src/com/android/server/selinux/OWNERS b/services/tests/mockingservicestests/src/com/android/server/selinux/OWNERS new file mode 100644 index 000000000000..49a0934ec642 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/selinux/OWNERS @@ -0,0 +1 @@ +include /services/core/java/com/android/server/selinux/OWNERS 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/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java index df2069efb0ce..7f7cc35ced55 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java @@ -1729,6 +1729,21 @@ public final class UserManagerTest { mUserManager.removeUser(userInfo.id); } + @Test + @RequiresFlagsEnabled(android.multiuser.Flags.FLAG_ENABLE_HIDING_PROFILES) + public void testGetProfileIdsExcludingHidden() throws Exception { + int mainUserId = mUserManager.getMainUser().getIdentifier(); + final UserInfo profile = createProfileForUser("Profile", + UserManager.USER_TYPE_PROFILE_PRIVATE, mainUserId); + + final int[] allProfiles = mUserManager.getProfileIds(mainUserId, /* enabledOnly */ false); + final int[] profilesExcludingHidden = mUserManager.getProfileIdsExcludingHidden( + mainUserId, /* enabledOnly */ false); + + assertThat(allProfiles).asList().contains(profile.id); + assertThat(profilesExcludingHidden).asList().doesNotContain(profile.id); + } + private String generateLongString() { String partialString = "Test Name Test Name Test Name Test Name Test Name Test Name Test " + "Name Test Name Test Name Test Name "; //String of length 100 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 2f740eac626b..6aacfd706adc 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -163,6 +163,7 @@ import android.app.AlarmManager; import android.app.AppOpsManager; import android.app.AutomaticZenRule; import android.app.IActivityManager; +import android.app.ICallNotificationEventCallback; import android.app.INotificationManager; import android.app.ITransientNotification; import android.app.IUriGrantsManager; @@ -4002,6 +4003,69 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testUpdateNotificationChannelFromPrivilegedListener_noSoundUriPermission() + throws Exception { + mService.setPreferencesHelper(mPreferencesHelper); + when(mCompanionMgr.getAssociations(PKG, mUserId)) + .thenReturn(singletonList(mock(AssociationInfo.class))); + when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(), + eq(mTestNotificationChannel.getId()), anyBoolean())) + .thenReturn(mTestNotificationChannel); + + final Uri soundUri = Uri.parse("content://media/test/sound/uri"); + final NotificationChannel updatedNotificationChannel = new NotificationChannel( + TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT); + updatedNotificationChannel.setSound(soundUri, + updatedNotificationChannel.getAudioAttributes()); + + doThrow(new SecurityException("no access")).when(mUgmInternal) + .checkGrantUriPermission(eq(Process.myUid()), any(), eq(soundUri), + anyInt(), eq(Process.myUserHandle().getIdentifier())); + + assertThrows(SecurityException.class, + () -> mBinderService.updateNotificationChannelFromPrivilegedListener(null, PKG, + Process.myUserHandle(), updatedNotificationChannel)); + + verify(mPreferencesHelper, never()).updateNotificationChannel( + anyString(), anyInt(), any(), anyBoolean(), anyInt(), anyBoolean()); + + verify(mListeners, never()).notifyNotificationChannelChanged(eq(PKG), + eq(Process.myUserHandle()), eq(mTestNotificationChannel), + eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED)); + } + + @Test + public void testUpdateNotificationChannelFromPrivilegedListener_noSoundUriPermission_sameSound() + throws Exception { + mService.setPreferencesHelper(mPreferencesHelper); + when(mCompanionMgr.getAssociations(PKG, mUserId)) + .thenReturn(singletonList(mock(AssociationInfo.class))); + when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(), + eq(mTestNotificationChannel.getId()), anyBoolean())) + .thenReturn(mTestNotificationChannel); + + final Uri soundUri = Settings.System.DEFAULT_NOTIFICATION_URI; + final NotificationChannel updatedNotificationChannel = new NotificationChannel( + TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT); + updatedNotificationChannel.setSound(soundUri, + updatedNotificationChannel.getAudioAttributes()); + + doThrow(new SecurityException("no access")).when(mUgmInternal) + .checkGrantUriPermission(eq(Process.myUid()), any(), eq(soundUri), + anyInt(), eq(Process.myUserHandle().getIdentifier())); + + mBinderService.updateNotificationChannelFromPrivilegedListener( + null, PKG, Process.myUserHandle(), updatedNotificationChannel); + + verify(mPreferencesHelper, times(1)).updateNotificationChannel( + anyString(), anyInt(), any(), anyBoolean(), anyInt(), anyBoolean()); + + verify(mListeners, never()).notifyNotificationChannelChanged(eq(PKG), + eq(Process.myUserHandle()), eq(mTestNotificationChannel), + eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED)); + } + + @Test public void testGetNotificationChannelFromPrivilegedListener_cdm_success() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); when(mCompanionMgr.getAssociations(PKG, mUserId)) @@ -14529,6 +14593,120 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertFalse(mBinderService.getPrivateNotificationsAllowed()); } + @Test + @EnableFlags(android.service.notification.Flags.FLAG_CALLSTYLE_CALLBACK_API) + public void testCallNotificationListener_NotifiedOnPostCallStyle() throws Exception { + ICallNotificationEventCallback listener = mock( + ICallNotificationEventCallback.class); + when(listener.asBinder()).thenReturn(mock(IBinder.class)); + mBinderService.registerCallNotificationEventListener(PKG, UserHandle.CURRENT, listener); + waitForIdle(); + + final UserHandle userHandle = UserHandle.getUserHandleForUid(mUid); + final NotificationRecord r = createAndPostCallStyleNotification(PKG, userHandle, + "testCallNotificationListener_NotifiedOnPostCallStyle"); + + verify(listener, times(1)).onCallNotificationPosted(PKG, userHandle); + + mBinderService.cancelNotificationWithTag(PKG, PKG, r.getSbn().getTag(), r.getSbn().getId(), + r.getSbn().getUserId()); + waitForIdle(); + + verify(listener, times(1)).onCallNotificationRemoved(PKG, userHandle); + } + + @Test + @EnableFlags(android.service.notification.Flags.FLAG_CALLSTYLE_CALLBACK_API) + public void testCallNotificationListener_NotNotifiedOnPostNonCallStyle() throws Exception { + ICallNotificationEventCallback listener = mock( + ICallNotificationEventCallback.class); + when(listener.asBinder()).thenReturn(mock(IBinder.class)); + mBinderService.registerCallNotificationEventListener(PKG, + UserHandle.getUserHandleForUid(mUid), listener); + waitForIdle(); + + Notification.Builder nb = new Notification.Builder(mContext, + mTestNotificationChannel.getId()).setSmallIcon(android.R.drawable.sym_def_app_icon); + final NotificationRecord r = createAndPostNotification(nb, + "testCallNotificationListener_NotNotifiedOnPostNonCallStyle"); + + verify(listener, never()).onCallNotificationPosted(anyString(), any()); + + mBinderService.cancelNotificationWithTag(PKG, PKG, r.getSbn().getTag(), r.getSbn().getId(), + r.getSbn().getUserId()); + waitForIdle(); + + verify(listener, never()).onCallNotificationRemoved(anyString(), any()); + } + + @Test + @EnableFlags(android.service.notification.Flags.FLAG_CALLSTYLE_CALLBACK_API) + public void testCallNotificationListener_registerForUserAll_notifiedOnAnyUserId() + throws Exception { + ICallNotificationEventCallback listener = mock( + ICallNotificationEventCallback.class); + when(listener.asBinder()).thenReturn(mock(IBinder.class)); + mBinderService.registerCallNotificationEventListener(PKG, UserHandle.ALL, listener); + waitForIdle(); + + final UserHandle otherUser = UserHandle.of(2); + final NotificationRecord r = createAndPostCallStyleNotification(PKG, + otherUser, "testCallNotificationListener_registerForUserAll_notifiedOnAnyUserId"); + + verify(listener, times(1)).onCallNotificationPosted(PKG, otherUser); + + mBinderService.cancelNotificationWithTag(PKG, PKG, r.getSbn().getTag(), r.getSbn().getId(), + r.getSbn().getUserId()); + waitForIdle(); + + verify(listener, times(1)).onCallNotificationRemoved(PKG, otherUser); + } + + @Test + @EnableFlags(android.service.notification.Flags.FLAG_CALLSTYLE_CALLBACK_API) + public void testCallNotificationListener_differentPackage_notNotified() throws Exception { + final String packageName = "package"; + ICallNotificationEventCallback listener = mock( + ICallNotificationEventCallback.class); + when(listener.asBinder()).thenReturn(mock(IBinder.class)); + mBinderService.registerCallNotificationEventListener(packageName, UserHandle.ALL, listener); + waitForIdle(); + + final NotificationRecord r = createAndPostCallStyleNotification(PKG, + UserHandle.of(mUserId), + "testCallNotificationListener_differentPackage_notNotified"); + + verify(listener, never()).onCallNotificationPosted(anyString(), any()); + + mBinderService.cancelNotificationWithTag(PKG, PKG, r.getSbn().getTag(), r.getSbn().getId(), + r.getSbn().getUserId()); + waitForIdle(); + + verify(listener, never()).onCallNotificationRemoved(anyString(), any()); + } + + private NotificationRecord createAndPostCallStyleNotification(String packageName, + UserHandle userHandle, String testName) throws Exception { + Person person = new Person.Builder().setName("caller").build(); + Notification.Builder nb = new Notification.Builder(mContext, + mTestNotificationChannel.getId()) + .setFlag(FLAG_USER_INITIATED_JOB, true) + .setStyle(Notification.CallStyle.forOngoingCall( + person, mock(PendingIntent.class))) + .setSmallIcon(android.R.drawable.sym_def_app_icon); + StatusBarNotification sbn = new StatusBarNotification(packageName, packageName, 1, + testName, mUid, 0, nb.build(), userHandle, null, 0); + NotificationRecord r = new NotificationRecord(mContext, sbn, mTestNotificationChannel); + + mService.addEnqueuedNotification(r); + mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), + r.getUid(), mPostNotificationTrackerFactory.newTracker(null)).run(); + waitForIdle(); + + return mService.findNotificationLocked( + packageName, r.getSbn().getTag(), r.getSbn().getId(), r.getSbn().getUserId()); + } + private NotificationRecord createAndPostNotification(Notification.Builder nb, String testName) throws RemoteException { StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, testName, mUid, 0, diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java index f604f1e77cf4..3ac78908d8ae 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java @@ -18,6 +18,8 @@ package com.android.server.notification; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + import android.app.Flags; import android.os.Parcel; import android.platform.test.flag.junit.SetFlagsRule; @@ -27,6 +29,8 @@ import androidx.test.runner.AndroidJUnit4; import com.android.server.UiServiceTestCase; +import com.google.common.collect.ImmutableSet; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -52,6 +56,10 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { .setShouldMaximizeDoze(true) .setShouldUseNightMode(false) .setShouldSuppressAmbientDisplay(false).setShouldSuppressAmbientDisplay(true) + .addExtraEffect("WILL BE GONE") + .setExtraEffects(ImmutableSet.of("1", "2")) + .addExtraEffects(ImmutableSet.of("3", "4")) + .addExtraEffect("5") .build(); assertThat(deviceEffects.shouldDimWallpaper()).isTrue(); @@ -64,6 +72,7 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { assertThat(deviceEffects.shouldMinimizeRadioUsage()).isFalse(); assertThat(deviceEffects.shouldUseNightMode()).isFalse(); assertThat(deviceEffects.shouldSuppressAmbientDisplay()).isTrue(); + assertThat(deviceEffects.getExtraEffects()).containsExactly("1", "2", "3", "4", "5"); } @Test @@ -73,11 +82,13 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { .setShouldDisableTiltToWake(true) .setShouldUseNightMode(true) .setShouldSuppressAmbientDisplay(true) + .addExtraEffect("1") .build(); ZenDeviceEffects modified = new ZenDeviceEffects.Builder(original) .setShouldDisplayGrayscale(true) .setShouldUseNightMode(false) + .addExtraEffect("2") .build(); assertThat(modified.shouldDimWallpaper()).isTrue(); // from original @@ -85,6 +96,32 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { assertThat(modified.shouldDisplayGrayscale()).isTrue(); // updated assertThat(modified.shouldUseNightMode()).isFalse(); // updated assertThat(modified.shouldSuppressAmbientDisplay()).isTrue(); // from original + assertThat(modified.getExtraEffects()).containsExactly("1", "2"); // updated + } + + @Test + public void builder_add_merges() { + ZenDeviceEffects zde1 = new ZenDeviceEffects.Builder() + .setShouldDimWallpaper(true) + .addExtraEffect("one") + .build(); + ZenDeviceEffects zde2 = new ZenDeviceEffects.Builder() + .setShouldDisableTouch(true) + .addExtraEffect("two") + .build(); + ZenDeviceEffects zde3 = new ZenDeviceEffects.Builder() + .setShouldMinimizeRadioUsage(true) + .addExtraEffect("three") + .build(); + + ZenDeviceEffects add = new ZenDeviceEffects.Builder().add(zde1).add(zde2).add(zde3).build(); + + assertThat(add).isEqualTo(new ZenDeviceEffects.Builder() + .setShouldDimWallpaper(true) + .setShouldDisableTouch(true) + .setShouldMinimizeRadioUsage(true) + .setExtraEffects(ImmutableSet.of("one", "two", "three")) + .build()); } @Test @@ -95,6 +132,7 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { .setShouldMinimizeRadioUsage(true) .setShouldUseNightMode(true) .setShouldSuppressAmbientDisplay(true) + .setExtraEffects(ImmutableSet.of("1", "2", "3")) .build(); Parcel parcel = Parcel.obtain(); @@ -113,6 +151,7 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { assertThat(copy.shouldUseNightMode()).isTrue(); assertThat(copy.shouldSuppressAmbientDisplay()).isTrue(); assertThat(copy.shouldDisplayGrayscale()).isFalse(); + assertThat(copy.getExtraEffects()).containsExactly("1", "2", "3"); } @Test @@ -128,4 +167,36 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { .build(); assertThat(effects.hasEffects()).isTrue(); } + + @Test + public void hasEffects_extras_returnsTrue() { + ZenDeviceEffects effects = new ZenDeviceEffects.Builder() + .addExtraEffect("extra") + .build(); + assertThat(effects.hasEffects()).isTrue(); + } + + @Test + public void validate_extrasLength() { + ZenDeviceEffects okay = new ZenDeviceEffects.Builder() + .addExtraEffect("short") + .addExtraEffect("anotherShort") + .build(); + + ZenDeviceEffects pushingIt = new ZenDeviceEffects.Builder() + .addExtraEffect("0123456789".repeat(60)) + .addExtraEffect("1234567890".repeat(60)) + .build(); + + ZenDeviceEffects excessive = new ZenDeviceEffects.Builder() + .addExtraEffect("0123456789".repeat(60)) + .addExtraEffect("1234567890".repeat(60)) + .addExtraEffect("2345678901".repeat(60)) + .addExtraEffect("3456789012".repeat(30)) + .build(); + + okay.validate(); // No exception. + pushingIt.validate(); // No exception. + assertThrows(Exception.class, () -> excessive.validate()); + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java index 539bb37419f1..12f9e26e05d0 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java @@ -49,6 +49,8 @@ import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.UiServiceTestCase; +import com.google.common.collect.ImmutableSet; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -86,7 +88,8 @@ public class ZenModeConfigTest extends UiServiceTestCase { private final int CREATION_TIME = 123; @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule( + SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT); @Before public final void setUp() { @@ -496,6 +499,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { .setShouldDisableTouch(true) .setShouldMinimizeRadioUsage(false) .setShouldMaximizeDoze(true) + .setExtraEffects(ImmutableSet.of("one", "two")) .build(); rule.creationTime = CREATION_TIME; @@ -543,6 +547,28 @@ public class ZenModeConfigTest extends UiServiceTestCase { } @Test + public void testRuleXml_weirdEffects() throws Exception { + ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); + rule.zenDeviceEffects = new ZenDeviceEffects.Builder() + .setShouldMaximizeDoze(true) + .addExtraEffect("one,stillOne,,andStillOne,,,andYetStill") + .addExtraEffect(",two,stillTwo,") + .addExtraEffect("three\\andThree") + .addExtraEffect("four\\,andFour") + .build(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + writeRuleXml(rule, baos); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ZenModeConfig.ZenRule fromXml = readRuleXml(bais); + + assertThat(fromXml.zenDeviceEffects.getExtraEffects()).isNotNull(); + assertThat(fromXml.zenDeviceEffects.getExtraEffects()) + .containsExactly("one,stillOne,,andStillOne,,,andYetStill", ",two,stillTwo,", + "three\\andThree", "four\\,andFour"); + } + + @Test public void testRuleXml_pkg_component() throws Exception { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.configurationActivity = new ComponentName("a", "a"); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java index 5b35e345e46b..ff1308c4f6db 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java @@ -132,4 +132,9 @@ public class ZenModeEventLoggerFake extends ZenModeEventLogger { checkInRange(i); return mChanges.get(i).getAreChannelsBypassing(); } + + public int[] getActiveRuleTypes(int i) throws IllegalArgumentException { + checkInRange(i); + return mChanges.get(i).getActiveRuleTypes(); + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 0d88b98de2a7..f9ba33b526a9 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -17,6 +17,7 @@ package com.android.server.notification; import static android.app.AutomaticZenRule.TYPE_BEDTIME; +import static android.app.AutomaticZenRule.TYPE_IMMERSIVE; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ACTIVATED; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_DEACTIVATED; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_DISABLED; @@ -68,6 +69,7 @@ import static com.android.os.dnd.DNDProtoEnums.PEOPLE_STARRED; import static com.android.os.dnd.DNDProtoEnums.ROOT_CONFIG; import static com.android.os.dnd.DNDProtoEnums.STATE_ALLOW; import static com.android.os.dnd.DNDProtoEnums.STATE_DISALLOW; +import static com.android.server.notification.ZenModeEventLogger.ACTIVE_RULE_TYPE_MANUAL; import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE; import static com.google.common.collect.Iterables.getOnlyElement; @@ -161,6 +163,7 @@ import com.android.server.UiServiceTestCase; import com.android.server.notification.ManagedServices.UserProfiles; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.truth.Correspondence; import com.google.common.util.concurrent.SettableFuture; import com.google.protobuf.InvalidProtocolBufferException; @@ -2581,6 +2584,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); ZenDeviceEffects original = new ZenDeviceEffects.Builder() .setShouldDisableTapToWake(true) + .addExtraEffect("extra") .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), new AutomaticZenRule.Builder("Rule", CONDITION_ID) @@ -2592,6 +2596,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenDeviceEffects updateFromApp = new ZenDeviceEffects.Builder() .setShouldUseNightMode(true) // Good .setShouldMaximizeDoze(true) // Bad + .addExtraEffect("should be rejected") // Bad .build(); mZenModeHelper.updateAutomaticZenRule(ruleId, new AutomaticZenRule.Builder("Rule", CONDITION_ID) @@ -2605,6 +2610,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ZenDeviceEffects.Builder() .setShouldUseNightMode(true) // From update. .setShouldDisableTapToWake(true) // From original. + .addExtraEffect("extra") .build()); } @@ -3550,6 +3556,89 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void testZenModeEventLog_activeRuleTypes() { + mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); + setupZenConfig(); + + // Event 1: turn on manual zen mode. Manual rule will have ACTIVE_RULE_TYPE_MANUAL + mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", null, Process.SYSTEM_UID); + + // Create bedtime rule + AutomaticZenRule bedtime = new AutomaticZenRule.Builder("Bedtime Mode (TM)", CONDITION_ID) + .setType(TYPE_BEDTIME) + .build(); + String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule("pkg", bedtime, UPDATE_ORIGIN_APP, + "reason", CUSTOM_PKG_UID); + + // Create immersive rule + AutomaticZenRule immersive = new AutomaticZenRule.Builder("Immersed", CONDITION_ID) + .setType(TYPE_IMMERSIVE) + .build(); + String immersiveId = mZenModeHelper.addAutomaticZenRule("pkg", immersive, UPDATE_ORIGIN_APP, + "reason", CUSTOM_PKG_UID); + + // Event 2: Activate bedtime rule + mZenModeHelper.setAutomaticZenRuleState(bedtimeRuleId, + new Condition(bedtime.getConditionId(), "", STATE_TRUE, SOURCE_SCHEDULE), + UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + + // Event 3: Turn immersive on + mZenModeHelper.setAutomaticZenRuleState(immersiveId, + new Condition(immersive.getConditionId(), "", STATE_TRUE, SOURCE_SCHEDULE), + UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + + // Event 4: Turn off bedtime mode, leaving just unknown + immersive + mZenModeHelper.setAutomaticZenRuleState(bedtimeRuleId, + new Condition(bedtime.getConditionId(), "", STATE_FALSE, SOURCE_SCHEDULE), + UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + + // Total of 4 events + assertEquals(4, mZenModeEventLogger.numLoggedChanges()); + + // First event: DND_TURNED_ON; active rules: 1; type is ACTIVE_RULE_TYPE_MANUAL + assertThat(mZenModeEventLogger.getEventId(0)).isEqualTo( + ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId()); + assertThat(mZenModeEventLogger.getChangedRuleType(0)).isEqualTo( + DNDProtoEnums.MANUAL_RULE); + assertThat(mZenModeEventLogger.getNumRulesActive(0)).isEqualTo(1); + int[] ruleTypes0 = mZenModeEventLogger.getActiveRuleTypes(0); + assertThat(ruleTypes0.length).isEqualTo(1); + assertThat(ruleTypes0[0]).isEqualTo(ACTIVE_RULE_TYPE_MANUAL); + + // Second event: active rules: 2; types are TYPE_MANUAL and TYPE_BEDTIME + assertThat(mZenModeEventLogger.getChangedRuleType(1)).isEqualTo( + DNDProtoEnums.AUTOMATIC_RULE); + assertThat(mZenModeEventLogger.getNumRulesActive(1)).isEqualTo(2); + int[] ruleTypes1 = mZenModeEventLogger.getActiveRuleTypes(1); + assertThat(ruleTypes1.length).isEqualTo(2); + assertThat(ruleTypes1[0]).isEqualTo(TYPE_BEDTIME); + assertThat(ruleTypes1[1]).isEqualTo(ACTIVE_RULE_TYPE_MANUAL); + + // Third event: active rules: 3 + assertThat(mZenModeEventLogger.getEventId(2)).isEqualTo( + ZenModeEventLogger.ZenStateChangedEvent.DND_ACTIVE_RULES_CHANGED.getId()); + assertThat(mZenModeEventLogger.getChangedRuleType(2)).isEqualTo( + DNDProtoEnums.AUTOMATIC_RULE); + int[] ruleTypes2 = mZenModeEventLogger.getActiveRuleTypes(2); + assertThat(ruleTypes2.length).isEqualTo(3); + assertThat(ruleTypes2[0]).isEqualTo(TYPE_BEDTIME); + assertThat(ruleTypes2[1]).isEqualTo(TYPE_IMMERSIVE); + assertThat(ruleTypes2[2]).isEqualTo(ACTIVE_RULE_TYPE_MANUAL); + + // Fourth event: active rules 2, types are TYPE_MANUAL and TYPE_IMMERSIVE + assertThat(mZenModeEventLogger.getEventId(3)).isEqualTo( + ZenModeEventLogger.ZenStateChangedEvent.DND_ACTIVE_RULES_CHANGED.getId()); + assertThat(mZenModeEventLogger.getChangedRuleType(3)).isEqualTo( + DNDProtoEnums.AUTOMATIC_RULE); + int[] ruleTypes3 = mZenModeEventLogger.getActiveRuleTypes(3); + assertThat(ruleTypes3.length).isEqualTo(2); + assertThat(ruleTypes3[0]).isEqualTo(TYPE_IMMERSIVE); + assertThat(ruleTypes3[1]).isEqualTo(ACTIVE_RULE_TYPE_MANUAL); + } + + @Test @DisableFlags(Flags.FLAG_MODES_API) public void testUpdateConsolidatedPolicy_preModesApiDefaultRulesOnly_takesGlobalDefault() { setupZenConfig(); @@ -4701,19 +4790,26 @@ public class ZenModeHelperTest extends UiServiceTestCase { verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(UPDATE_ORIGIN_INIT)); String ruleId = addRuleWithEffects( - new ZenDeviceEffects.Builder().setShouldDisplayGrayscale(true).build()); + new ZenDeviceEffects.Builder() + .setShouldDisplayGrayscale(true) + .addExtraEffect("ONE") + .build()); mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); verify(mDeviceEffectsApplier).apply( eq(new ZenDeviceEffects.Builder() .setShouldDisplayGrayscale(true) + .addExtraEffect("ONE") .build()), eq(UPDATE_ORIGIN_APP)); // Now create and activate a second rule that adds more effects. String secondRuleId = addRuleWithEffects( - new ZenDeviceEffects.Builder().setShouldDimWallpaper(true).build()); + new ZenDeviceEffects.Builder() + .setShouldDimWallpaper(true) + .addExtraEffect("TWO") + .build()); mZenModeHelper.setAutomaticZenRuleState(secondRuleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); @@ -4722,6 +4818,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { eq(new ZenDeviceEffects.Builder() .setShouldDisplayGrayscale(true) .setShouldDimWallpaper(true) + .setExtraEffects(ImmutableSet.of("ONE", "TWO")) .build()), eq(UPDATE_ORIGIN_APP)); } @@ -4732,7 +4829,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(UPDATE_ORIGIN_INIT)); - ZenDeviceEffects zde = new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(); + ZenDeviceEffects zde = new ZenDeviceEffects.Builder() + .setShouldUseNightMode(true) + .addExtraEffect("extra_effect") + .build(); String ruleId = addRuleWithEffects(zde); mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); @@ -4798,7 +4898,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setDeviceEffects(effects) .build(); return mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - UPDATE_ORIGIN_APP, "reasons", CUSTOM_PKG_UID); + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "reasons", Process.SYSTEM_UID); } @Test diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java index bdbb6c6baa70..7db707a42ff0 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -1797,7 +1797,6 @@ public class VibratorManagerServiceTest { cancelVibrate(service); // Clean up long effect. } - @FlakyTest @Test public void onExternalVibration_withNewSameImportanceButRepeating_cancelsOngoingVibration() throws Exception { diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java index d7b860f6a857..e4ac993f2d50 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java @@ -33,6 +33,7 @@ import android.service.voice.IDetectorSessionVisualQueryDetectionCallback; import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback; import android.service.voice.ISandboxedDetectionService; import android.service.voice.IVisualQueryDetectionVoiceInteractionCallback; +import android.service.voice.VisualQueryAttentionResult; import android.service.voice.VisualQueryDetectedResult; import android.service.voice.VisualQueryDetectionServiceFailure; import android.util.Slog; @@ -105,7 +106,7 @@ final class VisualQueryDetectorSession extends DetectorSession { new IDetectorSessionVisualQueryDetectionCallback.Stub(){ @Override - public void onAttentionGained() { + public void onAttentionGained(VisualQueryAttentionResult attentionResult) { Slog.v(TAG, "BinderCallback#onAttentionGained"); synchronized (mLock) { mEgressingData = true; @@ -113,7 +114,7 @@ final class VisualQueryDetectorSession extends DetectorSession { return; } try { - mAttentionListener.onAttentionGained(); + mAttentionListener.onAttentionGained(attentionResult); } catch (RemoteException e) { Slog.e(TAG, "Error delivering attention gained event.", e); try { @@ -129,7 +130,7 @@ final class VisualQueryDetectorSession extends DetectorSession { } @Override - public void onAttentionLost() { + public void onAttentionLost(int interactionIntention) { Slog.v(TAG, "BinderCallback#onAttentionLost"); synchronized (mLock) { mEgressingData = false; @@ -137,7 +138,7 @@ final class VisualQueryDetectorSession extends DetectorSession { return; } try { - mAttentionListener.onAttentionLost(); + mAttentionListener.onAttentionLost(interactionIntention); } catch (RemoteException e) { Slog.e(TAG, "Error delivering attention lost event.", e); try { diff --git a/telecomm/java/android/telecom/PhoneAccount.java b/telecomm/java/android/telecom/PhoneAccount.java index 6bdc43ec2414..e6fe406dd8e5 100644 --- a/telecomm/java/android/telecom/PhoneAccount.java +++ b/telecomm/java/android/telecom/PhoneAccount.java @@ -190,6 +190,8 @@ public final class PhoneAccount implements Parcelable { * this may be used to skip call filtering when it has already been performed on another device. * @hide */ + @SystemApi + @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) public static final String EXTRA_SKIP_CALL_FILTERING = "android.telecom.extra.SKIP_CALL_FILTERING"; diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index 3daa0143f244..15a978d167e1 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -1611,6 +1611,26 @@ public class TelecomManager { * {@link PhoneAccount} when the upper bound limit, 10, has already been reached. * * @param account The complete {@link PhoneAccount}. + * @throws UnsupportedOperationException if the caller cannot modify phone state and the device + * does not have the Telecom feature. + * @throws SecurityException if: + * <ol> + * <li>the caller cannot modify phone state and the phone account doesn't belong to the + * calling user.</li> + * <li>the caller is registering a self-managed phone and either they are not allowed to + * manage their own calls or if the account is call capable, a connection manager, or a + * sim account.</li> + * <li>the caller is registering a sim account without the ability to do so.</li> + * <li>the caller is registering a multi-user phone account but isn't a system app.</li> + * <li>the account can make SIM-based voice calls but the caller cannot register sim + * accounts or isn't a sim call manager.</li> + * <li>the account defines the EXTRA_SKIP_CALL_FILTERING extra but the caller isn't + * able to modify the phone state.</li> + * <li>the caller is registering an account for a different user but isn't able to + * interact across users.</li> + * <li>if simultaneous calling is available and the phone account package name doesn't + * correspond to the simultaneous calling accounts associated with this phone account.</li> + * </ol> */ public void registerPhoneAccount(PhoneAccount account) { ITelecomService service = getTelecomService(); diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index 711820796496..cd641b8be0b6 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -1156,6 +1156,28 @@ public class SubscriptionManager { */ public static final String TRANSFER_STATUS = SimInfo.COLUMN_TRANSFER_STATUS; + /** + * TelephonyProvider column name for satellite entitlement status. The value of this column is + * set based on entitlement query result for satellite configuration. + * By default, it's disabled. + * <P>Type: INTEGER (int)</P> + * + * @hide + */ + public static final String SATELLITE_ENTITLEMENT_STATUS = + SimInfo.COLUMN_SATELLITE_ENTITLEMENT_STATUS; + + /** + * TelephonyProvider column name for satellite entitlement plmns. The value of this column is + * set based on entitlement query result for satellite configuration. + * By default, it's empty. + * <P>Type: TEXT </P> + * + * @hide + */ + public static final String SATELLITE_ENTITLEMENT_PLMNS = + SimInfo.COLUMN_SATELLITE_ENTITLEMENT_PLMNS; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = {"USAGE_SETTING_"}, diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java index 7935d243397c..e3ce766b40bc 100644 --- a/telephony/java/android/telephony/euicc/EuiccManager.java +++ b/telephony/java/android/telephony/euicc/EuiccManager.java @@ -1024,11 +1024,22 @@ public class EuiccManager { /** * Attempt to download the given {@link DownloadableSubscription}. * - * <p>Requires the {@code android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} permission, - * or the calling app must be authorized to manage both the currently-active subscription on the + * <p>Requires the {@code android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} + * or the calling app must be authorized to manage both the currently-active + * subscription on the * current eUICC and the subscription to be downloaded according to the subscription metadata. * Without the former, an {@link #EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR} will be - * returned in the callback intent to prompt the user to accept the download. + * eturned in the callback intent to prompt the user to accept the download. + * + * <p> Starting from Android {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, + * if the caller has the + * {@code android.Manifest.permission#MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS} permission or + * is a profile owner or device owner, and + * {@code switchAfterDownload} is {@code false}, then the downloaded subscription + * will be managed by that caller. If {@code switchAfterDownload} is true, + * an {@link #EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR} will be + * returned in the callback intent to prompt the user to accept the download and the + * subscription will not be managed. * * <p>On a multi-active SIM device, requires the * {@code android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} permission, or a calling app @@ -1061,7 +1072,9 @@ public class EuiccManager { * @throws UnsupportedOperationException If the device does not have * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ - @RequiresPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) + @RequiresPermission(anyOf = { + Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS, + Manifest.permission.MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS}) public void downloadSubscription(DownloadableSubscription subscription, boolean switchAfterDownload, PendingIntent callbackIntent) { if (!isEnabled()) { @@ -1243,6 +1256,12 @@ public class EuiccManager { * <p>Requires that the calling app has carrier privileges according to the metadata of the * profile to be deleted, or the * {@code android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} permission. + * Starting from Android {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, if the + * caller is a device owner, profile owner, or holds the + * {@code android.Manifest.permission#MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS} permission, + * then the caller can delete a subscription that was downloaded by that caller. + * If such a caller tries to delete any other subscription then the + * operation will fail with {@link #EMBEDDED_SUBSCRIPTION_RESULT_ERROR}. * * @param subscriptionId the ID of the subscription to delete. * @param callbackIntent a PendingIntent to launch when the operation completes. @@ -1250,7 +1269,9 @@ public class EuiccManager { * @throws UnsupportedOperationException If the device does not have * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ - @RequiresPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) + @RequiresPermission(anyOf = { + Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS, + Manifest.permission.MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS}) public void deleteSubscription(int subscriptionId, PendingIntent callbackIntent) { if (!isEnabled()) { sendUnavailableError(callbackIntent); diff --git a/test-mock/api/test-current.txt b/test-mock/api/test-current.txt index 9ed010881067..14f1b64869ef 100644 --- a/test-mock/api/test-current.txt +++ b/test-mock/api/test-current.txt @@ -3,6 +3,7 @@ package android.test.mock { public class MockContext extends android.content.Context { method public int getDisplayId(); + method public void updateDisplay(int); } @Deprecated public class MockPackageManager extends android.content.pm.PackageManager { diff --git a/tests/UsbManagerTests/Android.bp b/tests/UsbManagerTests/Android.bp index 70c7dad2fd55..a16a7eafc8e8 100644 --- a/tests/UsbManagerTests/Android.bp +++ b/tests/UsbManagerTests/Android.bp @@ -33,6 +33,8 @@ android_test { "platform-test-annotations", "truth", "UsbManagerTestLib", + "flag-junit", + "TestParameterInjector", ], jni_libs: [ // Required for ExtendedMockito 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/tools/protologtool/OWNERS b/tools/protologtool/OWNERS new file mode 100644 index 000000000000..18cf2be9f7df --- /dev/null +++ b/tools/protologtool/OWNERS @@ -0,0 +1,3 @@ +# ProtoLog owners +# Bug component: 1157642 +include platform/development:/tools/winscope/OWNERS |