Merge "camera2: Add CameraDeviceSetup" into main
diff --git a/OWNERS b/OWNERS
index 935b768..6bab92b 100644
--- a/OWNERS
+++ b/OWNERS
@@ -40,4 +40,6 @@
per-file PERFORMANCE_OWNERS = file:/PERFORMANCE_OWNERS
-per-file PACKAGE_MANAGER_OWNERS = file:/PACKAGE_MANAGER_OWNERS
\ No newline at end of file
+per-file PACKAGE_MANAGER_OWNERS = file:/PACKAGE_MANAGER_OWNERS
+
+per-file WEAR_OWNERS = file:/WEAR_OWNERS
diff --git a/Ravenwood.bp b/Ravenwood.bp
index 93febca4..2babf6a 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -96,14 +96,19 @@
android_ravenwood_libgroup {
name: "ravenwood-runtime",
libs: [
- "framework-minus-apex.ravenwood",
- "hoststubgen-helper-runtime.ravenwood",
- "hoststubgen-helper-framework-runtime.ravenwood",
+ // Prefixed with "200" to ensure it's sorted early in Tradefed classpath
+ // so that we provide a concrete implementation before Mainline stubs
+ "200-kxml2-android",
"all-updatable-modules-system-stubs",
+ "android.test.mock.ravenwood",
+ "framework-minus-apex.ravenwood",
+ "hoststubgen-helper-framework-runtime.ravenwood",
+ "hoststubgen-helper-runtime.ravenwood",
+
+ // Provide runtime versions of utils linked in below
"junit",
"truth",
"ravenwood-junit-impl",
- "android.test.mock.ravenwood",
"mockito-ravenwood-prebuilt",
"inline-mockito-ravenwood-prebuilt",
],
diff --git a/WEAR_OWNERS b/WEAR_OWNERS
index 4127f99..da8c83e 100644
--- a/WEAR_OWNERS
+++ b/WEAR_OWNERS
@@ -10,3 +10,4 @@
rwmyers@google.com
nalmalki@google.com
shijianli@google.com
+latkin@google.com
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index 7de6799..60eb4ac 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 @@
@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 @@
* @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 @@
* 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 @@
* 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 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 @@
}
JobInfo jobInfo = new JobInfo(this);
jobInfo.enforceValidity(disallowPrefetchDeadlines, rejectNegativeNetworkEstimates,
- enforceMinimumTimeWindows);
+ enforceMinimumTimeWindows, rejectNegativeDelaysAndDeadlines);
return jobInfo;
}
@@ -2225,7 +2250,8 @@
*/
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 @@
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 a83c099..f819f15 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 @@
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 @@
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 53b14d6..d8934d8 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 @@
// 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 a0b9c5f..edd86e3 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 @@
.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 c733f5b..e885823 100644
--- a/api/api.go
+++ b/api/api.go
@@ -79,7 +79,45 @@
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 b6e4e0d..f177586 100644
--- a/cmds/uinput/README.md
+++ b/cmds/uinput/README.md
@@ -154,7 +154,8 @@
#### `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 a78a465..bd61000 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 @@
::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 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 @@
"(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 9769a75..72c8647 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 @@
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 25d3a34..b452fc7 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 @@
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 @@
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 @@
}
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 @@
* @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 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 @@
private class DeviceHandler extends Handler {
private long mPtr;
+ private long mLastInjectTimestampMicros = -1;
private int mBarrierToken;
DeviceHandler(Looper looper) {
@@ -160,7 +191,7 @@
@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 @@
}
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 @@
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 7652f24..da99162 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 @@
* 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 @@
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 @@
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 @@
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 0f16a27..9e7ee09 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 @@
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 @@
}
/**
+ * 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 @@
+ ", busId=" + mBusId
+ ", events=" + Arrays.toString(mInjections)
+ ", configuration=" + mConfiguration
- + ", duration=" + mDurationMillis + "ms"
+ + ", duration=" + mDurationNanos + "ns"
+ ", ff_effects_max=" + mFfEffectsMax
+ ", port=" + mInputPort
+ "}";
@@ -211,6 +221,10 @@
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 @@
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 @@
}
}
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 ed3ff33..6994f0c 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 @@
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 04df279..760e981 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 @@
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 a05cc67..5239fbc 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 @@
}
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 @@
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 @@
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 @@
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 @@
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 283e0ed..7052fd1 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -140,6 +140,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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
}
@@ -5448,7 +5473,6 @@
}
@FlaggedApi("android.security.content_uri_permission_apis") public final class ComponentCaller {
- ctor public ComponentCaller(@NonNull android.os.IBinder, @Nullable android.os.IBinder);
method public int checkContentUriPermission(@NonNull android.net.Uri, int);
method @Nullable public String getPackage();
method public int getUid();
@@ -7425,6 +7449,7 @@
method public int onStartCommand(android.content.Intent, int, int);
method public void onTaskRemoved(android.content.Intent);
method public void onTimeout(int);
+ method @FlaggedApi("android.app.introduce_new_service_ontimeout_callback") public void onTimeout(int, int);
method public void onTrimMemory(int);
method public boolean onUnbind(android.content.Intent);
method public final void startForeground(int, android.app.Notification);
@@ -8044,6 +8069,7 @@
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);
@@ -10045,11 +10071,22 @@
method public CharSequence coerceToText(android.content.Context);
method public String getHtmlText();
method public android.content.Intent getIntent();
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @Nullable public android.app.PendingIntent getPendingIntent();
method public CharSequence getText();
method @Nullable public android.view.textclassifier.TextLinks getTextLinks();
method public android.net.Uri getUri();
}
+ @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") public static final class ClipData.Item.Builder {
+ ctor public ClipData.Item.Builder();
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item build();
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setHtmlText(@Nullable String);
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setIntent(@Nullable android.content.Intent);
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setPendingIntent(@Nullable android.app.PendingIntent);
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setText(@Nullable CharSequence);
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setUri(@Nullable android.net.Uri);
+ }
+
public class ClipDescription implements android.os.Parcelable {
ctor public ClipDescription(CharSequence, String[]);
ctor public ClipDescription(android.content.ClipDescription);
@@ -10714,6 +10751,7 @@
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
@@ -11304,10 +11342,14 @@
field public static final String EXTRA_CHANGED_COMPONENT_NAME_LIST = "android.intent.extra.changed_component_name_list";
field public static final String EXTRA_CHANGED_PACKAGE_LIST = "android.intent.extra.changed_package_list";
field public static final String EXTRA_CHANGED_UID_LIST = "android.intent.extra.changed_uid_list";
+ field @FlaggedApi("android.service.chooser.chooser_payload_toggling") public static final String EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI = "android.intent.extra.CHOOSER_ADDITIONAL_CONTENT_URI";
field @FlaggedApi("android.service.chooser.chooser_album_text") public static final String EXTRA_CHOOSER_CONTENT_TYPE_HINT = "android.intent.extra.CHOOSER_CONTENT_TYPE_HINT";
field public static final String EXTRA_CHOOSER_CUSTOM_ACTIONS = "android.intent.extra.CHOOSER_CUSTOM_ACTIONS";
+ field @FlaggedApi("android.service.chooser.chooser_payload_toggling") public static final String EXTRA_CHOOSER_FOCUSED_ITEM_POSITION = "android.intent.extra.CHOOSER_FOCUSED_ITEM_POSITION";
field public static final String EXTRA_CHOOSER_MODIFY_SHARE_ACTION = "android.intent.extra.CHOOSER_MODIFY_SHARE_ACTION";
field public static final String EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER = "android.intent.extra.CHOOSER_REFINEMENT_INTENT_SENDER";
+ field @FlaggedApi("android.service.chooser.enable_chooser_result") public static final String EXTRA_CHOOSER_RESULT = "android.intent.extra.CHOOSER_RESULT";
+ field @FlaggedApi("android.service.chooser.enable_chooser_result") public static final String EXTRA_CHOOSER_RESULT_INTENT_SENDER = "android.intent.extra.CHOOSER_RESULT_INTENT_SENDER";
field public static final String EXTRA_CHOOSER_TARGETS = "android.intent.extra.CHOOSER_TARGETS";
field public static final String EXTRA_CHOSEN_COMPONENT = "android.intent.extra.CHOSEN_COMPONENT";
field public static final String EXTRA_CHOSEN_COMPONENT_INTENT_SENDER = "android.intent.extra.CHOSEN_COMPONENT_INTENT_SENDER";
@@ -11336,6 +11378,7 @@
field public static final String EXTRA_LOCALE_LIST = "android.intent.extra.LOCALE_LIST";
field public static final String EXTRA_LOCAL_ONLY = "android.intent.extra.LOCAL_ONLY";
field public static final String EXTRA_LOCUS_ID = "android.intent.extra.LOCUS_ID";
+ field @FlaggedApi("android.service.chooser.enable_sharesheet_metadata_extra") public static final String EXTRA_METADATA_TEXT = "android.intent.extra.METADATA_TEXT";
field public static final String EXTRA_MIME_TYPES = "android.intent.extra.MIME_TYPES";
field public static final String EXTRA_NOT_UNKNOWN_SOURCE = "android.intent.extra.NOT_UNKNOWN_SOURCE";
field public static final String EXTRA_ORIGINATING_URI = "android.intent.extra.ORIGINATING_URI";
@@ -13026,6 +13069,7 @@
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";
@@ -25878,8 +25922,11 @@
@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);
@@ -25914,6 +25961,7 @@
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
}
@@ -25923,7 +25971,10 @@
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);
}
@@ -27801,7 +27852,7 @@
package android.media.tv.ad {
- @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public class TvAdManager {
+ @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public final class TvAdManager {
method @NonNull public java.util.List<android.media.tv.ad.TvAdServiceInfo> getTvAdServiceList();
method public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.ad.TvAdManager.TvAdServiceCallback);
method public void sendAppLinkCommand(@NonNull String, @NonNull android.os.Bundle);
@@ -27812,6 +27863,14 @@
field public static final String APP_LINK_KEY_COMMAND_TYPE = "command_type";
field public static final String APP_LINK_KEY_PACKAGE_NAME = "package_name";
field public static final String APP_LINK_KEY_SERVICE_ID = "service_id";
+ field public static final int ERROR_BLOCKED = 5; // 0x5
+ field public static final int ERROR_ENCRYPTED = 6; // 0x6
+ field public static final int ERROR_NONE = 0; // 0x0
+ field public static final int ERROR_NOT_SUPPORTED = 2; // 0x2
+ field public static final int ERROR_RESOURCE_UNAVAILABLE = 4; // 0x4
+ field public static final int ERROR_UNKNOWN = 1; // 0x1
+ field public static final int ERROR_UNKNOWN_CHANNEL = 7; // 0x7
+ field public static final int ERROR_WEAK_SIGNAL = 3; // 0x3
field public static final String INTENT_KEY_AD_SERVICE_ID = "ad_service_id";
field public static final String INTENT_KEY_CHANNEL_URI = "channel_uri";
field public static final String INTENT_KEY_COMMAND_TYPE = "command_type";
@@ -27824,6 +27883,9 @@
field public static final String SESSION_DATA_TYPE_AD_REQUEST = "ad_request";
field public static final String SESSION_DATA_TYPE_BROADCAST_INFO_REQUEST = "broadcast_info_request";
field public static final String SESSION_DATA_TYPE_REMOVE_BROADCAST_INFO_REQUEST = "remove_broadcast_info_request";
+ field public static final int SESSION_STATE_ERROR = 3; // 0x3
+ field public static final int SESSION_STATE_RUNNING = 2; // 0x2
+ field public static final int SESSION_STATE_STOPPED = 1; // 0x1
}
public abstract static class TvAdManager.TvAdServiceCallback {
@@ -27846,7 +27908,12 @@
ctor public TvAdService.Session(@NonNull android.content.Context);
method public boolean isMediaViewEnabled();
method @CallSuper public void layoutSurface(int, int, int, int);
+ method @CallSuper public void notifySessionStateChanged(int, int);
method @Nullable public android.view.View onCreateMediaView();
+ method public void onCurrentChannelUri(@Nullable android.net.Uri);
+ method public void onCurrentTvInputId(@Nullable String);
+ method public void onCurrentVideoBounds(@NonNull android.graphics.Rect);
+ method public void onError(@NonNull String, @NonNull android.os.Bundle);
method public boolean onGenericMotionEvent(@NonNull android.view.MotionEvent);
method public boolean onKeyDown(int, @Nullable android.view.KeyEvent);
method public boolean onKeyLongPress(int, @Nullable android.view.KeyEvent);
@@ -27856,12 +27923,20 @@
method public abstract void onRelease();
method public void onResetAdService();
method public abstract boolean onSetSurface(@Nullable android.view.Surface);
+ method public void onSigningResult(@NonNull String, @NonNull byte[]);
method public void onStartAdService();
method public void onStopAdService();
method public void onSurfaceChanged(int, int, int);
method public boolean onTouchEvent(@NonNull android.view.MotionEvent);
+ method public void onTrackInfoList(@NonNull java.util.List<android.media.tv.TvTrackInfo>);
method public boolean onTrackballEvent(@NonNull android.view.MotionEvent);
method public void onTvInputSessionData(@NonNull String, @NonNull android.os.Bundle);
+ method public void onTvMessage(int, @NonNull android.os.Bundle);
+ method @CallSuper public void requestCurrentChannelUri();
+ method @CallSuper public void requestCurrentTvInputId();
+ method @CallSuper public void requestCurrentVideoBounds();
+ method @CallSuper public void requestSigning(@NonNull String, @NonNull String, @NonNull String, @NonNull byte[]);
+ method @CallSuper public void requestTrackInfoList();
method public void sendTvAdSessionData(@NonNull String, @NonNull android.os.Bundle);
method @CallSuper public void setMediaViewEnabled(boolean);
}
@@ -27880,9 +27955,12 @@
ctor public TvAdView(@NonNull android.content.Context);
ctor public TvAdView(@NonNull android.content.Context, @Nullable android.util.AttributeSet);
ctor public TvAdView(@NonNull android.content.Context, @Nullable android.util.AttributeSet, int);
+ method public void clearCallback();
method public void clearOnUnhandledInputEventListener();
method public boolean dispatchUnhandledInputEvent(@NonNull android.view.InputEvent);
method @Nullable public android.media.tv.ad.TvAdView.OnUnhandledInputEventListener getOnUnhandledInputEventListener();
+ method public void notifyError(@NonNull String, @NonNull android.os.Bundle);
+ method public void notifyTvMessage(@NonNull int, @NonNull android.os.Bundle);
method public void onAttachedToWindow();
method public void onDetachedFromWindow();
method public void onLayout(boolean, int, int, int, int);
@@ -27892,16 +27970,34 @@
method public void prepareAdService(@NonNull String, @NonNull String);
method public void reset();
method public void resetAdService();
+ method public void sendCurrentChannelUri(@Nullable android.net.Uri);
+ method public void sendCurrentTvInputId(@Nullable String);
+ method public void sendCurrentVideoBounds(@NonNull android.graphics.Rect);
+ method public void sendSigningResult(@NonNull String, @NonNull byte[]);
+ method public void sendTrackInfoList(@Nullable java.util.List<android.media.tv.TvTrackInfo>);
+ method public void setCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.ad.TvAdView.TvAdCallback);
method public void setOnUnhandledInputEventListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.ad.TvAdView.OnUnhandledInputEventListener);
method public boolean setTvView(@Nullable android.media.tv.TvView);
method public void startAdService();
method public void stopAdService();
+ field public static final String ERROR_KEY_ERROR_CODE = "error_code";
+ field public static final String ERROR_KEY_METHOD_NAME = "method_name";
}
public static interface TvAdView.OnUnhandledInputEventListener {
method public boolean onUnhandledInputEvent(@NonNull android.view.InputEvent);
}
+ public abstract static class TvAdView.TvAdCallback {
+ ctor public TvAdView.TvAdCallback();
+ method public void onRequestCurrentChannelUri(@NonNull String);
+ method public void onRequestCurrentTvInputId(@NonNull String);
+ method public void onRequestCurrentVideoBounds(@NonNull String);
+ method public void onRequestSigning(@NonNull String, @NonNull String, @NonNull String, @NonNull String, @NonNull byte[]);
+ method public void onRequestTrackInfoList(@NonNull String);
+ method public void onStateChanged(@NonNull String, int, int);
+ }
+
}
package android.media.tv.interactive {
@@ -33792,6 +33888,7 @@
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";
@@ -35475,6 +35572,7 @@
ctor public CallLog.Calls();
method public static String getLastOutgoingCall(android.content.Context);
field public static final int ANSWERED_EXTERNALLY_TYPE = 7; // 0x7
+ field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final String ASSERTED_DISPLAY_NAME = "asserted_display_name";
field public static final long AUTO_MISSED_EMERGENCY_CALL = 1L; // 0x1L
field public static final long AUTO_MISSED_MAXIMUM_DIALING = 4L; // 0x4L
field public static final long AUTO_MISSED_MAXIMUM_RINGING = 2L; // 0x2L
@@ -35521,6 +35619,7 @@
field public static final int FEATURES_WIFI = 8; // 0x8
field public static final String GEOCODED_LOCATION = "geocoded_location";
field public static final int INCOMING_TYPE = 1; // 0x1
+ field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final String IS_BUSINESS_CALL = "is_business_call";
field public static final String IS_READ = "is_read";
field public static final String LAST_MODIFIED = "last_modified";
field public static final String LIMIT_PARAM_KEY = "limit";
@@ -40220,6 +40319,21 @@
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();
@@ -40234,6 +40348,19 @@
method @NonNull public android.service.chooser.ChooserAction build();
}
+ @FlaggedApi("android.service.chooser.enable_chooser_result") public final class ChooserResult implements android.os.Parcelable {
+ method public int describeContents();
+ method @Nullable public android.content.ComponentName getSelectedComponent();
+ method public int getType();
+ method public boolean isShortcut();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field public static final int CHOOSER_RESULT_COPY = 1; // 0x1
+ field public static final int CHOOSER_RESULT_EDIT = 2; // 0x2
+ field public static final int CHOOSER_RESULT_SELECTED_COMPONENT = 0; // 0x0
+ field public static final int CHOOSER_RESULT_UNKNOWN = -1; // 0xffffffff
+ field @NonNull public static final android.os.Parcelable.Creator<android.service.chooser.ChooserResult> CREATOR;
+ }
+
@Deprecated public final class ChooserTarget implements android.os.Parcelable {
ctor @Deprecated public ChooserTarget(CharSequence, android.graphics.drawable.Icon, float, android.content.ComponentName, @Nullable android.os.Bundle);
method @Deprecated public int describeContents();
@@ -41998,8 +42125,10 @@
field @Deprecated public static final String AVAILABLE_PHONE_ACCOUNTS = "selectPhoneAccountAccounts";
field public static final String EVENT_CLEAR_DIAGNOSTIC_MESSAGE = "android.telecom.event.CLEAR_DIAGNOSTIC_MESSAGE";
field public static final String EVENT_DISPLAY_DIAGNOSTIC_MESSAGE = "android.telecom.event.DISPLAY_DIAGNOSTIC_MESSAGE";
+ field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final String EXTRA_ASSERTED_DISPLAY_NAME = "android.telecom.extra.ASSERTED_DISPLAY_NAME";
field public static final String EXTRA_DIAGNOSTIC_MESSAGE = "android.telecom.extra.DIAGNOSTIC_MESSAGE";
field public static final String EXTRA_DIAGNOSTIC_MESSAGE_ID = "android.telecom.extra.DIAGNOSTIC_MESSAGE_ID";
+ field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final String EXTRA_IS_BUSINESS_CALL = "android.telecom.extra.IS_BUSINESS_CALL";
field public static final String EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB = "android.telecom.extra.IS_SUPPRESSED_BY_DO_NOT_DISTURB";
field public static final String EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS = "android.telecom.extra.LAST_EMERGENCY_CALLBACK_TIME_MILLIS";
field public static final String EXTRA_SILENT_RINGING_REQUESTED = "android.telecom.extra.SILENT_RINGING_REQUESTED";
@@ -43627,6 +43756,7 @@
field public static final String KEY_SMS_REQUIRES_DESTINATION_NUMBER_CONVERSION_BOOL = "sms_requires_destination_number_conversion_bool";
field public static final String KEY_SUBSCRIPTION_GROUP_UUID_STRING = "subscription_group_uuid_string";
field public static final String KEY_SUPPORTED_PREMIUM_CAPABILITIES_INT_ARRAY = "supported_premium_capabilities_int_array";
+ field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final String KEY_SUPPORTS_BUSINESS_CALL_COMPOSER_BOOL = "supports_business_call_composer_bool";
field public static final String KEY_SUPPORTS_CALL_COMPOSER_BOOL = "supports_call_composer_bool";
field public static final String KEY_SUPPORTS_DEVICE_TO_DEVICE_COMMUNICATION_USING_DTMF_BOOL = "supports_device_to_device_communication_using_dtmf_bool";
field public static final String KEY_SUPPORTS_DEVICE_TO_DEVICE_COMMUNICATION_USING_RTP_BOOL = "supports_device_to_device_communication_using_rtp_bool";
@@ -45892,6 +46022,7 @@
field public static final int AUTHTYPE_EAP_SIM = 128; // 0x80
field public static final int AUTHTYPE_GBA_BOOTSTRAP = 132; // 0x84
field public static final int AUTHTYPE_GBA_NAF_KEY_EXTERNAL = 133; // 0x85
+ field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final int CALL_COMPOSER_STATUS_BUSINESS_ONLY = 2; // 0x2
field public static final int CALL_COMPOSER_STATUS_OFF = 0; // 0x0
field public static final int CALL_COMPOSER_STATUS_ON = 1; // 0x1
field public static final int CALL_STATE_IDLE = 0; // 0x0
@@ -46444,8 +46575,8 @@
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();
@@ -46928,6 +47059,7 @@
public static class MmTelFeature.MmTelCapabilities {
method public final boolean isCapable(int);
field public static final int CAPABILITY_TYPE_CALL_COMPOSER = 16; // 0x10
+ field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final int CAPABILITY_TYPE_CALL_COMPOSER_BUSINESS_ONLY = 32; // 0x20
field public static final int CAPABILITY_TYPE_SMS = 8; // 0x8
field public static final int CAPABILITY_TYPE_UT = 4; // 0x4
field public static final int CAPABILITY_TYPE_VIDEO = 2; // 0x2
@@ -52903,9 +53035,11 @@
field public static final int DRAG_FLAG_GLOBAL = 256; // 0x100
field public static final int DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION = 64; // 0x40
field public static final int DRAG_FLAG_GLOBAL_PREFIX_URI_PERMISSION = 128; // 0x80
+ field @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") public static final int DRAG_FLAG_GLOBAL_SAME_APPLICATION = 4096; // 0x1000
field public static final int DRAG_FLAG_GLOBAL_URI_READ = 1; // 0x1
field public static final int DRAG_FLAG_GLOBAL_URI_WRITE = 2; // 0x2
field public static final int DRAG_FLAG_OPAQUE = 512; // 0x200
+ field @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") public static final int DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG = 8192; // 0x2000
field @Deprecated public static final int DRAWING_CACHE_QUALITY_AUTO = 0; // 0x0
field @Deprecated public static final int DRAWING_CACHE_QUALITY_HIGH = 1048576; // 0x100000
field @Deprecated public static final int DRAWING_CACHE_QUALITY_LOW = 524288; // 0x80000
@@ -53995,8 +54129,11 @@
method @Deprecated @NonNull public android.view.WindowInsets consumeDisplayCutout();
method @Deprecated @NonNull public android.view.WindowInsets consumeStableInsets();
method @Deprecated @NonNull public android.view.WindowInsets consumeSystemWindowInsets();
+ method @FlaggedApi("android.view.flags.customizable_window_headers") @NonNull public java.util.List<android.graphics.Rect> getBoundingRects(int);
+ method @FlaggedApi("android.view.flags.customizable_window_headers") @NonNull public java.util.List<android.graphics.Rect> getBoundingRectsIgnoringVisibility(int);
method @Nullable public android.view.DisplayCutout getDisplayCutout();
method @Nullable public android.view.DisplayShape getDisplayShape();
+ method @FlaggedApi("android.view.flags.customizable_window_headers") @NonNull public android.util.Size getFrame();
method @NonNull public android.graphics.Insets getInsets(int);
method @NonNull public android.graphics.Insets getInsetsIgnoringVisibility(int);
method @Deprecated @NonNull public android.graphics.Insets getMandatorySystemGestureInsets();
@@ -54031,8 +54168,11 @@
ctor public WindowInsets.Builder();
ctor public WindowInsets.Builder(@NonNull android.view.WindowInsets);
method @NonNull public android.view.WindowInsets build();
+ method @FlaggedApi("android.view.flags.customizable_window_headers") @NonNull public android.view.WindowInsets.Builder setBoundingRects(int, @NonNull java.util.List<android.graphics.Rect>);
+ method @FlaggedApi("android.view.flags.customizable_window_headers") @NonNull public android.view.WindowInsets.Builder setBoundingRectsIgnoringVisibility(int, @NonNull java.util.List<android.graphics.Rect>);
method @NonNull public android.view.WindowInsets.Builder setDisplayCutout(@Nullable android.view.DisplayCutout);
method @NonNull public android.view.WindowInsets.Builder setDisplayShape(@NonNull android.view.DisplayShape);
+ method @FlaggedApi("android.view.flags.customizable_window_headers") @NonNull public android.view.WindowInsets.Builder setFrame(int, int);
method @NonNull public android.view.WindowInsets.Builder setInsets(int, @NonNull android.graphics.Insets);
method @NonNull public android.view.WindowInsets.Builder setInsetsIgnoringVisibility(int, @NonNull android.graphics.Insets) throws java.lang.IllegalArgumentException;
method @Deprecated @NonNull public android.view.WindowInsets.Builder setMandatorySystemGestureInsets(@NonNull android.graphics.Insets);
@@ -54126,8 +54266,10 @@
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
@@ -55594,6 +55736,14 @@
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();
@@ -56005,6 +56155,7 @@
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);
@@ -56034,6 +56185,7 @@
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();
@@ -56054,6 +56206,9 @@
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 e901f00..b36b963f 100644
--- a/core/api/lint-baseline.txt
+++ b/core/api/lint-baseline.txt
@@ -1093,6 +1093,66 @@
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 1273da7..7ba7835 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -102,6 +102,7 @@
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 @@
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.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 0f1da41..9e09931 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -282,6 +282,7 @@
field public static final String RADIO_SCAN_WITHOUT_LOCATION = "android.permission.RADIO_SCAN_WITHOUT_LOCATION";
field public static final String READ_ACTIVE_EMERGENCY_SESSION = "android.permission.READ_ACTIVE_EMERGENCY_SESSION";
field public static final String READ_APP_SPECIFIC_LOCALES = "android.permission.READ_APP_SPECIFIC_LOCALES";
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String READ_BLOCKED_NUMBERS = "android.permission.READ_BLOCKED_NUMBERS";
field public static final String READ_CARRIER_APP_INFO = "android.permission.READ_CARRIER_APP_INFO";
field public static final String READ_CELL_BROADCASTS = "android.permission.READ_CELL_BROADCASTS";
field public static final String READ_CLIPBOARD_IN_BACKGROUND = "android.permission.READ_CLIPBOARD_IN_BACKGROUND";
@@ -409,6 +410,7 @@
field public static final String WIFI_UPDATE_COEX_UNSAFE_CHANNELS = "android.permission.WIFI_UPDATE_COEX_UNSAFE_CHANNELS";
field public static final String WIFI_UPDATE_USABILITY_STATS_SCORE = "android.permission.WIFI_UPDATE_USABILITY_STATS_SCORE";
field public static final String WRITE_ALLOWLISTED_DEVICE_CONFIG = "android.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG";
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String WRITE_BLOCKED_NUMBERS = "android.permission.WRITE_BLOCKED_NUMBERS";
field public static final String WRITE_DEVICE_CONFIG = "android.permission.WRITE_DEVICE_CONFIG";
field public static final String WRITE_DREAM_STATE = "android.permission.WRITE_DREAM_STATE";
field public static final String WRITE_EMBEDDED_SUBSCRIPTIONS = "android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS";
@@ -1038,13 +1040,20 @@
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();
@@ -6642,6 +6651,7 @@
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);
@@ -11457,6 +11467,29 @@
package android.provider {
+ public static class BlockedNumberContract.BlockedNumbers {
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static void endBlockSuppression(@NonNull android.content.Context);
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @NonNull @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static android.provider.BlockedNumberContract.BlockedNumbers.BlockSuppressionStatus getBlockSuppressionStatus(@NonNull android.content.Context);
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static boolean getBlockedNumberSetting(@NonNull android.content.Context, @NonNull String);
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static void notifyEmergencyContact(@NonNull android.content.Context);
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static void setBlockedNumberSetting(@NonNull android.content.Context, @NonNull String, boolean);
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static boolean shouldShowEmergencyCallNotification(@NonNull android.content.Context);
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static int shouldSystemBlockNumber(@NonNull android.content.Context, @NonNull String, int, boolean);
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ACTION_BLOCK_SUPPRESSION_STATE_CHANGED = "android.provider.action.BLOCK_SUPPRESSION_STATE_CHANGED";
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ENHANCED_SETTING_KEY_BLOCK_PAYPHONE = "block_payphone_calls_setting";
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ENHANCED_SETTING_KEY_BLOCK_PRIVATE = "block_private_number_calls_setting";
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE = "block_unavailable_calls_setting";
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ENHANCED_SETTING_KEY_BLOCK_UNKNOWN = "block_unknown_calls_setting";
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED = "block_numbers_not_in_contacts_setting";
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION = "show_emergency_call_notification";
+ }
+
+ @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final class BlockedNumberContract.BlockedNumbers.BlockSuppressionStatus {
+ ctor public BlockedNumberContract.BlockedNumbers.BlockSuppressionStatus(boolean, long);
+ method public boolean getIsSuppressed();
+ method public long getUntilTimestampMillis();
+ }
+
public class CallLog {
method @RequiresPermission(allOf={android.Manifest.permission.WRITE_CALL_LOG, android.Manifest.permission.INTERACT_ACROSS_USERS}) public static void storeCallComposerPicture(@NonNull android.content.Context, @NonNull java.io.InputStream, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.Uri,android.provider.CallLog.CallComposerLoggingException>);
}
@@ -13378,6 +13411,23 @@
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();
@@ -13398,7 +13448,9 @@
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();
@@ -13840,6 +13892,7 @@
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";
}
@@ -16118,6 +16171,7 @@
field public static final int DIALSTRING_USSD = 2; // 0x2
field public static final String EXTRA_ADDITIONAL_CALL_INFO = "AdditionalCallInfo";
field public static final String EXTRA_ADDITIONAL_SIP_INVITE_FIELDS = "android.telephony.ims.extra.ADDITIONAL_SIP_INVITE_FIELDS";
+ field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final String EXTRA_ASSERTED_DISPLAY_NAME = "android.telephony.ims.extra.ASSERTED_DISPLAY_NAME";
field public static final String EXTRA_CALL_DISCONNECT_CAUSE = "android.telephony.ims.extra.CALL_DISCONNECT_CAUSE";
field public static final String EXTRA_CALL_NETWORK_TYPE = "android.telephony.ims.extra.CALL_NETWORK_TYPE";
field @Deprecated public static final String EXTRA_CALL_RAT_TYPE = "CallRadioTech";
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index a7f80dd..f8a6af1 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -921,7 +921,33 @@
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 @@
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 @@
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 {
@@ -2089,7 +2117,7 @@
package android.media.projection {
public final class MediaProjectionManager {
- method @NonNull public android.content.Intent createScreenCaptureIntent(@Nullable android.app.ActivityOptions.LaunchCookie);
+ method @NonNull public android.content.Intent createScreenCaptureIntent(@NonNull android.app.ActivityOptions.LaunchCookie);
}
}
@@ -2707,17 +2735,17 @@
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 @@
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 @@
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 @@
}
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 2c00c99..b25d5eb 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -146,6 +146,8 @@
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.Process;
+import android.os.ProfilingFrameworkInitializer;
+import android.os.ProfilingServiceManager;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -1232,6 +1234,15 @@
}
@Override
+ public final void scheduleTimeoutServiceForType(IBinder token, int startId, int fgsType) {
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
+ Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+ "scheduleTimeoutServiceForType. token=" + token);
+ }
+ sendMessage(H.TIMEOUT_SERVICE_FOR_TYPE, token, startId, fgsType);
+ }
+
+ @Override
public final void bindApplication(
String processName,
ApplicationInfo appInfo,
@@ -2288,6 +2299,8 @@
public static final int INSTRUMENT_WITHOUT_RESTART = 170;
public static final int FINISH_INSTRUMENTATION_WITHOUT_RESTART = 171;
+ public static final int TIMEOUT_SERVICE_FOR_TYPE = 172;
+
String codeToString(int code) {
if (DEBUG_MESSAGES) {
switch (code) {
@@ -2341,6 +2354,7 @@
case DUMP_RESOURCES: return "DUMP_RESOURCES";
case TIMEOUT_SERVICE: return "TIMEOUT_SERVICE";
case PING: return "PING";
+ case TIMEOUT_SERVICE_FOR_TYPE: return "TIMEOUT_SERVICE_FOR_TYPE";
}
}
return Integer.toString(code);
@@ -2427,6 +2441,14 @@
case PING:
((RemoteCallback) msg.obj).sendResult(null);
break;
+ case TIMEOUT_SERVICE_FOR_TYPE:
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+ "serviceTimeoutForType: " + msg.obj);
+ }
+ handleTimeoutServiceForType((IBinder) msg.obj, msg.arg1, msg.arg2);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
case CONFIGURATION_CHANGED:
mConfigurationController.handleConfigurationChanged((Configuration) msg.obj);
break;
@@ -5136,6 +5158,26 @@
Slog.wtf(TAG, "handleTimeoutService: token=" + token + " not found.");
}
}
+
+ private void handleTimeoutServiceForType(IBinder token, int startId, int fgsType) {
+ Service s = mServices.get(token);
+ if (s != null) {
+ try {
+ if (localLOGV) Slog.v(TAG, "Timeout service " + s);
+
+ s.callOnTimeLimitExceeded(startId, fgsType);
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(s, e)) {
+ throw new RuntimeException(
+ "Unable to call onTimeLimitExceeded on service " + s + ": " + e, e);
+ }
+ Slog.i(TAG, "handleTimeoutServiceForType: exception for " + token, e);
+ }
+ } else {
+ Slog.wtf(TAG, "handleTimeoutServiceForType: token=" + token + " not found.");
+ }
+ }
+
/**
* Resume the activity.
* @param r Target activity record.
@@ -8548,6 +8590,9 @@
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 d57a4e5..f6373d6 100644
--- a/core/java/android/app/AutomaticZenRule.java
+++ b/core/java/android/app/AutomaticZenRule.java
@@ -487,6 +487,9 @@
public void validate() {
if (Flags.modesApi()) {
checkValidType(mType);
+ if (mDeviceEffects != null) {
+ mDeviceEffects.validate();
+ }
}
}
diff --git a/core/java/android/app/ComponentCaller.java b/core/java/android/app/ComponentCaller.java
index a440dbc..44e8a0a 100644
--- a/core/java/android/app/ComponentCaller.java
+++ b/core/java/android/app/ComponentCaller.java
@@ -42,6 +42,9 @@
private final IBinder mActivityToken;
private final IBinder mCallerToken;
+ /**
+ * @hide
+ */
public ComponentCaller(@NonNull IBinder activityToken, @Nullable IBinder callerToken) {
mActivityToken = activityToken;
mCallerToken = callerToken;
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index ceeaf5d..cc0aafd 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -942,6 +942,8 @@
/** Returns if the service is a short-service is still "alive" and past the timeout. */
boolean shouldServiceTimeOut(in ComponentName className, in IBinder token);
+ /** Returns if the service has a time-limit restricted type and is past the time limit. */
+ boolean hasServiceTimeLimitExceeded(in ComponentName className, in IBinder token);
void registerUidFrozenStateChangedCallback(in IUidFrozenStateChangedCallback callback);
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)")
diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl
index 59e0e99..a04620c 100644
--- a/core/java/android/app/IApplicationThread.aidl
+++ b/core/java/android/app/IApplicationThread.aidl
@@ -178,5 +178,6 @@
in TranslationSpec targetSpec, in List<AutofillId> viewIds,
in UiTranslationSpec uiTranslationSpec);
void scheduleTimeoutService(IBinder token, int startId);
+ void scheduleTimeoutServiceForType(IBinder token, int startId, int fgsType);
void schedulePing(in RemoteCallback pong);
}
diff --git a/core/java/android/app/ICallNotificationEventCallback.aidl b/core/java/android/app/ICallNotificationEventCallback.aidl
new file mode 100644
index 0000000..ba34829
--- /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 578105f..b5e3556 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -24,6 +24,7 @@
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 @@
@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 aa9de81..d6e8ae3 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -3020,6 +3020,43 @@
}
/**
+ * @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 @@
*/
@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 8c886fe..9dfb5b0 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.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 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 @@
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/Service.java b/core/java/android/app/Service.java
index a155457..d470299 100644
--- a/core/java/android/app/Service.java
+++ b/core/java/android/app/Service.java
@@ -20,6 +20,7 @@
import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER;
import static android.text.TextUtils.formatSimple;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -1161,4 +1162,37 @@
*/
public void onTimeout(int startId) {
}
+
+ /** @hide */
+ public final void callOnTimeLimitExceeded(int startId, int fgsType) {
+ // Note, because all the service callbacks (and other similar callbacks, e.g. activity
+ // callbacks) are delivered using the main handler, it's possible the service is already
+ // stopped when before this method is called, so we do a double check here.
+ if (mToken == null) {
+ Log.w(TAG, "Service already destroyed, skipping onTimeLimitExceeded()");
+ return;
+ }
+ try {
+ if (!mActivityManager.hasServiceTimeLimitExceeded(
+ new ComponentName(this, mClassName), mToken)) {
+ Log.w(TAG, "Service no longer relevant, skipping onTimeLimitExceeded()");
+ return;
+ }
+ } catch (RemoteException ex) {
+ }
+ if (Flags.introduceNewServiceOntimeoutCallback()) {
+ onTimeout(startId, fgsType);
+ }
+ }
+
+ /**
+ * Callback called when a particular foreground service type has timed out.
+ *
+ * @param startId the startId passed to {@link #onStartCommand(Intent, int, int)} when
+ * the service started.
+ * @param fgsType the foreground service type which caused the timeout.
+ */
+ @FlaggedApi(Flags.FLAG_INTRODUCE_NEW_SERVICE_ONTIMEOUT_CALLBACK)
+ public void onTimeout(int startId, int fgsType) {
+ }
}
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index ba9c895..08c193f 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -186,6 +186,7 @@
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.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 @@
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/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
index c0b299b..ff23f09 100644
--- a/core/java/android/app/activity_manager.aconfig
+++ b/core/java/android/app/activity_manager.aconfig
@@ -27,3 +27,10 @@
description: "API to add OnUidImportanceListener with targetted UIDs"
bug: "286258140"
}
+
+flag {
+ namespace: "backstage_power"
+ name: "introduce_new_service_ontimeout_callback"
+ description: "Add a new callback in Service to indicate a FGS has reached its timeout."
+ bug: "317799821"
+}
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index c649e62..9d50810 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.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 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 efcf563..f72fdc0 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -613,4 +613,6 @@
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 3c98ef9..30cd1b7 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -36,6 +36,48 @@
}
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 {
+ 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 a16e94a..3304475 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -26,6 +26,7 @@
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 @@
*
* @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 @@
*
* @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 @@
*
* @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 @@
*
* @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 @@
*
* @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 9d6c14b..f727589 100644
--- a/core/java/android/companion/virtual/camera/VirtualCamera.java
+++ b/core/java/android/companion/virtual/camera/VirtualCamera.java
@@ -18,7 +18,9 @@
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 @@
* 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 14c7997..37e494b 100644
--- a/core/java/android/companion/virtual/sensor/VirtualSensor.java
+++ b/core/java/android/companion/virtual/sensor/VirtualSensor.java
@@ -18,7 +18,9 @@
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 @@
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 @@
*
* @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 0dbe411..21ad914 100644
--- a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java
+++ b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java
@@ -20,7 +20,9 @@
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 @@
*
* @hide
*/
+ @SuppressLint("UnflaggedApi") // @TestApi without associated feature.
+ @TestApi
public int getFlags() {
return mFlags;
}
diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java
index 67759f4..eb357fe 100644
--- a/core/java/android/content/ClipData.java
+++ b/core/java/android/content/ClipData.java
@@ -21,7 +21,13 @@
import static android.content.ContentResolver.SCHEME_CONTENT;
import static android.content.ContentResolver.SCHEME_FILE;
+import static com.android.window.flags.Flags.FLAG_DELEGATE_UNHANDLED_DRAGS;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.app.PendingIntent;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.pm.ActivityInfo;
import android.content.res.AssetFileDescriptor;
@@ -207,6 +213,7 @@
final CharSequence mText;
final String mHtmlText;
final Intent mIntent;
+ final PendingIntent mPendingIntent;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
Uri mUri;
private TextLinks mTextLinks;
@@ -214,12 +221,91 @@
// if the data is obtained from {@link #copyForTransferWithActivityInfo}
private ActivityInfo mActivityInfo;
+ /**
+ * A builder for a ClipData Item.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @SuppressLint("PackageLayering")
+ public static final class Builder {
+ private CharSequence mText;
+ private String mHtmlText;
+ private Intent mIntent;
+ private PendingIntent mPendingIntent;
+ private Uri mUri;
+
+ /**
+ * Sets the text for the item to be constructed.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @NonNull
+ public Builder setText(@Nullable CharSequence text) {
+ mText = text;
+ return this;
+ }
+
+ /**
+ * Sets the HTML text for the item to be constructed.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @NonNull
+ public Builder setHtmlText(@Nullable String htmlText) {
+ mHtmlText = htmlText;
+ return this;
+ }
+
+ /**
+ * Sets the Intent for the item to be constructed.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @NonNull
+ public Builder setIntent(@Nullable Intent intent) {
+ mIntent = intent;
+ return this;
+ }
+
+ /**
+ * Sets the PendingIntent for the item to be constructed. To prevent receiving apps from
+ * improperly manipulating the intent to launch another activity as this caller, the
+ * provided PendingIntent must be immutable (see {@link PendingIntent#FLAG_IMMUTABLE}).
+ * The system will clean up the PendingIntent when it is no longer used.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @NonNull
+ public Builder setPendingIntent(@Nullable PendingIntent pendingIntent) {
+ if (pendingIntent != null && !pendingIntent.isImmutable()) {
+ throw new IllegalArgumentException("Expected pending intent to be immutable");
+ }
+ mPendingIntent = pendingIntent;
+ return this;
+ }
+
+ /**
+ * Sets the URI for the item to be constructed.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @NonNull
+ public Builder setUri(@Nullable Uri uri) {
+ mUri = uri;
+ return this;
+ }
+
+ /**
+ * Constructs a new Item with the properties set on this builder.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @NonNull
+ public Item build() {
+ return new Item(mText, mHtmlText, mIntent, mPendingIntent, mUri);
+ }
+ }
+
/** @hide */
public Item(Item other) {
mText = other.mText;
mHtmlText = other.mHtmlText;
mIntent = other.mIntent;
+ mPendingIntent = other.mPendingIntent;
mUri = other.mUri;
mActivityInfo = other.mActivityInfo;
mTextLinks = other.mTextLinks;
@@ -229,10 +315,7 @@
* Create an Item consisting of a single block of (possibly styled) text.
*/
public Item(CharSequence text) {
- mText = text;
- mHtmlText = null;
- mIntent = null;
- mUri = null;
+ this(text, null, null, null, null);
}
/**
@@ -245,30 +328,21 @@
* </p>
*/
public Item(CharSequence text, String htmlText) {
- mText = text;
- mHtmlText = htmlText;
- mIntent = null;
- mUri = null;
+ this(text, htmlText, null, null, null);
}
/**
* Create an Item consisting of an arbitrary Intent.
*/
public Item(Intent intent) {
- mText = null;
- mHtmlText = null;
- mIntent = intent;
- mUri = null;
+ this(null, null, intent, null, null);
}
/**
* Create an Item consisting of an arbitrary URI.
*/
public Item(Uri uri) {
- mText = null;
- mHtmlText = null;
- mIntent = null;
- mUri = uri;
+ this(null, null, null, null, uri);
}
/**
@@ -276,10 +350,7 @@
* text, Intent, and/or URI.
*/
public Item(CharSequence text, Intent intent, Uri uri) {
- mText = text;
- mHtmlText = null;
- mIntent = intent;
- mUri = uri;
+ this(text, null, intent, null, uri);
}
/**
@@ -289,6 +360,14 @@
* will not be done from HTML formatted text into plain text.
*/
public Item(CharSequence text, String htmlText, Intent intent, Uri uri) {
+ this(text, htmlText, intent, null, uri);
+ }
+
+ /**
+ * Builder ctor.
+ */
+ private Item(CharSequence text, String htmlText, Intent intent, PendingIntent pendingIntent,
+ Uri uri) {
if (htmlText != null && text == null) {
throw new IllegalArgumentException(
"Plain text must be supplied if HTML text is supplied");
@@ -296,6 +375,7 @@
mText = text;
mHtmlText = htmlText;
mIntent = intent;
+ mPendingIntent = pendingIntent;
mUri = uri;
}
@@ -321,6 +401,15 @@
}
/**
+ * Returns the pending intent in this Item.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @Nullable
+ public PendingIntent getPendingIntent() {
+ return mPendingIntent;
+ }
+
+ /**
* Retrieve the raw URI contained in this Item.
*/
public Uri getUri() {
@@ -777,7 +866,7 @@
throw new NullPointerException("item is null");
}
mIcon = null;
- mItems = new ArrayList<Item>();
+ mItems = new ArrayList<>();
mItems.add(item);
mClipDescription.setIsStyledText(isStyledText());
}
@@ -794,7 +883,7 @@
throw new NullPointerException("item is null");
}
mIcon = null;
- mItems = new ArrayList<Item>();
+ mItems = new ArrayList<>();
mItems.add(item);
mClipDescription.setIsStyledText(isStyledText());
}
@@ -826,7 +915,7 @@
public ClipData(ClipData other) {
mClipDescription = other.mClipDescription;
mIcon = other.mIcon;
- mItems = new ArrayList<Item>(other.mItems);
+ mItems = new ArrayList<>(other.mItems);
}
/**
@@ -1042,6 +1131,35 @@
}
/**
+ * Checks if this clip data has a pending intent that is an activity type.
+ * @hide
+ */
+ public boolean hasActivityPendingIntents() {
+ final int size = mItems.size();
+ for (int i = 0; i < size; i++) {
+ final Item item = mItems.get(i);
+ if (item.mPendingIntent != null && item.mPendingIntent.isActivity()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Cleans up all pending intents in the ClipData.
+ * @hide
+ */
+ public void cleanUpPendingIntents() {
+ final int size = mItems.size();
+ for (int i = 0; i < size; i++) {
+ final Item item = mItems.get(i);
+ if (item.mPendingIntent != null) {
+ item.mPendingIntent.cancel();
+ }
+ }
+ }
+
+ /**
* Prepare this {@link ClipData} to leave an app process.
*
* @hide
@@ -1243,6 +1361,7 @@
TextUtils.writeToParcel(item.mText, dest, flags);
dest.writeString8(item.mHtmlText);
dest.writeTypedObject(item.mIntent, flags);
+ dest.writeTypedObject(item.mPendingIntent, flags);
dest.writeTypedObject(item.mUri, flags);
dest.writeTypedObject(mParcelItemActivityInfos ? item.mActivityInfo : null, flags);
dest.writeTypedObject(item.mTextLinks, flags);
@@ -1262,10 +1381,11 @@
CharSequence text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
String htmlText = in.readString8();
Intent intent = in.readTypedObject(Intent.CREATOR);
+ PendingIntent pendingIntent = in.readTypedObject(PendingIntent.CREATOR);
Uri uri = in.readTypedObject(Uri.CREATOR);
ActivityInfo info = in.readTypedObject(ActivityInfo.CREATOR);
TextLinks textLinks = in.readTypedObject(TextLinks.CREATOR);
- Item item = new Item(text, htmlText, intent, uri);
+ Item item = new Item(text, htmlText, intent, pendingIntent, uri);
item.setActivityInfo(info);
item.setTextLinks(textLinks);
mItems.add(item);
@@ -1273,7 +1393,7 @@
}
public static final @android.annotation.NonNull Parcelable.Creator<ClipData> CREATOR =
- new Parcelable.Creator<ClipData>() {
+ new Parcelable.Creator<>() {
@Override
public ClipData createFromParcel(Parcel source) {
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index b8d7543..9e192a0 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -6553,6 +6553,28 @@
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 @@
}
/**
+ * 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 08871d4..e8031a3 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -19,6 +19,7 @@
import static android.app.sdksandbox.SdkSandboxManager.ACTION_START_SANDBOXED_ACTIVITY;
import static android.content.ContentProvider.maybeAddUserId;
import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE;
+import static android.service.chooser.Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA;
import android.Manifest;
import android.accessibilityservice.AccessibilityService;
@@ -55,6 +56,7 @@
import android.os.Build;
import android.os.Bundle;
import android.os.BundleMerger;
+import android.os.CancellationSignal;
import android.os.IBinder;
import android.os.IncidentManager;
import android.os.Parcel;
@@ -72,7 +74,9 @@
import android.provider.DocumentsProvider;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
+import android.service.chooser.AdditionalContentContract;
import android.service.chooser.ChooserAction;
+import android.service.chooser.ChooserResult;
import android.telecom.PhoneAccount;
import android.telecom.TelecomManager;
import android.text.TextUtils;
@@ -1059,7 +1063,7 @@
}
if (sender != null) {
- intent.putExtra(EXTRA_CHOSEN_COMPONENT_INTENT_SENDER, sender);
+ intent.putExtra(EXTRA_CHOOSER_RESULT_INTENT_SENDER, sender);
}
// Migrate any clip data and flags from target.
@@ -6064,6 +6068,62 @@
public static final int CHOOSER_CONTENT_TYPE_ALBUM = 1;
/**
+ * Optional argument used to provide a {@link ContentProvider} {@link Uri} to an
+ * {@link #ACTION_CHOOSER} Intent which allows additional toggleable items to be included
+ * in the sharing UI.
+ * <p>
+ * For example, this could be used to show photos being shared in the context of the user's
+ * entire photo roll, with the option to change the set of photos being shared.
+ * <p>
+ * When this is provided in an {@link #ACTION_CHOOSER} Intent with an {@link #ACTION_SEND} or
+ * {@link #ACTION_SEND_MULTIPLE} target Intent, the sharesheet will query (see
+ * {@link ContentProvider#query(Uri, String[], Bundle, CancellationSignal)}) this URI to
+ * retrieve a set of additional items available for selection. The set of items returned by the
+ * content provider is expected to contain all the items from the {@link #EXTRA_STREAM}
+ * argument, in their relative order, which will be marked as selected. The URI's authority
+ * must be different from any shared items URI provided in {@link #EXTRA_STREAM} or returned by
+ * the provider.
+ *
+ * <p>The {@link Bundle} argument of the
+ * {@link ContentProvider#query(Uri, String[], Bundle, CancellationSignal)}
+ * method will contains the original intent Chooser has been launched with under the
+ * {@link #EXTRA_INTENT} key as a context for the current sharing session. The returned
+ * {@link android.database.Cursor} should contain
+ * {@link android.service.chooser.AdditionalContentContract.Columns#URI} column for the item URI
+ * and, optionally, {@link AdditionalContentContract.CursorExtraKeys#POSITION} extra that
+ * specifies the cursor starting position; the item at this position is expected to match the
+ * item specified by {@link #EXTRA_CHOOSER_FOCUSED_ITEM_POSITION}.</p>
+ *
+ * <p>When the user makes a selection change,
+ * {@link ContentProvider#call(String, String, Bundle)} method will be invoked with the "method"
+ * argument set to
+ * {@link android.service.chooser.AdditionalContentContract.MethodNames#ON_SELECTION_CHANGED},
+ * the "arg" argument set to this argument's value, and the "extras" {@link Bundle} argument
+ * containing {@link #EXTRA_INTENT} key containing the original intent Chooser has been launched
+ * with but with the modified target intent --Chooser will modify the target intent according to
+ * the selection changes made by the user.
+ * Applications may implement this method to change any of the following Chooser arguments by
+ * returning new values in the result bundle:
+ * {@link #EXTRA_CHOOSER_TARGETS}, {@link #EXTRA_ALTERNATE_INTENTS},
+ * {@link #EXTRA_CHOOSER_CUSTOM_ACTIONS},
+ * {@link #EXTRA_CHOOSER_MODIFY_SHARE_ACTION},
+ * {@link #EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER}.</p>
+ */
+ @FlaggedApi(android.service.chooser.Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING)
+ public static final String EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI =
+ "android.intent.extra.CHOOSER_ADDITIONAL_CONTENT_URI";
+
+ /**
+ * Optional argument to be used with {@link #EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI}, used in
+ * combination with {@link #EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI}.
+ * An integer, zero-based index into {@link #EXTRA_STREAM} argument indicating the item that
+ * should be focused by the Chooser in preview.
+ */
+ @FlaggedApi(android.service.chooser.Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING)
+ public static final String EXTRA_CHOOSER_FOCUSED_ITEM_POSITION =
+ "android.intent.extra.CHOOSER_FOCUSED_ITEM_POSITION";
+
+ /**
* An {@code ArrayList} of {@code String} annotations describing content for
* {@link #ACTION_CHOOSER}.
*
@@ -6156,6 +6216,17 @@
public static final String EXTRA_INITIAL_INTENTS = "android.intent.extra.INITIAL_INTENTS";
/**
+ * A CharSequence of additional text describing the content being shared. This text will be
+ * displayed to the user as a part of the sharesheet when included in an
+ * {@link #ACTION_CHOOSER} {@link Intent}.
+ *
+ * <p>e.g. When sharing a photo, metadata could inform the user that location data is included
+ * in the photo they are sharing.</p>
+ */
+ @FlaggedApi(FLAG_ENABLE_SHARESHEET_METADATA_EXTRA)
+ public static final String EXTRA_METADATA_TEXT = "android.intent.extra.METADATA_TEXT";
+
+ /**
* A {@link IntentSender} to start after instant app installation success.
* @hide
*/
@@ -6296,6 +6367,25 @@
"android.intent.extra.CHOSEN_COMPONENT_INTENT_SENDER";
/**
+ * An {@link IntentSender} that will be notified when a user successfully chooses a target
+ * component or initiates an action such as copy or edit within an {@link #ACTION_CHOOSER}
+ * activity. The IntentSender will have the extra {@link #EXTRA_CHOOSER_RESULT} describing
+ * the result.
+ */
+ @FlaggedApi(android.service.chooser.Flags.FLAG_ENABLE_CHOOSER_RESULT)
+ public static final String EXTRA_CHOOSER_RESULT_INTENT_SENDER =
+ "android.intent.extra.CHOOSER_RESULT_INTENT_SENDER";
+
+ /**
+ * A {@link ChooserResult} which describes how the sharing session completed.
+ * <p>
+ * An instance is supplied to the optional IntentSender provided to
+ * {@link #createChooser(Intent, CharSequence, IntentSender)} when the session completes.
+ */
+ @FlaggedApi(android.service.chooser.Flags.FLAG_ENABLE_CHOOSER_RESULT)
+ public static final String EXTRA_CHOOSER_RESULT = "android.intent.extra.CHOOSER_RESULT";
+
+ /**
* The {@link ComponentName} chosen by the user to complete an action.
*
* @see #EXTRA_CHOSEN_COMPONENT_INTENT_SENDER
diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java
index d913581..e290722 100644
--- a/core/java/android/content/IntentFilter.java
+++ b/core/java/android/content/IntentFilter.java
@@ -677,7 +677,7 @@
* 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 @@
}
// 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 @@
*
* @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 fb95608895..c6f5220 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 407ffbb..49c8a7c 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 static final String FEATURE_AUDIO_PRO = "android.hardware.audio.pro";
/**
+ * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}
+ * which indicates whether head tracking for spatial audio operates with low-latency,
+ * as defined by the CDD criteria for the feature.
+ *
+ */
+ @SdkConstant(SdkConstantType.FEATURE)
+ @FlaggedApi(FLAG_FEATURE_SPATIAL_AUDIO_HEADTRACKING_LOW_LATENCY)
+ public static final String FEATURE_AUDIO_SPATIAL_HEADTRACKING_LOW_LATENCY =
+ "android.hardware.audio.spatial.headtracking.low_latency";
+
+ /**
* Feature for {@link #getSystemAvailableFeatures} and
* {@link #hasSystemFeature}: The device is capable of communicating with
* other devices via Bluetooth.
diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java
index 2b378b1..5b0cee7 100644
--- a/core/java/android/content/pm/ServiceInfo.java
+++ b/core/java/android/content/pm/ServiceInfo.java
@@ -142,6 +142,28 @@
* the {@link android.R.attr#foregroundServiceType} attribute.
* Data(photo, file, account) upload/download, backup/restore, import/export, fetch,
* transfer over network between device and cloud.
+ *
+ * <p>This type has time limit of 6 hours starting from Android version
+ * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}.
+ * A foreground service of this type must be stopped within the timeout by
+ * {@link android.app.Service#stopSelf()},
+ * {@link android.content.Context#stopService(android.content.Intent)} or their overloads).
+ * {@link android.app.Service#stopForeground(int)} will also work, which will demote the
+ * service to a "background" service, which will soon be stopped by the system.
+ *
+ * <p>If the service isn't stopped within the timeout,
+ * {@link android.app.Service#onTimeout(int, int)} will be called.
+ *
+ * <p>Also note, even though
+ * {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC} can be used on
+ * Android versions prior to {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, since
+ * {@link android.app.Service#onTimeout(int, int)} did not exist on such versions, it will
+ * never be called.
+ *
+ * Because of this, developers must make sure to stop the foreground service even if
+ * {@link android.app.Service#onTimeout(int, int)} is not called on such versions.
+ *
+ * @see android.app.Service#onTimeout(int, int)
*/
@RequiresPermission(
value = Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC,
@@ -483,6 +505,27 @@
* Constant corresponding to {@code mediaProcessing} in
* the {@link android.R.attr#foregroundServiceType} attribute.
* Media processing use cases such as video or photo editing and processing.
+ *
+ * This type has time limit of 6 hours.
+ * A foreground service of this type must be stopped within the timeout by
+ * {@link android.app.Service#stopSelf()},
+ * {@link android.content.Context#stopService(android.content.Intent)} or their overloads).
+ * {@link android.app.Service#stopForeground(int)} will also work, which will demote the
+ * service to a "background" service, which will soon be stopped by the system.
+ *
+ * <p>If the service isn't stopped within the timeout,
+ * {@link android.app.Service#onTimeout(int, int)} will be called.
+ *
+ * <p>Also note, even though
+ * {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING} was added in
+ * Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, it can be also used
+ * on prior android versions (just like other new foreground service types can be used).
+ * However, because {@link android.app.Service#onTimeout(int, int)} did not exist on prior
+ * versions, it will never be called on such versions.
+ * Because of this, developers must make sure to stop the foreground service even if
+ * {@link android.app.Service#onTimeout(int, int)} is not called on such versions.
+ *
+ * @see android.app.Service#onTimeout(int, int)
*/
@RequiresPermission(
value = Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING
diff --git a/core/java/android/content/pm/overlay/OverlayPaths.java b/core/java/android/content/pm/overlay/OverlayPaths.java
index a4db733..bd74b0b 100644
--- a/core/java/android/content/pm/overlay/OverlayPaths.java
+++ b/core/java/android/content/pm/overlay/OverlayPaths.java
@@ -49,6 +49,13 @@
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/services/core/java/com/android/server/devicestate/DeviceState.java b/core/java/android/hardware/devicestate/DeviceState.java
similarity index 96%
rename from services/core/java/com/android/server/devicestate/DeviceState.java
rename to core/java/android/hardware/devicestate/DeviceState.java
index 2ba59b0..5a34905 100644
--- a/services/core/java/com/android/server/devicestate/DeviceState.java
+++ b/core/java/android/hardware/devicestate/DeviceState.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.server.devicestate;
+package android.hardware.devicestate;
import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
@@ -22,7 +22,6 @@
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
-import android.hardware.devicestate.DeviceStateManager;
import com.android.internal.util.Preconditions;
@@ -38,9 +37,9 @@
* state of the system. This is useful for variable-state devices, like foldable or rollable
* devices, that can be configured by users into differing hardware states, which each may have a
* different expected use case.
+ * @hide
*
- * @see DeviceStateProvider
- * @see DeviceStateManagerService
+ * @see DeviceStateManager
*/
public final class DeviceState {
/**
diff --git a/core/java/android/hardware/usb/UsbPortStatus.java b/core/java/android/hardware/usb/UsbPortStatus.java
index 4a5c4c8..8616b6b 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 com.android.internal.annotations.Immutable;
-import java.lang.StringBuilder;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -580,6 +577,21 @@
}
/**
+ * 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 0000000..cc56a31
--- /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 a6b2ed0..e090942 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -1899,7 +1899,7 @@
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 @@
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 0000000..cc77f5b
--- /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 ce03798..89576ed 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -1427,8 +1427,8 @@
public static final String DISALLOW_RECORD_AUDIO = "no_record_audio";
/**
- * Specifies if a user is not allowed to run in the background and should be stopped during
- * user switch. The default value is <code>false</code>.
+ * Specifies if a user is not allowed to run in the background and should be stopped and locked
+ * during user switch. The default value is <code>false</code>.
*
* <p>This restriction can be set by device owners and profile owners.
*
@@ -1955,6 +1955,26 @@
"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 @@
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 6941857..2cb16d337b 100644
--- a/core/java/android/os/storage/OWNERS
+++ b/core/java/android/os/storage/OWNERS
@@ -10,7 +10,6 @@
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 ea9375e..d485eca 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -16,13 +16,6 @@
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 3d1deea..a16d21e 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 @@
*
* @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 3d711a7..7202d9a 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 @@
*
* @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 2880454..2065d5d 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 @@
*
* @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 8c70501..9d7fb70 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -94,3 +94,11 @@
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/BlockedNumberContract.java b/core/java/android/provider/BlockedNumberContract.java
index 5d00b29..4075e90 100644
--- a/core/java/android/provider/BlockedNumberContract.java
+++ b/core/java/android/provider/BlockedNumberContract.java
@@ -15,12 +15,20 @@
*/
package android.provider;
+import android.Manifest;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import android.annotation.WorkerThread;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.telecom.Log;
+import android.telecom.TelecomManager;
+
+import com.android.server.telecom.flags.Flags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -214,6 +222,333 @@
* <p>TYPE: String</p>
*/
public static final String COLUMN_E164_NUMBER = "e164_number";
+
+ /**
+ * A protected broadcast intent action for letting components with
+ * {@link android.Manifest.permission#READ_BLOCKED_NUMBERS} know that the block suppression
+ * status as returned by {@link #getBlockSuppressionStatus(Context)} has been updated.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static final String ACTION_BLOCK_SUPPRESSION_STATE_CHANGED =
+ "android.provider.action.BLOCK_SUPPRESSION_STATE_CHANGED";
+
+ /**
+ * Preference key of block numbers not in contacts setting.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static final String ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED =
+ "block_numbers_not_in_contacts_setting";
+
+ /**
+ * Preference key of block private number calls setting.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static final String ENHANCED_SETTING_KEY_BLOCK_PRIVATE =
+ "block_private_number_calls_setting";
+
+ /**
+ * Preference key of block payphone calls setting.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static final String ENHANCED_SETTING_KEY_BLOCK_PAYPHONE =
+ "block_payphone_calls_setting";
+
+ /**
+ * Preference key of block unknown calls setting.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static final String ENHANCED_SETTING_KEY_BLOCK_UNKNOWN =
+ "block_unknown_calls_setting";
+
+ /**
+ * Preference key for whether should show an emergency call notification.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static final String ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION =
+ "show_emergency_call_notification";
+
+ /**
+ * Preference key of block unavailable calls setting.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static final String ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE =
+ "block_unavailable_calls_setting";
+
+ /**
+ * Notifies the provider that emergency services were contacted by the user.
+ * <p> This results in {@link #shouldSystemBlockNumber} returning {@code false} independent
+ * of the contents of the provider for a duration defined by
+ * {@link android.telephony.CarrierConfigManager#KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT}
+ * the provider unless {@link #endBlockSuppression(Context)} is called.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.READ_BLOCKED_NUMBERS,
+ android.Manifest.permission.WRITE_BLOCKED_NUMBERS
+ })
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static void notifyEmergencyContact(@NonNull Context context) {
+ verifyBlockedNumbersPermission(context);
+ try {
+ Log.i(LOG_TAG, "notifyEmergencyContact; caller=%s", context.getOpPackageName());
+ context.getContentResolver().call(
+ AUTHORITY_URI, SystemContract.METHOD_NOTIFY_EMERGENCY_CONTACT, null, null);
+ } catch (NullPointerException | IllegalArgumentException ex) {
+ // The content resolver can throw an NPE or IAE; we don't want to crash Telecom if
+ // either of these happen.
+ Log.w(null, "notifyEmergencyContact: provider not ready.");
+ }
+ }
+
+ /**
+ * Notifies the provider to disable suppressing blocking. If emergency services were not
+ * contacted recently at all, calling this method is a no-op.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.READ_BLOCKED_NUMBERS,
+ android.Manifest.permission.WRITE_BLOCKED_NUMBERS
+ })
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static void endBlockSuppression(@NonNull Context context) {
+ verifyBlockedNumbersPermission(context);
+ String caller = context.getOpPackageName();
+ Log.i(LOG_TAG, "endBlockSuppression: caller=%s", caller);
+ context.getContentResolver().call(
+ AUTHORITY_URI, SystemContract.METHOD_END_BLOCK_SUPPRESSION, null, null);
+ }
+
+ /**
+ * Returns {@code true} if {@code phoneNumber} is blocked taking
+ * {@link #notifyEmergencyContact(Context)} into consideration. If emergency services
+ * have not been contacted recently and enhanced call blocking not been enabled, this
+ * method is equivalent to {@link #isBlocked(Context, String)}.
+ *
+ * @param context the context of the caller.
+ * @param phoneNumber the number to check.
+ * @param numberPresentation the presentation code associated with the call.
+ * @param isNumberInContacts indicates if the provided number exists as a contact.
+ * @return result code indicating if the number should be blocked, and if so why.
+ * Valid values are: {@link #STATUS_NOT_BLOCKED}, {@link #STATUS_BLOCKED_IN_LIST},
+ * {@link #STATUS_BLOCKED_NOT_IN_CONTACTS}, {@link #STATUS_BLOCKED_PAYPHONE},
+ * {@link #STATUS_BLOCKED_RESTRICTED}, {@link #STATUS_BLOCKED_UNKNOWN_NUMBER}.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.READ_BLOCKED_NUMBERS,
+ android.Manifest.permission.WRITE_BLOCKED_NUMBERS
+ })
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static int shouldSystemBlockNumber(@NonNull Context context,
+ @NonNull String phoneNumber, @TelecomManager.Presentation int numberPresentation,
+ boolean isNumberInContacts) {
+ verifyBlockedNumbersPermission(context);
+ try {
+ String caller = context.getOpPackageName();
+ Bundle extras = new Bundle();
+ extras.putInt(BlockedNumberContract.EXTRA_CALL_PRESENTATION, numberPresentation);
+ extras.putBoolean(BlockedNumberContract.EXTRA_CONTACT_EXIST, isNumberInContacts);
+ final Bundle res = context.getContentResolver().call(AUTHORITY_URI,
+ SystemContract.METHOD_SHOULD_SYSTEM_BLOCK_NUMBER, phoneNumber, extras);
+ int blockResult = res != null ? res.getInt(RES_BLOCK_STATUS, STATUS_NOT_BLOCKED) :
+ BlockedNumberContract.STATUS_NOT_BLOCKED;
+ Log.d(LOG_TAG, "shouldSystemBlockNumber: number=%s, caller=%s, result=%s",
+ Log.piiHandle(phoneNumber), caller,
+ SystemContract.blockStatusToString(blockResult));
+ return blockResult;
+ } catch (NullPointerException | IllegalArgumentException ex) {
+ // The content resolver can throw an NPE or IAE; we don't want to crash Telecom if
+ // either of these happen.
+ Log.w(null, "shouldSystemBlockNumber: provider not ready.");
+ return BlockedNumberContract.STATUS_NOT_BLOCKED;
+ }
+ }
+
+ /**
+ * Returns the current status of block suppression.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.READ_BLOCKED_NUMBERS,
+ android.Manifest.permission.WRITE_BLOCKED_NUMBERS
+ })
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static @NonNull BlockSuppressionStatus getBlockSuppressionStatus(
+ @NonNull Context context) {
+ verifyBlockedNumbersPermission(context);
+ final Bundle res = context.getContentResolver().call(
+ AUTHORITY_URI, SystemContract.METHOD_GET_BLOCK_SUPPRESSION_STATUS, null, null);
+ BlockSuppressionStatus blockSuppressionStatus = new BlockSuppressionStatus(
+ res.getBoolean(SystemContract.RES_IS_BLOCKING_SUPPRESSED, false),
+ res.getLong(SystemContract.RES_BLOCKING_SUPPRESSED_UNTIL_TIMESTAMP, 0));
+ Log.d(LOG_TAG, "getBlockSuppressionStatus: caller=%s, status=%s",
+ context.getOpPackageName(), blockSuppressionStatus);
+ return blockSuppressionStatus;
+ }
+
+ /**
+ * Check whether should show the emergency call notification.
+ *
+ * @param context the context of the caller.
+ * @return {@code true} if should show emergency call notification. {@code false} otherwise.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.READ_BLOCKED_NUMBERS,
+ android.Manifest.permission.WRITE_BLOCKED_NUMBERS
+ })
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static boolean shouldShowEmergencyCallNotification(@NonNull Context context) {
+ verifyBlockedNumbersPermission(context);
+ try {
+ final Bundle res = context.getContentResolver().call(AUTHORITY_URI,
+ SystemContract.METHOD_SHOULD_SHOW_EMERGENCY_CALL_NOTIFICATION, null, null);
+ return res != null && res.getBoolean(RES_SHOW_EMERGENCY_CALL_NOTIFICATION, false);
+ } catch (NullPointerException | IllegalArgumentException ex) {
+ // The content resolver can throw an NPE or IAE; we don't want to crash Telecom if
+ // either of these happen.
+ Log.w(null, "shouldShowEmergencyCallNotification: provider not ready.");
+ return false;
+ }
+ }
+
+ /**
+ * Check whether the enhanced block setting is enabled.
+ *
+ * @param context the context of the caller.
+ * @param key the key of the setting to check, can be
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_PRIVATE}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_PAYPHONE}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_UNKNOWN}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION}
+ * @return {@code true} if the setting is enabled. {@code false} otherwise.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.READ_BLOCKED_NUMBERS,
+ android.Manifest.permission.WRITE_BLOCKED_NUMBERS
+ })
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static boolean getBlockedNumberSetting(
+ @NonNull Context context, @NonNull String key) {
+ verifyBlockedNumbersPermission(context);
+ Bundle extras = new Bundle();
+ extras.putString(EXTRA_ENHANCED_SETTING_KEY, key);
+ try {
+ final Bundle res = context.getContentResolver().call(AUTHORITY_URI,
+ SystemContract.METHOD_GET_ENHANCED_BLOCK_SETTING, null, extras);
+ return res != null && res.getBoolean(RES_ENHANCED_SETTING_IS_ENABLED, false);
+ } catch (NullPointerException | IllegalArgumentException ex) {
+ // The content resolver can throw an NPE or IAE; we don't want to crash Telecom if
+ // either of these happen.
+ Log.w(null, "getEnhancedBlockSetting: provider not ready.");
+ return false;
+ }
+ }
+
+ /**
+ * Set the enhanced block setting enabled status.
+ *
+ * @param context the context of the caller.
+ * @param key the key of the setting to set, can be
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_PRIVATE}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_PAYPHONE}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_UNKNOWN}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION}
+ * @param value the enabled statue of the setting to set.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.READ_BLOCKED_NUMBERS,
+ android.Manifest.permission.WRITE_BLOCKED_NUMBERS
+ })
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static void setBlockedNumberSetting(@NonNull Context context,
+ @NonNull String key, boolean value) {
+ verifyBlockedNumbersPermission(context);
+ Bundle extras = new Bundle();
+ extras.putString(EXTRA_ENHANCED_SETTING_KEY, key);
+ extras.putBoolean(EXTRA_ENHANCED_SETTING_VALUE, value);
+ context.getContentResolver().call(AUTHORITY_URI,
+ SystemContract.METHOD_SET_ENHANCED_BLOCK_SETTING, null, extras);
+ }
+
+ /**
+ * Represents the current status of
+ * {@link #shouldSystemBlockNumber(Context, String, int, boolean)}. If emergency services
+ * have been contacted recently, {@link #mIsSuppressed} is {@code true}, and blocking
+ * is disabled until the timestamp {@link #mUntilTimestampMillis}.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static final class BlockSuppressionStatus {
+ private boolean mIsSuppressed;
+
+ /**
+ * Timestamp in milliseconds from epoch.
+ */
+ private long mUntilTimestampMillis;
+
+ public BlockSuppressionStatus(boolean isSuppressed, long untilTimestampMillis) {
+ this.mIsSuppressed = isSuppressed;
+ this.mUntilTimestampMillis = untilTimestampMillis;
+ }
+
+ @Override
+ public String toString() {
+ return "[BlockSuppressionStatus; isSuppressed=" + mIsSuppressed + ", until="
+ + mUntilTimestampMillis + "]";
+ }
+
+ public boolean getIsSuppressed() {
+ return mIsSuppressed;
+ }
+
+ public long getUntilTimestampMillis() {
+ return mUntilTimestampMillis;
+ }
+ }
+
+ /**
+ * Verifies that the caller holds both the
+ * {@link android.Manifest.permission#READ_BLOCKED_NUMBERS} permission and the
+ * {@link android.Manifest.permission#WRITE_BLOCKED_NUMBERS} permission.
+ *
+ * @param context
+ * @throws SecurityException if the caller is missing the necessary permissions
+ */
+ private static void verifyBlockedNumbersPermission(Context context) {
+ context.enforceCallingOrSelfPermission(Manifest.permission.READ_BLOCKED_NUMBERS,
+ "Caller does not have the android.permission.READ_BLOCKED_NUMBERS permission");
+ context.enforceCallingOrSelfPermission(Manifest.permission.WRITE_BLOCKED_NUMBERS,
+ "Caller does not have the android.permission.WRITE_BLOCKED_NUMBERS permission");
+ }
}
/** @hide */
@@ -558,7 +893,7 @@
* {@link #ENHANCED_SETTING_KEY_BLOCK_PAYPHONE}
* {@link #ENHANCED_SETTING_KEY_BLOCK_UNKNOWN}
* {@link #ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE}
- * {@link #ENHANCED_SETTING_KEY_EMERGENCY_CALL_NOTIFICATION_SHOWING}
+ * {@link #ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION}
* @return {@code true} if the setting is enabled. {@code false} otherwise.
*/
public static boolean getEnhancedBlockSetting(Context context, String key) {
@@ -586,7 +921,7 @@
* {@link #ENHANCED_SETTING_KEY_BLOCK_PAYPHONE}
* {@link #ENHANCED_SETTING_KEY_BLOCK_UNKNOWN}
* {@link #ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE}
- * {@link #ENHANCED_SETTING_KEY_EMERGENCY_CALL_NOTIFICATION_SHOWING}
+ * {@link #ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION}
* @param value the enabled statue of the setting to set.
*/
public static void setEnhancedBlockSetting(Context context, String key, boolean value) {
diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java
index 7d127ad..c13dd36 100644
--- a/core/java/android/provider/CallLog.java
+++ b/core/java/android/provider/CallLog.java
@@ -19,6 +19,7 @@
import android.Manifest;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.LongDef;
import android.annotation.NonNull;
@@ -55,6 +56,8 @@
import android.text.TextUtils;
import android.util.Log;
+import com.android.server.telecom.flags.Flags;
+
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
@@ -427,6 +430,8 @@
private double mLongitude = Double.NaN;
private Uri mPictureUri;
private int mIsPhoneAccountMigrationPending;
+ private boolean mIsBusinessCall;
+ private String mBusinessName;
/**
* @param callerInfo the CallerInfo object to get the target contact from.
@@ -645,15 +650,44 @@
}
/**
+ * @param isBusinessCall should be set if the caller is a business call
+ */
+ @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public @NonNull AddCallParametersBuilder setIsBusinessCall(boolean isBusinessCall) {
+ mIsBusinessCall = isBusinessCall;
+ return this;
+ }
+
+ /**
+ * @param businessName should be set if the caller is a business call
+ */
+ @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public @NonNull AddCallParametersBuilder setBusinessName(String businessName) {
+ mBusinessName = businessName;
+ return this;
+ }
+
+ /**
* Builds the object
*/
public @NonNull AddCallParams build() {
- return new AddCallParams(mCallerInfo, mNumber, mPostDialDigits, mViaNumber,
- mPresentation, mCallType, mFeatures, mAccountHandle, mStart, mDuration,
- mDataUsage, mAddForAllUsers, mUserToBeInsertedTo, mIsRead, mCallBlockReason,
- mCallScreeningAppName, mCallScreeningComponentName, mMissedReason,
- mPriority, mSubject, mLatitude, mLongitude, mPictureUri,
- mIsPhoneAccountMigrationPending);
+ if (Flags.businessCallComposer()) {
+ return new AddCallParams(mCallerInfo, mNumber, mPostDialDigits, mViaNumber,
+ mPresentation, mCallType, mFeatures, mAccountHandle, mStart, mDuration,
+ mDataUsage, mAddForAllUsers, mUserToBeInsertedTo, mIsRead,
+ mCallBlockReason,
+ mCallScreeningAppName, mCallScreeningComponentName, mMissedReason,
+ mPriority, mSubject, mLatitude, mLongitude, mPictureUri,
+ mIsPhoneAccountMigrationPending, mIsBusinessCall, mBusinessName);
+ } else {
+ return new AddCallParams(mCallerInfo, mNumber, mPostDialDigits, mViaNumber,
+ mPresentation, mCallType, mFeatures, mAccountHandle, mStart, mDuration,
+ mDataUsage, mAddForAllUsers, mUserToBeInsertedTo, mIsRead,
+ mCallBlockReason,
+ mCallScreeningAppName, mCallScreeningComponentName, mMissedReason,
+ mPriority, mSubject, mLatitude, mLongitude, mPictureUri,
+ mIsPhoneAccountMigrationPending);
+ }
}
}
@@ -681,6 +715,8 @@
private double mLongitude = Double.NaN;
private Uri mPictureUri;
private int mIsPhoneAccountMigrationPending;
+ private boolean mIsBusinessCall;
+ private String mBusinessName;
private AddCallParams(CallerInfo callerInfo, String number, String postDialDigits,
String viaNumber, int presentation, int callType, int features,
@@ -717,6 +753,43 @@
mIsPhoneAccountMigrationPending = isPhoneAccountMigrationPending;
}
+ private AddCallParams(CallerInfo callerInfo, String number, String postDialDigits,
+ String viaNumber, int presentation, int callType, int features,
+ PhoneAccountHandle accountHandle, long start, int duration, long dataUsage,
+ boolean addForAllUsers, UserHandle userToBeInsertedTo, boolean isRead,
+ int callBlockReason,
+ CharSequence callScreeningAppName, String callScreeningComponentName,
+ long missedReason,
+ int priority, String subject, double latitude, double longitude, Uri pictureUri,
+ int isPhoneAccountMigrationPending, boolean isBusinessCall, String businessName) {
+ mCallerInfo = callerInfo;
+ mNumber = number;
+ mPostDialDigits = postDialDigits;
+ mViaNumber = viaNumber;
+ mPresentation = presentation;
+ mCallType = callType;
+ mFeatures = features;
+ mAccountHandle = accountHandle;
+ mStart = start;
+ mDuration = duration;
+ mDataUsage = dataUsage;
+ mAddForAllUsers = addForAllUsers;
+ mUserToBeInsertedTo = userToBeInsertedTo;
+ mIsRead = isRead;
+ mCallBlockReason = callBlockReason;
+ mCallScreeningAppName = callScreeningAppName;
+ mCallScreeningComponentName = callScreeningComponentName;
+ mMissedReason = missedReason;
+ mPriority = priority;
+ mSubject = subject;
+ mLatitude = latitude;
+ mLongitude = longitude;
+ mPictureUri = pictureUri;
+ mIsPhoneAccountMigrationPending = isPhoneAccountMigrationPending;
+ mIsBusinessCall = isBusinessCall;
+ mBusinessName = businessName;
+ }
+
}
/**
@@ -915,6 +988,19 @@
*/
public static final String NUMBER = "number";
+
+ /**
+ * Boolean indicating whether the call is a business call.
+ */
+ @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public static final String IS_BUSINESS_CALL = "is_business_call";
+
+ /**
+ * String that stores the asserted display name associated with business call.
+ */
+ @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public static final String ASSERTED_DISPLAY_NAME = "asserted_display_name";
+
/**
* The number presenting rules set by the network.
*
@@ -1713,7 +1799,6 @@
}
ContentValues values = new ContentValues(14);
-
values.put(NUMBER, params.mNumber);
values.put(POST_DIAL_DIGITS, params.mPostDialDigits);
values.put(VIA_NUMBER, params.mViaNumber);
@@ -1746,7 +1831,10 @@
values.put(COMPOSER_PHOTO_URI, params.mPictureUri.toString());
}
values.put(IS_PHONE_ACCOUNT_MIGRATION_PENDING, params.mIsPhoneAccountMigrationPending);
-
+ if (Flags.businessCallComposer()) {
+ values.put(IS_BUSINESS_CALL, Integer.valueOf(params.mIsBusinessCall ? 1 : 0));
+ values.put(ASSERTED_DISPLAY_NAME, params.mBusinessName);
+ }
if ((params.mCallerInfo != null) && (params.mCallerInfo.getContactId() > 0)) {
// Update usage information for the number associated with the contact ID.
// We need to use both the number and the ID for obtaining a data ID since other
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index b026ce9..ecf1937 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -11061,6 +11061,15 @@
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 @@
"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 658cec8..88bd87e 100644
--- a/core/java/android/provider/Telephony.java
+++ b/core/java/android/provider/Telephony.java
@@ -4956,6 +4956,26 @@
*/
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 @@
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 0000000..f679e8a
--- /dev/null
+++ b/core/java/android/service/chooser/AdditionalContentContract.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.chooser;
+
+import android.annotation.FlaggedApi;
+
+/**
+ * Specifies constants used by Chooser when interacting with the additional content provider,
+ * see {@link android.content.Intent#EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI}.
+ */
+@FlaggedApi(android.service.chooser.Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING)
+public interface AdditionalContentContract {
+
+ interface Columns {
+ /**
+ * Content URI for this item.
+ * <p>
+ * Note that this content URI must have a different authority from the content provided
+ * given in {@link android.content.Intent#EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI}.
+ */
+ String URI = "uri";
+ }
+
+ /**
+ * Constants for {@link android.database.Cursor#getExtras} keys.
+ */
+ interface CursorExtraKeys {
+ /**
+ * An integer, zero-based cursor position that corresponds to the URI specified
+ * with the {@link android.content.Intent#EXTRA_CHOOSER_FOCUSED_ITEM_POSITION} index into
+ * the @link android.content.Intent#EXTRA_STREAM} array.
+ */
+ String POSITION = "position";
+ }
+
+ /**
+ * Constants for method names used with {@link android.content.ContentResolver#call} method.
+ */
+ interface MethodNames {
+ /**
+ * A method name Chooser is using to notify the sharing app about a shared items selection
+ * change.
+ */
+ String ON_SELECTION_CHANGED = "onSelectionChanged";
+ }
+}
diff --git a/core/java/android/service/chooser/ChooserResult.java b/core/java/android/service/chooser/ChooserResult.java
new file mode 100644
index 0000000..4603be1
--- /dev/null
+++ b/core/java/android/service/chooser/ChooserResult.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.chooser;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
+import android.compat.annotation.Overridable;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * An event reported to a supplied [IntentSender] by the system chooser when an activity is selected
+ * or other actions are taken to complete the session.
+ *
+ * @see Intent#EXTRA_CHOOSER_RESULT_INTENT_SENDER
+ */
+@FlaggedApi(android.service.chooser.Flags.FLAG_ENABLE_CHOOSER_RESULT)
+public final class ChooserResult implements Parcelable {
+
+ /**
+ * Controls whether to send ChooserResult to the optional IntentSender supplied to the Chooser.
+ * <p>
+ * When enabled, ChooserResult is added to the provided Intent as
+ * {@link Intent#EXTRA_CHOOSER_RESULT}, and sent for actions such as copy and edit, in addition
+ * to activity selection. When disabled, only the selected component
+ * is provided in {@link Intent#EXTRA_CHOSEN_COMPONENT}.
+ * <p>
+ * See: {@link Intent#createChooser(Intent, CharSequence, IntentSender)}
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Overridable
+ public static final long SEND_CHOOSER_RESULT = 263474465L;
+
+ /** @hide */
+ @IntDef({
+ CHOOSER_RESULT_UNKNOWN,
+ CHOOSER_RESULT_SELECTED_COMPONENT,
+ CHOOSER_RESULT_COPY,
+ CHOOSER_RESULT_EDIT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ResultType { }
+
+ /** An unknown action was taken to complete the session. */
+ public static final int CHOOSER_RESULT_UNKNOWN = -1;
+ /** The session was completed by selecting an activity to launch. */
+ public static final int CHOOSER_RESULT_SELECTED_COMPONENT = 0;
+ /** The session was completed by invoking the copy action. */
+ public static final int CHOOSER_RESULT_COPY = 1;
+ /** The session was completed by invoking the edit action. */
+ public static final int CHOOSER_RESULT_EDIT = 2;
+
+ @ResultType
+ private final int mType;
+ private final ComponentName mSelectedComponent;
+ private final boolean mIsShortcut;
+
+ private ChooserResult(@NonNull Parcel source) {
+ mType = source.readInt();
+ mSelectedComponent = ComponentName.readFromParcel(source);
+ mIsShortcut = source.readBoolean();
+ }
+
+ /** @hide */
+ public ChooserResult(@ResultType int type, @Nullable ComponentName componentName,
+ boolean isShortcut) {
+ mType = type;
+ mSelectedComponent = componentName;
+ mIsShortcut = isShortcut;
+ }
+
+ /**
+ * The type of the result.
+ *
+ * @return the type of the result
+ */
+ @ResultType
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Provides the component of the Activity selected for results with type
+ * when type is {@link ChooserResult#CHOOSER_RESULT_SELECTED_COMPONENT}.
+ * <p>
+ * For all other types, this value is null.
+ *
+ * @return the component name selected
+ */
+ @Nullable
+ public ComponentName getSelectedComponent() {
+ return mSelectedComponent;
+ }
+
+ /**
+ * Whether the selected component was provided by the app from as a shortcut.
+ *
+ * @return true if the selected component is a shortcut, false otherwise
+ */
+ public boolean isShortcut() {
+ return mIsShortcut;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Parcelable.Creator<ChooserResult> CREATOR =
+ new Creator<>() {
+ @Override
+ public ChooserResult createFromParcel(Parcel source) {
+ return new ChooserResult(source);
+ }
+
+ @Override
+ public ChooserResult[] newArray(int size) {
+ return new ChooserResult[0];
+ }
+ };
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mType);
+ ComponentName.writeToParcel(mSelectedComponent, dest);
+ dest.writeBoolean(mIsShortcut);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ChooserResult that = (ChooserResult) o;
+ return mType == that.mType
+ && mIsShortcut == that.mIsShortcut
+ && Objects.equals(mSelectedComponent, that.mSelectedComponent);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mType, mSelectedComponent, mIsShortcut);
+ }
+}
diff --git a/core/java/android/service/notification/ZenDeviceEffects.java b/core/java/android/service/notification/ZenDeviceEffects.java
index 90049e6..22b1be0 100644
--- a/core/java/android/service/notification/ZenDeviceEffects.java
+++ b/core/java/android/service/notification/ZenDeviceEffects.java
@@ -20,6 +20,7 @@
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 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 @@
FIELD_DISABLE_TOUCH,
FIELD_MINIMIZE_RADIO_USAGE,
FIELD_MAXIMIZE_DOZE,
+ FIELD_EXTRA_EFFECTS
})
@Retention(RetentionPolicy.SOURCE)
public @interface ModifiableField {}
@@ -95,6 +100,12 @@
* @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 @@
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 @@
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 @@
&& 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 @@
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 @@
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 @@
}
/**
- * 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 @@
}
/**
+ * (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 @@
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 @@
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 @@
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 @@
mDisableTouch = zenDeviceEffects.shouldDisableTouch();
mMinimizeRadioUsage = zenDeviceEffects.shouldMinimizeRadioUsage();
mMaximizeDoze = zenDeviceEffects.shouldMaximizeDoze();
+ mExtraEffects.addAll(zenDeviceEffects.getExtraEffects());
}
/**
@@ -450,7 +501,7 @@
}
/**
- * 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 @@
}
/**
+ * 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 @@
if (effects.shouldDisableTouch()) setShouldDisableTouch(true);
if (effects.shouldMinimizeRadioUsage()) setShouldMinimizeRadioUsage(true);
if (effects.shouldMaximizeDoze()) setShouldMaximizeDoze(true);
+ addExtraEffects(effects.getExtraEffects());
return this;
}
@@ -487,7 +587,7 @@
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 d4a5356..f169ecd 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -27,6 +27,7 @@
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.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 @@
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 @@
.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 @@
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 @@
}
}
+ 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 @@
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 4f6eb88..c2036a4 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 @@
/**
* 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
similarity index 61%
copy from packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneModel.kt
copy to core/java/android/service/voice/VisualQueryAttentionResult.aidl
index f3d549f..38c8f07 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 0000000..690990b
--- /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 13cdfde..322148a 100644
--- a/core/java/android/service/voice/VisualQueryDetectedResult.java
+++ b/core/java/android/service/voice/VisualQueryDetectedResult.java
@@ -82,8 +82,6 @@
}
-
-
// 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 b60c775..887b575 100644
--- a/core/java/android/service/voice/VisualQueryDetectionService.java
+++ b/core/java/android/service/voice/VisualQueryDetectionService.java
@@ -262,23 +262,82 @@
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/BoringLayout.java b/core/java/android/text/BoringLayout.java
index 6410609..2028c40 100644
--- a/core/java/android/text/BoringLayout.java
+++ b/core/java/android/text/BoringLayout.java
@@ -711,18 +711,21 @@
public void draw(Canvas c, Path highlight, Paint highlightpaint,
int cursorOffset) {
if (mDirect != null && highlight == null) {
+ float leftShift = 0;
if (getUseBoundsForWidth()) {
- c.save();
RectF drawingRect = computeDrawingBoundingBox();
if (drawingRect.left < 0) {
- c.translate(-drawingRect.left, 0);
+ leftShift = -drawingRect.left;
+ c.translate(leftShift, 0);
}
}
c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
- if (getUseBoundsForWidth()) {
- c.restore();
+ if (leftShift != 0) {
+ // Manually translate back to the original position because of b/324498002, using
+ // save/restore disappears the toggle switch drawables.
+ c.translate(-leftShift, 0);
}
} else {
super.draw(c, highlight, highlightpaint, cursorOffset);
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 8ddb42d..e5d199a 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -464,11 +464,12 @@
@Nullable Path selectionPath,
@Nullable Paint selectionPaint,
int cursorOffsetVertical) {
+ float leftShift = 0;
if (mUseBoundsForWidth) {
- canvas.save();
RectF drawingRect = computeDrawingBoundingBox();
if (drawingRect.left < 0) {
- canvas.translate(-drawingRect.left, 0);
+ leftShift = -drawingRect.left;
+ canvas.translate(leftShift, 0);
}
}
final long lineRange = getLineRangeForDraw(canvas);
@@ -479,8 +480,10 @@
drawWithoutText(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint,
cursorOffsetVertical, firstLine, lastLine);
drawText(canvas, firstLine, lastLine);
- if (mUseBoundsForWidth) {
- canvas.restore();
+ if (leftShift != 0) {
+ // Manually translate back to the original position because of b/324498002, using
+ // save/restore disappears the toggle switch drawables.
+ canvas.translate(-leftShift, 0);
}
}
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index f37c4c2a..f68fcab9 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -110,3 +110,12 @@
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 2ebe2e9..f67844d 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/tracing/perfetto/TracingContext.java b/core/java/android/tracing/perfetto/TracingContext.java
index 0bce26e..c1a61a7 100644
--- a/core/java/android/tracing/perfetto/TracingContext.java
+++ b/core/java/android/tracing/perfetto/TracingContext.java
@@ -105,6 +105,5 @@
return res;
}
- // private static native void nativeFlush(long nativeDataSourcePointer);
private static native void nativeFlush(TracingContext thiz, long ctxPointer);
}
diff --git a/core/java/android/util/Xml.java b/core/java/android/util/Xml.java
index ec6e90b..50d419f 100644
--- a/core/java/android/util/Xml.java
+++ b/core/java/android/util/Xml.java
@@ -53,9 +53,12 @@
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
+import javax.xml.parsers.SAXParserFactory;
+
/**
* XML utility methods.
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public class Xml {
private Xml() {}
@@ -73,8 +76,33 @@
*
* @hide
*/
- public static final boolean ENABLE_BINARY_DEFAULT = SystemProperties
- .getBoolean("persist.sys.binary_xml", true);
+ public static final boolean ENABLE_BINARY_DEFAULT = shouldEnableBinaryDefault();
+
+ @android.ravenwood.annotation.RavenwoodReplace
+ private static boolean shouldEnableBinaryDefault() {
+ return SystemProperties.getBoolean("persist.sys.binary_xml", true);
+ }
+
+ private static boolean shouldEnableBinaryDefault$ravenwood() {
+ return true;
+ }
+
+ /**
+ * Feature flag: when set, {@link #resolvePullParser(InputStream)}} will attempt to sniff
+ * using {@code pread} optimization.
+ *
+ * @hide
+ */
+ public static final boolean ENABLE_RESOLVE_OPTIMIZATIONS = shouldEnableResolveOptimizations();
+
+ @android.ravenwood.annotation.RavenwoodReplace
+ private static boolean shouldEnableResolveOptimizations() {
+ return true;
+ }
+
+ private static boolean shouldEnableResolveOptimizations$ravenwood() {
+ return false;
+ }
/**
* Parses the given xml string and fires events on the given SAX handler.
@@ -82,7 +110,7 @@
public static void parse(String xml, ContentHandler contentHandler)
throws SAXException {
try {
- XMLReader reader = XmlObjectFactory.newXMLReader();
+ XMLReader reader = newXMLReader();
reader.setContentHandler(contentHandler);
reader.parse(new InputSource(new StringReader(xml)));
} catch (IOException e) {
@@ -96,7 +124,7 @@
*/
public static void parse(Reader in, ContentHandler contentHandler)
throws IOException, SAXException {
- XMLReader reader = XmlObjectFactory.newXMLReader();
+ XMLReader reader = newXMLReader();
reader.setContentHandler(contentHandler);
reader.parse(new InputSource(in));
}
@@ -107,7 +135,7 @@
*/
public static void parse(InputStream in, Encoding encoding,
ContentHandler contentHandler) throws IOException, SAXException {
- XMLReader reader = XmlObjectFactory.newXMLReader();
+ XMLReader reader = newXMLReader();
reader.setContentHandler(contentHandler);
InputSource source = new InputSource(in);
source.setEncoding(encoding.expatName);
@@ -120,19 +148,26 @@
@android.ravenwood.annotation.RavenwoodReplace
public static XmlPullParser newPullParser() {
try {
- XmlPullParser parser = XmlObjectFactory.newXmlPullParser();
+ XmlPullParser parser = newXmlPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true);
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
return parser;
} catch (XmlPullParserException e) {
- throw new AssertionError();
+ throw new AssertionError(e);
}
}
/** @hide */
public static XmlPullParser newPullParser$ravenwood() {
- // TODO: remove once we're linking against libcore
- return new BinaryXmlPullParser();
+ try {
+ // Prebuilt kxml2-android does not support FEATURE_PROCESS_DOCDECL, so omit here;
+ // it's quite rare and all tests are passing
+ XmlPullParser parser = newXmlPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ return parser;
+ } catch (XmlPullParserException e) {
+ throw new AssertionError(e);
+ }
}
/**
@@ -145,17 +180,10 @@
* @hide
*/
@SuppressWarnings("AndroidFrameworkEfficientXml")
- @android.ravenwood.annotation.RavenwoodReplace
public static @NonNull TypedXmlPullParser newFastPullParser() {
return XmlUtils.makeTyped(newPullParser());
}
- /** @hide */
- public static TypedXmlPullParser newFastPullParser$ravenwood() {
- // TODO: remove once we're linking against libcore
- return new BinaryXmlPullParser();
- }
-
/**
* Creates a new {@link XmlPullParser} that reads XML documents using a
* custom binary wire protocol which benchmarking has shown to be 8.5x
@@ -189,11 +217,10 @@
*
* @hide
*/
- @android.ravenwood.annotation.RavenwoodReplace
public static @NonNull TypedXmlPullParser resolvePullParser(@NonNull InputStream in)
throws IOException {
final byte[] magic = new byte[4];
- if (in instanceof FileInputStream) {
+ if (ENABLE_RESOLVE_OPTIMIZATIONS && in instanceof FileInputStream) {
try {
Os.pread(((FileInputStream) in).getFD(), magic, 0, magic.length, 0);
} catch (ErrnoException e) {
@@ -222,31 +249,11 @@
return xml;
}
- /** @hide */
- public static @NonNull TypedXmlPullParser resolvePullParser$ravenwood(@NonNull InputStream in)
- throws IOException {
- // TODO: remove once we're linking against libcore
- final TypedXmlPullParser xml = new BinaryXmlPullParser();
- try {
- xml.setInput(in, StandardCharsets.UTF_8.name());
- } catch (XmlPullParserException e) {
- throw new IOException(e);
- }
- return xml;
- }
-
/**
* Creates a new xml serializer.
*/
- @android.ravenwood.annotation.RavenwoodReplace
public static XmlSerializer newSerializer() {
- return XmlObjectFactory.newXmlSerializer();
- }
-
- /** @hide */
- public static XmlSerializer newSerializer$ravenwood() {
- // TODO: remove once we're linking against libcore
- return new BinaryXmlSerializer();
+ return newXmlSerializer();
}
/**
@@ -259,17 +266,10 @@
* @hide
*/
@SuppressWarnings("AndroidFrameworkEfficientXml")
- @android.ravenwood.annotation.RavenwoodReplace
public static @NonNull TypedXmlSerializer newFastSerializer() {
return XmlUtils.makeTyped(new FastXmlSerializer());
}
- /** @hide */
- public static @NonNull TypedXmlSerializer newFastSerializer$ravenwood() {
- // TODO: remove once we're linking against libcore
- return new BinaryXmlSerializer();
- }
-
/**
* Creates a new {@link XmlSerializer} that writes XML documents using a
* custom binary wire protocol which benchmarking has shown to be 4.4x
@@ -334,7 +334,6 @@
*
* @hide
*/
- @android.ravenwood.annotation.RavenwoodKeep
public static void copy(@NonNull XmlPullParser in, @NonNull XmlSerializer out)
throws XmlPullParserException, IOException {
// Some parsers may have already consumed the event that starts the
@@ -394,7 +393,6 @@
* unsupported, which can confuse serializers. This method normalizes empty
* strings to be {@code null}.
*/
- @android.ravenwood.annotation.RavenwoodKeep
private static @Nullable String normalizeNamespace(@Nullable String namespace) {
if (namespace == null || namespace.isEmpty()) {
return null;
@@ -457,4 +455,45 @@
? (AttributeSet) parser
: new XmlPullAttributes(parser);
}
+
+ @android.ravenwood.annotation.RavenwoodReplace
+ private static @NonNull XmlSerializer newXmlSerializer() {
+ return XmlObjectFactory.newXmlSerializer();
+ }
+
+ private static @NonNull XmlSerializer newXmlSerializer$ravenwood() {
+ try {
+ return XmlPullParserFactory.newInstance().newSerializer();
+ } catch (XmlPullParserException e) {
+ throw new UnsupportedOperationException(e);
+ }
+ }
+
+ @android.ravenwood.annotation.RavenwoodReplace
+ private static @NonNull XmlPullParser newXmlPullParser() {
+ return XmlObjectFactory.newXmlPullParser();
+ }
+
+ private static @NonNull XmlPullParser newXmlPullParser$ravenwood() {
+ try {
+ return XmlPullParserFactory.newInstance().newPullParser();
+ } catch (XmlPullParserException e) {
+ throw new UnsupportedOperationException(e);
+ }
+ }
+
+ @android.ravenwood.annotation.RavenwoodReplace
+ private static @NonNull XMLReader newXMLReader() {
+ return XmlObjectFactory.newXMLReader();
+ }
+
+ private static @NonNull XMLReader newXMLReader$ravenwood() {
+ try {
+ final SAXParserFactory factory = SAXParserFactory.newInstance();
+ factory.setNamespaceAware(true);
+ return factory.newSAXParser().getXMLReader();
+ } catch (Exception e) {
+ throw new UnsupportedOperationException(e);
+ }
+ }
}
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 0006139..1f2b4fa 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -1211,6 +1211,8 @@
* @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/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index b5b81d1..29cc859 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -73,6 +73,7 @@
import android.window.ISurfaceSyncGroupCompletedListener;
import android.window.ITaskFpsCallback;
import android.window.ITrustedPresentationListener;
+import android.window.IUnhandledDragListener;
import android.window.InputTransferToken;
import android.window.ScreenCapture;
import android.window.TrustedPresentationThresholds;
@@ -1091,4 +1092,10 @@
@EnforcePermission("DETECT_SCREEN_RECORDING")
void unregisterScreenRecordingCallback(IScreenRecordingCallback callback);
+
+ /**
+ * Sets the listener to be called back when a cross-window drag and drop operation is unhandled
+ * (ie. not handled by any window which can handle the drag).
+ */
+ void setUnhandledDragListener(IUnhandledDragListener listener);
}
diff --git a/core/java/android/view/InsetsFrameProvider.java b/core/java/android/view/InsetsFrameProvider.java
index 83bdb08..fe98fab 100644
--- a/core/java/android/view/InsetsFrameProvider.java
+++ b/core/java/android/view/InsetsFrameProvider.java
@@ -18,6 +18,7 @@
import android.annotation.IntRange;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.IBinder;
@@ -110,6 +111,12 @@
private Insets mMinimalInsetsSizeInDisplayCutoutSafe = null;
/**
+ * Indicates the bounding rectangles within the provided insets frame, in relative coordinates
+ * to the source frame.
+ */
+ private Rect[] mBoundingRects = null;
+
+ /**
* Creates an InsetsFrameProvider which describes what frame an insets source should have.
*
* @param owner the owner of this provider. We might have multiple sources with the same type on
@@ -205,6 +212,22 @@
return mMinimalInsetsSizeInDisplayCutoutSafe;
}
+ /**
+ * Sets the bounding rectangles within and relative to the source frame.
+ */
+ public InsetsFrameProvider setBoundingRects(@Nullable Rect[] boundingRects) {
+ mBoundingRects = boundingRects == null ? null : boundingRects.clone();
+ return this;
+ }
+
+ /**
+ * Returns the arbitrary bounding rects, or null if none were set.
+ */
+ @Nullable
+ public Rect[] getBoundingRects() {
+ return mBoundingRects;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -231,6 +254,9 @@
sb.append(", mMinimalInsetsSizeInDisplayCutoutSafe=")
.append(mMinimalInsetsSizeInDisplayCutoutSafe);
}
+ if (mBoundingRects != null) {
+ sb.append(", mBoundingRects=").append(Arrays.toString(mBoundingRects));
+ }
sb.append("}");
return sb.toString();
}
@@ -257,6 +283,7 @@
mInsetsSizeOverrides = in.createTypedArray(InsetsSizeOverride.CREATOR);
mArbitraryRectangle = in.readTypedObject(Rect.CREATOR);
mMinimalInsetsSizeInDisplayCutoutSafe = in.readTypedObject(Insets.CREATOR);
+ mBoundingRects = in.createTypedArray(Rect.CREATOR);
}
@Override
@@ -268,6 +295,7 @@
out.writeTypedArray(mInsetsSizeOverrides, flags);
out.writeTypedObject(mArbitraryRectangle, flags);
out.writeTypedObject(mMinimalInsetsSizeInDisplayCutoutSafe, flags);
+ out.writeTypedArray(mBoundingRects, flags);
}
public boolean idEquals(InsetsFrameProvider o) {
@@ -288,14 +316,15 @@
&& Arrays.equals(mInsetsSizeOverrides, other.mInsetsSizeOverrides)
&& Objects.equals(mArbitraryRectangle, other.mArbitraryRectangle)
&& Objects.equals(mMinimalInsetsSizeInDisplayCutoutSafe,
- other.mMinimalInsetsSizeInDisplayCutoutSafe);
+ other.mMinimalInsetsSizeInDisplayCutoutSafe)
+ && Arrays.equals(mBoundingRects, other.mBoundingRects);
}
@Override
public int hashCode() {
return Objects.hash(mId, mSource, mFlags, mInsetsSize,
Arrays.hashCode(mInsetsSizeOverrides), mArbitraryRectangle,
- mMinimalInsetsSizeInDisplayCutoutSafe);
+ mMinimalInsetsSizeInDisplayCutoutSafe, Arrays.hashCode(mBoundingRects));
}
public static final @NonNull Parcelable.Creator<InsetsFrameProvider> CREATOR =
diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java
index bc33d5e..f9eba29 100644
--- a/core/java/android/view/InsetsSource.java
+++ b/core/java/android/view/InsetsSource.java
@@ -38,6 +38,8 @@
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Objects;
import java.util.StringJoiner;
@@ -105,6 +107,12 @@
})
public @interface Flags {}
+ /**
+ * Used when there are no bounding rects to describe an inset, which is only possible when the
+ * insets itself is {@link Insets#NONE}.
+ */
+ private static final Rect[] NO_BOUNDING_RECTS = new Rect[0];
+
private @Flags int mFlags;
/**
@@ -117,6 +125,7 @@
/** Frame of the source in screen coordinate space */
private final Rect mFrame;
private @Nullable Rect mVisibleFrame;
+ private @Nullable Rect[] mBoundingRects;
private boolean mVisible;
@@ -127,6 +136,7 @@
private @InternalInsetsSide int mSideHint = SIDE_NONE;
private final Rect mTmpFrame = new Rect();
+ private final Rect mTmpBoundingRect = new Rect();
public InsetsSource(int id, @InsetsType int type) {
mId = id;
@@ -145,6 +155,9 @@
: null;
mFlags = other.mFlags;
mSideHint = other.mSideHint;
+ mBoundingRects = other.mBoundingRects != null
+ ? other.mBoundingRects.clone()
+ : null;
}
public void set(InsetsSource other) {
@@ -155,6 +168,9 @@
: null;
mFlags = other.mFlags;
mSideHint = other.mSideHint;
+ mBoundingRects = other.mBoundingRects != null
+ ? other.mBoundingRects.clone()
+ : null;
}
public InsetsSource setFrame(int left, int top, int right, int bottom) {
@@ -199,6 +215,15 @@
return this;
}
+ /**
+ * Set the bounding rectangles of this source. They are expected to be relative to the source
+ * frame.
+ */
+ public InsetsSource setBoundingRects(@Nullable Rect[] rects) {
+ mBoundingRects = rects != null ? rects.clone() : null;
+ return this;
+ }
+
public int getId() {
return mId;
}
@@ -228,6 +253,13 @@
}
/**
+ * Returns the bounding rectangles of this source.
+ */
+ public @Nullable Rect[] getBoundingRects() {
+ return mBoundingRects;
+ }
+
+ /**
* Calculates the insets this source will cause to a client window.
*
* @param relativeFrame The frame to calculate the insets relative to.
@@ -313,6 +345,82 @@
}
/**
+ * Calculates the bounding rects the source will cause to a client window.
+ */
+ public @NonNull Rect[] calculateBoundingRects(Rect relativeFrame, boolean ignoreVisibility) {
+ if (!ignoreVisibility && !mVisible) {
+ return NO_BOUNDING_RECTS;
+ }
+
+ final Rect frame = getFrame();
+ if (mBoundingRects == null) {
+ // No bounding rects set, make a single bounding rect that covers the intersection of
+ // the |frame| and the |relativeFrame|.
+ return mTmpBoundingRect.setIntersect(frame, relativeFrame)
+ ? new Rect[]{ new Rect(mTmpBoundingRect) }
+ : NO_BOUNDING_RECTS;
+
+ }
+
+ // Special treatment for captionBar inset type. During drag-resizing, the |frame| and
+ // |boundingRects| may not get updated as quickly as |relativeFrame|, so just assume the
+ // |frame| will always be either at the top or bottom of |relativeFrame|. This means some
+ // calculations to make |boundingRects| relative to |relativeFrame| can be skipped or
+ // simplified.
+ // TODO(b/254128050): remove special treatment.
+ if (getType() == WindowInsets.Type.captionBar()) {
+ final ArrayList<Rect> validBoundingRects = new ArrayList<>();
+ for (final Rect boundingRect : mBoundingRects) {
+ // Assume that the caption |frame| and |relativeFrame| perfectly align at the top
+ // or bottom, meaning that the provided |boundingRect|, which is relative to the
+ // |frame| either is already relative to |relativeFrame| (for top captionBar()), or
+ // just needs to be made relative to |relativeFrame| for bottom bars.
+ final int frameHeight = frame.height();
+ mTmpBoundingRect.set(boundingRect);
+ if (getId() == ID_IME_CAPTION_BAR) {
+ mTmpBoundingRect.offset(0, relativeFrame.height() - frameHeight);
+ }
+ validBoundingRects.add(new Rect(mTmpBoundingRect));
+ }
+ return validBoundingRects.toArray(new Rect[validBoundingRects.size()]);
+ }
+
+ // Regular treatment for non-captionBar inset types.
+ final ArrayList<Rect> validBoundingRects = new ArrayList<>();
+ for (final Rect boundingRect : mBoundingRects) {
+ // |boundingRect| was provided relative to |frame|. Make it absolute to be in the same
+ // coordinate system as |frame|.
+ final Rect absBoundingRect = new Rect(
+ boundingRect.left + frame.left,
+ boundingRect.top + frame.top,
+ boundingRect.right + frame.left,
+ boundingRect.bottom + frame.top
+ );
+ // Now find the intersection of that |absBoundingRect| with |relativeFrame|. In other
+ // words, whichever part of the bounding rect is inside the window frame.
+ if (!mTmpBoundingRect.setIntersect(absBoundingRect, relativeFrame)) {
+ // It's possible for this to be empty if the frame and bounding rects were larger
+ // than the |relativeFrame|, such as when a system window is wider than the app
+ // window width. Just ignore that rect since it will have no effect on the
+ // window insets.
+ continue;
+ }
+ // At this point, |mTmpBoundingRect| is a valid bounding rect located fully inside the
+ // window, convert it to be relative to the window so that apps don't need to know the
+ // location of the window to understand bounding rects.
+ validBoundingRects.add(new Rect(
+ mTmpBoundingRect.left - relativeFrame.left,
+ mTmpBoundingRect.top - relativeFrame.top,
+ mTmpBoundingRect.right - relativeFrame.left,
+ mTmpBoundingRect.bottom - relativeFrame.top));
+ }
+ if (validBoundingRects.isEmpty()) {
+ return NO_BOUNDING_RECTS;
+ }
+ return validBoundingRects.toArray(new Rect[validBoundingRects.size()]);
+ }
+
+ /**
* Outputs the intersection of two rectangles. The shared edges will also be counted in the
* intersection.
*
@@ -467,6 +575,7 @@
pw.print(" visible="); pw.print(mVisible);
pw.print(" flags="); pw.print(flagsToString(mFlags));
pw.print(" sideHint="); pw.print(sideToString(mSideHint));
+ pw.print(" boundingRects="); pw.print(Arrays.toString(mBoundingRects));
pw.println();
}
@@ -492,12 +601,14 @@
if (mSideHint != that.mSideHint) return false;
if (excludeInvisibleImeFrames && !mVisible && mType == WindowInsets.Type.ime()) return true;
if (!Objects.equals(mVisibleFrame, that.mVisibleFrame)) return false;
- return mFrame.equals(that.mFrame);
+ if (!mFrame.equals(that.mFrame)) return false;
+ return Arrays.equals(mBoundingRects, that.mBoundingRects);
}
@Override
public int hashCode() {
- return Objects.hash(mId, mType, mFrame, mVisibleFrame, mVisible, mFlags, mSideHint);
+ return Objects.hash(mId, mType, mFrame, mVisibleFrame, mVisible, mFlags, mSideHint,
+ Arrays.hashCode(mBoundingRects));
}
public InsetsSource(Parcel in) {
@@ -512,6 +623,7 @@
mVisible = in.readBoolean();
mFlags = in.readInt();
mSideHint = in.readInt();
+ mBoundingRects = in.createTypedArray(Rect.CREATOR);
}
@Override
@@ -533,6 +645,7 @@
dest.writeBoolean(mVisible);
dest.writeInt(mFlags);
dest.writeInt(mSideHint);
+ dest.writeTypedArray(mBoundingRects, flags);
}
@Override
@@ -543,6 +656,7 @@
+ " mVisible=" + mVisible
+ " mFlags=" + flagsToString(mFlags)
+ " mSideHint=" + sideToString(mSideHint)
+ + " mBoundingRects=" + Arrays.toString(mBoundingRects)
+ "}";
}
diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java
index c88da9e..21eec67 100644
--- a/core/java/android/view/InsetsState.java
+++ b/core/java/android/view/InsetsState.java
@@ -128,6 +128,8 @@
final Rect relativeFrameMax = new Rect(frame);
@InsetsType int forceConsumingTypes = 0;
@InsetsType int suppressScrimTypes = 0;
+ final Rect[][] typeBoundingRectsMap = new Rect[Type.SIZE][];
+ final Rect[][] typeMaxBoundingRectsMap = new Rect[Type.SIZE][];
for (int i = mSources.size() - 1; i >= 0; i--) {
final InsetsSource source = mSources.valueAt(i);
final @InsetsType int type = source.getType();
@@ -141,7 +143,7 @@
}
processSource(source, relativeFrame, false /* ignoreVisibility */, typeInsetsMap,
- idSideMap, typeVisibilityMap);
+ idSideMap, typeVisibilityMap, typeBoundingRectsMap);
// IME won't be reported in max insets as the size depends on the EditorInfo of the IME
// target.
@@ -154,7 +156,7 @@
}
processSource(ignoringVisibilitySource, relativeFrameMax,
true /* ignoreVisibility */, typeMaxInsetsMap, null /* idSideMap */,
- null /* typeVisibilityMap */);
+ null /* typeVisibilityMap */, typeMaxBoundingRectsMap);
}
}
final int softInputAdjustMode = legacySoftInputMode & SOFT_INPUT_MASK_ADJUST;
@@ -175,7 +177,8 @@
calculateRelativeRoundedCorners(frame),
calculateRelativePrivacyIndicatorBounds(frame),
calculateRelativeDisplayShape(frame),
- compatInsetsTypes, (legacySystemUiFlags & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0);
+ compatInsetsTypes, (legacySystemUiFlags & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0,
+ typeBoundingRectsMap, typeMaxBoundingRectsMap, frame.width(), frame.height());
}
private DisplayCutout calculateRelativeCutout(Rect frame) {
@@ -328,12 +331,13 @@
private void processSource(InsetsSource source, Rect relativeFrame, boolean ignoreVisibility,
Insets[] typeInsetsMap, @Nullable @InternalInsetsSide SparseIntArray idSideMap,
- @Nullable boolean[] typeVisibilityMap) {
+ @Nullable boolean[] typeVisibilityMap, Rect[][] typeBoundingRectsMap) {
Insets insets = source.calculateInsets(relativeFrame, ignoreVisibility);
+ final Rect[] boundingRects = source.calculateBoundingRects(relativeFrame, ignoreVisibility);
final int type = source.getType();
processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
- insets, type);
+ typeBoundingRectsMap, insets, boundingRects, type);
if (type == Type.MANDATORY_SYSTEM_GESTURES) {
// Mandatory system gestures are also system gestures.
@@ -342,24 +346,25 @@
// ability to set systemGestureInsets() independently from
// mandatorySystemGestureInsets() in the Builder.
processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
- insets, Type.SYSTEM_GESTURES);
+ typeBoundingRectsMap, insets, boundingRects, Type.SYSTEM_GESTURES);
}
if (type == Type.CAPTION_BAR) {
// Caption should also be gesture and tappable elements. This should not be needed when
// the caption is added from the shell, as the shell can add other types at the same
// time.
processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
- insets, Type.SYSTEM_GESTURES);
+ typeBoundingRectsMap, insets, boundingRects, Type.SYSTEM_GESTURES);
processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
- insets, Type.MANDATORY_SYSTEM_GESTURES);
+ typeBoundingRectsMap, insets, boundingRects, Type.MANDATORY_SYSTEM_GESTURES);
processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
- insets, Type.TAPPABLE_ELEMENT);
+ typeBoundingRectsMap, insets, boundingRects, Type.TAPPABLE_ELEMENT);
}
}
private void processSourceAsPublicType(InsetsSource source, Insets[] typeInsetsMap,
@InternalInsetsSide @Nullable SparseIntArray idSideMap,
- @Nullable boolean[] typeVisibilityMap, Insets insets, int type) {
+ @Nullable boolean[] typeVisibilityMap, Rect[][] typeBoundingRectsMap,
+ Insets insets, Rect[] boundingRects, int type) {
int index = indexOf(type);
// Don't put Insets.NONE into typeInsetsMap. Otherwise, two WindowInsets can be considered
@@ -384,6 +389,22 @@
idSideMap.put(source.getId(), insetSide);
}
}
+
+ if (typeBoundingRectsMap != null && boundingRects.length > 0) {
+ final Rect[] existing = typeBoundingRectsMap[index];
+ if (existing == null) {
+ typeBoundingRectsMap[index] = boundingRects;
+ } else {
+ typeBoundingRectsMap[index] = concatenate(existing, boundingRects);
+ }
+ }
+ }
+
+ private static Rect[] concatenate(Rect[] a, Rect[] b) {
+ final Rect[] c = new Rect[a.length + b.length];
+ System.arraycopy(a, 0, c, 0, a.length);
+ System.arraycopy(b, 0, c, a.length, b.length);
+ return c;
}
/**
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index c22986b..dc0b1a7 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -43,6 +43,7 @@
import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS;
import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP;
import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION;
+import static com.android.window.flags.Flags.FLAG_DELEGATE_UNHANDLED_DRAGS;
import static java.lang.Math.max;
@@ -68,6 +69,7 @@
import android.annotation.TestApi;
import android.annotation.UiContext;
import android.annotation.UiThread;
+import android.app.PendingIntent;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.AutofillOptions;
import android.content.ClipData;
@@ -5329,6 +5331,34 @@
public static final int DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION = 1 << 11;
/**
+ * Flag indicating that a drag can cross window boundaries (within the same application). When
+ * {@link #startDragAndDrop(ClipData, DragShadowBuilder, Object, int)} is called
+ * with this flag set, only visible windows belonging to the same application (ie. share the
+ * same UID) with targetSdkVersion >= {@link android.os.Build.VERSION_CODES#N API 24} will be
+ * able to participate in the drag operation and receive the dragged content.
+ *
+ * If both DRAG_FLAG_GLOBAL_SAME_APPLICATION and DRAG_FLAG_GLOBAL are set, then
+ * DRAG_FLAG_GLOBAL_SAME_APPLICATION takes precedence and the drag will only go to visible
+ * windows from the same application.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ public static final int DRAG_FLAG_GLOBAL_SAME_APPLICATION = 1 << 12;
+
+ /**
+ * Flag indicating that an unhandled drag should be delegated to the system to be started if no
+ * visible window wishes to handle the drop. When using this flag, the caller must provide
+ * ClipData with an Item that contains an immutable PendingIntent to an activity to be launched
+ * (not a broadcast, service, etc). See
+ * {@link ClipData.Item.Builder#setPendingIntent(PendingIntent)}.
+ *
+ * The system can decide to launch the intent or not based on factors like the current screen
+ * size or windowing mode. If the system does not launch the intent, it will be canceled via the
+ * normal drag and drop flow.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ public static final int DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG = 1 << 13;
+
+ /**
* Vertical scroll factor cached by {@link #getVerticalScrollFactor}.
*/
private float mVerticalScrollFactor;
@@ -6364,6 +6394,9 @@
case R.styleable.View_handwritingBoundsOffsetBottom:
mHandwritingBoundsOffsetBottom = a.getDimension(attr, 0);
break;
+ case R.styleable.View_contentSensitivity:
+ setContentSensitivity(a.getInt(attr, CONTENT_SENSITIVITY_AUTO));
+ break;
}
}
@@ -22269,6 +22302,9 @@
* Retrieve a unique token identifying the window this view is attached to.
* @return Return the window's token for use in
* {@link WindowManager.LayoutParams#token WindowManager.LayoutParams.token}.
+ * This token maybe null if this view is not attached to a window.
+ * @see #isAttachedToWindow() for current window attach state
+ * @see OnAttachStateChangeListener to listen to window attach/detach state changes
*/
public IBinder getWindowToken() {
return mAttachInfo != null ? mAttachInfo.mWindowToken : null;
@@ -28496,9 +28532,29 @@
Log.w(VIEW_LOG_TAG, "startDragAndDrop called with an invalid surface.");
return false;
}
+ if ((flags & DRAG_FLAG_GLOBAL) != 0 && ((flags & DRAG_FLAG_GLOBAL_SAME_APPLICATION) != 0)) {
+ Log.w(VIEW_LOG_TAG, "startDragAndDrop called with both DRAG_FLAG_GLOBAL "
+ + "and DRAG_FLAG_GLOBAL_SAME_APPLICATION, the drag will default to "
+ + "DRAG_FLAG_GLOBAL_SAME_APPLICATION");
+ flags &= ~DRAG_FLAG_GLOBAL;
+ }
if (data != null) {
- data.prepareToLeaveProcess((flags & View.DRAG_FLAG_GLOBAL) != 0);
+ if (com.android.window.flags.Flags.delegateUnhandledDrags()) {
+ data.prepareToLeaveProcess(
+ (flags & (DRAG_FLAG_GLOBAL_SAME_APPLICATION | DRAG_FLAG_GLOBAL)) != 0);
+ if ((flags & DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG) != 0) {
+ if (!data.hasActivityPendingIntents()) {
+ // Reset the flag if there is no launchable activity intent
+ flags &= ~DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG;
+ Log.w(VIEW_LOG_TAG, "startDragAndDrop called with "
+ + "DRAG_FLAG_START_INTENT_ON_UNHANDLED_DRAG but the clip data "
+ + "contains non-activity PendingIntents");
+ }
+ }
+ } else {
+ data.prepareToLeaveProcess((flags & DRAG_FLAG_GLOBAL) != 0);
+ }
}
Rect bounds = new Rect();
@@ -28524,6 +28580,7 @@
if (token != null) {
root.setLocalDragState(myLocalState);
mAttachInfo.mDragToken = token;
+ mAttachInfo.mDragData = data;
mAttachInfo.mViewRootImpl.setDragStartedViewForAccessibility(this);
setAccessibilityDragStarted(true);
}
@@ -28601,8 +28658,12 @@
if (mAttachInfo.mDragSurface != null) {
mAttachInfo.mDragSurface.release();
}
+ if (mAttachInfo.mDragData != null) {
+ mAttachInfo.mDragData.cleanUpPendingIntents();
+ }
mAttachInfo.mDragSurface = surface;
mAttachInfo.mDragToken = token;
+ mAttachInfo.mDragData = data;
// Cache the local state object for delivery with DragEvents
root.setLocalDragState(myLocalState);
if (a11yEnabled) {
@@ -31516,11 +31577,15 @@
IBinder mDragToken;
/**
+ * Used to track the data of the current drag operation for cleanup later.
+ */
+ ClipData mDragData;
+
+ /**
* The drag shadow surface for the current drag operation.
*/
public Surface mDragSurface;
-
/**
* The view that currently has a tooltip displayed.
*/
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 07c9795..28a7334 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -8599,6 +8599,10 @@
mAttachInfo.mDragSurface.release();
mAttachInfo.mDragSurface = null;
}
+ if (mAttachInfo.mDragData != null) {
+ mAttachInfo.mDragData.cleanUpPendingIntents();
+ mAttachInfo.mDragData = null;
+ }
}
}
}
diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java
index 921afaa..fbebe1e 100644
--- a/core/java/android/view/WindowInsets.java
+++ b/core/java/android/view/WindowInsets.java
@@ -34,6 +34,7 @@
import static android.view.WindowInsets.Type.indexOf;
import static android.view.WindowInsets.Type.systemBars;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -44,8 +45,10 @@
import android.content.Intent;
import android.graphics.Insets;
import android.graphics.Rect;
+import android.util.Size;
import android.view.View.OnApplyWindowInsetsListener;
import android.view.WindowInsets.Type.InsetsType;
+import android.view.flags.Flags;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethod;
@@ -54,7 +57,10 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
import java.util.Objects;
/**
@@ -78,6 +84,8 @@
private final Insets[] mTypeInsetsMap;
private final Insets[] mTypeMaxInsetsMap;
private final boolean[] mTypeVisibilityMap;
+ private final Rect[][] mTypeBoundingRectsMap;
+ private final Rect[][] mTypeMaxBoundingRectsMap;
@Nullable private Rect mTempRect;
private final boolean mIsRound;
@@ -85,6 +93,8 @@
@Nullable private final RoundedCorners mRoundedCorners;
@Nullable private final PrivacyIndicatorBounds mPrivacyIndicatorBounds;
@Nullable private final DisplayShape mDisplayShape;
+ private final int mFrameWidth;
+ private final int mFrameHeight;
private final @InsetsType int mForceConsumingTypes;
private final @InsetsType int mSuppressScrimTypes;
@@ -114,7 +124,7 @@
static {
CONSUMED = new WindowInsets(createCompatTypeMap(null), createCompatTypeMap(null),
createCompatVisibilityMap(createCompatTypeMap(null)), false, 0, 0, null,
- null, null, null, systemBars(), false);
+ null, null, null, systemBars(), false, null, null, 0, 0);
}
/**
@@ -139,7 +149,10 @@
RoundedCorners roundedCorners,
PrivacyIndicatorBounds privacyIndicatorBounds,
DisplayShape displayShape,
- @InsetsType int compatInsetsTypes, boolean compatIgnoreVisibility) {
+ @InsetsType int compatInsetsTypes, boolean compatIgnoreVisibility,
+ Rect[][] typeBoundingRectsMap,
+ Rect[][] typeMaxBoundingRectsMap,
+ int frameWidth, int frameHeight) {
mSystemWindowInsetsConsumed = typeInsetsMap == null;
mTypeInsetsMap = mSystemWindowInsetsConsumed
? new Insets[SIZE]
@@ -164,6 +177,14 @@
mRoundedCorners = roundedCorners;
mPrivacyIndicatorBounds = privacyIndicatorBounds;
mDisplayShape = displayShape;
+ mTypeBoundingRectsMap = (mSystemWindowInsetsConsumed || typeBoundingRectsMap == null)
+ ? new Rect[SIZE][]
+ : typeBoundingRectsMap.clone();
+ mTypeMaxBoundingRectsMap = (mStableInsetsConsumed || typeMaxBoundingRectsMap == null)
+ ? new Rect[SIZE][]
+ : typeMaxBoundingRectsMap.clone();
+ mFrameWidth = frameWidth;
+ mFrameHeight = frameHeight;
}
/**
@@ -181,7 +202,11 @@
src.mPrivacyIndicatorBounds,
src.mDisplayShape,
src.mCompatInsetsTypes,
- src.mCompatIgnoreVisibility);
+ src.mCompatIgnoreVisibility,
+ src.mSystemWindowInsetsConsumed ? null : src.mTypeBoundingRectsMap,
+ src.mStableInsetsConsumed ? null : src.mTypeMaxBoundingRectsMap,
+ src.mFrameWidth,
+ src.mFrameHeight);
}
private static DisplayCutout displayCutoutCopyConstructorArgument(WindowInsets w) {
@@ -233,7 +258,8 @@
@UnsupportedAppUsage
public WindowInsets(Rect systemWindowInsets) {
this(createCompatTypeMap(systemWindowInsets), null, new boolean[SIZE], false, 0, 0,
- null, null, null, null, systemBars(), false /* compatIgnoreVisibility */);
+ null, null, null, null, systemBars(), false /* compatIgnoreVisibility */,
+ new Rect[SIZE][], null, 0, 0);
}
/**
@@ -475,6 +501,111 @@
}
/**
+ * Returns a list of {@link Rect}s, each of which is the bounding rectangle for an area
+ * that is being partially or fully obscured inside the window.
+ *
+ * <p>
+ * May be used with or instead of {@link Insets} for finer avoidance of regions that may be
+ * partially obscuring the window but may be smaller than those provided by
+ * {@link #getInsets(int)}.
+ * </p>
+ *
+ * <p>
+ * The {@link Rect}s returned are always cropped to the bounds of the window frame and their
+ * coordinate values are relative to the {@link #getFrame()}, regardless of the window's
+ * position on screen.
+ * </p>
+ *
+ * <p>
+ * If inset by {@link #inset(Insets)}, bounding rects that intersect with the provided insets
+ * will be resized to only include the intersection with the remaining frame. Bounding rects
+ * may be completely removed if they no longer intersect with the new instance.
+ * </p>
+ *
+ * @param typeMask the insets type for which to obtain the bounding rectangles
+ * @return the bounding rectangles
+ */
+ @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS)
+ @NonNull
+ public List<Rect> getBoundingRects(@InsetsType int typeMask) {
+ Rect[] allRects = null;
+ for (int i = FIRST; i <= LAST; i = i << 1) {
+ if ((typeMask & i) == 0) {
+ continue;
+ }
+ final Rect[] rects = mTypeBoundingRectsMap[indexOf(i)];
+ if (rects == null) {
+ continue;
+ }
+ if (allRects == null) {
+ allRects = rects;
+ } else {
+ final Rect[] concat = new Rect[allRects.length + rects.length];
+ System.arraycopy(allRects, 0, concat, 0, allRects.length);
+ System.arraycopy(rects, 0, concat, allRects.length, rects.length);
+ allRects = concat;
+ }
+ }
+ if (allRects == null) {
+ return Collections.emptyList();
+ }
+ return Arrays.asList(allRects);
+ }
+
+ /**
+ * Returns a list of {@link Rect}s, each of which is the bounding rectangle for an area that
+ * can be partially or fully obscured inside the window, regardless of whether
+ * that type is currently visible or not.
+ *
+ * <p> The bounding rects represent areas of a window that <b>may</b> be partially or fully
+ * obscured by the {@code type}. This value does not change based on the visibility state of
+ * those elements. For example, if the status bar is normally shown, but temporarily hidden,
+ * the bounding rects returned here will provide the rects associated with the status bar being
+ * shown.</p>
+ *
+ * <p>
+ * May be used with or instead of {@link Insets} for finer avoidance of regions that may be
+ * partially obscuring the window but may be smaller than those provided by
+ * {@link #getInsetsIgnoringVisibility(int)}.
+ * </p>
+ *
+ * <p>
+ * The {@link Rect}s returned are always cropped to the bounds of the window frame and their
+ * coordinate values are relative to the {@link #getFrame()}, regardless of the window's
+ * position on screen.
+ * </p>
+ *
+ * @param typeMask the insets type for which to obtain the bounding rectangles
+ * @return the bounding rectangles
+ */
+ @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS)
+ @NonNull
+ public List<Rect> getBoundingRectsIgnoringVisibility(@InsetsType int typeMask) {
+ Rect[] allRects = null;
+ for (int i = FIRST; i <= LAST; i = i << 1) {
+ if ((typeMask & i) == 0) {
+ continue;
+ }
+ final Rect[] rects = mTypeMaxBoundingRectsMap[indexOf(i)];
+ if (rects == null) {
+ continue;
+ }
+ if (allRects == null) {
+ allRects = rects;
+ } else {
+ final Rect[] concat = new Rect[allRects.length + rects.length];
+ System.arraycopy(allRects, 0, concat, 0, allRects.length);
+ System.arraycopy(rects, 0, concat, allRects.length, rects.length);
+ allRects = concat;
+ }
+ }
+ if (allRects == null) {
+ return Collections.emptyList();
+ }
+ return Arrays.asList(allRects);
+ }
+
+ /**
* Returns the display cutout if there is one.
*
* <p>Note: the display cutout will already be {@link #consumeDisplayCutout consumed} during
@@ -555,7 +686,10 @@
mTypeVisibilityMap,
mIsRound, mForceConsumingTypes, mSuppressScrimTypes,
null /* displayCutout */, mRoundedCorners, mPrivacyIndicatorBounds, mDisplayShape,
- mCompatInsetsTypes, mCompatIgnoreVisibility);
+ mCompatInsetsTypes, mCompatIgnoreVisibility,
+ mSystemWindowInsetsConsumed ? null : mTypeBoundingRectsMap,
+ mStableInsetsConsumed ? null : mTypeMaxBoundingRectsMap,
+ mFrameWidth, mFrameHeight);
}
@@ -610,7 +744,7 @@
(mCompatInsetsTypes & displayCutout()) != 0
? null : displayCutoutCopyConstructorArgument(this),
mRoundedCorners, mPrivacyIndicatorBounds, mDisplayShape, mCompatInsetsTypes,
- mCompatIgnoreVisibility);
+ mCompatIgnoreVisibility, null, null, mFrameWidth, mFrameHeight);
}
// TODO(b/119190588): replace @code with @link below
@@ -914,6 +1048,10 @@
result.append(Type.toString(1 << i)).append("=").append(insets)
.append(" max=").append(maxInsets)
.append(" vis=").append(visible)
+ .append(" boundingRects=")
+ .append(Arrays.toString(mTypeBoundingRectsMap[i]))
+ .append(" maxBoundingRects=")
+ .append(Arrays.toString(mTypeMaxBoundingRectsMap[i]))
.append("\n ");
}
}
@@ -942,6 +1080,10 @@
result.append("displayCutoutConsumed=" + mDisplayCutoutConsumed);
result.append("\n ");
result.append(isRound() ? "round" : "");
+ result.append("\n ");
+ result.append("frameWidth=" + mFrameWidth);
+ result.append("\n ");
+ result.append("frameHeight=" + mFrameHeight);
result.append("}");
return result.toString();
}
@@ -1013,6 +1155,27 @@
}
/**
+ * Returns the assumed size of the window, relative to which the {@link #getInsets} and
+ * {@link #getBoundingRects} have been calculated.
+ *
+ * <p> May be used with {@link #getBoundingRects} to better understand their position within
+ * the window, such as the area between the edge of a bounding rect and the edge of the window.
+ *
+ * <p>Note: the size may not match the actual size of the window, which is determined during
+ * the layout pass - as {@link WindowInsets} are dispatched before layout.
+ *
+ * <p>Caution: using this value in determining the actual window size may make the result of
+ * layout passes unstable and should be avoided.
+ *
+ * @return the assumed size of the window during the inset calculation
+ */
+ @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS)
+ @NonNull
+ public Size getFrame() {
+ return new Size(mFrameWidth, mFrameHeight);
+ }
+
+ /**
* @see #inset(int, int, int, int)
* @hide
*/
@@ -1039,7 +1202,17 @@
? null
: mPrivacyIndicatorBounds.inset(left, top, right, bottom),
mDisplayShape,
- mCompatInsetsTypes, mCompatIgnoreVisibility);
+ mCompatInsetsTypes, mCompatIgnoreVisibility,
+ mSystemWindowInsetsConsumed
+ ? null
+ : insetBoundingRects(mTypeBoundingRectsMap, left, top, right, bottom,
+ mFrameWidth, mFrameHeight),
+ mStableInsetsConsumed
+ ? null
+ : insetBoundingRects(mTypeMaxBoundingRectsMap, left, top, right, bottom,
+ mFrameWidth, mFrameHeight),
+ Math.max(0, mFrameWidth - left - right),
+ Math.max(0, mFrameHeight - top - bottom));
}
@Override
@@ -1060,7 +1233,11 @@
&& Objects.equals(mDisplayCutout, that.mDisplayCutout)
&& Objects.equals(mRoundedCorners, that.mRoundedCorners)
&& Objects.equals(mPrivacyIndicatorBounds, that.mPrivacyIndicatorBounds)
- && Objects.equals(mDisplayShape, that.mDisplayShape);
+ && Objects.equals(mDisplayShape, that.mDisplayShape)
+ && Arrays.deepEquals(mTypeBoundingRectsMap, that.mTypeBoundingRectsMap)
+ && Arrays.deepEquals(mTypeMaxBoundingRectsMap, that.mTypeMaxBoundingRectsMap)
+ && mFrameWidth == that.mFrameWidth
+ && mFrameHeight == that.mFrameHeight;
}
@Override
@@ -1069,7 +1246,8 @@
Arrays.hashCode(mTypeVisibilityMap), mIsRound, mDisplayCutout, mRoundedCorners,
mForceConsumingTypes, mSuppressScrimTypes, mSystemWindowInsetsConsumed,
mStableInsetsConsumed, mDisplayCutoutConsumed, mPrivacyIndicatorBounds,
- mDisplayShape);
+ mDisplayShape, Arrays.deepHashCode(mTypeBoundingRectsMap),
+ Arrays.deepHashCode(mTypeMaxBoundingRectsMap), mFrameWidth, mFrameHeight);
}
@@ -1110,6 +1288,68 @@
return Insets.of(newLeft, newTop, newRight, newBottom);
}
+ static Rect[][] insetBoundingRects(Rect[][] typeBoundingRectsMap,
+ int insetLeft, int insetTop, int insetRight, int insetBottom, int frameWidth,
+ int frameHeight) {
+ if (insetLeft == 0 && insetTop == 0 && insetRight == 0 && insetBottom == 0) {
+ return typeBoundingRectsMap;
+ }
+ boolean cloned = false;
+ for (int i = 0; i < SIZE; i++) {
+ final Rect[] boundingRects = typeBoundingRectsMap[i];
+ if (boundingRects == null) {
+ continue;
+ }
+ final Rect[] insetBoundingRects = insetBoundingRects(boundingRects,
+ insetLeft, insetTop, insetRight, insetBottom, frameWidth, frameHeight);
+ if (!Arrays.equals(insetBoundingRects, boundingRects)) {
+ if (!cloned) {
+ typeBoundingRectsMap = typeBoundingRectsMap.clone();
+ cloned = true;
+ }
+ typeBoundingRectsMap[i] = insetBoundingRects;
+ }
+ }
+ return typeBoundingRectsMap;
+ }
+
+ static Rect[] insetBoundingRects(Rect[] boundingRects,
+ int left, int top, int right, int bottom, int frameWidth, int frameHeight) {
+ final List<Rect> insetBoundingRectsList = new ArrayList<>();
+ for (int i = 0; i < boundingRects.length; i++) {
+ final Rect insetRect = insetRect(boundingRects[i], left, top, right, bottom,
+ frameWidth, frameHeight);
+ if (insetRect != null) {
+ insetBoundingRectsList.add(insetRect);
+ }
+ }
+ return insetBoundingRectsList.toArray(new Rect[0]);
+ }
+
+ private static Rect insetRect(Rect orig, int insetLeft, int insetTop, int insetRight,
+ int insetBottom, int frameWidth, int frameHeight) {
+ if (orig == null) {
+ return null;
+ }
+
+ // Calculate the inset frame, and leave it in that coordinate space for easier comparison
+ // against the |orig| rect.
+ final Rect insetFrame = new Rect(insetLeft, insetTop, frameWidth - insetRight,
+ frameHeight - insetBottom);
+ // Then the intersecting portion of |orig| with the inset |insetFrame|.
+ final Rect insetRect = new Rect();
+ if (insetRect.setIntersect(insetFrame, orig)) {
+ // The intersection is the inset rect, but its position must be shifted to be relative
+ // to the frame. Since the new frame will start at left=|insetLeft| and top=|insetTop|,
+ // just offset that much back in the direction of the origin of the frame.
+ insetRect.offset(-insetLeft, -insetTop);
+ return insetRect;
+ } else {
+ // The |orig| rect does not intersect with the new frame at all, so don't report it.
+ return null;
+ }
+ }
+
/**
* @return whether system window insets have been consumed.
*/
@@ -1125,6 +1365,8 @@
private final Insets[] mTypeInsetsMap;
private final Insets[] mTypeMaxInsetsMap;
private final boolean[] mTypeVisibilityMap;
+ private final Rect[][] mTypeBoundingRectsMap;
+ private final Rect[][] mTypeMaxBoundingRectsMap;
private boolean mSystemInsetsConsumed = true;
private boolean mStableInsetsConsumed = true;
@@ -1137,6 +1379,8 @@
private @InsetsType int mSuppressScrimTypes;
private PrivacyIndicatorBounds mPrivacyIndicatorBounds = new PrivacyIndicatorBounds();
+ private int mFrameWidth;
+ private int mFrameHeight;
/**
* Creates a builder where all insets are initially consumed.
@@ -1145,6 +1389,8 @@
mTypeInsetsMap = new Insets[SIZE];
mTypeMaxInsetsMap = new Insets[SIZE];
mTypeVisibilityMap = new boolean[SIZE];
+ mTypeBoundingRectsMap = new Rect[SIZE][];
+ mTypeMaxBoundingRectsMap = new Rect[SIZE][];
}
/**
@@ -1165,6 +1411,10 @@
mSuppressScrimTypes = insets.mSuppressScrimTypes;
mPrivacyIndicatorBounds = insets.mPrivacyIndicatorBounds;
mDisplayShape = insets.mDisplayShape;
+ mTypeBoundingRectsMap = insets.mTypeBoundingRectsMap.clone();
+ mTypeMaxBoundingRectsMap = insets.mTypeMaxBoundingRectsMap.clone();
+ mFrameWidth = insets.mFrameWidth;
+ mFrameHeight = insets.mFrameHeight;
}
/**
@@ -1452,6 +1702,68 @@
}
/**
+ * Sets the bounding rects.
+ *
+ * @param typeMask the inset types to which these rects apply.
+ * @param rects the bounding rects.
+ * @return itself.
+ */
+ @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS)
+ @NonNull
+ public Builder setBoundingRects(@InsetsType int typeMask, @NonNull List<Rect> rects) {
+ for (int i = FIRST; i <= LAST; i = i << 1) {
+ if ((typeMask & i) == 0) {
+ continue;
+ }
+ mTypeBoundingRectsMap[indexOf(i)] = rects.toArray(new Rect[0]);
+ }
+ return this;
+ }
+
+ /**
+ * Sets the bounding rects while ignoring their visibility state.
+ *
+ * @param typeMask the inset types to which these rects apply.
+ * @param rects the bounding rects.
+ * @return itself.
+ *
+ * @throws IllegalArgumentException If {@code typeMask} contains {@link Type#ime()}.
+ * Maximum bounding rects are not available for this type as the height of the IME is
+ * dynamic depending on the {@link EditorInfo} of the currently focused view, as well as
+ * the UI state of the IME.
+ */
+ @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS)
+ @NonNull
+ public Builder setBoundingRectsIgnoringVisibility(@InsetsType int typeMask,
+ @NonNull List<Rect> rects) {
+ if (typeMask == IME) {
+ throw new IllegalArgumentException("Maximum bounding rects not available for IME");
+ }
+ for (int i = FIRST; i <= LAST; i = i << 1) {
+ if ((typeMask & i) == 0) {
+ continue;
+ }
+ mTypeMaxBoundingRectsMap[indexOf(i)] = rects.toArray(new Rect[0]);
+ }
+ return this;
+ }
+
+ /**
+ * Set the frame size.
+ *
+ * @param width the width of the frame.
+ * @param height the height of the frame.
+ * @return itself.
+ */
+ @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS)
+ @NonNull
+ public Builder setFrame(int width, int height) {
+ mFrameWidth = width;
+ mFrameHeight = height;
+ return this;
+ }
+
+ /**
* Builds a {@link WindowInsets} instance.
*
* @return the {@link WindowInsets} instance.
@@ -1462,7 +1774,10 @@
mStableInsetsConsumed ? null : mTypeMaxInsetsMap, mTypeVisibilityMap,
mIsRound, mForceConsumingTypes, mSuppressScrimTypes, mDisplayCutout,
mRoundedCorners, mPrivacyIndicatorBounds, mDisplayShape, systemBars(),
- false /* compatIgnoreVisibility */);
+ false /* compatIgnoreVisibility */,
+ mSystemInsetsConsumed ? null : mTypeBoundingRectsMap,
+ mStableInsetsConsumed ? null : mTypeMaxBoundingRectsMap,
+ mFrameWidth, mFrameHeight);
}
}
diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java
index cc2cd79..b7542dc 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.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 @@
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 @@
@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/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java
index ae00b70..2fb5213 100644
--- a/core/java/android/view/WindowManagerImpl.java
+++ b/core/java/android/view/WindowManagerImpl.java
@@ -520,11 +520,16 @@
public void registerTrustedPresentationListener(@NonNull IBinder window,
@NonNull TrustedPresentationThresholds thresholds, @NonNull Executor executor,
@NonNull Consumer<Boolean> listener) {
+ Objects.requireNonNull(window, "window must not be null");
+ Objects.requireNonNull(thresholds, "thresholds must not be null");
+ Objects.requireNonNull(executor, "executor must not be null");
+ Objects.requireNonNull(listener, "listener must not be null");
mGlobal.registerTrustedPresentationListener(window, thresholds, executor, listener);
}
@Override
public void unregisterTrustedPresentationListener(@NonNull Consumer<Boolean> listener) {
+ Objects.requireNonNull(listener, "listener must not be null");
mGlobal.unregisterTrustedPresentationListener(listener);
}
diff --git a/core/java/android/view/inputmethod/ConnectionlessHandwritingCallback.java b/core/java/android/view/inputmethod/ConnectionlessHandwritingCallback.java
new file mode 100644
index 0000000..d985732
--- /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 89da041..dc5e0e5 100644
--- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
@@ -34,6 +34,7 @@
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 @@
}
@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 @@
@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 b60efc1..7c9678f 100644
--- a/core/java/android/view/inputmethod/InputMethodInfo.java
+++ b/core/java/android/view/inputmethod/InputMethodInfo.java
@@ -204,6 +204,9 @@
*/
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 @@
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 @@
mSubtypes = source.mSubtypes;
mHandledConfigChanges = source.mHandledConfigChanges;
mSupportsStylusHandwriting = source.mSupportsStylusHandwriting;
+ mSupportsConnectionlessStylusHandwriting = source.mSupportsConnectionlessStylusHandwriting;
mForceDefault = source.mForceDefault;
mStylusHandwritingSettingsActivityAttr = source.mStylusHandwritingSettingsActivityAttr;
}
@@ -463,6 +470,7 @@
mSubtypes = new InputMethodSubtypeArray(source);
mHandledConfigChanges = source.readInt();
mSupportsStylusHandwriting = source.readBoolean();
+ mSupportsConnectionlessStylusHandwriting = source.readBoolean();
mStylusHandwritingSettingsActivityAttr = source.readString8();
mForceDefault = false;
}
@@ -479,6 +487,7 @@
false /* inlineSuggestionsEnabled */, false /* isVrOnly */,
false /* isVirtualDeviceOnly */, 0 /* handledConfigChanges */,
false /* supportsStylusHandwriting */,
+ false /* supportConnectionlessStylusHandwriting */,
null /* stylusHandwritingSettingsActivityAttr */,
false /* inlineSuggestionsEnabled */);
}
@@ -488,9 +497,11 @@
* @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 @@
true /* supportsSwitchingToNextInputMethod */,
false /* inlineSuggestionsEnabled */, false /* isVrOnly */,
false /* isVirtualDeviceOnly */, 0 /* handledConfigChanges */,
- supportStylusHandwriting, stylusHandwritingSettingsActivityAttr,
- false /* inlineSuggestionsEnabled */);
+ supportStylusHandwriting, supportConnectionlessStylusHandwriting,
+ stylusHandwritingSettingsActivityAttr, false /* inlineSuggestionsEnabled */);
}
/**
@@ -517,6 +528,7 @@
false /* inlineSuggestionsEnabled */, false /* isVrOnly */,
false /* isVirtualDeviceOnly */, handledConfigChanges,
false /* supportsStylusHandwriting */,
+ false /* supportConnectionlessStylusHandwriting */,
null /* stylusHandwritingSettingsActivityAttr */,
false /* inlineSuggestionsEnabled */);
}
@@ -533,6 +545,7 @@
true /* supportsSwitchingToNextInputMethod */, false /* inlineSuggestionsEnabled */,
false /* isVrOnly */, false /* isVirtualDeviceOnly */, 0 /* handledconfigChanges */,
false /* supportsStylusHandwriting */,
+ false /* supportConnectionlessStylusHandwriting */,
null /* stylusHandwritingSettingsActivityAttr */,
false /* inlineSuggestionsEnabled */);
}
@@ -549,6 +562,7 @@
supportsSwitchingToNextInputMethod, false /* inlineSuggestionsEnabled */, isVrOnly,
false /* isVirtualDeviceOnly */,
0 /* handledConfigChanges */, false /* supportsStylusHandwriting */,
+ false /* supportConnectionlessStylusHandwriting */,
null /* stylusHandwritingSettingsActivityAttr */,
false /* inlineSuggestionsEnabled */);
}
@@ -562,7 +576,8 @@
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 @@
mIsVirtualDeviceOnly = isVirtualDeviceOnly;
mHandledConfigChanges = handledConfigChanges;
mSupportsStylusHandwriting = supportsStylusHandwriting;
+ mSupportsConnectionlessStylusHandwriting = supportsConnectionlessStylusHandwriting;
mStylusHandwritingSettingsActivityAttr = stylusHandwritingSettingsActivityAttr;
}
@@ -763,6 +779,16 @@
}
/**
+ * 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 @@
+ " mSuppressesSpellChecker=" + mSuppressesSpellChecker
+ " mShowInInputMethodPicker=" + mShowInInputMethodPicker
+ " mSupportsStylusHandwriting=" + mSupportsStylusHandwriting
+ + " mSupportsConnectionlessStylusHandwriting="
+ + mSupportsConnectionlessStylusHandwriting
+ " mStylusHandwritingSettingsActivityAttr="
+ mStylusHandwritingSettingsActivityAttr);
pw.println(prefix + "mIsDefaultResId=0x"
@@ -947,6 +975,7 @@
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 3b07f27..f4b09df 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -38,6 +38,7 @@
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 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.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 @@
@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 @@
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 @@
@Override
public Boolean recompute(Integer userId) {
return IInputMethodManagerGlobalInvoker.isStylusHandwritingAvailableAsUser(
- userId);
+ userId, /* connectionless= */ false);
}
};
}
@@ -1594,6 +1615,30 @@
}
/**
+ * 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 @@
}
/**
+ * 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 @@
* {@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 @@
* {@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 @@
* <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 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/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig
index 8b91bcb..55986e7 100644
--- a/core/java/android/view/inputmethod/flags.aconfig
+++ b/core/java/android/view/inputmethod/flags.aconfig
@@ -70,3 +70,11 @@
bug: "308827131"
is_fixed_read_only: true
}
+
+flag {
+ name: "connectionless_handwriting"
+ namespace: "input_method"
+ description: "Feature flag for connectionless stylus handwriting APIs"
+ bug: "300979854"
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/webkit/IWebViewUpdateService.aidl b/core/java/android/webkit/IWebViewUpdateService.aidl
index c6bd20c..aeb746c 100644
--- a/core/java/android/webkit/IWebViewUpdateService.aidl
+++ b/core/java/android/webkit/IWebViewUpdateService.aidl
@@ -45,6 +45,7 @@
* 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 f5b81b0..f336b5d 100644
--- a/core/java/android/webkit/WebView.java
+++ b/core/java/android/webkit/WebView.java
@@ -3087,14 +3087,22 @@
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 0000000..9b15ab3
--- /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 8e89541..3fc0a30 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 @@
* 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 53b047a..c748a57 100644
--- a/core/java/android/webkit/WebViewFactory.java
+++ b/core/java/android/webkit/WebViewFactory.java
@@ -285,10 +285,16 @@
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 @@
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 @@
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 91412d7..a68a577 100644
--- a/core/java/android/webkit/WebViewLibraryLoader.java
+++ b/core/java/android/webkit/WebViewLibraryLoader.java
@@ -24,7 +24,6 @@
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 @@
} 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 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 02e48dd..84e34a3 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 @@
}
@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 0000000..8ada598
--- /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 9152b43..6f53dde 100644
--- a/core/java/android/webkit/WebViewUpdateService.java
+++ b/core/java/android/webkit/WebViewUpdateService.java
@@ -33,14 +33,22 @@
* 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 @@
* 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 @@
* 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 e7280d0..40e28cb 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 @@
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 @@
* 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 @@
* @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 @@
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/IUnhandledDragCallback.aidl b/core/java/android/window/IUnhandledDragCallback.aidl
new file mode 100644
index 0000000..7806b1f
--- /dev/null
+++ b/core/java/android/window/IUnhandledDragCallback.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.view.DragEvent;
+
+/**
+ * A callback for notifying the system when the unhandled drop is complete.
+ * {@hide}
+ */
+oneway interface IUnhandledDragCallback {
+ /**
+ * Called when the IUnhandledDropListener has fully handled the drop, and the drag can be
+ * cleaned up. If handled is `true`, then cleanup of the drag and drag surface will be
+ * immediate, otherwise, the system will treat the drag as a cancel back to the start of the
+ * drag.
+ */
+ void notifyUnhandledDropComplete(boolean handled);
+}
diff --git a/core/java/android/window/IUnhandledDragListener.aidl b/core/java/android/window/IUnhandledDragListener.aidl
new file mode 100644
index 0000000..52e9895
--- /dev/null
+++ b/core/java/android/window/IUnhandledDragListener.aidl
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.view.DragEvent;
+import android.window.IUnhandledDragCallback;
+
+/**
+ * An interface to a handler for global drags that are not consumed (ie. not handled by any window).
+ * {@hide}
+ */
+oneway interface IUnhandledDragListener {
+ /**
+ * Called when the user finishes the drag gesture but no windows have reported handling the
+ * drop. The DragEvent is populated with the drag surface for the listener to animate. The
+ * listener *MUST* call the provided callback exactly once when it has finished handling the
+ * drop. If the listener calls the callback with `true` then it is responsible for removing
+ * and releasing the drag surface passed through the DragEvent.
+ */
+ void onUnhandledDrop(in DragEvent event, in IUnhandledDragCallback callback);
+}
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index efc71d7..76a34ae 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -676,18 +676,20 @@
* This identifies them.
* @param type The {@link InsetsType} of the insets source.
* @param frame The rectangle area of the insets source.
+ * @param boundingRects The bounding rects within this inset, relative to the |frame|.
* @hide
*/
@NonNull
public WindowContainerTransaction addInsetsSource(
@NonNull WindowContainerToken receiver,
- IBinder owner, int index, @InsetsType int type, Rect frame) {
+ IBinder owner, int index, @InsetsType int type, Rect frame, Rect[] boundingRects) {
final HierarchyOp hierarchyOp =
new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_ADD_INSETS_FRAME_PROVIDER)
.setContainer(receiver.asBinder())
.setInsetsFrameProvider(new InsetsFrameProvider(owner, index, type)
.setSource(InsetsFrameProvider.SOURCE_ARBITRARY_RECTANGLE)
- .setArbitraryRectangle(frame))
+ .setArbitraryRectangle(frame)
+ .setBoundingRects(boundingRects))
.setInsetsFrameOwner(owner)
.build();
mHierarchyOps.add(hierarchyOp);
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index 5c911f4..45d7767 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -371,11 +371,11 @@
}
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 3e48da7..eeaa3ef 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 @@
/**
* 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/src/com/android/systemui/scene/shared/model/SceneModel.kt b/core/java/com/android/internal/inputmethod/IConnectionlessHandwritingCallback.aidl
similarity index 61%
copy from packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneModel.kt
copy to core/java/com/android/internal/inputmethod/IConnectionlessHandwritingCallback.aidl
index f3d549f..e564599 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneModel.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.
@@ -14,14 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.scene.shared.model
+package com.android.internal.inputmethod;
-/** 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,
-)
+/** 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 0b7593a..c5c17cf 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -1146,7 +1146,7 @@
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 @@
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 739ee48..b2a6a93 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
@@ -309,7 +309,12 @@
private static void readBatteryLevelInt(int batteryLevelInt, BatteryStats.HistoryItem out) {
out.batteryLevel = (byte) ((batteryLevelInt & 0xfe000000) >>> 25);
out.batteryTemperature = (short) ((batteryLevelInt & 0x01ff8000) >>> 15);
- out.batteryVoltage = (char) ((batteryLevelInt & 0x00007ffe) >>> 1);
+ int voltage = ((batteryLevelInt & 0x00007ffe) >>> 1);
+ if (voltage == 0x3FFF) {
+ voltage = -1;
+ }
+
+ out.batteryVoltage = (short) voltage;
}
/**
diff --git a/core/java/com/android/internal/os/TimeoutRecord.java b/core/java/com/android/internal/os/TimeoutRecord.java
index e9a8d4b..1f4abc1 100644
--- a/core/java/com/android/internal/os/TimeoutRecord.java
+++ b/core/java/com/android/internal/os/TimeoutRecord.java
@@ -45,6 +45,7 @@
TimeoutKind.APP_REGISTERED,
TimeoutKind.SHORT_FGS_TIMEOUT,
TimeoutKind.JOB_SERVICE,
+ TimeoutKind.FGS_TIMEOUT,
})
@Retention(RetentionPolicy.SOURCE)
@@ -59,6 +60,7 @@
int SHORT_FGS_TIMEOUT = 8;
int JOB_SERVICE = 9;
int APP_START = 10;
+ int FGS_TIMEOUT = 11;
}
/** Kind of timeout, e.g. BROADCAST_RECEIVER, etc. */
@@ -186,6 +188,12 @@
return TimeoutRecord.endingNow(TimeoutKind.SHORT_FGS_TIMEOUT, reason);
}
+ /** Record for a "foreground service" timeout. */
+ @NonNull
+ public static TimeoutRecord forFgsTimeout(String reason) {
+ return TimeoutRecord.endingNow(TimeoutKind.FGS_TIMEOUT, reason);
+ }
+
/** Record for a job related timeout. */
@NonNull
public static TimeoutRecord forJobService(String reason) {
diff --git a/core/java/com/android/internal/os/anr/AnrLatencyTracker.java b/core/java/com/android/internal/os/anr/AnrLatencyTracker.java
index f62ff38..e11067d 100644
--- a/core/java/com/android/internal/os/anr/AnrLatencyTracker.java
+++ b/core/java/com/android/internal/os/anr/AnrLatencyTracker.java
@@ -22,6 +22,7 @@
import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__BROADCAST_OF_INTENT;
import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__CONTENT_PROVIDER_NOT_RESPONDING;
import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__EXECUTING_SERVICE;
+import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__FGS_TIMEOUT;
import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__INPUT_DISPATCHING_TIMEOUT;
import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__INPUT_DISPATCHING_TIMEOUT_NO_FOCUSED_WINDOW;
import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__JOB_SERVICE;
@@ -548,6 +549,8 @@
return ANRLATENCY_REPORTED__ANR_TYPE__SHORT_FGS_TIMEOUT;
case TimeoutKind.JOB_SERVICE:
return ANRLATENCY_REPORTED__ANR_TYPE__JOB_SERVICE;
+ case TimeoutKind.FGS_TIMEOUT:
+ return ANRLATENCY_REPORTED__ANR_TYPE__FGS_TIMEOUT;
default:
return ANRLATENCY_REPORTED__ANR_TYPE__UNKNOWN_ANR_TYPE;
}
diff --git a/core/java/com/android/internal/protolog/OWNERS b/core/java/com/android/internal/protolog/OWNERS
new file mode 100644
index 0000000..18cf2be
--- /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 595bf3b..e95127b 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 @@
/** 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 @@
/** 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/java/com/android/internal/widget/NotificationOptimizedLinearLayout.java b/core/java/com/android/internal/widget/NotificationOptimizedLinearLayout.java
index b5e9b8f..0ceba25 100644
--- a/core/java/com/android/internal/widget/NotificationOptimizedLinearLayout.java
+++ b/core/java/com/android/internal/widget/NotificationOptimizedLinearLayout.java
@@ -53,6 +53,7 @@
* - LinearLayout doesn't have <code>weightSum</code>.
* - Horizontal LinearLayout's width should be measured EXACTLY.
* - Horizontal LinearLayout shouldn't need baseLineAlignment.
+ * - Horizontal LinearLayout shouldn't have any child that has negative left or right margin.
* - Vertical LinearLayout shouldn't have MATCH_PARENT children when it is not measured EXACTLY.
*
* @hide
@@ -88,7 +89,7 @@
final View weightedChildView = getSingleWeightedChild();
mShouldUseOptimizedLayout =
isUseOptimizedLinearLayoutFlagEnabled() && weightedChildView != null
- && isLinearLayoutUsable(widthMeasureSpec, heightMeasureSpec);
+ && isOptimizationPossible(widthMeasureSpec, heightMeasureSpec);
if (mShouldUseOptimizedLayout) {
onMeasureOptimized(weightedChildView, widthMeasureSpec, heightMeasureSpec);
@@ -118,7 +119,7 @@
* @param heightMeasureSpec The height measurement specification.
* @return `true` if optimization is possible, `false` otherwise.
*/
- private boolean isLinearLayoutUsable(int widthMeasureSpec, int heightMeasureSpec) {
+ private boolean isOptimizationPossible(int widthMeasureSpec, int heightMeasureSpec) {
final boolean hasWeightSum = getWeightSum() > 0.0f;
if (hasWeightSum) {
logSkipOptimizedOnMeasure("Has weightSum.");
@@ -142,10 +143,36 @@
logSkipOptimizedOnMeasure("Need to apply baseline.");
return false;
}
+
+ if (requiresNegativeMarginHandlingForHorizontalLinearLayout()) {
+ logSkipOptimizedOnMeasure("Need to handle negative margins.");
+ return false;
+ }
return true;
}
/**
+ * @return if the horizontal linearlayout requires to handle negative margins in its children.
+ * In that case, we can't use excessSpace because LinearLayout negative margin handling for
+ * excess space and WRAP_CONTENT is different.
+ */
+ private boolean requiresNegativeMarginHandlingForHorizontalLinearLayout() {
+ if (getOrientation() == VERTICAL) {
+ return false;
+ }
+
+ final List<View> activeChildren = getActiveChildren();
+ for (int i = 0; i < activeChildren.size(); i++) {
+ final View child = activeChildren.get(i);
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+ if (lp.leftMargin < 0 || lp.rightMargin < 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
* @return if the vertical linearlayout requires match_parent children remeasure
*/
private boolean requiresMatchParentRemeasureForVerticalLinearLayout(int widthMeasureSpec) {
@@ -337,94 +364,81 @@
*/
private void measureVerticalOptimized(@NonNull View weightedChildView, int widthMeasureSpec,
int heightMeasureSpec) {
- final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
- final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int totalLength = 0;
int maxWidth = 0;
- int usedHeight = 0;
- final List<View> activeChildren = getActiveChildren();
- final int activeChildCount = activeChildren.size();
+ final int availableHeight = MeasureSpec.getSize(heightMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
- final boolean isContentFirstItem = !activeChildren.isEmpty() && activeChildren.get(0)
- == weightedChildView;
-
- final boolean isContentLastItem = !activeChildren.isEmpty() && activeChildren.get(
- activeChildCount - 1) == weightedChildView;
-
- final int horizontalPaddings = getPaddingLeft() + getPaddingRight();
-
- // 1. Measure other child views.
- for (int i = 0; i < activeChildCount; i++) {
- final View child = activeChildren.get(i);
- if (child == weightedChildView) {
+ // 1. Measure all unweighted children
+ for (int i = 0; i < getChildCount(); i++) {
+ final View child = getChildAt(i);
+ if (child == null || child.getVisibility() == GONE) {
continue;
}
+
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
- int requiredVerticalPadding = lp.topMargin + lp.bottomMargin;
- if (!isContentFirstItem && i == 0) {
- requiredVerticalPadding += getPaddingTop();
- }
- if (!isContentLastItem && i == activeChildCount - 1) {
- requiredVerticalPadding += getPaddingBottom();
+ if (child == weightedChildView) {
+ // In excessMode, LinearLayout add weighted child top and bottom margins to
+ // totalLength when their sum is positive.
+ if (lp.height == 0 && heightMode == MeasureSpec.EXACTLY) {
+ totalLength = Math.max(totalLength, totalLength + lp.topMargin
+ + lp.bottomMargin);
+ }
+ continue;
}
- child.measure(ViewGroup.getChildMeasureSpec(widthMeasureSpec,
- horizontalPaddings + lp.leftMargin + lp.rightMargin,
- child.getLayoutParams().width),
- ViewGroup.getChildMeasureSpec(heightMeasureSpec, requiredVerticalPadding,
- lp.height));
+ measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
+ // LinearLayout only adds measured children heights and its top and bottom margins
+ // to totalLength when their sum is positive.
+ totalLength = Math.max(totalLength,
+ totalLength + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
- usedHeight += child.getMeasuredHeight() + requiredVerticalPadding;
}
- // measure content
+ // Add padding to totalLength that we are going to use for remaining space.
+ totalLength += mPaddingTop + mPaddingBottom;
+
+ // 2. generate measure spec for weightedChildView.
final MarginLayoutParams lp = (MarginLayoutParams) weightedChildView.getLayoutParams();
+ // height should be AT_MOST for non EXACT cases.
+ final int childHeightMeasureMode =
+ heightMode == MeasureSpec.EXACTLY ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
+ final int childHeightMeasureSpec;
- int usedSpace = usedHeight + lp.topMargin + lp.bottomMargin;
- if (isContentFirstItem) {
- usedSpace += getPaddingTop();
+ // In excess mode, LinearLayout measures weighted children with remaining space. Otherwise,
+ // it is measured with remaining space just like other children.
+ if (lp.height == 0 && heightMode == MeasureSpec.EXACTLY) {
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ Math.max(0, availableHeight - totalLength), childHeightMeasureMode);
+ } else {
+ final int usedHeight = lp.topMargin + lp.bottomMargin + totalLength;
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ Math.max(0, availableHeight - usedHeight), childHeightMeasureMode);
}
- if (isContentLastItem) {
- usedSpace += getPaddingBottom();
- }
+ final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
+ mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin, lp.width);
- final int availableWidth = MeasureSpec.getSize(widthMeasureSpec);
- final int availableHeight = MeasureSpec.getSize(heightMeasureSpec);
-
- final int childWidthMeasureSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
- horizontalPaddings + lp.leftMargin + lp.rightMargin, lp.width);
-
- // 2. Calculate remaining height for weightedChildView.
- final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
- Math.max(0, availableHeight - usedSpace), MeasureSpec.AT_MOST);
-
- // 3. Measure weightedChildView with the remaining remaining space.
+ // 3. Measure weightedChildView with the remaining space.
weightedChildView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+
+ totalLength = Math.max(totalLength,
+ totalLength + weightedChildView.getMeasuredHeight() + lp.topMargin
+ + lp.bottomMargin);
+
maxWidth = Math.max(maxWidth,
weightedChildView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
- final int totalUsedHeight = usedSpace + weightedChildView.getMeasuredHeight();
+ // Add padding to width
+ maxWidth += getPaddingLeft() + getPaddingRight();
- final int measuredWidth;
- if (widthMode == MeasureSpec.EXACTLY) {
- measuredWidth = availableWidth;
- } else {
- measuredWidth = maxWidth + getPaddingStart() + getPaddingEnd();
- }
-
- final int measuredHeight;
- if (heightMode == MeasureSpec.EXACTLY) {
- measuredHeight = availableHeight;
- } else {
- measuredHeight = totalUsedHeight;
- }
-
- // 4. Set the container size
- setMeasuredDimension(
- resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth),
- widthMeasureSpec),
- Math.max(getSuggestedMinimumHeight(), measuredHeight));
+ // Resolve final dimensions
+ final int finalWidth = resolveSizeAndState(Math.max(maxWidth, getSuggestedMinimumWidth()),
+ widthMeasureSpec, 0);
+ final int finalHeight = resolveSizeAndState(
+ Math.max(totalLength, getSuggestedMinimumHeight()), heightMeasureSpec, 0);
+ setMeasuredDimension(finalWidth, finalHeight);
}
@NonNull
diff --git a/core/jni/LayoutlibLoader.cpp b/core/jni/LayoutlibLoader.cpp
index 200ddef..01e9f43 100644
--- a/core/jni/LayoutlibLoader.cpp
+++ b/core/jni/LayoutlibLoader.cpp
@@ -416,11 +416,17 @@
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 @@
// 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 ce4a337..8dc9d0a 100644
--- a/core/jni/android_os_VintfObject.cpp
+++ b/core/jni/android_os_VintfObject.cpp
@@ -39,6 +39,7 @@
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 @@
return nullptr;
}
- Version latest;
+ SepolicyVersion latest;
for (const auto& range : versions) {
latest = std::max(latest, range.maxVer());
}
diff --git a/core/proto/OWNERS b/core/proto/OWNERS
index c65794e..b900fa6 100644
--- a/core/proto/OWNERS
+++ b/core/proto/OWNERS
@@ -22,7 +22,6 @@
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 c62e536..4fc9b40 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -144,6 +144,7 @@
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 34c4045..277824c 100644
--- a/core/res/Android.bp
+++ b/core/res/Android.bp
@@ -154,6 +154,14 @@
},
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 a425bb0..04367e7 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_*
@@ -3829,6 +3849,7 @@
@hide This is not a third-party API (intended for OEMs and system apps). -->
<permission android:name="android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES"
android:protectionLevel="signature|installer" />
+ <uses-permission android:name="android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES" />
<!-- @SystemApi @hide Allows an application to set a device owner on retail demo devices.-->
<permission android:name="android.permission.PROVISION_DEMO_DEVICE"
@@ -5921,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
@@ -7033,12 +7054,16 @@
<!-- Allows the holder to read blocked numbers. See
{@link android.provider.BlockedNumberContract}.
+ @SystemApi
+ @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies")
@hide -->
<permission android:name="android.permission.READ_BLOCKED_NUMBERS"
android:protectionLevel="signature" />
<!-- Allows the holder to write blocked numbers. See
{@link android.provider.BlockedNumberContract}.
+ @SystemApi
+ @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies")
@hide -->
<permission android:name="android.permission.WRITE_BLOCKED_NUMBERS"
android:protectionLevel="signature" />
diff --git a/core/res/OWNERS b/core/res/OWNERS
index 332ad2a..6924248 100644
--- a/core/res/OWNERS
+++ b/core/res/OWNERS
@@ -8,7 +8,6 @@
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 0000000..aba87f5
--- /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 0000000..0a67ce3
--- /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 0000000..2d7fe7d
--- /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 28c0f8a..b042c39 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 5cb6a9d..5f1f1b7 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 98cc88d..429070e 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 b8d93ac..509b988 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 0000000..0b00bd8
--- /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 41bc825..4ee03de 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 53a6270..b879c97 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 78ce2d9..104b7cd 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 0acccee..291a593 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 972fe7e..fa15c3f 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 5ee5555..dcb6bb0 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 3d19c85..7c290b1 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 84f1d6e..ee19144 100644
--- a/core/res/res/values/themes_device_defaults.xml
+++ b/core/res/res/values/themes_device_defaults.xml
@@ -283,6 +283,7 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 5aace81..e4cf7ac 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 @@
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 0806fa0..db95d7a 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 int getSoftApInterations() {
+ public int getSoftApIterations() {
return mSoftApIterations;
}
diff --git a/core/tests/coretests/src/android/view/InsetsSourceTest.java b/core/tests/coretests/src/android/view/InsetsSourceTest.java
index e1bcd4a..936f4d7 100644
--- a/core/tests/coretests/src/android/view/InsetsSourceTest.java
+++ b/core/tests/coretests/src/android/view/InsetsSourceTest.java
@@ -279,5 +279,204 @@
}
}
+ @Test
+ public void testCalculateBoundingRects_noBoundingRects_createsSingleRect() {
+ mSource.setFrame(new Rect(0, 0, 1000, 100));
+ mSource.setBoundingRects(null);
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 1000, 1000), false);
+
+ assertEquals(1, rects.length);
+ assertEquals(new Rect(0, 0, 1000, 100), rects[0]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_noBoundingRectsAndLargerFrame_singleRectFitsRelFrame() {
+ mSource.setFrame(new Rect(0, 0, 1000, 100));
+ mSource.setBoundingRects(null);
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 500, 1000), false);
+
+ assertEquals(1, rects.length);
+ assertEquals(new Rect(0, 0, 500, 100), rects[0]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_frameAtOrigin_resultRelativeToRelFrame() {
+ mSource.setFrame(new Rect(0, 0, 1000, 100));
+ mSource.setBoundingRects(new Rect[]{
+ new Rect(0, 0, 300, 100),
+ new Rect(800, 0, 1000, 100),
+ });
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 1000, 1000), false);
+
+ assertEquals(2, rects.length);
+ assertEquals(new Rect(0, 0, 300, 100), rects[0]);
+ assertEquals(new Rect(800, 0, 1000, 100), rects[1]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_notAtOrigin_resultRelativeToRelFrame() {
+ mSource.setFrame(new Rect(100, 100, 1100, 200));
+ mSource.setBoundingRects(new Rect[]{
+ new Rect(0, 0, 300, 100), // 300x100, aligned left
+ new Rect(800, 0, 1000, 100), // 200x100, aligned right
+ });
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(100, 100, 1100, 1100), false);
+
+ assertEquals(2, rects.length);
+ assertEquals(new Rect(0, 0, 300, 100), rects[0]);
+ assertEquals(new Rect(800, 0, 1000, 100), rects[1]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_boundingRectFullyInsideFrameInWindow() {
+ mSource.setFrame(new Rect(0, 0, 1000, 100));
+ mSource.setBoundingRects(new Rect[]{
+ new Rect(100, 0, 400, 100), // Inside |frame| and |relativeFrame|.
+ });
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 500, 100), false);
+
+ assertEquals(1, rects.length);
+ assertEquals(new Rect(100, 0, 400, 100), rects[0]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_boundingRectOutsideFrameInWindow_dropped() {
+ mSource.setFrame(new Rect(0, 0, 1000, 100));
+ mSource.setBoundingRects(new Rect[]{
+ new Rect(700, 0, 1000, 100), // Inside |frame|, but outside |relativeFrame|.
+ });
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 500, 100), false);
+
+ assertEquals(0, rects.length);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_boundingRectPartlyOutsideFrameInWindow_cropped() {
+ mSource.setFrame(new Rect(0, 0, 1000, 100));
+ mSource.setBoundingRects(new Rect[]{
+ new Rect(400, 0, 600, 100), // Inside |frame|, and only half inside |relativeFrame|.
+ });
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 500, 100), false);
+
+ assertEquals(1, rects.length);
+ assertEquals(new Rect(400, 0, 500, 100), rects[0]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_framesNotAtOrigin_resultRelativeToWindowFrame() {
+ mSource.setFrame(new Rect(100, 100, 1100, 200));
+ mSource.setBoundingRects(new Rect[]{
+ new Rect(0, 0, 300, 100), // 300x100 aligned to left.
+ new Rect(800, 0, 1000, 100) // 200x100 align to right.
+ });
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(100, 100, 1100, 1100), false);
+
+ assertEquals(2, rects.length);
+ assertEquals(new Rect(0, 0, 300, 100), rects[0]);
+ assertEquals(new Rect(800, 0, 1000, 100), rects[1]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_captionBar() {
+ mCaptionSource.setFrame(new Rect(0, 0, 1000, 100));
+ mCaptionSource.setBoundingRects(new Rect[]{
+ new Rect(0, 0, 200, 100), // 200x100, aligned left.
+ new Rect(800, 0, 1000, 100) // 200x100, aligned right.
+ });
+
+ final Rect[] rects = mCaptionSource.calculateBoundingRects(
+ new Rect(0, 0, 1000, 1000), false);
+
+ assertEquals(2, rects.length);
+ assertEquals(new Rect(0, 0, 200, 100), rects[0]);
+ assertEquals(new Rect(800, 0, 1000, 100), rects[1]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_captionBarFrameMisaligned_rectsFixedToTop() {
+ mCaptionSource.setFrame(new Rect(500, 500, 1500, 600));
+ mCaptionSource.setBoundingRects(new Rect[]{
+ new Rect(0, 0, 100, 100), // 100x100, aligned to left/top of frame
+ });
+
+ final Rect[] rects = mCaptionSource.calculateBoundingRects(
+ new Rect(495, 495, 1500, 1500), false);
+
+ assertEquals(1, rects.length);
+ // rect should be aligned to the top of relative frame, as if the caption frame had been
+ // corrected to be aligned at the top.
+ assertEquals(new Rect(0, 0, 100, 100), rects[0]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_imeCaptionBarFrameMisaligned_rectsFixedToBottom() {
+ mImeCaptionSource.setFrame(new Rect(500, 1400, 1500, 1500));
+ mImeCaptionSource.setBoundingRects(new Rect[]{
+ new Rect(0, 0, 100, 100), // 100x100, aligned to left/top of frame
+ });
+
+ final Rect[] rects = mImeCaptionSource.calculateBoundingRects(
+ new Rect(495, 495, 1500, 1500), false);
+
+ assertEquals(1, rects.length);
+ // rect should be aligned to the bottom of relative frame, as if the ime caption frame had
+ // been corrected to be aligned at the top.
+ assertEquals(new Rect(0, 905, 100, 1005), rects[0]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_imeCaptionBar() {
+ mImeCaptionSource.setFrame(new Rect(0, 900, 1000, 1000)); // Frame at the bottom.
+ mImeCaptionSource.setBoundingRects(new Rect[]{
+ new Rect(0, 0, 200, 100), // 200x100, aligned left.
+ });
+
+ final Rect[] rects = mImeCaptionSource.calculateBoundingRects(
+ new Rect(0, 0, 1000, 1000), false);
+
+ assertEquals(1, rects.length);
+ assertEquals(new Rect(0, 900, 200, 1000), rects[0]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_invisible() {
+ mSource.setFrame(new Rect(0, 0, 1000, 100));
+ mSource.setBoundingRects(new Rect[]{
+ new Rect(0, 0, 300, 100),
+ new Rect(800, 0, 1000, 100),
+ });
+ mSource.setVisible(false);
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 1000, 1000),
+ false /* ignoreVisibility */);
+
+ assertEquals(0, rects.length);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_ignoreVisibility() {
+ mSource.setFrame(new Rect(0, 0, 1000, 100));
+ mSource.setBoundingRects(new Rect[]{
+ new Rect(0, 0, 300, 100),
+ new Rect(800, 0, 1000, 100),
+ });
+ mSource.setVisible(false);
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 1000, 1000),
+ true /* ignoreVisibility */);
+
+ assertEquals(2, rects.length);
+ assertEquals(new Rect(0, 0, 300, 100), rects[0]);
+ assertEquals(new Rect(800, 0, 1000, 100), rects[1]);
+ }
+
// Parcel and equals already tested via InsetsStateTest
}
diff --git a/core/tests/coretests/src/android/view/InsetsStateTest.java b/core/tests/coretests/src/android/view/InsetsStateTest.java
index 672875a..16bd20a 100644
--- a/core/tests/coretests/src/android/view/InsetsStateTest.java
+++ b/core/tests/coretests/src/android/view/InsetsStateTest.java
@@ -63,6 +63,8 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.List;
+
/**
* Tests for {@link InsetsState}.
*
@@ -88,6 +90,8 @@
null /* owner */, 1 /* index */, navigationBars());
private static final int ID_BOTTOM_GESTURES = InsetsSource.createId(
null /* owner */, 0 /* index */, systemGestures());
+ private static final int ID_EXTRA_CAPTION_BAR = InsetsSource.createId(
+ null /* owner */, 2 /* index */, captionBar());
private final InsetsState mState = new InsetsState();
private final InsetsState mState2 = new InsetsState();
@@ -420,9 +424,11 @@
public void testEquals_visibility() {
mState.getOrCreateSource(ID_IME, ime())
.setFrame(new Rect(0, 0, 100, 100))
+ .setBoundingRects(new Rect[]{ new Rect(0, 0, 10, 10) })
.setVisible(true);
mState2.getOrCreateSource(ID_IME, ime())
- .setFrame(new Rect(0, 0, 100, 100));
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setBoundingRects(new Rect[]{ new Rect(0, 0, 10, 10) });
assertNotEqualsAndHashCode();
}
@@ -441,6 +447,30 @@
}
@Test
+ public void testEquals_sameBoundingRects() {
+ mState.getOrCreateSource(ID_CAPTION_BAR, captionBar())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setBoundingRects(new Rect[]{ new Rect(0, 0, 10, 10) })
+ .setVisible(true);
+ mState2.getOrCreateSource(ID_CAPTION_BAR, captionBar())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setBoundingRects(new Rect[]{ new Rect(0, 0, 10, 10) });
+ assertEqualsAndHashCode();
+ }
+
+ @Test
+ public void testEquals_differentBoundingRects() {
+ mState.getOrCreateSource(ID_CAPTION_BAR, captionBar())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setBoundingRects(new Rect[]{ new Rect(0, 0, 10, 10) })
+ .setVisible(true);
+ mState2.getOrCreateSource(ID_CAPTION_BAR, captionBar())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setBoundingRects(new Rect[]{ new Rect(0, 0, 20, 20) });
+ assertNotEqualsAndHashCode();
+ }
+
+ @Test
public void testEquals_samePrivacyIndicator() {
Rect one = new Rect(0, 1, 2, 3);
Rect two = new Rect(4, 5, 6, 7);
@@ -734,4 +764,94 @@
assertEquals(1, onIdNotFoundInState2Called[0]); // 1000.
assertEquals(1, onFinishCalled[0]);
}
+
+ @Test
+ public void testCalculateBoundingRects() {
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 1000, 100))
+ .setBoundingRects(null)
+ .setVisible(true);
+ mState.getOrCreateSource(ID_CAPTION_BAR, captionBar())
+ .setFrame(new Rect(0, 0, 1000, 100))
+ .setBoundingRects(new Rect[]{
+ new Rect(0, 0, 200, 100),
+ new Rect(800, 0, 1000, 100)
+ })
+ .setVisible(true);
+ SparseIntArray typeSideMap = new SparseIntArray();
+
+ WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 1000, 1000), null, false,
+ SOFT_INPUT_ADJUST_RESIZE, 0, 0, TYPE_APPLICATION, ACTIVITY_TYPE_UNDEFINED,
+ typeSideMap);
+
+ assertEquals(
+ List.of(new Rect(0, 0, 1000, 100)),
+ insets.getBoundingRects(Type.statusBars())
+ );
+ assertEquals(
+ List.of(
+ new Rect(0, 0, 200, 100),
+ new Rect(800, 0, 1000, 100)
+ ),
+ insets.getBoundingRects(Type.captionBar())
+ );
+ }
+
+ @Test
+ public void testCalculateBoundingRects_multipleSourcesOfSameType_concatenated() {
+ mState.getOrCreateSource(ID_CAPTION_BAR, captionBar())
+ .setFrame(new Rect(0, 0, 1000, 100))
+ .setBoundingRects(new Rect[]{new Rect(0, 0, 200, 100)})
+ .setVisible(true);
+ mState.getOrCreateSource(ID_EXTRA_CAPTION_BAR, captionBar())
+ .setFrame(new Rect(0, 0, 1000, 100))
+ .setBoundingRects(new Rect[]{new Rect(800, 0, 1000, 100)})
+ .setVisible(true);
+ SparseIntArray typeSideMap = new SparseIntArray();
+
+ WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 1000, 1000), null, false,
+ SOFT_INPUT_ADJUST_RESIZE, 0, 0, TYPE_APPLICATION, ACTIVITY_TYPE_UNDEFINED,
+ typeSideMap);
+
+ final List<Rect> expected = List.of(
+ new Rect(0, 0, 200, 100),
+ new Rect(800, 0, 1000, 100)
+ );
+ final List<Rect> actual = insets.getBoundingRects(captionBar());
+ assertEquals(expected.size(), actual.size());
+
+ // Order does not matter.
+ assertTrue(actual.containsAll(expected));
+ }
+
+ @Test
+ public void testCalculateBoundingRects_captionBar_reportedAsSysGesturesAndTappableElement() {
+ mState.getOrCreateSource(ID_CAPTION_BAR, captionBar())
+ .setFrame(new Rect(0, 0, 1000, 100))
+ .setBoundingRects(new Rect[]{new Rect(0, 0, 200, 100)})
+ .setVisible(true);
+ SparseIntArray typeSideMap = new SparseIntArray();
+
+ WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 1000, 1000), null, false,
+ SOFT_INPUT_ADJUST_RESIZE, 0, 0, TYPE_APPLICATION, ACTIVITY_TYPE_UNDEFINED,
+ typeSideMap);
+
+ assertEquals(
+ List.of(new Rect(0, 0, 200, 100)),
+ insets.getBoundingRects(Type.captionBar())
+ );
+ assertEquals(
+ List.of(new Rect(0, 0, 200, 100)),
+ insets.getBoundingRects(Type.systemGestures())
+ );
+ assertEquals(
+ List.of(new Rect(0, 0, 200, 100)),
+ insets.getBoundingRects(Type.mandatorySystemGestures())
+ );
+ assertEquals(
+ List.of(new Rect(0, 0, 200, 100)),
+ insets.getBoundingRects(Type.tappableElement())
+ );
+
+ }
}
diff --git a/core/tests/coretests/src/android/view/WindowInsetsTest.java b/core/tests/coretests/src/android/view/WindowInsetsTest.java
index 69abf5f..ab4543c 100644
--- a/core/tests/coretests/src/android/view/WindowInsetsTest.java
+++ b/core/tests/coretests/src/android/view/WindowInsetsTest.java
@@ -41,14 +41,14 @@
public void systemWindowInsets_afterConsuming_isConsumed() {
assertTrue(new WindowInsets(WindowInsets.createCompatTypeMap(new Rect(1, 2, 3, 4)), null,
null, false, 0, 0, null, null, null, null,
- WindowInsets.Type.systemBars(), false)
+ WindowInsets.Type.systemBars(), false, null, null, 0, 0)
.consumeSystemWindowInsets().isConsumed());
}
@Test
public void multiNullConstructor_isConsumed() {
assertTrue(new WindowInsets(null, null, null, false, 0, 0, null, null, null, null,
- WindowInsets.Type.systemBars(), false).isConsumed());
+ WindowInsets.Type.systemBars(), false, null, null, 0, 0).isConsumed());
}
@Test
@@ -65,7 +65,7 @@
WindowInsets.assignCompatInsets(insets, new Rect(0, 0, 0, 0));
WindowInsets windowInsets = new WindowInsets(insets, maxInsets, visible, false, 0,
0, null, null, null, DisplayShape.NONE, systemBars(),
- true /* compatIgnoreVisibility */);
+ true /* compatIgnoreVisibility */, null, null, 0, 0);
assertEquals(Insets.of(0, 10, 0, 0), windowInsets.getSystemWindowInsets());
}
}
diff --git a/core/tests/coretests/src/com/android/internal/widget/ActionBarOverlayLayoutTest.java b/core/tests/coretests/src/com/android/internal/widget/ActionBarOverlayLayoutTest.java
index 84dd274..8d66cfc 100644
--- a/core/tests/coretests/src/com/android/internal/widget/ActionBarOverlayLayoutTest.java
+++ b/core/tests/coretests/src/com/android/internal/widget/ActionBarOverlayLayoutTest.java
@@ -169,7 +169,8 @@
private WindowInsets insetsWith(Insets content, DisplayCutout cutout) {
return new WindowInsets(WindowInsets.createCompatTypeMap(content.toRect()), null, null,
- false, 0, 0, cutout, null, null, null, WindowInsets.Type.systemBars(), false);
+ false, 0, 0, cutout, null, null, null, WindowInsets.Type.systemBars(), false,
+ null, null, 0, 0);
}
private ViewGroup createViewGroupWithId(int id) {
diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationOptimizedLinearLayoutComparisonTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationOptimizedLinearLayoutComparisonTest.java
index 08333ec..bf9221a 100644
--- a/core/tests/coretests/src/com/android/internal/widget/NotificationOptimizedLinearLayoutComparisonTest.java
+++ b/core/tests/coretests/src/com/android/internal/widget/NotificationOptimizedLinearLayoutComparisonTest.java
@@ -31,6 +31,7 @@
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.widget.LinearLayout;
+import android.widget.TextView;
import android.widget.flags.Flags;
import androidx.test.InstrumentationRegistry;
@@ -73,7 +74,7 @@
private static final int[] LAYOUT_PARAMS = {MATCH_PARENT, WRAP_CONTENT, 0, 50};
private static final int[] CHILD_WEIGHTS = {0, 1};
-
+ private static final int[] CHILD_MARGINS = {0, 10, -10};
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -84,35 +85,96 @@
mContext = InstrumentationRegistry.getTargetContext();
}
+
@Test
public void test() throws Throwable {
+ final List<View> controlChildren =
+ new ArrayList<>();
+ final List<View> testChildren =
+ new ArrayList<>();
+
+ final View controlChild1 = buildChildView();
+ final View controlChild2 = buildChildView();
+ controlChildren.add(controlChild1);
+ controlChildren.add(controlChild2);
+
+ final View testChild1 = buildChildView();
+ final View testChild2 = buildChildView();
+ testChildren.add(testChild1);
+ testChildren.add(testChild2);
+
+ final LinearLayout controlContainer = buildLayout(false, controlChildren);
+
+ final LinearLayout testContainer = buildLayout(true, testChildren);
+
+ final LinearLayout.LayoutParams firstChildLayoutParams = new LinearLayout.LayoutParams(0,
+ 0);
+ final LinearLayout.LayoutParams secondChildLayoutParams = new LinearLayout.LayoutParams(0,
+ 0);
+ controlChild1.setLayoutParams(firstChildLayoutParams);
+ controlChild2.setLayoutParams(secondChildLayoutParams);
+ testChild1.setLayoutParams(firstChildLayoutParams);
+ testChild2.setLayoutParams(secondChildLayoutParams);
+
for (int orientation : ORIENTATIONS) {
- for (int widthSpec : MEASURE_SPECS) {
- for (int heightSpec : MEASURE_SPECS) {
- for (int firstChildGravity : GRAVITIES) {
- for (int secondChildGravity : GRAVITIES) {
- for (int firstChildLayoutWidth : LAYOUT_PARAMS) {
- for (int firstChildLayoutHeight : LAYOUT_PARAMS) {
- for (int secondChildLayoutWidth : LAYOUT_PARAMS) {
- for (int secondChildLayoutHeight : LAYOUT_PARAMS) {
+ controlContainer.setOrientation(orientation);
+ testContainer.setOrientation(orientation);
+
+ for (int firstChildLayoutWidth : LAYOUT_PARAMS) {
+ firstChildLayoutParams.width = firstChildLayoutWidth;
+ for (int firstChildLayoutHeight : LAYOUT_PARAMS) {
+ firstChildLayoutParams.height = firstChildLayoutHeight;
+
+ for (int secondChildLayoutWidth : LAYOUT_PARAMS) {
+ secondChildLayoutParams.width = secondChildLayoutWidth;
+ for (int secondChildLayoutHeight : LAYOUT_PARAMS) {
+ secondChildLayoutParams.height = secondChildLayoutHeight;
+
+ for (int firstChildMargin : CHILD_MARGINS) {
+ firstChildLayoutParams.setMargins(firstChildMargin,
+ firstChildMargin, firstChildMargin, firstChildMargin);
+ for (int secondChildMargin : CHILD_MARGINS) {
+ secondChildLayoutParams.setMargins(secondChildMargin,
+ secondChildMargin, secondChildMargin,
+ secondChildMargin);
+
+ for (int firstChildGravity : GRAVITIES) {
+ firstChildLayoutParams.gravity = firstChildGravity;
+ for (int secondChildGravity : GRAVITIES) {
+ secondChildLayoutParams.gravity = secondChildGravity;
+
for (int firstChildWeight : CHILD_WEIGHTS) {
+ firstChildLayoutParams.weight = firstChildWeight;
for (int secondChildWeight : CHILD_WEIGHTS) {
- executeTest(/*testSpec =*/createTestSpec(
- orientation,
- widthSpec, heightSpec,
- firstChildLayoutWidth,
- firstChildLayoutHeight,
- secondChildLayoutWidth,
- secondChildLayoutHeight,
- firstChildGravity,
- secondChildGravity,
- firstChildWeight,
- secondChildWeight));
+ secondChildLayoutParams.weight =
+ secondChildWeight;
+
+ for (int widthSpec : MEASURE_SPECS) {
+ for (int heightSpec : MEASURE_SPECS) {
+ executeTest(controlContainer,
+ testContainer,
+ createTestSpec(
+ orientation,
+ widthSpec, heightSpec,
+ firstChildLayoutWidth,
+ firstChildLayoutHeight,
+ secondChildLayoutWidth,
+ secondChildLayoutHeight,
+ firstChildGravity,
+ secondChildGravity,
+ firstChildWeight,
+ secondChildWeight,
+ firstChildMargin,
+ secondChildMargin)
+ );
+ }
+ }
}
}
}
}
}
+
}
}
}
@@ -121,47 +183,8 @@
}
}
- private void executeTest(TestSpec testSpec) {
- // GIVEN
- final List<View> controlChildren =
- new ArrayList<>();
- final List<View> testChildren =
- new ArrayList<>();
-
- controlChildren.add(
- buildChildView(
- testSpec.mFirstChildLayoutWidth,
- testSpec.mFirstChildLayoutHeight,
- testSpec.mFirstChildGravity,
- testSpec.mFirstChildWeight));
- controlChildren.add(
- buildChildView(
- testSpec.mSecondChildLayoutWidth,
- testSpec.mSecondChildLayoutHeight,
- testSpec.mSecondChildGravity,
- testSpec.mSecondChildWeight));
-
- testChildren.add(
- buildChildView(
- testSpec.mFirstChildLayoutWidth,
- testSpec.mFirstChildLayoutHeight,
- testSpec.mFirstChildGravity,
- testSpec.mFirstChildWeight));
- testChildren.add(
- buildChildView(
- testSpec.mSecondChildLayoutWidth,
- testSpec.mSecondChildLayoutHeight,
- testSpec.mSecondChildGravity,
- testSpec.mSecondChildWeight));
-
- final LinearLayout controlContainer = buildLayout(false,
- testSpec.mOrientation,
- controlChildren);
-
- final LinearLayout testContainer = buildLayout(true,
- testSpec.mOrientation,
- testChildren);
-
+ private void executeTest(LinearLayout controlContainer, LinearLayout testContainer,
+ TestSpec testSpec) {
// WHEN
controlContainer.measure(testSpec.mWidthSpec, testSpec.mHeightSpec);
testContainer.measure(testSpec.mWidthSpec, testSpec.mHeightSpec);
@@ -171,6 +194,7 @@
assertLayoutsEqual("Test Case:" + testSpec, controlContainer, testContainer);
}
+
private static class TestSpec {
private final int mOrientation;
private final int mWidthSpec;
@@ -183,6 +207,8 @@
private final int mSecondChildGravity;
private final int mFirstChildWeight;
private final int mSecondChildWeight;
+ private final int mFirstChildMargin;
+ private final int mSecondChildMargin;
TestSpec(
int orientation,
@@ -195,7 +221,9 @@
int firstChildGravity,
int secondChildGravity,
int firstChildWeight,
- int secondChildWeight) {
+ int secondChildWeight,
+ int firstChildMargin,
+ int secondChildMargin) {
mOrientation = orientation;
mWidthSpec = widthSpec;
mHeightSpec = heightSpec;
@@ -207,6 +235,8 @@
mSecondChildGravity = secondChildGravity;
mFirstChildWeight = firstChildWeight;
mSecondChildWeight = secondChildWeight;
+ mFirstChildMargin = firstChildMargin;
+ mSecondChildMargin = secondChildMargin;
}
@Override
@@ -223,6 +253,8 @@
+ ", mSecondChildGravity=" + mSecondChildGravity
+ ", mFirstChildWeight=" + mFirstChildWeight
+ ", mSecondChildWeight=" + mSecondChildWeight
+ + ", mFirstChildMargin=" + mFirstChildMargin
+ + ", mSecondChildMargin=" + mSecondChildMargin
+ '}';
}
@@ -246,15 +278,13 @@
}
}
- private LinearLayout buildLayout(boolean isNotificationOptimized,
- @LinearLayout.OrientationMode int orientation, List<View> children) {
+ private LinearLayout buildLayout(boolean isNotificationOptimized, List<View> children) {
final LinearLayout linearLayout;
if (isNotificationOptimized) {
linearLayout = new NotificationOptimizedLinearLayout(mContext);
} else {
linearLayout = new LinearLayout(mContext);
}
- linearLayout.setOrientation(orientation);
for (int i = 0; i < children.size(); i++) {
linearLayout.addView(children.get(i));
}
@@ -262,7 +292,8 @@
}
private void assertLayoutsEqual(String testCase, View controlView, View testView) {
- mExpect.withMessage("MeasuredWidths are not equal. Test Case:" + testCase)
+ mExpect.withMessage(
+ "MeasuredWidths are not equal. Test Case:" + testCase)
.that(testView.getMeasuredWidth()).isEqualTo(controlView.getMeasuredWidth());
mExpect.withMessage("MeasuredHeights are not equal. Test Case:" + testCase)
.that(testView.getMeasuredHeight()).isEqualTo(controlView.getMeasuredHeight());
@@ -286,23 +317,12 @@
}
}
- private static class TestView extends View {
- TestView(Context context) {
- super(context);
- }
-
- @Override
- public int getBaseline() {
- return 5;
- }
- }
-
-
private TestSpec createTestSpec(int orientation,
int widthSpec, int heightSpec,
int firstChildLayoutWidth, int firstChildLayoutHeight, int secondChildLayoutWidth,
int secondChildLayoutHeight, int firstChildGravity, int secondChildGravity,
- int firstChildWeight, int secondChildWeight) {
+ int firstChildWeight, int secondChildWeight, int firstChildMargin,
+ int secondChildMargin) {
return new TestSpec(
orientation,
@@ -314,16 +334,16 @@
firstChildGravity,
secondChildGravity,
firstChildWeight,
- secondChildWeight);
+ secondChildWeight,
+ firstChildMargin,
+ secondChildMargin);
}
- private View buildChildView(int childLayoutWidth, int childLayoutHeight,
- int childGravity, int childWeight) {
- final View childView = new TestView(mContext);
- // Set desired size using LayoutParams
- LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(childLayoutWidth,
- childLayoutHeight, childWeight);
- params.gravity = childGravity;
+ private View buildChildView() {
+ final View childView = new TextView(mContext);
+ // this is initial value. We are going to mutate this layout params during the test.
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(MATCH_PARENT,
+ WRAP_CONTENT);
childView.setLayoutParams(params);
return childView;
}
diff --git a/core/tests/devicestatetests/Android.bp b/core/tests/devicestatetests/Android.bp
index 7748de5..60848b3 100644
--- a/core/tests/devicestatetests/Android.bp
+++ b/core/tests/devicestatetests/Android.bp
@@ -29,6 +29,8 @@
"androidx.test.rules",
"frameworks-base-testutils",
"mockito-target-minus-junit4",
+ "platform-test-annotations",
+ "testng",
],
libs: ["android.test.runner"],
platform_apis: true,
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateTest.java b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateTest.java
similarity index 93%
rename from services/tests/servicestests/src/com/android/server/devicestate/DeviceStateTest.java
rename to core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateTest.java
index d54524e..396d403 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateTest.java
+++ b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.server.devicestate;
+package android.hardware.devicestate;
import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
@@ -25,18 +25,17 @@
import android.platform.test.annotations.Presubmit;
-import androidx.test.runner.AndroidJUnit4;
-
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
/**
- * Unit tests for {@link DeviceState}.
+ * Unit tests for {@link android.hardware.devicestate.DeviceState}.
* <p/>
* Run with <code>atest DeviceStateTest</code>.
*/
@Presubmit
-@RunWith(AndroidJUnit4.class)
+@RunWith(JUnit4.class)
public final class DeviceStateTest {
@Test
public void testConstruct() {
diff --git a/core/tests/utiltests/src/com/android/internal/util/FastXmlSerializerTest.java b/core/tests/utiltests/src/com/android/internal/util/FastXmlSerializerTest.java
index 7723d58..f91172d 100644
--- a/core/tests/utiltests/src/com/android/internal/util/FastXmlSerializerTest.java
+++ b/core/tests/utiltests/src/com/android/internal/util/FastXmlSerializerTest.java
@@ -45,7 +45,6 @@
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
-@IgnoreUnderRavenwood(blockedBy = Xml.class)
public class FastXmlSerializerTest {
private static final String TAG = "FastXmlSerializerTest";
@@ -146,6 +145,7 @@
@Test
@LargeTest
+ @IgnoreUnderRavenwood(reason = "Long test runtime")
public void testAllCharacters() throws Exception {
boolean ok = true;
for (int i = 0; i < 0xffff; i++) {
diff --git a/data/etc/com.android.launcher3.xml b/data/etc/com.android.launcher3.xml
index 5616d1d..47e2e38 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 d8713f7a..fdb5208 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -575,6 +575,9 @@
<permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
<!-- Permissions required for CTS test - CtsContactKeysProviderPrivilegedApp -->
<permission name="android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS"/>
+ <!-- Permission required for CTS test BlockedNumberContractTest -->
+ <permission name="android.permission.WRITE_BLOCKED_NUMBERS" />
+ <permission name="android.permission.READ_BLOCKED_NUMBERS" />
<!-- Permission required for CTS test - PackageManagerTest -->
<permission name="android.permission.DOMAIN_VERIFICATION_AGENT"/>
</privapp-permissions>
diff --git a/graphics/java/android/graphics/BitmapFactory.java b/graphics/java/android/graphics/BitmapFactory.java
index 1da8e18..d915b74 100644
--- a/graphics/java/android/graphics/BitmapFactory.java
+++ b/graphics/java/android/graphics/BitmapFactory.java
@@ -25,10 +25,13 @@
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 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 e6de597..9159a00 100644
--- a/graphics/java/android/view/PixelCopy.java
+++ b/graphics/java/android/view/PixelCopy.java
@@ -96,7 +96,7 @@
*
* 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 @@
*
* 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 @@
*
* 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 @@
*
* 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 @@
*
* 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 @@
*
* 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 c360cb8..cfc5980 100644
--- a/keystore/aaid/aidl/android/security/keystore/IKeyAttestationApplicationIdProvider.aidl
+++ b/keystore/aaid/aidl/android/security/keystore/IKeyAttestationApplicationIdProvider.aidl
@@ -20,8 +20,14 @@
/** @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 a12fa5f..310300d 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -19,6 +19,7 @@
// 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 28e7098..a13de9f 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 35c1e8c..b23fd52 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 @@
/** 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 @@
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 @@
}
@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 @@
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 84a616f..4e995bc 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 @@
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 @@
/** 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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
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 @@
if (bbev != null) {
bbev.animate().cancel();
}
+ if (mRunningDragAnimator != null) {
+ mRunningDragAnimator.cancel();
+ mRunningDragAnimator = null;
+ }
}
private @Nullable BubbleBarExpandedView getExpandedView() {
@@ -438,4 +477,35 @@
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 ebb8e3e..eddd43f 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.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 @@
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 @@
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 @@
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 @@
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 @@
// 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 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 5e634a2..7d37d60 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 @@
}
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 7c931df..11e4777 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 @@
* 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 @@
* 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 @@
*/
fun onUnstuckFromTarget(
target: MagneticTarget,
+ draggedObject: MagnetizedObject<*>,
velX: Float,
velY: Float,
wasFlungOut: Boolean
@@ -129,8 +132,9 @@
* 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 @@
// 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 @@
// 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 @@
// 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 @@
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/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index ead5ad2..bd9d89c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -64,6 +64,7 @@
import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler;
import com.android.wm.shell.draganddrop.DragAndDropController;
+import com.android.wm.shell.draganddrop.UnhandledDragController;
import com.android.wm.shell.freeform.FreeformComponents;
import com.android.wm.shell.freeform.FreeformTaskListener;
import com.android.wm.shell.freeform.FreeformTaskTransitionHandler;
@@ -558,6 +559,14 @@
@WMSingleton
@Provides
+ static UnhandledDragController provideUnhandledDragController(
+ IWindowManager wmService,
+ @ShellMainThread ShellExecutor mainExecutor) {
+ return new UnhandledDragController(wmService, mainExecutor);
+ }
+
+ @WMSingleton
+ @Provides
static DragAndDropController provideDragAndDropController(Context context,
ShellInit shellInit,
ShellController shellController,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index e872849..42c8d74 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.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 @@
)
}
+ 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 @@
/** 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 @@
* Move a task to desktop
*/
fun moveToDesktop(
- decor: DesktopModeWindowDecoration,
task: RunningTaskInfo,
wct: WindowContainerTransaction = WindowContainerTransaction()
) {
@@ -298,7 +302,7 @@
addMoveToDesktopChanges(wct, task)
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- enterDesktopTaskTransitionHandler.moveToDesktop(wct, decor)
+ enterDesktopTaskTransitionHandler.moveToDesktop(wct)
} else {
shellTaskOrganizer.applyTransaction(wct)
}
@@ -311,7 +315,6 @@
fun startDragToDesktop(
taskInfo: RunningTaskInfo,
dragToDesktopValueAnimator: MoveToDesktopAnimator,
- windowDecor: DesktopModeWindowDecoration
) {
KtProtoLog.v(
WM_SHELL_DESKTOP_MODE,
@@ -320,8 +323,7 @@
)
dragToDesktopTransitionHandler.startDragToDesktopTransition(
taskInfo.taskId,
- dragToDesktopValueAnimator,
- windowDecor
+ dragToDesktopValueAnimator
)
}
@@ -522,7 +524,7 @@
}
/** 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 @@
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 @@
*
* @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 @@
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 39610e3..af26e29 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_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 @@
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 @@
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 @@
fun startDragToDesktopTransition(
taskId: Int,
dragToDesktopAnimator: MoveToDesktopAnimator,
- windowDecoration: DesktopModeWindowDecoration
) {
if (inProgress) {
KtProtoLog.v(
@@ -128,14 +132,12 @@
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 @@
// 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 @@
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 @@
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 @@
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 @@
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 605600f..07cf202 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.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 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 @@
mTransactionSupplier = supplier;
}
+ void setOnTaskResizeAnimationListener(OnTaskResizeAnimationListener listener) {
+ mOnTaskResizeAnimationListener = listener;
+ }
+
/**
* Starts Transition of type TRANSIT_MOVE_TO_DESKTOP
* @param wct WindowContainerTransaction for transition
- * @param decor {@link DesktopModeWindowDecoration} of task being animated
*/
- public void moveToDesktop(@NonNull WindowContainerTransaction wct,
- DesktopModeWindowDecoration decor) {
- mDesktopModeWindowDecoration = decor;
+ public void moveToDesktop(@NonNull WindowContainerTransaction wct) {
final IBinder token = mTransitions.startTransition(TRANSIT_MOVE_TO_DESKTOP, wct, this);
mPendingTransitionTokens.add(token);
}
@@ -136,33 +137,33 @@
@NonNull TransitionInfo.Change change,
@NonNull SurfaceControl.Transaction startT,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
- if (mDesktopModeWindowDecoration == null) {
- Slog.e(TAG, "Window Decoration is not available for this transition");
+ final SurfaceControl leash = change.getLeash();
+ final Rect startBounds = change.getStartAbsBounds();
+ final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ if (mOnTaskResizeAnimationListener == null) {
+ Slog.e(TAG, "onTaskResizeAnimationListener is not available for this transition");
return false;
}
- final SurfaceControl leash = change.getLeash();
- final Rect startBounds = change.getStartAbsBounds();
- startT.setPosition(leash, startBounds.left, startBounds.right)
+ startT.setPosition(leash, startBounds.left, startBounds.top)
.setWindowCrop(leash, startBounds.width(), startBounds.height())
.show(leash);
- mDesktopModeWindowDecoration.showResizeVeil(startT, startBounds);
-
+ mOnTaskResizeAnimationListener.onAnimationStart(taskInfo.taskId, startT, startBounds);
final ValueAnimator animator = ValueAnimator.ofObject(new RectEvaluator(),
change.getStartAbsBounds(), change.getEndAbsBounds());
animator.setDuration(FREEFORM_ANIMATION_DURATION);
SurfaceControl.Transaction t = mTransactionSupplier.get();
animator.addUpdateListener(animation -> {
final Rect animationValue = (Rect) animator.getAnimatedValue();
- t.setPosition(leash, animationValue.left, animationValue.right)
+ t.setPosition(leash, animationValue.left, animationValue.top)
.setWindowCrop(leash, animationValue.width(), animationValue.height())
.show(leash);
- mDesktopModeWindowDecoration.updateResizeVeil(t, animationValue);
+ mOnTaskResizeAnimationListener.onBoundsChange(taskInfo.taskId, t, animationValue);
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- mDesktopModeWindowDecoration.hideResizeVeil();
+ mOnTaskResizeAnimationListener.onAnimationEnd(taskInfo.taskId);
mTransitions.getMainExecutor().execute(
() -> finishCallback.onTransitionFinished(null));
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
index 0218493..c469e65 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.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 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 @@
) : 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 @@
) : 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 @@
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 @@
)
.setWindowCrop(leash, startBounds.width(), startBounds.height())
.show(leash)
- windowDecor.showResizeVeil(startTransaction, startBounds)
+ onTaskResizeAnimationListener.onAnimationStart(
+ taskId,
+ startTransaction,
+ startBounds
+ )
},
onEnd = {
finishTransaction
@@ -101,7 +100,7 @@
)
.setWindowCrop(leash, endBounds.width(), endBounds.height())
.show(leash)
- windowDecor.hideResizeVeil()
+ onTaskResizeAnimationListener.onAnimationEnd(taskId)
finishCallback.onTransitionFinished(null)
boundsAnimator = null
}
@@ -111,7 +110,7 @@
tx.setPosition(leash, rect.left.toFloat(), rect.top.toFloat())
.setWindowCrop(leash, rect.width(), rect.height())
.show(leash)
- windowDecor.updateResizeVeil(tx, rect)
+ onTaskResizeAnimationListener.onBoundsChange(taskId, tx, rect)
}
start()
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/UnhandledDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/UnhandledDragController.kt
new file mode 100644
index 0000000..ccf48d0
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/UnhandledDragController.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.draganddrop
+
+import android.os.RemoteException
+import android.util.Log
+import android.view.DragEvent
+import android.view.IWindowManager
+import android.window.IUnhandledDragCallback
+import android.window.IUnhandledDragListener
+import androidx.annotation.VisibleForTesting
+import com.android.internal.protolog.common.ProtoLog
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.protolog.ShellProtoLogGroup
+import java.util.function.Consumer
+
+/**
+ * Manages the listener and callbacks for unhandled global drags.
+ */
+class UnhandledDragController(
+ val wmService: IWindowManager,
+ mainExecutor: ShellExecutor
+) {
+ private var callback: UnhandledDragAndDropCallback? = null
+
+ private val unhandledDragListener: IUnhandledDragListener =
+ object : IUnhandledDragListener.Stub() {
+ override fun onUnhandledDrop(event: DragEvent, callback: IUnhandledDragCallback) {
+ mainExecutor.execute() {
+ this@UnhandledDragController.onUnhandledDrop(event, callback)
+ }
+ }
+ }
+
+ /**
+ * Listener called when an unhandled drag is started.
+ */
+ interface UnhandledDragAndDropCallback {
+ /**
+ * Called when a global drag is unhandled (ie. dropped outside of all visible windows, or
+ * dropped on a window that does not want to handle it).
+ *
+ * The implementer _must_ call onFinishedCallback, and if it consumes the drop, then it is
+ * also responsible for releasing up the drag surface provided via the drag event.
+ */
+ fun onUnhandledDrop(dragEvent: DragEvent, onFinishedCallback: Consumer<Boolean>) {}
+ }
+
+ /**
+ * Sets a listener for callbacks when an unhandled drag happens.
+ */
+ fun setListener(listener: UnhandledDragAndDropCallback?) {
+ val updateWm = (callback == null && listener != null)
+ || (callback != null && listener == null)
+ callback = listener
+ if (updateWm) {
+ try {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+ "%s unhandled drag listener",
+ if (callback != null) "Registering" else "Unregistering")
+ wmService.setUnhandledDragListener(
+ if (callback != null) unhandledDragListener else null)
+ } catch (e: RemoteException) {
+ Log.e(TAG, "Failed to set unhandled drag listener")
+ }
+ }
+ }
+
+ @VisibleForTesting
+ fun onUnhandledDrop(dragEvent: DragEvent, wmCallback: IUnhandledDragCallback) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+ "onUnhandledDrop: %s", dragEvent)
+ if (callback == null) {
+ wmCallback.notifyUnhandledDropComplete(false)
+ }
+
+ callback?.onUnhandledDrop(dragEvent) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+ "Notifying onUnhandledDrop complete: %b", it)
+ wmCallback.notifyUnhandledDropComplete(it)
+ }
+ }
+
+ companion object {
+ private val TAG = UnhandledDragController::class.java.simpleName
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index a80241e..f2bdcae 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 @@
@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 4e75847..f929389 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 @@
});
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 @@
@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 @@
}
@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 97d3457..dffcc6d 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 @@
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 @@
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/taskview/TaskViewTaskController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java
index 93d7636..196e04e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java
@@ -480,7 +480,7 @@
WindowContainerTransaction wct = new WindowContainerTransaction();
if (mCaptionInsets != null) {
wct.addInsetsSource(mTaskToken, mCaptionInsetsOwner, 0,
- WindowInsets.Type.captionBar(), mCaptionInsets);
+ WindowInsets.Type.captionBar(), mCaptionInsets, null /* boundingRects */);
} else {
wct.removeInsetsSource(mTaskToken, mCaptionInsetsOwner, 0,
WindowInsets.Type.captionBar());
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 891eea0..7db3d38 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 @@
mShellCommandHandler.addDumpCallback(this::dump, this);
mDisplayInsetsController.addInsetsChangedListener(mContext.getDisplayId(),
new DesktopModeOnInsetsChangedListener());
+ mDesktopTasksController.ifPresent(c -> c.setOnTaskResizeAnimationListener(
+ new DeskopModeOnTaskResizeAnimationListener()));
}
@Override
@@ -356,7 +358,7 @@
// 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 @@
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 @@
}
mDesktopTasksController.ifPresent(c -> {
final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
- c.toggleDesktopTaskSize(decoration.mTaskInfo, decoration);
+ c.toggleDesktopTaskSize(decoration.mTaskInfo);
});
return true;
}
@@ -761,7 +761,7 @@
relevantDecor.mTaskInfo, relevantDecor.mTaskSurface);
mDesktopTasksController.ifPresent(
c -> c.startDragToDesktop(relevantDecor.mTaskInfo,
- mMoveToDesktopAnimator, relevantDecor));
+ mMoveToDesktopAnimator));
}
}
if (mMoveToDesktopAnimator != null) {
@@ -1020,6 +1020,34 @@
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 3f0a281..185365b 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 @@
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 0000000..09c62bf
--- /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 afe837e..dc65855 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 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,15 +296,39 @@
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);
+ mOwner, 0 /* index */, WindowInsets.Type.captionBar(), mCaptionInsetsRect,
+ boundingRects);
wct.addInsetsSource(mTaskInfo.token,
mOwner, 0 /* index */, WindowInsets.Type.mandatorySystemGestures(),
- mCaptionInsetsRect);
+ mCaptionInsetsRect, null /* boundingRects */);
} else {
wct.removeInsetsSource(mTaskInfo.token, mOwner, 0 /* index */,
WindowInsets.Type.captionBar());
@@ -377,6 +404,20 @@
}
}
+ 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.
*/
@@ -546,7 +587,7 @@
final int captionHeight = loadDimensionPixelSize(mContext.getResources(), captionHeightId);
final Rect captionInsets = new Rect(0, 0, 0, captionHeight);
wct.addInsetsSource(mTaskInfo.token, mOwner, 0 /* index */, WindowInsets.Type.captionBar(),
- captionInsets);
+ captionInsets, null /* boundingRects */);
}
static class RelayoutParams {
@@ -554,8 +595,9 @@
int mLayoutResId;
int mCaptionHeightId;
int mCaptionWidthId;
- int mShadowRadiusId;
+ final List<OccludingCaptionElement> mOccludingCaptionElements = new ArrayList<>();
+ int mShadowRadiusId;
int mCornerRadius;
Configuration mWindowDecorConfig;
@@ -567,14 +609,28 @@
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 144373f..2309c54 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.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 @@
@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 @@
@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 @@
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 aadadd6..8c47116 100644
--- a/libs/WindowManager/Shell/tests/unittest/Android.bp
+++ b/libs/WindowManager/Shell/tests/unittest/Android.bp
@@ -19,6 +19,7 @@
// 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 91503b1..7e26577 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 @@
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.BackProgressAnimator;
import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Before;
import org.junit.Test;
@@ -102,6 +104,36 @@
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 a9f054e..a4fb350 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 @@
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 @@
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 @@
// 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
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 @@
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 @@
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 @@
)
)
- verify(magnetListener).onReleasedInTarget(magneticTarget)
+ verify(magnetListener).onReleasedInTarget(magneticTarget, magnetizedObject)
verifyNoMoreInteractions(magnetListener)
}
@@ -437,14 +444,15 @@
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 @@
)
// 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 9249b0a..79634e6 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 @@
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 @@
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 @@
@Test
fun moveToDesktop_nonExistentTask_doesNothing() {
- controller.moveToDesktop(desktopModeWindowDecoration, 999)
+ controller.moveToDesktop(999)
verifyWCTNotExecuted()
}
@@ -332,7 +332,7 @@
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 @@
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 @@
@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 @@
@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 @@
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 be639e8..98e90d6 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_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.verify
import org.mockito.kotlin.verifyZeroInteractions
import org.mockito.kotlin.whenever
+import java.util.function.Supplier
/** Tests of [DragToDesktopTransitionHandler]. */
@SmallTest
@@ -246,7 +246,7 @@
)
)
.thenReturn(token)
- handler.startDragToDesktopTransition(task.taskId, dragAnimator, mock())
+ handler.startDragToDesktopTransition(task.taskId, dragAnimator)
return token
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/UnhandledDragControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/UnhandledDragControllerTest.kt
new file mode 100644
index 0000000..522f052
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/UnhandledDragControllerTest.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.draganddrop
+
+import android.os.RemoteException
+import android.view.DragEvent
+import android.view.DragEvent.ACTION_DROP
+import android.view.IWindowManager
+import android.view.SurfaceControl
+import android.window.IUnhandledDragCallback
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.draganddrop.UnhandledDragController.UnhandledDragAndDropCallback
+import java.util.function.Consumer
+import junit.framework.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/**
+ * Tests for the unhandled drag controller.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class UnhandledDragControllerTest : ShellTestCase() {
+ @Mock
+ private lateinit var mIWindowManager: IWindowManager
+
+ @Mock
+ private lateinit var mMainExecutor: ShellExecutor
+
+ private lateinit var mController: UnhandledDragController
+
+ @Before
+ @Throws(RemoteException::class)
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ mController = UnhandledDragController(mIWindowManager, mMainExecutor)
+ }
+
+ @Test
+ fun setListener_registersUnregistersWithWM() {
+ mController.setListener(object : UnhandledDragAndDropCallback {})
+ mController.setListener(object : UnhandledDragAndDropCallback {})
+ mController.setListener(object : UnhandledDragAndDropCallback {})
+ verify(mIWindowManager, Mockito.times(1))
+ .setUnhandledDragListener(ArgumentMatchers.any())
+
+ reset(mIWindowManager)
+ mController.setListener(null)
+ mController.setListener(null)
+ mController.setListener(null)
+ verify(mIWindowManager, Mockito.times(1))
+ .setUnhandledDragListener(ArgumentMatchers.isNull())
+ }
+
+ @Test
+ fun onUnhandledDrop_noListener_expectNotifyUnhandled() {
+ // Simulate an unhandled drop
+ val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, null, null, null,
+ null, null, false)
+ val wmCallback = mock(IUnhandledDragCallback::class.java)
+ mController.onUnhandledDrop(dropEvent, wmCallback)
+
+ verify(wmCallback).notifyUnhandledDropComplete(ArgumentMatchers.eq(false))
+ }
+
+ @Test
+ fun onUnhandledDrop_withListener_expectNotifyHandled() {
+ val lastDragEvent = arrayOfNulls<DragEvent>(1)
+
+ // Set a listener to listen for unhandled drops
+ mController.setListener(object : UnhandledDragAndDropCallback {
+ override fun onUnhandledDrop(dragEvent: DragEvent,
+ onFinishedCallback: Consumer<Boolean>) {
+ lastDragEvent[0] = dragEvent
+ onFinishedCallback.accept(true)
+ dragEvent.dragSurface.release()
+ }
+ })
+
+ // Simulate an unhandled drop
+ val dragSurface = mock(SurfaceControl::class.java)
+ val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, null, null, null,
+ dragSurface, null, false)
+ val wmCallback = mock(IUnhandledDragCallback::class.java)
+ mController.onUnhandledDrop(dropEvent, wmCallback)
+
+ verify(wmCallback).notifyUnhandledDropComplete(ArgumentMatchers.eq(true))
+ verify(dragSurface).release()
+ assertEquals(lastDragEvent.get(0), dropEvent)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
new file mode 100644
index 0000000..71eea4b
--- /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/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 7b53f70..228b25c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -259,7 +259,8 @@
any(),
eq(0 /* index */),
eq(WindowInsets.Type.captionBar()),
- eq(new Rect(100, 300, 400, 364)));
+ eq(new Rect(100, 300, 400, 364)),
+ any());
}
verify(mMockSurfaceControlStartT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS);
@@ -569,9 +570,9 @@
windowDecor.relayout(taskInfo);
verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(),
- eq(0) /* index */, eq(captionBar()), any());
+ eq(0) /* index */, eq(captionBar()), any(), any());
verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(),
- eq(0) /* index */, eq(mandatorySystemGestures()), any());
+ eq(0) /* index */, eq(mandatorySystemGestures()), any(), any());
}
@Test
diff --git a/media/java/android/media/AudioDevicePort.java b/media/java/android/media/AudioDevicePort.java
index 2de8eef..ab5c54b 100644
--- a/media/java/android/media/AudioDevicePort.java
+++ b/media/java/android/media/AudioDevicePort.java
@@ -33,7 +33,7 @@
* 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 4cdfc51..2b6f72e 100644
--- a/media/java/android/media/AudioHalVersionInfo.java
+++ b/media/java/android/media/AudioHalVersionInfo.java
@@ -79,6 +79,9 @@
/**
* 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 49890c1..8cc42e0 100644
--- a/media/java/android/media/OWNERS
+++ b/media/java/android/media/OWNERS
@@ -11,3 +11,8 @@
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 f1c5c9d..9b3477f 100644
--- a/media/java/android/media/metrics/EditingEndedEvent.java
+++ b/media/java/android/media/metrics/EditingEndedEvent.java
@@ -18,6 +18,7 @@
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 @@
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 @@
/** 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 @@
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 @@
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 @@
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 @@
+ "finalState = "
+ mFinalState
+ ", "
+ + "finalProgressPercent = "
+ + mFinalProgressPercent
+ + ", "
+ "errorCode = "
+ mErrorCode
+ ", "
+ "timeSinceCreatedMillis = "
+ mTimeSinceCreatedMillis
+ ", "
+ + "exporterName = "
+ + mExporterName
+ + ", "
+ + "muxerName = "
+ + mMuxerName
+ + ", "
+ "inputMediaItemInfos = "
+ mInputMediaItemInfos
+ ", "
@@ -307,29 +356,38 @@
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 @@
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 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 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 @@
}
/**
+ * 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 @@
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 @NonNull EditingEndedEvent build() {
return new EditingEndedEvent(
mFinalState,
+ mFinalProgressPercent,
mErrorCode,
mTimeSinceCreatedMillis,
+ mExporterName,
+ mMuxerName,
mInputMediaItemInfos,
mOutputMediaItemInfo,
mOperationTypes,
diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java
index e3290d6..2a0648d 100644
--- a/media/java/android/media/projection/MediaProjectionManager.java
+++ b/media/java/android/media/projection/MediaProjectionManager.java
@@ -180,7 +180,7 @@
@SuppressLint("UnflaggedApi")
@TestApi
@NonNull
- public Intent createScreenCaptureIntent(@Nullable LaunchCookie launchCookie) {
+ public Intent createScreenCaptureIntent(@NonNull LaunchCookie launchCookie) {
Intent i = createScreenCaptureIntent();
i.putExtra(EXTRA_LAUNCH_COOKIE, launchCookie);
return i;
diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java
index 59b10c6..76664a6 100644
--- a/media/java/android/media/tv/ad/TvAdManager.java
+++ b/media/java/android/media/tv/ad/TvAdManager.java
@@ -18,6 +18,7 @@
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringDef;
@@ -59,7 +60,7 @@
*/
@FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
@SystemService(Context.TV_AD_SERVICE)
-public class TvAdManager {
+public final class TvAdManager {
// TODO: implement more methods and unhide APIs.
private static final String TAG = "TvAdManager";
@@ -237,6 +238,76 @@
*/
public static final String SESSION_DATA_KEY_REQUEST_ID = "request_id";
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false, prefix = "SESSION_STATE_", value = {
+ SESSION_STATE_STOPPED,
+ SESSION_STATE_RUNNING,
+ SESSION_STATE_ERROR})
+ public @interface SessionState {}
+
+ /**
+ * Stopped (or not started) state of AD service session.
+ */
+ public static final int SESSION_STATE_STOPPED = 1;
+ /**
+ * Running state of AD service session.
+ */
+ public static final int SESSION_STATE_RUNNING = 2;
+ /**
+ * Error state of AD service session.
+ */
+ public static final int SESSION_STATE_ERROR = 3;
+
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false, prefix = "ERROR_", value = {
+ ERROR_NONE,
+ ERROR_UNKNOWN,
+ ERROR_NOT_SUPPORTED,
+ ERROR_WEAK_SIGNAL,
+ ERROR_RESOURCE_UNAVAILABLE,
+ ERROR_BLOCKED,
+ ERROR_ENCRYPTED,
+ ERROR_UNKNOWN_CHANNEL,
+ })
+ public @interface ErrorCode {}
+
+ /**
+ * No error.
+ */
+ public static final int ERROR_NONE = 0;
+ /**
+ * Unknown error code.
+ */
+ public static final int ERROR_UNKNOWN = 1;
+ /**
+ * Error code for an unsupported channel.
+ */
+ public static final int ERROR_NOT_SUPPORTED = 2;
+ /**
+ * Error code for weak signal.
+ */
+ public static final int ERROR_WEAK_SIGNAL = 3;
+ /**
+ * Error code when resource (e.g. tuner) is unavailable.
+ */
+ public static final int ERROR_RESOURCE_UNAVAILABLE = 4;
+ /**
+ * Error code for blocked contents.
+ */
+ public static final int ERROR_BLOCKED = 5;
+ /**
+ * Error code when the key or module is missing for the encrypted channel.
+ */
+ public static final int ERROR_ENCRYPTED = 6;
+ /**
+ * Error code when the current channel is an unknown channel.
+ */
+ public static final int ERROR_UNKNOWN_CHANNEL = 7;
+
private final ITvAdManager mService;
private final int mUserId;
diff --git a/media/java/android/media/tv/ad/TvAdService.java b/media/java/android/media/tv/ad/TvAdService.java
index 6c8a8fd..2bba0f3 100644
--- a/media/java/android/media/tv/ad/TvAdService.java
+++ b/media/java/android/media/tv/ad/TvAdService.java
@@ -280,7 +280,6 @@
/**
* Requests the bounds of the current video.
- * @hide
*/
@CallSuper
public void requestCurrentVideoBounds() {
@@ -304,7 +303,6 @@
/**
* Requests the URI of the current channel.
- * @hide
*/
@CallSuper
public void requestCurrentChannelUri() {
@@ -328,7 +326,6 @@
/**
* Requests the list of {@link TvTrackInfo}.
- * @hide
*/
@CallSuper
public void requestTrackInfoList() {
@@ -354,7 +351,6 @@
* Requests current TV input ID.
*
* @see android.media.tv.TvInputInfo
- * @hide
*/
@CallSuper
public void requestCurrentTvInputId() {
@@ -393,7 +389,6 @@
* @param data the original bytes to be signed.
*
* @see #onSigningResult(String, byte[])
- * @hide
*/
@CallSuper
public void requestSigning(@NonNull String signingId, @NonNull String algorithm,
@@ -535,28 +530,24 @@
* Receives current video bounds.
*
* @param bounds the rectangle area for rendering the current video.
- * @hide
*/
public void onCurrentVideoBounds(@NonNull Rect bounds) {
}
/**
* Receives current channel URI.
- * @hide
*/
public void onCurrentChannelUri(@Nullable Uri channelUri) {
}
/**
* Receives track list.
- * @hide
*/
public void onTrackInfoList(@NonNull List<TvTrackInfo> tracks) {
}
/**
* Receives current TV input ID.
- * @hide
*/
public void onCurrentTvInputId(@Nullable String inputId) {
}
@@ -569,7 +560,6 @@
* @param result the signed result.
*
* @see #requestSigning(String, String, String, byte[])
- * @hide
*/
public void onSigningResult(@NonNull String signingId, @NonNull byte[] result) {
}
@@ -584,7 +574,6 @@
* "onRequestSigning" can also be added to the params.
*
* @see TvAdView#ERROR_KEY_METHOD_NAME
- * @hide
*/
public void onError(@NonNull String errMsg, @NonNull Bundle params) {
}
@@ -601,7 +590,6 @@
* {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}.
* See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on
* how to parse this data.
- * @hide
*/
public void onTvMessage(@TvInputManager.TvMessageType int type,
@NonNull Bundle data) {
@@ -671,6 +659,30 @@
}
/**
+ * Notifies when the session state is changed.
+ *
+ * @param state the current session state.
+ * @param err the error code for error state. {@link TvAdManager#ERROR_NONE} is
+ * used when the state is not {@link TvAdManager#SESSION_STATE_ERROR}.
+ */
+ @CallSuper
+ public void notifySessionStateChanged(
+ @TvAdManager.SessionState int state,
+ @TvAdManager.ErrorCode int err) {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ if (DEBUG) {
+ Log.d(TAG, "notifySessionStateChanged (state="
+ + state + "; err=" + err + ")");
+ }
+ // TODO: handle session callback
+ }
+ });
+ }
+
+ /**
* Takes care of dispatching incoming input events and tells whether the event was handled.
*/
int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java
index ee01468..2fac8ce 100644
--- a/media/java/android/media/tv/ad/TvAdView.java
+++ b/media/java/android/media/tv/ad/TvAdView.java
@@ -61,10 +61,19 @@
* The name of the method where the error happened, if applicable. For example, if there is an
* error during signing, the request name is "onRequestSigning".
* @see #notifyError(String, Bundle)
- * @hide
*/
public static final String ERROR_KEY_METHOD_NAME = "method_name";
+ /**
+ * The error code of an error.
+ *
+ * <p>It can be {@link TvAdManager#ERROR_WEAK_SIGNAL},
+ * {@link TvAdManager#ERROR_RESOURCE_UNAVAILABLE}, etc.
+ *
+ * @see #notifyError(String, Bundle)
+ */
+ public static final String ERROR_KEY_ERROR_CODE = "error_code";
+
private final TvAdManager mTvAdManager;
private final Handler mHandler = new Handler();
@@ -486,7 +495,6 @@
* Sends current video bounds to related TV AD service.
*
* @param bounds the rectangle area for rendering the current video.
- * @hide
*/
public void sendCurrentVideoBounds(@NonNull Rect bounds) {
if (DEBUG) {
@@ -502,7 +510,6 @@
*
* @param channelUri The current channel URI; {@code null} if there is no currently tuned
* channel.
- * @hide
*/
public void sendCurrentChannelUri(@Nullable Uri channelUri) {
if (DEBUG) {
@@ -515,7 +522,6 @@
/**
* Sends track info list to related TV AD service.
- * @hide
*/
public void sendTrackInfoList(@Nullable List<TvTrackInfo> tracks) {
if (DEBUG) {
@@ -532,7 +538,6 @@
* @param inputId The current TV input ID whose channel is tuned. {@code null} if no channel is
* tuned.
* @see android.media.tv.TvInputInfo
- * @hide
*/
public void sendCurrentTvInputId(@Nullable String inputId) {
if (DEBUG) {
@@ -553,7 +558,6 @@
* @param signingId the ID to identify the request. It's the same as the corresponding ID in
* {@link TvAdService.Session#requestSigning(String, String, String, byte[])}
* @param result the signed result.
- * @hide
*/
public void sendSigningResult(@NonNull String signingId, @NonNull byte[] result) {
if (DEBUG) {
@@ -574,7 +578,7 @@
* can also be added to the params.
*
* @see #ERROR_KEY_METHOD_NAME
- * @hide
+ * @see #ERROR_KEY_ERROR_CODE
*/
public void notifyError(@NonNull String errMsg, @NonNull Bundle params) {
if (DEBUG) {
@@ -597,7 +601,6 @@
* {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}.
* See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on
* how to parse this data.
- * @hide
*/
public void notifyTvMessage(@NonNull @TvInputManager.TvMessageType int type,
@NonNull Bundle data) {
@@ -633,7 +636,6 @@
* @param callback the callback to receive events. MUST NOT be {@code null}.
*
* @see #clearCallback()
- * @hide
*/
public void setCallback(
@NonNull @CallbackExecutor Executor executor,
@@ -649,7 +651,6 @@
* Clears the callback.
*
* @see #setCallback(Executor, TvAdCallback)
- * @hide
*/
public void clearCallback() {
synchronized (mCallbackLock) {
@@ -845,7 +846,6 @@
/**
* Callback used to receive various status updates on the {@link TvAdView}.
- * @hide
*/
public abstract static class TvAdCallback {
@@ -898,5 +898,20 @@
public void onRequestSigning(@NonNull String serviceId, @NonNull String signingId,
@NonNull String algorithm, @NonNull String alias, @NonNull byte[] data) {
}
+
+ /**
+ * This is called when the state of corresponding AD service is changed.
+ *
+ * @param serviceId The ID of the AD service bound to this view.
+ * @param state the current state.
+ * @param err the error code for error state. {@link TvAdManager#ERROR_NONE}
+ * is used when the state is not
+ * {@link TvAdManager#SESSION_STATE_ERROR}.
+ */
+ public void onStateChanged(
+ @NonNull String serviceId,
+ @TvAdManager.SessionState int state,
+ @TvAdManager.ErrorCode int err) {
+ }
}
}
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index eba26d4..f332f81 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -873,6 +873,9 @@
/**
* Called when the corresponding TV input selected to a track.
+ *
+ * If the track is deselected and no track is currently selected,
+ * trackId is an empty string.
*/
public void onTrackSelected(@TvTrackInfo.Type int type, @NonNull String trackId) {
}
@@ -1845,6 +1848,10 @@
if (DEBUG) {
Log.d(TAG, "notifyTrackSelected (type=" + type + "trackId=" + trackId + ")");
}
+ // TvInputService accepts a Null String, but onTrackSelected expects NonNull.
+ if (trackId == null) {
+ trackId = "";
+ }
onTrackSelected(type, trackId);
}
diff --git a/packages/CredentialManager/wear/res/values/strings.xml b/packages/CredentialManager/wear/res/values/strings.xml
index be7e448..4a92936 100644
--- a/packages/CredentialManager/wear/res/values/strings.xml
+++ b/packages/CredentialManager/wear/res/values/strings.xml
@@ -33,4 +33,6 @@
<string name="dialog_continue_button">Continue</string>
<!-- Content description for the sign in options button of a screen. [CHAR LIMIT=NONE] -->
<string name="dialog_sign_in_options_button">Sign-in Options</string>
+ <!-- Title for multiple credentials flattened screen. [CHAR LIMIT=NONE] -->
+ <string name="choose_sign_in_title">Choose a sign in</string>
</resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/AccountRow.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/AccountRow.kt
index b2812d3..8b19e1b 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/AccountRow.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/AccountRow.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * 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.
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 5590219..7cd6bb3 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 @@
{
Text(
text = label,
- modifier = Modifier.fillMaxWidth(),
- textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = if (secondaryLabel != null) 1 else 2,
)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/UiState.kt
similarity index 60%
copy from packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneModel.kt
copy to packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/UiState.kt
index f3d549f..98a9e93 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneModel.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/UiState.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.
@@ -14,14 +14,16 @@
* limitations under the License.
*/
-package com.android.systemui.scene.shared.model
+package com.android.credentialmanager.ui.screens
-/** Models a scene. */
-data class SceneModel(
+import androidx.activity.result.IntentSenderRequest
- /** The key of the scene. */
- val key: SceneKey,
+sealed class UiState {
+ data object CredentialScreen : UiState()
- /** An optional name for the transition that led to this scene being the current scene. */
- val transitionName: String? = null,
-)
+ data class CredentialSelected(
+ val intentSenderRequest: IntentSenderRequest?
+ ) : UiState()
+
+ data object Cancel : UiState()
+}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt
new file mode 100644
index 0000000..6ba2011
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt
@@ -0,0 +1,140 @@
+/*
+ * 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 com.android.credentialmanager.ui.screens.multiple
+
+import com.android.credentialmanager.ui.screens.UiState
+import android.graphics.drawable.Drawable
+import androidx.activity.compose.rememberLauncherForActivityResult
+import com.android.credentialmanager.R
+import androidx.compose.ui.res.stringResource
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.rememberNavController
+import com.android.credentialmanager.CredentialSelectorUiState
+import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract
+import com.android.credentialmanager.model.get.CredentialEntryInfo
+import com.android.credentialmanager.ui.components.DismissChip
+import com.android.credentialmanager.ui.components.CredentialsScreenChip
+import com.android.credentialmanager.ui.components.SignInHeader
+import com.android.credentialmanager.ui.components.SignInOptionsChip
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+import com.google.android.horologist.compose.layout.ScalingLazyColumn
+import com.google.android.horologist.compose.layout.ScalingLazyColumnState
+
+/**
+ * Screen that shows multiple credentials to select from.
+ *
+ * @param credentialSelectorUiState The app bar view model.
+ * @param screenIcon The view model corresponding to the home page.
+ * @param columnState ScalingLazyColumn configuration to be be applied
+ * @param modifier styling for composable
+ * @param viewModel ViewModel that updates ui state for this screen
+ * @param navController handles navigation events from this screen
+ */
+@OptIn(ExperimentalHorologistApi::class)
+@Composable
+fun MultiCredentialsFoldScreen(
+ credentialSelectorUiState: CredentialSelectorUiState.Get.MultipleEntry,
+ screenIcon: Drawable?,
+ columnState: ScalingLazyColumnState,
+ modifier: Modifier = Modifier,
+ viewModel: MultiCredentialsFoldViewModel = hiltViewModel(),
+ navController: NavHostController = rememberNavController(),
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ when (val state = uiState) {
+ UiState.CredentialScreen -> {
+ MultiCredentialsFoldScreen(
+ state = credentialSelectorUiState,
+ onSignInOptionsClicked = viewModel::onSignInOptionsClicked,
+ onCredentialClicked = viewModel::onCredentialClicked,
+ onCancelClicked = viewModel::onCancelClicked,
+ screenIcon = screenIcon,
+ columnState = columnState,
+ modifier = modifier
+ )
+ }
+
+ is UiState.CredentialSelected -> {
+ val launcher = rememberLauncherForActivityResult(
+ StartBalIntentSenderForResultContract()
+ ) {
+ viewModel.onInfoRetrieved(it.resultCode, null)
+ }
+
+ SideEffect {
+ state.intentSenderRequest?.let {
+ launcher.launch(it)
+ }
+ }
+ }
+
+ UiState.Cancel -> {
+ navController.popBackStack()
+ }
+ }
+}
+
+@OptIn(ExperimentalHorologistApi::class)
+@Composable
+fun MultiCredentialsFoldScreen(
+ state: CredentialSelectorUiState.Get.MultipleEntry,
+ onSignInOptionsClicked: () -> Unit,
+ onCredentialClicked: (entryInfo: CredentialEntryInfo) -> Unit,
+ onCancelClicked: () -> Unit,
+ screenIcon: Drawable?,
+ columnState: ScalingLazyColumnState,
+ modifier: Modifier,
+) {
+ ScalingLazyColumn(
+ columnState = columnState,
+ modifier = modifier.fillMaxSize(),
+ ) {
+ item {
+ SignInHeader(
+ icon = screenIcon,
+ title = stringResource(R.string.choose_sign_in_title),
+ modifier = Modifier
+ .padding(top = 6.dp),
+ )
+ }
+
+ state.accounts.forEach {
+ it.sortedCredentialEntryList.forEach { credential: CredentialEntryInfo ->
+ item {
+ CredentialsScreenChip(
+ label = credential.userName,
+ onClick = { onCredentialClicked(credential) },
+ secondaryLabel = credential.credentialTypeDisplayName,
+ icon = credential.icon,
+ )
+ }
+ }
+ }
+ item { SignInOptionsChip(onSignInOptionsClicked) }
+ item { DismissChip(onCancelClicked) }
+ }
+}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldViewModel.kt
new file mode 100644
index 0000000..627a63d
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldViewModel.kt
@@ -0,0 +1,75 @@
+/*
+ * 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 com.android.credentialmanager.ui.screens.multiple
+
+import android.content.Intent
+import android.credentials.selection.ProviderPendingIntentResponse
+import android.credentials.selection.UserSelectionDialogResult
+import androidx.lifecycle.ViewModel
+import com.android.credentialmanager.client.CredentialManagerClient
+import com.android.credentialmanager.ktx.getIntentSenderRequest
+import com.android.credentialmanager.model.Request
+import com.android.credentialmanager.model.get.CredentialEntryInfo
+import com.android.credentialmanager.ui.screens.UiState
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import javax.inject.Inject
+
+/** ViewModel for [MultiCredentialsFoldScreen].*/
+@HiltViewModel
+class MultiCredentialsFoldViewModel @Inject constructor(
+ private val credentialManagerClient: CredentialManagerClient,
+) : ViewModel() {
+
+ private lateinit var requestGet: Request.Get
+ private lateinit var entryInfo: CredentialEntryInfo
+
+ private val _uiState =
+ MutableStateFlow<UiState>(UiState.CredentialScreen)
+ val uiState: StateFlow<UiState> = _uiState
+
+ fun onCredentialClicked(entryInfo: CredentialEntryInfo) {
+ this.entryInfo = entryInfo
+ _uiState.value = UiState.CredentialSelected(
+ intentSenderRequest = entryInfo.getIntentSenderRequest()
+ )
+ }
+
+ fun onSignInOptionsClicked() {
+ // TODO(b/322797032) Implement navigation route for single credential screen to multiple
+ // credentials
+ }
+
+ fun onCancelClicked() {
+ _uiState.value = UiState.Cancel
+ }
+
+ fun onInfoRetrieved(
+ resultCode: Int? = null,
+ resultData: Intent? = null,
+ ) {
+ val userSelectionDialogResult = UserSelectionDialogResult(
+ requestGet.token,
+ entryInfo.providerId,
+ entryInfo.entryKey,
+ entryInfo.entrySubkey,
+ if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null
+ )
+ credentialManagerClient.sendResult(userSelectionDialogResult)
+ }
+}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
index 92d8a39..1697e0f 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
@@ -42,7 +42,7 @@
import com.android.credentialmanager.ui.components.SignInHeader
import com.android.credentialmanager.ui.components.SignInOptionsChip
import com.android.credentialmanager.ui.screens.single.SingleAccountScreen
-import com.android.credentialmanager.ui.screens.single.UiState
+import com.android.credentialmanager.ui.screens.UiState
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.compose.layout.ScalingLazyColumnState
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreenViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreenViewModel.kt
index 35c39f6..37ffaca 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreenViewModel.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreenViewModel.kt
@@ -26,7 +26,7 @@
import com.android.credentialmanager.client.CredentialManagerClient
import com.android.credentialmanager.model.get.CredentialEntryInfo
import dagger.hilt.android.lifecycle.HiltViewModel
-import com.android.credentialmanager.ui.screens.single.UiState
+import com.android.credentialmanager.ui.screens.UiState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt
new file mode 100644
index 0000000..8c2c081
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt
@@ -0,0 +1,146 @@
+/*
+ * 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.credentialmanager.ui.screens.single.signInWithProvider
+
+import android.graphics.drawable.Drawable
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.rememberNavController
+import com.android.credentialmanager.CredentialSelectorUiState
+import com.android.credentialmanager.model.get.CredentialEntryInfo
+import com.android.credentialmanager.R
+import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract
+import com.android.credentialmanager.ui.components.AccountRow
+import com.android.credentialmanager.ui.components.ContinueChip
+import com.android.credentialmanager.ui.components.DismissChip
+import com.android.credentialmanager.ui.components.SignInHeader
+import com.android.credentialmanager.ui.components.SignInOptionsChip
+import com.android.credentialmanager.ui.screens.single.SingleAccountScreen
+import com.android.credentialmanager.ui.screens.UiState
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+import com.google.android.horologist.compose.layout.ScalingLazyColumnState
+
+/**
+ * Screen that shows sign in with provider credential.
+ *
+ * @param credentialSelectorUiState The app bar view model.
+ * @param screenIcon The view model corresponding to the home page.
+ * @param columnState ScalingLazyColumn configuration to be be applied to SingleAccountScreen
+ * @param modifier styling for composable
+ * @param viewModel ViewModel that updates ui state for this screen
+ * @param navController handles navigation events from this screen
+ */
+@OptIn(ExperimentalHorologistApi::class)
+@Composable
+fun SignInWithProviderScreen(
+ credentialSelectorUiState: CredentialSelectorUiState.Get.SingleEntry,
+ screenIcon: Drawable?,
+ columnState: ScalingLazyColumnState,
+ modifier: Modifier = Modifier,
+ viewModel: SignInWithProviderViewModel = hiltViewModel(),
+ navController: NavHostController = rememberNavController(),
+) {
+ viewModel.initialize(credentialSelectorUiState.entry)
+
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ when (uiState) {
+ UiState.CredentialScreen -> {
+ SignInWithProviderScreen(
+ credentialSelectorUiState.entry,
+ screenIcon,
+ columnState,
+ modifier,
+ viewModel
+ )
+ }
+
+ is UiState.CredentialSelected -> {
+ val launcher = rememberLauncherForActivityResult(
+ StartBalIntentSenderForResultContract()
+ ) {
+ viewModel.onInfoRetrieved(it.resultCode, null)
+ }
+
+ SideEffect {
+ (uiState as UiState.CredentialSelected).intentSenderRequest?.let {
+ launcher.launch(it)
+ }
+ }
+ }
+
+ UiState.Cancel -> {
+ // TODO(b/322797032) add valid navigation path here for going back
+ navController.popBackStack()
+ }
+ }
+}
+
+@OptIn(ExperimentalHorologistApi::class)
+@Composable
+fun SignInWithProviderScreen(
+ entry: CredentialEntryInfo,
+ screenIcon: Drawable?,
+ columnState: ScalingLazyColumnState,
+ modifier: Modifier = Modifier,
+ viewModel: SignInWithProviderViewModel,
+) {
+ SingleAccountScreen(
+ headerContent = {
+ SignInHeader(
+ icon = screenIcon,
+ title = stringResource(R.string.use_sign_in_with_provider_title,
+ entry.providerDisplayName),
+ )
+ },
+ accountContent = {
+ val displayName = entry.displayName
+ if (displayName != null) {
+ AccountRow(
+ primaryText = displayName,
+ secondaryText = entry.userName,
+ modifier = Modifier.padding(top = 10.dp),
+ )
+ } else {
+ AccountRow(
+ primaryText = entry.userName,
+ modifier = Modifier.padding(top = 10.dp),
+ )
+ }
+ },
+ columnState = columnState,
+ modifier = modifier.padding(horizontal = 10.dp)
+ ) {
+ item {
+ Column {
+ ContinueChip(viewModel::onContinueClick)
+ SignInOptionsChip(viewModel::onSignInOptionsClick)
+ DismissChip(viewModel::onDismissClick)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderViewModel.kt
new file mode 100644
index 0000000..7ba45e5
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderViewModel.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.credentialmanager.ui.screens.single.signInWithProvider
+
+import android.content.Intent
+import android.credentials.selection.ProviderPendingIntentResponse
+import android.credentials.selection.UserSelectionDialogResult
+import androidx.annotation.MainThread
+import androidx.lifecycle.ViewModel
+import com.android.credentialmanager.ktx.getIntentSenderRequest
+import com.android.credentialmanager.model.Request
+import com.android.credentialmanager.client.CredentialManagerClient
+import com.android.credentialmanager.model.get.CredentialEntryInfo
+import dagger.hilt.android.lifecycle.HiltViewModel
+import com.android.credentialmanager.ui.screens.UiState
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import javax.inject.Inject
+
+/** ViewModel for [SignInWithProviderScreen].*/
+@HiltViewModel
+class SignInWithProviderViewModel @Inject constructor(
+ private val credentialManagerClient: CredentialManagerClient,
+) : ViewModel() {
+
+ private val _uiState =
+ MutableStateFlow<UiState>(UiState.CredentialScreen)
+ val uiState: StateFlow<UiState> = _uiState
+
+ private lateinit var requestGet: Request.Get
+ private lateinit var entryInfo: CredentialEntryInfo
+
+ @MainThread
+ fun initialize(entry: CredentialEntryInfo) {
+ this.entryInfo = entry
+ }
+
+ fun onDismissClick() {
+ _uiState.value = UiState.Cancel
+ }
+
+ fun onContinueClick() {
+ _uiState.value = UiState.CredentialSelected(
+ intentSenderRequest = entryInfo.getIntentSenderRequest()
+ )
+ }
+
+ fun onSignInOptionsClick() {
+ // TODO(b/322797032) Implement navigation route for single credential screen to multiple
+ // credentials
+ }
+
+ fun onInfoRetrieved(
+ resultCode: Int? = null,
+ resultData: Intent? = null,
+ ) {
+ val userSelectionDialogResult = UserSelectionDialogResult(
+ requestGet.token,
+ entryInfo.providerId,
+ entryInfo.entryKey,
+ entryInfo.entrySubkey,
+ if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null
+ )
+ credentialManagerClient.sendResult(userSelectionDialogResult)
+ }
+}
+
diff --git a/packages/SettingsLib/res/values/arrays.xml b/packages/SettingsLib/res/values/arrays.xml
index 3252aad..1b29e83 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 58e0a89..1150ac1 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.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 @@
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();
}
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException when trying to fetch default WebView package Name", e);
+ } 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);
+ }
+ }
+
+ 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 0000000..a696f8c
--- /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 0000000..6098307
--- /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 0000000..0fb8c3f
--- /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 0000000..dfc4c0a
--- /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 8ae117e..e424797 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 @@
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 @@
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 5adae375..a32eead 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 @@
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 @@
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 612badd..d27ff17 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 @@
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 926e181..c086baa 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -862,6 +862,8 @@
<!-- Permission required for CTS test - CtsTelephonyProviderTestCases -->
<uses-permission android:name="android.permission.WRITE_APN_SETTINGS" />
+ <uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
+ <uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.LOG_FOREGROUND_RESOURCE_USE" />
diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS
index 967a36b..78cacec 100644
--- a/packages/SystemUI/OWNERS
+++ b/packages/SystemUI/OWNERS
@@ -39,7 +39,6 @@
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 56576f1..c2072ac 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -420,3 +420,23 @@
}
}
+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 0000000..b8c4fae
--- /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 36ab46b4..e642434 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 @@
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 5b6aa09..a1bbc7d 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.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 @@
viewModel: SceneContainerViewModel,
windowInsets: StateFlow<WindowInsets?>,
sceneByKey: Map<SceneKey, Scene>,
+ dataSourceDelegator: SceneDataSourceDelegator,
): View {
return ComposeView(context).apply {
setContent {
@@ -139,6 +141,7 @@
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 428bc39..0469cbe 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.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 @@
) : 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 f3bef7b..11a38f9 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.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 @@
) : 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 378a1e4..7b21d09 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.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 @@
) : 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 @@
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 46554a4..d36345a3 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.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 @@
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/ComposeAwareExtensions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeAwareExtensions.kt
new file mode 100644
index 0000000..a7de1ee
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeAwareExtensions.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.composable
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.Back
+import com.android.compose.animation.scene.Edge as ComposeAwareEdge
+import com.android.compose.animation.scene.SceneKey as ComposeAwareSceneKey
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
+import com.android.compose.animation.scene.TransitionKey as ComposeAwareTransitionKey
+import com.android.compose.animation.scene.UserAction as ComposeAwareUserAction
+import com.android.compose.animation.scene.UserActionDistance as ComposeAwareUserActionDistance
+import com.android.compose.animation.scene.UserActionResult as ComposeAwareUserActionResult
+import com.android.systemui.scene.shared.model.Direction
+import com.android.systemui.scene.shared.model.Edge
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.TransitionKey
+import com.android.systemui.scene.shared.model.UserAction
+import com.android.systemui.scene.shared.model.UserActionDistance
+import com.android.systemui.scene.shared.model.UserActionResult
+
+// TODO(b/293899074): remove this file once we can use the types from SceneTransitionLayout.
+
+fun SceneKey.asComposeAware(): ComposeAwareSceneKey {
+ return ComposeAwareSceneKey(
+ debugName = toString(),
+ identity = this,
+ )
+}
+
+fun TransitionKey.asComposeAware(): ComposeAwareTransitionKey {
+ return ComposeAwareTransitionKey(
+ debugName = debugName,
+ identity = this,
+ )
+}
+
+fun UserAction.asComposeAware(): ComposeAwareUserAction {
+ return when (this) {
+ is UserAction.Swipe ->
+ Swipe(
+ pointerCount = pointerCount,
+ fromSource =
+ when (this.fromEdge) {
+ null -> null
+ Edge.LEFT -> ComposeAwareEdge.Left
+ Edge.TOP -> ComposeAwareEdge.Top
+ Edge.RIGHT -> ComposeAwareEdge.Right
+ Edge.BOTTOM -> ComposeAwareEdge.Bottom
+ },
+ direction =
+ when (this.direction) {
+ Direction.LEFT -> SwipeDirection.Left
+ Direction.UP -> SwipeDirection.Up
+ Direction.RIGHT -> SwipeDirection.Right
+ Direction.DOWN -> SwipeDirection.Down
+ }
+ )
+ is UserAction.Back -> Back
+ }
+}
+
+fun UserActionResult.asComposeAware(): ComposeAwareUserActionResult {
+ val composeUnaware = this
+ return ComposeAwareUserActionResult(
+ toScene = composeUnaware.toScene.asComposeAware(),
+ transitionKey = composeUnaware.transitionKey?.asComposeAware(),
+ distance = composeUnaware.distance?.asComposeAware(),
+ )
+}
+
+fun UserActionDistance.asComposeAware(): ComposeAwareUserActionDistance {
+ val composeUnware = this
+ return object : ComposeAwareUserActionDistance {
+ override fun Density.absoluteDistance(
+ fromSceneSize: IntSize,
+ orientation: Orientation,
+ ): Float {
+ return composeUnware.absoluteDistance(
+ fromSceneWidth = fromSceneSize.width,
+ fromSceneHeight = fromSceneSize.height,
+ isHorizontal = orientation == Orientation.Horizontal,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeUnawareExtensions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeUnawareExtensions.kt
new file mode 100644
index 0000000..4c03664
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeUnawareExtensions.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.composable
+
+import com.android.compose.animation.scene.ObservableTransitionState as ComposeAwareObservableTransitionState
+import com.android.compose.animation.scene.SceneKey as ComposeAwareSceneKey
+import com.android.systemui.scene.shared.model.ObservableTransitionState
+import com.android.systemui.scene.shared.model.SceneKey
+
+fun ComposeAwareSceneKey.asComposeUnaware(): SceneKey {
+ return this.identity as SceneKey
+}
+
+fun ComposeAwareObservableTransitionState.asComposeUnaware(): ObservableTransitionState {
+ return when (this) {
+ is ComposeAwareObservableTransitionState.Idle ->
+ ObservableTransitionState.Idle(scene.asComposeUnaware())
+ is ComposeAwareObservableTransitionState.Transition ->
+ ObservableTransitionState.Transition(
+ fromScene = fromScene.asComposeUnaware(),
+ toScene = toScene.asComposeUnaware(),
+ progress = progress,
+ isInitiatedByUserInput = isInitiatedByUserInput,
+ isUserInputOngoing = isUserInputOngoing,
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
index 736ee1f..f90f29d 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.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 @@
) : 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 da1b417..5006beb 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.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.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 @@
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 @@
) {
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 @@
)
}
}
-
-// 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 2848245..61f8120 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 @@
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/SceneTransitionLayoutDataSource.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt
new file mode 100644
index 0000000..60c0b77
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.scene.ui.composable
+
+import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.observableTransitionState
+import com.android.systemui.scene.shared.model.SceneDataSource
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.TransitionKey
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * An implementation of [SceneDataSource] that's backed by a [MutableSceneTransitionLayoutState].
+ */
+class SceneTransitionLayoutDataSource(
+ private val state: MutableSceneTransitionLayoutState,
+
+ /**
+ * The [CoroutineScope] of the @Composable that's using this, it's critical that this is *not*
+ * the application scope.
+ */
+ private val coroutineScope: CoroutineScope,
+) : SceneDataSource {
+ override val currentScene: StateFlow<SceneKey> =
+ state
+ .observableTransitionState()
+ .flatMapLatest { observableTransitionState ->
+ when (observableTransitionState) {
+ is ObservableTransitionState.Idle -> flowOf(observableTransitionState.scene)
+ is ObservableTransitionState.Transition ->
+ observableTransitionState.isUserInputOngoing.map { isUserInputOngoing ->
+ if (isUserInputOngoing) {
+ observableTransitionState.fromScene
+ } else {
+ observableTransitionState.toScene
+ }
+ }
+ }
+ }
+ .map { it.asComposeUnaware() }
+ .stateIn(
+ scope = coroutineScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = state.transitionState.currentScene.asComposeUnaware(),
+ )
+
+ override fun changeScene(
+ toScene: SceneKey,
+ transitionKey: TransitionKey?,
+ ) {
+ state.setTargetScene(
+ targetScene = toScene.asComposeAware(),
+ transitionKey = transitionKey?.asComposeAware(),
+ coroutineScope = coroutineScope,
+ )
+ }
+}
diff --git a/packages/SystemUI/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 0c66701..5a9add1 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 1223ace..6f115d8 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.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 2d5cf5c..e71f996 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.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 @@
}
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 cac35cb..25df3e4 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.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 @@
) : 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 @@
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 0000000..ccbf4ef
--- /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 e8a43ac..38dc24e 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.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 @@
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 @@
sceneInteractor.setTransitionState(sceneTransitionStateFlow)
deviceEntryInteractor = kosmos.deviceEntryInteractor
+ fakeSceneDataSource = kosmos.fakeSceneDataSource
+
underTest =
KeyguardSecurityContainerController(
view,
@@ -810,7 +814,8 @@
// 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 @@
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 @@
// 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 @@
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 @@
// 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 @@
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 @@
// 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 @@
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 @@
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 @@
// 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 @@
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 fbb5415..ad29e68 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.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 @@
@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 @@
@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 @@
assertThat(message?.text).isEmpty()
assertThat(password).isEqualTo("password")
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+ assertThat(currentScene).isEqualTo(SceneKey.Bouncer)
}
@Test
@@ -201,7 +200,7 @@
@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 @@
// 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 @@
}
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 725bdbd..32de1f2 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.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 @@
@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 @@
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 @@
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 @@
@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 @@
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 @@
}
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 06e1258..ccf7094 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.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 @@
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 @@
@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 @@
assertThat(message?.text).isEmpty()
assertThat(pin).isEmpty()
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+ assertThat(currentScene).isEqualTo(SceneKey.Bouncer)
}
@Test
@@ -216,7 +215,7 @@
@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 @@
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 @@
@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 @@
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 @@
}
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 b4e2eab..5b20ae5 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.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 @@
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 @@
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 0000000..2e9ee5c
--- /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 05b5891..98719dd3 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.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 @@
@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 @@
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 @@
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 @@
underTest.attemptDeviceEntry()
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+ assertThat(currentScene).isEqualTo(SceneKey.Gone)
}
@Test
@@ -384,6 +383,6 @@
}
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/data/repository/LightRevealScrimRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
index 7242cb2..f6c0566 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
@@ -54,7 +54,7 @@
private lateinit var powerInteractor: PowerInteractor
private lateinit var underTest: LightRevealScrimRepositoryImpl
- @get:Rule val animatorTestRule = AnimatorTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
@Before
fun setUp() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
index 7261723..3455050 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.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 @@
)
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 @@
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 6fc5be1..15cf83c 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 @@
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 51f8b11..d47da3e 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.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 @@
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 @@
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 006f429..7c30c7e 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.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 @@
private lateinit var emergencyAffordanceManager: EmergencyAffordanceManager
private lateinit var telecomManager: TelecomManager
+ private val fakeSceneDataSource = kosmos.fakeSceneDataSource
@Before
fun setUp() {
@@ -270,7 +271,7 @@
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 @@
testScope.runTest {
emulateUserDrivenTransition(SceneKey.Bouncer)
+ fakeSceneDataSource.pause()
enterPin()
- assertCurrentScene(SceneKey.Gone)
- emulateUiSceneTransition(
+ emulatePendingTransitionProgress(
expectedVisible = false,
)
+ assertCurrentScene(SceneKey.Gone)
}
@Test
@@ -302,11 +304,12 @@
to = upDestinationSceneKey,
)
+ fakeSceneDataSource.pause()
enterPin()
- assertCurrentScene(SceneKey.Gone)
- emulateUiSceneTransition(
+ emulatePendingTransitionProgress(
expectedVisible = false,
)
+ assertCurrentScene(SceneKey.Gone)
}
@Test
@@ -451,10 +454,11 @@
to = upDestinationSceneKey,
)
+ fakeSceneDataSource.pause()
dismissIme()
+ emulatePendingTransitionProgress()
assertCurrentScene(SceneKey.Lockscreen)
- emulateUiSceneTransition()
}
@Test
@@ -507,8 +511,9 @@
@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 @@
@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 @@
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 @@
}
/**
- * 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].
+ * 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.
*
- * This should post a series of values into [transitionState] to emulate a gradual scene
- * transition and culminate with a call to [SceneContainerViewModel.onSceneChanged].
- *
- * 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 @@
}
// 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 @@
* 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 @@
) {
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 @@
.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 2ad872c..1da3bc1 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.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 @@
}
@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 942fbc2..4b9ebdc 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.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 @@
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
+ private val fakeSceneDataSource = kosmos.fakeSceneDataSource
private lateinit var underTest: SceneInteractor
@@ -63,24 +64,24 @@
@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 @@
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 @@
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 34c5173..ffea84b 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.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 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 @@
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 @@
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 @@
@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 @@
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 @@
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 @@
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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
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 @@
@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 @@
@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 @@
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 @@
@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 @@
@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 @@
@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 @@
@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 @@
SceneKey.Bouncer,
)
.forEach { sceneKey ->
- sceneInteractor.changeScene(SceneModel(sceneKey), "reason")
+ sceneInteractor.changeScene(sceneKey, "reason")
runCurrent()
verify(falsingCollector, never()).onSuccessfulUnlock()
}
@@ -458,7 +460,7 @@
// 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 @@
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 @@
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 @@
@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 @@
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 @@
@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 @@
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 @@
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 @@
@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 @@
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 @@
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 @@
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 @@
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 @@
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/shared/model/SceneDataSourceDelegatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegatorTest.kt
new file mode 100644
index 0000000..ed4b1e6
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegatorTest.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.shared.model
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.initialSceneKey
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SceneDataSourceDelegatorTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val initialSceneKey = kosmos.initialSceneKey
+ private val fakeSceneDataSource = kosmos.fakeSceneDataSource
+
+ private val underTest = kosmos.sceneDataSourceDelegator
+
+ @Test
+ fun currentScene_withoutDelegate_startsWithInitialScene() =
+ testScope.runTest {
+ val currentScene by collectLastValue(underTest.currentScene)
+ underTest.setDelegate(null)
+
+ assertThat(currentScene).isEqualTo(initialSceneKey)
+ }
+
+ @Test
+ fun currentScene_withoutDelegate_doesNothing() =
+ testScope.runTest {
+ val currentScene by collectLastValue(underTest.currentScene)
+ underTest.setDelegate(null)
+ assertThat(currentScene).isNotEqualTo(SceneKey.Bouncer)
+
+ underTest.changeScene(toScene = SceneKey.Bouncer)
+
+ assertThat(currentScene).isEqualTo(initialSceneKey)
+ }
+
+ @Test
+ fun currentScene_withDelegate_startsWithInitialScene() =
+ testScope.runTest {
+ val currentScene by collectLastValue(underTest.currentScene)
+ assertThat(currentScene).isEqualTo(initialSceneKey)
+ }
+
+ @Test
+ fun currentScene_withDelegate_changesScenes() =
+ testScope.runTest {
+ val currentScene by collectLastValue(underTest.currentScene)
+ assertThat(currentScene).isNotEqualTo(SceneKey.Bouncer)
+
+ underTest.changeScene(toScene = SceneKey.Bouncer)
+
+ assertThat(currentScene).isEqualTo(SceneKey.Bouncer)
+ }
+
+ @Test
+ fun currentScene_reflectsDelegate() =
+ testScope.runTest {
+ val currentScene by collectLastValue(underTest.currentScene)
+
+ fakeSceneDataSource.changeScene(toScene = SceneKey.Bouncer)
+
+ assertThat(currentScene).isEqualTo(SceneKey.Bouncer)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index a16ce43..6c78317 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 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 @@
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 @@
}
@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 @@
}
@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 51b8342..ec424b0 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.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 @@
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 @@
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 @@
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 @@
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 @@
}
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 d3c6598..ec4da04 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.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 @@
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 @@
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 @@
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 @@
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 @@
}
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 f1f5dc3..799e8f0 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.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 @@
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 @@
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 @@
@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 @@
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 @@
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 4d7d5d3..efd8f00 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.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 @@
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 @@
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 @@
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 @@
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 @@
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 50d5607..41fb57a 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 2ab0813..71ae0d7 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 975a602..ad6609a 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 @@
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 035ccbd..27f9106fd 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.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 @@
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 a883c00..6d4baf4 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 @@
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 @@
}
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 @@
} 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 685ea81..74ea58c 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.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 @@
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 @@
});
}
+ // 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 b015f70..8c68eac 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 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 e6939f0..ff9cdbd 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 @@
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 ef4554c..8197145 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 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 4a06585f5..9e68ff8 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 @@
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 4e23ecd9..8178ade 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.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 @@
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 1720de8..e893177 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.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 @@
@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 0000000..55d2bfc
--- /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 0000000..4515fcb
--- /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 2461c26..a5f6f7c 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 @@
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 @@
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 @@
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 73389cb..21fd87c 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.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 @@
* 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 @@
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
similarity index 88%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt
index 6382338..79455eb 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.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.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 @@
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 0000000..118215c
--- /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 f006b34..5f1667a 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 @@
*/
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 abe49ee..86b99ec 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 @@
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 @@
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 0b227fa..968c3e3 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.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 @@
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 9381830..0659c7c 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 @@
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 @@
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 508f71a..0000000
--- 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 566e006..56d64a2 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.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 @@
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 @@
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 474de77..d8b7b4a 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 @@
// 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 0000000..9186dde
--- /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 b2a3549..d400210 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 @@
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 404046b..6e70368 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.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"
+private const val TAG = "KeyguardBlueprintViewBinder"
+private const val DEBUG = true
- 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)
+@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)
+ }
- val cs =
- ConstraintSet().apply {
- clone(constraintLayout)
- val emptyLayout = ConstraintSet.Layout()
- knownIds.forEach {
- getConstraint(it).layout.copyFrom(emptyLayout)
- }
- blueprint.applyConstraints(this)
- }
+ override fun onTransitionEnd(transition: Transition) {
+ if (DEBUG) Log.e(TAG, "onTransitionEnd: ${transition::class.simpleName}")
+ runningTransitions.remove(transition)
+ }
- // Apply 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()
}
+
+ viewModel.currentBluePrint = blueprint
+ Trace.endSection()
}
+ }
- launch {
- viewModel.blueprintWithTransition.collect { source ->
- TransitionManager.endTransitions(constraintLayout)
+ launch {
+ viewModel.refreshTransition.collect { transition ->
+ Trace.beginSection("KeyguardBlueprintViewBinder#refreshTransition")
+ val cs =
+ ConstraintSet().apply {
+ clone(constraintLayout)
+ viewModel.currentBluePrint?.applyConstraints(this)
+ }
- 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 62a6e8b..01596ed 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.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 kotlinx.coroutines.launch
object KeyguardClockViewBinder {
+ private val TAG = KeyguardClockViewBinder::class.simpleName!!
+
@JvmStatic
fun bind(
clockSection: ClockSection,
@@ -68,9 +70,7 @@
if (!migrateClocksToBlueprint()) return@launch
viewModel.clockSize.collect {
updateBurnInLayer(keyguardRootView, viewModel)
- blueprintInteractor.refreshBlueprintWithTransition(
- IntraBlueprintTransitionType.ClockSize
- )
+ blueprintInteractor.refreshBlueprint(Type.ClockSize)
}
}
launch {
@@ -83,13 +83,9 @@
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 @@
if (
viewModel.useLargeClock && it.config.id == "DIGITAL_CLOCK_WEATHER"
) {
- blueprintInteractor.refreshBlueprintWithTransition(
- IntraBlueprintTransitionType.DefaultTransition
- )
+ blueprintInteractor.refreshBlueprint(Type.DefaultTransition)
}
}
}
@@ -112,6 +106,7 @@
}
}
}
+
@VisibleForTesting
fun updateBurnInLayer(
keyguardRootView: ConstraintLayout,
@@ -171,6 +166,7 @@
}
}
}
+
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 08a2b9c..b77f0c5 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.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 @@
clockViewModel,
smartspaceViewModel
)
- blueprintInteractor.refreshBlueprintWithTransition()
+ blueprintInteractor.refreshBlueprint(
+ Config(
+ Type.SmartspaceVisibility,
+ checkPriority = false,
+ terminatePrevious = false,
+ )
+ )
}
}
@@ -57,7 +65,13 @@
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 524aa1a..a7075d9 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.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 631b342..54a7ca4 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.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 @@
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 @@
// 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 @@
}
}
}
+
open fun applyDefaultConstraints(constraints: ConstraintSet) {
val guideline =
if (keyguardClockViewModel.clockShouldBeCentered.value) PARENT_ID
@@ -168,8 +164,8 @@
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 @@
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 2f99719..8255bcc 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 @@
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 @@
}
}
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 @@
}
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 @@
}
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 @@
}
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 99565b1..64cbb32 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,175 +17,308 @@
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
- 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
+ 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
}
- 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) } }
- }
- }
-
- 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()
- }
- }
- }
-
- class ClockInChangeBounds(
- viewModel: KeyguardClockViewModel,
- type: IntraBlueprintTransitionType
- ) : ChangeBounds() {
- init {
- if (type != IntraBlueprintTransitionType.NoTransition) {
- duration = CLOCK_IN_MILLIS
- startDelay = CLOCK_IN_START_DELAY_MILLIS
- interpolator = CLOCK_IN_INTERPOLATOR
- } else {
- duration = 0
- startDelay = 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
}
- if (viewModel.useLargeClock) {
- viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
- } else {
- addTarget(R.id.lockscreen_clock_view)
- }
- }
- }
+ 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)
+ )
- class ClockOutChangeBounds(
- viewModel: KeyguardClockViewModel,
- type: IntraBlueprintTransitionType
- ) : ChangeBounds() {
- init {
- if (type != IntraBlueprintTransitionType.NoTransition) {
- duration = CLOCK_OUT_MILLIS
- interpolator = CLOCK_OUT_INTERPOLATOR
- } else {
- duration = 0
+ 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)
}
- if (viewModel.useLargeClock) {
- addTarget(R.id.lockscreen_clock_view)
- } else {
- viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
- }
- }
- }
- class SmartspaceChangeBounds(
- viewModel: KeyguardClockViewModel,
- val type: IntraBlueprintTransitionType = IntraBlueprintTransitionType.DefaultTransition
- ) : ChangeBounds() {
- init {
- if (type != IntraBlueprintTransitionType.NoTransition) {
- duration =
- if (viewModel.useLargeClock) {
- STATUS_AREA_MOVE_UP_MILLIS
- } else {
- STATUS_AREA_MOVE_DOWN_MILLIS
+ 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)
+ }
}
- interpolator = Interpolators.EMPHASIZED
- } else {
- duration = 0
+ )
+ 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 ClockFaceInTransition(
+ config: IntraBlueprintTransition.Config,
+ val viewModel: KeyguardClockViewModel,
+ ) : VisibilityBoundsTransition() {
+ init {
+ 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) } }
+ } else {
+ addTarget(R.id.lockscreen_clock_view)
+ }
+ }
+
+ 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 {
+ 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!!
+ }
+ }
+
+ // TODO: Might need a mechanism to update this one while in-progress
+ class SmartspaceMoveTransition(
+ val config: IntraBlueprintTransition.Config,
+ viewModel: KeyguardClockViewModel,
+ ) : VisibilityBoundsTransition() {
+ init {
+ 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 c35dad7..60ab40c 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 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 6846886..065c20a 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 @@
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 0000000..49c64bd
--- /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 a3d5453..c9cf0c3 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 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 @@
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 @@
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 d22856b..edd3318 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 @@
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 3a162d7..846bcbf 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 8a900ece..17454a9 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.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 @@
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 350fa38..a302194 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 @@
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 @@
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 @@
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 b9e9fe7..494c86c 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.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 @@
) {
/**
- * 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 @@
/**
* 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 @@
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 dcd87c0..605a5d9 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.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 @@
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 @@
}
applicationScope.launch {
- sceneInteractor.desiredScene
- .map { it.key == SceneKey.Bouncer }
+ sceneInteractor.currentScene
+ .map { it == SceneKey.Bouncer }
.distinctUntilChanged()
.collect { switchedToBouncerScene ->
if (switchedToBouncerScene) {
@@ -422,7 +421,7 @@
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 c2c2e04..d59fcff 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 @@
fun logSceneChangeCommitted(
from: SceneKey,
to: SceneKey,
- reason: String,
) {
logBuffer.log(
tag = TAG,
@@ -70,9 +69,8 @@
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 2e45353..05056c1 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 @@
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 @@
* 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 @@
* 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/SceneDataSource.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt
new file mode 100644
index 0000000..f7b45e5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.shared.model
+
+import kotlinx.coroutines.flow.StateFlow
+
+/** Defines interface for classes that provide access to scene state. */
+interface SceneDataSource {
+
+ /**
+ * The current scene, as seen by the real data source in the UI layer.
+ *
+ * During a transition between two scenes, the original scene will still be reflected in
+ * [currentScene] until a time when the UI layer decides to commit the change, which is when
+ * [currentScene] will have the value of the target/new scene.
+ */
+ val currentScene: StateFlow<SceneKey>
+
+ /**
+ * Asks for an asynchronous scene switch to [toScene], which will use the corresponding
+ * installed transition or the one specified by [transitionKey], if provided.
+ */
+ fun changeScene(
+ toScene: SceneKey,
+ transitionKey: TransitionKey? = null,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
new file mode 100644
index 0000000..a50830c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.scene.shared.model
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Delegates calls to a runtime-provided [SceneDataSource] or to a no-op implementation if a
+ * delegate isn't set.
+ */
+@SysUISingleton
+class SceneDataSourceDelegator
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ config: SceneContainerConfig,
+) : SceneDataSource {
+
+ private val noOpDelegate = NoOpSceneDataSource(config.initialSceneKey)
+ private val delegateMutable = MutableStateFlow<SceneDataSource>(noOpDelegate)
+
+ override val currentScene: StateFlow<SceneKey> =
+ delegateMutable
+ .flatMapLatest { delegate -> delegate.currentScene }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = config.initialSceneKey,
+ )
+
+ override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) {
+ delegateMutable.value.changeScene(
+ toScene = toScene,
+ transitionKey = transitionKey,
+ )
+ }
+
+ /**
+ * Binds the current, dependency injection provided [SceneDataSource] to the given object.
+ *
+ * In other words: once this is invoked, the state and functionality of the [SceneDataSource]
+ * will be served by the given [delegate].
+ *
+ * If `null` is passed in, the delegator will use a no-op implementation of [SceneDataSource].
+ *
+ * This removes any previously set delegate.
+ */
+ fun setDelegate(delegate: SceneDataSource?) {
+ delegateMutable.value = delegate ?: noOpDelegate
+ }
+
+ private class NoOpSceneDataSource(
+ initialSceneKey: SceneKey,
+ ) : SceneDataSource {
+ override val currentScene: StateFlow<SceneKey> =
+ MutableStateFlow(initialSceneKey).asStateFlow()
+ override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) = Unit
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneModel.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKey.kt
similarity index 67%
rename from packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneModel.kt
rename to packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKey.kt
index f3d549f..87332ae 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKey.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.
@@ -16,12 +16,11 @@
package com.android.systemui.scene.shared.model
-/** 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,
+/**
+ * Key for a transition. This can be used to specify which transition spec should be used when
+ * starting the transition between two scenes.
+ */
+data class TransitionKey(
+ val debugName: String,
+ val identity: Any = Object(),
)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt
new file mode 100644
index 0000000..926878c
--- /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/shared/model/SceneModel.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/UserActionDistance.kt
similarity index 63%
copy from packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneModel.kt
copy to packages/SystemUI/src/com/android/systemui/scene/shared/model/UserActionDistance.kt
index f3d549f..b93f837 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/UserActionDistance.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.
@@ -16,12 +16,11 @@
package com.android.systemui.scene.shared.model
-/** Models a scene. */
-data class SceneModel(
+interface UserActionDistance {
- /** 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,
-)
+ /**
+ * Return the **absolute** distance of the user action (in pixels) given the size of the scene
+ * we are animating from and the orientation.
+ */
+ fun absoluteDistance(fromSceneWidth: Int, fromSceneHeight: Int, isHorizontal: Boolean): Float
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/UserActionResult.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/UserActionResult.kt
new file mode 100644
index 0000000..e1b96e4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/UserActionResult.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.shared.model
+
+data class UserActionResult(
+
+ /** The scene we should be transitioning due to the [UserAction]. */
+ val toScene: SceneKey,
+
+ /**
+ * The distance the action takes to animate from 0% to 100%.
+ *
+ * If `null`, a default distance will be used depending on the [UserAction] performed.
+ */
+ val distance: UserActionDistance? = null,
+
+ /**
+ * The key of the transition that should be used, if a specific one should be used.
+ *
+ * If `null`, the transition used will be the corresponding transition from the collection
+ * passed into the UI layer.
+ */
+ val transitionKey: TransitionKey? = null,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
index c88a04c..67dc0cc 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 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 @@
flags: SceneContainerFlags,
scenes: Set<Scene>,
layoutInsetController: LayoutInsetsController,
+ sceneDataSourceDelegator: SceneDataSourceDelegator,
) {
this.viewModel = viewModel
setLayoutInsetsController(layoutInsetController)
@@ -46,7 +48,8 @@
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 2b978b2..45b6f65 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.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 @@
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 @@
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 2431660..5d290ce 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.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 @@
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 4e8b403..7cb3be7 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.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 @@
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 @@
// 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 @@
private fun animateCollapseShadeInternal() {
sceneInteractor.changeScene(
- SceneModel(getCollapseDestinationScene(), "ShadeController.animateCollapseShade"),
- "ShadeController.animateCollapseShade"
+ getCollapseDestinationScene(),
+ "ShadeController.animateCollapseShade",
+ SlightlyFasterShadeCollapse,
)
}
@@ -183,17 +183,11 @@
}
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 @@
}
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 f40be4b..2cb9f9a 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.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 @@
flagsProvider: Provider<SceneContainerFlags>,
scenesProvider: Provider<Set<@JvmSuppressWildcards Scene>>,
layoutInsetController: NotificationInsetsController,
+ sceneDataSourceDelegator: Provider<SceneDataSourceDelegator>,
): WindowRootView {
return if (sceneContainerFlags.isEnabled()) {
val sceneWindowRootView =
@@ -84,6 +86,7 @@
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 9bbe1bd..a2e2598 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 @@
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 @@
} 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 04d9b0c..14230ba 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.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 @@
ScreenLifecycle screenLifecycle,
KeyguardBypassController keyguardBypassController,
AccessibilityManager accessibilityManager,
- FaceHelpMessageDeferral faceHelpMessageDeferral,
+ FaceHelpMessageDeferralFactory faceHelpMessageDeferral,
KeyguardLogger keyguardLogger,
AlternateBouncerInteractor alternateBouncerInteractor,
AlarmManager alarmManager,
@@ -308,7 +309,7 @@
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 4d37335..0e0f152 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 @@
}
} 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 e7012ea51..17fc5c6 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 @@
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/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
index de3a626..c8d6abe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
@@ -311,12 +311,13 @@
boundViewsByNotifKey[it.notifKey]?.first
}
val childCount = view.childCount
+ val toRemove = mutableListOf<View>()
for (i in 0 until childCount) {
val actual = view.getChildAt(i)
val expected = expectedChildren.getOrNull(i)
if (expected == null) {
Log.wtf(TAG, "[$logTag] Unexpected child $actual")
- view.removeView(actual)
+ toRemove.add(actual)
continue
}
if (actual === expected) {
@@ -325,6 +326,9 @@
view.removeView(expected)
view.addView(expected, i)
}
+ for (child in toRemove) {
+ view.removeView(child)
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt
index f096dd6..6e4b327 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 @@
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 c4d266e..ec8e5d7 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 @@
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 15fde0e..634de7a 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 @@
}
}
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/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 2206be5..38b3718 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.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.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 @@
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 @@
SecureSettings secureSettings,
CommandQueue commandQueue,
@Main Executor mainExecutor,
+ @Background Executor backgroundExecutor,
KeyguardLogger logger,
NotificationMediaManager notificationMediaManager,
StatusOverlayHoverListenerFactory statusOverlayHoverListenerFactory
@@ -323,6 +327,7 @@
mSecureSettings = secureSettings;
mCommandQueue = commandQueue;
mMainExecutor = mainExecutor;
+ mBackgroundExecutor = backgroundExecutor;
mLogger = logger;
mFirstBypassAttempt = mKeyguardBypassController.getBypassEnabled();
@@ -607,8 +612,18 @@
* 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 7f7eb04..9d70f42 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 @@
}
mBypassController = bypassController;
mNotificationContainer = notificationContainer;
- mKeyguardMessageAreaController = mKeyguardMessageAreaFactory.create(
- centralSurfaces.getKeyguardMessageArea());
+ if (!DeviceEntryUdfpsRefactor.isEnabled()) {
+ mKeyguardMessageAreaController = mKeyguardMessageAreaFactory.create(
+ centralSurfaces.getKeyguardMessageArea());
+ }
+
mCentralSurfacesRegistered = true;
registerListeners();
@@ -863,6 +866,7 @@
final boolean isShowingAlternateBouncer = mAlternateBouncerInteractor.isVisibleState();
if (mKeyguardMessageAreaController != null) {
+ DeviceEntryUdfpsRefactor.assertInLegacyMode();
mKeyguardMessageAreaController.setIsVisible(isShowingAlternateBouncer);
mKeyguardMessageAreaController.setMessage("");
}
@@ -1447,6 +1451,7 @@
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 fc2f6e9..c089092 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.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 @@
@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/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
index 9f4a906..f397627 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
@@ -444,7 +444,8 @@
UserHandle.of(userId))) {
boolean hasCACerts = !(conn.getService().getUserCaAliases().getList().isEmpty());
idWithCert = new Pair<Integer, Boolean>(userId, hasCACerts);
- } catch (RemoteException | InterruptedException | AssertionError e) {
+ } catch (RemoteException | InterruptedException | AssertionError
+ | IllegalStateException e) {
Log.i(TAG, "failed to get CA certs", e);
idWithCert = new Pair<Integer, Boolean>(userId, null);
} finally {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/tv/OWNERS
new file mode 100644
index 0000000..43450c4
--- /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 1af5c46..67d6a7f 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 @@
@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 1c17fc3..bb4a2c4 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 @@
controller.setSuggestionCardIds(storeLocations.toSet())
}
- private val binder: IWalletContextualLocationsService.Stub
- = object : IWalletContextualLocationsService.Stub() {
- override fun addWalletCardsUpdatedListener(listener: IWalletCardsUpdatedListener) {
- addWalletCardsUpdatedListenerInternal(listener)
+ private val binder: IWalletContextualLocationsService.Stub =
+ object : IWalletContextualLocationsService.Stub() {
+ override fun addWalletCardsUpdatedListener(listener: IWalletCardsUpdatedListener) {
+ addWalletCardsUpdatedListenerInternal(listener)
+ }
+ override fun onWalletContextualLocationsStateUpdated(storeLocations: List<String>) {
+ onWalletContextualLocationsStateUpdatedInternal(storeLocations)
+ }
}
- 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/android/animation/AnimatorTestRuleIsolationTest.kt b/packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt
index 0fe2283..f23fbee 100644
--- a/packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt
+++ b/packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt
@@ -34,7 +34,7 @@
@RunWithLooper
class AnimatorTestRuleIsolationTest : SysuiTestCase() {
- @get:Rule val animatorTestRule = AnimatorTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
@Test
fun testA() {
diff --git a/packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt b/packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt
index cc7f7e4..fd5f157 100644
--- a/packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt
+++ b/packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt
@@ -31,7 +31,7 @@
@RunWithLooper
class AnimatorTestRulePrecisionTest : SysuiTestCase() {
- @get:Rule val animatorTestRule = AnimatorTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
var value1: Float = -1f
var value2: Float = -1f
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
index fad8552..e893eb1 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
@@ -56,7 +56,7 @@
public class KeyguardStatusViewControllerTest extends KeyguardStatusViewControllerBaseTest {
@Rule
- public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this);
@Test
public void dozeTimeTick_updatesSlice() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java
index ba27fcd..dd428f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java
@@ -48,7 +48,7 @@
public class ExpandHelperTest extends SysuiTestCase {
@Rule
- public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this);
private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
private ExpandableNotificationRow mRow;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
index e006d59..64936862 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
@@ -82,7 +82,7 @@
public class WindowMagnificationAnimationControllerTest extends SysuiTestCase {
@Rule
- public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this);
@Rule
public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
private static final float DEFAULT_SCALE = 4.0f;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
index 2225ad6..f1b0c18 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
@@ -129,7 +129,8 @@
public class WindowMagnificationControllerTest extends SysuiTestCase {
@Rule
- public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ // NOTE: pass 'null' to allow this test advances time on the main thread.
+ public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(null);
@Rule
public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java
index a35a509..08b49e7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java
@@ -132,7 +132,7 @@
public class WindowMagnificationControllerWindowlessMagnifierTest extends SysuiTestCase {
@Rule
- public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this);
@Rule
public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java
index 9087816..abc95bc 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 @@
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 4a1bdbc..ce4db8f 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 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.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 @@
private String mLastAccessibilityButtonTargets;
private String mLastEnabledAccessibilityServices;
private WindowMetrics mWindowMetrics;
+ private MenuViewModel mMenuViewModel;
private MenuView mMenuView;
private MenuAnimationController mMenuAnimationController;
@@ -148,15 +152,17 @@
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 @@
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 @@
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/animation/AnimatorTestRuleOrderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt
index 2b51ac5..f07932c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt
@@ -19,7 +19,6 @@
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import androidx.core.animation.doOnEnd
-import androidx.test.filters.FlakyTest
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.doOnEnd
@@ -31,10 +30,9 @@
@RunWith(AndroidTestingRunner::class)
@SmallTest
@RunWithLooper
-@FlakyTest(bugId = 302149604)
class AnimatorTestRuleOrderTest : SysuiTestCase() {
- @get:Rule val animatorTestRule = AnimatorTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
var value1: Float = -1f
var value2: Float = -1f
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorTest.kt
new file mode 100644
index 0000000..a53f8d4
--- /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 bdf0e06..43c860c 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 0000000..f36f032
--- /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 e158e47..e97edcb 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.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 @@
}
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 @@
)
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 f2bd817..37836a5 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 @@
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 @@
@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 3389fa9..0000000
--- 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 8b16da2..9df00d3 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.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 @@
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 @@
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 @@
@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 7358b9d..0000000
--- 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/binder/KeyguardSurfaceBehindParamsApplierTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplierTest.kt
index 5b29a86..7787a7f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplierTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplierTest.kt
@@ -43,7 +43,7 @@
@RunWithLooper(setAsMainLooper = true)
@kotlinx.coroutines.ExperimentalCoroutinesApi
class KeyguardSurfaceBehindParamsApplierTest : SysuiTestCase() {
- @get:Rule val animatorTestRule = AnimatorTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
private lateinit var underTest: KeyguardSurfaceBehindParamsApplier
private lateinit var executor: FakeExecutor
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
index 2da74b0..08d44c1 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 @@
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 @@
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 @@
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 @@
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 @@
.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 8a531fd..bfb18c8 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.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.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 @@
@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 @@
}
@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/qs/tileimpl/QSIconViewImplTest_311121830.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt
index bbae0c9..ae2a9ad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt
@@ -40,7 +40,7 @@
@SmallTest
class QSIconViewImplTest_311121830 : SysuiTestCase() {
- @get:Rule val animatorRule = AnimatorTestRule()
+ @get:Rule val animatorRule = AnimatorTestRule(this)
@Test
fun alwaysLastIcon() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 8a22f4c..17e4e0f 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 @@
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 f582402..2f765d5 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 @@
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 459040a..2bd0d79 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.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 @@
@Mock
protected AccessibilityManager mAccessibilityManager;
@Mock
+ protected FaceHelpMessageDeferralFactory mFaceHelpMessageDeferralFactory;
+ @Mock
protected FaceHelpMessageDeferral mFaceHelpMessageDeferral;
@Mock
protected AlternateBouncerInteractor mAlternateBouncerInteractor;
@@ -224,6 +227,8 @@
.thenReturn(mDisclosureWithOrganization);
when(mUserTracker.getUserId()).thenReturn(mCurrentUserId);
+ when(mFaceHelpMessageDeferralFactory.create()).thenReturn(mFaceHelpMessageDeferral);
+
mIndicationHelper = new IndicationHelper(mKeyguardUpdateMonitor);
mWakeLock = new WakeLockFake();
@@ -257,7 +262,7 @@
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/events/SystemEventChipAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
index 8be2ef0..452302d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
@@ -51,7 +51,7 @@
class SystemEventChipAnimationControllerTest : SysuiTestCase() {
private lateinit var controller: SystemEventChipAnimationController
- @get:Rule val animatorTestRule = AnimatorTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
@Mock private lateinit var sbWindowController: StatusBarWindowController
@Mock private lateinit var insetsProvider: StatusBarContentInsetsProvider
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
index 875fe58..cacfa8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
@@ -76,7 +76,7 @@
private lateinit var chipAnimationController: SystemEventChipAnimationController
private lateinit var systemStatusAnimationScheduler: SystemStatusAnimationScheduler
- @get:Rule val animatorTestRule = AnimatorTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
@Before
fun setup() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
index 039fef9..82093ad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
@@ -54,7 +54,7 @@
@TestableLooper.RunWithLooper(setAsMainLooper = true)
class NotificationWakeUpCoordinatorTest : SysuiTestCase() {
- @get:Rule val animatorTestRule = AnimatorTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
private val kosmos = Kosmos()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
index 446b9d0..dbf7b6c 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.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 @@
} 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 d4300f0..b938029 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 @@
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 f266f03..91a9da3 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 @@
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 @@
}
@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/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
index 2d120cd..76c9740 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 @@
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 @@
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 bde2243..f91064b 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 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.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 @@
@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/statusbar/phone/PhoneStatusBarViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
index 4293a27..269b70f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
@@ -257,6 +257,10 @@
/* privacyIndicatorBounds = */ PrivacyIndicatorBounds(),
/* displayShape = */ DisplayShape.NONE,
/* compatInsetsTypes = */ 0,
- /* compatIgnoreVisibility = */ false
+ /* compatIgnoreVisibility = */ false,
+ /* typeBoundingRectsMap = */ arrayOf(),
+ /* typeMaxBoundingRectsMap = */ arrayOf(),
+ /* frameWidth = */ 0,
+ /* frameHeight = */ 0
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 54d3607..3da5ab9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -131,7 +131,7 @@
@Mock
private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@Rule
- public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this);
private List<StatusBarWindowStateListener> mStatusBarWindowStateListeners = new ArrayList<>();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt
index 2ce060c..997c00c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt
@@ -42,7 +42,7 @@
private val multiSourceMinAlphaController =
MultiSourceMinAlphaController(view, initialAlpha = INITIAL_ALPHA)
- @get:Rule val animatorTestRule = AnimatorTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
@Before
fun setup() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index 658e6b0..13167b2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -112,7 +112,7 @@
private final UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake();
@Rule
- public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this);
@Before
public void setUp() throws Exception {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 8a33778..25a0bc4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -157,7 +157,7 @@
private FakeSettings mSecureSettings;
@Rule
- public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this);
@Before
public void setup() throws Exception {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualLocationsServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualLocationsServiceTest.kt
index af1d788..e6b4d06 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.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 @@
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 @@
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 @@
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 @@
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 8d933dc..97bd96d 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 @@
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/animation/AnimatorTestRule.java b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java
index 1afe56f..5860c2d 100644
--- a/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java
+++ b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java
@@ -16,14 +16,21 @@
package android.animation;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
import android.animation.AnimationHandler.AnimationFrameCallback;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Looper;
import android.os.SystemClock;
+import android.testing.TestableLooper;
+import android.testing.TestableLooper.RunnableWithException;
import android.util.AndroidRuntimeException;
import android.util.Singleton;
import android.view.Choreographer;
+import android.view.animation.AnimationUtils;
+
+import androidx.test.platform.app.InstrumentationRegistry;
import com.android.internal.util.Preconditions;
@@ -74,25 +81,31 @@
return new TestHandler();
}
};
+ private final Object mTest;
private final long mStartTime;
private long mTotalTimeDelta = 0;
+ private volatile boolean mCanLockAnimationClock;
+ private Looper mLooperWithLockedAnimationClock;
/**
- * Construct an AnimatorTestRule with a custom start time.
- * @see #AnimatorTestRule()
+ * Construct an AnimatorTestRule with access to the test instance and a custom start time.
+ * @see #AnimatorTestRule(Object)
*/
- public AnimatorTestRule(long startTime) {
+ public AnimatorTestRule(Object test, long startTime) {
+ mTest = test;
mStartTime = startTime;
}
/**
- * Construct an AnimatorTestRule with a start time of {@link SystemClock#uptimeMillis()}.
- * Initializing the start time with this clock reduces the discrepancies with various internals
- * of classes like ValueAnimator which can sometimes read that clock via
- * {@link android.view.animation.AnimationUtils#currentAnimationTimeMillis()}.
+ * Construct an AnimatorTestRule for the given test instance with a start time of
+ * {@link SystemClock#uptimeMillis()}. Initializing the start time with this clock reduces the
+ * discrepancies with various internals of classes like ValueAnimator which can sometimes read
+ * that clock via {@link android.view.animation.AnimationUtils#currentAnimationTimeMillis()}.
+ *
+ * @param test the test instance used to access the {@link TestableLooper} used by the class.
*/
- public AnimatorTestRule() {
- this(SystemClock.uptimeMillis());
+ public AnimatorTestRule(Object test) {
+ this(test, SystemClock.uptimeMillis());
}
@NonNull
@@ -102,10 +115,16 @@
@Override
public void evaluate() throws Throwable {
final TestHandler testHandler = mTestHandler.get();
- AnimationHandler objAtStart = AnimationHandler.setTestHandler(testHandler);
+ final AnimationHandler objAtStart = AnimationHandler.setTestHandler(testHandler);
+ final RunnableWithException lockClock =
+ wrapWithRunBlocking(new LockAnimationClockRunnable());
+ final RunnableWithException unlockClock =
+ wrapWithRunBlocking(new UnlockAnimationClockRunnable());
try {
+ lockClock.run();
base.evaluate();
} finally {
+ unlockClock.run();
AnimationHandler objAtEnd = AnimationHandler.setTestHandler(objAtStart);
if (testHandler != objAtEnd) {
// pass or fail, inner logic not restoring the handler needs to be reported.
@@ -118,6 +137,78 @@
};
}
+ private RunnableWithException wrapWithRunBlocking(RunnableWithException runnable) {
+ RunnableWithException wrapped = TestableLooper.wrapWithRunBlocking(mTest, runnable);
+ if (wrapped != null) {
+ return wrapped;
+ }
+ return () -> runOnMainThrowing(runnable);
+ }
+
+ private static void runOnMainThrowing(RunnableWithException runnable) throws Exception {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ runnable.run();
+ } else {
+ final Throwable[] throwableBox = new Throwable[1];
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ try {
+ runnable.run();
+ } catch (Throwable t) {
+ throwableBox[0] = t;
+ }
+ });
+ if (throwableBox[0] == null) {
+ return;
+ } else if (throwableBox[0] instanceof RuntimeException ex) {
+ throw ex;
+ } else if (throwableBox[0] instanceof Error err) {
+ throw err;
+ } else {
+ throw new RuntimeException(throwableBox[0]);
+ }
+ }
+ }
+
+ private class LockAnimationClockRunnable implements RunnableWithException {
+ @Override
+ public void run() {
+ mLooperWithLockedAnimationClock = Looper.myLooper();
+ mCanLockAnimationClock = true;
+ lockAnimationClockToCurrentTime();
+ }
+ }
+
+ private class UnlockAnimationClockRunnable implements RunnableWithException {
+ @Override
+ public void run() {
+ mCanLockAnimationClock = false;
+ mLooperWithLockedAnimationClock = null;
+ AnimationUtils.unlockAnimationClock();
+ }
+ }
+
+ private void lockAnimationClockToCurrentTime() {
+ if (!mCanLockAnimationClock) {
+ throw new AssertionError("Unable to lock the animation clock; "
+ + "has the test started? already finished?");
+ }
+ if (mLooperWithLockedAnimationClock != Looper.myLooper()) {
+ throw new AssertionError("Animation clock being locked on " + Looper.myLooper()
+ + " but should only be locked on " + mLooperWithLockedAnimationClock);
+ }
+ long desiredTime = getCurrentTime();
+ AnimationUtils.lockAnimationClock(desiredTime);
+ if (!mCanLockAnimationClock) {
+ AnimationUtils.unlockAnimationClock();
+ throw new AssertionError("Threading error when locking the animation clock");
+ }
+ long outputTime = AnimationUtils.currentAnimationTimeMillis();
+ if (outputTime != desiredTime) {
+ throw new AssertionError("currentAnimationTimeMillis() is " + outputTime
+ + " after locking to " + desiredTime);
+ }
+ }
+
/**
* If any new {@link Animator}s have been registered since the last time the frame time was
* advanced, initialize them with the current frame time. Failing to do this will result in the
@@ -178,6 +269,7 @@
// advance time
mTotalTimeDelta += timeDelta;
}
+ lockAnimationClockToCurrentTime();
if (preFrameAction != null) {
preFrameAction.accept(timeDelta);
// After letting other code run, clear any new callbacks to avoid double-ticking them
diff --git a/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt b/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt
index 5e254bf..f6ddfcc 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 @@
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 dc5fd95..6dd8d07 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.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 @@
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 3724291..69b769e 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.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 @@
TestMocksModule::class,
CoroutineTestScopeModule::class,
FakeSystemUiModule::class,
+ SceneContainerFrameworkModule::class,
]
)
interface SysUITestModule {
@@ -63,6 +67,7 @@
@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 3f55f42..b8c880b 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.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 @@
@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/animation/AnimatorTestRule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/AnimatorTestRule.kt
index ba9c5ed..e2fc44f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/AnimatorTestRule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/AnimatorTestRule.kt
@@ -26,12 +26,16 @@
* A rule that wraps both [androidx.core.animation.AnimatorTestRule] and
* [android.animation.AnimatorTestRule] such that the clocks of the two animation handlers can be
* advanced together.
+ *
+ * @param test the instance of the test used to look up the TestableLooper. If a TestableLooper is
+ * found, the time can only be advanced on that thread; otherwise the time must be advanced on the
+ * main thread.
*/
-class AnimatorTestRule : TestRule {
+class AnimatorTestRule(test: Any?) : TestRule {
// Create the androidx rule, which initializes start time to SystemClock.uptimeMillis(),
// then copy that time to the platform rule so that the two clocks are in sync.
private val androidxRule = androidx.core.animation.AnimatorTestRule()
- private val platformRule = android.animation.AnimatorTestRule(androidxRule.startTime)
+ private val platformRule = android.animation.AnimatorTestRule(test, androidxRule.startTime)
private val advanceAndroidXTimeBy =
Consumer<Long> { timeDelta -> androidxRule.advanceTimeBy(timeDelta) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt
index 244ef8d..3c61353 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.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 0000000..77f48db
--- /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/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractorKosmos.kt
new file mode 100644
index 0000000..4fcf43a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricSettingsInteractorKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.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 0000000..c3f677e
--- /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 19cd950..8452963 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 10305f7..f6b3280 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.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 @@
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/FakeSceneModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/FakeSceneModule.kt
deleted file mode 100644
index 5d22a6e..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/FakeSceneModule.kt
+++ /dev/null
@@ -1,23 +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.scene
-
-import com.android.systemui.scene.shared.flag.FakeSceneContainerFlagsModule
-import com.android.systemui.scene.shared.model.FakeSceneContainerConfigModule
-import dagger.Module
-
-@Module(includes = [FakeSceneContainerConfigModule::class, FakeSceneContainerFlagsModule::class])
-object FakeSceneModule
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 e19941c..8734609 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/scene/shared/model/FakeSceneContainerConfigModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt
deleted file mode 100644
index 8811b8d..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt
+++ /dev/null
@@ -1,37 +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.scene.shared.model
-
-import dagger.Module
-import dagger.Provides
-
-@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,
- ),
-)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt
new file mode 100644
index 0000000..c208aad
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.shared.model
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeSceneDataSource(
+ initialSceneKey: SceneKey,
+) : SceneDataSource {
+
+ private val _currentScene = MutableStateFlow(initialSceneKey)
+ override val currentScene: StateFlow<SceneKey> = _currentScene.asStateFlow()
+
+ var isPaused = false
+ private set
+ var pendingScene: SceneKey? = null
+ private set
+
+ override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) {
+ if (isPaused) {
+ pendingScene = toScene
+ } else {
+ _currentScene.value = toScene
+ }
+ }
+
+ /**
+ * Pauses scene changes.
+ *
+ * Any following calls to [changeScene] will be conflated and the last one will be remembered.
+ */
+ fun pause() {
+ check(!isPaused) { "Can't pause what's already paused!" }
+
+ isPaused = true
+ }
+
+ /**
+ * Unpauses scene changes.
+ *
+ * If there were any calls to [changeScene] since [pause] was called, the latest of the bunch
+ * will be replayed.
+ *
+ * If [force] is `true`, there will be no check that [isPaused] is true.
+ *
+ * If [expectedScene] is provided, will assert that it's indeed the latest called.
+ */
+ fun unpause(
+ force: Boolean = false,
+ expectedScene: SceneKey? = null,
+ ) {
+ check(force || isPaused) { "Can't unpause what's already not paused!" }
+
+ isPaused = false
+ pendingScene?.let { _currentScene.value = it }
+ pendingScene = null
+
+ check(expectedScene == null || currentScene.value == expectedScene) {
+ """
+ Unexpected scene while unpausing.
+ Expected $expectedScene but was $currentScene.
+ """
+ .trimIndent()
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneDataSourceKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneDataSourceKosmos.kt
new file mode 100644
index 0000000..f519686
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneDataSourceKosmos.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.shared.model
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.scene.initialSceneKey
+import com.android.systemui.scene.sceneContainerConfig
+
+val Kosmos.fakeSceneDataSource by Fixture {
+ FakeSceneDataSource(
+ initialSceneKey = initialSceneKey,
+ )
+}
+
+val Kosmos.sceneDataSourceDelegator by Fixture {
+ SceneDataSourceDelegator(
+ applicationScope = applicationCoroutineScope,
+ config = sceneContainerConfig,
+ )
+ .apply { setDelegate(fakeSceneDataSource) }
+}
+
+val Kosmos.sceneDataSource by Fixture { sceneDataSourceDelegator }
diff --git a/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 0000000..a61f7ec
--- /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/Android.bp b/ravenwood/Android.bp
index 1ac69f6..35ce481 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -58,11 +58,14 @@
visibility: ["//visibility:public"],
}
-java_host_for_device {
- name: "core-xml-for-device",
- libs: [
- "core-xml-for-host",
+java_library {
+ // Prefixed with "200" to ensure it's sorted early in Tradefed classpath
+ // so that we provide a concrete implementation before Mainline stubs
+ name: "200-kxml2-android",
+ static_libs: [
+ "kxml2-android",
],
+ visibility: ["//frameworks/base"],
}
java_host_for_device {
diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING
index 0319848..a5b28ad 100644
--- a/ravenwood/TEST_MAPPING
+++ b/ravenwood/TEST_MAPPING
@@ -2,6 +2,9 @@
"presubmit": [
{
"name": "RavenwoodMockitoTest_device"
+ },
+ {
+ "name": "RavenwoodBivalentTest_device"
}
],
"ravenwood-presubmit": [
@@ -16,6 +19,14 @@
{
"name": "CtsUtilTestCasesRavenwood",
"host": true
+ },
+ {
+ "name": "RavenwoodCoreTest",
+ "host": true
+ },
+ {
+ "name": "RavenwoodBivalentTest",
+ "host": true
}
]
}
diff --git a/ravenwood/bivalenttest/Android.bp b/ravenwood/bivalenttest/Android.bp
new file mode 100644
index 0000000..a6b6ed9
--- /dev/null
+++ b/ravenwood/bivalenttest/Android.bp
@@ -0,0 +1,77 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+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",
+
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ ],
+ srcs: [
+ "test/**/*.java",
+ ],
+ jni_libs: [
+ "libravenwoodbivalenttest_jni",
+ ],
+ sdk_version: "test_current",
+ auto_gen_config: true,
+}
+
+android_test {
+ name: "RavenwoodBivalentTest_device",
+
+ srcs: [
+ "test/**/*.java",
+ ],
+ static_libs: [
+ "junit",
+ "truth",
+
+ "androidx.annotation_annotation",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+
+ "ravenwood-junit",
+ ],
+ jni_libs: [
+ "libravenwoodbivalenttest_jni",
+ ],
+ test_suites: [
+ "device-tests",
+ ],
+ optimize: {
+ enabled: false,
+ },
+}
diff --git a/ravenwood/bivalenttest/AndroidManifest.xml b/ravenwood/bivalenttest/AndroidManifest.xml
new file mode 100644
index 0000000..474c03f
--- /dev/null
+++ b/ravenwood/bivalenttest/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.ravenwood.bivalenttest">
+
+ <application android:debuggable="true" >
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.ravenwood.bivalenttest"
+ />
+</manifest>
diff --git a/ravenwood/bivalenttest/AndroidTest.xml b/ravenwood/bivalenttest/AndroidTest.xml
new file mode 100644
index 0000000..ac4695b
--- /dev/null
+++ b/ravenwood/bivalenttest/AndroidTest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Runs Frameworks Services Tests.">
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="apct-instrumentation" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="RavenwoodBivalentTest_device.apk" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.ravenwood.bivalenttest" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ </test>
+</configuration>
diff --git a/ravenwood/bivalenttest/README.md b/ravenwood/bivalenttest/README.md
new file mode 100644
index 0000000..7153555
--- /dev/null
+++ b/ravenwood/bivalenttest/README.md
@@ -0,0 +1,3 @@
+# Ravenwood bivalent test
+
+This test contains bivalent tests for Ravenwood itself.
\ No newline at end of file
diff --git a/ravenwood/bivalenttest/jni/ravenwood_core_test_jni.cpp b/ravenwood/bivalenttest/jni/ravenwood_core_test_jni.cpp
new file mode 100644
index 0000000..5e66b29
--- /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 0000000..3b106da
--- /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/bivalenttest/test/com/android/platform/test/ravenwood/bivalenttest/RavenwoodRuleTest.java b/ravenwood/bivalenttest/test/com/android/platform/test/ravenwood/bivalenttest/RavenwoodRuleTest.java
new file mode 100644
index 0000000..4b650b4
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/platform/test/ravenwood/bivalenttest/RavenwoodRuleTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.platform.test.ravenwood.bivalenttest;
+
+import android.platform.test.annotations.DisabledOnNonRavenwood;
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodRuleTest {
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+ @Test
+ @DisabledOnRavenwood
+ public void testDeviceOnly() {
+ Assert.assertFalse(RavenwoodRule.isOnRavenwood());
+ }
+
+ @Test
+ @DisabledOnNonRavenwood
+ public void testRavenwoodOnly() {
+ Assert.assertTrue(RavenwoodRule.isOnRavenwood());
+ }
+
+ // TODO: Add more tests
+}
diff --git a/ravenwood/coretest/Android.bp b/ravenwood/coretest/Android.bp
index 9b7f8f7..a78c5c1 100644
--- a/ravenwood/coretest/Android.bp
+++ b/ravenwood/coretest/Android.bp
@@ -12,9 +12,9 @@
static_libs: [
"androidx.annotation_annotation",
+ "androidx.test.ext.junit",
"androidx.test.rules",
],
-
srcs: [
"test/**/*.java",
],
diff --git a/ravenwood/coretest/README.md b/ravenwood/coretest/README.md
new file mode 100644
index 0000000..b60bfbf
--- /dev/null
+++ b/ravenwood/coretest/README.md
@@ -0,0 +1,3 @@
+# Ravenwood core test
+
+This test contains (non-bivalent) tests for Ravenwood itself -- e.g. tests for the ravenwood rules.
\ No newline at end of file
diff --git a/ravenwood/coretest/test/com/android/platform/test/ravenwood/coretest/RavenwoodTestRunnerValidationTest.java b/ravenwood/coretest/test/com/android/platform/test/ravenwood/coretest/RavenwoodTestRunnerValidationTest.java
index e58c282..2cd585f 100644
--- a/ravenwood/coretest/test/com/android/platform/test/ravenwood/coretest/RavenwoodTestRunnerValidationTest.java
+++ b/ravenwood/coretest/test/com/android/platform/test/ravenwood/coretest/RavenwoodTestRunnerValidationTest.java
@@ -17,7 +17,7 @@
import android.platform.test.ravenwood.RavenwoodRule;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.runner.AndroidJUnit4; // Intentionally use the deprecated one.
import org.junit.Assume;
import org.junit.Rule;
diff --git a/ravenwood/framework-minus-apex-ravenwood-policies.txt b/ravenwood/framework-minus-apex-ravenwood-policies.txt
index 0229611..2eef0cd 100644
--- a/ravenwood/framework-minus-apex-ravenwood-policies.txt
+++ b/ravenwood/framework-minus-apex-ravenwood-policies.txt
@@ -89,6 +89,7 @@
# Internals
class com.android.internal.util.FileRotator stubclass
+class com.android.internal.util.FastXmlSerializer stubclass
class com.android.internal.util.HexDump stubclass
class com.android.internal.util.MessageUtils stubclass
class com.android.internal.util.Preconditions stubclass
diff --git a/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java b/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java
new file mode 100644
index 0000000..8ca34ba
--- /dev/null
+++ b/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Tests marked with this annotation are only executed when running on Ravenwood, but not
+ * on a device.
+ *
+ * This is basically equivalent to the opposite of {@link DisabledOnRavenwood}, but in order to
+ * avoid complex structure, and there's no equivalent to the opposite {@link EnabledOnRavenwood},
+ * which means if a test class has this annotation, you can't negate it in subclasses or
+ * on a per-method basis.
+ *
+ * The {@code RAVENWOOD_RUN_DISABLED_TESTS} environmental variable won't work because it won't be
+ * propagated to the device. (We may support it in the future, possibly using a debug. sysprop.)
+ *
+ * @hide
+ */
+@Inherited
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface DisabledOnNonRavenwood {
+ /**
+ * General free-form description of why this test is being ignored.
+ */
+ String reason() default "";
+}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
index 8d76970..9a4d488 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
@@ -18,6 +18,7 @@
import static android.platform.test.ravenwood.RavenwoodRule.ENABLE_PROBE_IGNORED;
import static android.platform.test.ravenwood.RavenwoodRule.IS_ON_RAVENWOOD;
+import static android.platform.test.ravenwood.RavenwoodRule.shouldEnableOnDevice;
import static android.platform.test.ravenwood.RavenwoodRule.shouldEnableOnRavenwood;
import static android.platform.test.ravenwood.RavenwoodRule.shouldStillIgnoreInProbeIgnoreMode;
@@ -42,6 +43,7 @@
public Statement apply(Statement base, Description description) {
// No special treatment when running outside Ravenwood; run tests as-is
if (!IS_ON_RAVENWOOD) {
+ Assume.assumeTrue(shouldEnableOnDevice(description));
return base;
}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index 764573d..b90f112 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -22,6 +22,7 @@
import static org.junit.Assert.fail;
+import android.platform.test.annotations.DisabledOnNonRavenwood;
import android.platform.test.annotations.DisabledOnRavenwood;
import android.platform.test.annotations.EnabledOnRavenwood;
import android.platform.test.annotations.IgnoreUnderRavenwood;
@@ -211,6 +212,15 @@
return IS_ON_RAVENWOOD;
}
+ static boolean shouldEnableOnDevice(Description description) {
+ if (description.isTest()) {
+ if (description.getAnnotation(DisabledOnNonRavenwood.class) != null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* Determine if the given {@link Description} should be enabled when running on the
* Ravenwood test environment.
@@ -271,6 +281,7 @@
public Statement apply(Statement base, Description description) {
// No special treatment when running outside Ravenwood; run tests as-is
if (!IS_ON_RAVENWOOD) {
+ Assume.assumeTrue(shouldEnableOnDevice(description));
return base;
}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
new file mode 100644
index 0000000..b736a76
--- /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/ravenwood/minimum-test/README.md b/ravenwood/minimum-test/README.md
new file mode 100644
index 0000000..6b0abe9
--- /dev/null
+++ b/ravenwood/minimum-test/README.md
@@ -0,0 +1,3 @@
+# Ravenwood minimum test
+
+This directory contains a minimum possible ravenwood test -- no extra dependencies, etc.
\ No newline at end of file
diff --git a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java b/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
index 03cfad5..b477117 100644
--- a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
+++ b/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
@@ -28,7 +28,7 @@
@RunWith(AndroidJUnit4.class)
public class RavenwoodMinimumTest {
@Rule
- public RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
.setProcessApp()
.build();
diff --git a/ravenwood/mockito/AndroidTest.xml b/ravenwood/mockito/AndroidTest.xml
index 96bc275..5ba9b1f 100644
--- a/ravenwood/mockito/AndroidTest.xml
+++ b/ravenwood/mockito/AndroidTest.xml
@@ -22,8 +22,6 @@
<option name="test-file-name" value="RavenwoodMockitoTest_device.apk" />
</target_preparer>
- <option name="test-tag" value="FrameworksMockingServicesTests" />
-
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="com.android.ravenwood.mockitotest" />
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
diff --git a/ravenwood/mockito/README.md b/ravenwood/mockito/README.md
new file mode 100644
index 0000000..4ceb795
--- /dev/null
+++ b/ravenwood/mockito/README.md
@@ -0,0 +1,3 @@
+# Ravenwood mockito test
+
+This directory contains a sample bivalent test using Mockito.
\ No newline at end of file
diff --git a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING
index 5a548fd..82ab098 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/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index cd45b03..b8f6b3f 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -480,9 +480,14 @@
/**
* The available ANR timers.
*/
+ // ActivityManagerConstants.SERVICE_TIMEOUT/ActivityManagerConstants.SERVICE_BACKGROUND_TIMEOUT
private final ProcessAnrTimer mActiveServiceAnrTimer;
+ // see ServiceRecord$ShortFgsInfo#getAnrTime()
private final ServiceAnrTimer mShortFGSAnrTimer;
+ // ActivityManagerConstants.DEFAULT_SERVICE_START_FOREGROUND_TIMEOUT_MS
private final ServiceAnrTimer mServiceFGAnrTimer;
+ // see ServiceRecord#getEarliestStopTypeAndTime()
+ private final ServiceAnrTimer mFGSAnrTimer;
// allowlisted packageName.
ArraySet<String> mAllowListWhileInUsePermissionInFgs = new ArraySet<>();
@@ -744,10 +749,13 @@
"SERVICE_TIMEOUT");
this.mShortFGSAnrTimer = new ServiceAnrTimer(service,
ActivityManagerService.SERVICE_SHORT_FGS_ANR_TIMEOUT_MSG,
- "FGS_TIMEOUT");
+ "SHORT_FGS_TIMEOUT");
this.mServiceFGAnrTimer = new ServiceAnrTimer(service,
ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG,
"SERVICE_FOREGROUND_TIMEOUT");
+ this.mFGSAnrTimer = new ServiceAnrTimer(service,
+ ActivityManagerService.SERVICE_FGS_ANR_TIMEOUT_MSG,
+ "FGS_TIMEOUT");
}
void systemServicesReady() {
@@ -1570,6 +1578,7 @@
}
maybeStopShortFgsTimeoutLocked(service);
+ maybeStopFgsTimeoutLocked(service);
final int uid = service.appInfo.uid;
final String packageName = service.name.getPackageName();
@@ -1758,6 +1767,7 @@
}
maybeStopShortFgsTimeoutLocked(r);
+ maybeStopFgsTimeoutLocked(r);
final int uid = r.appInfo.uid;
final String packageName = r.name.getPackageName();
@@ -2243,6 +2253,8 @@
// Whether to extend the SHORT_SERVICE time out.
boolean extendShortServiceTimeout = false;
+ // Whether to extend the timeout for a time-limited FGS type.
+ boolean extendFgsTimeout = false;
// Whether setFgsRestrictionLocked() is called in here. Only used for logging.
boolean fgsRestrictionRecalculated = false;
@@ -2287,6 +2299,19 @@
final boolean isOldTypeShortFgsAndTimedOut =
r.shouldTriggerShortFgsTimeout(nowUptime);
+ // Calling startForeground on a FGS type which has a time limit will only be
+ // allowed if the app is in a state where it can normally start another FGS.
+ // The timeout will behave as follows:
+ // A) <TIME_LIMITED_TYPE> -> another <TIME_LIMITED_TYPE>
+ // - If the start succeeds, the timeout is reset.
+ // B) <TIME_LIMITED_TYPE> -> non-time-limited type
+ // - If the start succeeds, the timeout will stop.
+ // C) non-time-limited type -> <TIME_LIMITED_TYPE>
+ // - If the start succeeds, the timeout will start.
+ final boolean isOldTypeTimeLimited = r.isFgsTimeLimited();
+ final boolean isNewTypeTimeLimited =
+ r.canFgsTypeTimeOut(foregroundServiceType);
+
// If true, we skip the BFSL check.
boolean bypassBfslCheck = false;
@@ -2355,6 +2380,35 @@
// "if (r.mAllowStartForeground == REASON_DENIED...)" block below.
}
}
+ } else if (r.isForeground && isOldTypeTimeLimited) {
+
+ // See if the app could start an FGS or not.
+ r.clearFgsAllowStart();
+ setFgsRestrictionLocked(r.serviceInfo.packageName, r.app.getPid(),
+ r.appInfo.uid, r.intent.getIntent(), r, r.userId,
+ BackgroundStartPrivileges.NONE, false /* isBindService */);
+ fgsRestrictionRecalculated = true;
+
+ final boolean fgsStartAllowed = !isBgFgsRestrictionEnabledForService
+ || r.isFgsAllowedStart();
+
+ if (fgsStartAllowed) {
+ if (isNewTypeTimeLimited) {
+ // Note: in the future, we may want to look into metrics to see if
+ // apps are constantly switching between a time-limited type and a
+ // non-time-limited type or constantly calling startForeground()
+ // opportunistically on the same type to gain runtime and apply the
+ // stricter timeout. For now, always extend the timeout if the app
+ // is in a state where it's allowed to start a FGS.
+ extendFgsTimeout = true;
+ } else {
+ // FGS type is changing from a time-restricted type to one without
+ // a time limit so proceed as normal.
+ // The timeout will stop later, in maybeUpdateFgsTrackingLocked().
+ }
+ } else {
+ // This case will be handled in the BFSL check below.
+ }
} else if (r.mStartForegroundCount == 0) {
/*
If the service was started with startService(), not
@@ -2596,6 +2650,7 @@
maybeUpdateShortFgsTrackingLocked(r,
extendShortServiceTimeout);
+ maybeUpdateFgsTrackingLocked(r, extendFgsTimeout);
} else {
if (DEBUG_FOREGROUND_SERVICE) {
Slog.d(TAG, "Suppressing startForeground() for FAS " + r);
@@ -2631,6 +2686,7 @@
}
maybeStopShortFgsTimeoutLocked(r);
+ maybeStopFgsTimeoutLocked(r);
// Adjust notification handling before setting isForeground to false, because
// that state is relevant to the notification policy side.
@@ -3608,6 +3664,116 @@
}
}
+ void onFgsTimeout(ServiceRecord sr) {
+ synchronized (mAm) {
+ final long nowUptime = SystemClock.uptimeMillis();
+ final int fgsType = sr.getTimedOutFgsType(nowUptime);
+ if (fgsType == -1) {
+ mFGSAnrTimer.discard(sr);
+ return;
+ }
+ Slog.e(TAG_SERVICE, "FGS (" + ServiceInfo.foregroundServiceTypeToLabel(fgsType)
+ + ") timed out: " + sr);
+ mFGSAnrTimer.accept(sr);
+ traceInstant("FGS timed out: ", sr);
+
+ logFGSStateChangeLocked(sr,
+ FOREGROUND_SERVICE_STATE_CHANGED__STATE__TIMED_OUT,
+ nowUptime > sr.mFgsEnterTime ? (int) (nowUptime - sr.mFgsEnterTime) : 0,
+ FGS_STOP_REASON_UNKNOWN,
+ FGS_TYPE_POLICY_CHECK_UNKNOWN,
+ FOREGROUND_SERVICE_STATE_CHANGED__FGS_START_API__FGSSTARTAPI_NA,
+ false /* fgsRestrictionRecalculated */
+ );
+ try {
+ sr.app.getThread().scheduleTimeoutServiceForType(sr, sr.getLastStartId(), fgsType);
+ } catch (RemoteException e) {
+ Slog.w(TAG_SERVICE, "Exception from scheduleTimeoutServiceForType: " + e);
+ }
+
+ // ANR the service after giving the service some time to clean up.
+ // ServiceRecord.getEarliestStopTypeAndTime() is an absolute time with a reference that
+ // is not "now". Compute the time from "now" when starting the anr timer.
+ final long anrTime = sr.getEarliestStopTypeAndTime().second
+ + mAm.mConstants.mFgsAnrExtraWaitDuration - SystemClock.uptimeMillis();
+ mFGSAnrTimer.start(sr, anrTime);
+ }
+ }
+
+ private void maybeUpdateFgsTrackingLocked(ServiceRecord sr, boolean extendTimeout) {
+ if (!sr.isFgsTimeLimited()) {
+ // Reset timers if they exist.
+ sr.setIsFgsTimeLimited(false);
+ mFGSAnrTimer.cancel(sr);
+ mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr);
+ return;
+ }
+
+ if (extendTimeout || !sr.wasFgsPreviouslyTimeLimited()) {
+ traceInstant("FGS start: ", sr);
+ sr.setIsFgsTimeLimited(true);
+
+ // We'll restart the timeout.
+ mFGSAnrTimer.cancel(sr);
+ mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr);
+
+ final Message msg = mAm.mHandler.obtainMessage(
+ ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr);
+ mAm.mHandler.sendMessageAtTime(msg, sr.getEarliestStopTypeAndTime().second);
+ }
+ }
+
+ private void maybeStopFgsTimeoutLocked(ServiceRecord sr) {
+ sr.setIsFgsTimeLimited(false); // reset fgs boolean holding time-limited type state.
+ if (!sr.isFgsTimeLimited()) {
+ return; // if none of the types are time-limited, return.
+ }
+ Slog.d(TAG_SERVICE, "Stop FGS timeout: " + sr);
+ mFGSAnrTimer.cancel(sr);
+ mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr);
+ }
+
+ boolean hasServiceTimedOutLocked(ComponentName className, IBinder token) {
+ final int userId = UserHandle.getCallingUserId();
+ final long ident = mAm.mInjector.clearCallingIdentity();
+ try {
+ ServiceRecord sr = findServiceLocked(className, token, userId);
+ if (sr == null) {
+ return false;
+ }
+ final long nowUptime = SystemClock.uptimeMillis();
+ return sr.getTimedOutFgsType(nowUptime) != -1;
+ } finally {
+ mAm.mInjector.restoreCallingIdentity(ident);
+ }
+ }
+
+ void onFgsAnrTimeout(ServiceRecord sr) {
+ final long nowUptime = SystemClock.uptimeMillis();
+ final int fgsType = sr.getTimedOutFgsType(nowUptime);
+ if (fgsType == -1 || !sr.wasFgsPreviouslyTimeLimited()) {
+ return; // no timed out FGS type was found
+ }
+ final String reason = "A foreground service of type "
+ + ServiceInfo.foregroundServiceTypeToLabel(fgsType)
+ + " did not stop within a timeout: " + sr.getComponentName();
+
+ final TimeoutRecord tr = TimeoutRecord.forFgsTimeout(reason);
+
+ tr.mLatencyTracker.waitingOnAMSLockStarted();
+ synchronized (mAm) {
+ tr.mLatencyTracker.waitingOnAMSLockEnded();
+
+ Slog.e(TAG_SERVICE, "FGS ANR'ed: " + sr);
+ traceInstant("FGS ANR: ", sr);
+ mAm.appNotResponding(sr.app, tr);
+
+ // TODO: Can we close the ANR dialog here, if it's still shown? Currently, the ANR
+ // dialog really doesn't remember the "cause" (especially if there have been multiple
+ // ANRs), so it's not doable.
+ }
+ }
+
private void updateAllowlistManagerLocked(ProcessServiceRecord psr) {
psr.mAllowlistManager = false;
for (int i = psr.numberOfRunningServices() - 1; i >= 0; i--) {
@@ -3621,6 +3787,7 @@
private void stopServiceAndUpdateAllowlistManagerLocked(ServiceRecord service) {
maybeStopShortFgsTimeoutLocked(service);
+ maybeStopFgsTimeoutLocked(service);
final ProcessServiceRecord psr = service.app.mServices;
psr.stopService(service);
psr.updateBoundClientUids();
@@ -5893,6 +6060,7 @@
Slog.w(TAG_SERVICE, "Short FGS brought down without stopping: " + r);
maybeStopShortFgsTimeoutLocked(r);
}
+ maybeStopFgsTimeoutLocked(r);
// Report to all of the connections that the service is no longer
// available.
@@ -6015,6 +6183,7 @@
final boolean exitingFg = r.isForeground;
if (exitingFg) {
maybeStopShortFgsTimeoutLocked(r);
+ maybeStopFgsTimeoutLocked(r);
decActiveForegroundAppLocked(smap, r);
synchronized (mAm.mProcessStats.mLock) {
ServiceState stracker = r.getTracker();
@@ -8643,7 +8812,7 @@
event = EventLogTags.AM_FOREGROUND_SERVICE_DENIED;
} else if (state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__TIMED_OUT) {
event = EventLogTags.AM_FOREGROUND_SERVICE_TIMED_OUT;
- }else {
+ } else {
// Unknown event.
return;
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index 72e62c3..d97731c 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -1052,6 +1052,27 @@
public volatile long mShortFgsProcStateExtraWaitDuration =
DEFAULT_SHORT_FGS_PROC_STATE_EXTRA_WAIT_DURATION;
+ /** Timeout for a mediaProcessing FGS, in milliseconds. */
+ private static final String KEY_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION =
+ "media_processing_fgs_timeout_duration";
+
+ /** @see #KEY_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION */
+ static final long DEFAULT_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION = 6 * 60 * 60_000; // 6 hours
+
+ /** @see #KEY_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION */
+ public volatile long mMediaProcessingFgsTimeoutDuration =
+ DEFAULT_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION;
+
+ /** Timeout for a dataSync FGS, in milliseconds. */
+ private static final String KEY_DATA_SYNC_FGS_TIMEOUT_DURATION =
+ "data_sync_fgs_timeout_duration";
+
+ /** @see #KEY_DATA_SYNC_FGS_TIMEOUT_DURATION */
+ static final long DEFAULT_DATA_SYNC_FGS_TIMEOUT_DURATION = 6 * 60 * 60_000; // 6 hours
+
+ /** @see #KEY_DATA_SYNC_FGS_TIMEOUT_DURATION */
+ public volatile long mDataSyncFgsTimeoutDuration = DEFAULT_DATA_SYNC_FGS_TIMEOUT_DURATION;
+
/**
* If enabled, when starting an application, the system will wait for a
* {@link ActivityManagerService#finishAttachApplication} from the app before scheduling
@@ -1082,6 +1103,20 @@
public volatile long mShortFgsAnrExtraWaitDuration =
DEFAULT_SHORT_FGS_ANR_EXTRA_WAIT_DURATION;
+ /**
+ * If a service of a timeout-enforced type doesn't finish within this duration after its
+ * timeout, then we'll declare an ANR.
+ * i.e. if the time limit for a type is 1 hour, and this extra duration is 10 seconds, then
+ * the app will be ANR'ed 1 hour and 10 seconds after it started.
+ */
+ private static final String KEY_FGS_ANR_EXTRA_WAIT_DURATION = "fgs_anr_extra_wait_duration";
+
+ /** @see #KEY_FGS_ANR_EXTRA_WAIT_DURATION */
+ static final long DEFAULT_FGS_ANR_EXTRA_WAIT_DURATION = 10_000;
+
+ /** @see #KEY_FGS_ANR_EXTRA_WAIT_DURATION */
+ public volatile long mFgsAnrExtraWaitDuration = DEFAULT_FGS_ANR_EXTRA_WAIT_DURATION;
+
/** @see #KEY_USE_TIERED_CACHED_ADJ */
public boolean USE_TIERED_CACHED_ADJ = DEFAULT_USE_TIERED_CACHED_ADJ;
@@ -1264,9 +1299,18 @@
case KEY_SHORT_FGS_PROC_STATE_EXTRA_WAIT_DURATION:
updateShortFgsProcStateExtraWaitDuration();
break;
+ case KEY_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION:
+ updateMediaProcessingFgsTimeoutDuration();
+ break;
+ case KEY_DATA_SYNC_FGS_TIMEOUT_DURATION:
+ updateDataSyncFgsTimeoutDuration();
+ break;
case KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION:
updateShortFgsAnrExtraWaitDuration();
break;
+ case KEY_FGS_ANR_EXTRA_WAIT_DURATION:
+ updateFgsAnrExtraWaitDuration();
+ break;
case KEY_PROACTIVE_KILLS_ENABLED:
updateProactiveKillsEnabled();
break;
@@ -2064,6 +2108,27 @@
DEFAULT_SHORT_FGS_ANR_EXTRA_WAIT_DURATION);
}
+ private void updateMediaProcessingFgsTimeoutDuration() {
+ mMediaProcessingFgsTimeoutDuration = DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ KEY_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION,
+ DEFAULT_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION);
+ }
+
+ private void updateDataSyncFgsTimeoutDuration() {
+ mDataSyncFgsTimeoutDuration = DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ KEY_DATA_SYNC_FGS_TIMEOUT_DURATION,
+ DEFAULT_DATA_SYNC_FGS_TIMEOUT_DURATION);
+ }
+
+ private void updateFgsAnrExtraWaitDuration() {
+ mFgsAnrExtraWaitDuration = DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ KEY_FGS_ANR_EXTRA_WAIT_DURATION,
+ DEFAULT_FGS_ANR_EXTRA_WAIT_DURATION);
+ }
+
private void updateEnableWaitForFinishAttachApplication() {
mEnableWaitForFinishAttachApplication = DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
@@ -2295,6 +2360,13 @@
pw.print(" "); pw.print(KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION);
pw.print("="); pw.println(mShortFgsAnrExtraWaitDuration);
+ pw.print(" "); pw.print(KEY_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION);
+ pw.print("="); pw.println(mMediaProcessingFgsTimeoutDuration);
+ pw.print(" "); pw.print(KEY_DATA_SYNC_FGS_TIMEOUT_DURATION);
+ pw.print("="); pw.println(mDataSyncFgsTimeoutDuration);
+ pw.print(" "); pw.print(KEY_FGS_ANR_EXTRA_WAIT_DURATION);
+ pw.print("="); pw.println(mFgsAnrExtraWaitDuration);
+
pw.print(" "); pw.print(KEY_USE_TIERED_CACHED_ADJ);
pw.print("="); pw.println(USE_TIERED_CACHED_ADJ);
pw.print(" "); pw.print(KEY_TIERED_CACHED_ADJ_DECAY_TIME);
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 2750344..bfdcb95 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -1702,6 +1702,8 @@
static final int REMOVE_UID_FROM_OBSERVER_MSG = 81;
static final int BIND_APPLICATION_TIMEOUT_SOFT_MSG = 82;
static final int BIND_APPLICATION_TIMEOUT_HARD_MSG = 83;
+ static final int SERVICE_FGS_TIMEOUT_MSG = 84;
+ static final int SERVICE_FGS_ANR_TIMEOUT_MSG = 85;
static final int FIRST_BROADCAST_QUEUE_MSG = 200;
@@ -2064,6 +2066,12 @@
case BIND_APPLICATION_TIMEOUT_HARD_MSG: {
handleBindApplicationTimeoutHard((ProcessRecord) msg.obj);
} break;
+ case SERVICE_FGS_TIMEOUT_MSG: {
+ mServices.onFgsTimeout((ServiceRecord) msg.obj);
+ } break;
+ case SERVICE_FGS_ANR_TIMEOUT_MSG: {
+ mServices.onFgsAnrTimeout((ServiceRecord) msg.obj);
+ } break;
}
}
}
@@ -13794,6 +13802,13 @@
}
@Override
+ public boolean hasServiceTimeLimitExceeded(ComponentName className, IBinder token) {
+ synchronized (this) {
+ return mServices.hasServiceTimedOutLocked(className, token);
+ }
+ }
+
+ @Override
public int handleIncomingUser(int callingPid, int callingUid, int userId, boolean allowAll,
boolean requireFull, String name, String callerPackage) {
return mUserController.handleIncomingUser(callingPid, callingUid, userId, allowAll,
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 2771572..3c8d7fc 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -56,6 +56,7 @@
import android.os.UserHandle;
import android.provider.Settings;
import android.util.ArrayMap;
+import android.util.Pair;
import android.util.Slog;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
@@ -236,6 +237,8 @@
boolean mFgsNotificationShown;
// Whether FGS package has permissions to show notifications.
boolean mFgsHasNotificationPermission;
+ // Whether the FGS contains a type that is time limited.
+ private boolean mFgsIsTimeLimited;
// allow the service becomes foreground service? Service started from background may not be
// allowed to become a foreground service.
@@ -915,6 +918,7 @@
pw.print(prefix); pw.print("isForeground="); pw.print(isForeground);
pw.print(" foregroundId="); pw.print(foregroundId);
pw.printf(" types=%08X", foregroundServiceType);
+ pw.print(" fgsHasTimeLimitedType="); pw.print(mFgsIsTimeLimited);
pw.print(" foregroundNoti="); pw.println(foregroundNoti);
if (isShortFgs() && mShortFgsInfo != null) {
@@ -1789,6 +1793,83 @@
+ " " + (mShortFgsInfo == null ? "" : mShortFgsInfo.getDescription());
}
+ /**
+ * @return true if one of the types of this FGS has a time limit.
+ */
+ public boolean isFgsTimeLimited() {
+ return startRequested && isForeground && canFgsTypeTimeOut(foregroundServiceType);
+ }
+
+ /**
+ * Called when a FGS with a time-limited type starts ({@code true}) or stops ({@code false}).
+ */
+ public void setIsFgsTimeLimited(boolean fgsIsTimeLimited) {
+ this.mFgsIsTimeLimited = fgsIsTimeLimited;
+ }
+
+ /**
+ * @return whether {@link #mFgsIsTimeLimited} was previously set or not.
+ */
+ public boolean wasFgsPreviouslyTimeLimited() {
+ return mFgsIsTimeLimited;
+ }
+
+ /**
+ * @return the FGS type if the service has reached its time limit, otherwise -1.
+ */
+ public int getTimedOutFgsType(long nowUptime) {
+ if (!isAppAlive() || !isFgsTimeLimited()) {
+ return -1;
+ }
+
+ final Pair<Integer, Long> fgsTypeAndStopTime = getEarliestStopTypeAndTime();
+ if (fgsTypeAndStopTime.first != -1 && fgsTypeAndStopTime.second <= nowUptime) {
+ return fgsTypeAndStopTime.first;
+ }
+ return -1; // no fgs type exceeded time limit
+ }
+
+ /**
+ * @return a {@code Pair<fgs_type, stop_time>}, representing the earliest time at which the FGS
+ * should be stopped (fgs start time + time limit for most restrictive type)
+ */
+ Pair<Integer, Long> getEarliestStopTypeAndTime() {
+ int fgsType = -1;
+ long timeout = 0;
+ if ((foregroundServiceType & ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING)
+ == ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING) {
+ fgsType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING;
+ timeout = ams.mConstants.mMediaProcessingFgsTimeoutDuration;
+ }
+ if ((foregroundServiceType & ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
+ == ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) {
+ // update the timeout and type if this type has a more restrictive time limit
+ if (timeout == 0 || ams.mConstants.mDataSyncFgsTimeoutDuration < timeout) {
+ fgsType = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC;
+ timeout = ams.mConstants.mDataSyncFgsTimeoutDuration;
+ }
+ }
+ // Add the logic for time limits introduced in the future for other fgs types here.
+ return Pair.create(fgsType, timeout == 0 ? 0 : (mFgsEnterTime + timeout));
+ }
+
+ /**
+ * Check if the given types contain a type which is time restricted.
+ */
+ boolean canFgsTypeTimeOut(int fgsType) {
+ // The below conditionals are not simplified on purpose to help with readability.
+ if ((fgsType & ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING)
+ == ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING) {
+ return true;
+ }
+ if ((fgsType & ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
+ == ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) {
+ return true;
+ }
+ // Additional types which have time limits should be added here in the future.
+ return false;
+ }
+
private boolean isAppAlive() {
if (app == null) {
return false;
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 96c6be8..55ac4cf 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -1350,41 +1350,57 @@
}
/**
- * For mDelayUserDataLocking mode, storage once unlocked is kept unlocked.
- * Total number of unlocked user storage is limited by mMaxRunningUsers.
- * If there are more unlocked users, evict and lock the least recently stopped user and
- * lock that user's data. Regardless of the mode, ephemeral user is always locked
- * immediately.
+ * Returns which user, if any, should be locked when the given user is stopped.
+ *
+ * For typical (non-mDelayUserDataLocking) devices and users, this will be the provided user.
+ *
+ * However, for some devices or users (based on {@link #canDelayDataLockingForUser(int)}),
+ * storage once unlocked is kept unlocked, even after the user is stopped, so the user to be
+ * locked (if any) may differ.
+ *
+ * For mDelayUserDataLocking devices, the total number of unlocked user storage is limited
+ * (currently by mMaxRunningUsers). If there are more unlocked users, evict and lock the least
+ * recently stopped user and lock that user's data.
+ *
+ * Regardless of the mode, ephemeral user is always locked immediately.
*
* @return user id to lock. UserHandler.USER_NULL will be returned if no user should be locked.
*/
@GuardedBy("mLock")
private int updateUserToLockLU(@UserIdInt int userId, boolean allowDelayedLocking) {
- int userIdToLock = userId;
- // TODO: Decouple the delayed locking flows from mMaxRunningUsers or rename the property to
- // state maximum running unlocked users specifically
- if (canDelayDataLockingForUser(userIdToLock) && allowDelayedLocking
- && !getUserInfo(userId).isEphemeral()
- && !hasUserRestriction(UserManager.DISALLOW_RUN_IN_BACKGROUND, userId)) {
+ if (!canDelayDataLockingForUser(userId)
+ || !allowDelayedLocking
+ || getUserInfo(userId).isEphemeral()
+ || hasUserRestriction(UserManager.DISALLOW_RUN_IN_BACKGROUND, userId)) {
+ return userId;
+ }
+
+ // Once we reach here, we are in a delayed locking scenario.
+ // Now, no user will be locked, unless the device's policy dictates we should based on the
+ // maximum of such users allowed for the device.
+ if (mDelayUserDataLocking) {
// arg should be object, not index
mLastActiveUsersForDelayedLocking.remove((Integer) userId);
mLastActiveUsersForDelayedLocking.add(0, userId);
int totalUnlockedUsers = mStartedUsers.size()
+ mLastActiveUsersForDelayedLocking.size();
+ // TODO: Decouple the delayed locking flows from mMaxRunningUsers. These users aren't
+ // running so this calculation shouldn't be based on this parameter. Also note that
+ // that if these devices ever support background running users (such as profiles), the
+ // implementation is incorrect since starting such users can cause the max to be
+ // exceeded.
if (totalUnlockedUsers > mMaxRunningUsers) { // should lock a user
- userIdToLock = mLastActiveUsersForDelayedLocking.get(
+ final int userIdToLock = mLastActiveUsersForDelayedLocking.get(
mLastActiveUsersForDelayedLocking.size() - 1);
mLastActiveUsersForDelayedLocking
.remove(mLastActiveUsersForDelayedLocking.size() - 1);
- Slogf.i(TAG, "finishUserStopped, stopping user:" + userId
- + " lock user:" + userIdToLock);
- } else {
- Slogf.i(TAG, "finishUserStopped, user:" + userId + ", skip locking");
- // do not lock
- userIdToLock = UserHandle.USER_NULL;
+ Slogf.i(TAG, "finishUserStopped: should stop user " + userId
+ + " but should lock user " + userIdToLock);
+ return userIdToLock;
}
}
- return userIdToLock;
+ Slogf.i(TAG, "finishUserStopped: should stop user " + userId + " but without any locking");
+ return UserHandle.USER_NULL;
}
/**
diff --git a/services/core/java/com/android/server/appop/LegacyAppOpStateParser.java b/services/core/java/com/android/server/appop/LegacyAppOpStateParser.java
index a6d5050..9ed3a99 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 @@
*/
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 @@
}
}
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 53255a0..c59f4f7 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.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 @@
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 8d76731..3b5fa7f 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 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 @@
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 1bf2aef..4703efb 100644
--- a/services/core/java/com/android/server/biometrics/OWNERS
+++ b/services/core/java/com/android/server/biometrics/OWNERS
@@ -2,7 +2,6 @@
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/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
index cb15abc..cd064ae 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
@@ -19,11 +19,11 @@
import static android.Manifest.permission.CONTROL_DEVICE_STATE;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.hardware.devicestate.DeviceState.FLAG_CANCEL_OVERRIDE_REQUESTS;
import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
-import static com.android.server.devicestate.DeviceState.FLAG_CANCEL_OVERRIDE_REQUESTS;
import static com.android.server.devicestate.OverrideRequest.OVERRIDE_REQUEST_TYPE_BASE_STATE;
import static com.android.server.devicestate.OverrideRequest.OVERRIDE_REQUEST_TYPE_EMULATED_STATE;
import static com.android.server.devicestate.OverrideRequestController.FLAG_POWER_SAVE_ENABLED;
@@ -40,6 +40,7 @@
import android.app.ActivityManagerInternal;
import android.app.IProcessObserver;
import android.content.Context;
+import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateInfo;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateManagerInternal;
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java
index 8c6068d..02c9bb3 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateRequest;
import android.os.Binder;
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
index d5945f4..65b393a 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
@@ -21,6 +21,7 @@
import android.annotation.IntDef;
import android.annotation.IntRange;
+import android.hardware.devicestate.DeviceState;
import android.util.Dumpable;
import java.lang.annotation.Retention;
diff --git a/services/core/java/com/android/server/devicestate/OverrideRequest.java b/services/core/java/com/android/server/devicestate/OverrideRequest.java
index 20485c1..d92629f 100644
--- a/services/core/java/com/android/server/devicestate/OverrideRequest.java
+++ b/services/core/java/com/android/server/devicestate/OverrideRequest.java
@@ -18,6 +18,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateRequest;
import android.os.IBinder;
diff --git a/services/core/java/com/android/server/devicestate/OverrideRequestController.java b/services/core/java/com/android/server/devicestate/OverrideRequestController.java
index f5f2fa8..6c3fd83d 100644
--- a/services/core/java/com/android/server/devicestate/OverrideRequestController.java
+++ b/services/core/java/com/android/server/devicestate/OverrideRequestController.java
@@ -18,6 +18,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateRequest;
import android.os.IBinder;
import android.util.Slog;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
index c8c0482..a100fe0 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 @@
@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 @@
/**
* 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 @@
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 b8a63cd..5db18ad 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.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.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.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 @@
}
@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 @@
// 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 @@
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 {
@@ -3560,8 +3576,7 @@
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_HAS_IME);
mCurStatsToken = null;
- if (!Flags.useHandwritingListenerForTooltype()
- && lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) {
+ if (lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) {
curMethod.updateEditorToolType(lastClickToolType);
}
mVisibilityApplier.performShowIme(windowToken, statsToken,
@@ -5989,7 +6004,23 @@
@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 5d49863..48da270 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 393e7ef..34bb219 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 @@
@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 @@
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 @@
@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 7fabdf2..8cb5cef 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 @@
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 @@
}
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 2cd8fe0..f60f55c 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 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 @@
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 @@
.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 @@
}
}
+ 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 3507d2d..ea4e67a 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_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.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.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.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 @@
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 @@
}
/**
+ * 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 @@
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 @@
}
}
+ 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 @@
}
});
}
+
+ 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 @@
mGroupHelper.onNotificationRemoved(r.getSbn());
}
});
+ if (callstyleCallbackApi()) {
+ notifyCallNotificationEventListenerOnRemoved(r);
+ }
}
if (Flags.refactorAttentionHelper()) {
@@ -11707,6 +11833,10 @@
mNotificationRecordLogger.logNotificationPosted(report);
}
});
+
+ if (callstyleCallbackApi()) {
+ notifyCallNotificationEventListenerOnPosted(r);
+ }
}
@FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR)
@@ -12763,6 +12893,91 @@
}
}
+ @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 a90efe6..b9a267f 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 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.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.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 @@
// 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 @@
/* 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,33 +380,43 @@
}
/**
+ * 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.
+ */
+ @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 rules;
+ }
+
+ if (config.manualRule != null) {
+ // 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.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) {
- // If the config is null, return early
- if (config == null) {
- return 0;
- }
-
- 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 (config.automaticRules != null) {
- for (ZenModeConfig.ZenRule rule : config.automaticRules.values()) {
- if (rule != null && rule.isAutomaticActive()) {
- rules++;
- }
- }
- }
- return rules;
+ return activeRulesList(config).size();
}
// Determine the number of (automatic & manual) rules active after the change takes place.
@@ -412,6 +431,34 @@
}
/**
+ * 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 1c20b2d..54de197 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 @@
: 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 @@
.setShouldDisableTouch(oldEffects.shouldDisableTouch())
.setShouldMinimizeRadioUsage(oldEffects.shouldMinimizeRadioUsage())
.setShouldMaximizeDoze(oldEffects.shouldMaximizeDoze())
+ .setExtraEffects(oldEffects.getExtraEffects())
.build();
}
@@ -1311,6 +1313,9 @@
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 @@
/* 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 @@
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 @@
/* 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/IdmapManager.java b/services/core/java/com/android/server/om/IdmapManager.java
index 25a39cc..86d05d9 100644
--- a/services/core/java/com/android/server/om/IdmapManager.java
+++ b/services/core/java/com/android/server/om/IdmapManager.java
@@ -257,7 +257,7 @@
private boolean matchesActorSignature(@NonNull AndroidPackage targetPackage,
@NonNull AndroidPackage overlayPackage, int userId) {
String targetOverlayableName = overlayPackage.getOverlayTargetOverlayableName();
- if (targetOverlayableName != null) {
+ if (targetOverlayableName != null && !mPackageManager.getNamedActors().isEmpty()) {
try {
OverlayableInfo overlayableInfo = mPackageManager.getOverlayableForTarget(
targetPackage.getPackageName(), targetOverlayableName, userId);
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index b9464d9..8729522 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 @@
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 @@
// 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 @@
*/
@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 @@
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 @@
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 972c78d..c1b6ccc 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 @@
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 eae614a..b8b49f3e 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.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 @@
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/AppDataHelper.java b/services/core/java/com/android/server/pm/AppDataHelper.java
index 1dd7905..18ba2cf 100644
--- a/services/core/java/com/android/server/pm/AppDataHelper.java
+++ b/services/core/java/com/android/server/pm/AppDataHelper.java
@@ -503,21 +503,24 @@
private void assertPackageStorageValid(@NonNull Computer snapshot, String volumeUuid,
String packageName, int userId) throws PackageManagerException {
final PackageStateInternal packageState = snapshot.getPackageStateInternal(packageName);
- final PackageUserStateInternal userState = packageState.getUserStateOrDefault(userId);
if (packageState == null) {
throw PackageManagerException.ofInternalError("Package " + packageName + " is unknown",
PackageManagerException.INTERNAL_ERROR_STORAGE_INVALID_PACKAGE_UNKNOWN);
- } else if (!TextUtils.equals(volumeUuid, packageState.getVolumeUuid())) {
+ }
+ if (!TextUtils.equals(volumeUuid, packageState.getVolumeUuid())) {
throw PackageManagerException.ofInternalError(
"Package " + packageName + " found on unknown volume " + volumeUuid
+ "; expected volume " + packageState.getVolumeUuid(),
PackageManagerException.INTERNAL_ERROR_STORAGE_INVALID_VOLUME_UNKNOWN);
- } else if (!userState.isInstalled() && !userState.dataExists()) {
+ }
+ final PackageUserStateInternal userState = packageState.getUserStateOrDefault(userId);
+ if (!userState.isInstalled() && !userState.dataExists()) {
throw PackageManagerException.ofInternalError(
"Package " + packageName + " not installed for user " + userId
+ " or was deleted without DELETE_KEEP_DATA",
PackageManagerException.INTERNAL_ERROR_STORAGE_INVALID_NOT_INSTALLED_FOR_USER);
- } else if (packageState.getPkg() != null
+ }
+ if (packageState.getPkg() != null
&& !shouldHaveAppStorage(packageState.getPkg())) {
throw PackageManagerException.ofInternalError(
"Package " + packageName + " shouldn't have storage",
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index db5acc2..a1dac04 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 @@
+ "; old: " + pkgSetting.getPathString() + " @ "
+ pkgSetting.getVersionCode()
+ "; new: " + parsedPackage.getPath() + " @ "
- + parsedPackage.getPath());
+ + parsedPackage.getLongVersionCode());
}
}
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 796edde..f222fe9 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 @@
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 c94c49d..a9d2858 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 @@
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 @@
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/DeviceStateProviderImpl.java b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
index afcf5a0..76952b3 100644
--- a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
+++ b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
@@ -29,6 +29,7 @@
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
+import android.hardware.devicestate.DeviceState;
import android.os.Environment;
import android.os.PowerManager;
import android.util.ArrayMap;
@@ -40,7 +41,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import com.android.server.LocalServices;
-import com.android.server.devicestate.DeviceState;
import com.android.server.devicestate.DeviceStateProvider;
import com.android.server.input.InputManagerInternal;
import com.android.server.policy.devicestate.config.Conditions;
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 00036e4..af4da81 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 @@
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 d5bc912..b69ccb3 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.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 @@
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 0000000..6ca4da2
--- /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 0de73a5..b0dcf95 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 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 @@
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 @@
}
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 f4fb1a1..1bc635b 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 @@
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.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 @@
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 @@
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 @@
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 640c9dc..dcc68a7 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 @@
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/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java
index 6a3cf43..a3e2869 100644
--- a/services/core/java/com/android/server/wm/DragDropController.java
+++ b/services/core/java/com/android/server/wm/DragDropController.java
@@ -16,6 +16,9 @@
package com.android.server.wm;
+import static android.view.View.DRAG_FLAG_GLOBAL;
+import static android.view.View.DRAG_FLAG_GLOBAL_SAME_APPLICATION;
+
import static com.android.input.flags.Flags.enablePointerChoreographer;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DRAG;
import static com.android.server.wm.WindowManagerDebugConfig.SHOW_LIGHT_TRANSACTIONS;
@@ -30,15 +33,20 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
+import android.os.RemoteException;
import android.util.Slog;
import android.view.Display;
+import android.view.DragEvent;
import android.view.IWindow;
import android.view.InputDevice;
import android.view.PointerIcon;
import android.view.SurfaceControl;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
+import android.window.IUnhandledDragCallback;
+import android.window.IUnhandledDragListener;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wm.WindowManagerInternal.IDragDropCallback;
import java.util.Objects;
@@ -59,6 +67,7 @@
static final int MSG_TEAR_DOWN_DRAG_AND_DROP_INPUT = 1;
static final int MSG_ANIMATION_END = 2;
static final int MSG_REMOVE_DRAG_SURFACE_TIMEOUT = 3;
+ static final int MSG_UNHANDLED_DROP_LISTENER_TIMEOUT = 4;
/**
* Drag state per operation.
@@ -72,6 +81,21 @@
private WindowManagerService mService;
private final Handler mHandler;
+ // The unhandled drag listener for handling cross-window drags that end with no target window
+ private IUnhandledDragListener mUnhandledDragListener;
+ private final IBinder.DeathRecipient mUnhandledDragListenerDeathRecipient =
+ new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ synchronized (mService.mGlobalLock) {
+ if (hasPendingUnhandledDropCallback()) {
+ onUnhandledDropCallback(false /* consumedByListeners */);
+ }
+ setUnhandledDragListener(null);
+ }
+ }
+ };
+
/**
* Callback which is used to sync drag state with the vendor-specific code.
*/
@@ -83,10 +107,16 @@
mHandler = new DragHandler(service, looper);
}
+ @VisibleForTesting
+ Handler getHandler() {
+ return mHandler;
+ }
+
boolean dragDropActiveLocked() {
return mDragState != null && !mDragState.isClosing();
}
+ @VisibleForTesting
boolean dragSurfaceRelinquishedToDropTarget() {
return mDragState != null && mDragState.mRelinquishDragSurfaceToDropTarget;
}
@@ -96,6 +126,32 @@
mCallback.set(callback);
}
+ /**
+ * Sets the listener for unhandled cross-window drags.
+ */
+ public void setUnhandledDragListener(IUnhandledDragListener listener) {
+ if (mUnhandledDragListener != null && mUnhandledDragListener.asBinder() != null) {
+ mUnhandledDragListener.asBinder().unlinkToDeath(
+ mUnhandledDragListenerDeathRecipient, 0);
+ }
+ mUnhandledDragListener = listener;
+ if (listener != null && listener.asBinder() != null) {
+ try {
+ mUnhandledDragListener.asBinder().linkToDeath(
+ mUnhandledDragListenerDeathRecipient, 0);
+ } catch (RemoteException e) {
+ mUnhandledDragListener = null;
+ }
+ }
+ }
+
+ /**
+ * Returns whether there is an unhandled drag listener set.
+ */
+ boolean hasUnhandledDragListener() {
+ return mUnhandledDragListener != null;
+ }
+
void sendDragStartedIfNeededLocked(WindowState window) {
mDragState.sendDragStartedIfNeededLocked(window);
}
@@ -247,6 +303,10 @@
}
}
+ /**
+ * This is called from the drop target window that received ACTION_DROP
+ * (see DragState#reportDropWindowLock()).
+ */
void reportDropResult(IWindow window, boolean consumed) {
IBinder token = window.asBinder();
if (DEBUG_DRAG) {
@@ -273,22 +333,89 @@
// so be sure to halt the timeout even if the later WindowState
// lookup fails.
mHandler.removeMessages(MSG_DRAG_END_TIMEOUT, window.asBinder());
+
WindowState callingWin = mService.windowForClientLocked(null, window, false);
if (callingWin == null) {
Slog.w(TAG_WM, "Bad result-reporting window " + window);
return; // !!! TODO: throw here?
}
- mDragState.mDragResult = consumed;
- mDragState.mRelinquishDragSurfaceToDropTarget = consumed
- && mDragState.targetInterceptsGlobalDrag(callingWin);
- mDragState.endDragLocked();
+ // If the drop was not consumed by the target window, then check if it should be
+ // consumed by the system unhandled drag listener
+ if (!consumed && notifyUnhandledDrop(mDragState.mUnhandledDropEvent,
+ "window-drop")) {
+ // If the unhandled drag listener is notified, then defer ending the drag until
+ // the listener calls back
+ return;
+ }
+
+ final boolean relinquishDragSurfaceToDropTarget =
+ consumed && mDragState.targetInterceptsGlobalDrag(callingWin);
+ mDragState.endDragLocked(consumed, relinquishDragSurfaceToDropTarget);
}
} finally {
mCallback.get().postReportDropResult();
}
}
+ /**
+ * Notifies the unhandled drag listener if needed.
+ * @return whether the listener was notified and subsequent drag completion should be deferred
+ * until the listener calls back
+ */
+ boolean notifyUnhandledDrop(DragEvent dropEvent, String reason) {
+ final boolean isLocalDrag =
+ (mDragState.mFlags & (DRAG_FLAG_GLOBAL_SAME_APPLICATION | DRAG_FLAG_GLOBAL)) == 0;
+ if (!com.android.window.flags.Flags.delegateUnhandledDrags()
+ || mUnhandledDragListener == null
+ || isLocalDrag) {
+ // Skip if the flag is disabled, there is no unhandled-drag listener, or if this is a
+ // purely local drag
+ if (DEBUG_DRAG) Slog.d(TAG_WM, "Skipping unhandled listener "
+ + "(listener=" + mUnhandledDragListener + ", flags=" + mDragState.mFlags + ")");
+ return false;
+ }
+ if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DROP to unhandled listener (" + reason + ")");
+ try {
+ // Schedule timeout for the unhandled drag listener to call back
+ sendTimeoutMessage(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT, null, DRAG_TIMEOUT_MS);
+ mUnhandledDragListener.onUnhandledDrop(dropEvent, new IUnhandledDragCallback.Stub() {
+ @Override
+ public void notifyUnhandledDropComplete(boolean consumedByListener) {
+ if (DEBUG_DRAG) Slog.d(TAG_WM, "Unhandled listener finished handling DROP");
+ synchronized (mService.mGlobalLock) {
+ onUnhandledDropCallback(consumedByListener);
+ }
+ }
+ });
+ return true;
+ } catch (RemoteException e) {
+ Slog.e(TAG_WM, "Failed to call unhandled drag listener", e);
+ return false;
+ }
+ }
+
+ /**
+ * Called when the unhandled drag listener has completed handling the drop
+ * (if it was notififed).
+ */
+ @VisibleForTesting
+ void onUnhandledDropCallback(boolean consumedByListener) {
+ mHandler.removeMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT, null);
+ // If handled, then the listeners assume responsibility of cleaning up the drag surface
+ mDragState.mDragResult = consumedByListener;
+ mDragState.mRelinquishDragSurfaceToDropTarget = consumedByListener;
+ mDragState.closeLocked();
+ }
+
+ /**
+ * Returns whether we are currently waiting for the unhandled drag listener to callback after
+ * it was notified of an unhandled drag.
+ */
+ boolean hasPendingUnhandledDropCallback() {
+ return mHandler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT);
+ }
+
void cancelDragAndDrop(IBinder dragToken, boolean skipAnimation) {
if (DEBUG_DRAG) {
Slog.d(TAG_WM, "cancelDragAndDrop");
@@ -436,8 +563,8 @@
synchronized (mService.mGlobalLock) {
// !!! TODO: ANR the drag-receiving app
if (mDragState != null) {
- mDragState.mDragResult = false;
- mDragState.endDragLocked();
+ mDragState.endDragLocked(false /* consumed */,
+ false /* relinquishDragSurfaceToDropTarget */);
}
}
break;
@@ -473,6 +600,13 @@
}
break;
}
+
+ case MSG_UNHANDLED_DROP_LISTENER_TIMEOUT: {
+ synchronized (mService.mGlobalLock) {
+ onUnhandledDropCallback(false /* consumedByListener */);
+ }
+ break;
+ }
}
}
}
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index d302f06..76038b9 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -147,6 +147,11 @@
*/
private boolean mIsClosing;
+ // Stores the last drop event which was reported to a valid drop target window, or null
+ // otherwise. This drop event will contain private info and should only be consumed by the
+ // unhandled drag listener.
+ DragEvent mUnhandledDropEvent;
+
DragState(WindowManagerService service, DragDropController controller, IBinder token,
SurfaceControl surface, int flags, IBinder localWin) {
mService = service;
@@ -287,14 +292,54 @@
mData = null;
mThumbOffsetX = mThumbOffsetY = 0;
mNotifiedWindows = null;
+ if (mUnhandledDropEvent != null) {
+ mUnhandledDropEvent.recycle();
+ mUnhandledDropEvent = null;
+ }
// Notifies the controller that the drag state is closed.
mDragDropController.onDragStateClosedLocked(this);
}
/**
+ * Creates the drop event for this drag gesture. If `touchedWin` is null, then the drop event
+ * will be created for dispatching to the unhandled drag and the drag surface will be provided
+ * as a part of the dispatched event.
+ */
+ private DragEvent createDropEvent(float x, float y, @Nullable WindowState touchedWin,
+ boolean includeDragSurface) {
+ if (touchedWin != null) {
+ final int targetUserId = UserHandle.getUserId(touchedWin.getOwningUid());
+ final DragAndDropPermissionsHandler dragAndDropPermissions;
+ if ((mFlags & View.DRAG_FLAG_GLOBAL) != 0 && (mFlags & DRAG_FLAGS_URI_ACCESS) != 0
+ && mData != null) {
+ dragAndDropPermissions = new DragAndDropPermissionsHandler(mService.mGlobalLock,
+ mData,
+ mUid,
+ touchedWin.getOwningPackage(),
+ mFlags & DRAG_FLAGS_URI_PERMISSIONS,
+ mSourceUserId,
+ targetUserId);
+ } else {
+ dragAndDropPermissions = null;
+ }
+ if (mSourceUserId != targetUserId) {
+ if (mData != null) {
+ mData.fixUris(mSourceUserId);
+ }
+ }
+ return obtainDragEvent(DragEvent.ACTION_DROP, x, y, mData,
+ targetInterceptsGlobalDrag(touchedWin), dragAndDropPermissions);
+ } else {
+ return obtainDragEvent(DragEvent.ACTION_DROP, x, y, mData,
+ includeDragSurface /* includeDragSurface */, null /* dragAndDropPermissions */);
+ }
+ }
+
+ /**
* Notify the drop target and tells it about the data. If the drop event is not sent to the
- * target, invokes {@code endDragLocked} immediately.
+ * target, invokes {@code endDragLocked} after the unhandled drag listener gets a chance to
+ * handle the drop.
*/
boolean reportDropWindowLock(IBinder token, float x, float y) {
if (mAnimator != null) {
@@ -302,41 +347,27 @@
}
final WindowState touchedWin = mService.mInputToWindowMap.get(token);
+ final DragEvent unhandledDropEvent = createDropEvent(x, y, null /* touchedWin */,
+ true /* includePrivateInfo */);
if (!isWindowNotified(touchedWin)) {
- // "drop" outside a valid window -- no recipient to apply a
- // timeout to, and we can send the drag-ended message immediately.
- mDragResult = false;
- endDragLocked();
+ // Delegate to the unhandled drag listener as a first pass
+ if (mDragDropController.notifyUnhandledDrop(unhandledDropEvent, "unhandled-drop")) {
+ // The unhandled drag listener will call back to notify whether it has consumed
+ // the drag, so return here
+ return true;
+ }
+
+ // "drop" outside a valid window -- no recipient to apply a timeout to, and we can send
+ // the drag-ended message immediately.
+ endDragLocked(false /* consumed */, false /* relinquishDragSurfaceToDropTarget */);
if (DEBUG_DRAG) Slog.d(TAG_WM, "Drop outside a valid window " + touchedWin);
return false;
}
if (DEBUG_DRAG) Slog.d(TAG_WM, "sending DROP to " + touchedWin);
- final int targetUserId = UserHandle.getUserId(touchedWin.getOwningUid());
-
- final DragAndDropPermissionsHandler dragAndDropPermissions;
- if ((mFlags & View.DRAG_FLAG_GLOBAL) != 0 && (mFlags & DRAG_FLAGS_URI_ACCESS) != 0
- && mData != null) {
- dragAndDropPermissions = new DragAndDropPermissionsHandler(mService.mGlobalLock,
- mData,
- mUid,
- touchedWin.getOwningPackage(),
- mFlags & DRAG_FLAGS_URI_PERMISSIONS,
- mSourceUserId,
- targetUserId);
- } else {
- dragAndDropPermissions = null;
- }
- if (mSourceUserId != targetUserId) {
- if (mData != null) {
- mData.fixUris(mSourceUserId);
- }
- }
final IBinder clientToken = touchedWin.mClient.asBinder();
- final DragEvent event = obtainDragEvent(DragEvent.ACTION_DROP, x, y,
- mData, targetInterceptsGlobalDrag(touchedWin),
- dragAndDropPermissions);
+ final DragEvent event = createDropEvent(x, y, touchedWin, false /* includePrivateInfo */);
try {
touchedWin.mClient.dispatchDragEvent(event);
@@ -345,7 +376,7 @@
DragDropController.DRAG_TIMEOUT_MS);
} catch (RemoteException e) {
Slog.w(TAG_WM, "can't send drop notification to win " + touchedWin);
- endDragLocked();
+ endDragLocked(false /* consumed */, false /* relinquishDragSurfaceToDropTarget */);
return false;
} finally {
if (MY_PID != touchedWin.mSession.mPid) {
@@ -353,6 +384,7 @@
}
}
mToken = clientToken;
+ mUnhandledDropEvent = unhandledDropEvent;
return true;
}
@@ -478,6 +510,9 @@
boolean containsAppExtras) {
final boolean interceptsGlobalDrag = targetInterceptsGlobalDrag(newWin);
if (mDragInProgress && isValidDropTarget(newWin, containsAppExtras, interceptsGlobalDrag)) {
+ if (DEBUG_DRAG) {
+ Slog.d(TAG_WM, "Sending DRAG_STARTED to new window " + newWin);
+ }
// Only allow the extras to be dispatched to a global-intercepting drag target
ClipData data = interceptsGlobalDrag ? mData.copyForTransferWithActivityInfo() : null;
DragEvent event = obtainDragEvent(DragEvent.ACTION_DRAG_STARTED,
@@ -523,14 +558,25 @@
return false;
}
if (!targetWin.isPotentialDragTarget(interceptsGlobalDrag)) {
+ // Window should not be a target
return false;
}
- if ((mFlags & View.DRAG_FLAG_GLOBAL) == 0 || !targetWindowSupportsGlobalDrag(targetWin)) {
+ final boolean isGlobalSameAppDrag = (mFlags & View.DRAG_FLAG_GLOBAL_SAME_APPLICATION) != 0;
+ final boolean isGlobalDrag = (mFlags & View.DRAG_FLAG_GLOBAL) != 0;
+ final boolean isAnyGlobalDrag = isGlobalDrag || isGlobalSameAppDrag;
+ if (!isAnyGlobalDrag || !targetWindowSupportsGlobalDrag(targetWin)) {
// Drag is limited to the current window.
if (!isLocalWindow) {
return false;
}
}
+ if (isGlobalSameAppDrag) {
+ // Drag is limited to app windows from the same uid or windows that can intercept global
+ // drag
+ if (!interceptsGlobalDrag && mUid != targetWin.getUid()) {
+ return false;
+ }
+ }
return interceptsGlobalDrag
|| mCrossProfileCopyAllowed
@@ -547,7 +593,10 @@
/**
* @return whether the given window {@param targetWin} can intercept global drags.
*/
- public boolean targetInterceptsGlobalDrag(WindowState targetWin) {
+ public boolean targetInterceptsGlobalDrag(@Nullable WindowState targetWin) {
+ if (targetWin == null) {
+ return false;
+ }
return (targetWin.mAttrs.privateFlags & PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP) != 0;
}
@@ -561,9 +610,6 @@
if (isWindowNotified(newWin)) {
return;
}
- if (DEBUG_DRAG) {
- Slog.d(TAG_WM, "need to send DRAG_STARTED to new window " + newWin);
- }
sendDragStartedLocked(newWin, mCurrentX, mCurrentY,
containsApplicationExtras(mDataDescription));
}
@@ -578,7 +624,13 @@
return false;
}
- void endDragLocked() {
+ /**
+ * Ends the current drag, animating the drag surface back to the source if the drop was not
+ * consumed by the receiving window.
+ */
+ void endDragLocked(boolean dropConsumed, boolean relinquishDragSurfaceToDropTarget) {
+ mDragResult = dropConsumed;
+ mRelinquishDragSurfaceToDropTarget = relinquishDragSurfaceToDropTarget;
if (mAnimator != null) {
return;
}
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 80894b2..61fde5e 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -465,7 +465,9 @@
}
}
final InsetsSource source = new InsetsSource(id, provider.getType());
- source.setFrame(provider.getArbitraryRectangle()).updateSideHint(getBounds());
+ source.setFrame(provider.getArbitraryRectangle())
+ .updateSideHint(getBounds())
+ .setBoundingRects(provider.getBoundingRects());
mLocalInsetsSources.put(id, source);
mDisplayContent.getInsetsStateController().updateAboveInsetsState(true);
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 4ea76e1..de8d9f9 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -310,6 +310,7 @@
import android.window.ISurfaceSyncGroupCompletedListener;
import android.window.ITaskFpsCallback;
import android.window.ITrustedPresentationListener;
+import android.window.IUnhandledDragListener;
import android.window.InputTransferToken;
import android.window.ScreenCapture;
import android.window.SystemPerformanceHinter;
@@ -10026,4 +10027,16 @@
void onProcessActivityVisibilityChanged(int uid, boolean visible) {
mScreenRecordingCallbackController.onProcessActivityVisibilityChanged(uid, visible);
}
+
+ /**
+ * Sets the listener to be called back when a cross-window drag and drop operation is unhandled
+ * (ie. not handled by any window which can handle the drag).
+ */
+ @Override
+ public void setUnhandledDragListener(IUnhandledDragListener listener) throws RemoteException {
+ mAtmService.enforceTaskPermission("setUnhandledDragListener");
+ synchronized (mGlobalLock) {
+ mDragDropController.setUnhandledDragListener(listener);
+ }
+ }
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 85eac29..36b8381 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_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_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.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.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 @@
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(
@@ -22343,6 +22352,7 @@
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,
@@ -22423,7 +22433,7 @@
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,
@@ -24012,4 +24022,32 @@
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 5996e42..b09908e 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 @@
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/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
index 8b22718..bc264a4 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
@@ -16,11 +16,12 @@
package com.android.server.policy;
-import static com.android.server.devicestate.DeviceState.FLAG_CANCEL_OVERRIDE_REQUESTS;
-import static com.android.server.devicestate.DeviceState.FLAG_CANCEL_WHEN_REQUESTER_NOT_ON_TOP;
-import static com.android.server.devicestate.DeviceState.FLAG_EMULATED_ONLY;
-import static com.android.server.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE;
-import static com.android.server.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL;
+import static android.hardware.devicestate.DeviceState.FLAG_CANCEL_OVERRIDE_REQUESTS;
+import static android.hardware.devicestate.DeviceState.FLAG_CANCEL_WHEN_REQUESTER_NOT_ON_TOP;
+import static android.hardware.devicestate.DeviceState.FLAG_EMULATED_ONLY;
+import static android.hardware.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE;
+import static android.hardware.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL;
+
import static com.android.server.policy.BookStyleStateTransitions.DEFAULT_STATE_TRANSITIONS;
import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createConfig;
import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createTentModeClosedState;
@@ -36,7 +37,6 @@
import com.android.server.devicestate.DeviceStateProvider;
import com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration;
import com.android.server.policy.feature.flags.FeatureFlags;
-import com.android.server.policy.feature.flags.FeatureFlagsImpl;
import java.io.PrintWriter;
import java.util.function.Predicate;
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
index 021a667..bf2619b 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
@@ -34,6 +34,7 @@
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
+import android.hardware.devicestate.DeviceState;
import android.hardware.display.DisplayManager;
import android.os.Handler;
import android.os.Looper;
@@ -48,7 +49,6 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
-import com.android.server.devicestate.DeviceState;
import com.android.server.devicestate.DeviceStateProvider;
import com.android.server.policy.feature.flags.FeatureFlags;
import com.android.server.policy.feature.flags.FeatureFlagsImpl;
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
index 04cebab..930f4a6 100644
--- a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
@@ -51,6 +51,7 @@
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorManager;
+import android.hardware.devicestate.DeviceState;
import android.hardware.display.DisplayManager;
import android.hardware.input.InputSensorInfo;
import android.os.Handler;
@@ -58,7 +59,6 @@
import android.testing.AndroidTestingRunner;
import android.view.Display;
-import com.android.server.devicestate.DeviceState;
import com.android.server.devicestate.DeviceStateProvider.Listener;
import com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration;
import com.android.server.policy.feature.flags.FakeFeatureFlagsImpl;
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index c55d709..dbbfb03 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -48,7 +48,6 @@
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 @@
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 @@
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 2f5c109..67f66de 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 @@
/** 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 c0f0ce0..b363f54 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 656bc71..7bbcd50 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 @@
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 0000000..49a0934
--- /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 395b3aa..d36b553 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 @@
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 @@
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 @@
}
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 @@
assertThat(item.eventTag.string).isEqualTo(tag);
assertThat(item.eventTag.uid).isEqualTo(uid);
}
+ assertThat((int) item.batteryVoltage).isEqualTo(voltageMv);
assertThat(item.batteryChargeUah).isEqualTo(batteryChargeUah);
assertThat(item.batteryLevel).isEqualTo(batteryLevel);
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 26934d8..4307ec5 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -682,19 +682,19 @@
/* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false);
setUpAndStartUserInBackground(TEST_USER_ID);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID, /* delayedLocking= */ true,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID, /* allowDelayedLocking= */ true,
/* keyEvictedCallback= */ null, /* expectLocking= */ true);
setUpAndStartUserInBackground(TEST_USER_ID1);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* delayedLocking= */ true,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true,
/* keyEvictedCallback= */ mKeyEvictedCallback, /* expectLocking= */ true);
setUpAndStartUserInBackground(TEST_USER_ID2);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* delayedLocking= */ false,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* allowDelayedLocking= */ false,
/* keyEvictedCallback= */ null, /* expectLocking= */ true);
setUpAndStartUserInBackground(TEST_USER_ID3);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID3, /* delayedLocking= */ false,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID3, /* allowDelayedLocking= */ false,
/* keyEvictedCallback= */ mKeyEvictedCallback, /* expectLocking= */ true);
}
@@ -739,21 +739,21 @@
mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true,
/* maxRunningUsers= */ 3, /* delayUserDataLocking= */ true);
- // delayedLocking set and no KeyEvictedCallback, so it should not lock.
+ // allowDelayedLocking set and no KeyEvictedCallback, so it should not lock.
setUpAndStartUserInBackground(TEST_USER_ID);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID, /* delayedLocking= */ true,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID, /* allowDelayedLocking= */ true,
/* keyEvictedCallback= */ null, /* expectLocking= */ false);
setUpAndStartUserInBackground(TEST_USER_ID1);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* delayedLocking= */ true,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true,
/* keyEvictedCallback= */ mKeyEvictedCallback, /* expectLocking= */ true);
setUpAndStartUserInBackground(TEST_USER_ID2);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* delayedLocking= */ false,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* allowDelayedLocking= */ false,
/* keyEvictedCallback= */ null, /* expectLocking= */ true);
setUpAndStartUserInBackground(TEST_USER_ID3);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID3, /* delayedLocking= */ false,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID3, /* allowDelayedLocking= */ false,
/* keyEvictedCallback= */ mKeyEvictedCallback, /* expectLocking= */ true);
}
@@ -843,7 +843,7 @@
mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE);
setUpAndStartProfileInBackground(TEST_USER_ID1, UserManager.USER_TYPE_PROFILE_PRIVATE);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* delayedLocking= */ true,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true,
/* keyEvictedCallback */ null, /* expectLocking= */ false);
}
@@ -855,19 +855,20 @@
mSetFlagsRule.disableFlags(
android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE);
setUpAndStartProfileInBackground(TEST_USER_ID1, UserManager.USER_TYPE_PROFILE_PRIVATE);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* delayedLocking= */ true,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true,
/* keyEvictedCallback */ null, /* expectLocking= */ true);
mSetFlagsRule.disableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE);
mSetFlagsRule.enableFlags(
android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE);
setUpAndStartProfileInBackground(TEST_USER_ID2, UserManager.USER_TYPE_PROFILE_PRIVATE);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* delayedLocking= */ true,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* allowDelayedLocking= */ true,
/* keyEvictedCallback */ null, /* expectLocking= */ true);
}
+ /** Delayed-locking users (as opposed to devices) have no limits on how many can be unlocked. */
@Test
- public void testStopPrivateProfileWithDelayedLocking_maxRunningUsersBreached()
+ public void testStopPrivateProfileWithDelayedLocking_imperviousToNumberOfRunningUsers()
throws Exception {
mUserController.setInitialConfig(/* mUserSwitchUiEnabled */ true,
/* maxRunningUsers= */ 1, /* delayUserDataLocking= */ false);
@@ -875,10 +876,14 @@
android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE);
setUpAndStartProfileInBackground(TEST_USER_ID1, UserManager.USER_TYPE_PROFILE_PRIVATE);
setUpAndStartProfileInBackground(TEST_USER_ID2, UserManager.USER_TYPE_PROFILE_MANAGED);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* delayedLocking= */ true,
- /* keyEvictedCallback */ null, /* expectLocking= */ true);
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true,
+ /* keyEvictedCallback */ null, /* expectLocking= */ false);
}
+ /**
+ * Tests that when a device/user (managed profile) does not permit delayed locking, then
+ * even if allowDelayedLocking is true, the user will still be locked.
+ */
@Test
public void testStopManagedProfileWithDelayedLocking() throws Exception {
mUserController.setInitialConfig(/* mUserSwitchUiEnabled */ true,
@@ -886,7 +891,7 @@
mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE);
setUpAndStartProfileInBackground(TEST_USER_ID1, UserManager.USER_TYPE_PROFILE_MANAGED);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* delayedLocking= */ true,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true,
/* keyEvictedCallback */ null, /* expectLocking= */ true);
}
@@ -1087,29 +1092,29 @@
mUserStates.put(userId, mUserController.getStartedUserState(userId));
}
- private void assertUserLockedOrUnlockedAfterStopping(int userId, boolean delayedLocking,
+ private void assertUserLockedOrUnlockedAfterStopping(int userId, boolean allowDelayedLocking,
KeyEvictedCallback keyEvictedCallback, boolean expectLocking) throws Exception {
- int r = mUserController.stopUser(userId, /* force= */ true, /* delayedLocking= */
- delayedLocking, null, keyEvictedCallback);
+ int r = mUserController.stopUser(userId, /* force= */ true, /* allowDelayedLocking= */
+ allowDelayedLocking, null, keyEvictedCallback);
assertThat(r).isEqualTo(ActivityManager.USER_OP_SUCCESS);
- assertUserLockedOrUnlockedState(userId, delayedLocking, expectLocking);
+ assertUserLockedOrUnlockedState(userId, allowDelayedLocking, expectLocking);
}
private void assertProfileLockedOrUnlockedAfterStopping(int userId, boolean expectLocking)
throws Exception {
boolean profileStopped = mUserController.stopProfile(userId);
assertThat(profileStopped).isTrue();
- assertUserLockedOrUnlockedState(userId, /* delayedLocking= */ false, expectLocking);
+ assertUserLockedOrUnlockedState(userId, /* allowDelayedLocking= */ false, expectLocking);
}
- private void assertUserLockedOrUnlockedState(int userId, boolean delayedLocking,
+ private void assertUserLockedOrUnlockedState(int userId, boolean allowDelayedLocking,
boolean expectLocking) throws InterruptedException, RemoteException {
// fake all interim steps
UserState ussUser = mUserStates.get(userId);
ussUser.setState(UserState.STATE_SHUTDOWN);
// Passing delayedLocking invalidates incorrect internal data passing but currently there is
// no easy way to get that information passed through lambda.
- mUserController.finishUserStopped(ussUser, delayedLocking);
+ mUserController.finishUserStopped(ussUser, allowDelayedLocking);
waitForHandlerToComplete(FgThread.getHandler(), HANDLER_WAIT_TIME_MS);
verify(mInjector.mStorageManagerMock, times(expectLocking ? 1 : 0))
.lockCeStorage(userId);
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
index fa39364..b705077 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
@@ -31,6 +31,7 @@
import static org.testng.Assert.assertThrows;
import android.annotation.NonNull;
+import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateInfo;
import android.hardware.devicestate.DeviceStateRequest;
import android.hardware.devicestate.IDeviceStateManagerCallback;
@@ -72,7 +73,8 @@
new DeviceState(0, "DEFAULT", 0 /* flags */);
private static final DeviceState OTHER_DEVICE_STATE =
new DeviceState(1, "OTHER", 0 /* flags */);
- private static final DeviceState DEVICE_STATE_CANCEL_WHEN_REQUESTER_NOT_ON_TOP =
+ private static final DeviceState
+ DEVICE_STATE_CANCEL_WHEN_REQUESTER_NOT_ON_TOP =
new DeviceState(2, "DEVICE_STATE_CANCEL_WHEN_REQUESTER_NOT_ON_TOP",
DeviceState.FLAG_CANCEL_WHEN_REQUESTER_NOT_ON_TOP /* flags */);
// A device state that is not reported as being supported for the default test provider.
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java b/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java
index b3d25f2..cfdb586 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java
@@ -25,6 +25,7 @@
import static junit.framework.Assert.assertNull;
import android.annotation.Nullable;
+import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateRequest;
import android.os.Binder;
import android.platform.test.annotations.Presubmit;
@@ -48,8 +49,10 @@
@RunWith(AndroidJUnit4.class)
public final class OverrideRequestControllerTest {
- private static final DeviceState TEST_DEVICE_STATE_ZERO = new DeviceState(0, "TEST_STATE", 0);
- private static final DeviceState TEST_DEVICE_STATE_ONE = new DeviceState(1, "TEST_STATE", 0);
+ private static final DeviceState
+ TEST_DEVICE_STATE_ZERO = new DeviceState(0, "TEST_STATE", 0);
+ private static final DeviceState
+ TEST_DEVICE_STATE_ONE = new DeviceState(1, "TEST_STATE", 0);
private TestStatusChangeListener mStatusListener;
private OverrideRequestController mController;
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index df2069e..7f7cc35 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 @@
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/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
index 7e40f96..16909ab 100644
--- a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
@@ -40,12 +40,12 @@
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorManager;
+import android.hardware.devicestate.DeviceState;
import android.os.PowerManager;
import androidx.annotation.NonNull;
import com.android.server.LocalServices;
-import com.android.server.devicestate.DeviceState;
import com.android.server.devicestate.DeviceStateProvider;
import com.android.server.input.InputManagerInternal;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 046e057..6aacfd7 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.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;
@@ -303,7 +304,6 @@
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
-import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
@@ -4003,6 +4003,69 @@
}
@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))
@@ -14368,7 +14431,6 @@
}
@Test
- @Ignore("b/324348078")
public void cancelNotificationsFromListener_rapidClear_old_cancelOne() throws RemoteException {
mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
.FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
@@ -14458,7 +14520,6 @@
}
@Test
- @Ignore("b/324348078")
public void cancelNotificationsFromListener_rapidClear_old_cancelAll() throws RemoteException {
mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
.FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
@@ -14532,6 +14593,120 @@
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 f604f1e..3ac7890 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 @@
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 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 @@
.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 @@
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 @@
.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 @@
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 @@
.setShouldMinimizeRadioUsage(true)
.setShouldUseNightMode(true)
.setShouldSuppressAmbientDisplay(true)
+ .setExtraEffects(ImmutableSet.of("1", "2", "3"))
.build();
Parcel parcel = Parcel.obtain();
@@ -113,6 +151,7 @@
assertThat(copy.shouldUseNightMode()).isTrue();
assertThat(copy.shouldSuppressAmbientDisplay()).isTrue();
assertThat(copy.shouldDisplayGrayscale()).isFalse();
+ assertThat(copy.getExtraEffects()).containsExactly("1", "2", "3");
}
@Test
@@ -128,4 +167,36 @@
.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 539bb37..12f9e26 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.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 @@
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 @@
.setShouldDisableTouch(true)
.setShouldMinimizeRadioUsage(false)
.setShouldMaximizeDoze(true)
+ .setExtraEffects(ImmutableSet.of("one", "two"))
.build();
rule.creationTime = CREATION_TIME;
@@ -543,6 +547,28 @@
}
@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 5b35e34..ff1308c 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 @@
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 0d88b98d..f9ba33b 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.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.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 @@
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 @@
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 @@
new ZenDeviceEffects.Builder()
.setShouldUseNightMode(true) // From update.
.setShouldDisableTapToWake(true) // From original.
+ .addExtraEffect("extra")
.build());
}
@@ -3550,6 +3556,89 @@
}
@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 @@
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 @@
eq(new ZenDeviceEffects.Builder()
.setShouldDisplayGrayscale(true)
.setShouldDimWallpaper(true)
+ .setExtraEffects(ImmutableSet.of("ONE", "TWO"))
.build()),
eq(UPDATE_ORIGIN_APP));
}
@@ -4732,7 +4829,10 @@
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 @@
.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 bdbb6c6..7db707a 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 @@
cancelVibrate(service); // Clean up long effect.
}
- @FlakyTest
@Test
public void onExternalVibration_withNewSameImportanceButRepeating_cancelsOngoingVibration()
throws Exception {
diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
index 1fb7cd8..9e2b1ec 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
@@ -32,14 +32,17 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static com.android.server.wm.DragDropController.MSG_UNHANDLED_DROP_LISTENER_TIMEOUT;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.app.PendingIntent;
@@ -49,9 +52,12 @@
import android.content.pm.ShortcutServiceInternal;
import android.graphics.PixelFormat;
import android.os.Binder;
+import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
+import android.os.Message;
import android.os.Parcelable;
+import android.os.RemoteException;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
import android.view.DragEvent;
@@ -61,6 +67,7 @@
import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
+import android.window.IUnhandledDragListener;
import androidx.test.filters.SmallTest;
@@ -533,14 +540,98 @@
});
}
+ @Test
+ public void testUnhandledDragListenerNotCalledForNormalDrags() throws RemoteException {
+ assumeTrue(com.android.window.flags.Flags.delegateUnhandledDrags());
+
+ final IUnhandledDragListener listener = mock(IUnhandledDragListener.class);
+ doReturn(mock(Binder.class)).when(listener).asBinder();
+ mTarget.setUnhandledDragListener(listener);
+ doDragAndDrop(0, ClipData.newPlainText("label", "Test"), 0, 0);
+ verify(listener, times(0)).onUnhandledDrop(any(), any());
+ }
+
+ @Test
+ public void testUnhandledDragListenerReceivesUnhandledDropOverWindow() {
+ assumeTrue(com.android.window.flags.Flags.delegateUnhandledDrags());
+
+ final IUnhandledDragListener listener = mock(IUnhandledDragListener.class);
+ doReturn(mock(Binder.class)).when(listener).asBinder();
+ mTarget.setUnhandledDragListener(listener);
+ final int invalidXY = 100_000;
+ startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> {
+ // Notify the unhandled drag listener
+ mTarget.reportDropWindow(mWindow.mInputChannelToken, invalidXY, invalidXY);
+ mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY);
+ mTarget.reportDropResult(mWindow.mClient, false);
+ mTarget.onUnhandledDropCallback(true);
+ mToken = null;
+ try {
+ verify(listener, times(1)).onUnhandledDrop(any(), any());
+ } catch (RemoteException e) {
+ fail("Failed to verify unhandled drop: " + e);
+ }
+ });
+ }
+
+ @Test
+ public void testUnhandledDragListenerReceivesUnhandledDropOverNoValidWindow() {
+ assumeTrue(com.android.window.flags.Flags.delegateUnhandledDrags());
+
+ final IUnhandledDragListener listener = mock(IUnhandledDragListener.class);
+ doReturn(mock(Binder.class)).when(listener).asBinder();
+ mTarget.setUnhandledDragListener(listener);
+ final int invalidXY = 100_000;
+ startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> {
+ // Notify the unhandled drag listener
+ mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY);
+ mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY);
+ mTarget.onUnhandledDropCallback(true);
+ mToken = null;
+ try {
+ verify(listener, times(1)).onUnhandledDrop(any(), any());
+ } catch (RemoteException e) {
+ fail("Failed to verify unhandled drop: " + e);
+ }
+ });
+ }
+
+ @Test
+ public void testUnhandledDragListenerCallbackTimeout() {
+ assumeTrue(com.android.window.flags.Flags.delegateUnhandledDrags());
+
+ final IUnhandledDragListener listener = mock(IUnhandledDragListener.class);
+ doReturn(mock(Binder.class)).when(listener).asBinder();
+ mTarget.setUnhandledDragListener(listener);
+ final int invalidXY = 100_000;
+ startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> {
+ // Notify the unhandled drag listener
+ mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY);
+ mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY);
+
+ // Verify that the unhandled drop listener callback timeout has been scheduled
+ final Handler handler = mTarget.getHandler();
+ assertTrue(handler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT));
+
+ // Force trigger the timeout and verify that it actually cleans up the drag & timeout
+ handler.handleMessage(Message.obtain(handler, MSG_UNHANDLED_DROP_LISTENER_TIMEOUT));
+ assertFalse(handler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT));
+ assertFalse(mTarget.dragDropActiveLocked());
+ mToken = null;
+ });
+ }
+
private void doDragAndDrop(int flags, ClipData data, float dropX, float dropY) {
startDrag(flags, data, () -> {
mTarget.reportDropWindow(mWindow.mInputChannelToken, dropX, dropY);
- mTarget.handleMotionEvent(false, dropX, dropY);
+ mTarget.handleMotionEvent(false /* keepHandling */, dropX, dropY);
mToken = mWindow.mClient.asBinder();
});
}
+ /**
+ * Starts a drag with the given parameters, calls Runnable `r` after drag is started.
+ */
private void startDrag(int flag, ClipData data, Runnable r) {
final SurfaceSession appSession = new SurfaceSession();
try {
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index aa9c0c8..03b695d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -48,6 +48,7 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -901,7 +902,8 @@
new Binder(),
0 /* index */,
WindowInsets.Type.systemOverlays(),
- new Rect(0, 0, 1080, 200));
+ new Rect(0, 0, 1080, 200),
+ null /* boundingRects */);
mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct);
assertThat(navigationBarInsetsReceiverTask.mLocalInsetsSources
@@ -910,6 +912,31 @@
}
@Test
+ public void testAddInsetsSource_withBoundingRects() {
+ final Task rootTask = createTask(mDisplayContent);
+
+ final Task navigationBarInsetsReceiverTask = createTaskInRootTask(rootTask, 0);
+ navigationBarInsetsReceiverTask.getConfiguration().windowConfiguration.setBounds(new Rect(
+ 0, 200, 1080, 700));
+
+ final Rect[] boundingRects = new Rect[]{
+ new Rect(0, 0, 10, 10), new Rect(100, 100, 200, 100)
+ };
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.addInsetsSource(
+ navigationBarInsetsReceiverTask.mRemoteToken.toWindowContainerToken(),
+ new Binder(),
+ 0 /* index */,
+ WindowInsets.Type.systemOverlays(),
+ new Rect(0, 0, 1080, 200),
+ boundingRects);
+ mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct);
+
+ assertArrayEquals(boundingRects, navigationBarInsetsReceiverTask.mLocalInsetsSources
+ .valueAt(0).getBoundingRects());
+ }
+
+ @Test
public void testRemoveInsetsSource() {
final Task rootTask = createTask(mDisplayContent);
@@ -923,7 +950,8 @@
owner,
0 /* index */,
WindowInsets.Type.systemOverlays(),
- new Rect(0, 0, 1080, 200));
+ new Rect(0, 0, 1080, 200),
+ null /* boundingRects */);
mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct);
final WindowContainerTransaction wct2 = new WindowContainerTransaction();
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
index d7b860f..e4ac993 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.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 @@
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 @@
return;
}
try {
- mAttentionListener.onAttentionGained();
+ mAttentionListener.onAttentionGained(attentionResult);
} catch (RemoteException e) {
Slog.e(TAG, "Error delivering attention gained event.", e);
try {
@@ -129,7 +130,7 @@
}
@Override
- public void onAttentionLost() {
+ public void onAttentionLost(int interactionIntention) {
Slog.v(TAG, "BinderCallback#onAttentionLost");
synchronized (mLock) {
mEgressingData = false;
@@ -137,7 +138,7 @@
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/Call.java b/telecomm/java/android/telecom/Call.java
index 874c10c..a52614d 100644
--- a/telecomm/java/android/telecom/Call.java
+++ b/telecomm/java/android/telecom/Call.java
@@ -269,6 +269,27 @@
"android.telecom.extra.DIAGNOSTIC_MESSAGE";
/**
+ * Boolean indicating that the call is a verified business call.
+ *
+ * {@link Connection#setExtras(Bundle)} or {@link Connection#putExtras(Bundle)}
+ * should be used to notify Telecom this extra has been set.
+ */
+ @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public static final String EXTRA_IS_BUSINESS_CALL =
+ "android.telecom.extra.IS_BUSINESS_CALL";
+
+ /**
+ * String value indicating the asserted display name reported via
+ * ImsCallProfile#EXTRA_ASSERTED_DISPLAY_NAME.
+ *
+ * {@link Connection#setExtras(Bundle)} or {@link Connection#putExtras(Bundle)}
+ * should be used to notify Telecom this extra has been set.
+ */
+ @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public static final String EXTRA_ASSERTED_DISPLAY_NAME =
+ "android.telecom.extra.ASSERTED_DISPLAY_NAME";
+
+ /**
* Reject reason used with {@link #reject(int)} to indicate that the user is rejecting this
* call because they have declined to answer it. This typically means that they are unable
* to answer the call at this time and would prefer it be sent to voicemail.
diff --git a/telecomm/java/android/telecom/PhoneAccount.java b/telecomm/java/android/telecom/PhoneAccount.java
index 6bdc43e..e6fe406 100644
--- a/telecomm/java/android/telecom/PhoneAccount.java
+++ b/telecomm/java/android/telecom/PhoneAccount.java
@@ -190,6 +190,8 @@
* 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 3daa014..15a978d 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -1611,6 +1611,26 @@
* {@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/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index f7793f3..697c8ec 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -9778,6 +9778,13 @@
public static final String KEY_SUPPORTS_CALL_COMPOSER_BOOL = "supports_call_composer_bool";
/**
+ * Indicates if the carrier supports a business call composer.
+ */
+ @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public static final String KEY_SUPPORTS_BUSINESS_CALL_COMPOSER_BOOL =
+ "supports_business_call_composer_bool";
+
+ /**
* Indicates the carrier server url that serves the call composer picture.
*/
public static final String KEY_CALL_COMPOSER_PICTURE_SERVER_URL_STRING =
@@ -10861,6 +10868,7 @@
sDefaults.putBoolean(KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL, false);
sDefaults.putString(KEY_DEFAULT_PREFERRED_APN_NAME_STRING, "");
sDefaults.putBoolean(KEY_SUPPORTS_CALL_COMPOSER_BOOL, false);
+ sDefaults.putBoolean(KEY_SUPPORTS_BUSINESS_CALL_COMPOSER_BOOL, false);
sDefaults.putString(KEY_CALL_COMPOSER_PICTURE_SERVER_URL_STRING, "");
sDefaults.putBoolean(KEY_USE_ACS_FOR_RCS_BOOL, false);
sDefaults.putBoolean(KEY_NETWORK_TEMP_NOT_METERED_SUPPORTED_BOOL, true);
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 7118207..cd641b8 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -1156,6 +1156,28 @@
*/
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/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 61c7a42..041822b 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -10629,20 +10629,27 @@
}
/**
- * Call composer status OFF from user setting.
+ * Call composer status <b>OFF</b> from user setting.
*/
public static final int CALL_COMPOSER_STATUS_OFF = 0;
/**
- * Call composer status ON from user setting.
+ * Call composer status <b>ON</b> from user setting.
*/
public static final int CALL_COMPOSER_STATUS_ON = 1;
+ /**
+ * Call composer status <b>Business Only</b> from user setting.
+ */
+ @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public static final int CALL_COMPOSER_STATUS_BUSINESS_ONLY = 2;
+
/** @hide */
@IntDef(prefix = {"CALL_COMPOSER_STATUS_"},
value = {
CALL_COMPOSER_STATUS_ON,
CALL_COMPOSER_STATUS_OFF,
+ CALL_COMPOSER_STATUS_BUSINESS_ONLY
})
@Retention(RetentionPolicy.SOURCE)
public @interface CallComposerStatus {}
@@ -10663,9 +10670,16 @@
@RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
@RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING)
public void setCallComposerStatus(@CallComposerStatus int status) {
- if (status > CALL_COMPOSER_STATUS_ON
- || status < CALL_COMPOSER_STATUS_OFF) {
- throw new IllegalArgumentException("requested status is invalid");
+ if (com.android.server.telecom.flags.Flags.businessCallComposer()) {
+ if (status > CALL_COMPOSER_STATUS_BUSINESS_ONLY
+ || status < CALL_COMPOSER_STATUS_OFF) {
+ throw new IllegalArgumentException("requested status is invalid");
+ }
+ } else {
+ if (status > CALL_COMPOSER_STATUS_ON
+ || status < CALL_COMPOSER_STATUS_OFF) {
+ throw new IllegalArgumentException("requested status is invalid");
+ }
}
try {
ITelephony telephony = getITelephony();
diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java
index 7935d24..e3ce766 100644
--- a/telephony/java/android/telephony/euicc/EuiccManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccManager.java
@@ -1024,11 +1024,22 @@
/**
* 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 @@
* @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 @@
* <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 @@
* @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/telephony/java/android/telephony/ims/ImsCallProfile.java b/telephony/java/android/telephony/ims/ImsCallProfile.java
index d07edeb..cebfe01 100644
--- a/telephony/java/android/telephony/ims/ImsCallProfile.java
+++ b/telephony/java/android/telephony/ims/ImsCallProfile.java
@@ -16,6 +16,7 @@
package android.telephony.ims;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -299,6 +300,16 @@
"android.telephony.ims.extra.IS_BUSINESS_CALL";
/**
+ * The vendor IMS stack populates this {@code string} extra; it is used to hold the display name
+ * passed via the P-Asserted-Identity SIP header’s display-name field
+ *
+ * Reference: RFC3325
+ */
+ @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public static final String EXTRA_ASSERTED_DISPLAY_NAME =
+ "android.telephony.ims.extra.ASSERTED_DISPLAY_NAME";
+
+ /**
* Values for EXTRA_OIR / EXTRA_CNAP
*/
/**
diff --git a/telephony/java/android/telephony/ims/feature/MmTelFeature.java b/telephony/java/android/telephony/ims/feature/MmTelFeature.java
index 746246c..9789082 100644
--- a/telephony/java/android/telephony/ims/feature/MmTelFeature.java
+++ b/telephony/java/android/telephony/ims/feature/MmTelFeature.java
@@ -17,6 +17,7 @@
package android.telephony.ims.feature;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -59,6 +60,7 @@
import com.android.ims.internal.IImsMultiEndpoint;
import com.android.ims.internal.IImsUt;
import com.android.internal.telephony.util.TelephonyUtils;
+import com.android.server.telecom.flags.Flags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -513,7 +515,8 @@
CAPABILITY_TYPE_VIDEO,
CAPABILITY_TYPE_UT,
CAPABILITY_TYPE_SMS,
- CAPABILITY_TYPE_CALL_COMPOSER
+ CAPABILITY_TYPE_CALL_COMPOSER,
+ CAPABILITY_TYPE_CALL_COMPOSER_BUSINESS_ONLY
})
@Retention(RetentionPolicy.SOURCE)
public @interface MmTelCapability {}
@@ -550,11 +553,19 @@
*/
public static final int CAPABILITY_TYPE_CALL_COMPOSER = 1 << 4;
+
+ /**
+ * This MmTelFeature supports Business-only Call Composer
+ */
+ @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public static final int CAPABILITY_TYPE_CALL_COMPOSER_BUSINESS_ONLY = 1 << 5;
+
/**
* This is used to check the upper range of MmTel capability
* @hide
*/
- public static final int CAPABILITY_TYPE_MAX = CAPABILITY_TYPE_CALL_COMPOSER + 1;
+ public static final int CAPABILITY_TYPE_MAX =
+ CAPABILITY_TYPE_CALL_COMPOSER_BUSINESS_ONLY + 1;
/**
* @hide
@@ -601,6 +612,8 @@
builder.append(isCapable(CAPABILITY_TYPE_SMS));
builder.append(" CALL_COMPOSER: ");
builder.append(isCapable(CAPABILITY_TYPE_CALL_COMPOSER));
+ builder.append(" BUSINESS_COMPOSER_ONLY: ");
+ builder.append(isCapable(CAPABILITY_TYPE_CALL_COMPOSER_BUSINESS_ONLY));
builder.append("]");
return builder.toString();
}
diff --git a/test-mock/api/test-current.txt b/test-mock/api/test-current.txt
index 9ed0108..14f1b64 100644
--- a/test-mock/api/test-current.txt
+++ b/test-mock/api/test-current.txt
@@ -3,6 +3,7 @@
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 70c7dad..a16a7ea 100644
--- a/tests/UsbManagerTests/Android.bp
+++ b/tests/UsbManagerTests/Android.bp
@@ -33,6 +33,8 @@
"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 0000000..dabfcae
--- /dev/null
+++ b/tests/UsbManagerTests/src/android/hardware/usb/UsbPortStatusTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.usb;
+
+import static android.hardware.usb.UsbPortStatus.CONTAMINANT_DETECTION_NOT_SUPPORTED;
+import static android.hardware.usb.UsbPortStatus.CONTAMINANT_PROTECTION_NONE;
+import static android.hardware.usb.UsbPortStatus.DATA_ROLE_DEVICE;
+import static android.hardware.usb.UsbPortStatus.DATA_ROLE_HOST;
+import static android.hardware.usb.UsbPortStatus.DATA_ROLE_NONE;
+import static android.hardware.usb.UsbPortStatus.MODE_NONE;
+import static android.hardware.usb.UsbPortStatus.POWER_ROLE_NONE;
+import static android.hardware.usb.UsbPortStatus.POWER_ROLE_SINK;
+import static android.hardware.usb.UsbPortStatus.POWER_ROLE_SOURCE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.usb.flags.Flags;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+
+import com.google.testing.junit.testparameterinjector.TestParameter;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link android.hardware.usb.UsbPortStatus} */
+@RunWith(TestParameterInjector.class)
+public class UsbPortStatusTest {
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_IS_PD_COMPLIANT_API)
+ public void testIsPdCompliant(
+ @TestParameter boolean isSinkDeviceRoleSupported,
+ @TestParameter boolean isSinkHostRoleSupported,
+ @TestParameter boolean isSourceDeviceRoleSupported,
+ @TestParameter boolean isSourceHostRoleSupported) {
+ int supportedRoleCombinations = getSupportedRoleCombinations(
+ isSinkDeviceRoleSupported,
+ isSinkHostRoleSupported,
+ isSourceDeviceRoleSupported,
+ isSinkHostRoleSupported);
+ UsbPortStatus usbPortStatus = new UsbPortStatus(
+ MODE_NONE,
+ POWER_ROLE_NONE,
+ DATA_ROLE_NONE,
+ supportedRoleCombinations,
+ CONTAMINANT_PROTECTION_NONE,
+ CONTAMINANT_DETECTION_NOT_SUPPORTED);
+ boolean expectedResult = isSinkDeviceRoleSupported
+ && isSinkHostRoleSupported
+ && isSourceDeviceRoleSupported
+ && isSourceHostRoleSupported;
+
+ assertThat(usbPortStatus.isPdCompliant()).isEqualTo(expectedResult);
+ }
+
+ private int getSupportedRoleCombinations(
+ boolean isSinkDeviceRoleSupported,
+ boolean isSinkHostRoleSupported,
+ boolean isSourceDeviceRoleSupported,
+ boolean isSourceHostRoleSupported) {
+ int result = UsbPort.combineRolesAsBit(POWER_ROLE_NONE, DATA_ROLE_NONE);
+
+ if (isSinkDeviceRoleSupported) {
+ result |= UsbPort.combineRolesAsBit(POWER_ROLE_SINK, DATA_ROLE_DEVICE);
+ }
+ if (isSinkHostRoleSupported) {
+ result |= UsbPort.combineRolesAsBit(POWER_ROLE_SINK, DATA_ROLE_HOST);
+ }
+ if (isSourceDeviceRoleSupported) {
+ result |= UsbPort.combineRolesAsBit(POWER_ROLE_SOURCE, DATA_ROLE_DEVICE);
+ }
+ if (isSourceHostRoleSupported) {
+ result |= UsbPort.combineRolesAsBit(POWER_ROLE_SOURCE, DATA_ROLE_HOST);
+ }
+
+ return result;
+ }
+}
diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java
index 60c25b7..be5c84c 100644
--- a/tests/testables/src/android/testing/TestableLooper.java
+++ b/tests/testables/src/android/testing/TestableLooper.java
@@ -14,6 +14,8 @@
package android.testing;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
@@ -85,6 +87,57 @@
setupQueue(looper);
}
+ /**
+ * Wrap the given runnable so that it will run blocking on the Looper that will be set up for
+ * the given test.
+ * <p>
+ * This method is required to support any TestRule which needs to run setup and/or teardown code
+ * on the TestableLooper. Whether using {@link AndroidTestingRunner} or
+ * {@link TestWithLooperRule}, the TestRule's Statement evaluates on the test instrumentation
+ * thread, rather than the TestableLooper thread, so access to the TestableLooper is required.
+ * However, {@link #get(Object)} will return {@code null} both before and after the inner
+ * statement is evaluated:
+ * <ul>
+ * <li>Before the test {@link #get} returns {@code null} because while the TestableLooperHolder
+ * is accessible in sLoopers, it has not been initialized with an actual TestableLooper yet.
+ * This method's use of the internal LooperFrameworkMethod ensures that all setup and teardown
+ * of the TestableLooper happen as it would for all other wrapped code blocks.
+ * <li>After the test {@link #get} can return {@code null} because many tests call
+ * {@link #remove} in the teardown method. The fact that this method returns a runnable allows
+ * it to be called before the test (when the TestableLooperHolder is still in sLoopers), and
+ * then executed as teardown after the test.
+ * </ul>
+ *
+ * @param test the test instance (just like passed to {@link #get(Object)})
+ * @param runnable the operation that should eventually be run on the TestableLooper
+ * @return a runnable that will block the thread on which it is called until the given runnable
+ * is finished. Will be {@code null} if there is no looper for the given test.
+ * @hide
+ */
+ @Nullable
+ public static RunnableWithException wrapWithRunBlocking(
+ Object test, @NonNull RunnableWithException runnable) {
+ TestableLooperHolder looperHolder = sLoopers.get(test);
+ if (looperHolder == null) {
+ return null;
+ }
+ try {
+ FrameworkMethod base = new FrameworkMethod(runnable.getClass().getMethod("run"));
+ LooperFrameworkMethod wrapped = new LooperFrameworkMethod(base, looperHolder);
+ return () -> {
+ try {
+ wrapped.invokeExplosively(runnable);
+ } catch (RuntimeException | Error e) {
+ throw e;
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ };
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
public Looper getLooper() {
return mLooper;
}
diff --git a/tools/hoststubgen/hoststubgen/Android.bp b/tools/hoststubgen/hoststubgen/Android.bp
index 57bcc04..c6dd29c 100644
--- a/tools/hoststubgen/hoststubgen/Android.bp
+++ b/tools/hoststubgen/hoststubgen/Android.bp
@@ -180,7 +180,6 @@
"framework-minus-apex.ravenwood",
],
static_libs: [
- "core-xml-for-device",
"hoststubgen-helper-libcore-runtime.ravenwood",
],
}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
index 06eeb47c..1089f82 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
@@ -382,7 +382,7 @@
stubOutStream.putNextEntry(newEntry)
convertClass(classInternalName, /*forImpl=*/false, bis,
stubOutStream, filter, packageRedirector, enableChecker, classes,
- errors, stats)
+ errors, null)
stubOutStream.closeEntry()
}
}
@@ -415,7 +415,7 @@
enableChecker: Boolean,
classes: ClassNodes,
errors: HostStubGenErrors,
- stats: HostStubGenStats,
+ stats: HostStubGenStats?,
) {
val cr = ClassReader(input)
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt
index fe4072f..50518e1 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt
@@ -17,6 +17,7 @@
import com.android.hoststubgen.asm.toHumanReadableClassName
import com.android.hoststubgen.filters.FilterPolicyWithReason
+import org.objectweb.asm.Opcodes
import java.io.PrintWriter
open class HostStubGenStats {
@@ -28,12 +29,26 @@
private val stats = mutableMapOf<String, Stats>()
- fun onVisitPolicyForMethod(fullClassName: String, policy: FilterPolicyWithReason) {
+ fun onVisitPolicyForMethod(fullClassName: String, methodName: String, descriptor: String,
+ policy: FilterPolicyWithReason, access: Int) {
+ // Ignore methods that aren't public
+ if ((access and Opcodes.ACC_PUBLIC) == 0) return
+ // Ignore methods that are abstract
+ if ((access and Opcodes.ACC_ABSTRACT) != 0) return
+ // Ignore methods where policy isn't relevant
if (policy.isIgnoredForStats) return
val packageName = resolvePackageName(fullClassName)
val className = resolveClassName(fullClassName)
+ // Ignore methods for certain generated code
+ if (className.endsWith("Proto")
+ or className.endsWith("ProtoEnums")
+ or className.endsWith("LogTags")
+ or className.endsWith("StatsLog")) {
+ return
+ }
+
val packageStats = stats.getOrPut(packageName) { Stats() }
val classStats = packageStats.children.getOrPut(className) { Stats() }
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
index 53eb5a8..eb03f66 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
@@ -72,6 +72,6 @@
|| reason.contains("is-enum")
|| reason.contains("is-synthetic-method")
|| reason.contains("special-class")
- || reason.contains("substitute-from")
+ || reason.contains("substitute-to")
}
}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
index c20aa8b..45e140c 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
@@ -51,7 +51,7 @@
*/
data class Options (
val errors: HostStubGenErrors,
- val stats: HostStubGenStats,
+ val stats: HostStubGenStats?,
val enablePreTrace: Boolean,
val enablePostTrace: Boolean,
val enableNonStubMethodCallDetection: Boolean,
@@ -178,6 +178,7 @@
}
val p = filter.getPolicyForMethod(currentClassName, name, descriptor)
log.d("visitMethod: %s%s [%x] [%s] Policy: %s", name, descriptor, access, signature, p)
+ options.stats?.onVisitPolicyForMethod(currentClassName, name, descriptor, p, access)
log.withIndent {
// If it's a substitute-from method, then skip (== remove).
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
index beca945..416b782 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
@@ -141,11 +141,6 @@
substituted: Boolean,
superVisitor: MethodVisitor?,
): MethodVisitor? {
- // Record statistics about visiting this method when visible.
- if ((access and Opcodes.ACC_PRIVATE) == 0) {
- options.stats.onVisitPolicyForMethod(currentClassName, policy)
- }
-
// Inject method log, if needed.
var innerVisitor = superVisitor
diff --git a/tools/protologtool/OWNERS b/tools/protologtool/OWNERS
new file mode 100644
index 0000000..18cf2be
--- /dev/null
+++ b/tools/protologtool/OWNERS
@@ -0,0 +1,3 @@
+# ProtoLog owners
+# Bug component: 1157642
+include platform/development:/tools/winscope/OWNERS